Files
site-builder/craft/src/panels/topbar/TopBarCommandMenu.test.tsx
T
shadowdaoandClaude Sonnet 5.5 73aa35ac96 feat(site-builder): command registry, shortcut matcher, tree walkers and top-bar command menu
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>
2026-10-07 11:14:52 -07:00

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');
});
});