Skip to content

Commit 3a0dc58

Browse files
authored
add playwright screenshot option for browserbase env (#1070)
# why Currently, using playwright screenshot command is not available when the execution environment is Stagehand. A customer has indicated they would prefer to use Playwright's native screenshot command instead of CDP when using Browserbase as CDP screenshot causes unexpected behavior for their target site. # what changed - added a StagehandScreenshotOptions type with useCDP argument added - extended page type to accept custom stagehand screeenshot options - update screenshot proxy to default useCDP to true if the env is browserbase and use playwright screenshot if false - added eval for screenshot with and without cdp # test plan - tested and confirmed functionality with eval and external example script (not committed)
1 parent 7f38b3a commit 3a0dc58

File tree

4 files changed

+291
-30
lines changed

4 files changed

+291
-30
lines changed

evals/evals.config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,12 @@
839839
"categories": [
840840
"external_agent_benchmarks"
841841
]
842+
},
843+
{
844+
"name": "screenshot_cdp_toggle",
845+
"categories": [
846+
"regression"
847+
]
842848
}
843849
]
844850
}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
/**
4+
* Test the useCDP flag for screenshot functionality in Browserbase environments.
5+
* This test verifies that:
6+
* 1. Screenshots work with CDP (useCDP: true)
7+
* 2. Screenshots work with Playwright fallback (useCDP: false)
8+
* 3. Options are properly passed through in both modes
9+
*/
10+
export const screenshot_cdp_toggle: EvalFunction = async ({
11+
debugUrl,
12+
sessionUrl,
13+
stagehand,
14+
logger,
15+
}) => {
16+
try {
17+
// Navigate to a test page
18+
await stagehand.page.goto("https://example.com");
19+
20+
logger.log({
21+
message: "Testing screenshot with CDP enabled",
22+
level: 1,
23+
});
24+
25+
// Test 1: Screenshot with CDP
26+
const cdpScreenshot = await stagehand.page.screenshot({
27+
fullPage: true,
28+
useCDP: true,
29+
});
30+
31+
if (!cdpScreenshot || cdpScreenshot.length === 0) {
32+
logger.error({
33+
message: "CDP screenshot failed",
34+
level: 0,
35+
auxiliary: {
36+
size: {
37+
value: cdpScreenshot ? cdpScreenshot.length.toString() : "null",
38+
type: "string",
39+
},
40+
},
41+
});
42+
return {
43+
_success: false,
44+
error: "CDP screenshot produced empty result",
45+
debugUrl,
46+
sessionUrl,
47+
logs: logger.getLogs(),
48+
};
49+
}
50+
51+
logger.log({
52+
message: `CDP screenshot successful: ${cdpScreenshot.length} bytes`,
53+
level: 1,
54+
});
55+
56+
logger.log({
57+
message: "Testing screenshot with Playwright (CDP disabled)",
58+
level: 1,
59+
});
60+
61+
// Test 2: Screenshot with Playwright
62+
const playwrightScreenshot = await stagehand.page.screenshot({
63+
fullPage: true,
64+
useCDP: false,
65+
});
66+
67+
if (!playwrightScreenshot || playwrightScreenshot.length === 0) {
68+
logger.error({
69+
message: "Playwright screenshot failed",
70+
level: 0,
71+
auxiliary: {
72+
size: {
73+
value: playwrightScreenshot
74+
? playwrightScreenshot.length.toString()
75+
: "null",
76+
type: "string",
77+
},
78+
},
79+
});
80+
return {
81+
_success: false,
82+
error: "Playwright screenshot produced empty result",
83+
debugUrl,
84+
sessionUrl,
85+
logs: logger.getLogs(),
86+
};
87+
}
88+
89+
logger.log({
90+
message: `Playwright screenshot successful: ${playwrightScreenshot.length} bytes`,
91+
level: 1,
92+
});
93+
94+
// Test 3: Test with additional options (JPEG format)
95+
logger.log({
96+
message: "Testing screenshot with JPEG format and quality settings",
97+
level: 1,
98+
});
99+
100+
const jpegScreenshot = await stagehand.page.screenshot({
101+
type: "jpeg",
102+
quality: 80,
103+
useCDP: false,
104+
});
105+
106+
if (!jpegScreenshot || jpegScreenshot.length === 0) {
107+
logger.error({
108+
message: "JPEG screenshot failed",
109+
level: 0,
110+
});
111+
return {
112+
_success: false,
113+
error: "JPEG screenshot produced empty result",
114+
debugUrl,
115+
sessionUrl,
116+
logs: logger.getLogs(),
117+
};
118+
}
119+
120+
logger.log({
121+
message: `JPEG screenshot successful: ${jpegScreenshot.length} bytes`,
122+
level: 1,
123+
});
124+
125+
// Test 4: Test with clip option
126+
logger.log({
127+
message: "Testing screenshot with clip region",
128+
level: 1,
129+
});
130+
131+
const clippedScreenshot = await stagehand.page.screenshot({
132+
clip: { x: 0, y: 0, width: 500, height: 300 },
133+
useCDP: true,
134+
});
135+
136+
if (!clippedScreenshot || clippedScreenshot.length === 0) {
137+
logger.error({
138+
message: "Clipped screenshot failed",
139+
level: 0,
140+
});
141+
return {
142+
_success: false,
143+
error: "Clipped screenshot produced empty result",
144+
debugUrl,
145+
sessionUrl,
146+
logs: logger.getLogs(),
147+
};
148+
}
149+
150+
// Verify clipped screenshot is smaller than full page
151+
if (clippedScreenshot.length >= cdpScreenshot.length) {
152+
logger.error({
153+
message: "Clipped screenshot is not smaller than full screenshot",
154+
level: 0,
155+
auxiliary: {
156+
clipped_size: {
157+
value: clippedScreenshot.length.toString(),
158+
type: "integer",
159+
},
160+
full_size: {
161+
value: cdpScreenshot.length.toString(),
162+
type: "integer",
163+
},
164+
},
165+
});
166+
return {
167+
_success: false,
168+
error: "Clipped screenshot size validation failed",
169+
debugUrl,
170+
sessionUrl,
171+
logs: logger.getLogs(),
172+
};
173+
}
174+
175+
logger.log({
176+
message: `Clipped screenshot successful: ${clippedScreenshot.length} bytes`,
177+
level: 1,
178+
});
179+
180+
logger.log({
181+
message: "All screenshot tests passed successfully",
182+
level: 0,
183+
auxiliary: {
184+
cdp_size: {
185+
value: cdpScreenshot.length.toString(),
186+
type: "integer",
187+
},
188+
playwright_size: {
189+
value: playwrightScreenshot.length.toString(),
190+
type: "integer",
191+
},
192+
jpeg_size: {
193+
value: jpegScreenshot.length.toString(),
194+
type: "integer",
195+
},
196+
clipped_size: {
197+
value: clippedScreenshot.length.toString(),
198+
type: "integer",
199+
},
200+
},
201+
});
202+
203+
return {
204+
_success: true,
205+
cdpSize: cdpScreenshot.length,
206+
playwrightSize: playwrightScreenshot.length,
207+
jpegSize: jpegScreenshot.length,
208+
clippedSize: clippedScreenshot.length,
209+
debugUrl,
210+
sessionUrl,
211+
logs: logger.getLogs(),
212+
};
213+
} catch (error) {
214+
logger.error({
215+
message: "Screenshot CDP toggle test failed",
216+
level: 0,
217+
auxiliary: {
218+
error: {
219+
value: error.message || String(error),
220+
type: "string",
221+
},
222+
stack: {
223+
value: error.stack || "",
224+
type: "string",
225+
},
226+
},
227+
});
228+
229+
return {
230+
_success: false,
231+
error: error.message || String(error),
232+
debugUrl,
233+
sessionUrl,
234+
logs: logger.getLogs(),
235+
};
236+
} finally {
237+
await stagehand.close();
238+
}
239+
};

