import assert from "node:assert/strict"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { createServer } from "node:http"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test, { after } from "node:test"; import { executeActions } from "../src/actions.ts"; import { detectFrameProvider, frameAtPoint, frameWarning } from "../src/frames.ts"; import { launchTestBrowser } from "./helpers.ts"; // The runtime resolves PI_JEV_BROWSER_CONFIG at import time. const directory = mkdtempSync(join(tmpdir(), "pi-jev-browser-frames-")); const configPath = join(directory, "config.json"); writeFileSync( configPath, JSON.stringify({ outputDir: directory, recordVideo: false, headless: true, allowedOrigins: ["http://127.0.0.1:*"], }), ); process.env.PI_JEV_BROWSER_CONFIG = configPath; after(() => rmSync(directory, { recursive: true, force: true })); const FRAME_PAGE = `Frame page

Frame page

`; const INNER_PAGE = ` `; async function startServer() { return new Promise<{ url: string; close: () => Promise }>((resolve, reject) => { const server = createServer((request, response) => { response.setHeader("content-type", "text/html"); response.end(request.url?.startsWith("/inner") ? INNER_PAGE : FRAME_PAGE); }); server.once("error", reject); server.listen(0, "127.0.0.1", () => { server.off("error", reject); const address = server.address(); if (!address || typeof address === "string") return reject(new Error("no address")); resolve({ url: `http://127.0.0.1:${address.port}`, close: () => new Promise((done, fail) => server.close((error) => (error ? fail(error) : done())), ), }); }); }); } test("classifies known anti-bot frame providers", () => { assert.equal( detectFrameProvider("https://www.google.com/recaptcha/api2/anchor?k=x"), "reCAPTCHA", ); assert.equal(detectFrameProvider("https://newassets.hcaptcha.com/captcha/v1"), "hCaptcha"); assert.equal( detectFrameProvider("https://challenges.cloudflare.com/turnstile/v0/api.js"), "Cloudflare challenge", ); assert.equal(detectFrameProvider("https://example.com/widget"), undefined); assert.equal(detectFrameProvider("about:blank"), undefined); assert.equal(detectFrameProvider(""), undefined); }); test("frame warnings name the origin, position and provider", () => { const warning = frameWarning("click", 100.4, 160.6, { origin: "https://www.google.com", src: "https://www.google.com/recaptcha/api2/anchor", provider: "reCAPTCHA", }); assert.match(warning, /click at \(100, 161\)/); assert.match(warning, /https:\/\/www\.google\.com/); assert.match(warning, /\[reCAPTCHA\]/); assert.match(warning, /raw coordinate click/); }); test("coordinate clicks over a frame are reported, main-frame clicks are not", async () => { const server = await startServer(); const browser = await launchTestBrowser(); try { const page = await browser.newPage(); await page.goto(server.url, { waitUntil: "domcontentloaded" }); // Detection: the point over the iframe resolves to the iframe element. const overFrame = await frameAtPoint(page, 100, 160); assert.ok(overFrame, "expected a frame hit over the iframe"); assert.equal(overFrame.origin, server.url); assert.equal(await frameAtPoint(page, 100, 30), undefined); const hits: string[] = []; await executeActions(page, [{ type: "click", x: 100, y: 160 }], { assertUrlAllowed: () => undefined, onFrameHit: (info) => hits.push(info.warning), }); assert.equal(hits.length, 1); assert.match(hits[0], new RegExp(server.url.replace(/[.:]/g, "\\$&"))); // The click really did land in the frame. const inner = page.frames().find((frame) => frame.url().includes("/inner")); assert.ok(inner, "expected the inner frame"); assert.equal( await inner.evaluate( () => Boolean((window as unknown as { __innerClicked?: boolean }).__innerClicked), ), true, ); hits.length = 0; await executeActions(page, [{ type: "click", x: 100, y: 30 }], { assertUrlAllowed: () => undefined, onFrameHit: (info) => hits.push(info.warning), }); assert.deepEqual(hits, [], "a main-frame click must not warn"); } finally { await browser.close(); await server.close(); } }); test("jev_actions surfaces frame warnings and logs them", async () => { const server = await startServer(); const { PiBrowserManager } = await import("../src/runtime.ts"); const manager = new PiBrowserManager(); const host = { sessionId: "frames-test" }; // A stub policy keeps this test offline; no Jev decision is needed here. const policy = { async choose() { return { operation: "DONE" }; }, async text() { throw new Error("Unexpected helper"); }, }; try { // Clicking before a browser exists must fail cleanly rather than warn. await assert.rejects( manager.actions( { actions: [{ type: "click", x: 100, y: 160 }], includeScreenshot: false }, host, ), /No browser is active/, ); await manager.run( { url: server.url, goal: "Report the page heading and stop." }, host, policy, ); const result = await manager.actions( { actions: [{ type: "click", x: 100, y: 160 }], includeScreenshot: false }, host, ); assert.ok("warnings" in result && result.warnings, "expected frame warnings"); assert.equal(result.warnings?.length, 1); assert.match(String(result.warnings?.[0]), /landed inside a frame/); const logs = manager.logs({}, host); const security = logs.logs.filter( (entry) => entry.type === "security" && entry.level === "warning", ); assert.equal(security.length, 1); assert.match(security[0].text, /landed inside a frame/); } finally { await manager.stop(host); await server.close(); } });