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>
84 lines
2.8 KiB
TypeScript
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);
|
|
});
|
|
});
|