Skip to content

Commit 9ba4b0b

Browse files
pkivseanmcguire12
andauthored
feat: add comprehensive local browser launch configuration options (#494)
* feat: add comprehensive local browser launch configuration options - Add LocalBrowserLaunchOptions interface for granular browser control - Support custom user data dir, viewport, geolocation, cookies & more - Add thorough test suite for local browser configurations - Deprecate top-level headless option in favor of localBrowserLaunchOptions * ignore local tests when running e2e * run e2e:local in CI * prettier * changeset --------- Co-authored-by: seanmcguire12 <[email protected]>
1 parent 945ed04 commit 9ba4b0b

File tree

8 files changed

+447
-41
lines changed

8 files changed

+447
-41
lines changed

Diff for: .changeset/dull-crabs-talk.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
Added LocalBrowserLaunchOptions to provide comprehensive configuration options for local browser instances. Deprecated the top-level headless option in favor of using localBrowserLaunchOptions.headless

Diff for: .github/workflows/ci.yml

+30
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,36 @@ jobs:
113113
- name: Run E2E Tests (Deterministic Playwright)
114114
run: npm run e2e
115115

116+
run-e2e-local-tests:
117+
needs: [run-lint, run-build]
118+
runs-on: ubuntu-latest
119+
timeout-minutes: 50
120+
env:
121+
HEADLESS: true
122+
steps:
123+
- name: Check out repository code
124+
uses: actions/checkout@v4
125+
126+
- name: Set up Node.js
127+
uses: actions/setup-node@v4
128+
with:
129+
node-version: "20"
130+
131+
- name: Install dependencies
132+
run: |
133+
rm -rf node_modules
134+
rm -f package-lock.json
135+
npm install
136+
137+
- name: Install Playwright browsers
138+
run: npm exec playwright install --with-deps
139+
140+
- name: Build Stagehand
141+
run: npm run build
142+
143+
- name: Run local E2E Tests (Deterministic Playwright)
144+
run: npm run e2e:local
145+
116146
run-e2e-bb-tests:
117147
needs: [run-e2e-tests]
118148
runs-on: ubuntu-latest

Diff for: evals/deterministic/e2e.playwright.config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import { defineConfig, devices } from "@playwright/test";
66
export default defineConfig({
77
// Look in "tests" for test files...
88
testDir: "./tests",
9-
// ...but ignore anything in "tests/browserbase"
10-
testIgnore: ["**/browserbase/**"],
9+
// ...but ignore anything in "tests/browserbase & "tests/local"
10+
testIgnore: ["**/browserbase/**", "**/local/**"],
1111

1212
/* Fail the build on CI if you accidentally left test.only in the source code. */
1313
/* Run tests in files in parallel */

Diff for: evals/deterministic/local.playwright.config.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
3+
/**
4+
* See https://playwright.dev/docs/test-configuration.
5+
*/
6+
export default defineConfig({
7+
testDir: "./tests/local",
8+
9+
/* Maximum time one test can run for. */
10+
timeout: 30 * 1000,
11+
12+
/* Fail the build on CI if you accidentally left test.only in the source code. */
13+
forbidOnly: !!process.env.CI,
14+
15+
/* Run tests in files in parallel */
16+
fullyParallel: false,
17+
18+
/* Reporter to use */
19+
reporter: "line",
20+
21+
/* Retry on CI only */
22+
retries: process.env.CI ? 2 : 0,
23+
24+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
25+
use: {
26+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
27+
trace: "on-first-retry",
28+
},
29+
30+
/* Configure projects for major browsers */
31+
projects: [
32+
{
33+
name: "chromium",
34+
use: { ...devices["Desktop Chrome"] },
35+
},
36+
],
37+
});

Diff for: evals/deterministic/tests/local/create.test.ts

+243
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { test, expect } from "@playwright/test";
2+
import { Stagehand } from "@/dist";
3+
import path from "path";
4+
import fs from "fs";
5+
import os from "os";
6+
import type { Cookie } from "@playwright/test";
7+
8+
test.describe("Local browser launch options", () => {
9+
test("launches with default options when no localBrowserLaunchOptions provided", async () => {
10+
const stagehand = new Stagehand({
11+
env: "LOCAL",
12+
verbose: 1,
13+
headless: true,
14+
debugDom: true,
15+
domSettleTimeoutMs: 30_000,
16+
enableCaching: true,
17+
modelName: "gpt-4o",
18+
modelClientOptions: {
19+
apiKey: process.env.OPENAI_API_KEY,
20+
},
21+
});
22+
await stagehand.init();
23+
24+
const context = stagehand.context;
25+
expect(context.browser()).toBeDefined();
26+
expect(context.pages().length).toBe(1);
27+
28+
await stagehand.close();
29+
});
30+
31+
test("respects custom userDataDir", async () => {
32+
const customUserDataDir = path.join(os.tmpdir(), "custom-user-data");
33+
34+
const stagehand = new Stagehand({
35+
env: "LOCAL",
36+
verbose: 1,
37+
headless: true,
38+
debugDom: true,
39+
domSettleTimeoutMs: 30_000,
40+
enableCaching: true,
41+
modelName: "gpt-4o",
42+
modelClientOptions: {
43+
apiKey: process.env.OPENAI_API_KEY,
44+
},
45+
localBrowserLaunchOptions: {
46+
userDataDir: customUserDataDir,
47+
},
48+
});
49+
await stagehand.init();
50+
51+
expect(fs.existsSync(customUserDataDir)).toBeTruthy();
52+
53+
await stagehand.close();
54+
55+
// Cleanup
56+
fs.rmSync(customUserDataDir, { recursive: true, force: true });
57+
});
58+
59+
test("applies custom viewport settings", async () => {
60+
const customViewport = { width: 1920, height: 1080 };
61+
62+
const stagehand = new Stagehand({
63+
env: "LOCAL",
64+
verbose: 1,
65+
headless: true,
66+
debugDom: true,
67+
domSettleTimeoutMs: 30_000,
68+
enableCaching: true,
69+
modelName: "gpt-4o",
70+
modelClientOptions: {
71+
apiKey: process.env.OPENAI_API_KEY,
72+
},
73+
localBrowserLaunchOptions: {
74+
viewport: customViewport,
75+
},
76+
});
77+
await stagehand.init();
78+
79+
const page = await stagehand.context.newPage();
80+
const viewport = page.viewportSize();
81+
82+
expect(viewport).toEqual(customViewport);
83+
84+
await stagehand.close();
85+
});
86+
87+
test("applies custom cookies", async () => {
88+
const testCookies: Cookie[] = [
89+
{
90+
name: "testCookie",
91+
value: "testValue",
92+
domain: "example.com",
93+
path: "/",
94+
expires: -1,
95+
httpOnly: false,
96+
secure: false,
97+
sameSite: "Lax" as const,
98+
},
99+
];
100+
101+
const stagehand = new Stagehand({
102+
env: "LOCAL",
103+
verbose: 1,
104+
headless: true,
105+
debugDom: true,
106+
domSettleTimeoutMs: 30_000,
107+
enableCaching: true,
108+
modelName: "gpt-4o",
109+
modelClientOptions: {
110+
apiKey: process.env.OPENAI_API_KEY,
111+
},
112+
localBrowserLaunchOptions: {
113+
cookies: testCookies,
114+
},
115+
});
116+
await stagehand.init();
117+
118+
const page = await stagehand.context.newPage();
119+
await page.goto("https://example.com");
120+
const cookies = await stagehand.context.cookies();
121+
122+
expect(cookies[0]).toMatchObject(
123+
testCookies[0] as unknown as Record<string, unknown>,
124+
);
125+
126+
await stagehand.close();
127+
});
128+
129+
test("applies custom geolocation settings", async () => {
130+
const customGeolocation = {
131+
latitude: 40.7128,
132+
longitude: -74.006,
133+
};
134+
135+
const stagehand = new Stagehand({
136+
env: "LOCAL",
137+
verbose: 1,
138+
headless: true,
139+
debugDom: true,
140+
domSettleTimeoutMs: 30_000,
141+
enableCaching: true,
142+
modelName: "gpt-4o",
143+
modelClientOptions: {
144+
apiKey: process.env.OPENAI_API_KEY,
145+
},
146+
localBrowserLaunchOptions: {
147+
geolocation: customGeolocation,
148+
permissions: ["geolocation"],
149+
},
150+
});
151+
await stagehand.init();
152+
153+
const page = await stagehand.context.newPage();
154+
await page.goto("https://example.com");
155+
156+
const location = await page.evaluate(() => {
157+
return new Promise((resolve) => {
158+
navigator.geolocation.getCurrentPosition(
159+
(position) => {
160+
resolve({
161+
latitude: position.coords.latitude,
162+
longitude: position.coords.longitude,
163+
});
164+
},
165+
() => resolve(null),
166+
);
167+
});
168+
});
169+
170+
expect(location).toEqual(customGeolocation);
171+
172+
await stagehand.close();
173+
});
174+
175+
test("applies custom timezone and locale", async () => {
176+
const stagehand = new Stagehand({
177+
env: "LOCAL",
178+
verbose: 1,
179+
headless: true,
180+
debugDom: true,
181+
domSettleTimeoutMs: 30_000,
182+
enableCaching: true,
183+
modelName: "gpt-4o",
184+
modelClientOptions: {
185+
apiKey: process.env.OPENAI_API_KEY,
186+
},
187+
localBrowserLaunchOptions: {
188+
locale: "ja-JP",
189+
timezoneId: "Asia/Tokyo",
190+
},
191+
});
192+
await stagehand.init();
193+
194+
const page = await stagehand.context.newPage();
195+
await page.goto("https://example.com");
196+
197+
const { locale, timezone } = await page.evaluate(() => ({
198+
locale: navigator.language,
199+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
200+
}));
201+
202+
expect(locale).toBe("ja-JP");
203+
expect(timezone).toBe("Asia/Tokyo");
204+
205+
await stagehand.close();
206+
});
207+
208+
test("records video when enabled", async () => {
209+
const videoDir = path.join(os.tmpdir(), "test-videos");
210+
fs.mkdirSync(videoDir, { recursive: true });
211+
212+
const stagehand = new Stagehand({
213+
env: "LOCAL",
214+
verbose: 1,
215+
headless: true,
216+
debugDom: true,
217+
domSettleTimeoutMs: 30_000,
218+
enableCaching: true,
219+
modelName: "gpt-4o",
220+
modelClientOptions: {
221+
apiKey: process.env.OPENAI_API_KEY,
222+
},
223+
localBrowserLaunchOptions: {
224+
recordVideo: {
225+
dir: videoDir,
226+
size: { width: 800, height: 600 },
227+
},
228+
},
229+
});
230+
await stagehand.init();
231+
232+
const page = await stagehand.context.newPage();
233+
await page.goto("https://example.com");
234+
await stagehand.close();
235+
236+
const videos = fs.readdirSync(videoDir);
237+
expect(videos.length).toBeGreaterThan(0);
238+
expect(videos[0]).toMatch(/\.webm$/);
239+
240+
// Cleanup
241+
fs.rmSync(videoDir, { recursive: true, force: true });
242+
});
243+
});

0 commit comments

Comments
 (0)