diff --git a/src/browser/pw-tools-core.test.ts b/src/browser/pw-tools-core.test.ts index da0b6af9c..965d41485 100644 --- a/src/browser/pw-tools-core.test.ts +++ b/src/browser/pw-tools-core.test.ts @@ -2,7 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; let currentPage: Record | null = null; let currentRefLocator: Record | null = null; -let pageState: { console: unknown[]; armIdUpload: number; armIdDialog: number }; +let pageState: { + console: unknown[]; + armIdUpload: number; + armIdDialog: number; + armIdDownload: number; +}; const sessionMocks = vi.hoisted(() => ({ getPageForTargetId: vi.fn(async () => { @@ -26,7 +31,12 @@ describe("pw-tools-core", () => { beforeEach(() => { currentPage = null; currentRefLocator = null; - pageState = { console: [], armIdUpload: 0, armIdDialog: 0 }; + pageState = { + console: [], + armIdUpload: 0, + armIdDialog: 0, + armIdDownload: 0, + }; for (const fn of Object.values(sessionMocks)) fn.mockClear(); }); @@ -279,6 +289,113 @@ describe("pw-tools-core", () => { }); }); + it("waits for the next download and saves it", async () => { + let downloadHandler: ((download: unknown) => void) | undefined; + const on = vi.fn((event: string, handler: (download: unknown) => void) => { + if (event === "download") downloadHandler = handler; + }); + const off = vi.fn(); + + const saveAs = vi.fn(async () => {}); + const download = { + url: () => "https://example.com/file.bin", + suggestedFilename: () => "file.bin", + saveAs, + }; + + currentPage = { on, off }; + + const mod = await importModule(); + const p = mod.waitForDownloadViaPlaywright({ + cdpUrl: "http://127.0.0.1:18792", + targetId: "T1", + path: "/tmp/file.bin", + timeoutMs: 1000, + }); + + await Promise.resolve(); + expect(downloadHandler).toBeDefined(); + downloadHandler?.(download); + + const res = await p; + expect(saveAs).toHaveBeenCalledWith("/tmp/file.bin"); + expect(res.path).toBe("/tmp/file.bin"); + }); + + it("clicks a ref and saves the resulting download", async () => { + let downloadHandler: ((download: unknown) => void) | undefined; + const on = vi.fn((event: string, handler: (download: unknown) => void) => { + if (event === "download") downloadHandler = handler; + }); + const off = vi.fn(); + + const click = vi.fn(async () => {}); + currentRefLocator = { click }; + + const saveAs = vi.fn(async () => {}); + const download = { + url: () => "https://example.com/report.pdf", + suggestedFilename: () => "report.pdf", + saveAs, + }; + + currentPage = { on, off }; + + const mod = await importModule(); + const p = mod.downloadViaPlaywright({ + cdpUrl: "http://127.0.0.1:18792", + targetId: "T1", + ref: "e12", + path: "/tmp/report.pdf", + timeoutMs: 1000, + }); + + await Promise.resolve(); + expect(downloadHandler).toBeDefined(); + expect(click).toHaveBeenCalledWith({ timeout: 1000 }); + + downloadHandler?.(download); + + const res = await p; + expect(saveAs).toHaveBeenCalledWith("/tmp/report.pdf"); + expect(res.path).toBe("/tmp/report.pdf"); + }); + + it("waits for a matching response and returns its body", async () => { + let responseHandler: ((resp: unknown) => void) | undefined; + const on = vi.fn((event: string, handler: (resp: unknown) => void) => { + if (event === "response") responseHandler = handler; + }); + const off = vi.fn(); + currentPage = { on, off }; + + const resp = { + url: () => "https://example.com/api/data", + status: () => 200, + headers: () => ({ "content-type": "application/json" }), + text: async () => '{"ok":true,"value":123}', + }; + + const mod = await importModule(); + const p = mod.responseBodyViaPlaywright({ + cdpUrl: "http://127.0.0.1:18792", + targetId: "T1", + url: "**/api/data", + timeoutMs: 1000, + maxChars: 10, + }); + + await Promise.resolve(); + expect(responseHandler).toBeDefined(); + responseHandler?.(resp); + + const res = await p; + expect(res.url).toBe("https://example.com/api/data"); + expect(res.status).toBe(200); + expect(res.body).toBe('{"ok":true'); + expect(res.truncated).toBe(true); + }); + it("rewrites strict mode violations into snapshot hints", async () => { const click = vi.fn(async () => { throw new Error( diff --git a/src/browser/server.test.ts b/src/browser/server.test.ts index bac528cbd..f406db7ba 100644 --- a/src/browser/server.test.ts +++ b/src/browser/server.test.ts @@ -24,6 +24,11 @@ const pwMocks = vi.hoisted(() => ({ clickViaPlaywright: vi.fn(async () => {}), closePageViaPlaywright: vi.fn(async () => {}), closePlaywrightBrowserConnection: vi.fn(async () => {}), + downloadViaPlaywright: vi.fn(async () => ({ + url: "https://example.com/report.pdf", + suggestedFilename: "report.pdf", + path: "/tmp/report.pdf", + })), dragViaPlaywright: vi.fn(async () => {}), evaluateViaPlaywright: vi.fn(async () => "ok"), fillFormViaPlaywright: vi.fn(async () => {}), @@ -32,6 +37,12 @@ const pwMocks = vi.hoisted(() => ({ navigateViaPlaywright: vi.fn(async () => ({ url: "https://example.com" })), pdfViaPlaywright: vi.fn(async () => ({ buffer: Buffer.from("pdf") })), pressKeyViaPlaywright: vi.fn(async () => {}), + responseBodyViaPlaywright: vi.fn(async () => ({ + url: "https://example.com/api/data", + status: 200, + headers: { "content-type": "application/json" }, + body: '{"ok":true}', + })), resizeViewportViaPlaywright: vi.fn(async () => {}), selectOptionViaPlaywright: vi.fn(async () => {}), setInputFilesViaPlaywright: vi.fn(async () => {}), @@ -40,6 +51,11 @@ const pwMocks = vi.hoisted(() => ({ buffer: Buffer.from("png"), })), typeViaPlaywright: vi.fn(async () => {}), + waitForDownloadViaPlaywright: vi.fn(async () => ({ + url: "https://example.com/report.pdf", + suggestedFilename: "report.pdf", + path: "/tmp/report.pdf", + })), waitForViaPlaywright: vi.fn(async () => {}), })); @@ -558,6 +574,51 @@ describe("browser control server", () => { timeoutMs: 5678, }); + const waitDownload = await realFetch(`${base}/wait/download`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ path: "/tmp/report.pdf", timeoutMs: 1111 }), + }).then((r) => r.json()); + expect(waitDownload).toMatchObject({ ok: true }); + expect(pwMocks.waitForDownloadViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: cdpBaseUrl, + targetId: "abcd1234", + path: "/tmp/report.pdf", + timeoutMs: 1111, + }); + + const download = await realFetch(`${base}/download`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ ref: "e12", path: "/tmp/report.pdf" }), + }).then((r) => r.json()); + expect(download).toMatchObject({ ok: true }); + expect(pwMocks.downloadViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: cdpBaseUrl, + targetId: "abcd1234", + ref: "e12", + path: "/tmp/report.pdf", + timeoutMs: undefined, + }); + + const responseBody = await realFetch(`${base}/response/body`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + url: "**/api/data", + timeoutMs: 2222, + maxChars: 10, + }), + }).then((r) => r.json()); + expect(responseBody).toMatchObject({ ok: true }); + expect(pwMocks.responseBodyViaPlaywright).toHaveBeenCalledWith({ + cdpUrl: cdpBaseUrl, + targetId: "abcd1234", + url: "**/api/data", + timeoutMs: 2222, + maxChars: 10, + }); + const consoleRes = (await realFetch(`${base}/console?level=error`).then( (r) => r.json(), )) as { ok: boolean; messages?: unknown[] };