@flrande/bak-extension 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ 2026-03-08T17:10:39.579Z
@@ -1,5 +1,35 @@
1
1
  "use strict";
2
2
  (() => {
3
+ // src/context-metadata.ts
4
+ var DOCUMENT_NODE = 9;
5
+ var DOCUMENT_FRAGMENT_NODE = 11;
6
+ function isDocumentNode(value) {
7
+ return Boolean(
8
+ value && typeof value === "object" && "nodeType" in value && typeof value.nodeType === "number" && value.nodeType === DOCUMENT_NODE
9
+ );
10
+ }
11
+ function isShadowRootNode(value) {
12
+ return Boolean(
13
+ value && typeof value === "object" && "nodeType" in value && typeof value.nodeType === "number" && value.nodeType === DOCUMENT_FRAGMENT_NODE && "host" in value
14
+ );
15
+ }
16
+ function ownerDocumentForRoot(root, fallbackDocument) {
17
+ if (isDocumentNode(root)) {
18
+ return root;
19
+ }
20
+ if (isShadowRootNode(root)) {
21
+ return root.host.ownerDocument ?? fallbackDocument;
22
+ }
23
+ return root.ownerDocument ?? fallbackDocument;
24
+ }
25
+ function documentMetadata(root, fallbackDocument) {
26
+ const ownerDocument = ownerDocumentForRoot(root, fallbackDocument);
27
+ return {
28
+ url: ownerDocument.location?.href ?? fallbackDocument.location?.href ?? "",
29
+ title: ownerDocument.title ?? fallbackDocument.title ?? ""
30
+ };
31
+ }
32
+
3
33
  // src/privacy.ts
4
34
  var MAX_SAFE_TEXT_LENGTH = 120;
5
35
  var MAX_DEBUG_TEXT_LENGTH = 320;
@@ -170,6 +200,18 @@
170
200
  }
171
201
  }
172
202
  function patchConsoleCapture() {
203
+ const handleConsoleBridgeEvent = (event) => {
204
+ const detail = event.detail;
205
+ if (!detail || typeof detail !== "object") {
206
+ return;
207
+ }
208
+ const level = detail.level === "debug" || detail.level === "info" || detail.level === "warn" || detail.level === "error" ? detail.level : "log";
209
+ const message = typeof detail.message === "string" ? detail.message : "";
210
+ if (!message) {
211
+ return;
212
+ }
213
+ pushConsole(level, message, detail.source ?? "page");
214
+ };
173
215
  const methods = [
174
216
  { method: "log", level: "log" },
175
217
  { method: "debug", level: "debug" },
@@ -197,18 +239,8 @@
197
239
  original.apply(console, args);
198
240
  };
199
241
  }
