1import { assert, beforeEach, it } from "@effect/vitest";
2import * as NodeEvents from "node:events";
3import * as NodeProcess from "node:process";
4import { vi } from "vite-plus/test";
5
6const forkMock = vi.hoisted(() =>
7 vi.fn<
8 (
9 _path: string,
10 _args: ReadonlyArray<string>,
11 _options: { env?: NodeJS.ProcessEnv; execPath?: string },
12 ) => unknown
13 >(),
14);
15
16vi.mock("node:child_process", () => ({ fork: forkMock }));
17
18import {
19 makeSnapShotAccessibilityProcessPool,
20 startSnapShotAccessibilityProcess,
21} from "./SnapShotAccessibilityProcess.ts";
22
23const makeWorker = () =>
24 Object.assign(new NodeEvents.EventEmitter(), {
25 kill: vi.fn(() => true),
26 send: vi.fn(),
27 });
28let worker = makeWorker();
29const request = {
30 active: {
31 title: "Zoom Meeting",
32 bounds: { x: 10, y: 20, width: 800, height: 600 },
33 owner: { processId: 42 },
34 },
35 platform: "darwin" as const,
36 sourceTitle: "Zoom Meeting",
37 imageSize: { width: 1_600, height: 1_200 },
38};
39
40beforeEach(() => {
41 worker = makeWorker();
42 forkMock.mockReset().mockReturnValue(worker);
43});
44
45it("keeps T3 alive when Zoom crashes accessibility extraction", async () => {
46 const process = startSnapShotAccessibilityProcess("accessibility.cjs");
47 const read = process.read(request);
48 worker.emit("message", "ready");
49 worker.emit("message", "started");
50 await read.started;
51 worker.emit("exit", null, "SIGABRT");
52
53 assert.isUndefined(await read.result);
54});
55
56it("returns immediately when accessibility crashes before the screenshot is ready", async () => {
57 const process = startSnapShotAccessibilityProcess("accessibility.cjs");
58 worker.emit("exit", null, "SIGABRT");
59
60 const read = process.read(request);
61 await read.started;
62 assert.isUndefined(await read.result);
63});
64
65it("returns accessibility extracted by the helper", async () => {
66 const process = startSnapShotAccessibilityProcess("accessibility.cjs");
67 const read = process.read(request);
68 const context = { accessibleText: "Meeting controls" };
69 worker.emit("message", "ready");
70 worker.emit("message", "started");
71 worker.emit("message", { type: "result", context });
72
73 await read.started;
74 assert.deepEqual(await read.result, context);
75 assert.strictEqual(forkMock.mock.calls[0]?.[2]?.env?.ELECTRON_RUN_AS_NODE, "1");
76 assert.strictEqual(forkMock.mock.calls[0]?.[2]?.execPath, NodeProcess.execPath);
77});
78
79it("hands a warm helper to the capture and prewarms its replacement", async () => {
80 const warmWorker = makeWorker();
81 const replacementWorker = makeWorker();
82 forkMock.mockReset().mockReturnValueOnce(warmWorker).mockReturnValueOnce(replacementWorker);
83 const pool = makeSnapShotAccessibilityProcessPool("accessibility.cjs");
84
85 pool.warm();
86 warmWorker.emit("message", "ready");
87 const read = pool.read(request);
88
89 assert.lengthOf(forkMock.mock.calls, 2);
90 assert.deepEqual(warmWorker.send.mock.calls[0]?.[0], request);
91 assert.lengthOf(replacementWorker.send.mock.calls, 0);
92
93 warmWorker.emit("message", "started");
94 warmWorker.emit("message", { type: "result", context: { accessibleText: "Ready" } });
95 assert.deepEqual(await read.result, { accessibleText: "Ready" });
96
97 pool.close();
98 assert.lengthOf(replacementWorker.kill.mock.calls, 1);
99});
100
101it("replaces a warm helper that exits before capture", async () => {
102 const exitedWorker = makeWorker();
103 const captureWorker = makeWorker();
104 const replacementWorker = makeWorker();
105 forkMock
106 .mockReset()
107 .mockReturnValueOnce(exitedWorker)
108 .mockReturnValueOnce(captureWorker)
109 .mockReturnValueOnce(replacementWorker);
110 const pool = makeSnapShotAccessibilityProcessPool("accessibility.cjs");
111
112 pool.warm();
113 exitedWorker.emit("exit", 1);
114 const read = pool.read(request);
115 captureWorker.emit("message", "ready");
116 captureWorker.emit("message", "started");
117 captureWorker.emit("message", { type: "result", context: { accessibleText: "Recovered" } });
118
119 assert.deepEqual(await read.result, { accessibleText: "Recovered" });
120 assert.lengthOf(forkMock.mock.calls, 3);
121 assert.deepEqual(captureWorker.send.mock.calls[0]?.[0], request);
122 pool.close();
123});
124
125it("allows cold startup to exceed the request-start budget", async () => {
126 vi.useFakeTimers();
127 const process = startSnapShotAccessibilityProcess("accessibility.cjs");
128 try {
129 const read = process.read(request);
130 await vi.advanceTimersByTimeAsync(1_500);
131 assert.lengthOf(worker.kill.mock.calls, 0);
132 worker.emit("message", "ready");
133 assert.deepEqual(worker.send.mock.calls[0]?.[0], request);
134 worker.emit("message", "started");
135 worker.emit("message", { type: "result", context: { accessibleText: "Ready" } });
136 assert.deepEqual(await read.result, { accessibleText: "Ready" });
137 } finally {
138 process.close();
139 vi.useRealTimers();
140 }
141});