@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.
- package/dist/.bak-e2e-build-stamp +1 -0
- package/dist/content.global.js +138 -53
- package/package.json +11 -9
- package/src/content.ts +115 -54
- package/src/context-metadata.ts +56 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2026-03-08T17:10:39.579Z
|
package/dist/content.global.js
CHANGED
|
@@ -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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
|
1206
|
-
const
|
|
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(
|
|
1260
|
+
const shadowHosts = Array.from(root.querySelectorAll("*")).filter((element) => element.shadowRoot).length;
|
|
1214
1261
|
return {
|
|
1215
|
-
url:
|
|
1216
|
-
title:
|
|
1262
|
+
url: metadata.url,
|
|
1263
|
+
title: metadata.title,
|
|
1217
1264
|
totalElements: allElements.length,
|
|
1218
1265
|
interactiveElements: interactiveElements.length,
|
|
1219
|
-
headings:
|
|
1220
|
-
links:
|
|
1221
|
-
forms:
|
|
1222
|
-
iframes:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
|
1932
|
-
if (!
|
|
1933
|
-
throw
|
|
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(
|
|
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:
|
|
1978
|
-
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
|
+
"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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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) =>
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
|
1337
|
-
const
|
|
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(
|
|
1370
|
+
const shadowHosts = Array.from(root.querySelectorAll<HTMLElement>('*')).filter((element) => element.shadowRoot).length;
|
|
1351
1371
|
|
|
1352
1372
|
return {
|
|
1353
|
-
url:
|
|
1354
|
-
title:
|
|
1373
|
+
url: metadata.url,
|
|
1374
|
+
title: metadata.title,
|
|
1355
1375
|
totalElements: allElements.length,
|
|
1356
1376
|
interactiveElements: interactiveElements.length,
|
|
1357
|
-
headings:
|
|
1358
|
-
links:
|
|
1359
|
-
forms:
|
|
1360
|
-
iframes:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
|
|
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
|
|
2139
|
-
if (!
|
|
2140
|
-
throw
|
|
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(
|
|
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:
|
|
2187
|
-
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
|
+
}
|