Skip to content

Commit 8a43c5a

Browse files
set default download behaviour (#856)
1 parent 5145788 commit 8a43c5a

File tree

8 files changed

+199
-4
lines changed

8 files changed

+199
-4
lines changed

.changeset/eighty-papers-shout.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+
set download behaviour by default

evals/deterministic/tests/browserbase/contexts.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ test.describe("Contexts", () => {
7676
// We will be adding cookies to the context in this session, so we need mark persist=true
7777
stagehand = new Stagehand({
7878
...StagehandConfig,
79+
useAPI: false,
7980
browserbaseSessionCreateParams: {
8081
projectId: BROWSERBASE_PROJECT_ID,
8182
browserSettings: {
@@ -115,6 +116,7 @@ test.describe("Contexts", () => {
115116
// We don't need to persist cookies in this session, so we can mark persist=false
116117
const newStagehand = new Stagehand({
117118
...StagehandConfig,
119+
useAPI: false,
118120
browserbaseSessionCreateParams: {
119121
projectId: BROWSERBASE_PROJECT_ID,
120122
browserSettings: {

evals/deterministic/tests/browserbase/downloads.test.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import { Stagehand } from "@browserbasehq/stagehand";
55
import Browserbase from "@browserbasehq/sdk";
66

77
const downloadRe = /sandstorm-(\d{13})+\.mp3/;
8+
const pdfRe = /sample-(\d{13})+\.pdf/;
89

910
test("Downloads", async () => {
10-
const stagehand = new Stagehand(StagehandConfig);
11+
const stagehand = new Stagehand({
12+
...StagehandConfig,
13+
env: "BROWSERBASE",
14+
useAPI: false,
15+
});
1116
await stagehand.init();
1217
const page = stagehand.page;
18+
1319
const context = stagehand.context;
1420

1521
const client = await context.newCDPSession(page);
@@ -38,7 +44,7 @@ test("Downloads", async () => {
3844
);
3945
}
4046

41-
expect(async () => {
47+
await expect(async () => {
4248
const bb = new Browserbase();
4349
const zipBuffer = await bb.sessions.downloads.list(
4450
stagehand.browserbaseSessionID,
@@ -67,3 +73,59 @@ test("Downloads", async () => {
6773
timeout: 30_000,
6874
});
6975
});
76+
77+
test("Default download behaviour", async () => {
78+
const stagehand = new Stagehand({
79+
...StagehandConfig,
80+
env: "BROWSERBASE",
81+
useAPI: false,
82+
});
83+
await stagehand.init();
84+
const page = stagehand.page;
85+
86+
await page.goto(
87+
"https://browserbase.github.io/stagehand-eval-sites/sites/download-on-click/",
88+
);
89+
90+
const [download] = await Promise.all([
91+
page.waitForEvent("download"),
92+
page.locator("xpath=/html/body/button").click(),
93+
]);
94+
95+
const downloadError = await download.failure();
96+
97+
await stagehand.close();
98+
99+
if (downloadError !== null) {
100+
throw new Error(
101+
`Download for session ${stagehand.browserbaseSessionID} failed: ${downloadError}`,
102+
);
103+
}
104+
105+
await expect(async () => {
106+
const bb = new Browserbase();
107+
const zipBuffer = await bb.sessions.downloads.list(
108+
stagehand.browserbaseSessionID,
109+
);
110+
if (!zipBuffer) {
111+
throw new Error(
112+
`Download buffer is empty for session ${stagehand.browserbaseSessionID}`,
113+
);
114+
}
115+
116+
const zip = new AdmZip(Buffer.from(await zipBuffer.arrayBuffer()));
117+
const zipEntries = zip.getEntries();
118+
const pdfEntry = zipEntries.find((entry) => pdfRe.test(entry.entryName));
119+
120+
if (!pdfEntry) {
121+
throw new Error(
122+
`Session ${stagehand.browserbaseSessionID} is missing a file matching "${pdfRe.toString()}" in its zip entries: ${JSON.stringify(zipEntries.map((entry) => entry.entryName))}`,
123+
);
124+
}
125+
126+
const expectedFileSize = 13264;
127+
expect(pdfEntry.header.size).toBe(expectedFileSize);
128+
}).toPass({
129+
timeout: 30_000,
130+
});
131+
});

evals/deterministic/tests/browserbase/sessions.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ test.describe("Browserbase Sessions", () => {
1515
bigStagehand = new Stagehand({
1616
...StagehandConfig,
1717
env: "BROWSERBASE",
18+
useAPI: false,
1819
browserbaseSessionCreateParams: {
1920
projectId: process.env.BROWSERBASE_PROJECT_ID,
2021
keepAlive: true,
@@ -35,6 +36,7 @@ test.describe("Browserbase Sessions", () => {
3536
test("resumes a session via sessionId", async () => {
3637
const stagehand = new Stagehand({
3738
...StagehandConfig,
39+
useAPI: false,
3840
env: "BROWSERBASE",
3941
browserbaseSessionID: sessionId,
4042
});

evals/deterministic/tests/browserbase/uploads.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ test.describe("Playwright Upload", () => {
77
let stagehand: Stagehand;
88

99
test.beforeAll(async () => {
10-
stagehand = new Stagehand(StagehandConfig);
10+
stagehand = new Stagehand({
11+
...StagehandConfig,
12+
env: "BROWSERBASE",
13+
useAPI: false,
14+
});
1115
await stagehand.init();
1216
});
1317

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { expect, test } from "@playwright/test";
2+
import StagehandConfig from "@/evals/deterministic/stagehand.config";
3+
import { Stagehand } from "@browserbasehq/stagehand";
4+
import { promises as fs } from "fs";
5+
import path from "path";
6+
7+
test("Default download behaviour (local)", async () => {
8+
const downloadsDir: string = path.resolve(process.cwd(), "downloads");
9+
await fs.rm(downloadsDir, { recursive: true, force: true });
10+
await fs.mkdir(downloadsDir, { recursive: true });
11+
const stagehand = new Stagehand({
12+
...StagehandConfig,
13+
env: "LOCAL",
14+
});
15+
await stagehand.init();
16+
const page = stagehand.page;
17+
await page.goto(
18+
"https://browserbase.github.io/stagehand-eval-sites/sites/download-on-click/",
19+
);
20+
21+
const [download] = await Promise.all([
22+
page.waitForEvent("download"),
23+
page.locator("xpath=/html/body/button").click(),
24+
]);
25+
if ((await download.failure()) !== null) {
26+
await stagehand.close();
27+
throw new Error("Local download reported a failure");
28+
}
29+
30+
// Wait until Playwright has the real file path (guarantees it’s done)
31+
const downloadPath: string | null = await download.path();
32+
if (downloadPath === null) {
33+
await stagehand.close();
34+
throw new Error("Download completed, but path() returned null");
35+
}
36+
37+
const expectedFileSize = 13_264; // bytes
38+
const suggested: string = download.suggestedFilename();
39+
const finalPath: string = path.join(downloadsDir, suggested);
40+
41+
await expect
42+
.poll(
43+
async () => {
44+
try {
45+
const stat = await fs.stat(finalPath);
46+
return stat.isFile() && stat.size === expectedFileSize;
47+
} catch {
48+
return false;
49+
}
50+
},
51+
{
52+
message: `Expected "${suggested}" in ${downloadsDir}`,
53+
timeout: 10_000,
54+
},
55+
)
56+
.toBe(true);
57+
58+
const { size } = await fs.stat(finalPath);
59+
expect(size).toBe(expectedFileSize);
60+
61+
await stagehand.close();
62+
});
63+
64+
const downloadRe = /sandstorm\.mp3/;
65+
66+
test("Downloads", async () => {
67+
const stagehand = new Stagehand({
68+
...StagehandConfig,
69+
env: "LOCAL",
70+
});
71+
await stagehand.init();
72+
const page = stagehand.page;
73+
74+
await page.goto("https://browser-tests-alpha.vercel.app/api/download-test");
75+
76+
const [download] = await Promise.all([
77+
page.waitForEvent("download"),
78+
page.locator("#download").click(),
79+
]);
80+
81+
const downloadError = await download.failure();
82+
83+
if (downloadError !== null) {
84+
throw new Error(`Download failed: ${downloadError}`);
85+
}
86+
87+
const suggestedFilename = download.suggestedFilename();
88+
const filePath = path.join(stagehand.downloadsPath, suggestedFilename);
89+
90+
await stagehand.close();
91+
92+
// Verify the download exists and matches expected pattern
93+
expect(
94+
await fs
95+
.access(filePath)
96+
.then(() => true)
97+
.catch(() => false),
98+
).toBe(true);
99+
expect(suggestedFilename).toMatch(downloadRe);
100+
101+
// Verify file size
102+
const stats = await fs.stat(filePath);
103+
const expectedFileSize = 6137541;
104+
expect(stats.size).toBe(expectedFileSize);
105+
});

examples/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@browserbasehq/stagehand": "workspace:*"
1313
},
1414
"devDependencies": {
15-
"tsx": "^4.10.5"
15+
"tsx": "^4.10.5",
16+
"jszip": "^3.10.1"
1617
}
1718
}

lib/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,13 @@ export class Stagehand {
719719
}
720720
}
721721

722+
public get downloadsPath(): string {
723+
return this.env === "BROWSERBASE"
724+
? "downloads"
725+
: (this.localBrowserLaunchOptions?.downloadsPath ??
726+
path.resolve(process.cwd(), "downloads"));
727+
}
728+
722729
public get context(): EnhancedContext {
723730
if (!this.stagehandContext) {
724731
throw new StagehandNotInitializedError("context");
@@ -814,6 +821,13 @@ export class Stagehand {
814821
content: guardedScript,
815822
});
816823

824+
const session = await this.context.newCDPSession(this.page);
825+
await session.send("Browser.setDownloadBehavior", {
826+
behavior: "allow",
827+
downloadPath: this.downloadsPath,
828+
eventsEnabled: true,
829+
});
830+
817831
this.browserbaseSessionID = sessionId;
818832

819833
return { debugUrl, sessionUrl, sessionId };

0 commit comments

Comments
 (0)