Skip to content

iframes #778

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1889342
iframes wip
seanmcguire12 May 24, 2025
e01e974
works for act & observe
seanmcguire12 May 26, 2025
df54af9
move core logic into a11y/utils.ts
seanmcguire12 May 26, 2025
372c7da
iterative backend map building
seanmcguire12 May 26, 2025
d8104ef
dont use regex
seanmcguire12 May 27, 2025
6f2e406
assign ids
seanmcguire12 May 28, 2025
ee05623
works with extract
seanmcguire12 May 28, 2025
be5497b
revert example
seanmcguire12 May 28, 2025
f17bc5d
rm some useless comments
seanmcguire12 May 28, 2025
4f8f91a
fix targeted extract
seanmcguire12 May 29, 2025
44c1f9a
JSDoc
seanmcguire12 May 30, 2025
3d3eada
mv RichNode type
seanmcguire12 May 30, 2025
5aced2f
manage frame ordinals at the StagehandPage level
seanmcguire12 May 30, 2025
c45ffdf
add hn eval
seanmcguire12 May 30, 2025
5316ac6
more evals
seanmcguire12 May 30, 2025
fdd0644
parameterize iframe traversal
seanmcguire12 May 31, 2025
b630894
update evals with iframes: true
seanmcguire12 May 31, 2025
2770741
rm frame ID ordinal limit
seanmcguire12 Jun 2, 2025
e2d0c01
better errors and comment style
seanmcguire12 Jun 2, 2025
9207bec
rm unused error
seanmcguire12 Jun 2, 2025
a860c99
log warning if iframes are found when iframes: true is unset
seanmcguire12 Jun 2, 2025
d2c80e9
append iframes to observeResult when iframes: false
seanmcguire12 Jun 2, 2025
7aec392
changeset
seanmcguire12 Jun 2, 2025
06269e9
update docs
seanmcguire12 Jun 2, 2025
697e567
greptile comments
seanmcguire12 Jun 3, 2025
bc22241
make iframes experimental
seanmcguire12 Jun 9, 2025
1f80888
run evals with experimental: true
seanmcguire12 Jun 9, 2025
7430930
add ID_PATTERN const
seanmcguire12 Jun 9, 2025
a3b6b06
update package.json
seanmcguire12 Jun 9, 2025
a2d7e4d
fix nested same proc iframes
seanmcguire12 Jun 10, 2025
c17a209
nested iframes eval
seanmcguire12 Jun 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-bikes-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand-lib": minor
---

iframe support
4 changes: 4 additions & 0 deletions docs/reference/act.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ You can pass an `ObserveResult` to `act()` to perform the suggested action, whic
Variables to use in the action. Variables in the action string are referenced using %variable_name%
</ParamField>

<ParamField path="iframes" type="boolean" optional>
Set `iframes: true` if the target element exists within an iframe
</ParamField>

