Skip to content

Commit 207244e

Browse files
authored
support popups (#374)
* more descriptive errors * ctx-level new page detection * improve nav performance * remove new tab log * remove unused imports * changeset * page listener returns StagehandPage * add popup example * clean imports * override on(popup) * add guiding comment * update err message * update tests & add act page handler back * fix non popup events * enhance on() tests * Update few-elephants-cough.md * err msg renaming
1 parent ae301f1 commit 207244e

File tree

8 files changed

+245
-11
lines changed

8 files changed

+245
-11
lines changed

Diff for: .changeset/few-elephants-cough.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
Pass in a Stagehand Page object into the `on("popup")` listener to allow for multi-page handling.

Diff for: .gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ evals/public
1616
evals/playground.ts
1717
tmp/
1818
eval-summary.json
19-
pnpm-lock.yaml
19+
pnpm-lock.yaml
20+
evals/deterministic/tests/BrowserContext/tmp-test.har

Diff for: evals/deterministic/tests/page/on.test.ts

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { expect, test } from "@playwright/test";
2+
import { Stagehand } from "../../../../lib";
3+
import StagehandConfig from "../../stagehand.config";
4+
5+
test.describe("StagehandPage - page.on()", () => {
6+
test("should click on the crewAI blog tab", async () => {
7+
const stagehand = new Stagehand(StagehandConfig);
8+
await stagehand.init();
9+
10+
const page = stagehand.page;
11+
await page.goto(
12+
"https://docs.browserbase.com/integrations/crew-ai/introduction",
13+
);
14+
15+
let clickPromise: Promise<void>;
16+
17+
page.on("popup", async (newPage) => {
18+
clickPromise = newPage.click(
19+
"body > div.page-wrapper > div.navbar-2.w-nav > div.padding-global.top-bot > div > div.navigation-left > nav > a:nth-child(7)",
20+
);
21+
});
22+
23+
await page.goto(
24+
"https://docs.browserbase.com/integrations/crew-ai/introduction",
25+
);
26+
27+
await page.click(
28+
"#content-area > div.relative.mt-8.prose.prose-gray.dark\\:prose-invert > p:nth-child(2) > a",
29+
);
30+
31+
await clickPromise;
32+
33+
await stagehand.close();
34+
});
35+
36+
test("should close the new tab and navigate to it on the existing page", async () => {
37+
const stagehand = new Stagehand(StagehandConfig);
38+
await stagehand.init();
39+
40+
const page = stagehand.page;
41+
await page.goto(
42+
"https://docs.browserbase.com/integrations/crew-ai/introduction",
43+
);
44+
45+
let navigatePromise: Promise<unknown>;
46+
47+
page.on("popup", async (newPage) => {
48+
navigatePromise = Promise.allSettled([
49+
newPage.close(),
50+
page.goto(newPage.url(), { waitUntil: "domcontentloaded" }),
51+
]);
52+
});
53+
54+
// Click on the crewAI blog tab
55+
await page.click(
56+
"#content-area > div.relative.mt-8.prose.prose-gray.dark\\:prose-invert > p:nth-child(2) > a",
57+
);
58+
59+
await navigatePromise;
60+
61+
await page.click(
62+
"body > div.page-wrapper > div.navbar-2.w-nav > div.padding-global.top-bot > div > div.navigation-left > nav > a:nth-child(3)",
63+
);
64+
65+
await page.waitForLoadState("domcontentloaded");
66+
67+
const currentUrl = page.url();
68+
expect(currentUrl).toBe("https://www.crewai.com/open-source");
69+
70+
await stagehand.close();
71+
});
72+
73+
test("should handle console events", async () => {
74+
const stagehand = new Stagehand(StagehandConfig);
75+
await stagehand.init();
76+
77+
const page = stagehand.page;
78+
await page.goto("https://example.com");
79+
80+
const messages: string[] = [];
81+
page.on("console", (msg) => {
82+
messages.push(msg.text());
83+
});
84+
85+
await page.evaluate(() => console.log("Test console log"));
86+
87+
expect(messages).toContain("Test console log");
88+
89+
await stagehand.close();
90+
});
91+
92+
test("should handle dialog events", async () => {
93+
const stagehand = new Stagehand(StagehandConfig);
94+
await stagehand.init();
95+
96+
const page = stagehand.page;
97+
await page.goto("https://example.com");
98+
99+
page.on("dialog", async (dialog) => {
100+
expect(dialog.message()).toBe("Test alert");
101+
await dialog.dismiss();
102+
});
103+
104+
await page.evaluate(() => alert("Test alert"));
105+
106+
await stagehand.close();
107+
});
108+
109+
test("should handle request and response events", async () => {
110+
const stagehand = new Stagehand(StagehandConfig);
111+
await stagehand.init();
112+
113+
const page = stagehand.page;
114+
await page.goto("https://example.com");
115+
116+
const requests: string[] = [];
117+
const responses: string[] = [];
118+
119+
page.on("request", (request) => {
120+
requests.push(request.url());
121+
});
122+
123+
page.on("response", (response) => {
124+
responses.push(response.url());
125+
});
126+
127+
await page.goto("https://example.com");
128+
129+
expect(requests).toContain("https://example.com/");
130+
expect(responses).toContain("https://example.com/");
131+
132+
await stagehand.close();
133+
});
134+
});

Diff for: examples/popup.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* This file is meant to be used as a scratchpad for developing new evals.
3+
* To create a Stagehand project with best practices and configuration, run:
4+
*
5+
* npx create-browser-app@latest my-browser-app
6+
*/
7+
8+
import { ObserveResult, Stagehand } from "../lib";
9+
import StagehandConfig from "./stagehand.config";
10+
11+
async function example() {
12+
const stagehand = new Stagehand(StagehandConfig);
13+
await stagehand.init();
14+
15+
const page = await stagehand.page;
16+
17+
let observePromise: Promise<ObserveResult[]>;
18+
19+
page.on("popup", async (newPage) => {
20+
observePromise = newPage.observe({
21+
instruction: "return all the next possible actions from the page",
22+
});
23+
});
24+
25+
await page.goto(
26+
"https://docs.browserbase.com/integrations/crew-ai/introduction",
27+
);
28+
29+
await page.click(
30+
"#content-area > div.relative.mt-8.prose.prose-gray.dark\\:prose-invert > p:nth-child(2) > a",
31+
);
32+
33+
await page.waitForTimeout(5000);
34+
35+
if (observePromise) {
36+
const observeResult = await observePromise;
37+
38+
console.log("Observed", observeResult.length, "actions");
39+
}
40+
41+
await stagehand.close();
42+
}
43+
44+
(async () => {
45+
await example();
46+
})();

Diff for: lib/StagehandPage.ts

+43-3
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,24 @@ export class StagehandPage {
3434
) {
3535
this.intPage = Object.assign(page, {
3636
act: () => {
37-
throw new Error("act() is not implemented on the base page object");
37+
throw new Error(
38+
"You seem to be calling `act` on a page in an uninitialized `Stagehand` object. Ensure you are running `await stagehand.init()` on the Stagehand object before referencing the `page` object.",
39+
);
3840
},
3941
extract: () => {
40-
throw new Error("extract() is not implemented on the base page object");
42+
throw new Error(
43+
"You seem to be calling `extract` on a page in an uninitialized `Stagehand` object. Ensure you are running `await stagehand.init()` on the Stagehand object before referencing the `page` object.",
44+
);
4145
},
4246
observe: () => {
43-
throw new Error("observe() is not implemented on the base page object");
47+
throw new Error(
48+
"You seem to be calling `observe` on a page in an uninitialized `Stagehand` object. Ensure you are running `await stagehand.init()` on the Stagehand object before referencing the `page` object.",
49+
);
50+
},
51+
on: () => {
52+
throw new Error(
53+
"You seem to be referencing a page in an uninitialized `Stagehand` object. Ensure you are running `await stagehand.init()` on the Stagehand object before referencing the `page` object.",
54+
);
4455
},
4556
});
4657
this.stagehand = stagehand;
@@ -105,9 +116,38 @@ export class StagehandPage {
105116
};
106117
}
107118

119+
if (prop === "on") {
120+
return (event: string, listener: (param: unknown) => void) => {
121+
if (event === "popup") {
122+
return this.context.on("page", async (page) => {
123+
const newContext = await StagehandContext.init(
124+
page.context(),
125+
stagehand,
126+
);
127+
const newStagehandPage = new StagehandPage(
128+
page,
129+
stagehand,
130+
newContext,
131+
this.llmClient,
132+
);
133+
134+
await newStagehandPage.init();
135+
136+
listener(newStagehandPage.page);
137+
});
138+
}
139+
140+
return this.context.on(
141+
event as keyof PlaywrightPage["on"],
142+
listener,
143+
);
144+
};
145+
}
146+
108147
return target[prop as keyof PlaywrightPage];
109148
},
110149
});
150+
111151
await this._waitForSettledDom();
112152
return this;
113153
}

