1import { describe, expect, it, vi } from "vite-plus/test";
2
3const { spawnedPollers } = vi.hoisted(() => ({
4 spawnedPollers: [] as Array<{
5 command: string;
6 args: ReadonlyArray<string>;
7 kill: ReturnType<typeof vi.fn>;
8 emitStderr: (text: string) => void;
9 emitExit: (code: number) => void;
10 }>,
11}));
12
13vi.mock("node:child_process", () => ({
14 spawn: (command: string, args: ReadonlyArray<string>) => {
15 const stderrListeners: Array<(chunk: Buffer) => void> = [];
16 const onceListeners = new Map<string, Array<(value?: unknown) => void>>();
17 const record = {
18 command,
19 args,
20 kill: vi.fn(() => true),
21 emitStderr: (text: string) => {
22 for (const listener of stderrListeners) listener(Buffer.from(text));
23 },
24 emitExit: (code: number) => {
25 for (const listener of onceListeners.get("exit") ?? []) listener(code);
26 },
27 };
28 spawnedPollers.push(record);
29 const child = {
30 stderr: {
31 on: (_event: "data", listener: (chunk: Buffer) => void) => {
32 stderrListeners.push(listener);
33 return child;
34 },
35 },
36 once: (event: string, listener: (value?: unknown) => void) => {
37 onceListeners.set(event, [...(onceListeners.get(event) ?? []), listener]);
38 return child;
39 },
40 kill: record.kill,
41 };
42 return child;
43 },
44}));
45
46import { startMacModifierPairShortcutProcess } from "./MacModifierPairShortcutProcess.ts";
47
48describe("macOS modifier pair poller", () => {
49 it("resolves on ready, triggers on lines, and kills on stop", async () => {
50 spawnedPollers.length = 0;
51 const onTrigger = vi.fn();
52 const onFailure = vi.fn();
53 const started = startMacModifierPairShortcutProcess("meta", onTrigger, onFailure);
54 const poller = spawnedPollers[0]!;
55 expect(poller.command).toBe("/usr/bin/osascript");
56 expect(poller.args.slice(-2)).toEqual(["8", "16"]);
57
58 poller.emitStderr("ready\ntrig");
59 const stop = await started;
60 poller.emitStderr("ger\ntrigger\n");
61 expect(onTrigger).toHaveBeenCalledTimes(2);
62
63 stop();
64 expect(poller.kill).toHaveBeenCalledOnce();
65 poller.emitStderr("trigger\n");
66 expect(onTrigger).toHaveBeenCalledTimes(2);
67 poller.emitExit(0);
68 expect(onFailure).not.toHaveBeenCalled();
69 });
70
71 it("reports an unexpected exit after startup", async () => {
72 spawnedPollers.length = 0;
73 const onFailure = vi.fn();
74 const started = startMacModifierPairShortcutProcess("shift", () => undefined, onFailure);
75 const poller = spawnedPollers[0]!;
76 poller.emitStderr("ready\n");
77 await started;
78
79 poller.emitExit(1);
80 expect(onFailure).toHaveBeenCalledOnce();
81 });
82
83 it("rejects when the poller dies before it is ready", async () => {
84 spawnedPollers.length = 0;
85 const started = startMacModifierPairShortcutProcess(
86 "control",
87 () => undefined,
88 () => undefined,
89 );
90 spawnedPollers[0]!.emitExit(1);
91 await expect(started).rejects.toThrow(/exited with code 1/);
92 });
93});