import React from 'react'; import { describe, it, expect, vi } from 'vitest'; import { render, fireEvent, act } from '@testing-library/react'; import { PixelMenubar } from '../../navigation/PixelMenubar'; import type { PixelMenubarMenu } from '../../navigation/PixelMenubar'; function makeMenus( onNew = vi.fn(), onOpen = vi.fn(), onCopy = vi.fn(), onPasteText = vi.fn(), onPasteImage = vi.fn(), ): PixelMenubarMenu[] { return [ { label: 'File', items: [ { value: 'new', label: 'New File', shortcut: 'mod+n', onSelect: onNew }, { value: 'open', label: 'Open…', shortcut: 'mod+o', onSelect: onOpen }, { value: 'sep1', label: '', separator: true }, { value: 'quit', label: 'Quit', disabled: true }, ], }, { label: 'Edit', items: [ { value: 'copy', label: 'Copy', shortcut: 'mod+c', onSelect: onCopy }, { value: 'paste', label: 'Paste', submenu: [ { value: 'paste-text', label: 'Paste as Text', onSelect: onPasteText }, { value: 'paste-image', label: 'Paste as Image', onSelect: onPasteImage }, ], }, ], }, { label: 'View', items: [{ value: 'zoom', label: 'Zoom In' }], }, ]; } describe('PixelMenubar', () => { it('renders menus as triggers', () => { const { getByRole, getAllByRole } = render( , ); const bar = getByRole('menubar'); expect(bar).toBeTruthy(); const triggers = getAllByRole('menuitem'); // Top-level triggers: File, Edit, View const labels = triggers.map((t) => t.textContent?.trim()); expect(labels).toContain('File'); expect(labels).toContain('Edit'); expect(labels).toContain('View'); }); it('clicking trigger opens content with items', () => { const { getByText, queryByText, getAllByRole } = render( , ); // Before clicking, items shouldn't be in the DOM expect(queryByText('New File')).toBeNull(); fireEvent.click(getByText('File')); expect(getByText('New File')).toBeTruthy(); expect(getByText('Open…')).toBeTruthy(); expect(getByText('Quit')).toBeTruthy(); // menu role should now exist const menus = getAllByRole('menu'); expect(menus.length).toBeGreaterThan(0); }); it('item with onSelect fires on click', () => { const onNew = vi.fn(); const { getByText } = render( , ); fireEvent.click(getByText('File')); fireEvent.click(getByText('New File')); expect(onNew).toHaveBeenCalled(); }); it('submenu opens on hover and right arrow', () => { const { getByText, queryByText } = render( , ); fireEvent.click(getByText('Edit')); // Initially, submenu items not shown expect(queryByText('Paste as Text')).toBeNull(); // Hover the Paste item to open submenu const pasteTrigger = getByText('Paste'); fireEvent.mouseEnter(pasteTrigger); expect(getByText('Paste as Text')).toBeTruthy(); expect(getByText('Paste as Image')).toBeTruthy(); }); it('right arrow opens submenu when on a submenu parent', () => { const { getByText, queryByText, getByRole } = render( , ); const menubar = getByRole('menubar'); fireEvent.click(getByText('Edit')); // First focusable is Copy (0); ArrowDown → Paste (1) fireEvent.keyDown(menubar, { key: 'ArrowDown' }); // Right arrow opens submenu fireEvent.keyDown(menubar, { key: 'ArrowRight' }); expect(getByText('Paste as Text')).toBeTruthy(); expect(queryByText('Paste as Image')).toBeTruthy(); }); it('left/right keyboard switches between menus', () => { const { getByText, queryByText, getByRole } = render( , ); const menubar = getByRole('menubar'); // Open File first fireEvent.click(getByText('File')); expect(getByText('New File')).toBeTruthy(); // Right arrow → Edit menu fireEvent.keyDown(menubar, { key: 'ArrowRight' }); expect(queryByText('New File')).toBeNull(); expect(getByText('Copy')).toBeTruthy(); // Right arrow → View menu fireEvent.keyDown(menubar, { key: 'ArrowRight' }); expect(queryByText('Copy')).toBeNull(); expect(getByText('Zoom In')).toBeTruthy(); // Left arrow → back to Edit fireEvent.keyDown(menubar, { key: 'ArrowLeft' }); expect(queryByText('Zoom In')).toBeNull(); expect(getByText('Copy')).toBeTruthy(); }); it('Escape closes the open menu', () => { const { getByText, queryByText, getByRole } = render( , ); const menubar = getByRole('menubar'); fireEvent.click(getByText('File')); expect(getByText('New File')).toBeTruthy(); act(() => { fireEvent.keyDown(menubar, { key: 'Escape' }); }); expect(queryByText('New File')).toBeNull(); }); it('renders separator items as role=separator', () => { const { getByText, getAllByRole } = render( , ); fireEvent.click(getByText('File')); const seps = getAllByRole('separator'); expect(seps.length).toBeGreaterThan(0); }); it('disabled items have aria-disabled and do not fire onSelect', () => { const onQuit = vi.fn(); const menus: PixelMenubarMenu[] = [ { label: 'File', items: [{ value: 'quit', label: 'Quit', disabled: true, onSelect: onQuit }], }, ]; const { getByText } = render(); fireEvent.click(getByText('File')); const quit = getByText('Quit'); expect(quit.closest('[aria-disabled="true"]')).toBeTruthy(); fireEvent.click(quit); expect(onQuit).not.toHaveBeenCalled(); }); });