Diff for: lib/handlers/actHandler.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ import { ActionCache } from "../cache/ActionCache";
88
import { act, fillInVariables, verifyActCompletion } from "../inference";
99
import { LLMClient } from "../llm/LLMClient";
1010
import { LLMProvider } from "../llm/LLMProvider";
11+
import { StagehandContext } from "../StagehandContext";
12+
import { StagehandPage } from "../StagehandPage";
1113
import { generateId } from "../utils";
1214
import { ScreenshotService } from "../vision";
13-
import { StagehandPage } from "../StagehandPage";
14-
import { StagehandContext } from "../StagehandContext";
1515

1616
export class StagehandActHandler {
1717
private readonly stagehandPage: StagehandPage;

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"module": "./dist/index.js",
77
"types": "./dist/index.d.ts",
88
"scripts": {
9+
"popup": "npm run build-dom-scripts && tsx examples/popup.ts",
910
"2048": "npm run build-dom-scripts && tsx examples/2048.ts",
1011
"example": "npm run build-dom-scripts && tsx examples/example.ts",
1112
"debug-url": "npm run build-dom-scripts && tsx examples/debugUrl.ts",

Diff for: types/page.ts

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import type { Page as PlaywrightPage } from "@playwright/test";
2-
import type { BrowserContext as PlaywrightContext } from "@playwright/test";
3-
import type { Browser as PlaywrightBrowser } from "@playwright/test";
1+
import type {
2+
Browser as PlaywrightBrowser,
3+
BrowserContext as PlaywrightContext,
4+
Page as PlaywrightPage,
5+
} from "@playwright/test";
6+
import type { z } from "zod";
47
import type {
58
ActOptions,
69
ActResult,
@@ -9,13 +12,17 @@ import type {
912
ObserveOptions,
1013
ObserveResult,
1114
} from "./stagehand";
12-
import type { z } from "zod";
13-
export interface Page extends PlaywrightPage {
15+
16+
export interface Page extends Omit<PlaywrightPage, "on"> {
1417
act: (options: ActOptions) => Promise<ActResult>;
1518
extract: <T extends z.AnyZodObject>(
1619
options: ExtractOptions<T>,
1720
) => Promise<ExtractResult<T>>;
1821
observe: (options?: ObserveOptions) => Promise<ObserveResult[]>;
22+
23+
on: {
24+
(event: "popup", listener: (page: Page) => unknown): Page;
25+
} & PlaywrightPage["on"];
1926
}
2027

2128
// Empty type for now, but will be used in the future

0 commit comments

Comments
 (0)