From 2211e19ffe6fa6668821879c31b858120f95fad9 Mon Sep 17 00:00:00 2001 From: miguel Date: Tue, 4 Nov 2025 16:47:33 -0800 Subject: [PATCH 1/2] Add page.sendCDP --- .changeset/early-brooms-draw.md | 5 +++++ packages/core/lib/v3/understudy/page.ts | 26 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 .changeset/early-brooms-draw.md diff --git a/.changeset/early-brooms-draw.md b/.changeset/early-brooms-draw.md new file mode 100644 index 000000000..af7b7ce04 --- /dev/null +++ b/.changeset/early-brooms-draw.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +Add a page.sendCDP method diff --git a/packages/core/lib/v3/understudy/page.ts b/packages/core/lib/v3/understudy/page.ts index a9dd0751e..95da31f08 100644 --- a/packages/core/lib/v3/understudy/page.ts +++ b/packages/core/lib/v3/understudy/page.ts @@ -478,6 +478,32 @@ export class Page { return this._targetId; } + /** + * Send a CDP command through the main session. + * Allows external consumers to execute arbitrary Chrome DevTools Protocol commands. + * + * @param method - The CDP method name (e.g., "Page.enable", "Runtime.evaluate") + * @param params - Optional parameters for the CDP command + * @returns Promise resolving to the typed CDP response + * + * @example + * // Enable the Runtime domain + * await page.sendCDP("Runtime.enable"); + * + * @example + * // Evaluate JavaScript with typed response + * const result = await page.sendCDP( + * "Runtime.evaluate", + * { expression: "1 + 1" } + * ); + */ + public async sendCDP( + method: string, + params?: object, + ): Promise { + return this.mainSession.send(method, params); + } + /** Seed the cached URL before navigation events converge. */ public seedCurrentUrl(url: string | undefined | null): void { if (!url) return; From e668c48f4feb679db091799076970daabb6b76f0 Mon Sep 17 00:00:00 2001 From: miguel Date: Tue, 4 Nov 2025 17:00:23 -0800 Subject: [PATCH 2/2] unit test --- .../core/lib/v3/tests/page-send-cdp.spec.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 packages/core/lib/v3/tests/page-send-cdp.spec.ts diff --git a/packages/core/lib/v3/tests/page-send-cdp.spec.ts b/packages/core/lib/v3/tests/page-send-cdp.spec.ts new file mode 100644 index 000000000..4affbc75d --- /dev/null +++ b/packages/core/lib/v3/tests/page-send-cdp.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Page sendCDP method", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch(() => {}); + }); + + test("sends CDP commands and requires domain to be enabled first", async () => { + const page = v3.context.pages()[0]; + await page.goto("https://example.com"); + + // Try to add a virtual authenticator without enabling WebAuthn first + // This should fail because the domain needs to be enabled + await expect( + page.sendCDP("WebAuthn.addVirtualAuthenticator", { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: false, + hasUserVerification: false, + isUserVerified: false, + }, + }), + ).rejects.toThrow(); + + // Enable the WebAuthn domain + await page.sendCDP("WebAuthn.enable"); + + // Now adding a virtual authenticator should succeed + const result = await page.sendCDP<{ authenticatorId: string }>( + "WebAuthn.addVirtualAuthenticator", + { + options: { + protocol: "ctap2", + transport: "usb", + hasResidentKey: false, + hasUserVerification: false, + isUserVerified: false, + }, + }, + ); + + // Verify we got an authenticator ID back + expect(result).toHaveProperty("authenticatorId"); + expect(typeof result.authenticatorId).toBe("string"); + expect(result.authenticatorId.length).toBeGreaterThan(0); + }); +});