1import type * as Electron from "electron";
2import { assert, beforeEach, expect, it, vi } from "vite-plus/test";
3
4import type { RegionSnapShotChild, RegionSnapShotProcess } from "./RegionSnapShot.ts";
5
6const { createFromBufferMock, resizeMock } = vi.hoisted(() => ({
7 createFromBufferMock: vi.fn(),
8 resizeMock: vi.fn(),
9}));
10
11vi.mock("electron", () => ({
12 nativeImage: { createFromBuffer: createFromBufferMock },
13}));
14
15import { makeRegionSnapShotPool, startRegionSnapShotProcess } from "./RegionSnapShot.ts";
16
17beforeEach(() => {
18 vi.clearAllMocks();
19 createFromBufferMock.mockReturnValue({ resize: resizeMock });
20});
21
22function fakeChild() {
23 const messageListeners: Array<(message: unknown) => void> = [];
24 const exitListeners: Array<() => void> = [];
25 const send = vi.fn();
26 const kill = vi.fn();
27 const child: RegionSnapShotChild = {
28 send,
29 onMessage: (listener) => messageListeners.push(listener),
30 onExit: (listener) => exitListeners.push(listener),
31 kill,
32 };
33 return {
34 child,
35 send,
36 kill,
37 emit: (message: unknown) => {
38 for (const listener of messageListeners) listener(message);
39 },
40 exit: () => {
41 for (const listener of exitListeners) listener();
42 },
43 };
44}
45
46const region = { x: 0, y: 0, width: 2, height: 1 };
47
48it("rejects when the child exits before replying", async () => {
49 const fake = fakeChild();
50 const child = startRegionSnapShotProcess("worker.cjs", () => fake.child);
51 const capture = child.capture(region);
52 fake.emit("ready");
53 fake.exit();
54
55 await expect(capture).rejects.toThrow(/helper exited/);
56});
57
58it("cancels a pending capture on close and kills the child", async () => {
59 const fake = fakeChild();
60 const child = startRegionSnapShotProcess("worker.cjs", () => fake.child);
61 const capture = child.capture(region);
62 child.close();
63
64 await expect(capture).rejects.toThrow(/cancelled/);
65 assert.lengthOf(fake.kill.mock.calls, 1);
66 assert.isFalse(child.alive);
67 fake.emit("ready");
68 assert.lengthOf(fake.send.mock.calls, 0);
69});
70
71function fakeProcess() {
72 const result = Promise.withResolvers<{ width: number; height: number; png: Buffer }>();
73 void result.promise.catch(() => undefined);
74 let alive = true;
75 const close = vi.fn(() => {
76 alive = false;
77 result.reject(new Error("Windows window capture cancelled."));
78 });
79 const capture = vi.fn((_region: Electron.Rectangle) => result.promise);
80 const process: RegionSnapShotProcess = {
81 get alive() {
82 return alive;
83 },
84 capture,
85 close,
86 };
87 return {
88 process,
89 capture,
90 close,
91 resolve: (value: { width: number; height: number; png: Buffer }) => {
92 alive = false;
93 result.resolve(value);
94 },
95 };
96}
97
98function fakePool() {
99 const processes: Array<ReturnType<typeof fakeProcess>> = [];
100 const start = vi.fn(() => {
101 const process = fakeProcess();
102 processes.push(process);
103 return process.process;
104 });
105 return { pool: makeRegionSnapShotPool("worker.cjs", start), processes, start };
106}
107
108it("rejects a second capture while one is in flight", async () => {
109 const { pool, processes } = fakePool();
110 const first = pool.capture(region);
111
112 await expect(pool.capture(region)).rejects.toThrow(/still in progress/);
113 assert.lengthOf(processes, 1);
114
115 processes[0]!.resolve({ width: 1, height: 1, png: Buffer.from([1]) });
116 await first;
117});
118
119it("closes the standby and the in-flight child", async () => {
120 const { pool, processes } = fakePool();
121 pool.warm();
122 const capture = pool.capture(region);
123 pool.warm();
124 assert.lengthOf(processes, 2);
125
126 pool.close();
127
128 await expect(capture).rejects.toThrow(/cancelled/);
129 assert.lengthOf(processes[0]!.close.mock.calls, 1);
130 assert.lengthOf(processes[1]!.close.mock.calls, 1);
131 await expect(pool.capture(region)).rejects.toThrow(/unavailable/);
132 assert.lengthOf(processes, 2);
133});