Skip to content

Commit a683fab

Browse files
authored
added iframes as the last observeResult on pages with 1+ iframes (#500)
* added iframes as the last observeResult on pages with 1+ iframes * added changeset
1 parent 28ca9fb commit a683fab

File tree

7 files changed

+220
-1
lines changed

7 files changed

+220
-1
lines changed

Diff for: .changeset/odd-eggs-bake.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
Including Iframes in ObserveResults. This appends any iframe(s) found in the page to the end of observe results on any observe call.

Diff for: evals/evals.config.json

+8
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@
239239
{
240240
"name": "observe_taxes",
241241
"categories": ["observe"]
242+
},
243+
{
244+
"name": "observe_iframes1",
245+
"categories": ["observe"]
246+
},
247+
{
248+
"name": "observe_iframes2",
249+
"categories": ["observe"]
242250
}
243251
]
244252
}

Diff for: evals/tasks/observe_iframes1.ts

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { initStagehand } from "@/evals/initStagehand";
2+
import { EvalFunction } from "@/types/evals";
3+
4+
export const observe_iframes1: EvalFunction = async ({ modelName, logger }) => {
5+
const { stagehand, initResponse } = await initStagehand({
6+
modelName,
7+
logger,
8+
});
9+
10+
const { debugUrl, sessionUrl } = initResponse;
11+
12+
await stagehand.page.goto("https://tucowsdomains.com/abuse-form/phishing/");
13+
14+
const observations = await stagehand.page.observe({
15+
instruction: "find the main header of the page",
16+
});
17+
18+
if (observations.length === 0) {
19+
await stagehand.close();
20+
return {
21+
_success: false,
22+
observations,
23+
debugUrl,
24+
sessionUrl,
25+
logs: logger.getLogs(),
26+
};
27+
}
28+
29+
const possibleLocators = [
30+
`#primary > div.singlePage > section > div > div > article > div > iframe`,
31+
`#primary > div.heroBanner > section > div > h1`,
32+
];
33+
34+
const possibleHandles = [];
35+
for (const locatorStr of possibleLocators) {
36+
const locator = stagehand.page.locator(locatorStr);
37+
const handle = await locator.elementHandle();
38+
if (handle) {
39+
possibleHandles.push({ locatorStr, handle });
40+
}
41+
}
42+
43+
let foundMatch = false;
44+
let matchedLocator: string | null = null;
45+
46+
for (const observation of observations) {
47+
try {
48+
const observationLocator = stagehand.page
49+
.locator(observation.selector)
50+
.first();
51+
const observationHandle = await observationLocator.elementHandle();
52+
if (!observationHandle) {
53+
continue;
54+
}
55+
56+
for (const { locatorStr, handle: candidateHandle } of possibleHandles) {
57+
const isSameNode = await observationHandle.evaluate(
58+
(node, otherNode) => node === otherNode,
59+
candidateHandle,
60+
);
61+
if (isSameNode) {
62+
foundMatch = true;
63+
matchedLocator = locatorStr;
64+
break;
65+
}
66+
}
67+
68+
if (foundMatch) {
69+
break;
70+
}
71+
} catch (error) {
72+
console.warn(
73+
`Failed to check observation with selector ${observation.selector}:`,
74+
error.message,
75+
);
76+
continue;
77+
}
78+
}
79+
80+
await stagehand.close();
81+
82+
return {
83+
_success: foundMatch,
84+
matchedLocator,
85+
observations,
86+
debugUrl,
87+
sessionUrl,
88+
logs: logger.getLogs(),
89+
};
90+
};

Diff for: evals/tasks/observe_iframes2.ts

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { initStagehand } from "@/evals/initStagehand";
2+
import { EvalFunction } from "@/types/evals";
3+
4+
export const observe_iframes2: EvalFunction = async ({ modelName, logger }) => {
5+
const { stagehand, initResponse } = await initStagehand({
6+
modelName,
7+
logger,
8+
});
9+
10+
const { debugUrl, sessionUrl } = initResponse;
11+
12+
await stagehand.page.goto(
13+
"https://iframetester.com/?url=https://shopify.com",
14+
);
15+
await new Promise((resolve) => setTimeout(resolve, 5000));
16+
17+
const observations = await stagehand.page.observe({
18+
instruction: "find the main header of the page",
19+
});
20+
21+
if (observations.length === 0) {
22+
await stagehand.close();
23+
return {
24+
_success: false,
25+
observations,
26+
debugUrl,
27+
sessionUrl,
28+
logs: logger.getLogs(),
29+
};
30+
}
31+
32+
const possibleLocators = [`#iframe-window`, `body > header > h1`];
33+
34+
const possibleHandles = [];
35+
for (const locatorStr of possibleLocators) {
36+
const locator = stagehand.page.locator(locatorStr);
37+
const handle = await locator.elementHandle();
38+
if (handle) {
39+
possibleHandles.push({ locatorStr, handle });
40+
}
41+
}
42+
43+
let foundMatch = false;
44+
let matchedLocator: string | null = null;
45+
46+
for (const observation of observations) {
47+
try {
48+
const observationLocator = stagehand.page
49+
.locator(observation.selector)
50+
.first();
51+
const observationHandle = await observationLocator.elementHandle();
52+
if (!observationHandle) {
53+
continue;
54+
}
55+
56+
for (const { locatorStr, handle: candidateHandle } of possibleHandles) {
57+
const isSameNode = await observationHandle.evaluate(
58+
(node, otherNode) => node === otherNode,
59+
candidateHandle,
60+
);
61+
if (isSameNode) {
62+
foundMatch = true;
63+
matchedLocator = locatorStr;
64+
break;
65+
}
66+
}
67+
68+
if (foundMatch) {
69+
break;
70+
}
71+
} catch (error) {
72+
console.warn(
73+
`Failed to check observation with selector ${observation.selector}:`,
74+
error.message,
75+
);
76+
continue;
77+
}
78+
}
79+
80+
await stagehand.close();
81+
82+
return {
83+
_success: foundMatch,
84+
matchedLocator,
85+
observations,
86+
debugUrl,
87+
sessionUrl,
88+
logs: logger.getLogs(),
89+
};
90+
};

