cae900afd2
Closes the standing "no automated frontend tests" gap for the pure, testable logic: - format.test.ts (12): the booth display formatters — formatMoney (minor units → currency, malformed-code fallback), formatDuration (m / h+m / 0m / em-dash on negative-invalid), formatTime, and formatRelativeDateTime (today/yesterday words + catalog month names, no Intl dependence). - use-scanner.test.ts (5): the 2026-06-21 focus-independent hardware scan — a fast burst+Enter on <body> fires onScan; slow human typing (gap > 50ms) does not; paused (modal open) no-ops; keystrokes into an editable field are ignored; a lone Enter / too-short burst is ignored. Wires Vitest (jsdom + @testing-library/react) into @parking/web. web 17/17. Full workspace green: shared 87, devices 18, server 75, web 17 (= 197) + build/lint 14/14. (apps/vision still has its 2 pre-existing failures — next.) Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { renderHook } from "@testing-library/react";
|
|
import { useScanner } from "./use-scanner.js";
|
|
|
|
// The global hardware-scanner hook: a fast keystroke burst ended by Enter fires onScan,
|
|
// regardless of focus, WITHOUT hijacking human typing or editable fields, and pauses
|
|
// while a modal is open. This pins the 2026-06-21 focus-independent scan behaviour
|
|
// (otherwise only verifiable in Playwright).
|
|
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
/** Dispatch a keydown on the document with a controllable timeStamp (the hook measures
|
|
* inter-key gaps off e.timeStamp). jsdom sets timeStamp to 0, so we override it. */
|
|
function key(char: string, timeStamp: number, target: EventTarget = document.body) {
|
|
const e = new KeyboardEvent("keydown", { key: char, bubbles: true, cancelable: true });
|
|
Object.defineProperty(e, "timeStamp", { value: timeStamp });
|
|
Object.defineProperty(e, "target", { value: target });
|
|
document.dispatchEvent(e);
|
|
}
|
|
|
|
/** Type a code as a fast burst (5ms apart) ending in Enter, from a start time. */
|
|
function scan(code: string, start = 1000, gap = 5) {
|
|
let t = start;
|
|
for (const ch of code) { key(ch, t); t += gap; }
|
|
key("Enter", t);
|
|
return t;
|
|
}
|
|
|
|
describe("useScanner", () => {
|
|
it("fires onScan with the code on a fast burst + Enter (focus on body)", () => {
|
|
const onScan = vi.fn();
|
|
renderHook(() => useScanner({ onScan }));
|
|
scan("12345678901");
|
|
expect(onScan).toHaveBeenCalledTimes(1);
|
|
expect(onScan).toHaveBeenCalledWith("12345678901");
|
|
});
|
|
|
|
it("ignores slow, human-paced typing (gap > 50ms resets the buffer)", () => {
|
|
const onScan = vi.fn();
|
|
renderHook(() => useScanner({ onScan }));
|
|
// 120ms between keys — a person, not a scanner. Each gap resets the buffer, so by
|
|
// Enter only the last char remains (< MIN_LENGTH) → no scan.
|
|
scan("123", 1000, 120);
|
|
expect(onScan).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not fire while paused (a modal is open)", () => {
|
|
const onScan = vi.fn();
|
|
renderHook(() => useScanner({ onScan, paused: true }));
|
|
scan("12345678901");
|
|
expect(onScan).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("ignores keystrokes into an editable field (manual typing unaffected)", () => {
|
|
const onScan = vi.fn();
|
|
renderHook(() => useScanner({ onScan }));
|
|
const input = document.createElement("input");
|
|
document.body.appendChild(input);
|
|
scanInto("12345678901", input);
|
|
expect(onScan).not.toHaveBeenCalled();
|
|
input.remove();
|
|
});
|
|
|
|
it("ignores a lone Enter / too-short burst", () => {
|
|
const onScan = vi.fn();
|
|
renderHook(() => useScanner({ onScan }));
|
|
key("Enter", 1000);
|
|
expect(onScan).not.toHaveBeenCalled();
|
|
scan("ab"); // length 2 < MIN_LENGTH 3
|
|
expect(onScan).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
/** Burst with the event target set to an editable element. */
|
|
function scanInto(code: string, target: EventTarget, start = 1000, gap = 5) {
|
|
let t = start;
|
|
for (const ch of code) { key(ch, t, target); t += gap; }
|
|
key("Enter", t, target);
|
|
}
|