From 0d50c6b49aa963386be4f5f6308a58319f9bb9c3 Mon Sep 17 00:00:00 2001 From: RedBotster <258521541+redbotster@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:06:04 -0700 Subject: [PATCH] test: real Puppeteer/Playwright connect through the bridge (xfail for now) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit handshake.test.ts drives a synthetic client, so it stays green while a stock client still cannot open a page. This drives the real ones (puppeteer-core, playwright-core): connect -> newPage -> goto. It is the acceptance test for the handshake answerer (issue #2). Marked it.fails on purpose. As of the answerer landing, a stock client gets past Browser.getVersion and then blocks: with the gate enforced it is refused on the next method it sends (Target.getBrowserContexts for Puppeteer, Browser.setDownloadBehavior for Playwright); even with every method allowed, newPage() never resolves, because the proxy does not synthesise the target-lifecycle events the client waits for to build its Page. When the answerer presents a coherent target lifecycle for a client's own pages, the body passes, it.fails turns red, and that is the signal to drop the .fails. The framework clients are not added as dependencies — a smoke test should not pull Playwright's install into everyone's CI. The test skips unless they are present; run it with `pnpm add -D -w puppeteer-core playwright-core` then `pnpm test framework-connect`. Verified both paths locally: skips clean without them, passes as xfail with them. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01WqPU5z6K3maS9J6LrcsEic --- .../src/framework-connect.test.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 packages/browser-bridge/src/framework-connect.test.ts diff --git a/packages/browser-bridge/src/framework-connect.test.ts b/packages/browser-bridge/src/framework-connect.test.ts new file mode 100644 index 0000000..50596d7 --- /dev/null +++ b/packages/browser-bridge/src/framework-connect.test.ts @@ -0,0 +1,133 @@ +// Copyright (C) 2026 1Claw +// SPDX-License-Identifier: Apache-2.0 + +/** + * Can a stock framework client actually drive the bridge? + * + * The README tells people to point their framework's `cdp_url` at the URL the + * bridge prints. `handshake.test.ts` proves the proxy answers `Browser.getVersion` + * the way those clients expect, but it uses a synthetic client — so it stays + * green while a real Puppeteer or Playwright still cannot open a page. This is + * the test that drives the real thing, connect -> newPage -> goto, so the claim + * is checked against the clients people actually bring (Puppeteer, Playwright, + * and by extension the Playwright-based agents browser-use and Stagehand). + * + * These are `it.fails` on purpose. As of the handshake answerer landing + * (issue #2), a stock client gets past `Browser.getVersion` and then blocks: + * with the gate enforced it is refused on the next method it sends + * (`Target.getBrowserContexts` for Puppeteer, `Browser.setDownloadBehavior` for + * Playwright); even with every method allowed, `newPage()` never resolves, + * because the proxy does not synthesise the target-lifecycle events + * (`targetCreated` / `attachedToTarget` / `targetInfoChanged`) the client waits + * for to build its Page. The body below is the real acceptance test; when the + * answerer presents a coherent target lifecycle for a client's own pages, this + * starts passing, `it.fails` turns it red, and that is the signal to drop the + * `.fails`. + * + * The framework clients are NOT dependencies of this package — a smoke test + * should not pull Playwright's install into everyone's CI. The test skips + * unless they are present, so to run it locally: + * + * pnpm add -D -w puppeteer-core playwright-core + * pnpm test framework-connect + * + * and point at a Chromium with ONECLAW_BRIDGE_CHROME if it is not at the + * default path. + */ +import { existsSync } from "node:fs"; +import { createServer, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { startBridge, type BridgeHandle } from "./bridge.js"; +import { MockVaultDriver } from "./drivers/mock.js"; + +const CHROME = + process.env.ONECLAW_BRIDGE_CHROME ?? + { + darwin: "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + linux: "/usr/bin/google-chrome", + }[process.platform as "darwin" | "linux"] ?? + ""; +const HAVE_CHROME = CHROME !== "" && existsSync(CHROME); +const LAUNCH_ARGS = [ + "--headless=new", + ...(process.env.CI && process.platform === "linux" + ? ["--no-sandbox", "--disable-dev-shm-usage"] + : []), +]; + +/** + * Import an optional peer by a name the type checker cannot resolve, so a + * missing framework is a skipped test rather than a broken build. + */ +async function optional(name: string): Promise { + try { + return await import(/* @vite-ignore */ name); + } catch { + return null; + } +} + +const puppeteer = await optional("puppeteer-core"); +const playwright = await optional("playwright-core"); + +let server: Server; +let origin = ""; + +beforeAll(async () => { + server = createServer((_req, res) => { + res.writeHead(200, { "content-type": "text/html" }); + res.end("ok

ok

"); + }); + await new Promise((r) => server.listen(0, "127.0.0.1", r)); + origin = `http://127.0.0.1:${(server.address() as AddressInfo).port}/`; +}); + +afterAll(() => new Promise((r) => server.close(() => r()))); + +async function bridge(): Promise { + const backend = new MockVaultDriver({ + bindings: [{ id: "x", secret: "p", loginUrl: origin, allowedHosts: ["127.0.0.1"] }], + }); + return startBridge({ executablePath: CHROME, backend, host: "127.0.0.1", port: 0, args: LAUNCH_ARGS }); +} + +const runPuppeteer = HAVE_CHROME && puppeteer ? it.fails : it.skip; +const runPlaywright = HAVE_CHROME && playwright ? it.fails : it.skip; + +describe("a stock framework client can drive the bridge", () => { + runPuppeteer( + "puppeteer-core connects, opens a page, and navigates", + async () => { + const b = await bridge(); + try { + const browser = await puppeteer.connect({ browserWSEndpoint: b.url }); + const page = await browser.newPage(); + await page.goto(origin); + expect(await page.title()).toBe("ok"); + await browser.disconnect(); + } finally { + await b.close(); + } + }, + 40_000, + ); + + runPlaywright( + "playwright-core connects, opens a page, and navigates", + async () => { + const b = await bridge(); + try { + const browser = await playwright.chromium.connectOverCDP(b.url); + const ctx = browser.contexts()[0] ?? (await browser.newContext()); + const page = await ctx.newPage(); + await page.goto(origin); + expect(await page.title()).toBe("ok"); + await browser.close(); + } finally { + await b.close(); + } + }, + 40_000, + ); +});