459 lines · 17.5 KB · TypeScript
RawDownload
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>): 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<QuitConfirmationMode>;}) {  const notifications: Array<QuitShortcutHintEvent> = [];  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<QuitHoldKeyInput> = {},    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);  });});