// @effect-diagnostics nodeBuiltinImport:off -- Isolated child-process streams; no desktop session is touched. import * as NodeEvents from "node:events"; import * as NodeStream from "node:stream"; import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test"; const spawn = vi.hoisted(() => vi.fn()); vi.mock("node:child_process", () => ({ spawn })); import { startNativeCaptureFeedback } from "./NativeCaptureFeedback.ts"; const options = { bounds: { x: -800, y: 0, width: 800, height: 600 }, pid: 123, flash: true, animate: true, }; let child: NodeEvents.EventEmitter & { stdin: NodeStream.PassThrough; stdout: NodeStream.PassThrough; kill: ReturnType; }; let commands: string[]; beforeEach(() => { vi.useFakeTimers(); commands = []; child = Object.assign(new NodeEvents.EventEmitter(), { stdin: new NodeStream.PassThrough(), stdout: new NodeStream.PassThrough(), kill: vi.fn(), }); child.stdin.on("data", (value: Buffer) => commands.push(value.toString())); spawn.mockReturnValue(child); }); afterEach(() => { child.emit("close"); vi.useRealTimers(); }); function event(value: unknown) { child.stdout.write(`${JSON.stringify(value)}\n`); } it("waits for compositor readiness and landing, then waits for cleanup on acknowledgement", async () => { const starting = startNativeCaptureFeedback("/helper", "/private", options); let ready = false; void starting.then(() => { ready = true; }); await Promise.resolve(); expect(ready).toBe(false); event({ event: "ready", animate: true }); const feedback = (await starting)!; expect(feedback.animationStarted).toBe(true); const frame = { x: 0.1, y: 0.7, width: 0.2, height: 0.1 }; const flight = feedback.animateTo('My "Draft"', frame); let completed = false; const complete = feedback.complete().then(() => { completed = true; }); expect(commands.map((line) => JSON.parse(line))).toEqual([ { command: "animate", title: 'My "Draft"', frame }, ]); event({ event: "landed" }); await flight; expect(commands.map((line) => JSON.parse(line))).toHaveLength(2); expect(completed).toBe(false); child.emit("close"); await complete; expect(completed).toBe(true); expect(child.kill).not.toHaveBeenCalled(); expect(vi.getTimerCount()).toBe(0); }); it("doesn't claim a flight when the compositor disables motion; flash still works", async () => { const starting = startNativeCaptureFeedback("/helper", "/private", options); event({ event: "ready", animate: false }); const feedback = (await starting)!; expect(feedback.animationStarted).toBe(false); await feedback.animateTo("Draft", { x: 0, y: 0, width: 1, height: 1 }); expect(commands).toEqual([]); const complete = feedback.complete(); child.emit("close"); await complete; }); it("falls back on startup failure, malformed replies, and timeout without leaving timers", async () => { const starting = startNativeCaptureFeedback("/helper", "/private", options); child.stdout.write("not JSON\n"); expect(commands).toEqual(['{"command":"close"}\n']); await vi.advanceTimersByTimeAsync(1000); expect(child.kill).toHaveBeenCalledOnce(); child.emit("close"); expect(await starting).toBeUndefined(); expect(vi.getTimerCount()).toBe(0); }); it("bounds a missing ready receipt and cancels an in-flight animation on compositor failure", async () => { const timed = startNativeCaptureFeedback("/helper", "/private", options); await vi.advanceTimersByTimeAsync(2000); child.emit("close"); expect(await timed).toBeUndefined(); }); it("releases waiters if the overlay closes during flight", async () => { const starting = startNativeCaptureFeedback("/helper", "/private", options); event({ event: "ready", animate: true }); const feedback = (await starting)!; const flight = feedback.animateTo("Draft", { x: 0, y: 0, width: 1, height: 1 }); event({ event: "done" }); await flight; child.emit("close"); await feedback.complete(); expect(vi.getTimerCount()).toBe(0); });