Cases / #31 · 2026-10-10 · Medium
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.
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
- Launch Chromium and load the test page; wait until the cross-origin frame has rendered its heading.
- Look for each target with
document.querySelectorfrom the top document. - Look for the same targets with Playwright locators and frame locators.
- Print what each approach found, and the frames the page has.
The code
The same program in five languages (also on GitHub, 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:[email protected]:8000
// npm install [email protected]
// 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();
}
# 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()
// 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();
}
}
}
}
// 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 get github.com/playwright-community/[email protected]
// 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
querySelectorfrom 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
>>>, noshadowRootwalking. - 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.