@wenathlan/extension 1.1.32 → 1.1.34
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/README.md +8 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +388 -12
- package/dist/index.js.map +3 -3
- package/dist/memory.d.ts +81 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +13 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +67 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +288 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +892 -25
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +1826 -88
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +13 -3
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +322 -25
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +2 -1
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
3
|
// policy.ts
|
|
4
|
-
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile"]);
|
|
5
|
-
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset"]);
|
|
6
|
-
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot"]);
|
|
4
|
+
var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor"]);
|
|
5
|
+
var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
|
|
6
|
+
var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath", "a11ytree", "readvisible", "readertree", "detectlists", "detecttables", "readjson", "watchmutate", "waitquiet", "watchbanner", "detectinfinitescroll", "detectvirtual", "detectlazy", "readscrollpos", "readlang", "readoutline", "countpages", "listshadow", "listframes", "classifypage", "fingerprintsection", "diffsnapshots", "readselection", "watchfocus", "detectsticky", "detectscrolllock", "readopengraph", "detectlanguage", "deriveselector"]);
|
|
7
7
|
var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
|
|
8
8
|
function parseoptions(step) {
|
|
9
9
|
if (step.options === void 0) return {};
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Step options must be a JSON object.");
|
|
17
17
|
return parsed;
|
|
18
18
|
}
|
|
19
|
+
function resolutionverdict(count) {
|
|
20
|
+
if (!Number.isFinite(count) || count <= 0) return "absent";
|
|
21
|
+
return count === 1 ? "resolved" : "ambiguous";
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
// extension/pageactions.ts
|
|
21
25
|
function events(target) {
|
|
@@ -49,12 +53,12 @@
|
|
|
49
53
|
})();
|
|
50
54
|
switch (step.kind) {
|
|
51
55
|
case "presskey": {
|
|
52
|
-
const
|
|
56
|
+
const receiver2 = target instanceof HTMLElement ? target : document.activeElement instanceof HTMLElement ? document.activeElement : document.body;
|
|
53
57
|
const key = step.value ?? "";
|
|
54
58
|
const mods = modifiers(options);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
receiver2.dispatchEvent(keyevent("keydown", key, mods));
|
|
60
|
+
receiver2.dispatchEvent(keyevent("keypress", key, mods));
|
|
61
|
+
receiver2.dispatchEvent(keyevent("keyup", key, mods));
|
|
58
62
|
return { ok: true, summary: `Key ${key} delivered with ${mods.length} modifier${mods.length === 1 ? "" : "s"}.` };
|
|
59
63
|
}
|
|
60
64
|
case "clickdeep": {
|
|
@@ -179,6 +183,361 @@
|
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
185
|
|
|
186
|
+
// extension/pagexpath.ts
|
|
187
|
+
function owntext(element) {
|
|
188
|
+
let combined = "";
|
|
189
|
+
for (const node of element.childNodes) if (node.nodeType === Node.TEXT_NODE) combined += node.textContent ?? "";
|
|
190
|
+
return combined.replace(/\s+/g, " ").trim();
|
|
191
|
+
}
|
|
192
|
+
function attributesof(element) {
|
|
193
|
+
const attributes = {};
|
|
194
|
+
for (const attribute of [...element.attributes]) attributes[attribute.name] = attribute.value;
|
|
195
|
+
return attributes;
|
|
196
|
+
}
|
|
197
|
+
function wrap(element) {
|
|
198
|
+
return { tag: element.tagName.toLowerCase(), attributes: attributesof(element), text: owntext(element), children: [...element.children].map(wrap), element };
|
|
199
|
+
}
|
|
200
|
+
function buildxtree(root) {
|
|
201
|
+
return { tag: "#document", attributes: {}, text: "", children: root.documentElement ? [wrap(root.documentElement)] : [] };
|
|
202
|
+
}
|
|
203
|
+
function parsepredicate(raw) {
|
|
204
|
+
const body = raw.trim();
|
|
205
|
+
let match = /^@([\w-]+)$/.exec(body);
|
|
206
|
+
if (match) return { kind: "attr", name: match[1] };
|
|
207
|
+
match = /^@([\w-]+)\s*=\s*['"]([^'"]*)['"]$/.exec(body);
|
|
208
|
+
if (match) return { kind: "attr", name: match[1], value: match[2] };
|
|
209
|
+
match = /^contains\(\s*@([\w-]+)\s*,\s*['"]([^'"]*)['"]\s*\)$/.exec(body);
|
|
210
|
+
if (match) return { kind: "attr", name: match[1], value: match[2], contains: true };
|
|
211
|
+
match = /^text\(\)\s*=\s*['"]([^'"]*)['"]$/.exec(body);
|
|
212
|
+
if (match) return { kind: "text", value: match[1] };
|
|
213
|
+
match = /^contains\(\s*text\(\)\s*,\s*['"]([^'"]*)['"]\s*\)$/.exec(body);
|
|
214
|
+
if (match) return { kind: "text", value: match[1], contains: true };
|
|
215
|
+
match = /^(\d+)$/.exec(body);
|
|
216
|
+
if (match) return { kind: "position", index: Number.parseInt(match[1], 10) };
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function parsexpath(expression) {
|
|
220
|
+
const trimmed = expression.trim();
|
|
221
|
+
if (!trimmed.startsWith("/")) throw new Error("The reviewed xpath expression must start with a slash.");
|
|
222
|
+
const steps = [];
|
|
223
|
+
let index = 0;
|
|
224
|
+
while (index < trimmed.length) {
|
|
225
|
+
if (trimmed[index] !== "/") throw new Error("The reviewed xpath expression contains an unsupported segment.");
|
|
226
|
+
let slashes = 0;
|
|
227
|
+
while (index < trimmed.length && trimmed[index] === "/") {
|
|
228
|
+
slashes += 1;
|
|
229
|
+
index += 1;
|
|
230
|
+
}
|
|
231
|
+
const start = index;
|
|
232
|
+
let quote = "";
|
|
233
|
+
while (index < trimmed.length) {
|
|
234
|
+
const character = trimmed[index];
|
|
235
|
+
if (quote) {
|
|
236
|
+
if (character === quote) quote = "";
|
|
237
|
+
} else if (character === "'" || character === '"') quote = character;
|
|
238
|
+
else if (character === "/") break;
|
|
239
|
+
index += 1;
|
|
240
|
+
}
|
|
241
|
+
const body = trimmed.slice(start, index);
|
|
242
|
+
if (!body) throw new Error("The reviewed xpath expression contains an empty step.");
|
|
243
|
+
const parsed = /^(\*|[a-zA-Z][\w-]*)((?:\[[^\]]*\])*)$/.exec(body);
|
|
244
|
+
if (!parsed) throw new Error(`The reviewed xpath step ${body} is not supported.`);
|
|
245
|
+
const predicates = [];
|
|
246
|
+
const pattern = /\[([^\]]*)\]/g;
|
|
247
|
+
let predicate;
|
|
248
|
+
while ((predicate = pattern.exec(parsed[2] ?? "")) !== null) {
|
|
249
|
+
const parsedpredicate = parsepredicate(predicate[1]);
|
|
250
|
+
if (!parsedpredicate) throw new Error(`The reviewed xpath predicate [${predicate[1]}] is not supported.`);
|
|
251
|
+
predicates.push(parsedpredicate);
|
|
252
|
+
}
|
|
253
|
+
steps.push({ descendant: slashes > 1, tag: parsed[1].toLowerCase(), predicates });
|
|
254
|
+
}
|
|
255
|
+
return steps;
|
|
256
|
+
}
|
|
257
|
+
function descendants(node, includeself) {
|
|
258
|
+
const result = includeself ? [node] : [];
|
|
259
|
+
for (const child of node.children) {
|
|
260
|
+
result.push(child);
|
|
261
|
+
result.push(...descendants(child, false));
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function applypredicates(nodes, predicates) {
|
|
266
|
+
let result = nodes;
|
|
267
|
+
for (const predicate of predicates) {
|
|
268
|
+
if (predicate.kind === "position") {
|
|
269
|
+
const entry = result[predicate.index - 1];
|
|
270
|
+
result = entry ? [entry] : [];
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
result = result.filter((node) => {
|
|
274
|
+
if (predicate.kind === "attr") {
|
|
275
|
+
const value = node.attributes[predicate.name];
|
|
276
|
+
if (value === void 0) return false;
|
|
277
|
+
if (predicate.value === void 0) return true;
|
|
278
|
+
return predicate.contains ? value.includes(predicate.value) : value === predicate.value;
|
|
279
|
+
}
|
|
280
|
+
return predicate.contains ? node.text.includes(predicate.value) : node.text === predicate.value;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
function evaluatexpath(root, expression) {
|
|
286
|
+
const steps = parsexpath(expression);
|
|
287
|
+
let current = [root];
|
|
288
|
+
let first = true;
|
|
289
|
+
for (const step of steps) {
|
|
290
|
+
let matched = [];
|
|
291
|
+
for (const node of current) {
|
|
292
|
+
const pool = step.descendant ? descendants(node, first) : node.children;
|
|
293
|
+
matched = matched.concat(pool.filter((candidate) => candidate.tag === step.tag || step.tag === "*"));
|
|
294
|
+
}
|
|
295
|
+
current = applypredicates(matched, step.predicates);
|
|
296
|
+
first = false;
|
|
297
|
+
}
|
|
298
|
+
return current;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// extension/pageresolve.ts
|
|
302
|
+
function clean(value) {
|
|
303
|
+
return value.replace(/\s+/g, " ").trim();
|
|
304
|
+
}
|
|
305
|
+
function cssescape(value) {
|
|
306
|
+
return typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(value) : value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
307
|
+
}
|
|
308
|
+
function elementlabel(element) {
|
|
309
|
+
const aria = element.getAttribute("aria-label");
|
|
310
|
+
let linked = "";
|
|
311
|
+
const labelledby = element.getAttribute("aria-labelledby");
|
|
312
|
+
if (labelledby) {
|
|
313
|
+
try {
|
|
314
|
+
const owner = element.ownerDocument?.getElementById(labelledby);
|
|
315
|
+
if (owner) linked = owner.textContent ?? "";
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
let forlabel = "";
|
|
320
|
+
if (element.id) {
|
|
321
|
+
try {
|
|
322
|
+
const label = element.ownerDocument?.querySelector(`label[for="${cssescape(element.id)}"]`);
|
|
323
|
+
if (label instanceof HTMLElement) forlabel = label.textContent ?? "";
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return clean(aria || linked || forlabel || element.getAttribute("title") || element.textContent || "");
|
|
328
|
+
}
|
|
329
|
+
function implicitrole(element) {
|
|
330
|
+
const tag = element.tagName.toLowerCase();
|
|
331
|
+
if (tag === "button") return "button";
|
|
332
|
+
if (tag === "a" && element.getAttribute("href")) return "link";
|
|
333
|
+
if (tag === "select") return "combobox";
|
|
334
|
+
if (tag === "textarea") return "textbox";
|
|
335
|
+
if (tag === "details") return "group";
|
|
336
|
+
if (tag === "input") {
|
|
337
|
+
const type = element.getAttribute("type") ?? "text";
|
|
338
|
+
if (type === "checkbox") return "checkbox";
|
|
339
|
+
if (type === "radio") return "radio";
|
|
340
|
+
if (type === "button" || type === "submit" || type === "reset") return "button";
|
|
341
|
+
if (type === "range") return "slider";
|
|
342
|
+
return "textbox";
|
|
343
|
+
}
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
346
|
+
function elementselector(element) {
|
|
347
|
+
if (element.id) return `#${cssescape(element.id)}`;
|
|
348
|
+
const role = element.getAttribute("role");
|
|
349
|
+
const name = element.getAttribute("name");
|
|
350
|
+
if (role && name) return `[role="${cssescape(role)}"][name="${cssescape(name)}"]`;
|
|
351
|
+
if (name) return `${element.tagName.toLowerCase()}[name="${cssescape(name)}"]`;
|
|
352
|
+
const tag = element.tagName.toLowerCase();
|
|
353
|
+
const parent = element.parentElement;
|
|
354
|
+
if (!parent) return tag;
|
|
355
|
+
const peers = [...parent.children].filter((node) => node.tagName === element.tagName);
|
|
356
|
+
return `${tag}:nth-of-type(${peers.indexOf(element) + 1})`;
|
|
357
|
+
}
|
|
358
|
+
function owntext2(element) {
|
|
359
|
+
let combined = "";
|
|
360
|
+
for (const node of element.childNodes) if (node.nodeType === Node.TEXT_NODE) combined += node.textContent ?? "";
|
|
361
|
+
return clean(combined);
|
|
362
|
+
}
|
|
363
|
+
function summarize(element) {
|
|
364
|
+
return {
|
|
365
|
+
tag: element.tagName.toLowerCase(),
|
|
366
|
+
id: element.id,
|
|
367
|
+
role: element.getAttribute("role")?.toLowerCase() || implicitrole(element),
|
|
368
|
+
name: element.getAttribute("name") ?? "",
|
|
369
|
+
label: elementlabel(element),
|
|
370
|
+
text: owntext2(element),
|
|
371
|
+
selector: elementselector(element),
|
|
372
|
+
element
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
var clickableselector = "a[href], button, input, textarea, select, summary, [role=button], [role=link], [role=combobox], [role=option], [role=checkbox], [role=radio], [role=switch], [role=tab]";
|
|
376
|
+
function collectclickable(root) {
|
|
377
|
+
return [...root.querySelectorAll(clickableselector)].map(summarize);
|
|
378
|
+
}
|
|
379
|
+
function collectcandidates(root) {
|
|
380
|
+
return [...root.querySelectorAll("*")].map(summarize);
|
|
381
|
+
}
|
|
382
|
+
function matchtext(candidates, text) {
|
|
383
|
+
const wanted = clean(text).toLowerCase();
|
|
384
|
+
if (!wanted) return [];
|
|
385
|
+
const exact = candidates.filter((candidate) => candidate.text.toLowerCase() === wanted || candidate.label.toLowerCase() === wanted);
|
|
386
|
+
if (exact.length > 0) return exact;
|
|
387
|
+
return candidates.filter((candidate) => candidate.text.toLowerCase().includes(wanted) || candidate.label.toLowerCase().includes(wanted));
|
|
388
|
+
}
|
|
389
|
+
function matcharia(candidates, role, name) {
|
|
390
|
+
const wantedrole = clean(role).toLowerCase();
|
|
391
|
+
const wantedname = clean(name).toLowerCase();
|
|
392
|
+
if (!wantedrole || !wantedname) return [];
|
|
393
|
+
return candidates.filter((candidate) => candidate.role.toLowerCase() === wantedrole && (candidate.label.toLowerCase() === wantedname || candidate.name.toLowerCase() === wantedname));
|
|
394
|
+
}
|
|
395
|
+
function matchname(candidates, name, clickable) {
|
|
396
|
+
const wanted = clean(name).toLowerCase();
|
|
397
|
+
if (!wanted) return [];
|
|
398
|
+
const matches = candidates.filter((candidate) => candidate.label.toLowerCase() === wanted || candidate.name.toLowerCase() === wanted);
|
|
399
|
+
if (matches.length > 1 && clickable) {
|
|
400
|
+
const interactive = matches.filter(clickable);
|
|
401
|
+
if (interactive.length === 1) return interactive;
|
|
402
|
+
}
|
|
403
|
+
return matches;
|
|
404
|
+
}
|
|
405
|
+
function matchindex(candidates, index) {
|
|
406
|
+
if (!Number.isInteger(index) || index < 1) return [];
|
|
407
|
+
const entry = candidates[index - 1];
|
|
408
|
+
return entry ? [entry] : [];
|
|
409
|
+
}
|
|
410
|
+
function buildclickablemap(candidates, version, builtat = 0) {
|
|
411
|
+
const entries = candidates.map((candidate, position) => ({ number: position + 1, selector: candidate.selector, role: candidate.role || candidate.tag, label: candidate.label, mode: "selector" }));
|
|
412
|
+
return { version, entries, builtat };
|
|
413
|
+
}
|
|
414
|
+
function describeframes(root) {
|
|
415
|
+
const frames = [...root.querySelectorAll("iframe")].map((frame) => {
|
|
416
|
+
let content = null;
|
|
417
|
+
try {
|
|
418
|
+
content = frame.contentDocument;
|
|
419
|
+
} catch {
|
|
420
|
+
content = null;
|
|
421
|
+
}
|
|
422
|
+
let sameorigin = false;
|
|
423
|
+
try {
|
|
424
|
+
sameorigin = content !== null && frame.contentWindow?.location.origin === location.origin;
|
|
425
|
+
} catch {
|
|
426
|
+
sameorigin = false;
|
|
427
|
+
}
|
|
428
|
+
return sameorigin && content ? { sameorigin: true, document: describeframes(content) } : { sameorigin: false };
|
|
429
|
+
});
|
|
430
|
+
return { frames, live: root };
|
|
431
|
+
}
|
|
432
|
+
function walkframepath(root, path) {
|
|
433
|
+
let current = root;
|
|
434
|
+
for (const index of path) {
|
|
435
|
+
if (!Number.isInteger(index) || index < 0) return { ok: false, reason: "The reviewed frame path contains an invalid frame index." };
|
|
436
|
+
const entry = current.frames[index];
|
|
437
|
+
if (!entry) return { ok: false, reason: `Frame ${index} of the reviewed frame path is absent.` };
|
|
438
|
+
if (!entry.sameorigin || !entry.document) return { ok: false, reason: `Frame ${index} of the reviewed frame path is cross origin and was refused.` };
|
|
439
|
+
current = entry.document;
|
|
440
|
+
}
|
|
441
|
+
return { ok: true, document: current };
|
|
442
|
+
}
|
|
443
|
+
function describescopes(root) {
|
|
444
|
+
const elements = [...root.querySelectorAll("*")];
|
|
445
|
+
const shadows = [];
|
|
446
|
+
for (const element of elements) {
|
|
447
|
+
const shadow = element.shadowRoot;
|
|
448
|
+
if (shadow) {
|
|
449
|
+
const nested = describescopes(shadow);
|
|
450
|
+
nested.host = summarize(element);
|
|
451
|
+
shadows.push(nested);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return { candidates: elements.map(summarize), shadows };
|
|
455
|
+
}
|
|
456
|
+
function queryshadowchain(root, selectors) {
|
|
457
|
+
let scope = root;
|
|
458
|
+
for (let position = 0; position < selectors.length; position += 1) {
|
|
459
|
+
const found = scope.querySelector(selectors[position]);
|
|
460
|
+
if (!found) return null;
|
|
461
|
+
if (position === selectors.length - 1) return found;
|
|
462
|
+
const shadow = found.shadowRoot;
|
|
463
|
+
if (!shadow) return null;
|
|
464
|
+
scope = shadow;
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
function queryscoped(root, selector) {
|
|
469
|
+
const direct = root.querySelector(selector);
|
|
470
|
+
if (direct) return direct;
|
|
471
|
+
for (const element of [...root.querySelectorAll("*")]) {
|
|
472
|
+
const shadow = element.shadowRoot;
|
|
473
|
+
if (shadow) {
|
|
474
|
+
const found = queryscoped(shadow, selector);
|
|
475
|
+
if (found) return found;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
function parseoptionssafe(step) {
|
|
481
|
+
try {
|
|
482
|
+
return parseoptions(step);
|
|
483
|
+
} catch {
|
|
484
|
+
return {};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function targetsummary(mode, element) {
|
|
488
|
+
const rect = element.getBoundingClientRect();
|
|
489
|
+
return { mode, selector: elementselector(element), tag: element.tagName.toLowerCase(), label: elementlabel(element), geometry: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
|
|
490
|
+
}
|
|
491
|
+
function singleresolution(mode, matches) {
|
|
492
|
+
const verdict = resolutionverdict(matches.length);
|
|
493
|
+
if (verdict === "resolved") {
|
|
494
|
+
const winner = matches[0];
|
|
495
|
+
if (winner && winner.element instanceof HTMLElement) return { status: "resolved", element: winner.element, target: targetsummary(mode, winner.element) };
|
|
496
|
+
return { status: "absent", mode };
|
|
497
|
+
}
|
|
498
|
+
if (verdict === "ambiguous") return { status: "ambiguous", mode, candidates: matches.slice(0, 8).map((candidate) => candidate.label || candidate.selector) };
|
|
499
|
+
return { status: "absent", mode };
|
|
500
|
+
}
|
|
501
|
+
function resolvetargetref(reference, root) {
|
|
502
|
+
const mode = reference.mode;
|
|
503
|
+
if (mode === "selector") {
|
|
504
|
+
const selector = typeof reference.selector === "string" ? reference.selector : "";
|
|
505
|
+
const element = selector ? root.querySelector(selector) : null;
|
|
506
|
+
return element instanceof HTMLElement ? { status: "resolved", element, target: targetsummary("selector", element) } : { status: "absent", mode: "selector" };
|
|
507
|
+
}
|
|
508
|
+
if (mode === "point") {
|
|
509
|
+
const x = Number(reference.x);
|
|
510
|
+
const y = Number(reference.y);
|
|
511
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { status: "absent", mode: "point" };
|
|
512
|
+
const element = root.elementFromPoint(x, y);
|
|
513
|
+
return element instanceof HTMLElement ? { status: "resolved", element, target: targetsummary("point", element) } : { status: "absent", mode: "point" };
|
|
514
|
+
}
|
|
515
|
+
if (mode === "xpath") {
|
|
516
|
+
const expression = typeof reference.xpath === "string" ? reference.xpath : "";
|
|
517
|
+
if (!expression) return { status: "absent", mode: "xpath" };
|
|
518
|
+
const matches = evaluatexpath(buildxtree(root), expression);
|
|
519
|
+
const first = matches[0];
|
|
520
|
+
return first?.element instanceof HTMLElement ? { status: "resolved", element: first.element, target: targetsummary("xpath", first.element) } : { status: "absent", mode: "xpath" };
|
|
521
|
+
}
|
|
522
|
+
if (mode === "index") {
|
|
523
|
+
const matches = matchindex(collectclickable(root), Number(reference.index));
|
|
524
|
+
return singleresolution("index", matches);
|
|
525
|
+
}
|
|
526
|
+
const candidates = collectcandidates(root);
|
|
527
|
+
if (mode === "text") return singleresolution("text", matchtext(candidates, typeof reference.text === "string" ? reference.text : ""));
|
|
528
|
+
if (mode === "aria") return singleresolution("aria", matcharia(candidates, typeof reference.role === "string" ? reference.role : "", typeof reference.name === "string" ? reference.name : ""));
|
|
529
|
+
if (mode === "name") return singleresolution("name", matchname(candidates, typeof reference.name === "string" ? reference.name : ""));
|
|
530
|
+
return { status: "absent" };
|
|
531
|
+
}
|
|
532
|
+
function resolvestep(step, root) {
|
|
533
|
+
const reference = parseoptionssafe(step).targetref;
|
|
534
|
+
if (reference && typeof reference === "object" && !Array.isArray(reference)) return resolvetargetref(reference, root);
|
|
535
|
+
if (!step.target?.trim()) return { status: "none" };
|
|
536
|
+
const element = root.querySelector(step.target);
|
|
537
|
+
if (element instanceof HTMLElement) return { status: "resolved", element, target: targetsummary("selector", element) };
|
|
538
|
+
return { status: "absent", mode: "selector" };
|
|
539
|
+
}
|
|
540
|
+
|
|
182
541
|
// extension/pagereads.ts
|
|
183
542
|
var highlightid = "devthinkactionhighlight";
|
|
184
543
|
function clearhighlight() {
|
|
@@ -195,7 +554,7 @@
|
|
|
195
554
|
window.setTimeout(clearhighlight, 5e3);
|
|
196
555
|
return { ok: true, summary: "Target outlined for five seconds." };
|
|
197
556
|
}
|
|
198
|
-
function poll(predicate, description, timeout) {
|
|
557
|
+
function poll(root, predicate, description, timeout) {
|
|
199
558
|
return new Promise((resolve) => {
|
|
200
559
|
const started = Date.now();
|
|
201
560
|
const check = () => {
|
|
@@ -212,15 +571,15 @@
|
|
|
212
571
|
check();
|
|
213
572
|
});
|
|
214
573
|
}
|
|
215
|
-
function formstate() {
|
|
216
|
-
return [...
|
|
574
|
+
function formstate(root) {
|
|
575
|
+
return [...root.querySelectorAll("input, textarea, select")].map((element) => ({
|
|
217
576
|
type: element.getAttribute("type") ?? element.tagName.toLowerCase(),
|
|
218
577
|
name: element.getAttribute("name") ?? "",
|
|
219
578
|
value: element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement ? element.value : "",
|
|
220
579
|
...element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio") ? { checked: element.checked } : {}
|
|
221
580
|
}));
|
|
222
581
|
}
|
|
223
|
-
function runpageread(step, target) {
|
|
582
|
+
function runpageread(step, target, root = document) {
|
|
224
583
|
const options = (() => {
|
|
225
584
|
try {
|
|
226
585
|
return parseoptions(step);
|
|
@@ -268,7 +627,7 @@
|
|
|
268
627
|
return { ok: true, summary: "Target markup read.", details: { html: target.outerHTML } };
|
|
269
628
|
}
|
|
270
629
|
case "countelements": {
|
|
271
|
-
const count =
|
|
630
|
+
const count = root.querySelectorAll(step.target ?? "").length;
|
|
272
631
|
return { ok: true, summary: `Selector matches ${count} element${count === 1 ? "" : "s"}.`, details: { count } };
|
|
273
632
|
}
|
|
274
633
|
case "readtable": {
|
|
@@ -279,19 +638,19 @@
|
|
|
279
638
|
return { ok: true, summary: `Read table with ${headers.length} column${headers.length === 1 ? "" : "s"} and ${body.length} row${body.length === 1 ? "" : "s"}.`, details: { headers, rows: body } };
|
|
280
639
|
}
|
|
281
640
|
case "readlinks": {
|
|
282
|
-
const links = [...
|
|
641
|
+
const links = [...root.querySelectorAll("a[href]")].map((element) => ({ text: element.textContent?.trim() ?? "", href: element.getAttribute("href") ?? "" }));
|
|
283
642
|
return { ok: true, summary: `Read ${links.length} link${links.length === 1 ? "" : "s"}.`, details: { links } };
|
|
284
643
|
}
|
|
285
644
|
case "readimages": {
|
|
286
|
-
const images = [...
|
|
645
|
+
const images = [...root.querySelectorAll("img")].map((element) => ({ src: element.getAttribute("src") ?? "", alt: element.getAttribute("alt") ?? "" }));
|
|
287
646
|
return { ok: true, summary: `Read ${images.length} image${images.length === 1 ? "" : "s"}.`, details: { images } };
|
|
288
647
|
}
|
|
289
648
|
case "readmeta": {
|
|
290
|
-
const meta = [...
|
|
649
|
+
const meta = [...root.querySelectorAll("meta")].map((element) => ({ name: element.getAttribute("name") ?? "", property: element.getAttribute("property") ?? "", content: element.getAttribute("content") ?? "" }));
|
|
291
650
|
return { ok: true, summary: `Read ${meta.length} meta entr${meta.length === 1 ? "y" : "ies"}.`, details: { meta } };
|
|
292
651
|
}
|
|
293
652
|
case "readforms": {
|
|
294
|
-
const forms = formstate();
|
|
653
|
+
const forms = formstate(root);
|
|
295
654
|
return { ok: true, summary: `Read ${forms.length} form control${forms.length === 1 ? "" : "s"}.`, details: { forms } };
|
|
296
655
|
}
|
|
297
656
|
case "readstorage": {
|
|
@@ -311,43 +670,1379 @@
|
|
|
311
670
|
}
|
|
312
671
|
}
|
|
313
672
|
case "waitfor": {
|
|
314
|
-
const
|
|
673
|
+
const selector = step.target ?? "";
|
|
315
674
|
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
316
|
-
return poll(() => Boolean(
|
|
675
|
+
return poll(root, () => Boolean(root.querySelector(selector)), `Selector ${selector}`, timeout);
|
|
317
676
|
}
|
|
318
677
|
case "waittext": {
|
|
319
678
|
const text = step.value ?? "";
|
|
320
679
|
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
321
|
-
return poll(() => (
|
|
680
|
+
return poll(root, () => (root.body?.innerText ?? "").includes(text), `Text ${text}`, timeout);
|
|
681
|
+
}
|
|
682
|
+
case "mapclicks": {
|
|
683
|
+
const candidates = collectclickable(root);
|
|
684
|
+
const map = buildclickablemap(candidates, 0, 0);
|
|
685
|
+
return { ok: true, summary: `Mapped ${map.entries.length} clickable element${map.entries.length === 1 ? "" : "s"}.`, details: { entries: map.entries } };
|
|
686
|
+
}
|
|
687
|
+
case "verifyvisible": {
|
|
688
|
+
if (!target) return { ok: false, summary: "Verify target is no longer available." };
|
|
689
|
+
const rect = target.getBoundingClientRect();
|
|
690
|
+
const rendered = rect.width > 0 && rect.height > 0;
|
|
691
|
+
return { ok: rendered, summary: rendered ? `Target is rendered at ${Math.round(rect.x)},${Math.round(rect.y)} with size ${Math.round(rect.width)}x${Math.round(rect.height)}.` : "Target is not rendered.", details: { visible: rendered, geometry: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } } };
|
|
692
|
+
}
|
|
693
|
+
case "verifyenabled": {
|
|
694
|
+
if (!target) return { ok: false, summary: "Verify target is no longer available." };
|
|
695
|
+
const control = target;
|
|
696
|
+
const disabled = control.disabled === true || target.hasAttribute("disabled");
|
|
697
|
+
const readonly = control.readOnly === true || target.hasAttribute("readonly");
|
|
698
|
+
const enabled = !disabled && !readonly;
|
|
699
|
+
return { ok: enabled, summary: enabled ? "Target is enabled and writable." : disabled ? "Target is disabled." : "Target is readonly.", details: { enabled, disabled, readonly } };
|
|
700
|
+
}
|
|
701
|
+
case "resolvexpath": {
|
|
702
|
+
const reference = options.targetref;
|
|
703
|
+
const expression = typeof reference?.xpath === "string" ? reference.xpath : "";
|
|
704
|
+
if (!expression) return { ok: false, summary: "The reviewed xpath expression is absent." };
|
|
705
|
+
let matches = [];
|
|
706
|
+
try {
|
|
707
|
+
matches = evaluatexpath(buildxtree(root), expression);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
return { ok: false, summary: `The reviewed xpath expression failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
710
|
+
}
|
|
711
|
+
const summaries = matches.map((node) => ({ tag: node.tag, ...node.element ? { selector: elementselector(node.element), label: elementlabel(node.element) } : {} }));
|
|
712
|
+
return { ok: matches.length > 0, summary: matches.length > 0 ? `Resolved ${matches.length} element${matches.length === 1 ? "" : "s"} for the reviewed xpath.` : "The reviewed xpath matched no elements.", details: { mode: "xpath", matches: summaries } };
|
|
322
713
|
}
|
|
323
714
|
default:
|
|
324
715
|
return { ok: false, summary: "Unsupported page read." };
|
|
325
716
|
}
|
|
326
717
|
}
|
|
327
718
|
|
|
328
|
-
// extension/
|
|
329
|
-
function
|
|
330
|
-
return
|
|
719
|
+
// extension/pagecontrols.ts
|
|
720
|
+
function typetimeschedule(text, delay) {
|
|
721
|
+
return [...text].map((character, position) => ({ key: character, delay: position === 0 ? 0 : delay }));
|
|
331
722
|
}
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
const labelledby = element.getAttribute("aria-labelledby");
|
|
335
|
-
const linked = labelledby ? document.getElementById(labelledby)?.textContent : "";
|
|
336
|
-
return clean(aria || linked || element.getAttribute("title") || element.textContent || "");
|
|
723
|
+
function appendvalue(current, addition) {
|
|
724
|
+
return current + addition;
|
|
337
725
|
}
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
726
|
+
function valueevents() {
|
|
727
|
+
return ["input", "change"];
|
|
728
|
+
}
|
|
729
|
+
function multichoices(values, options) {
|
|
730
|
+
const present = [];
|
|
731
|
+
const missing = [];
|
|
732
|
+
for (const value of values) {
|
|
733
|
+
const option = options.find((candidate) => candidate.value === value || candidate.label === value);
|
|
734
|
+
if (option) present.push(option.value);
|
|
735
|
+
else missing.push(value);
|
|
736
|
+
}
|
|
737
|
+
return { present, missing };
|
|
738
|
+
}
|
|
739
|
+
function radiochoice(inputs, choice) {
|
|
740
|
+
return inputs.findIndex((candidate) => candidate.value === choice || candidate.label === choice);
|
|
741
|
+
}
|
|
742
|
+
function slidervalue(requested, min, max, step) {
|
|
743
|
+
const lower = Math.min(min, max);
|
|
744
|
+
const upper = Math.max(min, max);
|
|
745
|
+
const clamped = Math.min(upper, Math.max(lower, requested));
|
|
746
|
+
if (!Number.isFinite(step) || step <= 0) return clamped;
|
|
747
|
+
return Math.round((clamped - lower) / step) * step + lower;
|
|
748
|
+
}
|
|
749
|
+
function datevalue(requested) {
|
|
750
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(requested)) return null;
|
|
751
|
+
const parts = requested.split("-").map((part) => Number.parseInt(part, 10));
|
|
752
|
+
const year = parts[0];
|
|
753
|
+
const month = parts[1];
|
|
754
|
+
const day = parts[2];
|
|
755
|
+
if (!year || !month || !day || month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
756
|
+
return requested;
|
|
757
|
+
}
|
|
758
|
+
function colorvalue(requested) {
|
|
759
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(requested)) return null;
|
|
760
|
+
return requested.toLowerCase();
|
|
761
|
+
}
|
|
762
|
+
function expandstate(open) {
|
|
763
|
+
return open ? { open: true, changed: false } : { open: true, changed: true };
|
|
764
|
+
}
|
|
765
|
+
function events2(target) {
|
|
766
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
767
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
768
|
+
}
|
|
769
|
+
function modifiers2(options) {
|
|
770
|
+
return Array.isArray(options.modifiers) ? options.modifiers.filter((item) => typeof item === "string") : [];
|
|
771
|
+
}
|
|
772
|
+
function keyevent2(type, key, mods) {
|
|
773
|
+
const code = key.length === 1 ? `Key${key.toUpperCase()}` : key;
|
|
774
|
+
return new KeyboardEvent(type, { key, code, bubbles: true, cancelable: true, composed: true, ctrlKey: mods.includes("ctrl"), shiftKey: mods.includes("shift"), altKey: mods.includes("alt"), metaKey: mods.includes("meta") });
|
|
775
|
+
}
|
|
776
|
+
function fieldlike2(target) {
|
|
777
|
+
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement ? target : null;
|
|
778
|
+
}
|
|
779
|
+
function receiver(target) {
|
|
780
|
+
return target instanceof HTMLElement ? target : document.activeElement instanceof HTMLElement ? document.activeElement : document.body;
|
|
781
|
+
}
|
|
782
|
+
function wait(delay) {
|
|
783
|
+
return new Promise((resolve) => window.setTimeout(resolve, delay));
|
|
784
|
+
}
|
|
785
|
+
function focuswhenneeded(target, options) {
|
|
786
|
+
if (!(target instanceof HTMLElement)) return;
|
|
787
|
+
if (options.focus === false) return;
|
|
788
|
+
if (options.focus === true || document.activeElement !== target) target.focus();
|
|
789
|
+
}
|
|
790
|
+
function pollfor(predicate, description, timeout) {
|
|
791
|
+
return new Promise((resolve) => {
|
|
792
|
+
const started = Date.now();
|
|
793
|
+
const check = () => {
|
|
794
|
+
if (predicate()) {
|
|
795
|
+
resolve({ ok: true, summary: `${description} is now present on the page.` });
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (timeout > 0 && Date.now() - started >= timeout) {
|
|
799
|
+
resolve({ ok: false, summary: `${description} did not appear within ${timeout} milliseconds.` });
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
window.setTimeout(check, 100);
|
|
803
|
+
};
|
|
804
|
+
check();
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
function runpagecontrol(step, target, root = document) {
|
|
808
|
+
let options = {};
|
|
809
|
+
try {
|
|
810
|
+
options = parseoptions(step);
|
|
811
|
+
} catch {
|
|
812
|
+
options = {};
|
|
813
|
+
}
|
|
814
|
+
switch (step.kind) {
|
|
815
|
+
case "typetime": {
|
|
816
|
+
const field = fieldlike2(target);
|
|
817
|
+
if (!field) return { ok: false, summary: "Target cannot receive timed text." };
|
|
818
|
+
const text = step.value ?? "";
|
|
819
|
+
const delay = typeof options.delay === "number" && options.delay > 0 ? options.delay : 0;
|
|
820
|
+
focuswhenneeded(field, options);
|
|
821
|
+
const schedule = typetimeschedule(text, delay);
|
|
822
|
+
return (async () => {
|
|
823
|
+
for (const entry of schedule) {
|
|
824
|
+
await wait(entry.delay);
|
|
825
|
+
field.dispatchEvent(keyevent2("keydown", entry.key, []));
|
|
826
|
+
field.dispatchEvent(new KeyboardEvent("keypress", { key: entry.key, bubbles: true, cancelable: true }));
|
|
827
|
+
field.value = `${field.value}${entry.key}`;
|
|
828
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
829
|
+
}
|
|
830
|
+
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
831
|
+
return { ok: true, summary: `Typed ${text.length} character${text.length === 1 ? "" : "s"} with a per keystroke delay of ${delay} milliseconds.` };
|
|
832
|
+
})();
|
|
833
|
+
}
|
|
834
|
+
case "appendtext": {
|
|
835
|
+
const field = fieldlike2(target);
|
|
836
|
+
if (!field) return { ok: false, summary: "Target cannot hold a value." };
|
|
837
|
+
focuswhenneeded(field, options);
|
|
838
|
+
field.value = appendvalue(field.value, step.value ?? "");
|
|
839
|
+
events2(field);
|
|
840
|
+
return { ok: true, summary: "Reviewed text appended to the current field value." };
|
|
841
|
+
}
|
|
842
|
+
case "setvalue": {
|
|
843
|
+
const field = fieldlike2(target);
|
|
844
|
+
if (!field) return { ok: false, summary: "Target cannot hold a value." };
|
|
845
|
+
focuswhenneeded(field, options);
|
|
846
|
+
field.value = step.value ?? "";
|
|
847
|
+
events2(field);
|
|
848
|
+
return { ok: true, summary: `Field value set through the dom property with ${valueevents().join(" and ")} events.` };
|
|
849
|
+
}
|
|
850
|
+
case "typeedit": {
|
|
851
|
+
if (!(target instanceof HTMLElement) || !target.isContentEditable) return { ok: false, summary: "Target is not a content editable region." };
|
|
852
|
+
focuswhenneeded(target, options);
|
|
853
|
+
const text = step.value ?? "";
|
|
854
|
+
return (async () => {
|
|
855
|
+
for (const character of [...text]) {
|
|
856
|
+
target.dispatchEvent(new InputEvent("beforeinput", { bubbles: true, cancelable: true, data: character, inputType: "insertText" }));
|
|
857
|
+
target.append(document.createTextNode(character));
|
|
858
|
+
target.dispatchEvent(new InputEvent("input", { bubbles: true, data: character, inputType: "insertText" }));
|
|
859
|
+
}
|
|
860
|
+
return { ok: true, summary: `Typed ${text.length} character${text.length === 1 ? "" : "s"} into the content editable region.` };
|
|
861
|
+
})();
|
|
862
|
+
}
|
|
863
|
+
case "keyhold": {
|
|
864
|
+
const key = step.value ?? "";
|
|
865
|
+
const mods = modifiers2(options);
|
|
866
|
+
receiver(target).dispatchEvent(keyevent2("keydown", key, mods));
|
|
867
|
+
const holdid = typeof options.holdid === "string" && options.holdid ? options.holdid : "";
|
|
868
|
+
return { ok: true, summary: `Key ${key} pressed and held${holdid ? ` under hold id ${holdid}` : ""}.`, details: { ...holdid ? { holdid } : {}, modifiers: mods } };
|
|
869
|
+
}
|
|
870
|
+
case "keyrelease": {
|
|
871
|
+
const key = step.value ?? "";
|
|
872
|
+
const mods = modifiers2(options);
|
|
873
|
+
receiver(target).dispatchEvent(keyevent2("keyup", key, mods));
|
|
874
|
+
return { ok: true, summary: `Key ${key} released.`, details: { modifiers: mods } };
|
|
875
|
+
}
|
|
876
|
+
case "submitsearch": {
|
|
877
|
+
const field = fieldlike2(target);
|
|
878
|
+
if (!field) return { ok: false, summary: "Target is not a search field." };
|
|
879
|
+
const results = typeof options.results === "string" ? options.results : "";
|
|
880
|
+
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
881
|
+
focuswhenneeded(field, options);
|
|
882
|
+
field.dispatchEvent(keyevent2("keydown", "Enter", []));
|
|
883
|
+
field.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", bubbles: true, cancelable: true }));
|
|
884
|
+
field.dispatchEvent(keyevent2("keyup", "Enter", []));
|
|
885
|
+
return pollfor(() => Boolean(document.querySelector(results)), `Results region ${results}`, timeout);
|
|
886
|
+
}
|
|
887
|
+
case "selectmulti": {
|
|
888
|
+
if (!(target instanceof HTMLSelectElement) || !target.multiple) return { ok: false, summary: "Target is not a multi select control." };
|
|
889
|
+
const choices = [...target.options].map((option) => ({ value: option.value, label: clean(option.textContent || option.value) }));
|
|
890
|
+
const requested = Array.isArray(options.values) ? options.values.filter((item) => typeof item === "string") : [];
|
|
891
|
+
const outcome = multichoices(requested, choices);
|
|
892
|
+
if (outcome.missing.length > 0) return { ok: false, summary: `Reviewed option${outcome.missing.length === 1 ? "" : "s"} ${outcome.missing.join(", ")} ${outcome.missing.length === 1 ? "is" : "are"} not part of the select control.` };
|
|
893
|
+
for (const option of target.options) option.selected = outcome.present.includes(option.value);
|
|
894
|
+
events2(target);
|
|
895
|
+
return { ok: true, summary: `Selected ${outcome.present.length} reviewed option${outcome.present.length === 1 ? "" : "s"} in the multi select control.`, details: { selected: outcome.present } };
|
|
896
|
+
}
|
|
897
|
+
case "chooseradio": {
|
|
898
|
+
const radios = target instanceof HTMLInputElement && target.type === "radio" ? [...root.querySelectorAll(`input[type=radio][name="${CSS.escape(target.name)}"]`)] : target ? [...target.querySelectorAll("input[type=radio]")] : [];
|
|
899
|
+
if (radios.length === 0) return { ok: false, summary: "No radio group owns the reviewed target." };
|
|
900
|
+
const inputs = radios.map((radio) => ({ value: radio.value, label: radio.labels && radio.labels.length > 0 ? clean(radio.labels[0]?.textContent || "") || radio.value : radio.value }));
|
|
901
|
+
const index = radiochoice(inputs, step.value ?? "");
|
|
902
|
+
const chosen = radios[index];
|
|
903
|
+
if (!chosen) return { ok: false, summary: "The reviewed radio option is not part of the group." };
|
|
904
|
+
chosen.checked = true;
|
|
905
|
+
events2(chosen);
|
|
906
|
+
return { ok: true, summary: `Picked reviewed radio option ${step.value}.`, details: { value: chosen.value } };
|
|
907
|
+
}
|
|
908
|
+
case "setslider": {
|
|
909
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "range") return { ok: false, summary: "Target is not a range slider." };
|
|
910
|
+
const requested = Number(step.value);
|
|
911
|
+
if (!Number.isFinite(requested)) return { ok: false, summary: "The reviewed slider value is not a number." };
|
|
912
|
+
focuswhenneeded(target, options);
|
|
913
|
+
const value = slidervalue(requested, Number(target.min), Number(target.max), Number(target.step));
|
|
914
|
+
target.value = String(value);
|
|
915
|
+
events2(target);
|
|
916
|
+
return { ok: true, summary: `Slider dragged to the reviewed value ${value}.`, details: { value } };
|
|
917
|
+
}
|
|
918
|
+
case "setdate": {
|
|
919
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "date") return { ok: false, summary: "Target is not a date input." };
|
|
920
|
+
const value = datevalue(step.value ?? "");
|
|
921
|
+
if (value === null) return { ok: false, summary: "The reviewed date is invalid." };
|
|
922
|
+
focuswhenneeded(target, options);
|
|
923
|
+
target.value = value;
|
|
924
|
+
events2(target);
|
|
925
|
+
return { ok: true, summary: `Date input set to ${value}.`, details: { value } };
|
|
926
|
+
}
|
|
927
|
+
case "setcolor": {
|
|
928
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "color") return { ok: false, summary: "Target is not a color input." };
|
|
929
|
+
const value = colorvalue(step.value ?? "");
|
|
930
|
+
if (value === null) return { ok: false, summary: "The reviewed color is invalid." };
|
|
931
|
+
focuswhenneeded(target, options);
|
|
932
|
+
target.value = value;
|
|
933
|
+
events2(target);
|
|
934
|
+
return { ok: true, summary: `Color input set to ${value}.`, details: { value } };
|
|
935
|
+
}
|
|
936
|
+
case "expanddetails": {
|
|
937
|
+
const details = target instanceof HTMLElement ? target.closest("details") : null;
|
|
938
|
+
if (!details) return { ok: false, summary: "Target is not inside a details section." };
|
|
939
|
+
const outcome = expandstate(details.open);
|
|
940
|
+
details.open = outcome.open;
|
|
941
|
+
return { ok: true, summary: outcome.changed ? "Collapsed details section opened." : "Details section was already open.", details: { changed: outcome.changed } };
|
|
942
|
+
}
|
|
943
|
+
default:
|
|
944
|
+
return { ok: false, summary: "Unsupported control action." };
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// extension/pagepointer.ts
|
|
949
|
+
var basecadence = 16;
|
|
950
|
+
function distance(a, b) {
|
|
951
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
952
|
+
}
|
|
953
|
+
function ease(easing, progress) {
|
|
954
|
+
if (easing === "easeinout") return progress * progress * (3 - 2 * progress);
|
|
955
|
+
return progress;
|
|
956
|
+
}
|
|
957
|
+
function ispointref(value) {
|
|
958
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
959
|
+
const point = value;
|
|
960
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
961
|
+
}
|
|
962
|
+
function pathhops(path, profile, random = Math.random, cadence = basecadence) {
|
|
963
|
+
const easing = profile?.easing === "easeinout" ? "easeinout" : "linear";
|
|
964
|
+
const peak = typeof profile?.peak === "number" && profile.peak > 0 ? profile.peak : void 0;
|
|
965
|
+
const jitter = typeof profile?.jitter === "number" && profile.jitter > 0 ? profile.jitter : 0;
|
|
966
|
+
const points = [path.start, ...path.waypoints ?? [], path.end];
|
|
967
|
+
const lengths = [];
|
|
968
|
+
let total = 0;
|
|
969
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
970
|
+
const length = distance(points[index - 1], points[index]);
|
|
971
|
+
lengths.push(length);
|
|
972
|
+
total += length;
|
|
973
|
+
}
|
|
974
|
+
const reviewedduration = typeof path.duration === "number" && Number.isFinite(path.duration) && path.duration > 0 ? path.duration : void 0;
|
|
975
|
+
const duration = reviewedduration ?? (peak !== void 0 && total > 0 ? total / peak * 1e3 : 300);
|
|
976
|
+
const hops = [];
|
|
977
|
+
let previous = points[0];
|
|
978
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
979
|
+
const from = points[index - 1];
|
|
980
|
+
const to = points[index];
|
|
981
|
+
const length = lengths[index - 1] ?? 0;
|
|
982
|
+
if (total <= 0 || length <= 0) {
|
|
983
|
+
hops.push({ x: to.x, y: to.y, delay: 0 });
|
|
984
|
+
previous = to;
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const segmentduration = duration * length / total;
|
|
988
|
+
const count = Math.max(1, Math.ceil(segmentduration / Math.max(1, cadence)));
|
|
989
|
+
for (let hop = 1; hop <= count; hop += 1) {
|
|
990
|
+
const progress = hop / count;
|
|
991
|
+
const eased = ease(easing, progress);
|
|
992
|
+
const position = { x: from.x + (to.x - from.x) * eased, y: from.y + (to.y - from.y) * eased };
|
|
993
|
+
const step = distance(previous, position);
|
|
994
|
+
const base = segmentduration / count;
|
|
995
|
+
const capped = peak !== void 0 ? Math.max(base, step / peak * 1e3) : base;
|
|
996
|
+
hops.push({ x: position.x, y: position.y, delay: Math.max(0, capped + (jitter > 0 ? random() * jitter : 0)) });
|
|
997
|
+
previous = position;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return hops;
|
|
1001
|
+
}
|
|
1002
|
+
function clickplan(x, y, modifiers3) {
|
|
1003
|
+
const shift = modifiers3.includes("shift");
|
|
1004
|
+
const pointer = (type) => ({ type, eventkind: "pointer", x, y, shift });
|
|
1005
|
+
const mouse = (type) => ({ type, eventkind: "mouse", x, y, shift });
|
|
1006
|
+
return [pointer("pointerover"), pointer("pointermove"), pointer("pointerdown"), mouse("mousedown"), pointer("pointerup"), mouse("mouseup"), mouse("click")];
|
|
1007
|
+
}
|
|
1008
|
+
function dispatchplanned(element, event) {
|
|
1009
|
+
const init = { bubbles: true, cancelable: true, composed: true, clientX: event.x, clientY: event.y, shiftKey: event.shift };
|
|
1010
|
+
if (event.eventkind === "pointer") element.dispatchEvent(new PointerEvent(event.type, init));
|
|
1011
|
+
else element.dispatchEvent(new MouseEvent(event.type, init));
|
|
1012
|
+
}
|
|
1013
|
+
function dispatchclick(element, modifiers3 = []) {
|
|
1014
|
+
const rect = element.getBoundingClientRect();
|
|
1015
|
+
const x = rect.left + rect.width / 2;
|
|
1016
|
+
const y = rect.top + rect.height / 2;
|
|
1017
|
+
for (const event of clickplan(x, y, modifiers3)) dispatchplanned(element, event);
|
|
1018
|
+
}
|
|
1019
|
+
function ensurevisible(element) {
|
|
1020
|
+
try {
|
|
1021
|
+
element.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
|
|
1022
|
+
} catch {
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
function settle(delay) {
|
|
1026
|
+
return new Promise((resolve) => window.setTimeout(resolve, delay));
|
|
1027
|
+
}
|
|
1028
|
+
function dispatchmove(x, y) {
|
|
1029
|
+
const element = document.elementFromPoint(x, y);
|
|
1030
|
+
const receiver2 = element ?? document.documentElement;
|
|
1031
|
+
receiver2.dispatchEvent(new PointerEvent("pointermove", { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y }));
|
|
1032
|
+
}
|
|
1033
|
+
async function travel(path, profile) {
|
|
1034
|
+
const hops = pathhops(path, profile);
|
|
1035
|
+
const startelement = document.elementFromPoint(path.start.x, path.start.y) ?? document.documentElement;
|
|
1036
|
+
startelement.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, cancelable: true, composed: true, clientX: path.start.x, clientY: path.start.y }));
|
|
1037
|
+
for (const hop of hops) {
|
|
1038
|
+
await settle(hop.delay);
|
|
1039
|
+
dispatchmove(hop.x, hop.y);
|
|
1040
|
+
}
|
|
1041
|
+
const endelement = document.elementFromPoint(path.end.x, path.end.y) ?? document.documentElement;
|
|
1042
|
+
endelement.dispatchEvent(new PointerEvent("pointerout", { bubbles: true, cancelable: true, composed: true, clientX: path.end.x, clientY: path.end.y }));
|
|
1043
|
+
return { ok: true, summary: `Pointer traveled ${hops.length} hop${hops.length === 1 ? "" : "s"} to the reviewed end point.` };
|
|
1044
|
+
}
|
|
1045
|
+
function runpointerstep(step, resolution) {
|
|
1046
|
+
let options = {};
|
|
1047
|
+
try {
|
|
1048
|
+
options = parseoptions(step);
|
|
1049
|
+
} catch {
|
|
1050
|
+
options = {};
|
|
1051
|
+
}
|
|
1052
|
+
if (step.kind === "movepointer") {
|
|
1053
|
+
const path = options.pointpath;
|
|
1054
|
+
if (!path || !ispointref(path.start) || !ispointref(path.end)) return { ok: false, summary: "The reviewed pointer path is absent." };
|
|
1055
|
+
const waypoints = Array.isArray(path.waypoints) && path.waypoints.every((item) => ispointref(item)) ? path.waypoints : void 0;
|
|
1056
|
+
const fullpath = { start: path.start, end: path.end, ...waypoints ? { waypoints } : {}, ...typeof path.duration === "number" && Number.isFinite(path.duration) ? { duration: path.duration } : {} };
|
|
1057
|
+
return travel(fullpath, options.speedprofile);
|
|
1058
|
+
}
|
|
1059
|
+
if (step.kind === "clickpoint") {
|
|
1060
|
+
const reference = options.targetref;
|
|
1061
|
+
const x = Number(reference?.x);
|
|
1062
|
+
const y = Number(reference?.y);
|
|
1063
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { ok: false, summary: "The reviewed click coordinates are absent." };
|
|
1064
|
+
const element = document.elementFromPoint(x, y);
|
|
1065
|
+
if (!(element instanceof HTMLElement)) return { ok: false, summary: "No element is rendered at the reviewed coordinates." };
|
|
1066
|
+
ensurevisible(element);
|
|
1067
|
+
for (const event of clickplan(x, y, [])) dispatchplanned(element, event);
|
|
1068
|
+
return { ok: true, summary: `Clicked the element at the reviewed coordinates ${x},${y}.` };
|
|
1069
|
+
}
|
|
1070
|
+
if (step.kind === "shiftclick") {
|
|
1071
|
+
if (resolution.status === "ambiguous") return { ok: false, summary: `The reviewed reference matched ${resolution.candidates.length} elements; choose one candidate.`, details: { mode: resolution.mode, candidates: resolution.candidates } };
|
|
1072
|
+
if (resolution.status !== "resolved") return { ok: false, summary: "Action target is no longer available." };
|
|
1073
|
+
ensurevisible(resolution.element);
|
|
1074
|
+
dispatchclick(resolution.element, ["shift"]);
|
|
1075
|
+
return { ok: true, summary: `Shift click delivered to ${resolution.target.label || resolution.target.tag}.`, details: { mode: resolution.target.mode, resolvedtarget: resolution.target } };
|
|
1076
|
+
}
|
|
1077
|
+
return { ok: false, summary: "Unsupported pointer action." };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// extension/pageinteract.ts
|
|
1081
|
+
function optionsof(step) {
|
|
1082
|
+
try {
|
|
1083
|
+
return parseoptions(step);
|
|
1084
|
+
} catch {
|
|
1085
|
+
return {};
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function innerstep(step) {
|
|
1089
|
+
const options = optionsof(step);
|
|
1090
|
+
const kind = options.kind;
|
|
1091
|
+
if (typeof kind !== "string" || !kind.trim()) return null;
|
|
1092
|
+
const inneroptions = options.options;
|
|
1093
|
+
return {
|
|
1094
|
+
id: `${step.id}inner`,
|
|
1095
|
+
kind,
|
|
1096
|
+
summary: step.summary,
|
|
1097
|
+
risk: step.risk,
|
|
1098
|
+
...typeof options.target === "string" ? { target: options.target } : {},
|
|
1099
|
+
...typeof options.value === "string" ? { value: options.value } : {},
|
|
1100
|
+
...inneroptions && typeof inneroptions === "object" && !Array.isArray(inneroptions) ? { options: JSON.stringify(inneroptions) } : {}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function clickresolved(stepkind, resolution) {
|
|
1104
|
+
if (resolution.status === "ambiguous") return { ok: false, summary: `The reviewed ${resolution.mode} reference matched ${resolution.candidates.length} elements: ${resolution.candidates.join("; ")}.`, details: { mode: resolution.mode, candidates: resolution.candidates } };
|
|
1105
|
+
if (resolution.status !== "resolved") return { ok: false, summary: "The reviewed target is no longer available." };
|
|
1106
|
+
ensurevisible(resolution.element);
|
|
1107
|
+
dispatchclick(resolution.element);
|
|
1108
|
+
return { ok: true, summary: `Clicked ${resolution.target.label || resolution.target.tag} resolved by ${stepkind} ${resolution.target.mode} mode.`, details: { mode: resolution.target.mode, resolvedtarget: resolution.target } };
|
|
1109
|
+
}
|
|
1110
|
+
function runinteractstep(step, expectedorigin, dispatch) {
|
|
1111
|
+
if (step.kind === "clicktext" || step.kind === "clickaria" || step.kind === "clickname") {
|
|
1112
|
+
return clickresolved(step.kind, resolvestep(step, document));
|
|
1113
|
+
}
|
|
1114
|
+
if (step.kind === "pierceshadow") {
|
|
1115
|
+
const options = optionsof(step);
|
|
1116
|
+
const shadow = Array.isArray(options.shadow) ? options.shadow.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1117
|
+
const element = shadow.length > 0 ? queryshadowchain(document, shadow) : queryscoped(document, step.target ?? "");
|
|
1118
|
+
if (!(element instanceof HTMLElement)) return { ok: false, summary: "The reviewed shadow target is not available." };
|
|
1119
|
+
ensurevisible(element);
|
|
1120
|
+
dispatchclick(element);
|
|
1121
|
+
const summary = targetsummary("selector", element);
|
|
1122
|
+
return { ok: true, summary: `Clicked ${summary.label || summary.tag} resolved through ${shadow.length > 0 ? "the reviewed shadow path" : "open shadow roots"}.`, details: { mode: "selector", resolvedtarget: summary } };
|
|
1123
|
+
}
|
|
1124
|
+
if (step.kind === "enterframe") {
|
|
1125
|
+
const options = optionsof(step);
|
|
1126
|
+
const path = Array.isArray(options.framepath) ? options.framepath.filter((item) => typeof item === "number" && Number.isInteger(item) && item >= 0) : [];
|
|
1127
|
+
const walk = walkframepath(describeframes(document), path);
|
|
1128
|
+
if (!walk.ok) return { ok: false, summary: walk.reason };
|
|
1129
|
+
const framedocument = walk.document.live;
|
|
1130
|
+
if (!framedocument) return { ok: false, summary: "The reviewed frame document is not available." };
|
|
1131
|
+
const inner = innerstep(step);
|
|
1132
|
+
if (!inner) return { ok: false, summary: "The reviewed inner step is absent." };
|
|
1133
|
+
return dispatch(inner, expectedorigin, framedocument);
|
|
1134
|
+
}
|
|
1135
|
+
return { ok: false, summary: "Unsupported interaction action." };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// extension/pagedialogs.ts
|
|
1139
|
+
function harvestdialoglog(root) {
|
|
1140
|
+
const raw = root.documentElement.dataset.devthinkdialoglog;
|
|
1141
|
+
if (!raw) return [];
|
|
1142
|
+
delete root.documentElement.dataset.devthinkdialoglog;
|
|
1143
|
+
try {
|
|
1144
|
+
const parsed = JSON.parse(raw);
|
|
1145
|
+
if (!Array.isArray(parsed)) return [];
|
|
1146
|
+
return parsed.filter((item) => Boolean(item) && typeof item === "object" && typeof item.dialog === "string");
|
|
1147
|
+
} catch {
|
|
1148
|
+
return [];
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// extension/pageobserve.ts
|
|
1153
|
+
var maxframedepth = 4;
|
|
1154
|
+
function elementstates(element) {
|
|
1155
|
+
const states = [];
|
|
1156
|
+
if (element.hasAttribute("disabled") || element.getAttribute("aria-disabled") === "true") states.push("disabled");
|
|
1157
|
+
if (element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio") && element.checked) states.push("checked");
|
|
1158
|
+
const expanded = element.getAttribute("aria-expanded");
|
|
1159
|
+
if (expanded !== null) states.push(`expanded ${expanded}`);
|
|
1160
|
+
if (element.getAttribute("aria-selected") === "true") states.push("selected");
|
|
1161
|
+
if (element.hasAttribute("required") || element.getAttribute("aria-required") === "true") states.push("required");
|
|
1162
|
+
if (element.hasAttribute("readonly") || element.getAttribute("aria-readonly") === "true") states.push("readonly");
|
|
1163
|
+
if (element.getAttribute("aria-hidden") === "true") states.push("hidden");
|
|
1164
|
+
return states;
|
|
1165
|
+
}
|
|
1166
|
+
function elementvalue(element) {
|
|
1167
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) return element.value;
|
|
1168
|
+
return "";
|
|
1169
|
+
}
|
|
1170
|
+
function elementhidden(element) {
|
|
1171
|
+
if (element instanceof HTMLInputElement && element.type === "hidden") return true;
|
|
1172
|
+
if (element.hasAttribute("hidden") || element.getAttribute("aria-hidden") === "true") return true;
|
|
1173
|
+
try {
|
|
1174
|
+
const style = element.ownerDocument?.defaultView?.getComputedStyle(element);
|
|
1175
|
+
if (style && (style.display === "none" || style.visibility === "hidden")) return true;
|
|
1176
|
+
} catch {
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
function framenode(frame, depth) {
|
|
1181
|
+
let content = null;
|
|
1182
|
+
try {
|
|
1183
|
+
content = frame.contentDocument;
|
|
1184
|
+
} catch {
|
|
1185
|
+
content = null;
|
|
1186
|
+
}
|
|
1187
|
+
let sameorigin = false;
|
|
1188
|
+
try {
|
|
1189
|
+
sameorigin = content !== null && frame.contentWindow?.location.origin === location.origin;
|
|
1190
|
+
} catch {
|
|
1191
|
+
sameorigin = false;
|
|
1192
|
+
}
|
|
1193
|
+
const node = wrap2(frame, depth);
|
|
1194
|
+
if (sameorigin && content && depth < maxframedepth) node.children.push(...wrapchildren(content, depth + 1));
|
|
1195
|
+
return node;
|
|
1196
|
+
}
|
|
1197
|
+
function wrap2(element, depth) {
|
|
1198
|
+
const shadow = element.shadowRoot;
|
|
1199
|
+
const node = {
|
|
1200
|
+
tag: element.tagName.toLowerCase(),
|
|
1201
|
+
selector: elementselector(element),
|
|
1202
|
+
id: element.id,
|
|
1203
|
+
classes: [...element.classList],
|
|
1204
|
+
role: element.getAttribute("role")?.toLowerCase() || implicitrole(element),
|
|
1205
|
+
name: elementlabel(element),
|
|
1206
|
+
text: owntext2(element),
|
|
1207
|
+
value: elementvalue(element),
|
|
1208
|
+
states: elementstates(element),
|
|
1209
|
+
hidden: elementhidden(element),
|
|
1210
|
+
children: [],
|
|
1211
|
+
element
|
|
1212
|
+
};
|
|
1213
|
+
if (shadow) node.children.push(...wrapchildren(shadow, depth));
|
|
1214
|
+
if (element instanceof HTMLIFrameElement) return framenode(element, depth);
|
|
1215
|
+
node.children.push(...wrapchildren(element, depth));
|
|
1216
|
+
return node;
|
|
1217
|
+
}
|
|
1218
|
+
function wrapchildren(scope, depth) {
|
|
1219
|
+
return [...scope.querySelectorAll(":scope > *")].map((child) => wrap2(child, depth));
|
|
1220
|
+
}
|
|
1221
|
+
function buildpagetree(scope) {
|
|
1222
|
+
const root = {
|
|
1223
|
+
tag: "#document",
|
|
1224
|
+
selector: "",
|
|
1225
|
+
id: "",
|
|
1226
|
+
classes: [],
|
|
1227
|
+
role: "document",
|
|
1228
|
+
name: "",
|
|
1229
|
+
text: "",
|
|
1230
|
+
value: "",
|
|
1231
|
+
states: [],
|
|
1232
|
+
hidden: false,
|
|
1233
|
+
children: []
|
|
1234
|
+
};
|
|
1235
|
+
if (scope instanceof Document) {
|
|
1236
|
+
root.children = scope.documentElement ? [wrap2(scope.documentElement, 0)] : [];
|
|
1237
|
+
} else {
|
|
1238
|
+
root.children = [...wrapchildren(scope, 0)];
|
|
1239
|
+
}
|
|
1240
|
+
return root;
|
|
1241
|
+
}
|
|
1242
|
+
function countnodes(node) {
|
|
1243
|
+
return 1 + node.children.reduce((total, child) => total + countnodes(child), 0);
|
|
1244
|
+
}
|
|
1245
|
+
function builda11ytree(node) {
|
|
1246
|
+
const children = node.children.filter((child) => !child.hidden).map(builda11ytree);
|
|
1247
|
+
return {
|
|
1248
|
+
role: node.role || "generic",
|
|
1249
|
+
name: node.name,
|
|
1250
|
+
states: node.states,
|
|
1251
|
+
...node.value ? { value: node.value } : {},
|
|
1252
|
+
childcount: children.length,
|
|
1253
|
+
children
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
function visibleentries(node) {
|
|
1257
|
+
if (node.hidden) return [];
|
|
1258
|
+
const entries = node.text ? [{ selector: node.selector || node.tag, text: node.text }] : [];
|
|
1259
|
+
for (const child of node.children) entries.push(...visibleentries(child));
|
|
1260
|
+
return entries;
|
|
1261
|
+
}
|
|
1262
|
+
function visibletext(node) {
|
|
1263
|
+
return visibleentries(node).map((entry) => entry.text).join(" ");
|
|
1264
|
+
}
|
|
1265
|
+
function nodetextlength(node) {
|
|
1266
|
+
return node.text.length + node.children.reduce((total, child) => total + nodetextlength(child), 0);
|
|
1267
|
+
}
|
|
1268
|
+
function nodelinktext(node) {
|
|
1269
|
+
const own = node.tag === "a" ? node.text.length : 0;
|
|
1270
|
+
return own + node.children.reduce((total, child) => total + nodelinktext(child), 0);
|
|
1271
|
+
}
|
|
1272
|
+
function wordsin(text) {
|
|
1273
|
+
return text.split(/\s+/).filter(Boolean).length;
|
|
1274
|
+
}
|
|
1275
|
+
function findbyline(node) {
|
|
1276
|
+
const markers = ["byline", "author"];
|
|
1277
|
+
const direct = node.classes.some((item) => markers.some((marker) => item.toLowerCase().includes(marker))) || markers.some((marker) => node.id.toLowerCase().includes(marker));
|
|
1278
|
+
if (direct && node.text) return node.text;
|
|
1279
|
+
for (const child of node.children) {
|
|
1280
|
+
const found = findbyline(child);
|
|
1281
|
+
if (found) return found;
|
|
1282
|
+
}
|
|
1283
|
+
return "";
|
|
1284
|
+
}
|
|
1285
|
+
function findheading(node, tags) {
|
|
1286
|
+
if (tags.includes(node.tag) && node.text) return node.text;
|
|
1287
|
+
for (const child of node.children) {
|
|
1288
|
+
const found = findheading(child, tags);
|
|
1289
|
+
if (found) return found;
|
|
1290
|
+
}
|
|
1291
|
+
return "";
|
|
1292
|
+
}
|
|
1293
|
+
function buildreader(root, title) {
|
|
1294
|
+
let best;
|
|
1295
|
+
let bestscore = 0;
|
|
1296
|
+
const walk = (node) => {
|
|
1297
|
+
if (node.tag !== "#document") {
|
|
1298
|
+
const length = nodetextlength(node);
|
|
1299
|
+
const links = nodelinktext(node);
|
|
1300
|
+
const score = length * (1 - (length > 0 ? links / length : 0));
|
|
1301
|
+
if (score > bestscore) {
|
|
1302
|
+
bestscore = score;
|
|
1303
|
+
best = node;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
for (const child of node.children) walk(child);
|
|
1307
|
+
};
|
|
1308
|
+
walk(root);
|
|
1309
|
+
const article = best ?? root;
|
|
1310
|
+
const blocks = article.children.filter((child) => !child.hidden && child.text).map((child) => ({ kind: child.tag, text: child.text, words: wordsin(child.text) }));
|
|
1311
|
+
const ownblock = article.text ? [{ kind: article.tag, text: article.text, words: wordsin(article.text) }] : [];
|
|
1312
|
+
const allblocks = [...ownblock, ...blocks];
|
|
1313
|
+
return {
|
|
1314
|
+
title: findheading(article, ["h1"]) || findheading(root, ["h1"]) || title,
|
|
1315
|
+
byline: findbyline(article) || findbyline(root),
|
|
1316
|
+
blocks: allblocks,
|
|
1317
|
+
words: allblocks.reduce((total, block) => total + block.words, 0),
|
|
1318
|
+
characters: allblocks.reduce((total, block) => total + block.text.length, 0)
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function pageoutline(root, title) {
|
|
1322
|
+
const headings = [];
|
|
1323
|
+
const walk = (node) => {
|
|
1324
|
+
const level = /^h([1-6])$/.exec(node.tag);
|
|
1325
|
+
if (level && node.text) headings.push({ level: Number.parseInt(level[1], 10), text: node.text });
|
|
1326
|
+
for (const child of node.children) walk(child);
|
|
1327
|
+
};
|
|
1328
|
+
walk(root);
|
|
1329
|
+
return { title: findheading(root, ["h1"]) || title, headings };
|
|
1330
|
+
}
|
|
1331
|
+
function captureselection(root) {
|
|
1332
|
+
const selection = root.getSelection?.() ?? null;
|
|
1333
|
+
const text = selection ? clean(selection.toString()) : "";
|
|
1334
|
+
return { text, length: text.length };
|
|
1335
|
+
}
|
|
1336
|
+
function opengraphfields(meta, jsonld) {
|
|
1337
|
+
const graph = {};
|
|
1338
|
+
for (const entry of meta) {
|
|
1339
|
+
if (entry.property.startsWith("og:") && entry.content) graph[entry.property] = entry.content;
|
|
1340
|
+
}
|
|
1341
|
+
const structured = [];
|
|
1342
|
+
let refused = 0;
|
|
1343
|
+
for (const raw of jsonld) {
|
|
1344
|
+
try {
|
|
1345
|
+
structured.push(JSON.parse(raw));
|
|
1346
|
+
} catch {
|
|
1347
|
+
refused += 1;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return { graph, structured, refused };
|
|
1351
|
+
}
|
|
1352
|
+
var stopwords = {
|
|
1353
|
+
en: ["the", "is", "at", "which", "on", "and", "of", "to", "in", "that", "it", "with"],
|
|
1354
|
+
pt: ["de", "que", "n\xE3o", "uma", "para", "com", "por", "mais", "como", "p\xE1gina", "este", "voc\xEA"],
|
|
1355
|
+
es: ["que", "el", "las", "los", "por", "una", "para", "con", "como", "p\xE1gina", "m\xE1s", "este"],
|
|
1356
|
+
fr: ["le", "les", "des", "que", "pour", "dans", "est", "sur", "avec", "page", "plus", "cette"],
|
|
1357
|
+
de: ["der", "die", "und", "das", "ist", "von", "mit", "f\xFCr", "auf", "den", "nicht", "seite"],
|
|
1358
|
+
it: ["che", "il", "la", "per", "una", "del", "sono", "non", "con", "pagina", "pi\xF9", "questo"],
|
|
1359
|
+
nl: ["het", "een", "en", "van", "is", "dat", "op", "te", "voor", "met", "niet", "pagina"]
|
|
1360
|
+
};
|
|
1361
|
+
function detecttextlanguage(text) {
|
|
1362
|
+
const words = text.toLowerCase().split(/[^a-zà-ÿ]+/).filter(Boolean);
|
|
1363
|
+
if (words.length === 0) return "";
|
|
1364
|
+
let best = "";
|
|
1365
|
+
let bestscore = 0;
|
|
1366
|
+
for (const [language, dictionary] of Object.entries(stopwords)) {
|
|
1367
|
+
const score = words.filter((word) => dictionary.includes(word)).length;
|
|
1368
|
+
if (score > bestscore) {
|
|
1369
|
+
bestscore = score;
|
|
1370
|
+
best = language;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return best;
|
|
1374
|
+
}
|
|
1375
|
+
function taglanguage(text) {
|
|
1376
|
+
return { text, language: detecttextlanguage(text) };
|
|
1377
|
+
}
|
|
1378
|
+
function documentlanguage(signals) {
|
|
1379
|
+
if (signals.lang.trim()) return { language: signals.lang.trim(), source: "document" };
|
|
1380
|
+
if (signals.meta.trim()) return { language: signals.meta.trim(), source: "meta" };
|
|
1381
|
+
return { language: detecttextlanguage(signals.text), source: "content" };
|
|
1382
|
+
}
|
|
1383
|
+
function shadowpaths(scope) {
|
|
1384
|
+
const paths = [];
|
|
1385
|
+
const walk = (tree, prefix) => {
|
|
1386
|
+
for (const shadow of tree.shadows) {
|
|
1387
|
+
if (!shadow.host) continue;
|
|
1388
|
+
const path = prefix ? `${prefix} > ${shadow.host.selector}` : shadow.host.selector;
|
|
1389
|
+
paths.push(path);
|
|
1390
|
+
walk(shadow, path);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
walk(scope, "");
|
|
1394
|
+
return paths;
|
|
1395
|
+
}
|
|
1396
|
+
function framelist(root) {
|
|
1397
|
+
return [...root.querySelectorAll("iframe")].map((frame, index) => {
|
|
1398
|
+
let origin = "";
|
|
1399
|
+
try {
|
|
1400
|
+
origin = frame.contentWindow?.location.origin ?? "";
|
|
1401
|
+
} catch {
|
|
1402
|
+
origin = "";
|
|
1403
|
+
}
|
|
1404
|
+
const rect = frame.getBoundingClientRect();
|
|
1405
|
+
return { index, origin, sameorigin: origin !== "" && origin === location.origin, width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
1406
|
+
});
|
|
349
1407
|
}
|
|
350
1408
|
function stepoptions(step) {
|
|
1409
|
+
try {
|
|
1410
|
+
return parseoptions(step);
|
|
1411
|
+
} catch {
|
|
1412
|
+
return {};
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
function runpageobservation(step, target, root = document) {
|
|
1416
|
+
switch (step.kind) {
|
|
1417
|
+
case "a11ytree": {
|
|
1418
|
+
const tree = builda11ytree(buildpagetree(root));
|
|
1419
|
+
const count = countnodes(tree);
|
|
1420
|
+
return { ok: true, summary: `Captured the accessibility tree with ${count} node${count === 1 ? "" : "s"}.`, details: { tree, nodecount: count } };
|
|
1421
|
+
}
|
|
1422
|
+
case "readvisible": {
|
|
1423
|
+
const scope = target ?? root;
|
|
1424
|
+
const tree = buildpagetree(scope);
|
|
1425
|
+
const entries = visibleentries(tree);
|
|
1426
|
+
return { ok: true, summary: `Read the rendered text of ${entries.length} visible element${entries.length === 1 ? "" : "s"}.`, details: { entries, text: visibletext(tree) } };
|
|
1427
|
+
}
|
|
1428
|
+
case "readertree": {
|
|
1429
|
+
const article = buildreader(buildpagetree(root), root.title);
|
|
1430
|
+
return { ok: true, summary: `Extracted the reader view with ${article.blocks.length} block${article.blocks.length === 1 ? "" : "s"} and ${article.words} words.`, details: { article } };
|
|
1431
|
+
}
|
|
1432
|
+
case "readoutline": {
|
|
1433
|
+
const outline = pageoutline(buildpagetree(root), root.title);
|
|
1434
|
+
return { ok: true, summary: `Read the outline with ${outline.headings.length} heading${outline.headings.length === 1 ? "" : "s"}.`, details: { title: outline.title, headings: outline.headings } };
|
|
1435
|
+
}
|
|
1436
|
+
case "readselection": {
|
|
1437
|
+
const selection = captureselection(root);
|
|
1438
|
+
return { ok: true, summary: selection.text ? `Read ${selection.length} characters of the current selection.` : "No text is currently selected.", details: { text: selection.text, length: selection.length } };
|
|
1439
|
+
}
|
|
1440
|
+
case "readopengraph": {
|
|
1441
|
+
const meta = [...root.querySelectorAll("meta")].map((element) => ({ property: element.getAttribute("property") ?? "", name: element.getAttribute("name") ?? "", content: element.getAttribute("content") ?? "" }));
|
|
1442
|
+
const jsonld = [...root.querySelectorAll('script[type="application/ld+json"]')].map((element) => element.textContent ?? "");
|
|
1443
|
+
const fields = opengraphfields(meta, jsonld);
|
|
1444
|
+
return { ok: true, summary: `Read ${Object.keys(fields.graph).length} open graph entr${Object.keys(fields.graph).length === 1 ? "y" : "ies"} and ${fields.structured.length} structured payload${fields.structured.length === 1 ? "" : "s"}${fields.refused > 0 ? `; ${fields.refused} malformed payload${fields.refused === 1 ? " was" : "s were"} refused` : ""}.`, details: { graph: fields.graph, structured: fields.structured, refused: fields.refused } };
|
|
1445
|
+
}
|
|
1446
|
+
case "readlang": {
|
|
1447
|
+
const metatag = root.querySelector('meta[http-equiv="content-language"]')?.getAttribute("content") ?? "";
|
|
1448
|
+
const outcome = documentlanguage({ lang: root.documentElement?.getAttribute("lang") ?? "", meta: metatag, text: root.body?.innerText ?? "" });
|
|
1449
|
+
return { ok: true, summary: `Detected page language ${outcome.language || "unknown"} from the ${outcome.source} signal.`, details: { language: outcome.language, source: outcome.source } };
|
|
1450
|
+
}
|
|
1451
|
+
case "detectlanguage": {
|
|
1452
|
+
const options = stepoptions(step);
|
|
1453
|
+
const text = typeof options.text === "string" && options.text ? options.text : target?.textContent ?? root.body?.innerText ?? "";
|
|
1454
|
+
const routed = taglanguage(clean(text));
|
|
1455
|
+
return { ok: routed.language !== "", summary: routed.language ? `Detected language ${routed.language} for the extracted text.` : "The extracted text language is undetermined.", details: { language: routed.language, routed } };
|
|
1456
|
+
}
|
|
1457
|
+
case "listshadow": {
|
|
1458
|
+
const paths = shadowpaths(describescopes(root));
|
|
1459
|
+
return { ok: true, summary: `Listed ${paths.length} open shadow root${paths.length === 1 ? "" : "s"}.`, details: { shadows: paths } };
|
|
1460
|
+
}
|
|
1461
|
+
case "listframes": {
|
|
1462
|
+
const frames = framelist(root);
|
|
1463
|
+
return { ok: true, summary: `Listed ${frames.length} iframe${frames.length === 1 ? "" : "s"}.`, details: { frames } };
|
|
1464
|
+
}
|
|
1465
|
+
default:
|
|
1466
|
+
return { ok: false, summary: "Unsupported page observation." };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// extension/pagedetect.ts
|
|
1471
|
+
function detectlistpatterns(samples) {
|
|
1472
|
+
const patterns = [];
|
|
1473
|
+
for (const sample of samples) {
|
|
1474
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1475
|
+
for (const child of sample.children) {
|
|
1476
|
+
const key = `${child.tag}|${child.classes}`;
|
|
1477
|
+
const group = groups.get(key) ?? [];
|
|
1478
|
+
group.push(child);
|
|
1479
|
+
groups.set(key, group);
|
|
1480
|
+
}
|
|
1481
|
+
for (const [key, group] of groups) {
|
|
1482
|
+
if (group.length < 2) continue;
|
|
1483
|
+
if (!group.some((item) => item.text)) continue;
|
|
1484
|
+
const [tag, classes] = key.split("|");
|
|
1485
|
+
const classpart = (classes ?? "").split(" ").filter(Boolean).map((name) => `.${name}`).join("");
|
|
1486
|
+
patterns.push({ container: sample.container, itemselector: `${tag}${classpart}`, repeat: group.length, samples: group.map((item) => item.text).filter(Boolean) });
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return patterns;
|
|
1490
|
+
}
|
|
1491
|
+
function normalizetable(rows, caption) {
|
|
1492
|
+
const firstheader = rows.find((row) => row.header);
|
|
1493
|
+
const headers = firstheader?.cells ?? [];
|
|
1494
|
+
const body = firstheader ? rows.filter((row) => row !== firstheader) : rows;
|
|
1495
|
+
const width = rows.reduce((largest, row) => Math.max(largest, row.cells.length), 0);
|
|
1496
|
+
const columns = [];
|
|
1497
|
+
for (let index = 0; index < width; index += 1) {
|
|
1498
|
+
const label = headers[index] ?? `column ${index + 1}`;
|
|
1499
|
+
const cells = body.filter((row) => Boolean((row.cells[index] ?? "").trim())).length;
|
|
1500
|
+
columns.push({ label, cells });
|
|
1501
|
+
}
|
|
1502
|
+
return { headers, columns, rows: body.length, caption };
|
|
1503
|
+
}
|
|
1504
|
+
function paginationestimate(entries) {
|
|
1505
|
+
const pages = [];
|
|
1506
|
+
let current = 0;
|
|
1507
|
+
for (const entry of entries) {
|
|
1508
|
+
const parsed = /^\d+$/.exec(entry.text.trim());
|
|
1509
|
+
if (parsed) {
|
|
1510
|
+
const page = Number.parseInt(parsed[0], 10);
|
|
1511
|
+
pages.push(page);
|
|
1512
|
+
if (entry.current) current = page;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const total = Math.max(0, ...pages, current);
|
|
1516
|
+
return { current, total, links: entries.length, pages };
|
|
1517
|
+
}
|
|
1518
|
+
function infinitescrollranges(ranges) {
|
|
1519
|
+
return ranges.filter((range) => range.scrollheight > range.clientheight && range.triggers.length > 0).map((range) => ({ selector: range.selector, scrollrange: range.scrollheight - range.clientheight, triggers: range.triggers }));
|
|
1520
|
+
}
|
|
1521
|
+
function virtualizedcontainers(containers) {
|
|
1522
|
+
const results = [];
|
|
1523
|
+
for (const container of containers) {
|
|
1524
|
+
const first = container.rows[0];
|
|
1525
|
+
if (!first || container.rows.length < 2 || first.height <= 0) continue;
|
|
1526
|
+
if (!container.rows.every((row) => row.height === first.height)) continue;
|
|
1527
|
+
if (container.scrollheight <= container.rows.length * first.height) continue;
|
|
1528
|
+
results.push({ selector: container.selector, rendered: container.rows.length, estimated: Math.floor(container.scrollheight / first.height) });
|
|
1529
|
+
}
|
|
1530
|
+
return results;
|
|
1531
|
+
}
|
|
1532
|
+
function lazysurvey(images) {
|
|
1533
|
+
const lazy = [];
|
|
1534
|
+
const placeholders = [];
|
|
1535
|
+
for (const image of images) {
|
|
1536
|
+
if (image.loading === "lazy") lazy.push({ selector: image.selector, reason: "loading attribute" });
|
|
1537
|
+
else if (image.datasrc) lazy.push({ selector: image.selector, reason: "deferred source" });
|
|
1538
|
+
if (!image.src) placeholders.push({ selector: image.selector, reason: "empty source" });
|
|
1539
|
+
else if (image.src.startsWith("data:")) placeholders.push({ selector: image.selector, reason: "inline data placeholder" });
|
|
1540
|
+
}
|
|
1541
|
+
return { lazy, placeholders };
|
|
1542
|
+
}
|
|
1543
|
+
function overlaygeometry(elements, viewport) {
|
|
1544
|
+
const area = viewport.width * viewport.height;
|
|
1545
|
+
return elements.filter((element) => (element.position === "sticky" || element.position === "fixed") && element.top <= 0 && element.height > 0).map((element) => {
|
|
1546
|
+
const coverage = area > 0 ? element.height * element.width / area : 0;
|
|
1547
|
+
return { selector: element.selector, position: element.position, coverage: Math.round(coverage * 1e3) / 1e3, hides: coverage >= overlaythreshold };
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
function scrolllockstate(signals) {
|
|
1551
|
+
const reasons = [];
|
|
1552
|
+
if (signals.bodyoverflow.includes("hidden") || signals.htmloverflow.includes("hidden")) reasons.push("overflow hidden");
|
|
1553
|
+
if (signals.bodyposition === "fixed") reasons.push("fixed body");
|
|
1554
|
+
if (signals.modal) reasons.push("modal open");
|
|
1555
|
+
return { locked: reasons.length > 0, reasons, scrollable: signals.scrollable };
|
|
1556
|
+
}
|
|
1557
|
+
var consentkeywords = ["cookie", "consent", "gdpr", "lgpd", "privacy", "ccpa"];
|
|
1558
|
+
function bannermatches(candidates, at) {
|
|
1559
|
+
const reports = [];
|
|
1560
|
+
for (const candidate of candidates) {
|
|
1561
|
+
const haystack = `${candidate.id} ${candidate.classes.join(" ")} ${candidate.text}`.toLowerCase();
|
|
1562
|
+
const keyword = consentkeywords.find((word) => haystack.includes(word));
|
|
1563
|
+
if (!keyword) continue;
|
|
1564
|
+
if (!candidate.text && candidate.controls.length === 0) continue;
|
|
1565
|
+
reports.push({ kind: keyword, selector: candidate.selector, text: candidate.text.slice(0, 200), controls: candidate.controls, at });
|
|
1566
|
+
}
|
|
1567
|
+
return reports;
|
|
1568
|
+
}
|
|
1569
|
+
function classifytemplate(signals) {
|
|
1570
|
+
if (signals.password) return "login";
|
|
1571
|
+
if (signals.paragraphs >= 3) return "article";
|
|
1572
|
+
if (signals.tables > 0) return "table";
|
|
1573
|
+
if (signals.forms > 0 && signals.inputs > 0) return "form";
|
|
1574
|
+
if (signals.lists > 0) return "list";
|
|
1575
|
+
return "generic";
|
|
1576
|
+
}
|
|
1577
|
+
function sectionfingerprint(section) {
|
|
1578
|
+
const canonical = [section.tag, String(section.children), String(section.textlength), ...Object.keys(section.attributes).sort().map((key) => `${key}=${section.attributes[key] ?? ""}`)].join("|");
|
|
1579
|
+
let hash = 5381;
|
|
1580
|
+
for (let index = 0; index < canonical.length; index += 1) hash = (hash << 5) + hash + canonical.charCodeAt(index) >>> 0;
|
|
1581
|
+
return `fp${hash.toString(16)}`;
|
|
1582
|
+
}
|
|
1583
|
+
function scrollreport(window2, containers) {
|
|
1584
|
+
const range = Math.max(0, window2.scrollheight - window2.clientheight);
|
|
1585
|
+
return {
|
|
1586
|
+
window: { x: window2.scrollx, y: window2.scrolly, attop: window2.scrolly <= 0, atbottom: window2.scrolly >= range, height: window2.scrollheight },
|
|
1587
|
+
containers: containers.map((container) => {
|
|
1588
|
+
const containerrange = Math.max(0, container.scrollheight - container.clientheight);
|
|
1589
|
+
return { selector: container.selector, scrolltop: container.scrolltop, scrollleft: container.scrollleft, scrollrange: containerrange, atbottom: container.scrolltop >= containerrange };
|
|
1590
|
+
})
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
var loadmorepattern = /(load more|show more|see more|ver mais|carregar mais|load older|afficher plus|mehr anzeigen)/i;
|
|
1594
|
+
var paginationtext = /^(next|prev|previous|last|first|next page|previous page|»|«|›|‹|\d+)$/i;
|
|
1595
|
+
var overlaythreshold = 0.25;
|
|
1596
|
+
function signatureof(element) {
|
|
1597
|
+
return `${element.tagName.toLowerCase()}|${[...element.classList].sort().join(" ")}`;
|
|
1598
|
+
}
|
|
1599
|
+
function collectsiblings(root) {
|
|
1600
|
+
const samples = [];
|
|
1601
|
+
for (const element of [...root.querySelectorAll("*")]) {
|
|
1602
|
+
const children = [...element.children];
|
|
1603
|
+
if (children.length < 2) continue;
|
|
1604
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1605
|
+
for (const child of children) {
|
|
1606
|
+
const key = signatureof(child);
|
|
1607
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1608
|
+
}
|
|
1609
|
+
if (![...counts.values()].some((count) => count >= 2)) continue;
|
|
1610
|
+
samples.push({
|
|
1611
|
+
container: elementselector(element),
|
|
1612
|
+
children: children.map((child) => ({ tag: child.tagName.toLowerCase(), classes: [...child.classList].sort().join(" "), text: clean(child.textContent ?? ""), selector: elementselector(child) }))
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
return samples;
|
|
1616
|
+
}
|
|
1617
|
+
function collecttables(root) {
|
|
1618
|
+
return [...root.querySelectorAll("table")].map((table) => ({
|
|
1619
|
+
selector: elementselector(table),
|
|
1620
|
+
rows: [...table.querySelectorAll("tr")].map((row) => ({ cells: [...row.querySelectorAll("th, td")].map((cell) => clean(cell.textContent ?? "")), header: Boolean(row.querySelector("th")) })),
|
|
1621
|
+
caption: clean(table.querySelector("caption")?.textContent ?? "")
|
|
1622
|
+
}));
|
|
1623
|
+
}
|
|
1624
|
+
function collectpagination(root) {
|
|
1625
|
+
const entries = [];
|
|
1626
|
+
for (const element of [...root.querySelectorAll("a[href], button, [role=button], [role=link], li, span")]) {
|
|
1627
|
+
const text = clean(element.textContent ?? "");
|
|
1628
|
+
if (!text || !paginationtext.test(text)) continue;
|
|
1629
|
+
if (!element.closest("nav, footer, [class*=pag i], [id*=pag i]")) continue;
|
|
1630
|
+
const current = element.getAttribute("aria-current") === "page" || [...element.classList].some((name) => /current|active|selecionado/i.test(name));
|
|
1631
|
+
entries.push({ text, selector: elementselector(element), current });
|
|
1632
|
+
}
|
|
1633
|
+
return entries;
|
|
1634
|
+
}
|
|
1635
|
+
function collecttriggers(scope) {
|
|
1636
|
+
const triggers = [];
|
|
1637
|
+
for (const element of [...scope.querySelectorAll("button, a[href], [role=button], [class*=loading i], [class*=sentinel i], [class*=spinner i]")]) {
|
|
1638
|
+
const label = clean(element.getAttribute("aria-label") ?? element.textContent ?? "");
|
|
1639
|
+
if (loadmorepattern.test(label)) triggers.push(elementselector(element));
|
|
1640
|
+
}
|
|
1641
|
+
return triggers;
|
|
1642
|
+
}
|
|
1643
|
+
function collectscrollranges(root) {
|
|
1644
|
+
const ranges = [];
|
|
1645
|
+
const scrolling = root.scrollingElement ?? root.documentElement;
|
|
1646
|
+
const viewheight = root.defaultView?.innerHeight ?? 0;
|
|
1647
|
+
if (scrolling && scrolling.scrollHeight > viewheight) ranges.push({ selector: "window", scrollheight: scrolling.scrollHeight, clientheight: viewheight, triggers: collecttriggers(root) });
|
|
1648
|
+
for (const element of [...root.querySelectorAll("*")]) {
|
|
1649
|
+
if (!(element instanceof HTMLElement)) continue;
|
|
1650
|
+
if (element.scrollHeight <= element.clientHeight) continue;
|
|
1651
|
+
ranges.push({ selector: elementselector(element), scrollheight: element.scrollHeight, clientheight: element.clientHeight, triggers: collecttriggers(element) });
|
|
1652
|
+
}
|
|
1653
|
+
return ranges;
|
|
1654
|
+
}
|
|
1655
|
+
function collectvirtual(root) {
|
|
1656
|
+
const containers = [];
|
|
1657
|
+
for (const element of [...root.querySelectorAll("*")]) {
|
|
1658
|
+
const children = [...element.children];
|
|
1659
|
+
const first = children[0];
|
|
1660
|
+
if (!first || children.length < 2) continue;
|
|
1661
|
+
if (!children.every((child) => signatureof(child) === signatureof(first))) continue;
|
|
1662
|
+
const heights = children.map((child) => child.getBoundingClientRect().height);
|
|
1663
|
+
if (!heights.every((height) => height > 0 && height === heights[0])) continue;
|
|
1664
|
+
containers.push({ selector: elementselector(element), scrollheight: element.scrollHeight, rows: children.map((child) => ({ selector: elementselector(child), height: child.getBoundingClientRect().height, classes: [...child.classList].join(" ") })) });
|
|
1665
|
+
}
|
|
1666
|
+
return containers;
|
|
1667
|
+
}
|
|
1668
|
+
function collectimages(root) {
|
|
1669
|
+
return [...root.querySelectorAll("img")].map((image) => ({
|
|
1670
|
+
selector: elementselector(image),
|
|
1671
|
+
src: image.getAttribute("src") ?? "",
|
|
1672
|
+
datasrc: image.getAttribute("data-src") ?? image.getAttribute("data-original") ?? "",
|
|
1673
|
+
loading: image.getAttribute("loading") ?? "",
|
|
1674
|
+
width: image.naturalWidth,
|
|
1675
|
+
height: image.naturalHeight
|
|
1676
|
+
}));
|
|
1677
|
+
}
|
|
1678
|
+
function collectoverlays(root) {
|
|
1679
|
+
const elements = [];
|
|
1680
|
+
for (const element of [...root.querySelectorAll("*")]) {
|
|
1681
|
+
if (!(element instanceof HTMLElement)) continue;
|
|
1682
|
+
const view = element.ownerDocument.defaultView;
|
|
1683
|
+
const position = view ? view.getComputedStyle(element).position : "";
|
|
1684
|
+
if (position !== "sticky" && position !== "fixed") continue;
|
|
1685
|
+
const rect = element.getBoundingClientRect();
|
|
1686
|
+
elements.push({ selector: elementselector(element), position, top: rect.top, height: rect.height, width: rect.width });
|
|
1687
|
+
}
|
|
1688
|
+
return elements;
|
|
1689
|
+
}
|
|
1690
|
+
function collectlocksignals(root) {
|
|
1691
|
+
const view = root.defaultView;
|
|
1692
|
+
const bodystyle = root.body ? view ? view.getComputedStyle(root.body) : void 0 : void 0;
|
|
1693
|
+
const htmlstyle = view ? view.getComputedStyle(root.documentElement) : void 0;
|
|
1694
|
+
return {
|
|
1695
|
+
bodyoverflow: bodystyle?.overflow ?? "",
|
|
1696
|
+
htmloverflow: htmlstyle?.overflow ?? "",
|
|
1697
|
+
bodyposition: bodystyle?.position ?? "",
|
|
1698
|
+
modal: Boolean(root.querySelector("dialog[open], [aria-modal=true]")),
|
|
1699
|
+
scrollable: root.documentElement.scrollHeight > root.documentElement.clientHeight
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
var bannerselector = '[id*="cookie" i], [class*="cookie" i], [id*="consent" i], [class*="consent" i], [id*="gdpr" i], [class*="gdpr" i], [id*="privacy" i], [class*="privacy" i], [id*="banner" i], [class*="banner" i], dialog, [role="dialog"], [aria-modal="true"]';
|
|
1703
|
+
function collectbannercandidates(root) {
|
|
1704
|
+
const found = [...root.querySelectorAll(bannerselector)];
|
|
1705
|
+
return found.filter((element) => !found.some((other) => other !== element && other.contains(element))).map((element) => ({
|
|
1706
|
+
selector: elementselector(element),
|
|
1707
|
+
id: element.id,
|
|
1708
|
+
classes: [...element.classList],
|
|
1709
|
+
text: clean(element.textContent ?? "").slice(0, 200),
|
|
1710
|
+
controls: [...element.querySelectorAll("button, a[href], [role=button]")].map((control) => clean(control.getAttribute("aria-label") ?? control.textContent ?? "")).filter(Boolean)
|
|
1711
|
+
}));
|
|
1712
|
+
}
|
|
1713
|
+
function runpagedetection(step, target, root = document) {
|
|
1714
|
+
switch (step.kind) {
|
|
1715
|
+
case "detectlists": {
|
|
1716
|
+
const patterns = detectlistpatterns(collectsiblings(root));
|
|
1717
|
+
return { ok: true, summary: `Detected ${patterns.length} repeated list${patterns.length === 1 ? "" : "s"}.`, details: { lists: patterns } };
|
|
1718
|
+
}
|
|
1719
|
+
case "detecttables": {
|
|
1720
|
+
const tables = collecttables(root).map((entry) => {
|
|
1721
|
+
const shape = normalizetable(entry.rows, entry.caption);
|
|
1722
|
+
return { selector: entry.selector, headers: shape.headers, columns: shape.columns, rows: shape.rows, caption: shape.caption };
|
|
1723
|
+
});
|
|
1724
|
+
return { ok: true, summary: `Detected ${tables.length} data table${tables.length === 1 ? "" : "s"}.`, details: { tables } };
|
|
1725
|
+
}
|
|
1726
|
+
case "countpages": {
|
|
1727
|
+
const estimate = paginationestimate(collectpagination(root));
|
|
1728
|
+
return { ok: true, summary: `Counted ${estimate.links} pagination entr${estimate.links === 1 ? "y" : "ies"} and estimated ${estimate.total} total page${estimate.total === 1 ? "" : "s"}.`, details: { current: estimate.current, total: estimate.total, links: estimate.links, pages: estimate.pages } };
|
|
1729
|
+
}
|
|
1730
|
+
case "detectinfinitescroll": {
|
|
1731
|
+
const containers = infinitescrollranges(collectscrollranges(root));
|
|
1732
|
+
return { ok: true, summary: `Detected ${containers.length} infinite scroll container${containers.length === 1 ? "" : "s"}.`, details: { containers } };
|
|
1733
|
+
}
|
|
1734
|
+
case "detectvirtual": {
|
|
1735
|
+
const containers = virtualizedcontainers(collectvirtual(root));
|
|
1736
|
+
return { ok: true, summary: `Detected ${containers.length} virtualized list${containers.length === 1 ? "" : "s"}.`, details: { containers } };
|
|
1737
|
+
}
|
|
1738
|
+
case "detectlazy": {
|
|
1739
|
+
const survey = lazysurvey(collectimages(root));
|
|
1740
|
+
return { ok: true, summary: `Detected ${survey.lazy.length} lazy image${survey.lazy.length === 1 ? "" : "s"} and ${survey.placeholders.length} placeholder${survey.placeholders.length === 1 ? "" : "s"}.`, details: { lazy: survey.lazy, placeholders: survey.placeholders } };
|
|
1741
|
+
}
|
|
1742
|
+
case "detectsticky": {
|
|
1743
|
+
const overlays = overlaygeometry(collectoverlays(root), { width: root.defaultView?.innerWidth ?? 0, height: root.defaultView?.innerHeight ?? 0 });
|
|
1744
|
+
return { ok: true, summary: `Detected ${overlays.length} sticky or fixed overlay${overlays.length === 1 ? "" : "s"}.`, details: { overlays } };
|
|
1745
|
+
}
|
|
1746
|
+
case "detectscrolllock": {
|
|
1747
|
+
const lock = scrolllockstate(collectlocksignals(root));
|
|
1748
|
+
return { ok: true, summary: lock.locked ? `Scroll is locked: ${lock.reasons.join(", ")}.` : "Scroll is not locked.", details: { locked: lock.locked, reasons: lock.reasons, scrollable: lock.scrollable } };
|
|
1749
|
+
}
|
|
1750
|
+
case "classifypage": {
|
|
1751
|
+
const signals = {
|
|
1752
|
+
paragraphs: root.querySelectorAll("p").length,
|
|
1753
|
+
headings: root.querySelectorAll("h1, h2, h3, h4, h5, h6").length,
|
|
1754
|
+
lists: root.querySelectorAll("ul, ol").length,
|
|
1755
|
+
tables: root.querySelectorAll("table").length,
|
|
1756
|
+
forms: root.querySelectorAll("form").length,
|
|
1757
|
+
inputs: root.querySelectorAll("input, textarea, select").length,
|
|
1758
|
+
password: Boolean(root.querySelector("input[type=password]"))
|
|
1759
|
+
};
|
|
1760
|
+
const template = classifytemplate(signals);
|
|
1761
|
+
const fingerprint = sectionfingerprint({ tag: "body", attributes: {}, children: root.body?.children.length ?? 0, textlength: (root.body?.innerText ?? "").length });
|
|
1762
|
+
return { ok: true, summary: `Classified the page template as ${template}.`, details: { template, fingerprint } };
|
|
1763
|
+
}
|
|
1764
|
+
case "fingerprintsection": {
|
|
1765
|
+
if (!target) return { ok: false, summary: "Fingerprint target is no longer available." };
|
|
1766
|
+
const attributes = {};
|
|
1767
|
+
for (const attribute of [...target.attributes]) attributes[attribute.name] = attribute.value;
|
|
1768
|
+
const fingerprint = sectionfingerprint({ tag: target.tagName.toLowerCase(), attributes, children: target.children.length, textlength: (target.textContent ?? "").length });
|
|
1769
|
+
return { ok: true, summary: `Computed section fingerprint ${fingerprint}.`, details: { fingerprint, section: elementselector(target) } };
|
|
1770
|
+
}
|
|
1771
|
+
case "readscrollpos": {
|
|
1772
|
+
const report = scrollreport(
|
|
1773
|
+
{ scrollx: root.defaultView?.scrollX ?? 0, scrolly: root.defaultView?.scrollY ?? 0, scrollheight: root.documentElement.scrollHeight, clientheight: root.defaultView?.innerHeight ?? 0 },
|
|
1774
|
+
[...root.querySelectorAll("*")].filter((element) => element instanceof HTMLElement && element.scrollHeight > element.clientHeight).map((element) => ({ selector: elementselector(element), scrolltop: element.scrollTop, scrollleft: element.scrollLeft, scrollheight: element.scrollHeight, clientheight: element.clientHeight }))
|
|
1775
|
+
);
|
|
1776
|
+
return { ok: true, summary: `Read the scroll position at ${Math.round(report.window.x)},${Math.round(report.window.y)} with ${report.containers.length} scrollable container${report.containers.length === 1 ? "" : "s"}.`, details: { scroll: report } };
|
|
1777
|
+
}
|
|
1778
|
+
default:
|
|
1779
|
+
return { ok: false, summary: "Unsupported page detection." };
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// extension/pagewatch.ts
|
|
1784
|
+
var defaultpoll = 250;
|
|
1785
|
+
function parsewatchoptions(step, fallbackid) {
|
|
1786
|
+
let options = {};
|
|
1787
|
+
try {
|
|
1788
|
+
options = parseoptions(step);
|
|
1789
|
+
} catch {
|
|
1790
|
+
options = {};
|
|
1791
|
+
}
|
|
1792
|
+
const scopes = Array.isArray(options.scopes) ? options.scopes.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
|
|
1793
|
+
const events3 = Array.isArray(options.events) ? options.events.filter((item) => typeof item === "string" && item.trim().length > 0) : void 0;
|
|
1794
|
+
const lifetime = typeof options.lifetime === "number" && Number.isFinite(options.lifetime) && options.lifetime > 0 ? options.lifetime : 0;
|
|
1795
|
+
return {
|
|
1796
|
+
watchid: typeof options.watchid === "string" && options.watchid.trim() ? options.watchid : fallbackid,
|
|
1797
|
+
...scopes ? { scopes } : {},
|
|
1798
|
+
...events3 ? { events: events3 } : {},
|
|
1799
|
+
lifetime,
|
|
1800
|
+
poll: typeof options.poll === "number" && Number.isFinite(options.poll) && options.poll >= 0 ? options.poll : defaultpoll
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
function batchmutations(records, windowms) {
|
|
1804
|
+
const batches = [];
|
|
1805
|
+
let current = [];
|
|
1806
|
+
let opened = -1;
|
|
1807
|
+
for (const record of records) {
|
|
1808
|
+
if (current.length === 0 || windowms > 0 && record.at - opened >= windowms) {
|
|
1809
|
+
if (current.length > 0) batches.push(current);
|
|
1810
|
+
current = [record];
|
|
1811
|
+
opened = record.at;
|
|
1812
|
+
} else current.push(record);
|
|
1813
|
+
}
|
|
1814
|
+
if (current.length > 0) batches.push(current);
|
|
1815
|
+
return batches;
|
|
1816
|
+
}
|
|
1817
|
+
function quietfor(entries, now) {
|
|
1818
|
+
let last = 0;
|
|
1819
|
+
for (const entry of entries) if (entry.responseend > last) last = entry.responseend;
|
|
1820
|
+
return Math.max(0, now - last);
|
|
1821
|
+
}
|
|
1822
|
+
function quietresolution(samples, idle, timeout) {
|
|
1823
|
+
const start = samples[0]?.at ?? 0;
|
|
1824
|
+
const last = samples[samples.length - 1];
|
|
1825
|
+
const waited = Math.max(0, (last?.at ?? 0) - start);
|
|
1826
|
+
const reached = samples.find((sample) => sample.quietfor >= idle);
|
|
1827
|
+
if (reached) return { ok: true, quietfor: reached.quietfor, waited: reached.at - start, samples: samples.length };
|
|
1828
|
+
return { ok: false, quietfor: last?.quietfor ?? 0, waited, samples: samples.length };
|
|
1829
|
+
}
|
|
1830
|
+
function nodehash(summary) {
|
|
1831
|
+
const canonical = [summary.tag, summary.text, ...Object.keys(summary.attributes).sort().map((key) => `${key}=${summary.attributes[key] ?? ""}`)].join("|");
|
|
1832
|
+
let hash = 5381;
|
|
1833
|
+
for (let index = 0; index < canonical.length; index += 1) hash = (hash << 5) + hash + canonical.charCodeAt(index) >>> 0;
|
|
1834
|
+
return hash.toString(16);
|
|
1835
|
+
}
|
|
1836
|
+
function diffsummaries(base, target) {
|
|
1837
|
+
const basemap = new Map(base.map((node) => [node.selector, node]));
|
|
1838
|
+
const targetmap = new Map(target.map((node) => [node.selector, node]));
|
|
1839
|
+
const added = [];
|
|
1840
|
+
const removed = [];
|
|
1841
|
+
const changed = [];
|
|
1842
|
+
for (const [selector, node] of targetmap) {
|
|
1843
|
+
const previous = basemap.get(selector);
|
|
1844
|
+
if (!previous) {
|
|
1845
|
+
added.push({ kind: "added", selector, summary: node.text || node.tag });
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
if (nodehash(previous) !== nodehash(node)) changed.push({ kind: "changed", selector, summary: `${previous.text || previous.tag} became ${node.text || node.tag}` });
|
|
1849
|
+
}
|
|
1850
|
+
for (const [selector, node] of basemap) {
|
|
1851
|
+
if (!targetmap.has(selector)) removed.push({ kind: "removed", selector, summary: node.text || node.tag });
|
|
1852
|
+
}
|
|
1853
|
+
return { added, removed, changed };
|
|
1854
|
+
}
|
|
1855
|
+
function scanjson(scripts) {
|
|
1856
|
+
const states = [];
|
|
1857
|
+
let refused = 0;
|
|
1858
|
+
for (const script of scripts) {
|
|
1859
|
+
if (script.src) continue;
|
|
1860
|
+
const content = script.content.trim();
|
|
1861
|
+
if (!(script.type.includes("json") || content.startsWith("{") || content.startsWith("["))) continue;
|
|
1862
|
+
try {
|
|
1863
|
+
states.push({ scripturl: script.src, rootpath: script.id, payload: JSON.parse(content) });
|
|
1864
|
+
} catch {
|
|
1865
|
+
refused += 1;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return { states, refused };
|
|
1869
|
+
}
|
|
1870
|
+
function rankselectors(shape) {
|
|
1871
|
+
const candidates = [];
|
|
1872
|
+
if (shape.id) candidates.push({ selector: `#${shape.id}`, strategy: "id", score: 100 });
|
|
1873
|
+
for (const [name, value] of Object.entries(shape.attributes)) {
|
|
1874
|
+
if (!value) continue;
|
|
1875
|
+
if (name === "name" || name.startsWith("data-") || name.startsWith("aria-")) candidates.push({ selector: `${shape.tag}[${name}="${value}"]`, strategy: "attribute", score: 80 });
|
|
1876
|
+
}
|
|
1877
|
+
if (shape.text) candidates.push({ selector: shape.text, strategy: "text", score: 60 });
|
|
1878
|
+
if (shape.index > 0) candidates.push({ selector: `${shape.tag}:nth-of-type(${shape.index})`, strategy: "structural", score: 40 });
|
|
1879
|
+
return candidates.sort((left, right) => right.score - left.score);
|
|
1880
|
+
}
|
|
1881
|
+
function wait2(ms) {
|
|
1882
|
+
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
1883
|
+
}
|
|
1884
|
+
function quietruleof(step) {
|
|
1885
|
+
let options = {};
|
|
1886
|
+
try {
|
|
1887
|
+
options = parseoptions(step);
|
|
1888
|
+
} catch {
|
|
1889
|
+
options = {};
|
|
1890
|
+
}
|
|
1891
|
+
const rule = options.quietrule;
|
|
1892
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { idle: 0 };
|
|
1893
|
+
const quiet = rule;
|
|
1894
|
+
return {
|
|
1895
|
+
idle: typeof quiet.idle === "number" && Number.isFinite(quiet.idle) && quiet.idle > 0 ? quiet.idle : 0,
|
|
1896
|
+
...typeof quiet.poll === "number" && Number.isFinite(quiet.poll) && quiet.poll >= 0 ? { poll: quiet.poll } : {},
|
|
1897
|
+
...typeof quiet.timeout === "number" && Number.isFinite(quiet.timeout) && quiet.timeout >= 0 ? { timeout: quiet.timeout } : {}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
async function watchmutations(step, root) {
|
|
1901
|
+
const options = parsewatchoptions(step, step.id);
|
|
1902
|
+
if (options.lifetime <= 0) return { ok: false, summary: "The reviewed mutation watch lifetime is absent." };
|
|
1903
|
+
const roots = options.scopes ? options.scopes.flatMap((selector) => [...root.querySelectorAll(selector)]) : [root];
|
|
1904
|
+
if (roots.length === 0) return { ok: false, summary: "The reviewed watch scopes match no elements." };
|
|
1905
|
+
const allowed = options.events;
|
|
1906
|
+
const collected = [];
|
|
1907
|
+
const observer = new MutationObserver((records) => {
|
|
1908
|
+
for (const record of records) {
|
|
1909
|
+
if (allowed && !allowed.includes(record.type)) continue;
|
|
1910
|
+
const target = record.target instanceof Element ? record.target : null;
|
|
1911
|
+
collected.push({ watchid: options.watchid, event: record.type, targetpath: target ? elementselector(target) : "#text", at: Date.now() });
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
for (const scope of roots) observer.observe(scope, { childList: true, attributes: true, characterData: true, subtree: true });
|
|
1915
|
+
await wait2(options.lifetime);
|
|
1916
|
+
observer.disconnect();
|
|
1917
|
+
const batches = batchmutations(collected, options.poll);
|
|
1918
|
+
return { ok: true, summary: `Watched ${collected.length} mutation${collected.length === 1 ? "" : "s"} in ${batches.length} batch${batches.length === 1 ? "" : "es"} for the reviewed lifetime of ${options.lifetime} milliseconds.`, details: { events: collected, batches: batches.length, watchid: options.watchid, lifetime: options.lifetime, scopes: options.scopes ?? [] } };
|
|
1919
|
+
}
|
|
1920
|
+
async function watchfocus(step, root) {
|
|
1921
|
+
const options = parsewatchoptions(step, step.id);
|
|
1922
|
+
if (options.lifetime <= 0) return { ok: false, summary: "The reviewed focus watch lifetime is absent." };
|
|
1923
|
+
const collected = [];
|
|
1924
|
+
const record = (kind) => (event) => {
|
|
1925
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
1926
|
+
collected.push({ watchid: options.watchid, kind, targetpath: target ? elementselector(target) : "#document", at: Date.now() });
|
|
1927
|
+
};
|
|
1928
|
+
const onfocus = record("focus");
|
|
1929
|
+
const onblur = record("blur");
|
|
1930
|
+
root.addEventListener("focusin", onfocus, true);
|
|
1931
|
+
root.addEventListener("focusout", onblur, true);
|
|
1932
|
+
await wait2(options.lifetime);
|
|
1933
|
+
root.removeEventListener("focusin", onfocus, true);
|
|
1934
|
+
root.removeEventListener("focusout", onblur, true);
|
|
1935
|
+
return { ok: true, summary: `Watched ${collected.length} focus change${collected.length === 1 ? "" : "s"} for the reviewed lifetime of ${options.lifetime} milliseconds.`, details: { events: collected, watchid: options.watchid, lifetime: options.lifetime } };
|
|
1936
|
+
}
|
|
1937
|
+
async function watchbanners(step, root) {
|
|
1938
|
+
const options = parsewatchoptions(step, step.id);
|
|
1939
|
+
if (options.lifetime <= 0) return { ok: false, summary: "The reviewed banner watch lifetime is absent." };
|
|
1940
|
+
const started = Date.now();
|
|
1941
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1942
|
+
while (Date.now() - started < options.lifetime) {
|
|
1943
|
+
const at = Date.now();
|
|
1944
|
+
for (const report of bannermatches(collectbannercandidates(root), at)) {
|
|
1945
|
+
if (!seen.has(report.selector)) seen.set(report.selector, report);
|
|
1946
|
+
}
|
|
1947
|
+
await wait2(options.poll);
|
|
1948
|
+
}
|
|
1949
|
+
const reports = [...seen.values()];
|
|
1950
|
+
return { ok: true, summary: `Watched for consent banners for the reviewed lifetime of ${options.lifetime} milliseconds and observed ${reports.length} banner${reports.length === 1 ? "" : "s"}.`, details: { banners: reports, watchid: options.watchid, lifetime: options.lifetime } };
|
|
1951
|
+
}
|
|
1952
|
+
async function waitquiet(step) {
|
|
1953
|
+
const rule = quietruleof(step);
|
|
1954
|
+
if (rule.idle <= 0) return { ok: false, summary: "The reviewed quiet idle threshold is absent." };
|
|
1955
|
+
const poll2 = rule.poll ?? 100;
|
|
1956
|
+
const timeout = rule.timeout ?? 0;
|
|
1957
|
+
const started = performance.now();
|
|
1958
|
+
const samples = [];
|
|
1959
|
+
for (; ; ) {
|
|
1960
|
+
const now = performance.now();
|
|
1961
|
+
const entries = performance.getEntriesByType("resource").map((entry) => ({ responseend: entry.responseEnd }));
|
|
1962
|
+
samples.push({ at: now - started, quietfor: quietfor(entries, now) });
|
|
1963
|
+
const latest = samples[samples.length - 1];
|
|
1964
|
+
if (latest && latest.quietfor >= rule.idle) break;
|
|
1965
|
+
if (timeout > 0 && now - started >= timeout) break;
|
|
1966
|
+
await wait2(poll2);
|
|
1967
|
+
}
|
|
1968
|
+
const outcome = quietresolution(samples, rule.idle, timeout);
|
|
1969
|
+
return {
|
|
1970
|
+
ok: outcome.ok,
|
|
1971
|
+
summary: outcome.ok ? `The network stayed quiet for ${Math.round(outcome.quietfor)} milliseconds, meeting the reviewed idle threshold of ${rule.idle} milliseconds.` : `The network did not stay quiet for ${rule.idle} milliseconds${timeout > 0 ? ` within the reviewed timeout of ${timeout} milliseconds` : ""}.`,
|
|
1972
|
+
details: { samples, idle: rule.idle, timeout, waited: Math.round(outcome.waited) }
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function scriptsurfaces(target, root) {
|
|
1976
|
+
const elements = target ? [target] : [...root.querySelectorAll("script")];
|
|
1977
|
+
return elements.map((element) => ({ src: element.getAttribute("src") ?? "", type: element.getAttribute("type") ?? "", id: element.id, content: element.textContent ?? "" }));
|
|
1978
|
+
}
|
|
1979
|
+
function readjson(step, target, root) {
|
|
1980
|
+
const outcome = scanjson(scriptsurfaces(target, root));
|
|
1981
|
+
if (target && outcome.states.length === 0 && outcome.refused > 0) return { ok: false, summary: "The reviewed json payload is malformed and was refused." };
|
|
1982
|
+
return { ok: true, summary: `Extracted ${outcome.states.length} embedded json state${outcome.states.length === 1 ? "" : "s"}${outcome.refused > 0 ? ` and refused ${outcome.refused} malformed payload${outcome.refused === 1 ? "" : "s"}` : ""}.`, details: { states: outcome.states, refused: outcome.refused } };
|
|
1983
|
+
}
|
|
1984
|
+
function tonodesummaries(value) {
|
|
1985
|
+
if (!Array.isArray(value)) return null;
|
|
1986
|
+
const summaries = [];
|
|
1987
|
+
for (const entry of value) {
|
|
1988
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
1989
|
+
const candidate = entry;
|
|
1990
|
+
if (typeof candidate.selector !== "string") continue;
|
|
1991
|
+
const attributes = {};
|
|
1992
|
+
if (candidate.attributes && typeof candidate.attributes === "object" && !Array.isArray(candidate.attributes)) {
|
|
1993
|
+
for (const [key, item] of Object.entries(candidate.attributes)) if (typeof item === "string") attributes[key] = item;
|
|
1994
|
+
}
|
|
1995
|
+
summaries.push({ selector: candidate.selector, tag: typeof candidate.tag === "string" ? candidate.tag : "", text: typeof candidate.text === "string" ? candidate.text : "", attributes });
|
|
1996
|
+
}
|
|
1997
|
+
return summaries;
|
|
1998
|
+
}
|
|
1999
|
+
function diffsnapshots(step) {
|
|
2000
|
+
let options = {};
|
|
2001
|
+
try {
|
|
2002
|
+
options = parseoptions(step);
|
|
2003
|
+
} catch {
|
|
2004
|
+
options = {};
|
|
2005
|
+
}
|
|
2006
|
+
const base = tonodesummaries(options.base);
|
|
2007
|
+
const target = tonodesummaries(options.target);
|
|
2008
|
+
if (!base || !target) return { ok: false, summary: "Two stored observation versions must be reviewed before diffing." };
|
|
2009
|
+
const versions = Array.isArray(options.versions) && options.versions.length === 2 ? options.versions : [0, 0];
|
|
2010
|
+
const diff = diffsummaries(base, target);
|
|
2011
|
+
return { ok: true, summary: `Diffed observation versions ${versions[0] ?? 0} and ${versions[1] ?? 0}: ${diff.added.length} added, ${diff.removed.length} removed and ${diff.changed.length} changed node${diff.added.length + diff.removed.length + diff.changed.length === 1 ? "" : "s"}.`, details: { versions, added: diff.added, removed: diff.removed, changed: diff.changed } };
|
|
2012
|
+
}
|
|
2013
|
+
function deriveselector(target) {
|
|
2014
|
+
if (!(target instanceof Element)) return { ok: false, summary: "Derivation target is no longer available." };
|
|
2015
|
+
const attributes = {};
|
|
2016
|
+
for (const attribute of [...target.attributes]) attributes[attribute.name] = attribute.value;
|
|
2017
|
+
const parent = target.parentElement;
|
|
2018
|
+
const siblings = parent ? [...parent.children].filter((node) => node.tagName === target.tagName) : [target];
|
|
2019
|
+
const candidates = rankselectors({ id: target.id, tag: target.tagName.toLowerCase(), attributes, text: clean(target.textContent ?? "").slice(0, 80), index: siblings.indexOf(target) + 1, siblings: siblings.length });
|
|
2020
|
+
const best = candidates[0];
|
|
2021
|
+
return { ok: candidates.length > 0, summary: best ? `Derived ${candidates.length} selector candidate${candidates.length === 1 ? "" : "s"}; the most stable is ${best.selector} through the ${best.strategy} strategy with stability ${best.score}.` : "No selector candidate could be derived.", details: { candidates } };
|
|
2022
|
+
}
|
|
2023
|
+
function runpagewatch(step, target, root = document) {
|
|
2024
|
+
switch (step.kind) {
|
|
2025
|
+
case "watchmutate":
|
|
2026
|
+
return watchmutations(step, root);
|
|
2027
|
+
case "watchfocus":
|
|
2028
|
+
return watchfocus(step, root);
|
|
2029
|
+
case "watchbanner":
|
|
2030
|
+
return watchbanners(step, root);
|
|
2031
|
+
case "waitquiet":
|
|
2032
|
+
return waitquiet(step);
|
|
2033
|
+
case "readjson":
|
|
2034
|
+
return readjson(step, target, root);
|
|
2035
|
+
case "diffsnapshots":
|
|
2036
|
+
return diffsnapshots(step);
|
|
2037
|
+
case "deriveselector":
|
|
2038
|
+
return deriveselector(target);
|
|
2039
|
+
default:
|
|
2040
|
+
return { ok: false, summary: "Unsupported watched observation." };
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// extension/pagebridge.ts
|
|
2045
|
+
function stepoptions2(step) {
|
|
351
2046
|
if (!step.options) return {};
|
|
352
2047
|
try {
|
|
353
2048
|
const parsed = JSON.parse(step.options);
|
|
@@ -360,11 +2055,13 @@
|
|
|
360
2055
|
function clearpreview() {
|
|
361
2056
|
document.getElementById(previewid)?.remove();
|
|
362
2057
|
}
|
|
363
|
-
function previewtarget(
|
|
2058
|
+
function previewtarget(step, expectedorigin) {
|
|
364
2059
|
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before preview." };
|
|
365
2060
|
clearpreview();
|
|
366
|
-
const
|
|
367
|
-
if (
|
|
2061
|
+
const resolution = resolvestep(step, document);
|
|
2062
|
+
if (resolution.status === "ambiguous") return { ok: false, summary: `The reviewed ${resolution.mode} reference matched ${resolution.candidates.length} elements: ${resolution.candidates.join("; ")}.`, candidates: resolution.candidates };
|
|
2063
|
+
if (resolution.status !== "resolved") return { ok: false, summary: "Reviewed target is no longer available." };
|
|
2064
|
+
const target = resolution.element;
|
|
368
2065
|
const rect = target.getBoundingClientRect();
|
|
369
2066
|
if (rect.width <= 0 || rect.height <= 0) return { ok: false, summary: "Reviewed target is not currently visible." };
|
|
370
2067
|
const overlay = document.createElement("div");
|
|
@@ -373,43 +2070,65 @@
|
|
|
373
2070
|
Object.assign(overlay.style, { position: "fixed", left: `${Math.max(0, rect.left - 3)}px`, top: `${Math.max(0, rect.top - 3)}px`, width: `${rect.width + 6}px`, height: `${rect.height + 6}px`, border: "3px solid #2f80ed", borderRadius: "6px", boxShadow: "0 0 0 3px rgba(47,128,237,.28)", pointerEvents: "none", zIndex: "2147483647", boxSizing: "border-box" });
|
|
374
2071
|
document.documentElement.append(overlay);
|
|
375
2072
|
window.setTimeout(clearpreview, 5e3);
|
|
376
|
-
return { ok: true, summary: `Previewing ${
|
|
2073
|
+
return { ok: true, summary: `Previewing ${elementlabel(target) || target.tagName.toLowerCase()} for five seconds.`, resolvedtarget: resolution.target };
|
|
377
2074
|
}
|
|
378
2075
|
function capturesnapshot() {
|
|
379
2076
|
const candidates = [...document.querySelectorAll("a[href], button, input, textarea, select, [role=button], [role=link], [role=combobox], [role=option], [role=checkbox], [role=radio], [role=switch], [role=tab], details, summary")];
|
|
380
|
-
const interactive = candidates.map((element) => ({ selector:
|
|
2077
|
+
const interactive = candidates.map((element) => ({ selector: elementselector(element), role: element.getAttribute("role") || element.tagName.toLowerCase(), label: elementlabel(element) })).filter((item) => item.label || item.role);
|
|
381
2078
|
const forms = [...document.querySelectorAll("input, textarea, select")].map((element) => ({
|
|
382
|
-
label:
|
|
2079
|
+
label: elementlabel(element),
|
|
383
2080
|
type: element.getAttribute("type") || element.tagName.toLowerCase(),
|
|
384
2081
|
name: element.getAttribute("name") || "",
|
|
385
2082
|
...element instanceof HTMLSelectElement ? { options: [...element.options].map((option) => clean(option.textContent || option.value)) } : {}
|
|
386
2083
|
}));
|
|
387
2084
|
const text = clean(document.body?.innerText || "");
|
|
388
|
-
|
|
2085
|
+
const tree = buildpagetree(document);
|
|
2086
|
+
const tables = collecttables(document).map((entry) => {
|
|
2087
|
+
const shape = normalizetable(entry.rows, entry.caption);
|
|
2088
|
+
return { selector: entry.selector, headers: shape.headers, columns: shape.columns, rows: shape.rows, caption: shape.caption };
|
|
2089
|
+
});
|
|
2090
|
+
return {
|
|
2091
|
+
schemaversion: 3,
|
|
2092
|
+
url: location.href,
|
|
2093
|
+
title: clean(document.title),
|
|
2094
|
+
textpreview: text,
|
|
2095
|
+
textlength: document.body?.innerText.length ?? 0,
|
|
2096
|
+
forms,
|
|
2097
|
+
interactive,
|
|
2098
|
+
capturedat: Date.now(),
|
|
2099
|
+
mode: "passive",
|
|
2100
|
+
a11y: builda11ytree(tree),
|
|
2101
|
+
reader: buildreader(tree, clean(document.title)),
|
|
2102
|
+
listpattern: detectlistpatterns(collectsiblings(document)),
|
|
2103
|
+
tableshape: tables
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function readdialogs() {
|
|
2107
|
+
return harvestdialoglog(document);
|
|
389
2108
|
}
|
|
390
|
-
function extractcontent(targetselector) {
|
|
2109
|
+
function extractcontent(targetselector, root) {
|
|
391
2110
|
if (!targetselector) {
|
|
392
|
-
const links = [...
|
|
2111
|
+
const links = [...root.querySelectorAll("a[href]")].map((element) => {
|
|
393
2112
|
const href = element instanceof HTMLAnchorElement ? element.getAttribute("href") ?? "" : "";
|
|
394
2113
|
return { text: element.textContent?.trim() ?? "", href };
|
|
395
2114
|
});
|
|
396
2115
|
return { ok: true, summary: `Extracted ${links.length} link entries.`, details: { links } };
|
|
397
2116
|
}
|
|
398
|
-
const target =
|
|
2117
|
+
const target = root.querySelector(targetselector);
|
|
399
2118
|
if (!target) return { ok: false, summary: "Extraction target is no longer available." };
|
|
400
2119
|
const text = target.textContent ?? "";
|
|
401
2120
|
return { ok: true, summary: `Extracted ${text.length} characters of content.`, details: { text } };
|
|
402
2121
|
}
|
|
403
2122
|
function scrolltarget(target) {
|
|
404
2123
|
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
|
|
405
|
-
return { ok: true, summary: `Scrolled ${
|
|
2124
|
+
return { ok: true, summary: `Scrolled ${elementlabel(target) || target.tagName.toLowerCase()} into view.` };
|
|
406
2125
|
}
|
|
407
2126
|
function hovertarget(target) {
|
|
408
2127
|
for (const type of ["pointerover", "mouseover", "pointerenter"]) {
|
|
409
2128
|
target.dispatchEvent(new PointerEvent(type, { bubbles: type !== "pointerenter", cancelable: true, composed: true }));
|
|
410
2129
|
}
|
|
411
2130
|
target.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false, cancelable: true }));
|
|
412
|
-
return { ok: true, summary: `Hover events delivered to ${
|
|
2131
|
+
return { ok: true, summary: `Hover events delivered to ${elementlabel(target) || target.tagName.toLowerCase()}.` };
|
|
413
2132
|
}
|
|
414
2133
|
function selectoption(target, value) {
|
|
415
2134
|
if (!(target instanceof HTMLSelectElement)) return { ok: false, summary: "Target is not a select element." };
|
|
@@ -420,9 +2139,15 @@
|
|
|
420
2139
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
421
2140
|
return { ok: true, summary: `Selected ${clean(option.textContent || option.value)}.` };
|
|
422
2141
|
}
|
|
423
|
-
var readkinds = /* @__PURE__ */ new Set(["readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "waitfor", "waittext", "highlight"]);
|
|
2142
|
+
var readkinds = /* @__PURE__ */ new Set(["readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "waitfor", "waittext", "highlight", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath"]);
|
|
424
2143
|
var mutatingkinds = /* @__PURE__ */ new Set(["presskey", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "setattribute", "removeattribute", "writestorage", "evaluate", "fullscreen"]);
|
|
425
|
-
|
|
2144
|
+
var controlkinds = /* @__PURE__ */ new Set(["typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails"]);
|
|
2145
|
+
var interactkinds = /* @__PURE__ */ new Set(["clicktext", "clickaria", "clickname", "pierceshadow", "enterframe"]);
|
|
2146
|
+
var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick"]);
|
|
2147
|
+
var observationkinds = /* @__PURE__ */ new Set(["a11ytree", "readvisible", "readertree", "readoutline", "readselection", "readopengraph", "readlang", "detectlanguage", "listshadow", "listframes"]);
|
|
2148
|
+
var detectionkinds = /* @__PURE__ */ new Set(["detectlists", "detecttables", "detectinfinitescroll", "detectvirtual", "detectlazy", "detectsticky", "detectscrolllock", "countpages", "classifypage", "fingerprintsection", "readscrollpos"]);
|
|
2149
|
+
var watchstepkinds = /* @__PURE__ */ new Set(["watchmutate", "watchbanner", "watchfocus", "waitquiet", "readjson", "diffsnapshots", "deriveselector"]);
|
|
2150
|
+
async function performstep(step, expectedorigin, rootdocument = document) {
|
|
426
2151
|
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
|
|
427
2152
|
if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
|
|
428
2153
|
if (step.kind === "wait") {
|
|
@@ -430,7 +2155,7 @@
|
|
|
430
2155
|
const duration = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
431
2156
|
return new Promise((resolve) => window.setTimeout(() => resolve({ ok: true, summary: `Reviewed wait of ${duration} milliseconds completed.` }), duration));
|
|
432
2157
|
}
|
|
433
|
-
if (step.kind === "extract") return extractcontent(step.target);
|
|
2158
|
+
if (step.kind === "extract") return extractcontent(step.target, rootdocument);
|
|
434
2159
|
if (step.kind === "navigate") {
|
|
435
2160
|
if (!step.value || new URL(step.value).origin !== expectedorigin) return { ok: false, summary: "Navigation target is outside the approved origin." };
|
|
436
2161
|
location.assign(step.value);
|
|
@@ -449,7 +2174,7 @@
|
|
|
449
2174
|
return { ok: true, summary: "History forward requested." };
|
|
450
2175
|
}
|
|
451
2176
|
if (step.kind === "scrollpage") {
|
|
452
|
-
const options =
|
|
2177
|
+
const options = stepoptions2(step);
|
|
453
2178
|
window.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
|
|
454
2179
|
return { ok: true, summary: "Window scrolled by the reviewed amounts." };
|
|
455
2180
|
}
|
|
@@ -461,39 +2186,52 @@
|
|
|
461
2186
|
window.scrollTo(0, 0);
|
|
462
2187
|
return { ok: true, summary: "Window scrolled to the page top." };
|
|
463
2188
|
}
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (step.kind
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
2189
|
+
const resolution = resolvestep(step, rootdocument);
|
|
2190
|
+
if (resolution.status === "ambiguous") {
|
|
2191
|
+
return { ok: false, summary: `The reviewed ${resolution.mode} reference matched ${resolution.candidates.length} elements: ${resolution.candidates.join("; ")}.`, details: { mode: resolution.mode, candidates: resolution.candidates } };
|
|
2192
|
+
}
|
|
2193
|
+
const element = resolution.status === "resolved" ? resolution.element : null;
|
|
2194
|
+
let result;
|
|
2195
|
+
if (readkinds.has(step.kind)) result = runpageread(step, element, rootdocument);
|
|
2196
|
+
else if (controlkinds.has(step.kind)) result = runpagecontrol(step, element, rootdocument);
|
|
2197
|
+
else if (interactkinds.has(step.kind)) return await runinteractstep(step, expectedorigin, performstep);
|
|
2198
|
+
else if (pointerkinds.has(step.kind)) result = runpointerstep(step, resolution);
|
|
2199
|
+
else if (observationkinds.has(step.kind)) result = runpageobservation(step, element, rootdocument);
|
|
2200
|
+
else if (detectionkinds.has(step.kind)) result = runpagedetection(step, element, rootdocument);
|
|
2201
|
+
else if (watchstepkinds.has(step.kind)) result = runpagewatch(step, element, rootdocument);
|
|
2202
|
+
else if (mutatingkinds.has(step.kind)) result = runpageaction(step, element);
|
|
2203
|
+
else {
|
|
2204
|
+
if (!element) return { ok: false, summary: "Action target is no longer available." };
|
|
2205
|
+
if (step.kind === "scrollby") {
|
|
2206
|
+
const options = stepoptions2(step);
|
|
2207
|
+
element.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
|
|
2208
|
+
result = { ok: true, summary: "Container scrolled by the reviewed amounts." };
|
|
2209
|
+
} else if (step.kind === "focus") {
|
|
2210
|
+
element.focus();
|
|
2211
|
+
result = { ok: true, summary: "Target focused." };
|
|
2212
|
+
} else if (step.kind === "inspect") result = { ok: true, summary: `Target: ${elementlabel(element) || element.tagName.toLowerCase()}.` };
|
|
2213
|
+
else if (step.kind === "click") {
|
|
2214
|
+
element.click();
|
|
2215
|
+
result = { ok: true, summary: "Reviewed click completed." };
|
|
2216
|
+
} else if (step.kind === "scroll") result = scrolltarget(element);
|
|
2217
|
+
else if (step.kind === "hover") result = hovertarget(element);
|
|
2218
|
+
else if (step.kind === "select") result = selectoption(element, step.value ?? "");
|
|
2219
|
+
else if (step.kind === "type") {
|
|
2220
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) return { ok: false, summary: "Target cannot receive text." };
|
|
2221
|
+
if (typeof step.value !== "string") return { ok: false, summary: "Approved text is absent." };
|
|
2222
|
+
element.focus();
|
|
2223
|
+
element.value = step.value;
|
|
2224
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2225
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2226
|
+
result = { ok: true, summary: "Approved text entered." };
|
|
2227
|
+
} else return { ok: false, summary: "Unsupported action." };
|
|
2228
|
+
}
|
|
2229
|
+
const output = await result;
|
|
2230
|
+
if (resolution.status === "resolved") {
|
|
2231
|
+
return { ...output, details: { ...output.details ?? {}, mode: resolution.target.mode, resolvedtarget: resolution.target } };
|
|
2232
|
+
}
|
|
2233
|
+
return output;
|
|
2234
|
+
}
|
|
2235
|
+
Object.assign(globalThis, { devthinkbridge: { capturesnapshot, previewtarget, performstep, readdialogs } });
|
|
498
2236
|
})();
|
|
499
2237
|
//# sourceMappingURL=pagebridge.js.map
|