[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #31 · 2026-10-10 · Example · Medium · for [web scraping](https://cdpfleet.com/web-scraping), [testing](https://cdpfleet.com/cross-browser-testing)

# Content you can't querySelector: iframes and shadow DOM

Same-origin and cross-origin iframes, open and nested shadow roots, and a closed one: what document.querySelector finds (nothing), what Playwright locators find (all but the closed root), and how.

[Chromium](https://cdpfleet.com/docs/browsers/chromium)

Run on production on 2026-10-10: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

## The problem

Modern pages hide content where a plain `document.querySelector` can't reach: widgets in iframes (often from another origin — payments, maps, chat), and design-system components that render inside shadow roots. A scraper that evaluates `querySelector` in the page comes back empty and looks broken. Which of these can you reach from Playwright, and with what?

## What we used, and why

| What | Why |
| --- | --- |
| `page.setContent()` with a test page | One page with every case — a `srcdoc` iframe, a cross-origin iframe (httpbin.org), an open shadow root with a nested one, and a closed shadow root — so the result doesn't depend on a third-party layout. |
| `document.querySelector` in `evaluate` | The baseline: what page JavaScript sees from the top document. |
| `page.frameLocator(sel).locator(…)` | Reaches into an iframe, same-origin or not — the remote browser runs out-of-process frames for you. |
| `page.locator(css)` | Playwright's CSS engine pierces open shadow roots at any depth. |
| `page.frames()` | Every frame as an object with its own URL and title. |

## How it works

1.  Launch Chromium and load the test page; wait until the cross-origin frame has rendered its heading.
2.  Look for each target with `document.querySelector` from the top document.
3.  Look for the same targets with Playwright locators and frame locators.
4.  Print what each approach found, and the frames the page has.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/31-iframes-and-shadow-dom), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)
-   `PROXY_URL` — your proxy, e.g. `http://user:pass@proxy.example.com:8000`

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// Content a plain document.querySelector can't see: a same-origin iframe, a cross-origin
// iframe, an open shadow root (with another one nested inside) and a closed shadow root.
const HTML = `<!doctype html><title>Hidden content</title>
<h1>Main document</h1>
<iframe id="same" srcdoc="<p id='inner'>same-origin iframe text</p>"></iframe>
<iframe id="cross" src="https://httpbin.org/html"></iframe>
<open-card></open-card>
<closed-card></closed-card>
<script>
customElements.define('open-card', class extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: 'open' });
    root.innerHTML = '<p class="msg">open shadow text</p><nested-badge></nested-badge>';
  }
});
customElements.define('nested-badge', class extends HTMLElement {
  connectedCallback() { this.attachShadow({ mode: 'open' }).innerHTML = '<span class="badge">nested shadow text</span>'; }
});
customElements.define('closed-card', class extends HTMLElement {
  connectedCallback() { this.attachShadow({ mode: 'closed' }).innerHTML = '<p class="secret">closed shadow text</p>'; }
});
</script>`;

const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
  method: 'POST',
  headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
  body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });

try {
  const page = await browser.newPage();
  await page.setContent(HTML);
  await page.frameLocator('#cross').locator('h1').waitFor({ timeout: 60000 });

  const qs = (sel) => page.evaluate((s) => document.querySelector(s)?.textContent ?? null, sel);
  const pw = async (loc) => (await loc.count()) ? (await loc.first().textContent()) : null;

  const rows = [
    { target: 'same-origin iframe', querySelector: await qs('#inner'), playwright: await pw(page.frameLocator('#same').locator('#inner')), how: "page.frameLocator('#same').locator('#inner')" },
    { target: 'cross-origin iframe', querySelector: await qs('h1 + div p'), playwright: await pw(page.frameLocator('#cross').locator('h1')), how: "page.frameLocator('#cross').locator('h1')" },
    { target: 'open shadow root', querySelector: await qs('.msg'), playwright: await pw(page.locator('.msg')), how: "page.locator('.msg') — CSS pierces open shadow roots" },
    { target: 'nested open shadow root', querySelector: await qs('.badge'), playwright: await pw(page.locator('.badge')), how: "page.locator('.badge') — any depth" },
    { target: 'closed shadow root', querySelector: await qs('.secret'), playwright: await pw(page.locator('.secret')), how: 'not reachable from page scripts or locators' },
  ];
  // The cross-origin frame is a separate document: its URL and title come from the frame object.
  const cross = page.frames().find((f) => f.url().startsWith('https://httpbin.org'));
  console.log(JSON.stringify({ frames: page.frames().length, cross_origin_frame: { url: cross?.url() ?? null, title: cross ? await cross.title() : null }, rows }, null, 2));
} finally {
  await browser.close();
}
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]

