Skip to content

Commit 0df1e23

Browse files
Scroll on 'main scrollable element' when possible (#405)
1 parent 1f2b2c5 commit 0df1e23

12 files changed

+377
-45
lines changed

Diff for: .changeset/stupid-starfishes-type.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
in ProcessAllOfDom, scroll on large scrollable elements instead of just the root DOM

Diff for: evals/evals.config.json

+8
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,14 @@
207207
{
208208
"name": "extract_jstor_news",
209209
"categories": ["text_extract"]
210+
},
211+
{
212+
"name": "extract_apartments",
213+
"categories": ["text_extract"]
214+
},
215+
{
216+
"name": "extract_zillow",
217+
"categories": ["text_extract"]
210218
}
211219
]
212220
}

Diff for: evals/tasks/extract_apartments.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { z } from "zod";
2+
import { initStagehand } from "../initStagehand";
3+
import { EvalFunction } from "../../types/evals";
4+
5+
export const extract_apartments: EvalFunction = async ({
6+
modelName,
7+
logger,
8+
useTextExtract,
9+
}) => {
10+
const { stagehand, initResponse } = await initStagehand({
11+
modelName,
12+
logger,
13+
domSettleTimeoutMs: 3000,
14+
});
15+
16+
const { debugUrl, sessionUrl } = initResponse;
17+
18+
await stagehand.page.goto(
19+
"https://www.apartments.com/san-francisco-ca/2-bedrooms/",
20+
);
21+
const apartment_listings = await stagehand.page.extract({
22+
instruction:
23+
"Extract all the apartment listings with their prices and their addresses.",
24+
schema: z.object({
25+
listings: z.array(
26+
z.object({
27+
price: z.string().describe("The price of the listing"),
28+
trails: z.string().describe("The address of the listing"),
29+
}),
30+
),
31+
}),
32+
modelName,
33+
useTextExtract,
34+
});
35+
36+
await stagehand.close();
37+
const listings = apartment_listings.listings;
38+
const expectedLength = 40;
39+
40+
if (listings.length < expectedLength) {
41+
logger.error({
42+
message: "Incorrect number of listings extracted",
43+
level: 0,
44+
auxiliary: {
45+
expected: {
46+
value: expectedLength.toString(),
47+
type: "integer",
48+
},
49+
actual: {
50+
value: listings.length.toString(),
51+
type: "integer",
52+
},
53+
},
54+
});
55+
return {
56+
_success: false,
57+
error: "Incorrect number of listings extracted",
58+
logs: logger.getLogs(),
59+
debugUrl,
60+
sessionUrl,
61+
};
62+
}
63+
64+
return {
65+
_success: true,
66+
logs: logger.getLogs(),
67+
debugUrl,
68+
sessionUrl,
69+
};
70+
};

Diff for: evals/tasks/extract_zillow.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { z } from "zod";
2+
import { initStagehand } from "../initStagehand";
3+
import { EvalFunction } from "../../types/evals";
4+
5+
export const extract_zillow: EvalFunction = async ({
6+
modelName,
7+
logger,
8+
useTextExtract,
9+
}) => {
10+
const { stagehand, initResponse } = await initStagehand({
11+
modelName,
12+
logger,
13+
domSettleTimeoutMs: 3000,
14+
configOverrides: {
15+
debugDom: false,
16+
},
17+
});
18+
19+
const { debugUrl, sessionUrl } = initResponse;
20+
21+
await stagehand.page.goto("https://zillow-eval.surge.sh/");
22+
// timeout for 5 seconds
23+
await stagehand.page.waitForTimeout(5000);
24+
const real_estate_listings = await stagehand.page.extract({
25+
instruction:
26+
"Extract EACH AND EVERY HOME PRICE AND ADDRESS ON THE PAGE. DO NOT MISS ANY OF THEM.",
27+
schema: z.object({
28+
listings: z.array(
29+
z.object({
30+
price: z.string().describe("The price of the home"),
31+
trails: z.string().describe("The address of the home"),
32+
}),
33+
),
34+
}),
35+
modelName,
36+
useTextExtract,
37+
});
38+
39+
await stagehand.close();
40+
const listings = real_estate_listings.listings;
41+
const expectedLength = 38;
42+
43+
if (listings.length < expectedLength) {
44+
logger.error({
45+
message: "Incorrect number of listings extracted",
46+
level: 0,
47+
auxiliary: {
48+
expected: {
49+
value: expectedLength.toString(),
50+
type: "integer",
51+
},
52+
actual: {
53+
value: listings.length.toString(),
54+
type: "integer",
55+
},
56+
},
57+
});
58+
return {
59+
_success: false,
60+
error: "Incorrect number of listings extracted",
61+
logs: logger.getLogs(),
62+
debugUrl,
63+
sessionUrl,
64+
};
65+
}
66+
67+
return {
68+
_success: true,
69+
logs: logger.getLogs(),
70+
debugUrl,
71+
sessionUrl,
72+
};
73+
};

