Files
TREK/client/src/components/shared/ContextMenu.test.tsx
T
2026-04-07 12:31:09 +02:00

83 lines
3.1 KiB
TypeScript

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(<ContextMenu menu={null} onClose={onClose} />);
expect(document.body.querySelector('[style*="z-index: 999999"]')).toBeNull();
});
it('FE-COMP-CTX-002: renders menu items at the specified position', () => {
render(<ContextMenu menu={makeMenu(150, 250)} onClose={onClose} />);
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(<ContextMenu menu={menu} onClose={onClose} />);
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(<ContextMenu menu={menu} onClose={onClose} />);
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(<ContextMenu menu={menu} onClose={onClose} />);
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(<ContextMenu menu={makeMenu()} onClose={onClose} />);
// Document click event triggers the close handler
act(() => {
document.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(onClose).toHaveBeenCalledOnce();
});
});