# Content a plain document.querySelector can't see: a same-origin iframe, a cross-origin
# iframe, an open shadow root (with another one nested inside) and a closed shadow root.
HTML = """<!doctype html><title>Hidden content</title>
<h1>Main document</h1>
<iframe id="same" srcdoc="<p id='inner'>same-origin iframe text</p>"></iframe>
<iframe id="cross" src="https://httpbin.org/html"></iframe>
<open-card></open-card>
<closed-card></closed-card>
<script>
customElements.define('open-card', class extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: 'open' });
    root.innerHTML = '<p class="msg">open shadow text</p><nested-badge></nested-badge>';
  }
});
customElements.define('nested-badge', class extends HTMLElement {
  connectedCallback() { this.attachShadow({ mode: 'open' }).innerHTML = '<span class="badge">nested shadow text</span>'; }
});
customElements.define('closed-card', class extends HTMLElement {
  connectedCallback() { this.attachShadow({ mode: 'closed' }).innerHTML = '<p class="secret">closed shadow text</p>'; }
});
</script>"""

res = requests.post("https://starter.cdpfleet.com/chromium/session",
    headers={"x-api-key": KEY},
    json={"proxy": os.environ["PROXY_URL"], "headless": "new"}, timeout=60)
if not res.ok:
    raise SystemExit(f"launch {res.status_code} {res.text}")

