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