200
- window.addEventListener("bak:console", (event) => {
201
- const detail = event.detail;
202
- if (!detail || typeof detail !== "object") {
203
- return;
204
- }
205
- const level = detail.level === "debug" || detail.level === "info" || detail.level === "warn" || detail.level === "error" ? detail.level : "log";
206
- const message = typeof detail.message === "string" ? detail.message : "";
207
- if (!message) {
208
- return;
209
- }
210
- pushConsole(level, message, detail.source ?? "page");
211
- });
242
+ document.addEventListener("bak:console", handleConsoleBridgeEvent);
243
+ window.addEventListener("bak:console", handleConsoleBridgeEvent);
212
244
  try {
213
245
  const injector = document.createElement("script");
214
246
  injector.textContent = `
@@ -217,7 +249,11 @@
217
249
  if (g.__bakPageConsolePatched) return;
218
250
  g.__bakPageConsolePatched = true;
219
251
  const emit = (level, message, source) =>
220
- window.dispatchEvent(new CustomEvent('bak:console', { detail: { level, message, source, ts: Date.now() } }));
252
+ document.dispatchEvent(new CustomEvent('bak:console', {
253
+ bubbles: true,
254
+ composed: true,
255
+ detail: { level, message, source, ts: Date.now() }
256
+ }));
221
257
  const serialize = (value) => {
222
258
  if (value instanceof Error) return value.message;
223
259
  if (typeof value === 'string') return value;
@@ -262,7 +298,7 @@
262
298
  }
263
299
  }
264
300
  function patchNetworkCapture() {
265
- window.addEventListener("bak:network", (event) => {
301
+ const handleNetworkBridgeEvent = (event) => {
266
302
  const detail = event.detail;
267
303
  if (!detail || typeof detail !== "object") {
268
304
  return;
@@ -279,7 +315,9 @@
279
315
  requestBytes: typeof detail.requestBytes === "number" ? detail.requestBytes : void 0,
280
316
  responseBytes: typeof detail.responseBytes === "number" ? detail.responseBytes : void 0
281
317
  });
282
- });
318
+ };
319
+ document.addEventListener("bak:network", handleNetworkBridgeEvent);
320
+ window.addEventListener("bak:network", handleNetworkBridgeEvent);
283
321
  try {
284
322
  const injector = document.createElement("script");
285
323
  injector.textContent = `
@@ -288,7 +326,11 @@
288
326
  if (g.__bakPageNetworkPatched) return;
289
327
  g.__bakPageNetworkPatched = true;
290
328
  let seq = 0;
291
- const emit = (entry) => window.dispatchEvent(new CustomEvent('bak:network', { detail: entry }));
329
+ const emit = (entry) => document.dispatchEvent(new CustomEvent('bak:network', {
330
+ bubbles: true,
331
+ composed: true,
332
+ detail: entry
333
+ }));
292
334
  const nativeFetch = window.fetch.bind(window);
293
335
  window.fetch = async (input, init) => {
294
336
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
@@ -1081,9 +1123,13 @@
1081
1123
  return failAssessment("E_NOT_FOUND", `${action} target is outside viewport`);
1082
1124
  }
1083
1125
  let shadowHostBridge = false;
1084
- const rootNode = target.getRootNode();
1085
- if (rootNode instanceof ShadowRoot) {
1086
- shadowHostBridge = hit === rootNode.host;
1126
+ let rootNode = target.getRootNode();
1127
+ while (isShadowRootNode(rootNode)) {
1128
+ if (hit === rootNode.host) {
1129
+ shadowHostBridge = true;
1130
+ break;
1131
+ }
1132
+ rootNode = rootNode.host.getRootNode();
1087
1133
  }
1088
1134
  if (hit !== target && !target.contains(hit) && !shadowHostBridge) {
1089
1135
  return failAssessment("E_PERMISSION", `${action} target is obstructed by ${describeNode(hit)}`, {
@@ -1201,31 +1247,32 @@
1201
1247
  metaKey: lowered.includes("meta") || lowered.includes("cmd")
1202
1248
  };
1203
1249
  }
1204
- function domSummary() {
1205
- const allElements = Array.from(document.querySelectorAll("*"));
1206
- const interactiveElements = Array.from(document.querySelectorAll("*")).filter((element) => isInteractive(element));
1250
+ function domSummary(root) {
1251
+ const metadata = documentMetadata(root, document);
1252
+ const allElements = Array.from(root.querySelectorAll("*"));
1253
+ const interactiveElements = Array.from(root.querySelectorAll("*")).filter((element) => isInteractive(element));
1207
1254
  const tags = /* @__PURE__ */ new Map();
1208
1255
  for (const element of allElements) {
1209
1256
  const tag = element.tagName.toLowerCase();
1210
1257
  tags.set(tag, (tags.get(tag) ?? 0) + 1);
1211
1258
  }
1212
1259
  const tagHistogram = [...tags.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([tag, count]) => ({ tag, count }));
1213
- const shadowHosts = Array.from(document.querySelectorAll("*")).filter((element) => element.shadowRoot).length;
1260
+ const shadowHosts = Array.from(root.querySelectorAll("*")).filter((element) => element.shadowRoot).length;
1214
1261
  return {
1215
- url: window.location.href,
1216
- title: document.title,
1262
+ url: metadata.url,
1263
+ title: metadata.title,
1217
1264
  totalElements: allElements.length,
1218
1265
  interactiveElements: interactiveElements.length,
1219
- headings: document.querySelectorAll("h1,h2,h3,h4,h5,h6").length,
1220
- links: document.querySelectorAll("a[href]").length,
1221
- forms: document.querySelectorAll("form").length,
1222
- iframes: document.querySelectorAll("iframe,frame").length,
1266
+ headings: root.querySelectorAll("h1,h2,h3,h4,h5,h6").length,
1267
+ links: root.querySelectorAll("a[href]").length,
1268
+ forms: root.querySelectorAll("form").length,
1269
+ iframes: root.querySelectorAll("iframe,frame").length,
1223
1270
  shadowHosts,
1224
1271
  tagHistogram
1225
1272
  };
1226
1273
  }
1227
- function pageTextChunks(maxChunks = 24, chunkSize = 320) {
1228
- const nodes = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,td,th,label,button,a,span,div"));
1274
+ function pageTextChunks(root, maxChunks = 24, chunkSize = 320) {
1275
+ const nodes = Array.from(root.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,td,th,label,button,a,span,div"));
1229
1276
  const chunks = [];
1230
1277
  for (const node of nodes) {
1231
1278
  if (!isElementVisible(node)) {
@@ -1246,9 +1293,9 @@
1246
1293
  }
1247
1294
  return chunks;
1248
1295
  }
1249
- function pageAccessibility(limit = 200) {
1296
+ function pageAccessibility(root, limit = 200) {
1250
1297
  const nodes = [];
1251
- for (const element of getInteractiveElements(document, true).slice(0, limit)) {
1298
+ for (const element of getInteractiveElements(root, true).slice(0, limit)) {
1252
1299
  nodes.push({
1253
1300
  role: inferRole(element),
1254
1301
  name: inferName(element),
@@ -1531,7 +1578,7 @@
1531
1578
  return Boolean(querySelectorAcrossOpenShadow(root, message.value));
1532
1579
  }
1533
1580
  if (message.mode === "text") {
1534
- if (root instanceof Document) {
1581
+ if (isDocumentNode(root)) {
1535
1582
  const bodyText = root.body?.innerText ?? root.documentElement?.textContent ?? "";
1536
1583
  return bodyText.includes(message.value);
1537
1584
  }
@@ -1574,7 +1621,7 @@
1574
1621
  const observer = new MutationObserver(() => {
1575
1622
  check();
1576
1623
  });
1577
- const observationRoot = root instanceof Document ? root.documentElement : root;
1624
+ const observationRoot = isDocumentNode(root) ? root.documentElement : root;
1578
1625
  if (observationRoot) {
1579
1626
  observer.observe(observationRoot, {
1580
1627
  childList: true,
@@ -1593,23 +1640,49 @@
1593
1640
  }
1594
1641
  async function dispatchRpc(method, params = {}) {
1595
1642
  switch (method) {
1596
- case "page.title":
1597
- return { title: document.title };
1598
- case "page.url":
1599
- return { url: window.location.href };
1600
- case "page.text":
1643
+ case "page.title": {
1644
+ const rootResult = resolveRootForLocator();
1645
+ if (!rootResult.ok) {
1646
+ throw rootResult.error;
1647
+ }
1648
+ return { title: documentMetadata(rootResult.root, document).title };
1649
+ }
1650
+ case "page.url": {
1651
+ const rootResult = resolveRootForLocator();
1652
+ if (!rootResult.ok) {
1653
+ throw rootResult.error;
1654
+ }
1655
+ return { url: documentMetadata(rootResult.root, document).url };
1656
+ }
1657
+ case "page.text": {
1658
+ const rootResult = resolveRootForLocator();
1659
+ if (!rootResult.ok) {
1660
+ throw rootResult.error;
1661
+ }
1601
1662
  return {
1602
1663
  chunks: pageTextChunks(
1664
+ rootResult.root,
1603
1665
  typeof params.maxChunks === "number" ? params.maxChunks : 24,
1604
1666
  typeof params.chunkSize === "number" ? params.chunkSize : 320
1605
1667
  )
1606
1668
  };
1607
- case "page.dom":
1608
- return { summary: domSummary() };
1609
- case "page.accessibilityTree":
1669
+ }
1670
+ case "page.dom": {
1671
+ const rootResult = resolveRootForLocator();
1672
+ if (!rootResult.ok) {
1673
+ throw rootResult.error;
1674
+ }
1675
+ return { summary: domSummary(rootResult.root) };
1676
+ }
1677
+ case "page.accessibilityTree": {
1678
+ const rootResult = resolveRootForLocator();
1679
+ if (!rootResult.ok) {
1680
+ throw rootResult.error;
1681
+ }
1610
1682
  return {
1611
- nodes: pageAccessibility(typeof params.limit === "number" ? params.limit : 200)
1683
+ nodes: pageAccessibility(rootResult.root, typeof params.limit === "number" ? params.limit : 200)
1612
1684
  };
1685
+ }
1613
1686
  case "page.scrollTo": {
1614
1687
  const x = typeof params.x === "number" ? params.x : window.scrollX;
1615
1688
  const y = typeof params.y === "number" ? params.y : window.scrollY;
@@ -1928,12 +2001,12 @@
1928
2001
  if (hostSelectors.length === 0) {
1929
2002
  throw { code: "E_INVALID_PARAMS", message: "hostSelectors or locator.css is required" };
1930
2003
  }
1931
- const rootResult = resolveRootForLocator();
1932
- if (!rootResult.ok) {
1933
- throw rootResult.error;
2004
+ const frameResult = resolveFrameDocument(contextState.framePath);
2005
+ if (!frameResult.ok) {
2006
+ throw frameResult.error;
1934
2007
  }
1935
2008
  const candidate = [...contextState.shadowPath, ...hostSelectors];
1936
- const check = resolveShadowRoot(rootResult.root, candidate);
2009
+ const check = resolveShadowRoot(frameResult.document, candidate);
1937
2010
  if (!check.ok) {
1938
2011
  throw check.error;
1939
2012
  }
@@ -1973,18 +2046,30 @@
1973
2046
  const consoleLimit = typeof params.consoleLimit === "number" ? Math.max(1, Math.floor(params.consoleLimit)) : 80;
1974
2047
  const networkLimit = typeof params.networkLimit === "number" ? Math.max(1, Math.floor(params.networkLimit)) : 80;
1975
2048
  const includeAccessibility = params.includeAccessibility === true;
2049
+ const rootResult = resolveRootForLocator();
2050
+ if (!rootResult.ok) {
2051
+ throw rootResult.error;
2052
+ }
2053
+ const metadata = documentMetadata(rootResult.root, document);
1976
2054
  return {
1977
- url: window.location.href,
1978
- title: document.title,
2055
+ url: metadata.url,
2056
+ title: metadata.title,
1979
2057
  context: {
1980
2058
  framePath: [...contextState.framePath],
1981
2059
  shadowPath: [...contextState.shadowPath]
1982
2060
  },
1983
- dom: domSummary(),
1984
- text: pageTextChunks(12, 260),
2061
+ dom: domSummary(rootResult.root),
2062
+ text: pageTextChunks(rootResult.root, 12, 260),
2063
+ elements: collectElements(),
2064
+ metrics: pageMetrics(),
2065
+ viewport: {
2066
+ width: window.innerWidth,
2067
+ height: window.innerHeight,
2068
+ devicePixelRatio: window.devicePixelRatio
2069
+ },
1985
2070
  console: consoleEntries.slice(-consoleLimit),
1986
2071
  network: filterNetworkEntries({ limit: networkLimit }),
1987
- accessibility: includeAccessibility ? pageAccessibility(200) : void 0
2072
+ accessibility: includeAccessibility ? pageAccessibility(rootResult.root, 200) : void 0
1988
2073
  };
1989
2074
  }
1990
2075
  default:
package/package.json CHANGED
@@ -1,18 +1,20 @@
1
1
  {
2
2
  "name": "@flrande/bak-extension",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
- "dependencies": {
6
- "@flrande/bak-protocol": "0.2.3"
7
- },
8
- "devDependencies": {
9
- "@types/chrome": "^0.1.14",
10
- "tsup": "^8.5.0"
11
- },
12
5
  "scripts": {
13
6
  "build": "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
14
7
  "dev": "node scripts/copy-assets.mjs && tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --watch",
15
8
  "typecheck": "tsc -p tsconfig.json --noEmit",
16
9
  "lint": "eslint src --ext .ts"
10
+ },
11
+ "dependencies": {
12
+ "@flrande/bak-protocol": "workspace:*"
13
+ },
14
+ "devDependencies": {
15
+ "@types/chrome": "^0.1.14",
16
+ "tsup": "^8.5.0"
17
17
  }
18
- }
18
+ }
19
+
20
+
package/src/content.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  PageMetrics,
9
9
  PageTextChunk
10
10
  } from '@flrande/bak-protocol';
11
+ import { documentMetadata, isDocumentNode, isShadowRootNode } from './context-metadata.js';
11
12
  import { inferSafeName, redactElementText, type RedactTextOptions } from './privacy.js';
12
13
  import { unsupportedLocatorHint } from './limitations.js';
13
14
 
@@ -130,6 +131,20 @@ function pushConsole(level: ConsoleEntry['level'], message: string, source?: str
130
131
  }
131
132
 
132
133
  function patchConsoleCapture(): void {
134
+ const handleConsoleBridgeEvent = (event: Event): void => {
135
+ const detail = (event as CustomEvent<{ level?: ConsoleEntry['level']; message?: string; source?: string; ts?: number }>).detail;
136
+ if (!detail || typeof detail !== 'object') {
137
+ return;
138
+ }
139
+ const level: ConsoleEntry['level'] =
140
+ detail.level === 'debug' || detail.level === 'info' || detail.level === 'warn' || detail.level === 'error' ? detail.level : 'log';
141
+ const message = typeof detail.message === 'string' ? detail.message : '';
142
+ if (!message) {
143
+ return;
144
+ }
145
+ pushConsole(level, message, detail.source ?? 'page');
146
+ };
147
+
133
148
  const methods: Array<{ method: 'log' | 'debug' | 'info' | 'warn' | 'error'; level: ConsoleEntry['level'] }> = [
134
149
  { method: 'log', level: 'log' },
135
150
  { method: 'debug', level: 'debug' },
@@ -161,19 +176,8 @@ function patchConsoleCapture(): void {
161
176
  };
162
177
  }
163
178
 
164
- window.addEventListener('bak:console', (event: Event) => {
165
- const detail = (event as CustomEvent<{ level?: ConsoleEntry['level']; message?: string; source?: string; ts?: number }>).detail;
166
- if (!detail || typeof detail !== 'object') {
167
- return;
168
- }
169
- const level: ConsoleEntry['level'] =
170
- detail.level === 'debug' || detail.level === 'info' || detail.level === 'warn' || detail.level === 'error' ? detail.level : 'log';
171
- const message = typeof detail.message === 'string' ? detail.message : '';
172
- if (!message) {
173
- return;
174
- }
175
- pushConsole(level, message, detail.source ?? 'page');
176
- });
179
+ document.addEventListener('bak:console', handleConsoleBridgeEvent as EventListener);
180
+ window.addEventListener('bak:console', handleConsoleBridgeEvent as EventListener);
177
181
 
178
182
  try {
179
183
  const injector = document.createElement('script');
@@ -183,7 +187,11 @@ function patchConsoleCapture(): void {
183
187
  if (g.__bakPageConsolePatched) return;
184
188
  g.__bakPageConsolePatched = true;
185
189
  const emit = (level, message, source) =>
186
- window.dispatchEvent(new CustomEvent('bak:console', { detail: { level, message, source, ts: Date.now() } }));
190
+ document.dispatchEvent(new CustomEvent('bak:console', {
191
+ bubbles: true,
192
+ composed: true,
193
+ detail: { level, message, source, ts: Date.now() }
194
+ }));
187
195
  const serialize = (value) => {
188
196
  if (value instanceof Error) return value.message;
189
197
  if (typeof value === 'string') return value;
@@ -233,7 +241,7 @@ function pushNetwork(entry: NetworkEntry): void {
233
241
  }
234
242
 
235
243
  function patchNetworkCapture(): void {
236
- window.addEventListener('bak:network', (event: Event) => {
244
+ const handleNetworkBridgeEvent = (event: Event): void => {
237
245
  const detail = (event as CustomEvent<NetworkEntry>).detail;
238
246
  if (!detail || typeof detail !== 'object') {
239
247
  return;
@@ -247,10 +255,13 @@ function patchNetworkCapture(): void {
247
255
  ok: detail.ok === true,
248
256
  ts: typeof detail.ts === 'number' ? detail.ts : Date.now(),
249
257
  durationMs: typeof detail.durationMs === 'number' ? detail.durationMs : 0,
250
- requestBytes: typeof detail.requestBytes === 'number' ? detail.requestBytes : undefined,
251
- responseBytes: typeof detail.responseBytes === 'number' ? detail.responseBytes : undefined
252
- });
253
- });
258
+ requestBytes: typeof detail.requestBytes === 'number' ? detail.requestBytes : undefined,
259
+ responseBytes: typeof detail.responseBytes === 'number' ? detail.responseBytes : undefined
260
+ });
261
+ };
262
+
263
+ document.addEventListener('bak:network', handleNetworkBridgeEvent as EventListener);
264
+ window.addEventListener('bak:network', handleNetworkBridgeEvent as EventListener);
254
265
 
255
266
  try {
256
267
  const injector = document.createElement('script');
@@ -260,7 +271,11 @@ function patchNetworkCapture(): void {
260
271
  if (g.__bakPageNetworkPatched) return;
261
272
  g.__bakPageNetworkPatched = true;
262
273
  let seq = 0;
263
- const emit = (entry) => window.dispatchEvent(new CustomEvent('bak:network', { detail: entry }));
274
+ const emit = (entry) => document.dispatchEvent(new CustomEvent('bak:network', {
275
+ bubbles: true,
276
+ composed: true,
277
+ detail: entry
278
+ }));
264
279
  const nativeFetch = window.fetch.bind(window);
265
280
  window.fetch = async (input, init) => {
266
281
  const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
@@ -1186,9 +1201,13 @@ function assessActionTarget(target: HTMLElement, action: ActionName): ActionAsse
1186
1201
  }
1187
1202
 
1188
1203
  let shadowHostBridge = false;
1189
- const rootNode = target.getRootNode();
1190
- if (rootNode instanceof ShadowRoot) {
1191
- shadowHostBridge = hit === rootNode.host;
1204
+ let rootNode = target.getRootNode();
1205
+ while (isShadowRootNode(rootNode)) {
1206
+ if (hit === rootNode.host) {
1207
+ shadowHostBridge = true;
1208
+ break;
1209
+ }
1210
+ rootNode = rootNode.host.getRootNode();
1192
1211
  }
1193
1212
 
1194
1213
  if (hit !== target && !target.contains(hit) && !shadowHostBridge) {
@@ -1332,9 +1351,10 @@ function parseHotkey(keys: string[]): {
1332
1351
  };
1333
1352
  }
1334
1353
 
1335
- function domSummary(): PageDomSummary {
1336
- const allElements = Array.from(document.querySelectorAll('*'));
1337
- const interactiveElements = Array.from(document.querySelectorAll<HTMLElement>('*')).filter((element) => isInteractive(element));
1354
+ function domSummary(root: ParentNode): PageDomSummary {
1355
+ const metadata = documentMetadata(root, document);
1356
+ const allElements = Array.from(root.querySelectorAll('*'));
1357
+ const interactiveElements = Array.from(root.querySelectorAll<HTMLElement>('*')).filter((element) => isInteractive(element));
1338
1358
  const tags = new Map<string, number>();
1339
1359
 
1340
1360
  for (const element of allElements) {
@@ -1347,24 +1367,24 @@ function domSummary(): PageDomSummary {
1347
1367
  .slice(0, 20)
1348
1368
  .map(([tag, count]) => ({ tag, count }));
1349
1369
 
1350
- const shadowHosts = Array.from(document.querySelectorAll<HTMLElement>('*')).filter((element) => element.shadowRoot).length;
1370
+ const shadowHosts = Array.from(root.querySelectorAll<HTMLElement>('*')).filter((element) => element.shadowRoot).length;
1351
1371
 
1352
1372
  return {
1353
- url: window.location.href,
1354
- title: document.title,
1373
+ url: metadata.url,
1374
+ title: metadata.title,
1355
1375
  totalElements: allElements.length,
1356
1376
  interactiveElements: interactiveElements.length,
1357
- headings: document.querySelectorAll('h1,h2,h3,h4,h5,h6').length,
1358
- links: document.querySelectorAll('a[href]').length,
1359
- forms: document.querySelectorAll('form').length,
1360
- iframes: document.querySelectorAll('iframe,frame').length,
1377
+ headings: root.querySelectorAll('h1,h2,h3,h4,h5,h6').length,
1378
+ links: root.querySelectorAll('a[href]').length,
1379
+ forms: root.querySelectorAll('form').length,
1380
+ iframes: root.querySelectorAll('iframe,frame').length,
1361
1381
  shadowHosts,
1362
1382
  tagHistogram
1363
1383
  };
1364
1384
  }
1365
1385
 
1366
- function pageTextChunks(maxChunks = 24, chunkSize = 320): PageTextChunk[] {
1367
- const nodes = Array.from(document.querySelectorAll<HTMLElement>('h1,h2,h3,h4,h5,h6,p,li,td,th,label,button,a,span,div'));
1386
+ function pageTextChunks(root: ParentNode, maxChunks = 24, chunkSize = 320): PageTextChunk[] {
1387
+ const nodes = Array.from(root.querySelectorAll<HTMLElement>('h1,h2,h3,h4,h5,h6,p,li,td,th,label,button,a,span,div'));
1368
1388
  const chunks: PageTextChunk[] = [];
1369
1389
 
1370
1390
  for (const node of nodes) {
@@ -1391,9 +1411,9 @@ function pageTextChunks(maxChunks = 24, chunkSize = 320): PageTextChunk[] {
1391
1411
  return chunks;
1392
1412
  }
1393
1413
 
1394
- function pageAccessibility(limit = 200): AccessibilityNode[] {
1414
+ function pageAccessibility(root: ParentNode, limit = 200): AccessibilityNode[] {
1395
1415
  const nodes: AccessibilityNode[] = [];
1396
- for (const element of getInteractiveElements(document, true).slice(0, limit)) {
1416
+ for (const element of getInteractiveElements(root, true).slice(0, limit)) {
1397
1417
  nodes.push({
1398
1418
  role: inferRole(element),
1399
1419
  name: inferName(element),
@@ -1716,7 +1736,7 @@ function waitConditionMet(message: WaitMessage, root: ParentNode): boolean {
1716
1736
  }
1717
1737
 
1718
1738
  if (message.mode === 'text') {
1719
- if (root instanceof Document) {
1739
+ if (isDocumentNode(root)) {
1720
1740
  const bodyText = root.body?.innerText ?? root.documentElement?.textContent ?? '';
1721
1741
  return bodyText.includes(message.value);
1722
1742
  }
@@ -1765,7 +1785,7 @@ async function waitFor(message: WaitMessage): Promise<ActionResult> {
1765
1785
  const observer = new MutationObserver(() => {
1766
1786
  check();
1767
1787
  });
1768
- const observationRoot = root instanceof Document ? root.documentElement : (root as Node);
1788
+ const observationRoot = isDocumentNode(root) ? root.documentElement : (root as Node);
1769
1789
  if (observationRoot) {
1770
1790
  observer.observe(observationRoot, {
1771
1791
  childList: true,
@@ -1787,23 +1807,52 @@ async function waitFor(message: WaitMessage): Promise<ActionResult> {
1787
1807
 
1788
1808
  async function dispatchRpc(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
1789
1809
  switch (method) {
1790
- case 'page.title':
1791
- return { title: document.title };
1792
- case 'page.url':
1793
- return { url: window.location.href };
1810
+ case 'page.title': {
1811
+ const rootResult = resolveRootForLocator();
1812
+ if (!rootResult.ok) {
1813
+ throw rootResult.error;
1814
+ }
1815
+ return { title: documentMetadata(rootResult.root, document).title };
1816
+ }
1817
+ case 'page.url': {
1818
+ const rootResult = resolveRootForLocator();
1819
+ if (!rootResult.ok) {
1820
+ throw rootResult.error;
1821
+ }
1822
+ return { url: documentMetadata(rootResult.root, document).url };
1823
+ }
1794
1824
  case 'page.text':
1825
+ {
1826
+ const rootResult = resolveRootForLocator();
1827
+ if (!rootResult.ok) {
1828
+ throw rootResult.error;
1829
+ }
1795
1830
  return {
1796
1831
  chunks: pageTextChunks(
1832
+ rootResult.root,
1797
1833
  typeof params.maxChunks === 'number' ? params.maxChunks : 24,
1798
1834
  typeof params.chunkSize === 'number' ? params.chunkSize : 320
1799
1835
  )
1800
1836
  };
1837
+ }
1801
1838
  case 'page.dom':
1802
- return { summary: domSummary() };
1839
+ {
1840
+ const rootResult = resolveRootForLocator();
1841
+ if (!rootResult.ok) {
1842
+ throw rootResult.error;
1843
+ }
1844
+ return { summary: domSummary(rootResult.root) };
1845
+ }
1803
1846
  case 'page.accessibilityTree':
1847
+ {
1848
+ const rootResult = resolveRootForLocator();
1849
+ if (!rootResult.ok) {
1850
+ throw rootResult.error;
1851
+ }
1804
1852
  return {
1805
- nodes: pageAccessibility(typeof params.limit === 'number' ? params.limit : 200)
1853
+ nodes: pageAccessibility(rootResult.root, typeof params.limit === 'number' ? params.limit : 200)
1806
1854
  };
1855
+ }
1807
1856
  case 'page.scrollTo': {
1808
1857
  const x = typeof params.x === 'number' ? params.x : window.scrollX;
1809
1858
  const y = typeof params.y === 'number' ? params.y : window.scrollY;
@@ -2135,12 +2184,12 @@ async function dispatchRpc(method: string, params: Record<string, unknown> = {})
2135
2184
  if (hostSelectors.length === 0) {
2136
2185
  throw { code: 'E_INVALID_PARAMS', message: 'hostSelectors or locator.css is required' } satisfies ActionError;
2137
2186
  }
2138
- const rootResult = resolveRootForLocator();
2139
- if (!rootResult.ok) {
2140
- throw rootResult.error;
2187
+ const frameResult = resolveFrameDocument(contextState.framePath);
2188
+ if (!frameResult.ok) {
2189
+ throw frameResult.error;
2141
2190
  }
2142
2191
  const candidate = [...contextState.shadowPath, ...hostSelectors];
2143
- const check = resolveShadowRoot(rootResult.root, candidate);
2192
+ const check = resolveShadowRoot(frameResult.document, candidate);
2144
2193
  if (!check.ok) {
2145
2194
  throw check.error;
2146
2195
  }
@@ -2182,18 +2231,30 @@ async function dispatchRpc(method: string, params: Record<string, unknown> = {})
2182
2231
  const consoleLimit = typeof params.consoleLimit === 'number' ? Math.max(1, Math.floor(params.consoleLimit)) : 80;
2183
2232
  const networkLimit = typeof params.networkLimit === 'number' ? Math.max(1, Math.floor(params.networkLimit)) : 80;
2184
2233
  const includeAccessibility = params.includeAccessibility === true;
2234
+ const rootResult = resolveRootForLocator();
2235
+ if (!rootResult.ok) {
2236
+ throw rootResult.error;
2237
+ }
2238
+ const metadata = documentMetadata(rootResult.root, document);
2185
2239
  return {
2186
- url: window.location.href,
2187
- title: document.title,
2240
+ url: metadata.url,
2241
+ title: metadata.title,
2188
2242
  context: {
2189
2243
  framePath: [...contextState.framePath],
2190
2244
  shadowPath: [...contextState.shadowPath]
2191
2245
  },
2192
- dom: domSummary(),
2193
- text: pageTextChunks(12, 260),
2246
+ dom: domSummary(rootResult.root),
2247
+ text: pageTextChunks(rootResult.root, 12, 260),
2248
+ elements: collectElements(),
2249
+ metrics: pageMetrics(),
2250
+ viewport: {
2251
+ width: window.innerWidth,
2252
+ height: window.innerHeight,
2253
+ devicePixelRatio: window.devicePixelRatio
2254
+ },
2194
2255
  console: consoleEntries.slice(-consoleLimit),
2195
2256
  network: filterNetworkEntries({ limit: networkLimit }),
2196
- accessibility: includeAccessibility ? pageAccessibility(200) : undefined
2257
+ accessibility: includeAccessibility ? pageAccessibility(rootResult.root, 200) : undefined
2197
2258
  };
2198
2259
  }
2199
2260
  default:
@@ -0,0 +1,56 @@
1
+ export interface DocumentLike {
2
+ nodeType?: number;
3
+ title?: string;
4
+ location?: {
5
+ href?: string;
6
+ };
7
+ }
8
+
9
+ export interface ShadowRootLike {
10
+ nodeType?: number;
11
+ host?: {
12
+ ownerDocument?: DocumentLike | null;
13
+ };
14
+ }
15
+
16
+ const DOCUMENT_NODE = 9;
17
+ const DOCUMENT_FRAGMENT_NODE = 11;
18
+
19
+ export function isDocumentNode(value: unknown): value is Document {
20
+ return Boolean(
21
+ value &&
22
+ typeof value === 'object' &&
23
+ 'nodeType' in value &&
24
+ typeof (value as { nodeType?: unknown }).nodeType === 'number' &&
25
+ (value as { nodeType: number }).nodeType === DOCUMENT_NODE
26
+ );
27
+ }
28
+
29
+ export function isShadowRootNode(value: unknown): value is ShadowRoot {
30
+ return Boolean(
31
+ value &&
32
+ typeof value === 'object' &&
33
+ 'nodeType' in value &&
34
+ typeof (value as { nodeType?: unknown }).nodeType === 'number' &&
35
+ (value as { nodeType: number }).nodeType === DOCUMENT_FRAGMENT_NODE &&
36
+ 'host' in value
37
+ );
38
+ }
39
+
40
+ export function ownerDocumentForRoot(root: ParentNode, fallbackDocument: Document): Document {
41
+ if (isDocumentNode(root)) {
42
+ return root;
43
+ }
44
+ if (isShadowRootNode(root)) {
45
+ return root.host.ownerDocument ?? fallbackDocument;
46
+ }
47
+ return (root as Node).ownerDocument ?? fallbackDocument;
48
+ }
49
+
50
+ export function documentMetadata(root: ParentNode, fallbackDocument: Document): { url: string; title: string } {
51
+ const ownerDocument = ownerDocumentForRoot(root, fallbackDocument);
52
+ return {
53
+ url: ownerDocument.location?.href ?? fallbackDocument.location?.href ?? '',
54
+ title: ownerDocument.title ?? fallbackDocument.title ?? ''
55
+ };
56
+ }