with sync_playwright() as p:
    browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
    try:
        page = browser.new_page()
        page.set_content(HTML)
        page.frame_locator("#cross").locator("h1").wait_for(timeout=60000)

        def qs(sel):
            return page.evaluate("(s) => document.querySelector(s)?.textContent ?? null", sel)

        def pw(loc):
            return loc.first.text_content() if loc.count() else None

        rows = [
            {"target": "same-origin iframe", "querySelector": qs("#inner"), "playwright": pw(page.frame_locator("#same").locator("#inner")), "how": "page.frameLocator('#same').locator('#inner')"},
            {"target": "cross-origin iframe", "querySelector": qs("h1 + div p"), "playwright": pw(page.frame_locator("#cross").locator("h1")), "how": "page.frameLocator('#cross').locator('h1')"},
            {"target": "open shadow root", "querySelector": qs(".msg"), "playwright": pw(page.locator(".msg")), "how": "page.locator('.msg') — CSS pierces open shadow roots"},
            {"target": "nested open shadow root", "querySelector": qs(".badge"), "playwright": pw(page.locator(".badge")), "how": "page.locator('.badge') — any depth"},
            {"target": "closed shadow root", "querySelector": qs(".secret"), "playwright": pw(page.locator(".secret")), "how": "not reachable from page scripts or locators"},
        ]
        # The cross-origin frame is a separate document: its URL and title come from the frame object.
        cross = next((f for f in page.frames if f.url.startswith("https://httpbin.org")), None)
        print(json.dumps({
            "frames": len(page.frames),
            "cross_origin_frame": {"url": cross.url if cross else None, "title": cross.title() if cross else None},
            "rows": rows,
        }, indent=2, ensure_ascii=False))
    finally:
        browser.close()
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final Gson GSON = new GsonBuilder().serializeNulls().create();

  // Content a plain document.querySelector can't see: a same-origin iframe, a cross-origin
  // iframe, an open shadow root (with another one nested inside) and a closed shadow root.
  static final String HTML = """
      <!doctype html><title>Hidden content</title>
      <h1>Main document</h1>
      <iframe id="same" srcdoc="<p id='inner'>same-origin iframe text</p>"></iframe>
      <iframe id="cross" src="https://httpbin.org/html"></iframe>
      <open-card></open-card>
      <closed-card></closed-card>
      <script>
      customElements.define('open-card', class extends HTMLElement {
        connectedCallback() {
          const root = this.attachShadow({ mode: 'open' });
          root.innerHTML = '<p class="msg">open shadow text</p><nested-badge></nested-badge>';
        }
      });
      customElements.define('nested-badge', class extends HTMLElement {
        connectedCallback() { this.attachShadow({ mode: 'open' }).innerHTML = '<span class="badge">nested shadow text</span>'; }
      });
      customElements.define('closed-card', class extends HTMLElement {
        connectedCallback() { this.attachShadow({ mode: 'closed' }).innerHTML = '<p class="secret">closed shadow text</p>'; }
      });
      </script>""";

  static String qs(Page page, String sel) {
    return (String) page.evaluate("(s) => document.querySelector(s)?.textContent ?? null", sel);
  }

  static String pw(Locator loc) {
    return loc.count() > 0 ? loc.first().textContent() : null;
  }

  static JsonObject row(String target, String querySelector, String playwright, String how) {
    JsonObject r = new JsonObject();
    r.addProperty("target", target);
    r.addProperty("querySelector", querySelector);
    r.addProperty("playwright", playwright);
    r.addProperty("how", how);
    return r;
  }

  public static void main(String[] args) throws Exception {
    String body = "{\"proxy\": " + GSON.toJson(System.getenv("PROXY_URL")) + ", \"headless\": \"new\"}";
    HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
        .header("x-api-key", KEY).header("content-type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
    if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
    String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();

    try (Playwright playwright = Playwright.create()) {
      Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      try {
        Page page = browser.newPage();
        page.setContent(HTML);
        page.frameLocator("#cross").locator("h1").waitFor(new Locator.WaitForOptions().setTimeout(60000));

        JsonArray rows = new JsonArray();
        rows.add(row("same-origin iframe", qs(page, "#inner"), pw(page.frameLocator("#same").locator("#inner")), "page.frameLocator('#same').locator('#inner')"));
        rows.add(row("cross-origin iframe", qs(page, "h1 + div p"), pw(page.frameLocator("#cross").locator("h1")), "page.frameLocator('#cross').locator('h1')"));
        rows.add(row("open shadow root", qs(page, ".msg"), pw(page.locator(".msg")), "page.locator('.msg') — CSS pierces open shadow roots"));
        rows.add(row("nested open shadow root", qs(page, ".badge"), pw(page.locator(".badge")), "page.locator('.badge') — any depth"));
        rows.add(row("closed shadow root", qs(page, ".secret"), pw(page.locator(".secret")), "not reachable from page scripts or locators"));

        // The cross-origin frame is a separate document: its URL and title come from the frame object.
        Frame cross = page.frames().stream().filter(f -> f.url().startsWith("https://httpbin.org")).findFirst().orElse(null);
        JsonObject crossOut = new JsonObject();
        crossOut.addProperty("url", cross != null ? cross.url() : null);
        crossOut.addProperty("title", cross != null ? cross.title() : null);

        JsonObject out = new JsonObject();
        out.addProperty("frames", page.frames().size());
        out.add("cross_origin_frame", crossOut);
        out.add("rows", rows);

        System.out.println(new GsonBuilder().serializeNulls().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
      } finally {
        browser.close();
      }
    }
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

// Content a plain document.querySelector can't see: a same-origin iframe, a cross-origin
// iframe, an open shadow root (with another one nested inside) and a closed shadow root.
const string Html = """
    <!doctype html><title>Hidden content</title>
    <h1>Main document</h1>
    <iframe id="same" srcdoc="<p id='inner'>same-origin iframe text</p>"></iframe>
    <iframe id="cross" src="https://httpbin.org/html"></iframe>
    <open-card></open-card>
    <closed-card></closed-card>
    <script>
    customElements.define('open-card', class extends HTMLElement {
      connectedCallback() {
        const root = this.attachShadow({ mode: 'open' });
        root.innerHTML = '<p class="msg">open shadow text</p><nested-badge></nested-badge>';
      }
    });
    customElements.define('nested-badge', class extends HTMLElement {
      connectedCallback() { this.attachShadow({ mode: 'open' }).innerHTML = '<span class="badge">nested shadow text</span>'; }
    });
    customElements.define('closed-card', class extends HTMLElement {
      connectedCallback() { this.attachShadow({ mode: 'closed' }).innerHTML = '<p class="secret">closed shadow text</p>'; }
    });
    </script>
    """;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
    new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;

using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
    var page = await browser.NewPageAsync();
    await page.SetContentAsync(Html);
    await page.FrameLocator("#cross").Locator("h1").WaitForAsync(new() { Timeout = 60000 });

    async Task<string?> Qs(string sel) => await page.EvaluateAsync<string?>("(s) => document.querySelector(s)?.textContent ?? null", sel);
    async Task<string?> Pw(ILocator loc) => await loc.CountAsync() > 0 ? await loc.First.TextContentAsync() : null;
    JsonObject Row(string target, string? querySelector, string? playwright, string how) =>
        new() { ["target"] = target, ["querySelector"] = querySelector, ["playwright"] = playwright, ["how"] = how };

    var rows = new JsonArray
    {
        Row("same-origin iframe", await Qs("#inner"), await Pw(page.FrameLocator("#same").Locator("#inner")), "page.frameLocator('#same').locator('#inner')"),
        Row("cross-origin iframe", await Qs("h1 + div p"), await Pw(page.FrameLocator("#cross").Locator("h1")), "page.frameLocator('#cross').locator('h1')"),
        Row("open shadow root", await Qs(".msg"), await Pw(page.Locator(".msg")), "page.locator('.msg') — CSS pierces open shadow roots"),
        Row("nested open shadow root", await Qs(".badge"), await Pw(page.Locator(".badge")), "page.locator('.badge') — any depth"),
        Row("closed shadow root", await Qs(".secret"), await Pw(page.Locator(".secret")), "not reachable from page scripts or locators"),
    };
    // The cross-origin frame is a separate document: its URL and title come from the frame object.
    var cross = page.Frames.FirstOrDefault(f => f.Url.StartsWith("https://httpbin.org"));

    var result = new JsonObject
    {
        ["frames"] = page.Frames.Count,
        ["cross_origin_frame"] = new JsonObject { ["url"] = cross?.Url, ["title"] = cross != null ? await cross.TitleAsync() : null },
        ["rows"] = rows,
    };
    Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
    await browser.CloseAsync();
}
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

// Content a plain document.querySelector can't see: a same-origin iframe, a cross-origin
// iframe, an open shadow root (with another one nested inside) and a closed shadow root.
const html = `<!doctype html><title>Hidden content</title>
<h1>Main document</h1>
<iframe id="same" srcdoc="<p id='inner'>same-origin iframe text</p>"></iframe>
<iframe id="cross" src="https://httpbin.org/html"></iframe>
<open-card></open-card>
<closed-card></closed-card>
<script>
customElements.define('open-card', class extends HTMLElement {
  connectedCallback() {
    const root = this.attachShadow({ mode: 'open' });
    root.innerHTML = '<p class="msg">open shadow text</p><nested-badge></nested-badge>';
  }
});
customElements.define('nested-badge', class extends HTMLElement {
  connectedCallback() { this.attachShadow({ mode: 'open' }).innerHTML = '<span class="badge">nested shadow text</span>'; }
});
customElements.define('closed-card', class extends HTMLElement {
  connectedCallback() { this.attachShadow({ mode: 'closed' }).innerHTML = '<p class="secret">closed shadow text</p>'; }
});
</script>`

func launch(name string, options map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(options)
	req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
	req.Header.Set("x-api-key", key)
	req.Header.Set("content-type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != http.StatusOK {
		msg, _ := io.ReadAll(res.Body)
		return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
	}
	var session map[string]any
	return session, json.NewDecoder(res.Body).Decode(&session)
}

type row struct {
	Target        string  `json:"target"`
	QuerySelector *string `json:"querySelector"`
	Playwright    *string `json:"playwright"`
	How           string  `json:"how"`
}

type crossFrame struct {
	URL   *string `json:"url"`
	Title *string `json:"title"`
}

type output struct {
	Frames           int        `json:"frames"`
	CrossOriginFrame crossFrame `json:"cross_origin_frame"`
	Rows             []row      `json:"rows"`
}

// qs runs a plain document.querySelector in the top document.
func qs(page playwright.Page, sel string) *string {
	v, err := page.Evaluate("(s) => document.querySelector(s)?.textContent ?? null", sel)
	if err != nil {
		log.Fatal(err)
	}
	if s, ok := v.(string); ok {
		return &s
	}
	return nil
}

// pw returns the first match's text, or nil if the locator finds nothing.
func pw(loc playwright.Locator) *string {
	n, err := loc.Count()
	if err != nil {
		log.Fatal(err)
	}
	if n == 0 {
		return nil
	}
	s, err := loc.First().TextContent()
	if err != nil {
		log.Fatal(err)
	}
	return &s
}

func main() {
	session, err := launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": "new"})
	if err != nil {
		log.Fatal(err)
	}
	pwr, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
	if err != nil {
		log.Fatal(err)
	}
	defer pwr.Stop()
	browser, err := pwr.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
	if err != nil {
		log.Fatal(err)
	}
	defer browser.Close()

	page, err := browser.NewPage()
	if err != nil {
		log.Fatal(err)
	}
	if err := page.SetContent(html); err != nil {
		log.Fatal(err)
	}
	if err := page.FrameLocator("#cross").Locator("h1").WaitFor(playwright.LocatorWaitForOptions{Timeout: playwright.Float(60000)}); err != nil {
		log.Fatal(err)
	}

	rows := []row{
		{"same-origin iframe", qs(page, "#inner"), pw(page.FrameLocator("#same").Locator("#inner")), "page.frameLocator('#same').locator('#inner')"},
		{"cross-origin iframe", qs(page, "h1 + div p"), pw(page.FrameLocator("#cross").Locator("h1")), "page.frameLocator('#cross').locator('h1')"},
		{"open shadow root", qs(page, ".msg"), pw(page.Locator(".msg")), "page.locator('.msg') — CSS pierces open shadow roots"},
		{"nested open shadow root", qs(page, ".badge"), pw(page.Locator(".badge")), "page.locator('.badge') — any depth"},
		{"closed shadow root", qs(page, ".secret"), pw(page.Locator(".secret")), "not reachable from page scripts or locators"},
	}

	// The cross-origin frame is a separate document: its URL and title come from the frame object.
	out := output{Frames: len(page.Frames()), Rows: rows}
	for _, f := range page.Frames() {
		if strings.HasPrefix(f.URL(), "https://httpbin.org") {
			url := f.URL()
			title, err := f.Title()
			if err != nil {
				log.Fatal(err)
			}
			out.CrossOriginFrame = crossFrame{URL: &url, Title: &title}
			break
		}
	}

	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	enc.SetEscapeHTML(false)
	enc.SetIndent("", "  ")
	enc.Encode(out)
	fmt.Print(buf.String())
}
```

## What we got

| Target | document.querySelector | Playwright | How |
| --- | --- | --- | --- |
| same-origin iframe | — (not found) | same-origin iframe text | page.frameLocator('#same').locator('#inner') |
| cross-origin iframe | — (not found) | Herman Melville - Moby-Dick | page.frameLocator('#cross').locator('h1') |
| open shadow root | — (not found) | open shadow text | page.locator('.msg') — CSS pierces open shadow roots |
| nested open shadow root | — (not found) | nested shadow text | page.locator('.badge') — any depth |
| closed shadow root | — (not found) | — (not found) | not reachable from page scripts or locators |

From the Node.js run on 2026-10-10. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
{
  "frames": 3,
  "cross_origin_frame": {
    "url": "https://httpbin.org/html",
    "title": ""
  },
  "rows": [
    {
      "target": "same-origin iframe",
      "querySelector": null,
      "playwright": "same-origin iframe text",
      "how": "page.frameLocator('#same').locator('#inner')"
    },
    {
      "target": "cross-origin iframe",
      "querySelector": null,
      "playwright": "Herman Melville - Moby-Dick",
      "how": "page.frameLocator('#cross').locator('h1')"
    },
    {
      "target": "open shadow root",
      "querySelector": null,
      "playwright": "open shadow text",
      "how": "page.locator('.msg') — CSS pierces open shadow roots"
    },
    {
      "target": "nested open shadow root",
      "querySelector": null,
      "playwright": "nested shadow text",
      "how": "page.locator('.badge') — any depth"
    },
    {
      "target": "closed shadow root",
      "querySelector": null,
      "playwright": null,
      "how": "not reachable from page scripts or locators"
    }
  ]
}
```

## Takeaways

-   **`querySelector` from the top document finds none of it** — not the iframes' content, not even the open shadow roots.
-   **Frame locators reach both iframes,** including the cross-origin one: Playwright talks to each frame's own process, which page JavaScript can't.
-   **Locators pierce open shadow roots at any depth** with plain CSS — no `>>>`, no `shadowRoot` walking.
-   **Closed shadow roots stay closed:** neither page scripts nor locators can read them. Use what they render (screenshots, accessibility tree) or the component's public API.
-   **Every language uses the same calls** (`frameLocator`, `locator`) — the five programs print identical results.

---
Source: https://cdpfleet.com/docs/cases/iframes-and-shadow-dom · Markdown: https://cdpfleet.com/docs/cases/iframes-and-shadow-dom.md · All docs for AI tools: https://cdpfleet.com/llms.txt