<ParamField path="domSettleTimeoutMs" type="number" optional>
Timeout in milliseconds for waiting for the DOM to settle
</ParamField>
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/extract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ const apartments = await stagehand.page.extract({
Defines the structure of the data to extract
</ParamField>
<ParamField path="iframes" type="boolean" optional>
Set `iframes: true` if the extraction content exists within an iframe.
</ParamField>
<ParamField path="useTextExtract" type="boolean" deprecated>
This field is now **deprecated** and has no effect.
</ParamField>
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/observe.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ Observe can also return a suggested action for the candidate element by setting
Returns an observe result object that contains a suggested action for the candidate element. The suggestion includes method, and arguments (if any). Defaults to `true`.
</ParamField>

<ParamField path="iframes" type="boolean" optional>
Set `iframes: true` if content from iframes should be included in the observation.
</ParamField>

<ParamField path="modelName" type="AvailableModel" optional>
Specifies the model to use
</ParamField>
Expand Down
16 changes: 16 additions & 0 deletions evals/evals.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,22 @@
{
"name": "login",
"categories": ["act", "regression"]
},
{
"name": "iframe_hn",
"categories": ["extract"]
},
{
"name": "iframe_same_proc",
"categories": ["act"]
},
{
"name": "iframe_form_filling",
"categories": ["act"]
},
{
"name": "iframes_nested",
"categories": ["act"]
}
]
}
1 change: 1 addition & 0 deletions evals/initStagehand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const StagehandConfig = {
enableCaching,
domSettleTimeoutMs: 30_000,
disablePino: true,
experimental: true,
browserbaseSessionCreateParams: {
projectId: process.env.BROWSERBASE_PROJECT_ID!,
browserSettings: {
Expand Down
70 changes: 70 additions & 0 deletions evals/tasks/iframe_form_filling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { EvalFunction } from "@/types/evals";

export const iframe_form_filling: EvalFunction = async ({
debugUrl,
sessionUrl,
stagehand,
logger,
}) => {
const page = stagehand.page;
await page.goto(
"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-form-filling/",
);

await page.act({
action: "type 'nunya' into the 'first name' field",
iframes: true,
});
await page.act({
action: "type 'business' into the 'last name' field",
iframes: true,
});
await page.act({
action: "type '[email protected]' into the 'email' field",
iframes: true,
});
await page.act({
action: "click 'phone' as the preferred contact method",
iframes: true,
});
await page.act({
action: "type 'yooooooooooooooo' into the message box",
iframes: true,
});

const iframe = page.frameLocator("iframe");

const firstNameValue: string = await iframe
.locator('input[placeholder="Jane"]')
.inputValue();

const lastNameValue: string = await iframe
.locator('input[placeholder="Doe"]')
.inputValue();

const emailValue: string = await iframe
.locator('input[placeholder="[email protected]"]')
.inputValue();

const contactValue: boolean = await iframe
.locator("xpath=/html/body/main/section[1]/form/fieldset/label[2]/input")
.isChecked();

const messageValue: string = await iframe
.locator('textarea[placeholder="Say hello…"]')
.inputValue();

const passed: boolean =
firstNameValue.toLowerCase().trim() === "nunya" &&
lastNameValue.toLowerCase().trim() === "business" &&
emailValue.toLowerCase() === "[email protected]" &&
messageValue.toLowerCase() === "yooooooooooooooo" &&
contactValue;

return {
_success: passed,
logs: logger.getLogs(),
debugUrl,
sessionUrl,
};
};
48 changes: 48 additions & 0 deletions evals/tasks/iframe_hn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { EvalFunction } from "@/types/evals";
import { z } from "zod";

export const iframe_hn: EvalFunction = async ({
debugUrl,
sessionUrl,
stagehand,
logger,
}) => {
const page = stagehand.page;
await page.goto(
"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-hn/",
);

const result = await page.extract({
instruction: "extract the title of the first hackernews story",
schema: z.object({
story_title: z.string(),
}),
iframes: true,
});

await stagehand.close();

const title = result.story_title.toLowerCase();
const expectedTitleSubstring = "overengineered anchor links";

if (!title.includes(expectedTitleSubstring)) {
logger.error({
message: `Extracted title: ${title} does not contain expected substring: ${expectedTitleSubstring}`,
level: 0,
});
return {
_success: false,
error: `Extracted title: ${title} does not contain expected substring: ${expectedTitleSubstring}`,
logs: logger.getLogs(),
debugUrl,
sessionUrl,
};
}

return {
_success: true,
logs: logger.getLogs(),
debugUrl,
sessionUrl,
};
};
45 changes: 45 additions & 0 deletions evals/tasks/iframe_same_proc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { EvalFunction } from "@/types/evals";

export const iframe_same_proc: EvalFunction = async ({
debugUrl,
sessionUrl,
stagehand,
logger,
}) => {
const page = stagehand.page;
await page.goto(
"https://browserbase.github.io/stagehand-eval-sites/sites/iframe-same-proc/",
);

await page.act({
action: "type 'stagehand' into the 'your name' field",
iframes: true,
});

// overly specific prompting is okay here. we are just trying to evaluate whether
// we are properly traversing iframes
await page.act({
action:
"select 'Green' from the favorite colour dropdown. Ensure the word 'Green' is capitalized. Choose the selectOption playwright method.",
iframes: true,
});

const iframe = page.frameLocator("iframe");

const nameValue: string = await iframe
.locator('input[placeholder="Alice"]')
.inputValue();

const colorValue: string = await iframe.locator("select").inputValue();

const passed: boolean =
nameValue.toLowerCase().trim() === "stagehand" &&
colorValue.toLowerCase().trim() === "green";

return {
_success: passed,
logs: logger.getLogs(),
debugUrl,
sessionUrl,
};
};
49 changes: 49 additions & 0 deletions evals/tasks/iframes_nested.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { EvalFunction } from "@/types/evals";
import { FrameLocator } from "@playwright/test";

export const iframes_nested: EvalFunction = async ({
debugUrl,
sessionUrl,
stagehand,
logger,
}) => {
const page = stagehand.page;
try {
await page.goto(
"https://browserbase.github.io/stagehand-eval-sites/sites/nested-iframes/",
);

await page.act({
action: "type 'stagehand' into the 'username' field",
iframes: true,
});

const inner: FrameLocator = page
.frameLocator("iframe.lvl1") // level 1
.frameLocator("iframe.lvl2") // level 2
.frameLocator("iframe.lvl3"); // level 3 – form lives here

const usernameText = await inner
.locator('input[name="username"]')
.inputValue();

const passed: boolean = usernameText.toLowerCase().trim() === "stagehand";

return {
_success: passed,
logs: logger.getLogs(),
debugUrl,
sessionUrl,
};
} catch (error) {
return {
_success: false,
logs: logger.getLogs(),
debugUrl,
sessionUrl,
error,
};
} finally {
await stagehand.close();
}
};
Loading