@wenathlan/extension 1.1.32 → 1.1.33
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 +5 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +221 -12
- package/dist/index.js.map +2 -2
- package/dist/memory.d.ts +33 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/policy.d.ts +8 -0
- package/dist/policy.d.ts.map +1 -1
- package/dist/protocol.d.ts +33 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/types.d.ts +114 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/extension/dist/background.js +565 -23
- package/extension/dist/background.js.map +4 -4
- package/extension/dist/manifest.json +1 -1
- package/extension/dist/pagebridge.js +894 -86
- package/extension/dist/pagebridge.js.map +4 -4
- package/extension/dist/popup.html +1 -1
- package/extension/dist/popup.js +12 -3
- package/extension/dist/popup.js.map +2 -2
- package/extension/dist/sidepanel.html +1 -1
- package/extension/dist/sidepanel.js +134 -25
- package/extension/dist/sidepanel.js.map +2 -2
- package/extension/dist/style.css +1 -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"]);
|
|
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,348 @@
|
|
|
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 queryshadowchain(root, selectors) {
|
|
444
|
+
let scope = root;
|
|
445
|
+
for (let position = 0; position < selectors.length; position += 1) {
|
|
446
|
+
const found = scope.querySelector(selectors[position]);
|
|
447
|
+
if (!found) return null;
|
|
448
|
+
if (position === selectors.length - 1) return found;
|
|
449
|
+
const shadow = found.shadowRoot;
|
|
450
|
+
if (!shadow) return null;
|
|
451
|
+
scope = shadow;
|
|
452
|
+
}
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
function queryscoped(root, selector) {
|
|
456
|
+
const direct = root.querySelector(selector);
|
|
457
|
+
if (direct) return direct;
|
|
458
|
+
for (const element of [...root.querySelectorAll("*")]) {
|
|
459
|
+
const shadow = element.shadowRoot;
|
|
460
|
+
if (shadow) {
|
|
461
|
+
const found = queryscoped(shadow, selector);
|
|
462
|
+
if (found) return found;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
function parseoptionssafe(step) {
|
|
468
|
+
try {
|
|
469
|
+
return parseoptions(step);
|
|
470
|
+
} catch {
|
|
471
|
+
return {};
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function targetsummary(mode, element) {
|
|
475
|
+
const rect = element.getBoundingClientRect();
|
|
476
|
+
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 } };
|
|
477
|
+
}
|
|
478
|
+
function singleresolution(mode, matches) {
|
|
479
|
+
const verdict = resolutionverdict(matches.length);
|
|
480
|
+
if (verdict === "resolved") {
|
|
481
|
+
const winner = matches[0];
|
|
482
|
+
if (winner && winner.element instanceof HTMLElement) return { status: "resolved", element: winner.element, target: targetsummary(mode, winner.element) };
|
|
483
|
+
return { status: "absent", mode };
|
|
484
|
+
}
|
|
485
|
+
if (verdict === "ambiguous") return { status: "ambiguous", mode, candidates: matches.slice(0, 8).map((candidate) => candidate.label || candidate.selector) };
|
|
486
|
+
return { status: "absent", mode };
|
|
487
|
+
}
|
|
488
|
+
function resolvetargetref(reference, root) {
|
|
489
|
+
const mode = reference.mode;
|
|
490
|
+
if (mode === "selector") {
|
|
491
|
+
const selector = typeof reference.selector === "string" ? reference.selector : "";
|
|
492
|
+
const element = selector ? root.querySelector(selector) : null;
|
|
493
|
+
return element instanceof HTMLElement ? { status: "resolved", element, target: targetsummary("selector", element) } : { status: "absent", mode: "selector" };
|
|
494
|
+
}
|
|
495
|
+
if (mode === "point") {
|
|
496
|
+
const x = Number(reference.x);
|
|
497
|
+
const y = Number(reference.y);
|
|
498
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { status: "absent", mode: "point" };
|
|
499
|
+
const element = root.elementFromPoint(x, y);
|
|
500
|
+
return element instanceof HTMLElement ? { status: "resolved", element, target: targetsummary("point", element) } : { status: "absent", mode: "point" };
|
|
501
|
+
}
|
|
502
|
+
if (mode === "xpath") {
|
|
503
|
+
const expression = typeof reference.xpath === "string" ? reference.xpath : "";
|
|
504
|
+
if (!expression) return { status: "absent", mode: "xpath" };
|
|
505
|
+
const matches = evaluatexpath(buildxtree(root), expression);
|
|
506
|
+
const first = matches[0];
|
|
507
|
+
return first?.element instanceof HTMLElement ? { status: "resolved", element: first.element, target: targetsummary("xpath", first.element) } : { status: "absent", mode: "xpath" };
|
|
508
|
+
}
|
|
509
|
+
if (mode === "index") {
|
|
510
|
+
const matches = matchindex(collectclickable(root), Number(reference.index));
|
|
511
|
+
return singleresolution("index", matches);
|
|
512
|
+
}
|
|
513
|
+
const candidates = collectcandidates(root);
|
|
514
|
+
if (mode === "text") return singleresolution("text", matchtext(candidates, typeof reference.text === "string" ? reference.text : ""));
|
|
515
|
+
if (mode === "aria") return singleresolution("aria", matcharia(candidates, typeof reference.role === "string" ? reference.role : "", typeof reference.name === "string" ? reference.name : ""));
|
|
516
|
+
if (mode === "name") return singleresolution("name", matchname(candidates, typeof reference.name === "string" ? reference.name : ""));
|
|
517
|
+
return { status: "absent" };
|
|
518
|
+
}
|
|
519
|
+
function resolvestep(step, root) {
|
|
520
|
+
const reference = parseoptionssafe(step).targetref;
|
|
521
|
+
if (reference && typeof reference === "object" && !Array.isArray(reference)) return resolvetargetref(reference, root);
|
|
522
|
+
if (!step.target?.trim()) return { status: "none" };
|
|
523
|
+
const element = root.querySelector(step.target);
|
|
524
|
+
if (element instanceof HTMLElement) return { status: "resolved", element, target: targetsummary("selector", element) };
|
|
525
|
+
return { status: "absent", mode: "selector" };
|
|
526
|
+
}
|
|
527
|
+
|
|
182
528
|
// extension/pagereads.ts
|
|
183
529
|
var highlightid = "devthinkactionhighlight";
|
|
184
530
|
function clearhighlight() {
|
|
@@ -195,7 +541,7 @@
|
|
|
195
541
|
window.setTimeout(clearhighlight, 5e3);
|
|
196
542
|
return { ok: true, summary: "Target outlined for five seconds." };
|
|
197
543
|
}
|
|
198
|
-
function poll(predicate, description, timeout) {
|
|
544
|
+
function poll(root, predicate, description, timeout) {
|
|
199
545
|
return new Promise((resolve) => {
|
|
200
546
|
const started = Date.now();
|
|
201
547
|
const check = () => {
|
|
@@ -212,15 +558,15 @@
|
|
|
212
558
|
check();
|
|
213
559
|
});
|
|
214
560
|
}
|
|
215
|
-
function formstate() {
|
|
216
|
-
return [...
|
|
561
|
+
function formstate(root) {
|
|
562
|
+
return [...root.querySelectorAll("input, textarea, select")].map((element) => ({
|
|
217
563
|
type: element.getAttribute("type") ?? element.tagName.toLowerCase(),
|
|
218
564
|
name: element.getAttribute("name") ?? "",
|
|
219
565
|
value: element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement ? element.value : "",
|
|
220
566
|
...element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio") ? { checked: element.checked } : {}
|
|
221
567
|
}));
|
|
222
568
|
}
|
|
223
|
-
function runpageread(step, target) {
|
|
569
|
+
function runpageread(step, target, root = document) {
|
|
224
570
|
const options = (() => {
|
|
225
571
|
try {
|
|
226
572
|
return parseoptions(step);
|
|
@@ -268,7 +614,7 @@
|
|
|
268
614
|
return { ok: true, summary: "Target markup read.", details: { html: target.outerHTML } };
|
|
269
615
|
}
|
|
270
616
|
case "countelements": {
|
|
271
|
-
const count =
|
|
617
|
+
const count = root.querySelectorAll(step.target ?? "").length;
|
|
272
618
|
return { ok: true, summary: `Selector matches ${count} element${count === 1 ? "" : "s"}.`, details: { count } };
|
|
273
619
|
}
|
|
274
620
|
case "readtable": {
|
|
@@ -279,19 +625,19 @@
|
|
|
279
625
|
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
626
|
}
|
|
281
627
|
case "readlinks": {
|
|
282
|
-
const links = [...
|
|
628
|
+
const links = [...root.querySelectorAll("a[href]")].map((element) => ({ text: element.textContent?.trim() ?? "", href: element.getAttribute("href") ?? "" }));
|
|
283
629
|
return { ok: true, summary: `Read ${links.length} link${links.length === 1 ? "" : "s"}.`, details: { links } };
|
|
284
630
|
}
|
|
285
631
|
case "readimages": {
|
|
286
|
-
const images = [...
|
|
632
|
+
const images = [...root.querySelectorAll("img")].map((element) => ({ src: element.getAttribute("src") ?? "", alt: element.getAttribute("alt") ?? "" }));
|
|
287
633
|
return { ok: true, summary: `Read ${images.length} image${images.length === 1 ? "" : "s"}.`, details: { images } };
|
|
288
634
|
}
|
|
289
635
|
case "readmeta": {
|
|
290
|
-
const meta = [...
|
|
636
|
+
const meta = [...root.querySelectorAll("meta")].map((element) => ({ name: element.getAttribute("name") ?? "", property: element.getAttribute("property") ?? "", content: element.getAttribute("content") ?? "" }));
|
|
291
637
|
return { ok: true, summary: `Read ${meta.length} meta entr${meta.length === 1 ? "y" : "ies"}.`, details: { meta } };
|
|
292
638
|
}
|
|
293
639
|
case "readforms": {
|
|
294
|
-
const forms = formstate();
|
|
640
|
+
const forms = formstate(root);
|
|
295
641
|
return { ok: true, summary: `Read ${forms.length} form control${forms.length === 1 ? "" : "s"}.`, details: { forms } };
|
|
296
642
|
}
|
|
297
643
|
case "readstorage": {
|
|
@@ -311,42 +657,486 @@
|
|
|
311
657
|
}
|
|
312
658
|
}
|
|
313
659
|
case "waitfor": {
|
|
314
|
-
const
|
|
660
|
+
const selector = step.target ?? "";
|
|
315
661
|
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
316
|
-
return poll(() => Boolean(
|
|
662
|
+
return poll(root, () => Boolean(root.querySelector(selector)), `Selector ${selector}`, timeout);
|
|
317
663
|
}
|
|
318
664
|
case "waittext": {
|
|
319
665
|
const text = step.value ?? "";
|
|
320
666
|
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
321
|
-
return poll(() => (
|
|
667
|
+
return poll(root, () => (root.body?.innerText ?? "").includes(text), `Text ${text}`, timeout);
|
|
668
|
+
}
|
|
669
|
+
case "mapclicks": {
|
|
670
|
+
const candidates = collectclickable(root);
|
|
671
|
+
const map = buildclickablemap(candidates, 0, 0);
|
|
672
|
+
return { ok: true, summary: `Mapped ${map.entries.length} clickable element${map.entries.length === 1 ? "" : "s"}.`, details: { entries: map.entries } };
|
|
673
|
+
}
|
|
674
|
+
case "verifyvisible": {
|
|
675
|
+
if (!target) return { ok: false, summary: "Verify target is no longer available." };
|
|
676
|
+
const rect = target.getBoundingClientRect();
|
|
677
|
+
const rendered = rect.width > 0 && rect.height > 0;
|
|
678
|
+
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 } } };
|
|
679
|
+
}
|
|
680
|
+
case "verifyenabled": {
|
|
681
|
+
if (!target) return { ok: false, summary: "Verify target is no longer available." };
|
|
682
|
+
const control = target;
|
|
683
|
+
const disabled = control.disabled === true || target.hasAttribute("disabled");
|
|
684
|
+
const readonly = control.readOnly === true || target.hasAttribute("readonly");
|
|
685
|
+
const enabled = !disabled && !readonly;
|
|
686
|
+
return { ok: enabled, summary: enabled ? "Target is enabled and writable." : disabled ? "Target is disabled." : "Target is readonly.", details: { enabled, disabled, readonly } };
|
|
687
|
+
}
|
|
688
|
+
case "resolvexpath": {
|
|
689
|
+
const reference = options.targetref;
|
|
690
|
+
const expression = typeof reference?.xpath === "string" ? reference.xpath : "";
|
|
691
|
+
if (!expression) return { ok: false, summary: "The reviewed xpath expression is absent." };
|
|
692
|
+
let matches = [];
|
|
693
|
+
try {
|
|
694
|
+
matches = evaluatexpath(buildxtree(root), expression);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return { ok: false, summary: `The reviewed xpath expression failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
697
|
+
}
|
|
698
|
+
const summaries = matches.map((node) => ({ tag: node.tag, ...node.element ? { selector: elementselector(node.element), label: elementlabel(node.element) } : {} }));
|
|
699
|
+
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
700
|
}
|
|
323
701
|
default:
|
|
324
702
|
return { ok: false, summary: "Unsupported page read." };
|
|
325
703
|
}
|
|
326
704
|
}
|
|
327
705
|
|
|
328
|
-
// extension/
|
|
329
|
-
function
|
|
330
|
-
return
|
|
706
|
+
// extension/pagecontrols.ts
|
|
707
|
+
function typetimeschedule(text, delay) {
|
|
708
|
+
return [...text].map((character, position) => ({ key: character, delay: position === 0 ? 0 : delay }));
|
|
331
709
|
}
|
|
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 || "");
|
|
710
|
+
function appendvalue(current, addition) {
|
|
711
|
+
return current + addition;
|
|
337
712
|
}
|
|
338
|
-
function
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
713
|
+
function valueevents() {
|
|
714
|
+
return ["input", "change"];
|
|
715
|
+
}
|
|
716
|
+
function multichoices(values, options) {
|
|
717
|
+
const present = [];
|
|
718
|
+
const missing = [];
|
|
719
|
+
for (const value of values) {
|
|
720
|
+
const option = options.find((candidate) => candidate.value === value || candidate.label === value);
|
|
721
|
+
if (option) present.push(option.value);
|
|
722
|
+
else missing.push(value);
|
|
723
|
+
}
|
|
724
|
+
return { present, missing };
|
|
725
|
+
}
|
|
726
|
+
function radiochoice(inputs, choice) {
|
|
727
|
+
return inputs.findIndex((candidate) => candidate.value === choice || candidate.label === choice);
|
|
728
|
+
}
|
|
729
|
+
function slidervalue(requested, min, max, step) {
|
|
730
|
+
const lower = Math.min(min, max);
|
|
731
|
+
const upper = Math.max(min, max);
|
|
732
|
+
const clamped = Math.min(upper, Math.max(lower, requested));
|
|
733
|
+
if (!Number.isFinite(step) || step <= 0) return clamped;
|
|
734
|
+
return Math.round((clamped - lower) / step) * step + lower;
|
|
349
735
|
}
|
|
736
|
+
function datevalue(requested) {
|
|
737
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(requested)) return null;
|
|
738
|
+
const parts = requested.split("-").map((part) => Number.parseInt(part, 10));
|
|
739
|
+
const year = parts[0];
|
|
740
|
+
const month = parts[1];
|
|
741
|
+
const day = parts[2];
|
|
742
|
+
if (!year || !month || !day || month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
743
|
+
return requested;
|
|
744
|
+
}
|
|
745
|
+
function colorvalue(requested) {
|
|
746
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(requested)) return null;
|
|
747
|
+
return requested.toLowerCase();
|
|
748
|
+
}
|
|
749
|
+
function expandstate(open) {
|
|
750
|
+
return open ? { open: true, changed: false } : { open: true, changed: true };
|
|
751
|
+
}
|
|
752
|
+
function events2(target) {
|
|
753
|
+
target.dispatchEvent(new Event("input", { bubbles: true }));
|
|
754
|
+
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
755
|
+
}
|
|
756
|
+
function modifiers2(options) {
|
|
757
|
+
return Array.isArray(options.modifiers) ? options.modifiers.filter((item) => typeof item === "string") : [];
|
|
758
|
+
}
|
|
759
|
+
function keyevent2(type, key, mods) {
|
|
760
|
+
const code = key.length === 1 ? `Key${key.toUpperCase()}` : key;
|
|
761
|
+
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") });
|
|
762
|
+
}
|
|
763
|
+
function fieldlike2(target) {
|
|
764
|
+
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement ? target : null;
|
|
765
|
+
}
|
|
766
|
+
function receiver(target) {
|
|
767
|
+
return target instanceof HTMLElement ? target : document.activeElement instanceof HTMLElement ? document.activeElement : document.body;
|
|
768
|
+
}
|
|
769
|
+
function wait(delay) {
|
|
770
|
+
return new Promise((resolve) => window.setTimeout(resolve, delay));
|
|
771
|
+
}
|
|
772
|
+
function focuswhenneeded(target, options) {
|
|
773
|
+
if (!(target instanceof HTMLElement)) return;
|
|
774
|
+
if (options.focus === false) return;
|
|
775
|
+
if (options.focus === true || document.activeElement !== target) target.focus();
|
|
776
|
+
}
|
|
777
|
+
function pollfor(predicate, description, timeout) {
|
|
778
|
+
return new Promise((resolve) => {
|
|
779
|
+
const started = Date.now();
|
|
780
|
+
const check = () => {
|
|
781
|
+
if (predicate()) {
|
|
782
|
+
resolve({ ok: true, summary: `${description} is now present on the page.` });
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (timeout > 0 && Date.now() - started >= timeout) {
|
|
786
|
+
resolve({ ok: false, summary: `${description} did not appear within ${timeout} milliseconds.` });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
window.setTimeout(check, 100);
|
|
790
|
+
};
|
|
791
|
+
check();
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
function runpagecontrol(step, target, root = document) {
|
|
795
|
+
let options = {};
|
|
796
|
+
try {
|
|
797
|
+
options = parseoptions(step);
|
|
798
|
+
} catch {
|
|
799
|
+
options = {};
|
|
800
|
+
}
|
|
801
|
+
switch (step.kind) {
|
|
802
|
+
case "typetime": {
|
|
803
|
+
const field = fieldlike2(target);
|
|
804
|
+
if (!field) return { ok: false, summary: "Target cannot receive timed text." };
|
|
805
|
+
const text = step.value ?? "";
|
|
806
|
+
const delay = typeof options.delay === "number" && options.delay > 0 ? options.delay : 0;
|
|
807
|
+
focuswhenneeded(field, options);
|
|
808
|
+
const schedule = typetimeschedule(text, delay);
|
|
809
|
+
return (async () => {
|
|
810
|
+
for (const entry of schedule) {
|
|
811
|
+
await wait(entry.delay);
|
|
812
|
+
field.dispatchEvent(keyevent2("keydown", entry.key, []));
|
|
813
|
+
field.dispatchEvent(new KeyboardEvent("keypress", { key: entry.key, bubbles: true, cancelable: true }));
|
|
814
|
+
field.value = `${field.value}${entry.key}`;
|
|
815
|
+
field.dispatchEvent(new Event("input", { bubbles: true }));
|
|
816
|
+
}
|
|
817
|
+
field.dispatchEvent(new Event("change", { bubbles: true }));
|
|
818
|
+
return { ok: true, summary: `Typed ${text.length} character${text.length === 1 ? "" : "s"} with a per keystroke delay of ${delay} milliseconds.` };
|
|
819
|
+
})();
|
|
820
|
+
}
|
|
821
|
+
case "appendtext": {
|
|
822
|
+
const field = fieldlike2(target);
|
|
823
|
+
if (!field) return { ok: false, summary: "Target cannot hold a value." };
|
|
824
|
+
focuswhenneeded(field, options);
|
|
825
|
+
field.value = appendvalue(field.value, step.value ?? "");
|
|
826
|
+
events2(field);
|
|
827
|
+
return { ok: true, summary: "Reviewed text appended to the current field value." };
|
|
828
|
+
}
|
|
829
|
+
case "setvalue": {
|
|
830
|
+
const field = fieldlike2(target);
|
|
831
|
+
if (!field) return { ok: false, summary: "Target cannot hold a value." };
|
|
832
|
+
focuswhenneeded(field, options);
|
|
833
|
+
field.value = step.value ?? "";
|
|
834
|
+
events2(field);
|
|
835
|
+
return { ok: true, summary: `Field value set through the dom property with ${valueevents().join(" and ")} events.` };
|
|
836
|
+
}
|
|
837
|
+
case "typeedit": {
|
|
838
|
+
if (!(target instanceof HTMLElement) || !target.isContentEditable) return { ok: false, summary: "Target is not a content editable region." };
|
|
839
|
+
focuswhenneeded(target, options);
|
|
840
|
+
const text = step.value ?? "";
|
|
841
|
+
return (async () => {
|
|
842
|
+
for (const character of [...text]) {
|
|
843
|
+
target.dispatchEvent(new InputEvent("beforeinput", { bubbles: true, cancelable: true, data: character, inputType: "insertText" }));
|
|
844
|
+
target.append(document.createTextNode(character));
|
|
845
|
+
target.dispatchEvent(new InputEvent("input", { bubbles: true, data: character, inputType: "insertText" }));
|
|
846
|
+
}
|
|
847
|
+
return { ok: true, summary: `Typed ${text.length} character${text.length === 1 ? "" : "s"} into the content editable region.` };
|
|
848
|
+
})();
|
|
849
|
+
}
|
|
850
|
+
case "keyhold": {
|
|
851
|
+
const key = step.value ?? "";
|
|
852
|
+
const mods = modifiers2(options);
|
|
853
|
+
receiver(target).dispatchEvent(keyevent2("keydown", key, mods));
|
|
854
|
+
const holdid = typeof options.holdid === "string" && options.holdid ? options.holdid : "";
|
|
855
|
+
return { ok: true, summary: `Key ${key} pressed and held${holdid ? ` under hold id ${holdid}` : ""}.`, details: { ...holdid ? { holdid } : {}, modifiers: mods } };
|
|
856
|
+
}
|
|
857
|
+
case "keyrelease": {
|
|
858
|
+
const key = step.value ?? "";
|
|
859
|
+
const mods = modifiers2(options);
|
|
860
|
+
receiver(target).dispatchEvent(keyevent2("keyup", key, mods));
|
|
861
|
+
return { ok: true, summary: `Key ${key} released.`, details: { modifiers: mods } };
|
|
862
|
+
}
|
|
863
|
+
case "submitsearch": {
|
|
864
|
+
const field = fieldlike2(target);
|
|
865
|
+
if (!field) return { ok: false, summary: "Target is not a search field." };
|
|
866
|
+
const results = typeof options.results === "string" ? options.results : "";
|
|
867
|
+
const timeout = typeof options.timeout === "number" ? options.timeout : 0;
|
|
868
|
+
focuswhenneeded(field, options);
|
|
869
|
+
field.dispatchEvent(keyevent2("keydown", "Enter", []));
|
|
870
|
+
field.dispatchEvent(new KeyboardEvent("keypress", { key: "Enter", bubbles: true, cancelable: true }));
|
|
871
|
+
field.dispatchEvent(keyevent2("keyup", "Enter", []));
|
|
872
|
+
return pollfor(() => Boolean(document.querySelector(results)), `Results region ${results}`, timeout);
|
|
873
|
+
}
|
|
874
|
+
case "selectmulti": {
|
|
875
|
+
if (!(target instanceof HTMLSelectElement) || !target.multiple) return { ok: false, summary: "Target is not a multi select control." };
|
|
876
|
+
const choices = [...target.options].map((option) => ({ value: option.value, label: clean(option.textContent || option.value) }));
|
|
877
|
+
const requested = Array.isArray(options.values) ? options.values.filter((item) => typeof item === "string") : [];
|
|
878
|
+
const outcome = multichoices(requested, choices);
|
|
879
|
+
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.` };
|
|
880
|
+
for (const option of target.options) option.selected = outcome.present.includes(option.value);
|
|
881
|
+
events2(target);
|
|
882
|
+
return { ok: true, summary: `Selected ${outcome.present.length} reviewed option${outcome.present.length === 1 ? "" : "s"} in the multi select control.`, details: { selected: outcome.present } };
|
|
883
|
+
}
|
|
884
|
+
case "chooseradio": {
|
|
885
|
+
const radios = target instanceof HTMLInputElement && target.type === "radio" ? [...root.querySelectorAll(`input[type=radio][name="${CSS.escape(target.name)}"]`)] : target ? [...target.querySelectorAll("input[type=radio]")] : [];
|
|
886
|
+
if (radios.length === 0) return { ok: false, summary: "No radio group owns the reviewed target." };
|
|
887
|
+
const inputs = radios.map((radio) => ({ value: radio.value, label: radio.labels && radio.labels.length > 0 ? clean(radio.labels[0]?.textContent || "") || radio.value : radio.value }));
|
|
888
|
+
const index = radiochoice(inputs, step.value ?? "");
|
|
889
|
+
const chosen = radios[index];
|
|
890
|
+
if (!chosen) return { ok: false, summary: "The reviewed radio option is not part of the group." };
|
|
891
|
+
chosen.checked = true;
|
|
892
|
+
events2(chosen);
|
|
893
|
+
return { ok: true, summary: `Picked reviewed radio option ${step.value}.`, details: { value: chosen.value } };
|
|
894
|
+
}
|
|
895
|
+
case "setslider": {
|
|
896
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "range") return { ok: false, summary: "Target is not a range slider." };
|
|
897
|
+
const requested = Number(step.value);
|
|
898
|
+
if (!Number.isFinite(requested)) return { ok: false, summary: "The reviewed slider value is not a number." };
|
|
899
|
+
focuswhenneeded(target, options);
|
|
900
|
+
const value = slidervalue(requested, Number(target.min), Number(target.max), Number(target.step));
|
|
901
|
+
target.value = String(value);
|
|
902
|
+
events2(target);
|
|
903
|
+
return { ok: true, summary: `Slider dragged to the reviewed value ${value}.`, details: { value } };
|
|
904
|
+
}
|
|
905
|
+
case "setdate": {
|
|
906
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "date") return { ok: false, summary: "Target is not a date input." };
|
|
907
|
+
const value = datevalue(step.value ?? "");
|
|
908
|
+
if (value === null) return { ok: false, summary: "The reviewed date is invalid." };
|
|
909
|
+
focuswhenneeded(target, options);
|
|
910
|
+
target.value = value;
|
|
911
|
+
events2(target);
|
|
912
|
+
return { ok: true, summary: `Date input set to ${value}.`, details: { value } };
|
|
913
|
+
}
|
|
914
|
+
case "setcolor": {
|
|
915
|
+
if (!(target instanceof HTMLInputElement) || target.type !== "color") return { ok: false, summary: "Target is not a color input." };
|
|
916
|
+
const value = colorvalue(step.value ?? "");
|
|
917
|
+
if (value === null) return { ok: false, summary: "The reviewed color is invalid." };
|
|
918
|
+
focuswhenneeded(target, options);
|
|
919
|
+
target.value = value;
|
|
920
|
+
events2(target);
|
|
921
|
+
return { ok: true, summary: `Color input set to ${value}.`, details: { value } };
|
|
922
|
+
}
|
|
923
|
+
case "expanddetails": {
|
|
924
|
+
const details = target instanceof HTMLElement ? target.closest("details") : null;
|
|
925
|
+
if (!details) return { ok: false, summary: "Target is not inside a details section." };
|
|
926
|
+
const outcome = expandstate(details.open);
|
|
927
|
+
details.open = outcome.open;
|
|
928
|
+
return { ok: true, summary: outcome.changed ? "Collapsed details section opened." : "Details section was already open.", details: { changed: outcome.changed } };
|
|
929
|
+
}
|
|
930
|
+
default:
|
|
931
|
+
return { ok: false, summary: "Unsupported control action." };
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// extension/pagepointer.ts
|
|
936
|
+
var basecadence = 16;
|
|
937
|
+
function distance(a, b) {
|
|
938
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
939
|
+
}
|
|
940
|
+
function ease(easing, progress) {
|
|
941
|
+
if (easing === "easeinout") return progress * progress * (3 - 2 * progress);
|
|
942
|
+
return progress;
|
|
943
|
+
}
|
|
944
|
+
function ispointref(value) {
|
|
945
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
946
|
+
const point = value;
|
|
947
|
+
return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
|
|
948
|
+
}
|
|
949
|
+
function pathhops(path, profile, random = Math.random, cadence = basecadence) {
|
|
950
|
+
const easing = profile?.easing === "easeinout" ? "easeinout" : "linear";
|
|
951
|
+
const peak = typeof profile?.peak === "number" && profile.peak > 0 ? profile.peak : void 0;
|
|
952
|
+
const jitter = typeof profile?.jitter === "number" && profile.jitter > 0 ? profile.jitter : 0;
|
|
953
|
+
const points = [path.start, ...path.waypoints ?? [], path.end];
|
|
954
|
+
const lengths = [];
|
|
955
|
+
let total = 0;
|
|
956
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
957
|
+
const length = distance(points[index - 1], points[index]);
|
|
958
|
+
lengths.push(length);
|
|
959
|
+
total += length;
|
|
960
|
+
}
|
|
961
|
+
const reviewedduration = typeof path.duration === "number" && Number.isFinite(path.duration) && path.duration > 0 ? path.duration : void 0;
|
|
962
|
+
const duration = reviewedduration ?? (peak !== void 0 && total > 0 ? total / peak * 1e3 : 300);
|
|
963
|
+
const hops = [];
|
|
964
|
+
let previous = points[0];
|
|
965
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
966
|
+
const from = points[index - 1];
|
|
967
|
+
const to = points[index];
|
|
968
|
+
const length = lengths[index - 1] ?? 0;
|
|
969
|
+
if (total <= 0 || length <= 0) {
|
|
970
|
+
hops.push({ x: to.x, y: to.y, delay: 0 });
|
|
971
|
+
previous = to;
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
const segmentduration = duration * length / total;
|
|
975
|
+
const count = Math.max(1, Math.ceil(segmentduration / Math.max(1, cadence)));
|
|
976
|
+
for (let hop = 1; hop <= count; hop += 1) {
|
|
977
|
+
const progress = hop / count;
|
|
978
|
+
const eased = ease(easing, progress);
|
|
979
|
+
const position = { x: from.x + (to.x - from.x) * eased, y: from.y + (to.y - from.y) * eased };
|
|
980
|
+
const step = distance(previous, position);
|
|
981
|
+
const base = segmentduration / count;
|
|
982
|
+
const capped = peak !== void 0 ? Math.max(base, step / peak * 1e3) : base;
|
|
983
|
+
hops.push({ x: position.x, y: position.y, delay: Math.max(0, capped + (jitter > 0 ? random() * jitter : 0)) });
|
|
984
|
+
previous = position;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return hops;
|
|
988
|
+
}
|
|
989
|
+
function clickplan(x, y, modifiers3) {
|
|
990
|
+
const shift = modifiers3.includes("shift");
|
|
991
|
+
const pointer = (type) => ({ type, eventkind: "pointer", x, y, shift });
|
|
992
|
+
const mouse = (type) => ({ type, eventkind: "mouse", x, y, shift });
|
|
993
|
+
return [pointer("pointerover"), pointer("pointermove"), pointer("pointerdown"), mouse("mousedown"), pointer("pointerup"), mouse("mouseup"), mouse("click")];
|
|
994
|
+
}
|
|
995
|
+
function dispatchplanned(element, event) {
|
|
996
|
+
const init = { bubbles: true, cancelable: true, composed: true, clientX: event.x, clientY: event.y, shiftKey: event.shift };
|
|
997
|
+
if (event.eventkind === "pointer") element.dispatchEvent(new PointerEvent(event.type, init));
|
|
998
|
+
else element.dispatchEvent(new MouseEvent(event.type, init));
|
|
999
|
+
}
|
|
1000
|
+
function dispatchclick(element, modifiers3 = []) {
|
|
1001
|
+
const rect = element.getBoundingClientRect();
|
|
1002
|
+
const x = rect.left + rect.width / 2;
|
|
1003
|
+
const y = rect.top + rect.height / 2;
|
|
1004
|
+
for (const event of clickplan(x, y, modifiers3)) dispatchplanned(element, event);
|
|
1005
|
+
}
|
|
1006
|
+
function ensurevisible(element) {
|
|
1007
|
+
try {
|
|
1008
|
+
element.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function settle(delay) {
|
|
1013
|
+
return new Promise((resolve) => window.setTimeout(resolve, delay));
|
|
1014
|
+
}
|
|
1015
|
+
function dispatchmove(x, y) {
|
|
1016
|
+
const element = document.elementFromPoint(x, y);
|
|
1017
|
+
const receiver2 = element ?? document.documentElement;
|
|
1018
|
+
receiver2.dispatchEvent(new PointerEvent("pointermove", { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y }));
|
|
1019
|
+
}
|
|
1020
|
+
async function travel(path, profile) {
|
|
1021
|
+
const hops = pathhops(path, profile);
|
|
1022
|
+
const startelement = document.elementFromPoint(path.start.x, path.start.y) ?? document.documentElement;
|
|
1023
|
+
startelement.dispatchEvent(new PointerEvent("pointerover", { bubbles: true, cancelable: true, composed: true, clientX: path.start.x, clientY: path.start.y }));
|
|
1024
|
+
for (const hop of hops) {
|
|
1025
|
+
await settle(hop.delay);
|
|
1026
|
+
dispatchmove(hop.x, hop.y);
|
|
1027
|
+
}
|
|
1028
|
+
const endelement = document.elementFromPoint(path.end.x, path.end.y) ?? document.documentElement;
|
|
1029
|
+
endelement.dispatchEvent(new PointerEvent("pointerout", { bubbles: true, cancelable: true, composed: true, clientX: path.end.x, clientY: path.end.y }));
|
|
1030
|
+
return { ok: true, summary: `Pointer traveled ${hops.length} hop${hops.length === 1 ? "" : "s"} to the reviewed end point.` };
|
|
1031
|
+
}
|
|
1032
|
+
function runpointerstep(step, resolution) {
|
|
1033
|
+
let options = {};
|
|
1034
|
+
try {
|
|
1035
|
+
options = parseoptions(step);
|
|
1036
|
+
} catch {
|
|
1037
|
+
options = {};
|
|
1038
|
+
}
|
|
1039
|
+
if (step.kind === "movepointer") {
|
|
1040
|
+
const path = options.pointpath;
|
|
1041
|
+
if (!path || !ispointref(path.start) || !ispointref(path.end)) return { ok: false, summary: "The reviewed pointer path is absent." };
|
|
1042
|
+
const waypoints = Array.isArray(path.waypoints) && path.waypoints.every((item) => ispointref(item)) ? path.waypoints : void 0;
|
|
1043
|
+
const fullpath = { start: path.start, end: path.end, ...waypoints ? { waypoints } : {}, ...typeof path.duration === "number" && Number.isFinite(path.duration) ? { duration: path.duration } : {} };
|
|
1044
|
+
return travel(fullpath, options.speedprofile);
|
|
1045
|
+
}
|
|
1046
|
+
if (step.kind === "clickpoint") {
|
|
1047
|
+
const reference = options.targetref;
|
|
1048
|
+
const x = Number(reference?.x);
|
|
1049
|
+
const y = Number(reference?.y);
|
|
1050
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return { ok: false, summary: "The reviewed click coordinates are absent." };
|
|
1051
|
+
const element = document.elementFromPoint(x, y);
|
|
1052
|
+
if (!(element instanceof HTMLElement)) return { ok: false, summary: "No element is rendered at the reviewed coordinates." };
|
|
1053
|
+
ensurevisible(element);
|
|
1054
|
+
for (const event of clickplan(x, y, [])) dispatchplanned(element, event);
|
|
1055
|
+
return { ok: true, summary: `Clicked the element at the reviewed coordinates ${x},${y}.` };
|
|
1056
|
+
}
|
|
1057
|
+
if (step.kind === "shiftclick") {
|
|
1058
|
+
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 } };
|
|
1059
|
+
if (resolution.status !== "resolved") return { ok: false, summary: "Action target is no longer available." };
|
|
1060
|
+
ensurevisible(resolution.element);
|
|
1061
|
+
dispatchclick(resolution.element, ["shift"]);
|
|
1062
|
+
return { ok: true, summary: `Shift click delivered to ${resolution.target.label || resolution.target.tag}.`, details: { mode: resolution.target.mode, resolvedtarget: resolution.target } };
|
|
1063
|
+
}
|
|
1064
|
+
return { ok: false, summary: "Unsupported pointer action." };
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// extension/pageinteract.ts
|
|
1068
|
+
function optionsof(step) {
|
|
1069
|
+
try {
|
|
1070
|
+
return parseoptions(step);
|
|
1071
|
+
} catch {
|
|
1072
|
+
return {};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function innerstep(step) {
|
|
1076
|
+
const options = optionsof(step);
|
|
1077
|
+
const kind = options.kind;
|
|
1078
|
+
if (typeof kind !== "string" || !kind.trim()) return null;
|
|
1079
|
+
const inneroptions = options.options;
|
|
1080
|
+
return {
|
|
1081
|
+
id: `${step.id}inner`,
|
|
1082
|
+
kind,
|
|
1083
|
+
summary: step.summary,
|
|
1084
|
+
risk: step.risk,
|
|
1085
|
+
...typeof options.target === "string" ? { target: options.target } : {},
|
|
1086
|
+
...typeof options.value === "string" ? { value: options.value } : {},
|
|
1087
|
+
...inneroptions && typeof inneroptions === "object" && !Array.isArray(inneroptions) ? { options: JSON.stringify(inneroptions) } : {}
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function clickresolved(stepkind, resolution) {
|
|
1091
|
+
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 } };
|
|
1092
|
+
if (resolution.status !== "resolved") return { ok: false, summary: "The reviewed target is no longer available." };
|
|
1093
|
+
ensurevisible(resolution.element);
|
|
1094
|
+
dispatchclick(resolution.element);
|
|
1095
|
+
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 } };
|
|
1096
|
+
}
|
|
1097
|
+
function runinteractstep(step, expectedorigin, dispatch) {
|
|
1098
|
+
if (step.kind === "clicktext" || step.kind === "clickaria" || step.kind === "clickname") {
|
|
1099
|
+
return clickresolved(step.kind, resolvestep(step, document));
|
|
1100
|
+
}
|
|
1101
|
+
if (step.kind === "pierceshadow") {
|
|
1102
|
+
const options = optionsof(step);
|
|
1103
|
+
const shadow = Array.isArray(options.shadow) ? options.shadow.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
|
|
1104
|
+
const element = shadow.length > 0 ? queryshadowchain(document, shadow) : queryscoped(document, step.target ?? "");
|
|
1105
|
+
if (!(element instanceof HTMLElement)) return { ok: false, summary: "The reviewed shadow target is not available." };
|
|
1106
|
+
ensurevisible(element);
|
|
1107
|
+
dispatchclick(element);
|
|
1108
|
+
const summary = targetsummary("selector", element);
|
|
1109
|
+
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 } };
|
|
1110
|
+
}
|
|
1111
|
+
if (step.kind === "enterframe") {
|
|
1112
|
+
const options = optionsof(step);
|
|
1113
|
+
const path = Array.isArray(options.framepath) ? options.framepath.filter((item) => typeof item === "number" && Number.isInteger(item) && item >= 0) : [];
|
|
1114
|
+
const walk = walkframepath(describeframes(document), path);
|
|
1115
|
+
if (!walk.ok) return { ok: false, summary: walk.reason };
|
|
1116
|
+
const framedocument = walk.document.live;
|
|
1117
|
+
if (!framedocument) return { ok: false, summary: "The reviewed frame document is not available." };
|
|
1118
|
+
const inner = innerstep(step);
|
|
1119
|
+
if (!inner) return { ok: false, summary: "The reviewed inner step is absent." };
|
|
1120
|
+
return dispatch(inner, expectedorigin, framedocument);
|
|
1121
|
+
}
|
|
1122
|
+
return { ok: false, summary: "Unsupported interaction action." };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// extension/pagedialogs.ts
|
|
1126
|
+
function harvestdialoglog(root) {
|
|
1127
|
+
const raw = root.documentElement.dataset.devthinkdialoglog;
|
|
1128
|
+
if (!raw) return [];
|
|
1129
|
+
delete root.documentElement.dataset.devthinkdialoglog;
|
|
1130
|
+
try {
|
|
1131
|
+
const parsed = JSON.parse(raw);
|
|
1132
|
+
if (!Array.isArray(parsed)) return [];
|
|
1133
|
+
return parsed.filter((item) => Boolean(item) && typeof item === "object" && typeof item.dialog === "string");
|
|
1134
|
+
} catch {
|
|
1135
|
+
return [];
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// extension/pagebridge.ts
|
|
350
1140
|
function stepoptions(step) {
|
|
351
1141
|
if (!step.options) return {};
|
|
352
1142
|
try {
|
|
@@ -360,11 +1150,13 @@
|
|
|
360
1150
|
function clearpreview() {
|
|
361
1151
|
document.getElementById(previewid)?.remove();
|
|
362
1152
|
}
|
|
363
|
-
function previewtarget(
|
|
1153
|
+
function previewtarget(step, expectedorigin) {
|
|
364
1154
|
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before preview." };
|
|
365
1155
|
clearpreview();
|
|
366
|
-
const
|
|
367
|
-
if (
|
|
1156
|
+
const resolution = resolvestep(step, document);
|
|
1157
|
+
if (resolution.status === "ambiguous") return { ok: false, summary: `The reviewed ${resolution.mode} reference matched ${resolution.candidates.length} elements: ${resolution.candidates.join("; ")}.`, candidates: resolution.candidates };
|
|
1158
|
+
if (resolution.status !== "resolved") return { ok: false, summary: "Reviewed target is no longer available." };
|
|
1159
|
+
const target = resolution.element;
|
|
368
1160
|
const rect = target.getBoundingClientRect();
|
|
369
1161
|
if (rect.width <= 0 || rect.height <= 0) return { ok: false, summary: "Reviewed target is not currently visible." };
|
|
370
1162
|
const overlay = document.createElement("div");
|
|
@@ -373,13 +1165,13 @@
|
|
|
373
1165
|
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
1166
|
document.documentElement.append(overlay);
|
|
375
1167
|
window.setTimeout(clearpreview, 5e3);
|
|
376
|
-
return { ok: true, summary: `Previewing ${
|
|
1168
|
+
return { ok: true, summary: `Previewing ${elementlabel(target) || target.tagName.toLowerCase()} for five seconds.`, resolvedtarget: resolution.target };
|
|
377
1169
|
}
|
|
378
1170
|
function capturesnapshot() {
|
|
379
1171
|
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:
|
|
1172
|
+
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
1173
|
const forms = [...document.querySelectorAll("input, textarea, select")].map((element) => ({
|
|
382
|
-
label:
|
|
1174
|
+
label: elementlabel(element),
|
|
383
1175
|
type: element.getAttribute("type") || element.tagName.toLowerCase(),
|
|
384
1176
|
name: element.getAttribute("name") || "",
|
|
385
1177
|
...element instanceof HTMLSelectElement ? { options: [...element.options].map((option) => clean(option.textContent || option.value)) } : {}
|
|
@@ -387,29 +1179,32 @@
|
|
|
387
1179
|
const text = clean(document.body?.innerText || "");
|
|
388
1180
|
return { schemaversion: 2, url: location.href, title: clean(document.title), textpreview: text, textlength: document.body?.innerText.length ?? 0, forms, interactive, capturedat: Date.now() };
|
|
389
1181
|
}
|
|
390
|
-
function
|
|
1182
|
+
function readdialogs() {
|
|
1183
|
+
return harvestdialoglog(document);
|
|
1184
|
+
}
|
|
1185
|
+
function extractcontent(targetselector, root) {
|
|
391
1186
|
if (!targetselector) {
|
|
392
|
-
const links = [...
|
|
1187
|
+
const links = [...root.querySelectorAll("a[href]")].map((element) => {
|
|
393
1188
|
const href = element instanceof HTMLAnchorElement ? element.getAttribute("href") ?? "" : "";
|
|
394
1189
|
return { text: element.textContent?.trim() ?? "", href };
|
|
395
1190
|
});
|
|
396
1191
|
return { ok: true, summary: `Extracted ${links.length} link entries.`, details: { links } };
|
|
397
1192
|
}
|
|
398
|
-
const target =
|
|
1193
|
+
const target = root.querySelector(targetselector);
|
|
399
1194
|
if (!target) return { ok: false, summary: "Extraction target is no longer available." };
|
|
400
1195
|
const text = target.textContent ?? "";
|
|
401
1196
|
return { ok: true, summary: `Extracted ${text.length} characters of content.`, details: { text } };
|
|
402
1197
|
}
|
|
403
1198
|
function scrolltarget(target) {
|
|
404
1199
|
target.scrollIntoView({ block: "center", inline: "nearest", behavior: "auto" });
|
|
405
|
-
return { ok: true, summary: `Scrolled ${
|
|
1200
|
+
return { ok: true, summary: `Scrolled ${elementlabel(target) || target.tagName.toLowerCase()} into view.` };
|
|
406
1201
|
}
|
|
407
1202
|
function hovertarget(target) {
|
|
408
1203
|
for (const type of ["pointerover", "mouseover", "pointerenter"]) {
|
|
409
1204
|
target.dispatchEvent(new PointerEvent(type, { bubbles: type !== "pointerenter", cancelable: true, composed: true }));
|
|
410
1205
|
}
|
|
411
1206
|
target.dispatchEvent(new MouseEvent("mouseenter", { bubbles: false, cancelable: true }));
|
|
412
|
-
return { ok: true, summary: `Hover events delivered to ${
|
|
1207
|
+
return { ok: true, summary: `Hover events delivered to ${elementlabel(target) || target.tagName.toLowerCase()}.` };
|
|
413
1208
|
}
|
|
414
1209
|
function selectoption(target, value) {
|
|
415
1210
|
if (!(target instanceof HTMLSelectElement)) return { ok: false, summary: "Target is not a select element." };
|
|
@@ -420,9 +1215,12 @@
|
|
|
420
1215
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
421
1216
|
return { ok: true, summary: `Selected ${clean(option.textContent || option.value)}.` };
|
|
422
1217
|
}
|
|
423
|
-
var readkinds = /* @__PURE__ */ new Set(["readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "waitfor", "waittext", "highlight"]);
|
|
1218
|
+
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
1219
|
var mutatingkinds = /* @__PURE__ */ new Set(["presskey", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "setattribute", "removeattribute", "writestorage", "evaluate", "fullscreen"]);
|
|
425
|
-
|
|
1220
|
+
var controlkinds = /* @__PURE__ */ new Set(["typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails"]);
|
|
1221
|
+
var interactkinds = /* @__PURE__ */ new Set(["clicktext", "clickaria", "clickname", "pierceshadow", "enterframe"]);
|
|
1222
|
+
var pointerkinds = /* @__PURE__ */ new Set(["movepointer", "clickpoint", "shiftclick"]);
|
|
1223
|
+
async function performstep(step, expectedorigin, rootdocument = document) {
|
|
426
1224
|
if (location.origin !== expectedorigin) return { ok: false, summary: "Page origin changed before action." };
|
|
427
1225
|
if (step.kind === "observe") return { ok: true, summary: "Observation completed." };
|
|
428
1226
|
if (step.kind === "wait") {
|
|
@@ -430,7 +1228,7 @@
|
|
|
430
1228
|
const duration = Number.isFinite(requested) && requested > 0 ? requested : 0;
|
|
431
1229
|
return new Promise((resolve) => window.setTimeout(() => resolve({ ok: true, summary: `Reviewed wait of ${duration} milliseconds completed.` }), duration));
|
|
432
1230
|
}
|
|
433
|
-
if (step.kind === "extract") return extractcontent(step.target);
|
|
1231
|
+
if (step.kind === "extract") return extractcontent(step.target, rootdocument);
|
|
434
1232
|
if (step.kind === "navigate") {
|
|
435
1233
|
if (!step.value || new URL(step.value).origin !== expectedorigin) return { ok: false, summary: "Navigation target is outside the approved origin." };
|
|
436
1234
|
location.assign(step.value);
|
|
@@ -461,39 +1259,49 @@
|
|
|
461
1259
|
window.scrollTo(0, 0);
|
|
462
1260
|
return { ok: true, summary: "Window scrolled to the page top." };
|
|
463
1261
|
}
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (step.kind
|
|
475
|
-
|
|
476
|
-
return { ok:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
1262
|
+
const resolution = resolvestep(step, rootdocument);
|
|
1263
|
+
if (resolution.status === "ambiguous") {
|
|
1264
|
+
return { ok: false, summary: `The reviewed ${resolution.mode} reference matched ${resolution.candidates.length} elements: ${resolution.candidates.join("; ")}.`, details: { mode: resolution.mode, candidates: resolution.candidates } };
|
|
1265
|
+
}
|
|
1266
|
+
const element = resolution.status === "resolved" ? resolution.element : null;
|
|
1267
|
+
let result;
|
|
1268
|
+
if (readkinds.has(step.kind)) result = runpageread(step, element, rootdocument);
|
|
1269
|
+
else if (controlkinds.has(step.kind)) result = runpagecontrol(step, element, rootdocument);
|
|
1270
|
+
else if (interactkinds.has(step.kind)) return await runinteractstep(step, expectedorigin, performstep);
|
|
1271
|
+
else if (pointerkinds.has(step.kind)) result = runpointerstep(step, resolution);
|
|
1272
|
+
else if (mutatingkinds.has(step.kind)) result = runpageaction(step, element);
|
|
1273
|
+
else {
|
|
1274
|
+
if (!element) return { ok: false, summary: "Action target is no longer available." };
|
|
1275
|
+
if (step.kind === "scrollby") {
|
|
1276
|
+
const options = stepoptions(step);
|
|
1277
|
+
element.scrollBy({ left: typeof options.x === "number" ? options.x : 0, top: typeof options.y === "number" ? options.y : 600, behavior: "auto" });
|
|
1278
|
+
result = { ok: true, summary: "Container scrolled by the reviewed amounts." };
|
|
1279
|
+
} else if (step.kind === "focus") {
|
|
1280
|
+
element.focus();
|
|
1281
|
+
result = { ok: true, summary: "Target focused." };
|
|
1282
|
+
} else if (step.kind === "inspect") result = { ok: true, summary: `Target: ${elementlabel(element) || element.tagName.toLowerCase()}.` };
|
|
1283
|
+
else if (step.kind === "click") {
|
|
1284
|
+
element.click();
|
|
1285
|
+
result = { ok: true, summary: "Reviewed click completed." };
|
|
1286
|
+
} else if (step.kind === "scroll") result = scrolltarget(element);
|
|
1287
|
+
else if (step.kind === "hover") result = hovertarget(element);
|
|
1288
|
+
else if (step.kind === "select") result = selectoption(element, step.value ?? "");
|
|
1289
|
+
else if (step.kind === "type") {
|
|
1290
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) return { ok: false, summary: "Target cannot receive text." };
|
|
1291
|
+
if (typeof step.value !== "string") return { ok: false, summary: "Approved text is absent." };
|
|
1292
|
+
element.focus();
|
|
1293
|
+
element.value = step.value;
|
|
1294
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1295
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1296
|
+
result = { ok: true, summary: "Approved text entered." };
|
|
1297
|
+
} else return { ok: false, summary: "Unsupported action." };
|
|
1298
|
+
}
|
|
1299
|
+
const output = await result;
|
|
1300
|
+
if (resolution.status === "resolved") {
|
|
1301
|
+
return { ...output, details: { ...output.details ?? {}, mode: resolution.target.mode, resolvedtarget: resolution.target } };
|
|
1302
|
+
}
|
|
1303
|
+
return output;
|
|
1304
|
+
}
|
|
1305
|
+
Object.assign(globalThis, { devthinkbridge: { capturesnapshot, previewtarget, performstep, readdialogs } });
|
|
498
1306
|
})();
|
|
499
1307
|
//# sourceMappingURL=pagebridge.js.map
|