1import { assert, beforeEach, it } from "@effect/vitest";
2import * as NodeEvents from "node:events";
3import { vi } from "vite-plus/test";
4
5const workerConstructorMock = vi.hoisted(() => vi.fn());
6
7vi.mock("node:worker_threads", () => ({
8 Worker: function Worker(...args: ReadonlyArray<unknown>) {
9 return workerConstructorMock(...args);
10 },
11}));
12
13import { startWindowsForegroundFocusThread } from "./WindowsForegroundFocusThread.ts";
14
15const makeWorker = () =>
16 Object.assign(new NodeEvents.EventEmitter(), {
17 postMessage: vi.fn(),
18 terminate: vi.fn(async () => 0),
19 unref: vi.fn(),
20 });
21
22let worker = makeWorker();
23const target = {
24 windowId: 7,
25 processId: 42,
26 title: "T3 Code (Dev)",
27 bounds: { x: 100, y: 50, width: 1_200, height: 800 },
28 contentBounds: { x: 108, y: 50, width: 1_184, height: 792 },
29};
30
31beforeEach(() => {
32 worker = makeWorker();
33 workerConstructorMock.mockReset().mockReturnValue(worker);
34});
35
36it("queues focus until the helper is ready", async () => {
37 const thread = startWindowsForegroundFocusThread("focus.cjs");
38 const focused = thread.focus(target);
39
40 assert.lengthOf(worker.postMessage.mock.calls, 0);
41 worker.emit("message", "ready");
42 assert.deepEqual(worker.postMessage.mock.calls[0]?.[0], {
43 type: "focus",
44 requestId: 1,
45 target,
46 });
47 worker.emit("message", { type: "result", requestId: 1, focused: true });
48
49 assert.isTrue(await focused);
50 assert.deepEqual(workerConstructorMock.mock.calls, [["focus.cjs"]]);
51 thread.close();
52});
53
54it("fails pending focus when the helper exits", async () => {
55 const replacement = makeWorker();
56 workerConstructorMock.mockReturnValueOnce(worker).mockReturnValueOnce(replacement);
57 const thread = startWindowsForegroundFocusThread("focus.cjs");
58 const focused = thread.focus(target);
59
60 worker.emit("exit", 1);
61
62 assert.isFalse(await focused);
63 assert.lengthOf(workerConstructorMock.mock.calls, 1);
64 const prepared = thread.prepare(target);
65 replacement.emit("message", "ready");
66 replacement.emit("message", { type: "result", requestId: 2, focused: true });
67 assert.isTrue(await prepared);
68 assert.lengthOf(workerConstructorMock.mock.calls, 2);
69 thread.close();
70});
71
72it("replaces a timed-out helper before accepting more work", async () => {
73 vi.useFakeTimers();
74 const replacement = makeWorker();
75 workerConstructorMock.mockReturnValueOnce(worker).mockReturnValueOnce(replacement);
76 const thread = startWindowsForegroundFocusThread("focus.cjs");
77
78 try {
79 worker.emit("message", "ready");
80 const focused = thread.focus(target);
81 await vi.advanceTimersByTimeAsync(1_000);
82
83 assert.isFalse(await focused);
84 assert.lengthOf(worker.terminate.mock.calls, 1);
85 assert.lengthOf(workerConstructorMock.mock.calls, 2);
86
87 replacement.emit("message", "ready");
88 const prepared = thread.prepare(target);
89 assert.deepEqual(replacement.postMessage.mock.calls[0]?.[0], {
90 type: "prepare",
91 requestId: 2,
92 target,
93 });
94 replacement.emit("message", { type: "result", requestId: 2, focused: true });
95 assert.isTrue(await prepared);
96 } finally {
97 thread.close();
98 vi.useRealTimers();
99 }
100});