import { render, screen, fireEvent, act } from '../../../tests/helpers/render'; import userEvent from '@testing-library/user-event'; import { ContextMenu } from './ContextMenu'; import { Trash2, Edit } from 'lucide-react'; const makeMenu = (x = 100, y = 200, overrides?: object[]) => ({ x, y, items: overrides ?? [ { label: 'Edit', icon: Edit, onClick: vi.fn() }, { label: 'Delete', icon: Trash2, onClick: vi.fn(), danger: true }, ], }); describe('ContextMenu', () => { const onClose = vi.fn(); beforeEach(() => { onClose.mockClear(); }); it('FE-COMP-CTX-001: renders nothing when menu is null', () => { render(); expect(document.body.querySelector('[style*="z-index: 999999"]')).toBeNull(); }); it('FE-COMP-CTX-002: renders menu items at the specified position', () => { render(); expect(screen.getByText('Edit')).toBeTruthy(); expect(screen.getByText('Delete')).toBeTruthy(); // Portal root div has position fixed at the given coords const portal = document.body.querySelector('[style*="position: fixed"]') as HTMLElement; expect(portal.style.left).toBe('150px'); expect(portal.style.top).toBe('250px'); }); it('FE-COMP-CTX-003: clicking a menu item calls its onClick and onClose', async () => { const onClick = vi.fn(); const menu = makeMenu(100, 200, [{ label: 'Copy', onClick }]); const user = userEvent.setup(); render(); await user.click(screen.getByText('Copy')); expect(onClick).toHaveBeenCalledOnce(); // onClose is called once by the button handler and once by the document click listener expect(onClose).toHaveBeenCalled(); }); it('FE-COMP-CTX-004: divider items render as a separator without text', () => { const menu = makeMenu(100, 200, [ { label: 'Item A', onClick: vi.fn() }, { divider: true }, { label: 'Item B', onClick: vi.fn() }, ]); render(); expect(screen.getByText('Item A')).toBeTruthy(); expect(screen.getByText('Item B')).toBeTruthy(); // Divider should not have any button text const buttons = screen.getAllByRole('button'); expect(buttons).toHaveLength(2); }); it('FE-COMP-CTX-005: danger items have red color styling', () => { const menu = makeMenu(100, 200, [ { label: 'Remove', onClick: vi.fn(), danger: true }, ]); render(); const btn = screen.getByRole('button', { name: /remove/i }); // Danger buttons use color #ef4444 inline style expect(btn.style.color).toBe('rgb(239, 68, 68)'); }); it('FE-COMP-CTX-006: clicking outside the menu closes it via document click listener', () => { render(); // Document click event triggers the close handler act(() => { document.dispatchEvent(new MouseEvent('click', { bubbles: true })); }); expect(onClose).toHaveBeenCalledOnce(); }); });