Skip to content

Commit 88d28cc

Browse files
[fix]: make page.evaluate() use main world (#1331)
# why - currently, we add the init scripts to the main world - this is problematic, because `page.evaluate()` uses the `v3-world` - this means that you can't easily evaluate the script you add with `addInitScript()` # what changed - makes sure that `page.evaluate()` uses the main world # test plan - added a test which calls `page.evaluate()` on a script added via `context.addInitScript()` <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes a context mismatch: page.evaluate now runs in the main world so scripts injected via context.addInitScript are callable. Frame.evaluate was updated to use the main world too. ## Why: - evaluate() used the v3 isolated world while init scripts were in the main world, making injected globals inaccessible. ## What: - Switched page.evaluate and frame.evaluate to use the main-world execution context via executionContextRegistry. - Added a test verifying an init-script function is callable from page.evaluate. ## Test Plan: - [x] New test: init-script function is callable from page.evaluate (passes). - [x] Ran existing context.addInitScript tests to ensure no regressions. <sup>Written for commit d4bf59d. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
1 parent 6b5a3c9 commit 88d28cc

File tree

4 files changed

+47
-30
lines changed

4 files changed

+47
-30
lines changed

.changeset/seven-mice-draw.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
fix: page.evaluate() now works with scripts injected via context.addInitScript()

packages/core/lib/v3/tests/context-addInitScript.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,25 @@ test.describe("context.addInitScript", () => {
113113
});
114114
expect(observed).toEqual(payload);
115115
});
116+
117+
test("context.addInitScript installs a function callable from page.evaluate", async () => {
118+
const page = await ctx.awaitActivePage();
119+
120+
await ctx.addInitScript(() => {
121+
// installed before any navigation
122+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
123+
// @ts-expect-error
124+
window.sayHelloFromStagehand = () => "hello from stagehand";
125+
});
126+
127+
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
128+
129+
const result = await page.evaluate(() => {
130+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
131+
// @ts-expect-error
132+
return window.sayHelloFromStagehand();
133+
});
134+
135+
expect(result).toBe("hello from stagehand");
136+
});
116137
});

packages/core/lib/v3/understudy/frame.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Protocol } from "devtools-protocol";
33
import type { CDPSessionLike } from "./cdp";
44
import { Locator } from "./locator";
55
import { StagehandEvalError } from "../types/public/sdkErrors";
6+
import { executionContexts } from "./executionContextRegistry";
67

