Problem
Page.content() only serializes light DOM — shadow root contents are invisible to it, which is expected per the DOM serialization spec. But Playwright's own CSS-based locators pierce open shadow roots by default when matching at runtime.
That mismatch is a real footgun for anything that builds selectors or automation from page.content()'s output (scrapers, selector generators, AI browser agents, test recorders): a CSS path that looks unique in the light-DOM-only HTML can still match a second element inside an open shadow root — silently, since matching succeeds and no error is raised. The result is a click (or any locator action) landing on the wrong element, indistinguishable from a correct one.
Minimal repro
html = """<html><body>
<div id="host"><button onclick="mark('light')">Light</button></div>
<script>
window.hits = [];
function mark(v) { window.hits.push(v) }
host.attachShadow({mode: 'open'}).innerHTML =
"<button onclick=\"window.hits.push('shadow')\">Shadow</button>";
</script>
</body></html>"""
page.set_content(html)
# Looks unique in page.content() — the shadow button is invisible to it.
assert page.content().count("<button") == 1
# But Playwright's own locator pierces the shadow root and finds BOTH:
assert page.locator("#host > button").count() == 2 # not 1
A tool that parses page.content(), sees one <button>, and hands back #host > button as "the unique selector" has just produced one that silently matches two live elements.
This was already proposed, and closed for inactivity — not rejected on merit
#30816 asked for exactly this (page.content({ serializableShadowRoots: true }) via Element.getHTML({ serializableShadowRoots: true })) and was closed after ~16 months with:
"If you have additional information not present in this issue that you think will help prioritizing it, please open a new issue and reference this one. More support or clarity on its necessity may prompt a review."
This is that — a concrete failure mode instead of a general "let the crawler grab everything," plus proof it's buildable.
Already built and validated, independently
Hit this building zerodom (an MCP server generating verified-unique CSS selectors for AI agents), and implemented exactly what #30816 proposed — walking the DOM for open shadow roots, serializing them via Element.getHTML({ serializableShadowRoots: true, shadowRoots }), and feeding the result back into selector generation so it can see (and correctly disambiguate against) shadow content it would otherwise be blind to.
Validated across 111 real sites — 1,334 of 10,756 audited selectors had to be scoped against open shadow roots (121 of 129 on shoelace.style alone), every one resolving to exactly one live element once shadow content was actually visible.
Ask
Not asking for that exact implementation — just that Page.content() (or a documented alternative) makes shadow-root content visible, so tools built on it stop silently producing selectors that look correct and aren't. Happy to share the full implementation/tests, or help however's useful given how content() is actually implemented across Chromium/Firefox/WebKit.
Problem
Page.content()only serializes light DOM — shadow root contents are invisible to it, which is expected per the DOM serialization spec. But Playwright's own CSS-based locators pierce open shadow roots by default when matching at runtime.That mismatch is a real footgun for anything that builds selectors or automation from
page.content()'s output (scrapers, selector generators, AI browser agents, test recorders): a CSS path that looks unique in the light-DOM-only HTML can still match a second element inside an open shadow root — silently, since matching succeeds and no error is raised. The result is a click (or any locator action) landing on the wrong element, indistinguishable from a correct one.Minimal repro
A tool that parses
page.content(), sees one<button>, and hands back#host > buttonas "the unique selector" has just produced one that silently matches two live elements.This was already proposed, and closed for inactivity — not rejected on merit
#30816 asked for exactly this (
page.content({ serializableShadowRoots: true })viaElement.getHTML({ serializableShadowRoots: true })) and was closed after ~16 months with:This is that — a concrete failure mode instead of a general "let the crawler grab everything," plus proof it's buildable.
Already built and validated, independently
Hit this building zerodom (an MCP server generating verified-unique CSS selectors for AI agents), and implemented exactly what #30816 proposed — walking the DOM for open shadow roots, serializing them via
Element.getHTML({ serializableShadowRoots: true, shadowRoots }), and feeding the result back into selector generation so it can see (and correctly disambiguate against) shadow content it would otherwise be blind to.Validated across 111 real sites — 1,334 of 10,756 audited selectors had to be scoped against open shadow roots (121 of 129 on shoelace.style alone), every one resolving to exactly one live element once shadow content was actually visible.
Ask
Not asking for that exact implementation — just that
Page.content()(or a documented alternative) makes shadow-root content visible, so tools built on it stop silently producing selectors that look correct and aren't. Happy to share the full implementation/tests, or help however's useful given howcontent()is actually implemented across Chromium/Firefox/WebKit.