Skip to content

Commit 93741b2

Browse files
committed
test: add tests for shared utilities (videoDetection, parallelWorker, shutdown)
- Add 50 new unit tests for the three new shared modules - videoDetection.test.ts: test all provider detection functions - parallelWorker.test.ts: test worker pool creation, task processing, error handling - shutdown.test.ts: test signal handling, cleanup registration, shutdown flow Coverage improvement: 67% → 86% statements
1 parent e9d6ce7 commit 93741b2

File tree

3 files changed

+727
-0
lines changed

3 files changed

+727
-0
lines changed

src/shared/parallelWorker.test.ts

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import type { BrowserContext, Page } from "playwright";
3+
import {
4+
parallelProcess,
5+
parallelProcessWithPages,
6+
createWorkerPool,
7+
closeWorkerPool,
8+
} from "./parallelWorker.js";
9+
10+
/**
11+
* Creates a mock Playwright Page with tracked close function.
12+
*/
13+
function createMockPage(id: string): { page: Page; closeFn: ReturnType<typeof vi.fn> } {
14+
const closeFn = vi.fn().mockResolvedValue(undefined);
15+
const page = {
16+
id,
17+
close: closeFn,
18+
goto: vi.fn().mockResolvedValue(undefined),
19+
} as unknown as Page;
20+
return { page, closeFn };
21+
}
22+
23+
/**
24+
* Creates a mock BrowserContext that can create pages.
25+
*/
26+
function createMockContext(pagesToCreate: Page[]): {
27+
context: BrowserContext;
28+
newPageFn: ReturnType<typeof vi.fn>;
29+
} {
30+
let pageIndex = 0;
31+
const newPageFn = vi.fn().mockImplementation(() => {
32+
if (pageIndex < pagesToCreate.length) {
33+
return Promise.resolve(pagesToCreate[pageIndex++]);
34+
}
35+
throw new Error("Cannot create more pages");
36+
});
37+
const context = { newPage: newPageFn } as unknown as BrowserContext;
38+
return { context, newPageFn };
39+
}
40+
41+
describe("parallelWorker", () => {
42+
describe("createWorkerPool", () => {
43+
it("creates the requested number of worker pages", async () => {
44+
const mocks = [createMockPage("1"), createMockPage("2"), createMockPage("3")];
45+
const pages = mocks.map((m) => m.page);
46+
const { context, newPageFn } = createMockContext(pages);
47+
const { page: mainPage } = createMockPage("main");
48+
49+
const result = await createWorkerPool(context, mainPage, 3);
50+
51+
expect(result.pages).toHaveLength(3);
52+
expect(result.isUsingMainPage).toBe(false);
53+
expect(newPageFn).toHaveBeenCalledTimes(3);
54+
});
55+
56+
it("falls back to main page if page creation fails", async () => {
57+
const newPageFn = vi.fn().mockRejectedValue(new Error("Cannot create page"));
58+
const context = { newPage: newPageFn } as unknown as BrowserContext;
59+
const { page: mainPage } = createMockPage("main");
60+
61+
const result = await createWorkerPool(context, mainPage, 3);
62+
63+
expect(result.pages).toHaveLength(1);
64+
expect(result.pages[0]).toBe(mainPage);
65+
expect(result.isUsingMainPage).toBe(true);
66+
});
67+
});
68+
69+
describe("closeWorkerPool", () => {
70+
it("closes all pages except the main page", async () => {
71+
const mock1 = createMockPage("1");
72+
const mock2 = createMockPage("2");
73+
const mockMain = createMockPage("main");
74+
const pages = [mock1.page, mock2.page, mockMain.page];
75+
76+
await closeWorkerPool(pages, mockMain.page);
77+
78+
expect(mock1.closeFn).toHaveBeenCalled();
79+
expect(mock2.closeFn).toHaveBeenCalled();
80+
expect(mockMain.closeFn).not.toHaveBeenCalled();
81+
});
82+
83+
it("closes all pages when no main page is provided", async () => {
84+
const mock1 = createMockPage("1");
85+
const mock2 = createMockPage("2");
86+
const pages = [mock1.page, mock2.page];
87+
88+
await closeWorkerPool(pages);
89+
90+
expect(mock1.closeFn).toHaveBeenCalled();
91+
expect(mock2.closeFn).toHaveBeenCalled();
92+
});
93+
94+
it("ignores close errors", async () => {
95+
const closeFn = vi.fn().mockRejectedValue(new Error("Close failed"));
96+
const page = { close: closeFn } as unknown as Page;
97+
98+
// Should not throw
99+
await expect(closeWorkerPool([page])).resolves.toBeUndefined();
100+
});
101+
});
102+
103+
describe("parallelProcessWithPages", () => {
104+
it("processes all tasks using worker pages", async () => {
105+
const pages = [createMockPage("1").page, createMockPage("2").page];
106+
const tasks = ["task1", "task2", "task3", "task4"];
107+
const processor = vi.fn(
108+
async (_page: Page, task: string): Promise<string> => `result-${task}`
109+
);
110+
111+
const result = await parallelProcessWithPages(pages, tasks, processor, {});
112+
113+
expect(result.results).toHaveLength(4);
114+
expect(result.results).toContain("result-task1");
115+
expect(result.results).toContain("result-task2");
116+
expect(result.results).toContain("result-task3");
117+
expect(result.results).toContain("result-task4");
118+
expect(result.errors).toHaveLength(0);
119+
});
120+
121+
it("maintains result order regardless of completion order", async () => {
122+
const pages = [createMockPage("1").page];
123+
const tasks = [1, 2, 3];
124+
const processor = vi.fn(
125+
async (_page: Page, task: number): Promise<string> => `result-${task}`
126+
);
127+
128+
const result = await parallelProcessWithPages(pages, tasks, processor, {});
129+
130+
// Results should be in order of task indices, not completion order
131+
expect(result.results[0]).toBe("result-1");
132+
expect(result.results[1]).toBe("result-2");
133+
expect(result.results[2]).toBe("result-3");
134+
});
135+
136+
it("collects errors with task indices", async () => {
137+
const pages = [createMockPage("1").page];
138+
const tasks = ["good", "bad", "good2"];
139+
const processor = vi.fn(async (_page: Page, task: string): Promise<string> => {
140+
if (task === "bad") {
141+
throw new Error("Task failed");
142+
}
143+
return `result-${task}`;
144+
});
145+
146+
const result = await parallelProcessWithPages(pages, tasks, processor, {});
147+
148+
expect(result.results).toHaveLength(2);
149+
expect(result.errors).toHaveLength(1);
150+
expect(result.errors[0]!.index).toBe(1);
151+
expect(result.errors[0]!.error).toBeInstanceOf(Error);
152+
});
153+
154+
it("calls onError callback when a task fails", async () => {
155+
const pages = [createMockPage("1").page];
156+
const tasks = ["fail"];
157+
const onError = vi.fn();
158+
const processor = vi.fn().mockRejectedValue(new Error("Boom"));
159+
160+
await parallelProcessWithPages(pages, tasks, processor, { onError });
161+
162+
expect(onError).toHaveBeenCalledWith(expect.any(Error), 0);
163+
});
164+
165+
it("respects shouldContinue and stops early", async () => {
166+
const pages = [createMockPage("1").page];
167+
const tasks = [1, 2, 3, 4, 5];
168+
let processedCount = 0;
169+
170+
const processor = vi.fn(async (_page: Page, task: number): Promise<number> => {
171+
processedCount++;
172+
return task;
173+
});
174+
175+
// Stop after processing 2 tasks
176+
let callCount = 0;
177+
const shouldContinue = () => {
178+
callCount++;
179+
return callCount <= 2;
180+
};
181+
182+
await parallelProcessWithPages(pages, tasks, processor, {
183+
shouldContinue,
184+
});
185+
186+
// Should have processed approximately 2 tasks (depends on timing)
187+
expect(processedCount).toBeLessThan(tasks.length);
188+
});
189+
190+
it("handles empty task list", async () => {
191+
const pages = [createMockPage("1").page];
192+
const processor = vi.fn();
193+
194+
const result = await parallelProcessWithPages(pages, [], processor, {});
195+
196+
expect(result.results).toHaveLength(0);
197+
expect(result.errors).toHaveLength(0);
198+
expect(processor).not.toHaveBeenCalled();
199+
});
200+
201+
it("passes page, task, and index to processor", async () => {
202+
const { page } = createMockPage("1");
203+
const pages = [page];
204+
const tasks = ["a", "b"];
205+
const processor = vi.fn(async (_page: Page, _task: string): Promise<string> => "done");
206+
207+
await parallelProcessWithPages(pages, tasks, processor, {});
208+
209+
expect(processor).toHaveBeenCalledWith(page, "a", 0);
210+
expect(processor).toHaveBeenCalledWith(page, "b", 1);
211+
});
212+
});
213+
214+
describe("parallelProcess", () => {
215+
it("creates worker pages and processes tasks", async () => {
216+
const mock1 = createMockPage("1");
217+
const mock2 = createMockPage("2");
218+
const workerPages = [mock1.page, mock2.page];
219+
const { context, newPageFn } = createMockContext(workerPages);
220+
const { page: mainPage } = createMockPage("main");
221+
const tasks = ["t1", "t2"];
222+
const processor = vi.fn(async (_page: Page, task: string): Promise<string> => `r-${task}`);
223+
224+
const result = await parallelProcess(context, mainPage, tasks, processor, {
225+
concurrency: 2,
226+
});
227+
228+
expect(result.results).toHaveLength(2);
229+
expect(newPageFn).toHaveBeenCalledTimes(2);
230+
});
231+
232+
it("closes worker pages after processing", async () => {
233+
const mock1 = createMockPage("1");
234+
const workerPages = [mock1.page];
235+
const { context } = createMockContext(workerPages);
236+
const { page: mainPage } = createMockPage("main");
237+
const processor = vi.fn(async (_page: Page, _task: string): Promise<string> => "done");
238+
239+
await parallelProcess(context, mainPage, ["task"], processor, {
240+
concurrency: 1,
241+
});
242+
243+
expect(mock1.closeFn).toHaveBeenCalled();
244+
});
245+
246+
it("falls back to main page if tab creation fails", async () => {
247+
const newPageFn = vi.fn().mockRejectedValue(new Error("No tabs"));
248+
const context = { newPage: newPageFn } as unknown as BrowserContext;
249+
const { page: mainPage, closeFn: mainCloseFn } = createMockPage("main");
250+
const processor = vi.fn(async (_page: Page, _task: string): Promise<string> => "done");
251+
252+
const result = await parallelProcess(context, mainPage, ["task"], processor, {
253+
concurrency: 2,
254+
});
255+
256+
expect(result.results).toHaveLength(1);
257+
// Main page should not be closed
258+
expect(mainCloseFn).not.toHaveBeenCalled();
259+
});
260+
});
261+
});

0 commit comments

Comments
 (0)