Diff for: lib/a11y/utils.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export async function buildHierarchicalTree(
156156
): Promise<TreeResult> {
157157
// Map to store processed nodes for quick lookup
158158
const nodeMap = new Map<string, AccessibilityNode>();
159+
const iframe_list: AccessibilityNode[] = [];
159160

160161
// First pass: Create nodes that are meaningful
161162
// We only keep nodes that either have a name or children to avoid cluttering the tree
@@ -194,6 +195,15 @@ export async function buildHierarchicalTree(
194195
// Second pass: Establish parent-child relationships
195196
// This creates the actual tree structure by connecting nodes based on parentId
196197
nodes.forEach((node) => {
198+
// Add iframes to a list and include in the return object
199+
const isIframe = node.role === "Iframe";
200+
if (isIframe) {
201+
const iframeNode = {
202+
role: node.role,
203+
nodeId: node.nodeId,
204+
};
205+
iframe_list.push(iframeNode);
206+
}
197207
if (node.parentId && nodeMap.has(node.nodeId)) {
198208
const parentNode = nodeMap.get(node.parentId);
199209
const currentNode = nodeMap.get(node.nodeId);
@@ -228,6 +238,7 @@ export async function buildHierarchicalTree(
228238
return {
229239
tree: finalTree,
230240
simplified: simplifiedFormat,
241+
iframes: iframe_list,
231242
};
232243
}
233244

@@ -283,7 +294,6 @@ export async function getAccessibilityTree(
283294
message: `got accessibility tree in ${Date.now() - startTime}ms`,
284295
level: 1,
285296
});
286-
287297
return hierarchicalTree;
288298
} catch (error) {
289299
logger({

Diff for: lib/handlers/observeHandler.ts

+15
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getAccessibilityTree,
99
getXPathByResolvedObjectId,
1010
} from "../a11y/utils";
11+
import { AccessibilityNode } from "../../types/context";
1112

1213
export class StagehandObserveHandler {
1314
private readonly stagehand: Stagehand;
@@ -83,6 +84,7 @@ export class StagehandObserveHandler {
8384

8485
let selectorMap: Record<string, string[]> = {};
8586
let outputString: string;
87+
let iframes: AccessibilityNode[] = [];
8688
const useAccessibilityTree = !onlyVisible;
8789
if (useAccessibilityTree) {
8890
await this.stagehandPage._waitForSettledDom();
@@ -93,6 +95,7 @@ export class StagehandObserveHandler {
9395
level: 1,
9496
});
9597
outputString = tree.simplified;
98+
iframes = tree.iframes;
9699
} else {
97100
const evalResult = await this.stagehand.page.evaluate(() => {
98101
return window.processAllOfDom().then((result) => result);
@@ -111,6 +114,18 @@ export class StagehandObserveHandler {
111114
isUsingAccessibilityTree: useAccessibilityTree,
112115
returnAction,
113116
});
117+
118+
//Add iframes to the observation response if there are any on the page
119+
if (iframes.length > 0) {
120+
iframes.forEach((iframe) => {
121+
observationResponse.elements.push({
122+
elementId: Number(iframe.nodeId),
123+
description: "an iframe",
124+
method: "not-supported",
125+
arguments: [],
126+
});
127+
});
128+
}
114129
const elementsWithSelectors = await Promise.all(
115130
observationResponse.elements.map(async (element) => {
116131
const { elementId, ...rest } = element;

Diff for: types/context.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ export type AccessibilityNode = {
2424
export interface TreeResult {
2525
tree: AccessibilityNode[];
2626
simplified: string;
27+
iframes?: AccessibilityNode[];
2728
}

0 commit comments

Comments
 (0)