lib/StagehandPage.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import type { CDPSession, Page as PlaywrightPage, Frame } from "playwright";
22
import { selectors } from "playwright";
33
import { z } from "zod/v3";
4-
import { Page, defaultExtractSchema } from "../types/page";
4+
import {
5+
Page,
6+
defaultExtractSchema,
7+
StagehandScreenshotOptions,
8+
} from "../types/page";
59
import {
610
ExtractOptions,
711
ExtractResult,
@@ -415,37 +419,41 @@ ${scriptContent} \
415419
}
416420

417421
// Handle screenshots with CDP
418-
if (prop === "screenshot" && this.stagehand.env === "BROWSERBASE") {
419-
return async (
420-
options: {
421-
type?: "png" | "jpeg";
422-
quality?: number;
423-
fullPage?: boolean;
424-
clip?: { x: number; y: number; width: number; height: number };
425-
omitBackground?: boolean;
426-
} = {},
427-
) => {
428-
const cdpOptions: Record<string, unknown> = {
429-
format: options.type === "jpeg" ? "jpeg" : "png",
430-
quality: options.quality,
431-
clip: options.clip,
432-
omitBackground: options.omitBackground,
433-
fromSurface: true,
434-
};
435-
436-
if (options.fullPage) {
437-
cdpOptions.captureBeyondViewport = true;
438-
}
422+
if (prop === "screenshot") {
423+
return async (options: StagehandScreenshotOptions = {}) => {
424+
const rawScreenshot: typeof target.screenshot =
425+
Object.getPrototypeOf(target).screenshot.bind(target);
426+
427+
const {
428+
useCDP = this.stagehand.env === "BROWSERBASE",
429+
...playwrightOptions
430+
} = options;
431+
432+
if (useCDP && this.stagehand.env === "BROWSERBASE") {
433+
const cdpOptions: Record<string, unknown> = {
434+
format: options.type === "jpeg" ? "jpeg" : "png",
435+
quality: options.quality,
436+
clip: options.clip,
437+
omitBackground: options.omitBackground,
438+
fromSurface: true,
439+
};
440+
441+
if (options.fullPage) {
442+
cdpOptions.captureBeyondViewport = true;
443+
}
439444

440-
const data = await this.sendCDP<{ data: string }>(
441-
"Page.captureScreenshot",
442-
cdpOptions,
443-
);
445+
const data = await this.sendCDP<{ data: string }>(
446+
"Page.captureScreenshot",
447+
cdpOptions,
448+
);
444449

445-
// Convert base64 to buffer
446-
const buffer = Buffer.from(data.data, "base64");
450+
// Convert base64 to buffer
451+
const buffer = Buffer.from(data.data, "base64");
447452

448-
return buffer;
453+
return buffer;
454+
} else {
455+
return await rawScreenshot(playwrightOptions);
456+
}
449457
};
450458
}
451459

types/page.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
Browser as PlaywrightBrowser,
33
BrowserContext as PlaywrightContext,
44
Page as PlaywrightPage,
5+
PageScreenshotOptions,
56
} from "playwright";
67
import { z } from "zod/v3";
78
import type {
@@ -21,7 +22,12 @@ export const pageTextSchema = z.object({
2122
page_text: z.string(),
2223
});
2324

24-
export interface Page extends Omit<PlaywrightPage, "on"> {
25+
export interface StagehandScreenshotOptions extends PageScreenshotOptions {
26+
/** Controls whether to use CDP for screenshots in Browserbase environment. Defaults to true. */
27+
useCDP?: boolean;
28+
}
29+
30+
export interface Page extends Omit<PlaywrightPage, "on" | "screenshot"> {
2531
act(action: string): Promise<ActResult>;
2632
act(options: ActOptions): Promise<ActResult>;
2733
act(observation: ObserveResult): Promise<ActResult>;
@@ -38,6 +44,8 @@ export interface Page extends Omit<PlaywrightPage, "on"> {
3844
observe(instruction: string): Promise<ObserveResult[]>;
3945
observe(options?: ObserveOptions): Promise<ObserveResult[]>;
4046

47+
screenshot(options?: StagehandScreenshotOptions): Promise<Buffer>;
48+
4149
on: {
4250
(event: "popup", listener: (page: Page) => unknown): Page;
4351
} & PlaywrightPage["on"];

0 commit comments

Comments
 (0)