Skip to content

Commit 261bba4

Browse files
add support for shadow doms (open & closed mode) (#954)
# why - adds support for taking actions on shadow DOMs (both closed & open mode) - addresses the following issues: - #380 - #870 - #908 - #943 # what changed - This PR adds some injected JS which 1. intercepts `Element.prototype.attachShadow` early and stashes closed mode shadow roots in a `WeakMap` 2. provides a 'backdoor' for safely accessing these closed roots without mutating anything in the actual DOM - to access the 'backdoor', this PR adds a custom locator engine `selectors.register('stagehand', …)` - the engine does a DFS over: - regular DOM nodes, - open shadow roots via `el.shadowRoot`, - closed roots via `window.__stagehand__.getClosedRoot(el)` - returns a regular playwright locator # note - all the logic here is behind the `experimental` flag in the stagehand constructor, so that we can give people access without breaking existing behaviour - this means that this feature is not available on the API (yet), and you'll need to set `experimental: true` in order to use it # test plan - added 8 different evals. They are primarily aimed at testing shadow dom interactions with the various types of shadow DOMs (open & closed mode) and iframes (OOPIFs & SPIFs) - will also run: - `regression` evals - `act` evals - `extract` evals - `observe` evals
1 parent 3d80421 commit 261bba4

20 files changed

+1020
-37
lines changed

.changeset/calm-snails-carry.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+
add support for shadow DOMs (open & closed mode) when experimental: true

evals/evals.config.json

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,48 @@
420420
"categories": ["agent"]
421421
},
422422
{
423-
"name": "iframe_scroll",
423+
"name": "osr_in_oopif",
424+
"categories": ["act"]
425+
},
426+
{
427+
"name": "csr_in_oopif",
428+
"categories": ["act"]
429+
},
430+
{
431+
"name": "csr_in_spif",
432+
"categories": ["act"]
433+
},
434+
{
435+
"name": "csr_in_spif",
436+
"categories": ["act"]
437+
},
438+
{
439+
"name": "spif_in_osr",
440+
"categories": ["act"]
441+
},
442+
{
443+
"name": "oopif_in_osr",
444+
"categories": ["act"]
445+
},
446+
{
447+
"name": "spif_in_csr",
448+
"categories": ["act"]
449+
},
450+
{
451+
"name": "oopif_in_csr",
452+
"categories": ["act"]
453+
},
454+
{
455+
"name": "osr_in_spif",
424456
"categories": ["act"]
425457
},
426458
{
427459
"name": "namespace_xpath",
428460
"categories": ["act"]
461+
},
462+
{
463+
"name": "iframe_scroll",
464+
"categories": ["act"]
429465
}
430466
]
431467
}

evals/tasks/csr_in_oopif.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const csr_in_oopif: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
// this eval is designed to test whether stagehand can successfully
10+
// click inside an CSR (closed mode shadow) root that is inside an
11+
// OOPIF (out of process iframe)
12+
13+
const page = stagehand.page;
14+
try {
15+
await page.goto(
16+
"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-root-in-oopif/",
17+
);
18+
await page.act({ action: "click the button", iframes: true });
19+
20+
const extraction = await page.extract({
21+
instruction: "extract the entire page text",
22+
iframes: true,
23+
});
24+
25+
const pageText = extraction.extraction;
26+
27+
if (pageText.includes("button successfully clicked")) {
28+
return {
29+
_success: true,
30+
message: `successfully clicked the button`,
31+
debugUrl,
32+
sessionUrl,
33+
logs: logger.getLogs(),
34+
};
35+
}
36+
return {
37+
_success: false,
38+
message: `unable to click on the button`,
39+
debugUrl,
40+
sessionUrl,
41+
logs: logger.getLogs(),
42+
};
43+
} catch (error) {
44+
return {
45+
_success: false,
46+
message: `error: ${error.message}`,
47+
debugUrl,
48+
sessionUrl,
49+
logs: logger.getLogs(),
50+
};
51+
} finally {
52+
await stagehand.close();
53+
}
54+
};

evals/tasks/csr_in_spif.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const csr_in_spif: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
// this eval is designed to test whether stagehand can successfully
10+
// click inside an CSR (closed mode shadow) root that is inside an
11+
// SPIF (same process iframe)
12+
13+
const page = stagehand.page;
14+
try {
15+
await page.goto(
16+
"https://browserbase.github.io/stagehand-eval-sites/sites/closed-shadow-dom-in-spif/",
17+
);
18+
await page.act({ action: "click the button", iframes: true });
19+
20+
const extraction = await page.extract({
21+
instruction: "extract the entire page text",
22+
iframes: true,
23+
});
24+
25+
const pageText = extraction.extraction;
26+
27+
if (pageText.includes("button successfully clicked")) {
28+
return {
29+
_success: true,
30+
message: `successfully clicked the button`,
31+
debugUrl,
32+
sessionUrl,
33+
logs: logger.getLogs(),
34+
};
35+
}
36+
return {
37+
_success: false,
38+
message: `unable to click on the button`,
39+
debugUrl,
40+
sessionUrl,
41+
logs: logger.getLogs(),
42+
};
43+
} catch (error) {
44+
return {
45+
_success: false,
46+
message: `error: ${error.message}`,
47+
debugUrl,
48+
sessionUrl,
49+
logs: logger.getLogs(),
50+
};
51+
} finally {
52+
await stagehand.close();
53+
}
54+
};

