Library only; nothing is mounted. Adds minimal DeviceContext/NoticeContext stubs (owned by W0-breakpoints / W0-registry-notices) that expose the agreed interfaces. Refs #39 #49 #34 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { describe, test, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
import React from 'react';
|
|
import { renderEditorHarness, EditorHarness } from '../../test-utils/editorHarness';
|
|
import { registerCommands, __resetCommandsForTests } from '../../editor/commands';
|
|
import { TopBarCommandMenu } from './TopBarCommandMenu';
|
|
|
|
let h: EditorHarness;
|
|
const run = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
__resetCommandsForTests();
|
|
run.mockReset();
|
|
registerCommands([
|
|
{ id: 'site.download', title: 'Download site', group: 'site', shortcut: 'mod+shift+e', run },
|
|
{ id: 'site.disabled', title: 'Disabled thing', group: 'site', when: () => false, run },
|
|
{ id: 'site.hidden', title: 'Hidden thing', group: 'site', hidden: true, run },
|
|
{ id: 'edit.duplicate', title: 'Duplicate', group: 'edit', run },
|
|
]);
|
|
h = renderEditorHarness();
|
|
h.mountChild(<TopBarCommandMenu />);
|
|
});
|
|
afterEach(() => {
|
|
h.unmount();
|
|
__resetCommandsForTests();
|
|
});
|
|
|
|
const button = () => h.container.querySelector('.topbar-command-btn') as HTMLButtonElement;
|
|
const items = () => Array.from(h.container.querySelectorAll('.topbar-command-item')) as HTMLButtonElement[];
|
|
|
|
describe('TopBarCommandMenu', () => {
|
|
test('lists only visible site commands, with shortcut labels and disabled state', () => {
|
|
expect(items()).toHaveLength(0);
|
|
h.act(() => button().click());
|
|
const titles = items().map((i) => i.querySelector('.topbar-command-title')!.textContent);
|
|
expect(titles).toEqual(['Download site', 'Disabled thing']);
|
|
expect(items()[0].querySelector('.topbar-command-shortcut')!.textContent).toMatch(/Shift\+E$|⇧E$/);
|
|
expect(items()[0].disabled).toBe(false);
|
|
expect(items()[1].disabled).toBe(true);
|
|
});
|
|
|
|
test('click runs the command and closes the menu', async () => {
|
|
h.act(() => button().click());
|
|
await h.act(async () => {
|
|
items()[0].click();
|
|
});
|
|
expect(run).toHaveBeenCalledTimes(1);
|
|
expect(items()).toHaveLength(0);
|
|
});
|
|
|
|
test('disabled item does not run', async () => {
|
|
h.act(() => button().click());
|
|
await h.act(async () => {
|
|
items()[1].click();
|
|
});
|
|
expect(run).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('Escape closes; outside mousedown closes', () => {
|
|
h.act(() => button().click());
|
|
expect(items().length).toBeGreaterThan(0);
|
|
h.act(() => {
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
});
|
|
expect(items()).toHaveLength(0);
|
|
h.act(() => button().click());
|
|
h.act(() => {
|
|
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
});
|
|
expect(items()).toHaveLength(0);
|
|
});
|
|
|
|
test('late registration shows up without reopening', () => {
|
|
h.act(() => button().click());
|
|
h.act(() => {
|
|
registerCommands([{ id: 'site.late', title: 'Late one', group: 'site', run }]);
|
|
});
|
|
expect(items().map((i) => i.textContent)).toContain('Late one');
|
|
});
|
|
});
|