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("doesn't claim a flight when the compositor disables motion; flash still works", async () => {
73 const starting = startNativeCaptureFeedback("/helper", "/private", options);
74 event({ event: "ready", animate: false });
75 const feedback = (await starting)!;
76 expect(feedback.animationStarted).toBe(false);
77 await feedback.animateTo("Draft", { x: 0, y: 0, width: 1, height: 1 });
78 expect(commands).toEqual([]);
79 const complete = feedback.complete();
80 child.emit("close");
81 await complete;
82});
83
84it("falls back on startup failure, malformed replies, and timeout without leaving timers", async () => {
85 const starting = startNativeCaptureFeedback("/helper", "/private", options);
86 child.stdout.write("not JSON\n");
87 expect(commands).toEqual(['{"command":"close"}\n']);
88 await vi.advanceTimersByTimeAsync(1000);
89 expect(child.kill).toHaveBeenCalledOnce();
90 child.emit("close");
91 expect(await starting).toBeUndefined();
92 expect(vi.getTimerCount()).toBe(0);
93});
94
95it("bounds a missing ready receipt and cancels an in-flight animation on compositor failure", async () => {
96 const timed = startNativeCaptureFeedback("/helper", "/private", options);
97 await vi.advanceTimersByTimeAsync(2000);
98 child.emit("close");
99 expect(await timed).toBeUndefined();
100});
101
102it("releases waiters if the overlay closes during flight", async () => {
103 const starting = startNativeCaptureFeedback("/helper", "/private", options);
104 event({ event: "ready", animate: true });
105 const feedback = (await starting)!;
106 const flight = feedback.animateTo("Draft", { x: 0, y: 0, width: 1, height: 1 });
107 event({ event: "done" });
108 await flight;
109 child.emit("close");
110 await feedback.complete();
111 expect(vi.getTimerCount()).toBe(0);
112});