glytch/fktry/src/ui/hotkeys.dom.test.ts
type-two a6ecf88623 [ui] DOM tests under happy-dom: chips, window binding, fax stamp
The three that genuinely needed a real DOM and were blocked last round.
happy-dom is actually installed now.

Chips: node reuse across updates, state classes clearing, spec ordering, and
a dangling item id rendering drab instead of taking the panel down.
Hotkeys: attach/detach really binding and unbinding the window, space
preventing page scroll, and no hotkey firing while typing in a field.
Fax: the stamp lands and clears on its own, and two commissions completing
back to back don't let the first timer clear the second early.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:34:22 +10:00

84 lines
2.8 KiB
TypeScript

// @vitest-environment happy-dom
import { describe, expect, it, vi } from 'vitest';
import { createHotkeys, isTypingTarget } from './hotkeys';
/** The half of hotkeys.ts that needs a real window: attach/detach and typing guards. */
describe('createHotkeys attach/detach', () => {
it('routes real window keydown events once attached', () => {
const keys = createHotkeys();
const hit = vi.fn();
keys.bind('r', hit);
keys.attach();
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'r' }));
expect(hit).toHaveBeenCalledTimes(1);
keys.detach();
});
it('stops routing after detach — no listener left on the window', () => {
const keys = createHotkeys();
const hit = vi.fn();
keys.bind('r', hit);
keys.attach();
keys.detach();
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'r' }));
expect(hit).not.toHaveBeenCalled();
});
it('ignores keys bound before attach until attach is called', () => {
const keys = createHotkeys();
const hit = vi.fn();
keys.bind('x', hit);
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' }));
expect(hit).not.toHaveBeenCalled();
keys.attach();
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'x' }));
expect(hit).toHaveBeenCalledTimes(1);
keys.detach();
});
it('calls preventDefault on space so the page never scrolls under the factory', () => {
const keys = createHotkeys();
keys.bind(' ', () => {});
keys.attach();
const ev = new KeyboardEvent('keydown', { key: ' ', cancelable: true });
window.dispatchEvent(ev);
expect(ev.defaultPrevented).toBe(true);
keys.detach();
});
it('does not fire a hotkey while the player is typing in a field', () => {
const keys = createHotkeys();
const hit = vi.fn();
keys.bind('r', hit);
keys.attach();
const input = document.createElement('input');
document.body.append(input);
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'r', bubbles: true }));
expect(hit).not.toHaveBeenCalled();
input.remove();
keys.detach();
});
});
describe('isTypingTarget', () => {
it('is true for form fields and contenteditable, false for the world', () => {
for (const tag of ['input', 'textarea', 'select']) {
expect(isTypingTarget(document.createElement(tag))).toBe(true);
}
const div = document.createElement('div');
expect(isTypingTarget(div)).toBe(false);
// The inspector's recipe <select> is exactly why this guard exists.
const editable = document.createElement('div');
editable.setAttribute('contenteditable', 'true');
expect(isTypingTarget(editable)).toBe(true);
});
it('is false for null and non-elements', () => {
expect(isTypingTarget(null)).toBe(false);
expect(isTypingTarget('r')).toBe(false);
});
});