import { execFile } from "node:child_process"; import { mkdtemp, readFile, rm } from "node:fs/promises"; import http from "node:http"; import os from "node:os"; import path from "node:path"; import { promisify } from "node:util"; import { chromium, expect, test } from "@playwright/test"; const execute = promisify(execFile); const helper = path.resolve( import.meta.dirname, "../../../infra/sandboxes/computer/rakazo-page-browser", ); test("live page helper preserves identity, masks passwords, and reports partial actions", async () => { const profile = await mkdtemp(path.join(os.tmpdir(), "page-browser-test-")); const server = http.createServer((_req, res) => { res.setHeader("content-type", "text/html"); res.end(`Fixture

Readable article content

`); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); if (!address || typeof address === "string") throw new Error("No fixture server"); let context: Awaited> | undefined; try { context = await chromium.launchPersistentContext(profile, { headless: true, args: ["--remote-debugging-port=0"], }); let port = ""; await expect(async () => { port = (await readFile(path.join(profile, "DevToolsActivePort"), "utf8")).split("\n")[0]; expect(port).toMatch(/^\d+$/); }).toPass({ timeout: 5_000 }); async function command(name: string, args: Record = {}) { const { stdout } = await execute("python3", [helper, name, JSON.stringify(args)], { env: { ...process.env, RAKAZO_CDP_PORT: port }, timeout: 30_000, }); return JSON.parse(stdout); } const page = context.pages()[0]!; await page.goto(`http://127.0.0.1:${address.port}`); const snapshot = await command("snapshot"); expect(snapshot.ok).toBe(true); expect(snapshot.title).toBe("Fixture"); expect(snapshot.tree).toContain("Readable article content"); expect(JSON.stringify(snapshot)).not.toContain("fake-test-password"); // Element boxes are screen pixels for the visible bot cursor. expect(snapshot.screen.width).toBeGreaterThan(0); expect(snapshot.screen.height).toBeGreaterThan(0); const saveBox = snapshot.elements.find((element: { name: string }) => element.name === "Save") .box as number[]; const saveRect = (await page.locator("#save").boundingBox())!; expect(saveBox[2]).toBe(Math.round(saveRect.width)); expect(saveBox[3]).toBe(Math.round(saveRect.height)); expect(saveBox[0]).toBeGreaterThanOrEqual(Math.round(saveRect.x)); expect(saveBox[1]).toBeGreaterThanOrEqual(Math.round(saveRect.y)); const ref = (snapshot: { elements: { name: string; ref: string }[] }, name: string) => snapshot.elements.find((element) => element.name === name)!.ref; const result = await command("act", { actions: [ { kind: "fill", ref: ref(snapshot, "Name"), text: "Example" }, { kind: "click", ref: ref(snapshot, "Save") }, { kind: "click", ref: "missing-ref" }, ], }); expect(result).toMatchObject({ ok: false, completed: 2, uncertain: false }); await expect(page.locator("#name")).toHaveValue("Example"); await expect(page.locator("#save")).toHaveAttribute("data-clicks", "1"); const stale = await command("snapshot"); await page.locator("#save").evaluate((el) => { el.outerHTML = ''; }); expect( await command("act", { actions: [{ kind: "click", ref: ref(stale, "Save") }] }), ).toMatchObject({ ok: false, completed: 0 }); const refreshed = await command("snapshot"); expect(ref(refreshed, "Replacement")).not.toBe(ref(stale, "Save")); expect( await command("act", { actions: [ { kind: "click", ref: ref(refreshed, "Agree") }, { kind: "select", ref: ref(refreshed, "Plan"), text: "Pro Plan" }, { kind: "press", ref: ref(refreshed, "Name"), key: "Enter" }, { kind: "scroll", direction: "down", amount: 2 }, { kind: "wait", ms: 10 }, ], }), ).toMatchObject({ ok: true, completed: 5 }); await expect(page.getByRole("checkbox")).toBeChecked(); await expect(page.locator("#plan")).toHaveValue("pro"); expect( await command("navigate", { url: `http://127.0.0.1:${address.port}/next` }), ).toMatchObject({ ok: true, title: "Fixture" }); expect( await command("act", { actions: [{ kind: "click", ref: ref(refreshed, "Replacement") }] }), ).toMatchObject({ ok: false, completed: 0 }); expect(await command("navigate", { url: "file:///tmp/fixture" })).toMatchObject({ ok: false }); await page.route("http://insecure.test/", (route) => route.fulfill({ contentType: "text/html", body: "Insecure fixture", }), ); await page.goto("http://insecure.test/"); expect(await page.evaluate(() => window.isSecureContext)).toBe(false); expect(await command("snapshot")).toMatchObject({ ok: true, title: "Insecure fixture" }); await page.goto(`http://127.0.0.1:${address.port}`); const other = await context.newPage(); await other.setContent("Other tab"); await page.bringToFront(); // Headless Chromium marks both pages visible/focused. Ambiguity must fail safely. expect(await command("snapshot")).toMatchObject({ ok: false, fallback: "computer_act", error: expect.stringContaining("unambiguous"), }); await expect(page).toHaveTitle("Fixture"); await expect(other).toHaveTitle("Other tab"); // Reading an existing page neither creates nor switches tabs. expect(context.pages()).toHaveLength(2); } finally { await context?.close(); await rm(profile, { recursive: true, force: true }); server.closeAllConnections(); await new Promise((resolve) => server.close(() => resolve())); } });