Skip to content

proxy page.evaluate() #688

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/empty-bugs-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

wrap page.evaluate to make sure we have injected browser side scripts before calling them
28 changes: 28 additions & 0 deletions evals/deterministic/tests/page/addInitScript.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
51 changes: 50 additions & 1 deletion lib/StagehandPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) => {
Expand Down Expand Up @@ -117,6 +120,34 @@ export class StagehandPage {
}
}

private async ensureStagehandScript(): Promise<void> {
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" },
},
});
}
}

private async _refreshPageFromAPI() {
if (!this.api) return;

Expand Down Expand Up @@ -217,14 +248,32 @@ export class StagehandPage {

async init(): Promise<StagehandPage> {
try {
const page = this.intPage;
const page = this.rawPage;
const stagehand = this.stagehand;

// Create a proxy that updates active page on method calls
const handler = {
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 before the user’s evaluation
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) {
Expand Down
1 change: 1 addition & 0 deletions lib/dom/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StagehandContainer } from "./StagehandContainer";
export {};
declare global {
interface Window {
__stagehandInjected?: boolean;
chunkNumber: number;
showChunks?: boolean;
processDom: (chunksSeen: Array<number>) => Promise<{
Expand Down
8 changes: 7 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading