1// @effect-diagnostics nodeBuiltinImport:off -- Isolated child-process streams; no desktop session is touched.
2import * as NodeEvents from "node:events";
3import * as NodeStream from "node:stream";
4import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
5
6const spawn = vi.hoisted(() => vi.fn());
7vi.mock("node:child_process", () => ({ spawn }));
8import { startNativeCaptureFeedback } from "./NativeCaptureFeedback.ts";
9
10const options = {
11 bounds: { x: -800, y: 0, width: 800, height: 600 },
12 pid: 123,
13 flash: true,
14 animate: true,
15};
16let child: NodeEvents.EventEmitter & {
17 stdin: NodeStream.PassThrough;
18 stdout: NodeStream.PassThrough;
19 kill: ReturnType<typeof vi.fn>;
20};
21let commands: string[];
22beforeEach(() => {
23 vi.useFakeTimers();
24 commands = [];
25 child = Object.assign(new NodeEvents.EventEmitter(), {
26 stdin: new NodeStream.PassThrough(),
27 stdout: new NodeStream.PassThrough(),
28 kill: vi.fn(),
29 });
30 child.stdin.on("data", (value: Buffer) => commands.push(value.toString()));
31 spawn.mockReturnValue(child);
32});
33afterEach(() => {
34 child.emit("close");
35 vi.useRealTimers();
36});
37function event(value: unknown) {
38 child.stdout.write(`${JSON.stringify(value)}\n`);
39}
40
41it("waits for compositor readiness and landing, then waits for cleanup on acknowledgement", async () => {
42 const starting = startNativeCaptureFeedback("/helper", "/private", options);
43 let ready = false;
44 void starting.then(() => {
45 ready = true;
46 });
47 await Promise.resolve();
48 expect(ready).toBe(false);
49 event({ event: "ready", animate: true });
50 const feedback = (await starting)!;
51 expect(feedback.animationStarted).toBe(true);
52 const frame = { x: 0.1, y: 0.7, width: 0.2, height: 0.1 };
53 const flight = feedback.animateTo('My "Draft"', frame);
54 let completed = false;
55 const complete = feedback.complete().then(() => {
56 completed = true;
57 });
58 expect(commands.map((line) => JSON.parse(line))).toEqual([
59 { command: "animate", title: 'My "Draft"', frame },
60 ]);
61 event({ event: "landed" });
62 await flight;
63 expect(commands.map((line) => JSON.parse(line))).toHaveLength(2);
64 expect(completed).toBe(false);
65 child.emit("close");
66 await complete;
67 expect(completed).toBe(true);
68 expect(child.kill).not.toHaveBeenCalled();
69 expect(vi.getTimerCount()).toBe(0);
70});
71
72it("releases waiters if the overlay closes during flight", async () => {
73 const starting = startNativeCaptureFeedback("/helper", "/private", options);
74 event({ event: "ready", animate: true });
75 const feedback = (await starting)!;
76 const flight = feedback.animateTo("Draft", { x: 0, y: 0, width: 1, height: 1 });
77 event({ event: "done" });
78 await flight;
79 child.emit("close");
80 await feedback.complete();
81 expect(vi.getTimerCount()).toBe(0);
82});