78
interface FrameManager {
89
session: CDPSessionLike;
@@ -116,7 +117,7 @@ export class Frame implements FrameManager {
116117
}
117118

118119
/**
119-
* Evaluate a function or expression in this frame's isolated world.
120+
* Evaluate a function or expression in this frame's main world.
120121
* - If a string is provided, treated as a JS expression.
121122
* - If a function is provided, it is stringified and invoked with the optional argument.
122123
*/
@@ -125,7 +126,7 @@ export class Frame implements FrameManager {
125126
arg?: Arg,
126127
): Promise<R> {
127128
await this.session.send("Runtime.enable").catch(() => {});
128-
const contextId = await this.getExecutionContextId();
129+
const contextId = await this.getMainWorldExecutionContextId();
129130

130131
const isString = typeof pageFunctionOrExpression === "string";
131132
let expression: string;
@@ -293,16 +294,8 @@ export class Frame implements FrameManager {
293294
return new Locator(this, selector, options);
294295
}
295296

296-
/** Create/get an isolated world for this frame and return its executionContextId */
297-
private async getExecutionContextId(): Promise<number> {
298-
await this.session.send("Page.enable");
299-
await this.session.send("Runtime.enable");
300-
const { executionContextId } = await this.session.send<{
301-
executionContextId: number;
302-
}>("Page.createIsolatedWorld", {
303-
frameId: this.frameId,
304-
worldName: "v3-world",
305-
});
306-
return executionContextId;
297+
/** Resolve the main-world execution context id for this frame. */
298+
private async getMainWorldExecutionContextId(): Promise<number> {
299+
return executionContexts.waitForMainWorld(this.session, this.frameId, 1000);
307300
}
308301
}

packages/core/lib/v3/understudy/page.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { FrameLocator } from "./frameLocator";
88
import { deepLocatorFromPage } from "./deepLocator";
99
import { resolveXpathForLocation } from "./a11y/snapshot";
1010
import { FrameRegistry } from "./frameRegistry";
11+
import { executionContexts } from "./executionContextRegistry";
1112
import { LoadState } from "../types/public/page";
1213
import { NetworkManager } from "./networkManager";
1314
import { LifecycleWatcher } from "./lifecycleWatcher";
@@ -132,7 +133,9 @@ export class Page {
132133
session: CDPSessionLike,
133134
source: string,
134135
): Promise<void> {
135-
await session.send("Page.addScriptToEvaluateOnNewDocument", { source });
136+
await session.send("Page.addScriptToEvaluateOnNewDocument", {
137+
source: source,
138+
});
136139
}
137140

138141
// Replay every previously registered init script onto a newly adopted session.
@@ -975,7 +978,7 @@ export class Page {
975978
async title(): Promise<string> {
976979
try {
977980
await this.mainSession.send("Runtime.enable").catch(() => {});
978-
const ctxId = await this.createIsolatedWorldForCurrentMain();
981+
const ctxId = await this.mainWorldExecutionContextId();
979982
const { result } =
980983
await this.mainSession.send<Protocol.Runtime.EvaluateResponse>(
981984
"Runtime.evaluate",
@@ -1157,7 +1160,7 @@ export class Page {
11571160
}
11581161

11591162
/**
1160-
* Evaluate a function or expression in the current main frame's isolated world.
1163+
* Evaluate a function or expression in the current main frame's main world.
11611164
* - If a string is provided, it is treated as a JS expression.
11621165
* - If a function is provided, it is stringified and invoked with the optional argument.
11631166
* - The return value should be JSON-serializable. Non-serializable objects will
@@ -1168,7 +1171,7 @@ export class Page {
11681171
arg?: Arg,
11691172
): Promise<R> {
11701173
await this.mainSession.send("Runtime.enable").catch(() => {});
1171-
const ctxId = await this.createIsolatedWorldForCurrentMain();
1174+
const ctxId = await this.mainWorldExecutionContextId();
11721175

11731176
const isString = typeof pageFunctionOrExpression === "string";
11741177
let expression: string;
@@ -1979,18 +1982,13 @@ export class Page {
19791982

19801983
// ---- Page-level lifecycle waiter that follows main frame id swaps ----
19811984

1982-
/**
1983-
* Create an isolated world for the **current** main frame and return its context id.
1984-
*/
1985-
private async createIsolatedWorldForCurrentMain(): Promise<number> {
1986-
await this.mainSession.send("Runtime.enable").catch(() => {});
1987-
const { executionContextId } = await this.mainSession.send<{
1988-
executionContextId: number;
1989-
}>("Page.createIsolatedWorld", {
1990-
frameId: this.mainFrameId(),
1991-
worldName: "v3-world",
1992-
});
1993-
return executionContextId;
1985+
/** Resolve the main-world execution context for the current main frame. */
1986+
private async mainWorldExecutionContextId(): Promise<number> {
1987+
return executionContexts.waitForMainWorld(
1988+
this.mainSession,
1989+
this.mainFrameId(),
1990+
1000,
1991+
);
19941992
}
19951993

19961994
/**
@@ -2009,7 +2007,7 @@ export class Page {
20092007

20102008
// Fast path: check the *current* main frame's readyState.
20112009
try {
2012-
const ctxId = await this.createIsolatedWorldForCurrentMain();
2010+
const ctxId = await this.mainWorldExecutionContextId();
20132011
const { result } =
20142012
await this.mainSession.send<Protocol.Runtime.EvaluateResponse>(
20152013
"Runtime.evaluate",

0 commit comments

Comments
 (0)