diff --git a/.changeset/empty-bugs-occur.md b/.changeset/empty-bugs-occur.md new file mode 100644 index 00000000..5eecb548 --- /dev/null +++ b/.changeset/empty-bugs-occur.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +wrap page.evaluate to make sure we have injected browser side scripts before calling them diff --git a/evals/deterministic/tests/page/addInitScript.test.ts b/evals/deterministic/tests/page/addInitScript.test.ts index 56be8ef4..835b12be 100644 --- a/evals/deterministic/tests/page/addInitScript.test.ts +++ b/evals/deterministic/tests/page/addInitScript.test.ts @@ -37,4 +37,32 @@ test.describe("StagehandPage - addInitScript", () => { await stagehand.close(); }); + + test("checks if init scripts are re-added and available even if they've been deleted", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto( + "https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/", + ); + + // delete the __stagehandInjected flag, and delete the + // getScrollableElementXpaths function + await page.evaluate(() => { + delete window.getScrollableElementXpaths; + delete window.__stagehandInjected; + }); + + // attempt to call the getScrollableElementXpaths function + // which we previously deleted. page.evaluate should realize + // its been deleted and re-inject it + const xpaths = await page.evaluate(() => { + return window.getScrollableElementXpaths(); + }); + + await stagehand.close(); + // this is the only scrollable element on the page + expect(xpaths).toContain("/html"); + }); }); diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 6d2aad42..557c95cf 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -31,9 +31,11 @@ import { StagehandDefaultError, } from "../types/stagehandErrors"; import { StagehandAPIError } from "@/types/stagehandApiErrors"; +import { scriptContent } from "@/lib/dom/build/scriptContent"; export class StagehandPage { private stagehand: Stagehand; + private rawPage: PlaywrightPage; private intPage: Page; private intContext: StagehandContext; private actHandler: StagehandActHandler; @@ -60,6 +62,7 @@ export class StagehandPage { api?: StagehandAPI, waitForCaptchaSolves?: boolean, ) { + this.rawPage = page; // Create a proxy to intercept all method calls and property access this.intPage = new Proxy(page, { get: (target: PlaywrightPage, prop: keyof PlaywrightPage) => { @@ -117,6 +120,35 @@ export class StagehandPage { } } + private async ensureStagehandScript(): Promise { + try { + const injected = await this.rawPage.evaluate( + () => !!window.__stagehandInjected, + ); + + if (injected) return; + + const guardedScript = `if (!window.__stagehandInjected) { \ +window.__stagehandInjected = true; \ +${scriptContent} \ +}`; + + await this.rawPage.addInitScript({ content: guardedScript }); + await this.rawPage.evaluate(guardedScript); + } catch (err) { + this.stagehand.log({ + category: "dom", + message: "Failed to inject Stagehand helper script", + level: 1, + auxiliary: { + error: { value: (err as Error).message, type: "string" }, + trace: { value: (err as Error).stack, type: "string" }, + }, + }); + throw err; + } + } + private async _refreshPageFromAPI() { if (!this.api) return; @@ -217,7 +249,7 @@ export class StagehandPage { async init(): Promise { try { - const page = this.intPage; + const page = this.rawPage; const stagehand = this.stagehand; // Create a proxy that updates active page on method calls @@ -225,6 +257,24 @@ export class StagehandPage { get: (target: PlaywrightPage, prop: string | symbol) => { const value = target[prop as keyof PlaywrightPage]; + // Inject-on-demand for evaluate + if ( + prop === "evaluate" || + prop === "evaluateHandle" || + prop === "$eval" || + prop === "$$eval" + ) { + return async (...args: unknown[]) => { + this.intContext.setActivePage(this); + // Make sure helpers exist + await this.ensureStagehandScript(); + return (value as (...a: unknown[]) => unknown).apply( + target, + args, + ); + }; + } + // Handle enhanced methods if (prop === "act" || prop === "extract" || prop === "observe") { if (!this.llmClient) { diff --git a/lib/dom/global.d.ts b/lib/dom/global.d.ts index 742c1543..aff82fa2 100644 --- a/lib/dom/global.d.ts +++ b/lib/dom/global.d.ts @@ -3,6 +3,7 @@ import { StagehandContainer } from "./StagehandContainer"; export {}; declare global { interface Window { + __stagehandInjected?: boolean; chunkNumber: number; showChunks?: boolean; processDom: (chunksSeen: Array) => Promise<{ diff --git a/lib/index.ts b/lib/index.ts index a12b63a7..735647fb 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -715,8 +715,14 @@ export class Stagehand { await this.page.setViewportSize({ width: 1280, height: 720 }); } + const guardedScript = ` + if (!window.__stagehandInjected) { + window.__stagehandInjected = true; + ${scriptContent} + } +`; await this.context.addInitScript({ - content: scriptContent, + content: guardedScript, }); this.browserbaseSessionID = sessionId;