" for { ns: 'common' }.
+ const contextTitle = await screen.findByText('common.promptEditor.context.item.title')
+ expect(contextTitle).toBeInTheDocument()
+
+ // Hover over another menu item to trigger `onSetHighlight` -> `setHighlightedIndex(index)`.
+ const queryTitle = await screen.findByText('common.promptEditor.query.item.title')
+ const queryItem = queryTitle.closest('[tabindex="-1"]')
+ expect(queryItem).not.toBeNull()
+ await user.hover(queryItem as HTMLElement)
+
+ // Flush the 0ms timer in renderMenu that calls refs.setReference(anchor).
+ await flushNextTick()
+
+ fireEvent.click(contextTitle)
+
+ // Selecting an option should dispatch a command (from the real option implementation).
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
+
+ // The trigger character should be removed from editor content via `nodeToRemove.remove()`.
+ await waitFor(() => {
+ expect(readEditorText(editor)).not.toContain('{')
+ })
+ })
+
+ it('does not remove the trigger when selecting an option with an empty key (nodeToRemove && key falsy)', async () => {
+ const captures: Captures = { editor: null, eventEmitter: null }
+ render((
+
+ ))
+ const editor = await waitForEditor(captures)
+
+ await setEditorText(editor, '{', true)
+
+ // There is no accessible "option" role here (menu items are plain divs).
+ // We locate menu items by `tabindex="-1"` inside the listbox.
+ const listbox = await screen.findByRole('listbox', { name: /typeahead menu/i })
+ const menuItems = Array.from(listbox.querySelectorAll('[tabindex="-1"]'))
+
+ // Expect at least: (1) our empty variable option, (2) the "add variable" option.
+ expect(menuItems.length).toBeGreaterThanOrEqual(2)
+ expect(within(listbox).getByText('common.promptEditor.variable.modal.add')).toBeInTheDocument()
+
+ fireEvent.click(menuItems[0] as HTMLElement)
+
+ // Since the key is falsy, ComponentPicker should NOT call nodeToRemove.remove().
+ // The trigger remains in editor content.
+ await waitFor(() => {
+ expect(readEditorText(editor)).toContain('{')
+ })
+ })
+
+ it('subscribes to EventEmitter and dispatches INSERT_VARIABLE_VALUE_BLOCK_COMMAND only for matching messages', async () => {
+ const captures: Captures = { editor: null, eventEmitter: null }
+ render((
+
+ ))
+
+ const editor = await waitForEditor(captures)
+ const eventEmitter = await waitForEventEmitter(captures)
+ const dispatchSpy = vi.spyOn(editor, 'dispatchCommand')
+
+ // Non-object emissions (string) should be ignored by the subscription callback.
+ eventEmitter.emit('some-string')
+ expect(dispatchSpy).not.toHaveBeenCalledWith(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, expect.any(String))
+
+ // Mismatched type should be ignored.
+ eventEmitter.emit({ type: 'OTHER', payload: 'x' })
+ expect(dispatchSpy).not.toHaveBeenCalledWith(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, '{{x}}')
+
+ // Matching type should dispatch with {{payload}} wrapping.
+ eventEmitter.emit({ type: INSERT_VARIABLE_VALUE_BLOCK_COMMAND as unknown as string, payload: 'foo' })
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, '{{foo}}')
+ })
+
+ it('handles workflow variable selection: flat vars (current/error_message/last_run) and closes on Escape from search input', async () => {
+ const captures: Captures = { editor: null, eventEmitter: null }
+
+ const workflowVariableBlock = makeWorkflowVariableBlock({}, [
+ { nodeId: 'custom-flat', title: 'custom-flat', isFlat: true, vars: [makeWorkflowNodeVar('custom_flat', VarType.string)] },
+ makeWorkflowVarNode('node-output', 'Node Output', [
+ makeWorkflowNodeVar('output', VarType.string),
+ ]),
+ ])
+
+ render((
+
+ ))
+
+ const editor = await waitForEditor(captures)
+ const dispatchSpy = vi.spyOn(editor, 'dispatchCommand')
+
+ // Open menu and select current (flat).
+ await setEditorText(editor, '{', true)
+ await flushNextTick()
+ const currentLabel = await screen.findByText('current_prompt')
+ await act(async () => {
+ fireEvent.click(currentLabel)
+ })
+ await flushNextTick()
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_CURRENT_BLOCK_COMMAND, GeneratorType.prompt)
+
+ // Re-open menu and select error_message (flat).
+ await setEditorText(editor, '{', true)
+ await flushNextTick()
+ const errorMessageLabel = await screen.findByText('error_message')
+ await act(async () => {
+ fireEvent.click(errorMessageLabel)
+ })
+ await flushNextTick()
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_ERROR_MESSAGE_BLOCK_COMMAND, null)
+
+ // Re-open menu and select last_run (flat).
+ await setEditorText(editor, '{', true)
+ await flushNextTick()
+ const lastRunLabel = await screen.findByText('last_run')
+ await act(async () => {
+ fireEvent.click(lastRunLabel)
+ })
+ await flushNextTick()
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_LAST_RUN_BLOCK_COMMAND, null)
+
+ // Re-open menu and press Escape in the VarReferenceVars search input to exercise handleClose().
+ await setEditorText(editor, '{', true)
+ await flushNextTick()
+ const searchInput = await screen.findByPlaceholderText('workflow.common.searchVar')
+ await act(async () => {
+ fireEvent.keyDown(searchInput, { key: 'Escape' })
+ })
+ await flushNextTick()
+ expect(dispatchSpy).toHaveBeenCalledWith(KEY_ESCAPE_COMMAND, expect.any(KeyboardEvent))
+
+ // Re-open menu and select a flat var that is not handled by the special-case list.
+ // This covers the "no-op" path in the `isFlat` branch.
+ dispatchSpy.mockClear()
+ await setEditorText(editor, '{', true)
+ await flushNextTick()
+ const customFlatLabel = await screen.findByText('custom_flat')
+ await act(async () => {
+ fireEvent.click(customFlatLabel)
+ })
+ await flushNextTick()
+ expect(dispatchSpy).not.toHaveBeenCalled()
+ })
+
+ it('handles workflow variable selection for nested fields: sys.query, sys.files, and normal paths', async () => {
+ const captures: Captures = { editor: null, eventEmitter: null }
+ const user = userEvent.setup()
+
+ const workflowVariableBlock = makeWorkflowVariableBlock({}, [
+ makeWorkflowVarNode('node-1', 'Node 1', [
+ makeWorkflowNodeVar('sys.query', VarType.object, [makeWorkflowNodeVar('q', VarType.string)]),
+ makeWorkflowNodeVar('sys.files', VarType.object, [makeWorkflowNodeVar('f', VarType.string)]),
+ makeWorkflowNodeVar('output', VarType.object, [makeWorkflowNodeVar('x', VarType.string)]),
+ ]),
+ ])
+
+ render((
+
+ ))
+
+ const editor = await waitForEditor(captures)
+ const dispatchSpy = vi.spyOn(editor, 'dispatchCommand')
+
+ const openPickerAndSelectField = async (variableTitle: string, fieldName: string) => {
+ await setEditorText(editor, '{', true)
+ await screen.findByPlaceholderText('workflow.common.searchVar')
+ await act(async () => { /* flush effects */ })
+
+ const label = document.querySelector(`[title="${variableTitle}"]`)
+ expect(label).not.toBeNull()
+ const row = (label as HTMLElement).parentElement?.parentElement
+ expect(row).not.toBeNull()
+
+ // `ahooks/useHover` listens for native `mouseenter` / `mouseleave`. `user.hover` triggers
+ // a realistic event sequence that reliably hits those listeners in JSDOM.
+ await user.hover(row as HTMLElement)
+ const field = await screen.findByText(fieldName)
+ fireEvent.mouseDown(field)
+ await user.unhover(row as HTMLElement)
+ }
+
+ await openPickerAndSelectField('sys.query', 'q')
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, ['sys.query'])
+ await waitFor(() => expect(readEditorText(editor)).not.toContain('{'))
+
+ await openPickerAndSelectField('sys.files', 'f')
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, ['sys.files'])
+ await waitFor(() => expect(readEditorText(editor)).not.toContain('{'))
+
+ await openPickerAndSelectField('output', 'x')
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_WORKFLOW_VARIABLE_BLOCK_COMMAND, ['node-1', 'output', 'x'])
+ await waitFor(() => expect(readEditorText(editor)).not.toContain('{'))
+ })
+
+ it('skips removing the trigger when selection is null (needRemove is null) and still dispatches', async () => {
+ const captures: Captures = { editor: null, eventEmitter: null }
+
+ const workflowVariableBlock = makeWorkflowVariableBlock({}, [
+ { nodeId: 'current', title: 'current_prompt', isFlat: true, vars: [makeWorkflowNodeVar('current', VarType.string)] },
+ ])
+
+ render((
+
+ ))
+
+ const editor = await waitForEditor(captures)
+ const dispatchSpy = vi.spyOn(editor, 'dispatchCommand')
+
+ await setEditorText(editor, '{', true)
+ const currentLabel = await screen.findByText('current_prompt')
+
+ // Force selection to null and click within the same act() to avoid the typeahead UI unmounting
+ // before the click handler fires.
+ await act(async () => {
+ editor.update(() => {
+ $setSelection(null)
+ })
+ currentLabel.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ })
+
+ expect(dispatchSpy).toHaveBeenCalledWith(INSERT_CURRENT_BLOCK_COMMAND, undefined)
+ await waitFor(() => expect(readEditorText(editor)).toContain('{'))
+ })
+
+ it('covers the anchor-ref guard when anchorElementRef.current becomes null before the scheduled callback runs', async () => {
+ // `@lexical/react` keeps `anchorElementRef.current` as a stable element reference, which means the
+ // "anchor is null" path is hard to reach through normal interactions in JSDOM.
+ //
+ // To reach 100% branch coverage for `index.tsx`, we:
+ // 1) Pause timers so the scheduled callback doesn't run immediately.
+ // 2) Find the `useRef` hook object used by LexicalTypeaheadMenuPlugin that points at `#typeahead-menu`.
+ // 3) Set that ref's `.current = null` before advancing timers.
+ //
+ // This avoids mocking third-party modules while still exercising the guard.
+ vi.useFakeTimers()
+
+ const captures: Captures = { editor: null, eventEmitter: null }
+ render((
+
+ ))
+
+ await act(async () => { /* flush effects */ })
+ expect(captures.editor).not.toBeNull()
+ const editor = captures.editor as LexicalEditor
+
+ await setEditorText(editor, '{', true)
+ const typeaheadMenu = document.getElementById('typeahead-menu')
+ expect(typeaheadMenu).not.toBeNull()
+
+ const ce = screen.getByTestId(CONTENT_EDITABLE_TEST_ID)
+ const fiber = getReactFiberFromDom(ce)
+ expect(fiber).not.toBeNull()
+ const root = (() => {
+ let cur = fiber as ReactFiber
+ while (cur.return)
+ cur = cur.return
+ return cur
+ })()
+
+ const anchorRef = findHookRefPointingToElement(root, typeaheadMenu as Element)
+ expect(anchorRef).not.toBeNull()
+ anchorRef!.current = null
+
+ await act(async () => {
+ vi.runOnlyPendingTimers()
+ })
+
+ vi.useRealTimers()
+ })
+
+ it('renders the workflow-variable divider when workflowVariableBlock is shown and options are non-empty', async () => {
+ const captures: Captures = { editor: null, eventEmitter: null }
+
+ const workflowVariableBlock = makeWorkflowVariableBlock({}, [
+ makeWorkflowVarNode('node-1', 'Node 1', [
+ makeWorkflowNodeVar('output', VarType.string),
+ ]),
+ ])
+
+ render((
+
+ ))
+
+ const editor = await waitForEditor(captures)
+ await setEditorText(editor, '{', true)
+
+ // Both sections are present.
+ expect(await screen.findByPlaceholderText('workflow.common.searchVar')).toBeInTheDocument()
+ expect(await screen.findByText('common.promptEditor.context.item.title')).toBeInTheDocument()
+
+ // With a single option group, the only divider should be the workflow-var/options separator.
+ expect(document.querySelectorAll('.bg-divider-subtle')).toHaveLength(1)
+ })
+})
diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/menu.spec.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/menu.spec.tsx
new file mode 100644
index 0000000000..2ee4fb7e0b
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/menu.spec.tsx
@@ -0,0 +1,123 @@
+import { render } from '@testing-library/react'
+import { Fragment } from 'react'
+import { PickerBlockMenuOption } from './menu'
+
+describe('PickerBlockMenuOption', () => {
+ // Define the render props type locally to match the component's internal type accurately
+ type MenuOptionRenderProps = {
+ isSelected: boolean
+ onSelect: () => void
+ onSetHighlight: () => void
+ queryString: string | null
+ }
+
+ const mockRender = vi.fn((props: MenuOptionRenderProps) => (
+
+ {props.isSelected ? 'Selected' : 'Not Selected'}
+ {props.queryString && ` Query: ${props.queryString}`}
+
+ ))
+ const mockOnSelect = vi.fn()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('Constructor and Initialization', () => {
+ it('should correctly initialize with provided key and group', () => {
+ const option = new PickerBlockMenuOption({
+ key: 'test-key',
+ group: 'test-group',
+ render: mockRender,
+ })
+
+ // Check inheritance from MenuOption (key)
+ expect(option.key).toBe('test-key')
+ // Check custom property (group)
+ expect(option.group).toBe('test-group')
+ })
+
+ it('should initialize without group when not provided', () => {
+ const option = new PickerBlockMenuOption({
+ key: 'test-key-no-group',
+ render: mockRender,
+ })
+
+ expect(option.key).toBe('test-key-no-group')
+ expect(option.group).toBeUndefined()
+ })
+ })
+
+ describe('onSelectMenuOption', () => {
+ it('should call the provided onSelect callback', () => {
+ const option = new PickerBlockMenuOption({
+ key: 'test-key',
+ onSelect: mockOnSelect,
+ render: mockRender,
+ })
+
+ option.onSelectMenuOption()
+ expect(mockOnSelect).toHaveBeenCalledTimes(1)
+ })
+
+ it('should handle cases where onSelect is not provided (optional chaining)', () => {
+ const option = new PickerBlockMenuOption({
+ key: 'test-key',
+ render: mockRender,
+ })
+
+ // This covers the branch where this.data.onSelect is undefined
+ expect(() => option.onSelectMenuOption()).not.toThrow()
+ })
+ })
+
+ describe('renderMenuOption', () => {
+ it('should call the render function with correct props and return the element', () => {
+ const option = new PickerBlockMenuOption({
+ key: 'test-key',
+ render: mockRender,
+ })
+
+ const renderProps: MenuOptionRenderProps = {
+ isSelected: true,
+ onSelect: vi.fn(),
+ onSetHighlight: vi.fn(),
+ queryString: 'search-string',
+ }
+
+ // Execute renderMenuOption
+ const renderedElement = option.renderMenuOption(renderProps)
+
+ // Use RTL to verify the rendered output
+ const { getByTestId, getByText } = render(renderedElement)
+
+ // Assertions
+ expect(mockRender).toHaveBeenCalledWith(renderProps)
+ expect(getByTestId('menu-item')).toBeInTheDocument()
+ expect(getByText('Selected Query: search-string')).toBeInTheDocument()
+ })
+
+ it('should use Fragment with the correct key as the wrapper', () => {
+ // In React testing, verifying the key of a Fragment directly from the element can be tricky,
+ // but we can verify the structure and that it renders correctly.
+ const option = new PickerBlockMenuOption({
+ key: 'fragment-key',
+ render: mockRender,
+ })
+
+ const renderProps: MenuOptionRenderProps = {
+ isSelected: false,
+ onSelect: vi.fn(),
+ onSetHighlight: vi.fn(),
+ queryString: null,
+ }
+
+ const element = option.renderMenuOption(renderProps)
+
+ // Verify the element type is Fragment (rendered output doesn't show Fragment in DOM)
+ // but we can check the JSX structure if needed.
+ expect(element.type).toBe(Fragment)
+ expect(element.key).toBe('fragment-key')
+ })
+ })
+})
diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.spec.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.spec.tsx
new file mode 100644
index 0000000000..c48c52a0b7
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/prompt-option.spec.tsx
@@ -0,0 +1,131 @@
+import { createEvent, fireEvent, render, screen } from '@testing-library/react'
+import { PromptMenuItem } from './prompt-option'
+
+describe('PromptMenuItem', () => {
+ const defaultProps = {
+ icon: icon,
+ title: 'Test Option',
+ isSelected: false,
+ onClick: vi.fn(),
+ onMouseEnter: vi.fn(),
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('Rendering', () => {
+ it('should render the icon and title correctly', () => {
+ render()
+
+ expect(screen.getByTestId('test-icon')).toBeInTheDocument()
+ expect(screen.getByText('Test Option')).toBeInTheDocument()
+ })
+
+ it('should have the correct display name', () => {
+ expect(PromptMenuItem.displayName).toBe('PromptMenuItem')
+ })
+ })
+
+ describe('Styling and States', () => {
+ it('should apply selected styles when isSelected is true and not disabled', () => {
+ const { container } = render()
+ const menuDiv = container.firstChild as HTMLElement
+
+ expect(menuDiv.className).toContain('!bg-state-base-hover')
+ expect(menuDiv.className).toContain('cursor-pointer')
+ expect(menuDiv.className).not.toContain('cursor-not-allowed')
+ })
+
+ it('should apply disabled styles and ignore isSelected when disabled is true', () => {
+ const { container } = render(
+ ,
+ )
+ const menuDiv = container.firstChild as HTMLElement
+
+ expect(menuDiv.className).toContain('cursor-not-allowed')
+ expect(menuDiv.className).toContain('opacity-30')
+ expect(menuDiv.className).not.toContain('!bg-state-base-hover')
+ })
+
+ it('should render with default styles when not selected and not disabled', () => {
+ const { container } = render()
+ const menuDiv = container.firstChild as HTMLElement
+
+ expect(menuDiv.className).toContain('cursor-pointer')
+ expect(menuDiv.className).not.toContain('!bg-state-base-hover')
+ expect(menuDiv.className).not.toContain('cursor-not-allowed')
+ })
+ })
+
+ describe('Interactions', () => {
+ describe('onClick', () => {
+ it('should call onClick when not disabled', () => {
+ render()
+
+ fireEvent.click(screen.getByText('Test Option'))
+
+ expect(defaultProps.onClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('should NOT call onClick when disabled', () => {
+ render()
+
+ fireEvent.click(screen.getByText('Test Option'))
+
+ expect(defaultProps.onClick).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('onMouseEnter', () => {
+ it('should call onMouseEnter when not disabled', () => {
+ render()
+
+ fireEvent.mouseEnter(screen.getByText('Test Option'))
+
+ expect(defaultProps.onMouseEnter).toHaveBeenCalledTimes(1)
+ })
+
+ it('should NOT call onMouseEnter when disabled', () => {
+ render()
+
+ fireEvent.mouseEnter(screen.getByText('Test Option'))
+
+ expect(defaultProps.onMouseEnter).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('onMouseDown', () => {
+ it('should prevent default and stop propagation', () => {
+ render()
+
+ const element = screen.getByText('Test Option').parentElement!
+
+ // Use createEvent to properly spy on preventDefault and stopPropagation
+ const mouseDownEvent = createEvent.mouseDown(element)
+ const preventDefault = vi.fn()
+ const stopPropagation = vi.fn()
+
+ mouseDownEvent.preventDefault = preventDefault
+ mouseDownEvent.stopPropagation = stopPropagation
+
+ fireEvent(element, mouseDownEvent)
+
+ expect(preventDefault).toHaveBeenCalled()
+ expect(stopPropagation).toHaveBeenCalled()
+ })
+ })
+ })
+
+ describe('Reference Management', () => {
+ it('should call setRefElement with the div element if provided', () => {
+ const setRefElement = vi.fn()
+ const { container } = render(
+ ,
+ )
+
+ const menuDiv = container.firstChild
+ expect(setRefElement).toHaveBeenCalledWith(menuDiv)
+ })
+ })
+})
diff --git a/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.spec.tsx b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.spec.tsx
new file mode 100644
index 0000000000..228f2ac657
--- /dev/null
+++ b/web/app/components/base/prompt-editor/plugins/component-picker-block/variable-option.spec.tsx
@@ -0,0 +1,124 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import { VariableMenuItem } from './variable-option'
+
+describe('VariableMenuItem', () => {
+ const defaultProps = {
+ title: 'Variable Name',
+ isSelected: false,
+ queryString: null,
+ onClick: vi.fn(),
+ onMouseEnter: vi.fn(),
+ }
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe('Rendering', () => {
+ it('should render the title correctly', () => {
+ render()
+ expect(screen.getByText('Variable Name')).toBeInTheDocument()
+ expect(screen.getByTitle('Variable Name')).toBeInTheDocument()
+ })
+
+ it('should render the icon when provided', () => {
+ render(
+ icon}
+ />,
+ )
+ expect(screen.getByTestId('test-icon')).toBeInTheDocument()
+ })
+
+ it('should render the extra element when provided', () => {
+ render(
+ extra}
+ />,
+ )
+ expect(screen.getByTestId('extra')).toBeInTheDocument()
+ })
+
+ it('should apply selection styles when isSelected is true', () => {
+ const { container } = render()
+ const item = container.firstChild as HTMLElement
+ expect(item).toHaveClass('bg-state-base-hover')
+ })
+ })
+
+ describe('Highlighting Logic (queryString)', () => {
+ it('should not highlight anything when queryString is null', () => {
+ render()
+ const titleContainer = screen.getByTitle('Variable Name')
+ // Ensure no highlighted span with text exists
+ expect(titleContainer.querySelector('.text-text-accent')?.textContent).toBe('')
+ })
+
+ it('should highlight matching text case-insensitively', () => {
+ render()
+ const highlighted = screen.getByText('User')
+ expect(highlighted).toHaveClass('text-text-accent')
+
+ const titleContainer = screen.getByTitle('User Name')
+ expect(titleContainer.textContent).toBe('User Name')
+ })
+
+ it('should handle partial match in the middle of the string', () => {
+ render()
+ const highlighted = screen.getByText('tem')
+ expect(highlighted).toHaveClass('text-text-accent')
+
+ const titleContainer = screen.getByTitle('System Variable')
+ expect(titleContainer.textContent).toBe('System Variable')
+ expect(titleContainer.innerHTML).toContain('Sys')
+ expect(titleContainer.innerHTML).toContain(' Variable')
+ })
+
+ it('should handle no match gracefully', () => {
+ render()
+ expect(screen.getByText('Variable')).toBeInTheDocument()
+ const titleContainer = screen.getByTitle('Variable')
+ expect(titleContainer.querySelector('.text-text-accent')?.textContent).toBe('')
+ })
+ })
+
+ describe('Events', () => {
+ it('should trigger onClick when clicked', () => {
+ render()
+ fireEvent.click(screen.getByTitle('Variable Name'))
+ expect(defaultProps.onClick).toHaveBeenCalledTimes(1)
+ })
+
+ it('should trigger onMouseEnter when mouse enters', () => {
+ render()
+ fireEvent.mouseEnter(screen.getByTitle('Variable Name'))
+ expect(defaultProps.onMouseEnter).toHaveBeenCalledTimes(1)
+ })
+
+ it('should prevent default and stop propagation onMouseDown', () => {
+ render()
+ const mousedownEvent = new MouseEvent('mousedown', {
+ bubbles: true,
+ cancelable: true,
+ })
+ const preventDefaultSpy = vi.spyOn(mousedownEvent, 'preventDefault')
+ const stopPropagationSpy = vi.spyOn(mousedownEvent, 'stopPropagation')
+
+ fireEvent(screen.getByTitle('Variable Name'), mousedownEvent)
+
+ expect(preventDefaultSpy).toHaveBeenCalled()
+ expect(stopPropagationSpy).toHaveBeenCalled()
+ })
+ })
+
+ describe('Ref handling', () => {
+ it('should call setRefElement with the element', () => {
+ const setRefElement = vi.fn()
+ render()
+
+ expect(setRefElement).toHaveBeenCalledWith(expect.any(HTMLDivElement))
+ })
+ })
+})