Diff for: lib/dom/ElementContainer.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { StagehandContainer } from "./StagehandContainer";
2+
3+
export class ElementContainer implements StagehandContainer {
4+
constructor(private el: HTMLElement) {}
5+
6+
public getViewportHeight(): number {
7+
return this.el.clientHeight;
8+
}
9+
10+
public getScrollHeight(): number {
11+
return this.el.scrollHeight;
12+
}
13+
14+
public async scrollTo(offset: number): Promise<void> {
15+
await new Promise((resolve) => setTimeout(resolve, 1500));
16+
this.el.scrollTo({ top: offset, left: 0, behavior: "smooth" });
17+
await this.waitForScrollEnd();
18+
}
19+
20+
private async waitForScrollEnd(): Promise<void> {
21+
return new Promise<void>((resolve) => {
22+
let scrollEndTimer: number;
23+
const handleScroll = () => {
24+
clearTimeout(scrollEndTimer);
25+
scrollEndTimer = window.setTimeout(() => {
26+
this.el.removeEventListener("scroll", handleScroll);
27+
resolve();
28+
}, 100);
29+
};
30+
this.el.addEventListener("scroll", handleScroll, { passive: true });
31+
handleScroll();
32+
});
33+
}
34+
}

Diff for: lib/dom/GlobalPageContainer.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { StagehandContainer } from "./StagehandContainer";
2+
import { calculateViewportHeight } from "./utils";
3+
4+
export class GlobalPageContainer implements StagehandContainer {
5+
public getViewportHeight(): number {
6+
return calculateViewportHeight();
7+
}
8+
9+
public getScrollHeight(): number {
10+
return document.documentElement.scrollHeight;
11+
}
12+
13+
public async scrollTo(offset: number): Promise<void> {
14+
await new Promise((resolve) => setTimeout(resolve, 1500));
15+
window.scrollTo({ top: offset, left: 0, behavior: "smooth" });
16+
await this.waitForScrollEnd();
17+
}
18+
19+
private async waitForScrollEnd(): Promise<void> {
20+
return new Promise<void>((resolve) => {
21+
let scrollEndTimer: number;
22+
const handleScroll = () => {
23+
clearTimeout(scrollEndTimer);
24+
scrollEndTimer = window.setTimeout(() => {
25+
window.removeEventListener("scroll", handleScroll);
26+
resolve();
27+
}, 100);
28+
};
29+
window.addEventListener("scroll", handleScroll, { passive: true });
30+
handleScroll();
31+
});
32+
}
33+
}

Diff for: lib/dom/StagehandContainer.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface StagehandContainer {
2+
getViewportHeight(): number;
3+
4+
getScrollHeight(): number;
5+
6+
scrollTo(offset: number): Promise<void>;
7+
}

Diff for: lib/dom/containerFactory.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { StagehandContainer } from "./StagehandContainer";
2+
import { GlobalPageContainer } from "./GlobalPageContainer";
3+
import { ElementContainer } from "./ElementContainer";
4+
5+
/**
6+
* Decide which container to create.
7+
*/
8+
export function createStagehandContainer(
9+
obj: Window | HTMLElement,
10+
): StagehandContainer {
11+
if (obj instanceof Window) {
12+
return new GlobalPageContainer();
13+
} else {
14+
return new ElementContainer(obj);
15+
}
16+
}

Diff for: lib/dom/global.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { StagehandContainer } from "./StagehandContainer";
2+
13
export {};
24
declare global {
35
interface Window {
@@ -19,7 +21,7 @@ declare global {
1921
}>;
2022
debugDom: () => Promise<void>;
2123
cleanupDebug: () => void;
22-
scrollToHeight: (height: number) => Promise<void>;
24+
createStagehandContainer: (obj: Window | HTMLElement) => StagehandContainer;
2325
waitForDomSettle: () => Promise<void>;
2426
__playwright?: unknown;
2527
__pw_manual?: unknown;

0 commit comments

Comments
 (0)