evals/tasks/oopif_in_csr.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const oopif_in_csr: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
// this eval is designed to test whether stagehand can successfully
10+
// fill a form inside a OOPIF (out of process iframe) that is inside an
11+
// CSR (closed mode shadow) root
12+
13+
const page = stagehand.page;
14+
try {
15+
await page.goto(
16+
"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-open-shadow-dom/",
17+
);
18+
await page.act({
19+
action: "fill 'nunya' into the first name field",
20+
iframes: true,
21+
});
22+
23+
const extraction = await page.extract({
24+
instruction: "extract the entire page text",
25+
iframes: true,
26+
});
27+
28+
const pageText = extraction.extraction;
29+
30+
if (pageText.includes("nunya")) {
31+
return {
32+
_success: true,
33+
message: `successfully filled the form`,
34+
debugUrl,
35+
sessionUrl,
36+
logs: logger.getLogs(),
37+
};
38+
}
39+
return {
40+
_success: false,
41+
message: `unable to fill the form`,
42+
debugUrl,
43+
sessionUrl,
44+
logs: logger.getLogs(),
45+
};
46+
} catch (error) {
47+
return {
48+
_success: false,
49+
message: `error: ${error.message}`,
50+
debugUrl,
51+
sessionUrl,
52+
logs: logger.getLogs(),
53+
};
54+
} finally {
55+
await stagehand.close();
56+
}
57+
};

evals/tasks/oopif_in_osr.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const oopif_in_osr: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
// this eval is designed to test whether stagehand can successfully
10+
// fill a form inside a OOPIF (out of process iframe) that is inside an
11+
// OSR (open mode shadow) root
12+
13+
const page = stagehand.page;
14+
try {
15+
await page.goto(
16+
"https://browserbase.github.io/stagehand-eval-sites/sites/oopif-in-open-shadow-dom/",
17+
);
18+
await page.act({
19+
action: "fill 'nunya' into the first name field",
20+
iframes: true,
21+
});
22+
23+
const extraction = await page.extract({
24+
instruction: "extract the entire page text",
25+
iframes: true,
26+
});
27+
28+
const pageText = extraction.extraction;
29+
30+
if (pageText.includes("nunya")) {
31+
return {
32+
_success: true,
33+
message: `successfully filled the form`,
34+
debugUrl,
35+
sessionUrl,
36+
logs: logger.getLogs(),
37+
};
38+
}
39+
return {
40+
_success: false,
41+
message: `unable to fill the form`,
42+
debugUrl,
43+
sessionUrl,
44+
logs: logger.getLogs(),
45+
};
46+
} catch (error) {
47+
return {
48+
_success: false,
49+
message: `error: ${error.message}`,
50+
debugUrl,
51+
sessionUrl,
52+
logs: logger.getLogs(),
53+
};
54+
} finally {
55+
await stagehand.close();
56+
}
57+
};

evals/tasks/osr_in_oopif.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const osr_in_oopif: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
// this eval is designed to test whether stagehand can successfully
10+
// click inside an OSR (open mode shadow) root that is inside an
11+
// OOPIF (out of process iframe)
12+
13+
const page = stagehand.page;
14+
try {
15+
await page.goto(
16+
"https://browserbase.github.io/stagehand-eval-sites/sites/open-shadow-root-in-oopif/",
17+
);
18+
await page.act({ action: "click the button", iframes: true });
19+
20+
const extraction = await page.extract({
21+
instruction: "extract the entire page text",
22+
iframes: true,
23+
});
24+
25+
const pageText = extraction.extraction;
26+
27+
if (pageText.includes("button successfully clicked")) {
28+
return {
29+
_success: true,
30+
message: `successfully clicked the button`,
31+
debugUrl,
32+
sessionUrl,
33+
logs: logger.getLogs(),
34+
};
35+
}
36+
return {
37+
_success: false,
38+
message: `unable to click on the button`,
39+
debugUrl,
40+
sessionUrl,
41+
logs: logger.getLogs(),
42+
};
43+
} catch (error) {
44+
return {
45+
_success: false,
46+
message: `error: ${error.message}`,
47+
debugUrl,
48+
sessionUrl,
49+
logs: logger.getLogs(),
50+
};
51+
} finally {
52+
await stagehand.close();
53+
}
54+
};

evals/tasks/osr_in_spif.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { EvalFunction } from "@/types/evals";
2+
3+
export const osr_in_spif: EvalFunction = async ({
4+
debugUrl,
5+
sessionUrl,
6+
stagehand,
7+
logger,
8+
}) => {
9+
// this eval is designed to test whether stagehand can successfully
10+
// click inside an OSR (open mode shadow) root that is inside an
11+
// SPIF (same process iframe)
12+
13+
const page = stagehand.page;
14+
try {
15+
await page.goto(
16+
"https://browserbase.github.io/stagehand-eval-sites/sites/open-shadow-root-in-spif/",
17+
);
18+
await page.act({ action: "click the button", iframes: true });
19+
20+
const extraction = await page.extract({
21+
instruction: "extract the entire page text",
22+
iframes: true,
23+
});
24+
25+
const pageText = extraction.extraction;
26+
27+
if (pageText.includes("button successfully clicked")) {
28+
return {
29+
_success: true,
30+
message: `successfully clicked the button`,
31+
debugUrl,
32+
sessionUrl,
33+
logs: logger.getLogs(),
34+
};
35+
}
36+
return {
37+
_success: false,
38+
message: `unable to click on the button`,
39+
debugUrl,
40+
sessionUrl,
41+
logs: logger.getLogs(),
42+
};
43+
} catch (error) {
44+
return {
45+
_success: false,
46+
message: `error: ${error.message}`,
47+
debugUrl,
48+
sessionUrl,
49+
logs: logger.getLogs(),
50+
};
51+
} finally {
52+
await stagehand.close();
53+
}
54+
};

0 commit comments

Comments
 (0)