import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import { makeQuitShortcutHandler, QUIT_DOUBLE_PRESS_MS, QUIT_HOLD_DURATION_MS, QUIT_HOLD_RELEASE_GRACE_MS, } from "./QuitHold.ts"; import type { QuitHoldKeyInput } from "./QuitHold.ts"; import type { QuitConfirmationMode, QuitShortcutHintEvent } from "@t3tools/contracts"; const HOLD_DOWN = { state: "down", mode: "hold" } as const; const DOUBLE_CLICK_DOWN = { state: "down", mode: "double-click" } as const; const UP = { state: "up" } as const; function makeInput(overrides: Partial): QuitHoldKeyInput { return { type: "keyDown", key: "q", meta: true, control: false, alt: false, shift: false, isAutoRepeat: false, ...overrides, }; } function makeHarness(options?: { mode?: QuitConfirmationMode; platform?: NodeJS.Platform; getMode?: () => Promise; }) { const notifications: Array = []; const concealWindow = vi.fn(); const quit = vi.fn(); const handler = makeQuitShortcutHandler({ platform: options?.platform ?? "darwin", getMode: options?.getMode ?? (() => Promise.resolve(options?.mode ?? "hold")), notify: (event) => notifications.push(event), concealWindow, quit, }); const preventDefault = vi.fn(); const send = async (input: QuitHoldKeyInput) => { handler({ preventDefault }, input); // Let the getMode promise settle. await Promise.resolve(); await Promise.resolve(); }; // Simulates the OS auto-repeating the held shortcut every `intervalMs`. const holdFor = async ( durationMs: number, repeatOverrides: Partial = {}, intervalMs = 100, ) => { for (let elapsed = 0; elapsed < durationMs; elapsed += intervalMs) { vi.advanceTimersByTime(intervalMs); await send(makeInput({ isAutoRepeat: true, ...repeatOverrides })); } }; return { notifications, concealWindow, quit, preventDefault, send, holdFor }; } describe("makeQuitShortcutHandler", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("shows the hint on a tap without quitting, even when the release is never seen", async () => { // macOS suppresses the letter's keyUp while Cmd is held, so a tap may // produce no keyUp at all. Quit must still not fire. const harness = makeHarness(); await harness.send(makeInput({})); expect(harness.preventDefault).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([HOLD_DOWN]); vi.advanceTimersByTime(QUIT_HOLD_DURATION_MS + QUIT_HOLD_RELEASE_GRACE_MS); expect(harness.quit).not.toHaveBeenCalled(); // The watchdog dismisses the hint once the press is clearly over. expect(harness.notifications).toEqual([HOLD_DOWN, UP]); }); it("conceals a completed hold, then quits after release", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); expect(harness.concealWindow).toHaveBeenCalledTimes(1); expect(harness.quit).not.toHaveBeenCalled(); await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); expect(harness.quit).not.toHaveBeenCalled(); vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([HOLD_DOWN, UP]); }); it("keeps a concealed hold committed when another key is pressed", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(QUIT_HOLD_DURATION_MS); await harness.send(makeInput({ key: "Shift", shift: true })); expect(harness.concealWindow).toHaveBeenCalledTimes(1); expect(harness.quit).not.toHaveBeenCalled(); vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS); expect(harness.quit).toHaveBeenCalledTimes(1); }); it("keeps a concealed hold committed through a fresh Cmd+Q press", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(QUIT_HOLD_DURATION_MS); await harness.send(makeInput({})); expect(harness.concealWindow).toHaveBeenCalledTimes(1); expect(harness.quit).not.toHaveBeenCalled(); await harness.send(makeInput({ type: "keyUp" })); expect(harness.quit).toHaveBeenCalledTimes(1); }); it("quits when a completed hold goes quiet without release events", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(QUIT_HOLD_DURATION_MS + QUIT_HOLD_RELEASE_GRACE_MS * 2); // If neither keyUp reaches the handler, continued repeats must keep the // app alive. Once they stop, the quiet period is the release signal. expect(harness.quit).not.toHaveBeenCalled(); vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([HOLD_DOWN, UP]); }); it("waits for slow repeats to stop before quitting", async () => { const harness = makeHarness(); await harness.send(makeInput({})); vi.advanceTimersByTime(300); await harness.send(makeInput({ isAutoRepeat: true })); vi.advanceTimersByTime(900); await harness.send(makeInput({ isAutoRepeat: true })); vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS); expect(harness.quit).not.toHaveBeenCalled(); vi.advanceTimersByTime(300); await harness.send(makeInput({ isAutoRepeat: true })); vi.advanceTimersByTime(1_799); expect(harness.quit).not.toHaveBeenCalled(); vi.advanceTimersByTime(1); expect(harness.quit).toHaveBeenCalledTimes(1); }); it("uses the initial repeat delay when the first repeat completes the hold", async () => { const harness = makeHarness(); await harness.send(makeInput({})); vi.advanceTimersByTime(QUIT_HOLD_DURATION_MS + 100); await harness.send(makeInput({ isAutoRepeat: true })); vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS); expect(harness.quit).not.toHaveBeenCalled(); vi.advanceTimersByTime(1_999); expect(harness.quit).not.toHaveBeenCalled(); vi.advanceTimersByTime(1); expect(harness.quit).toHaveBeenCalledTimes(1); }); it("waits for Q release when Cmd is released first", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); harness.preventDefault.mockClear(); await harness.send(makeInput({ meta: false, isAutoRepeat: true })); expect(harness.preventDefault).toHaveBeenCalledTimes(1); vi.advanceTimersByTime(QUIT_HOLD_RELEASE_GRACE_MS * 2); expect(harness.quit).not.toHaveBeenCalled(); await harness.send(makeInput({ type: "keyUp", meta: false })); expect(harness.quit).toHaveBeenCalledTimes(1); }); it("does not quit when the hold stops before the duration", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(500); await harness.send(makeInput({ type: "keyUp" })); expect(harness.notifications).toEqual([HOLD_DOWN, UP]); vi.advanceTimersByTime((QUIT_HOLD_DURATION_MS + QUIT_HOLD_RELEASE_GRACE_MS) * 2); expect(harness.concealWindow).not.toHaveBeenCalled(); expect(harness.quit).not.toHaveBeenCalled(); }); it("cancels the hold when the modifier is released first", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); expect(harness.notifications).toEqual([HOLD_DOWN, UP]); vi.advanceTimersByTime((QUIT_HOLD_DURATION_MS + QUIT_HOLD_RELEASE_GRACE_MS) * 2); expect(harness.quit).not.toHaveBeenCalled(); }); it("quits without showing a hint in direct mode", async () => { const harness = makeHarness({ mode: "direct" }); await harness.send(makeInput({})); expect(harness.concealWindow).not.toHaveBeenCalled(); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([]); }); it("honors direct mode when the key is released before its mode read settles", async () => { let resolveMode: ((mode: QuitConfirmationMode) => void) | undefined; const harness = makeHarness({ getMode: () => new Promise((resolve) => { resolveMode = resolve; }), }); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); resolveMode?.("direct"); await Promise.resolve(); await Promise.resolve(); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([]); }); it("does not arm hold mode after a released key's mode read settles", async () => { let resolveMode: ((mode: QuitConfirmationMode) => void) | undefined; const harness = makeHarness({ getMode: () => new Promise((resolve) => { resolveMode = resolve; }), }); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); resolveMode?.("hold"); await Promise.resolve(); await Promise.resolve(); expect(harness.quit).not.toHaveBeenCalled(); expect(harness.notifications).toEqual([]); }); it.each(["direct", "hold", "double-click"] as const)( "quits on a quick second press without waiting for a pending %s mode read", async (mode) => { const resolvers: Array<(mode: QuitConfirmationMode) => void> = []; const harness = makeHarness({ getMode: () => new Promise((resolve) => resolvers.push(resolve)), }); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS - 100); await harness.send(makeInput({})); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([]); await harness.send(makeInput({ type: "keyUp" })); resolvers[0]?.(mode); await Promise.resolve(); await Promise.resolve(); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([]); }, ); it("discards a stale mode resolution from a superseded press", async () => { // Press #1's mode is still pending when the user releases and // presses again; its late resolution must not act for press #2. const resolvers: Array<(mode: QuitConfirmationMode) => void> = []; const harness = makeHarness({ getMode: () => new Promise((resolve) => resolvers.push(resolve)), }); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); // Outside the double-press window, so the second press starts a new hold. vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS + 100); await harness.send(makeInput({})); expect(resolvers).toHaveLength(2); // Press #1 resolves late with "direct". It must not quit press #2. resolvers[0]?.("direct"); await Promise.resolve(); await Promise.resolve(); expect(harness.quit).not.toHaveBeenCalled(); // Press #2 resolves to hold and completes the gesture. resolvers[1]?.("hold"); await harness.holdFor(QUIT_HOLD_DURATION_MS + 200); await harness.send(makeInput({ type: "keyUp" })); expect(harness.quit).toHaveBeenCalledTimes(1); }); it("quits on a quick double press in double-click mode when the first release is unseen", async () => { const harness = makeHarness({ mode: "double-click" }); await harness.send(makeInput({})); vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS - 100); await harness.send(makeInput({})); expect(harness.concealWindow).not.toHaveBeenCalled(); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN, UP]); }); it("keeps the double-press hint visible after key release until the window ends", async () => { const harness = makeHarness({ mode: "double-click" }); await harness.send(makeInput({})); vi.advanceTimersByTime(100); await harness.send(makeInput({ type: "keyUp" })); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN]); vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS - 101); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN]); vi.advanceTimersByTime(1); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN, UP]); }); it("accepts a second full shortcut after the modifier is released and pressed again", async () => { const harness = makeHarness({ mode: "double-click" }); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); await harness.send(makeInput({ type: "keyUp", key: "Meta", meta: false })); vi.advanceTimersByTime(100); await harness.send(makeInput({ key: "Meta" })); await harness.send(makeInput({})); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN, UP]); }); it("expires a delayed double-press hint from keydown rather than mode resolution", async () => { let resolveMode: ((mode: QuitConfirmationMode) => void) | undefined; const harness = makeHarness({ getMode: () => new Promise((resolve) => { resolveMode = resolve; }), }); await harness.send(makeInput({})); vi.advanceTimersByTime(100); await harness.send(makeInput({ type: "keyUp" })); vi.advanceTimersByTime(100); resolveMode?.("double-click"); await Promise.resolve(); await Promise.resolve(); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN]); vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS - 201); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN]); vi.advanceTimersByTime(1); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN, UP]); }); it("treats two slow presses as separate attempts in double-click mode", async () => { const harness = makeHarness({ mode: "double-click" }); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS + 100); await harness.send(makeInput({})); expect(harness.quit).not.toHaveBeenCalled(); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN, UP, DOUBLE_CLICK_DOWN]); }); it("quits on a quick second press in hold mode", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS - 100); await harness.send(makeInput({})); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([HOLD_DOWN, UP]); }); it("quits on a quick second press in hold mode when the first release is unseen", async () => { const harness = makeHarness(); await harness.send(makeInput({})); vi.advanceTimersByTime(QUIT_DOUBLE_PRESS_MS - 100); await harness.send(makeInput({})); expect(harness.concealWindow).not.toHaveBeenCalled(); expect(harness.quit).toHaveBeenCalledTimes(1); expect(harness.notifications).toEqual([HOLD_DOWN, UP]); }); it("does not count auto-repeat as a second press", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(QUIT_DOUBLE_PRESS_MS - 100); expect(harness.quit).not.toHaveBeenCalled(); expect(harness.notifications).toEqual([HOLD_DOWN]); }); it("does not count a released tap after another shortcut interrupts it", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.send(makeInput({ type: "keyUp" })); await harness.send(makeInput({ key: "c" })); vi.advanceTimersByTime(100); await harness.send(makeInput({})); expect(harness.quit).not.toHaveBeenCalled(); expect(harness.notifications).toEqual([HOLD_DOWN, UP, HOLD_DOWN]); }); it("cancels the hold when another key interrupts it", async () => { const harness = makeHarness(); await harness.send(makeInput({})); await harness.holdFor(500); // Shift pressed mid-hold breaks the gesture... await harness.send(makeInput({ shift: true })); expect(harness.notifications).toEqual([HOLD_DOWN, UP]); // ...so later repeats past the threshold must not quit. await harness.holdFor(QUIT_HOLD_DURATION_MS); expect(harness.quit).not.toHaveBeenCalled(); }); it("does not count an interrupted press toward a double press", async () => { const harness = makeHarness({ mode: "double-click" }); await harness.send(makeInput({})); await harness.send(makeInput({ shift: true })); // A fresh press right after the interruption starts a new attempt. await harness.send(makeInput({})); expect(harness.quit).not.toHaveBeenCalled(); expect(harness.notifications).toEqual([DOUBLE_CLICK_DOWN, UP, DOUBLE_CLICK_DOWN]); }); it("ignores other shortcuts", async () => { const harness = makeHarness(); await harness.send(makeInput({ key: "w" })); await harness.send(makeInput({ shift: true })); await harness.send(makeInput({ meta: false })); expect(harness.preventDefault).not.toHaveBeenCalled(); expect(harness.notifications).toEqual([]); }); it("uses control on non-mac platforms", async () => { const harness = makeHarness({ platform: "linux" }); await harness.send(makeInput({ meta: false, control: true })); expect(harness.preventDefault).toHaveBeenCalledTimes(1); await harness.holdFor(QUIT_HOLD_DURATION_MS + 200, { meta: false, control: true }); await harness.send(makeInput({ type: "keyUp", meta: false, control: true })); expect(harness.quit).toHaveBeenCalledTimes(1); }); });