@mobileaidev/ai-app-bridge-web 0.4.6 → 0.4.7
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 -0
- package/package.json +1 -1
- package/src/index.js +77 -4
package/README.md
CHANGED
|
@@ -23,6 +23,14 @@ unchecked. Use the current SDK with the current Host; missing `checked` is an
|
|
|
23
23
|
invalid snapshot. A semantic click must still be followed by a fresh observation
|
|
24
24
|
and an independent business check where persistence matters.
|
|
25
25
|
|
|
26
|
+
Visible controls without text, an ARIA label or a placeholder may also expose
|
|
27
|
+
`context.ancestors`: up to two distinct text samples from their actual DOM
|
|
28
|
+
ancestors, within six parent levels and 160 characters per sample. Collection
|
|
29
|
+
is bounded to 64 nodes per ancestor, excludes form values and hidden branches,
|
|
30
|
+
and stops before BODY/HTML. Truncation flags describe the limits. This fresh
|
|
31
|
+
observation helps an agent distinguish otherwise unlabelled record menus; it
|
|
32
|
+
does not infer a business row or change selectors and action identity.
|
|
33
|
+
|
|
26
34
|
## Capture windows and network bodies
|
|
27
35
|
|
|
28
36
|
Network body capture is explicitly enabled with `captureRequestBodies: true`
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -413,6 +413,56 @@
|
|
|
413
413
|
role: attr(hit, 'role'), className: typeof hit.className === 'string' ? trimText(hit.className, 300) : '' } : null };
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
function ancestorContext(element, identity, cache) {
|
|
417
|
+
if ([identity.text, identity.ariaLabel, identity.placeholder].some(value => value.trim())) return null;
|
|
418
|
+
const ancestors = [], seen = new Set();
|
|
419
|
+
let parent = element.parentElement, depth = 1;
|
|
420
|
+
while (parent && !['BODY', 'HTML'].includes(parent.tagName) && depth <= 6 && ancestors.length < 2) {
|
|
421
|
+
const sample = ancestorText(parent, cache);
|
|
422
|
+
if (sample.text && !seen.has(sample.text)) {
|
|
423
|
+
seen.add(sample.text);
|
|
424
|
+
const id = text(parent.id), role = attr(parent, 'role');
|
|
425
|
+
ancestors.push({ depth, tag: text(parent.tagName).toLowerCase(), id: id.slice(0, 80), role: role.slice(0, 40),
|
|
426
|
+
text: sample.text, textTruncated: sample.truncated, identityTruncated: id.length > 80 || role.length > 40 });
|
|
427
|
+
}
|
|
428
|
+
parent = parent.parentElement; depth++;
|
|
429
|
+
}
|
|
430
|
+
// DOM context describes actual ancestors, not a guessed business row or a visibility guarantee.
|
|
431
|
+
return ancestors.length ? { source: 'ancestor-dom-text', ancestors,
|
|
432
|
+
truncated: ancestors.some(item => item.textTruncated || item.identityTruncated)
|
|
433
|
+
|| Boolean(parent && !['BODY', 'HTML'].includes(parent.tagName)) } : null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function ancestorText(root, cache) {
|
|
437
|
+
if (cache.has(root)) return cache.get(root);
|
|
438
|
+
let node = root, visited = 0, value = '', truncated = false;
|
|
439
|
+
while (node && visited < 64) {
|
|
440
|
+
visited++;
|
|
441
|
+
let descend = true;
|
|
442
|
+
if (node.nodeType === 1) {
|
|
443
|
+
const style = node.ownerDocument?.defaultView?.getComputedStyle(node);
|
|
444
|
+
descend = !['SCRIPT', 'STYLE', 'TEMPLATE', 'NOSCRIPT', 'INPUT', 'TEXTAREA', 'SELECT'].includes(node.tagName)
|
|
445
|
+
&& !isSensitiveInput(node) && !node.hidden && attr(node, 'aria-hidden') !== 'true'
|
|
446
|
+
&& style?.display !== 'none' && !['hidden', 'collapse'].includes(style?.visibility) && style?.opacity !== '0';
|
|
447
|
+
} else if (node.nodeType === 3) {
|
|
448
|
+
const raw = text(node.nodeValue);
|
|
449
|
+
// Normalize a bounded raw sample so indentation does not consume the readable-text budget.
|
|
450
|
+
const part = raw.slice(0, 1024).replace(/\s+/g, ' ').trim();
|
|
451
|
+
const next = value + (value && part ? ' ' : '') + part;
|
|
452
|
+
value = next.slice(0, 160);
|
|
453
|
+
if (raw.length > 1024 || next.length > 160) { truncated = true; break; }
|
|
454
|
+
}
|
|
455
|
+
if (descend && node.firstChild) node = node.firstChild;
|
|
456
|
+
else {
|
|
457
|
+
while (node !== root && !node.nextSibling) node = node.parentNode;
|
|
458
|
+
node = node === root ? null : node.nextSibling;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const result = { text: value, truncated: truncated || Boolean(node) };
|
|
462
|
+
cache.set(root, result);
|
|
463
|
+
return result;
|
|
464
|
+
}
|
|
465
|
+
|
|
416
466
|
function snapshotDom(options = {}) {
|
|
417
467
|
const doc = options.document || globalValue('document');
|
|
418
468
|
if (!doc || !doc.querySelectorAll) {
|
|
@@ -427,18 +477,22 @@
|
|
|
427
477
|
const maxControls = Number(options.maxControls || 200);
|
|
428
478
|
const offset = options.offset ?? 0;
|
|
429
479
|
const controls = [];
|
|
480
|
+
const contextCache = new WeakMap();
|
|
430
481
|
const matched = options.query ? matchingElements(options.query, doc) : doc.querySelectorAll(selector);
|
|
431
482
|
const elements = Array.prototype.slice.call(matched, offset, offset + maxControls);
|
|
432
483
|
elements.forEach((element, index) => {
|
|
433
484
|
const sensitive = isSensitiveInput(element);
|
|
434
485
|
const elementValue = text(element.value);
|
|
486
|
+
const identity = elementIdentity(element, doc), visible = elementVisible(element);
|
|
487
|
+
const context = visible ? ancestorContext(element, identity, contextCache) : null;
|
|
435
488
|
controls.push({
|
|
436
|
-
...
|
|
489
|
+
...identity,
|
|
490
|
+
...(context ? { context } : {}),
|
|
437
491
|
index: offset + index,
|
|
438
492
|
sensitive,
|
|
439
493
|
...(sensitive ? { valueLength: elementValue.length } : {}),
|
|
440
494
|
disabled: elementDisabled(element),
|
|
441
|
-
visible
|
|
495
|
+
visible,
|
|
442
496
|
editable: elementEditable(element),
|
|
443
497
|
checked: elementChecked(element),
|
|
444
498
|
ariaChecked: element.getAttribute('aria-checked'),
|
|
@@ -493,12 +547,31 @@
|
|
|
493
547
|
element.dispatchEvent(new win.InputEvent('input', { bubbles: true, inputType: 'insertReplacementText', data: value }));
|
|
494
548
|
ensureTarget(selected, task, { focus: true, edited: true }); task.permit(); element.dispatchEvent(new win.Event('change', { bubbles: true }));
|
|
495
549
|
}
|
|
496
|
-
|
|
550
|
+
// The final input/change dispatch may collapse, disable or remove its own
|
|
551
|
+
// editor. Report what happened to that original object; never reselect it
|
|
552
|
+
// or treat its resulting layout as a precondition for the completed input.
|
|
553
|
+
const targetState = { samePage: samePage(task.pageRef(), selected.ref.pageRef),
|
|
554
|
+
sameDocument: element.ownerDocument === globalValue('document'), connected: element.isConnected,
|
|
555
|
+
visible: elementVisible(element), disabled: elementDisabled(element), editable: elementEditable(element),
|
|
556
|
+
focused: doc.activeElement === element, bounds: bounds(element) };
|
|
557
|
+
if (!targetState.samePage || !targetState.sameDocument || !targetState.connected) {
|
|
558
|
+
return { ok: false, error: targetState.samePage ? 'web_element_changed' : 'reobserve_required',
|
|
559
|
+
resolved: selected.ref, inputDispatched: true, valueState: 'unconfirmed', targetState,
|
|
560
|
+
message: 'Input was dispatched, but its original element is no longer in the current page. Reobserve before continuing.' };
|
|
561
|
+
}
|
|
562
|
+
recordTargetChanges(selected, elementIdentity(element), true);
|
|
563
|
+
const warnings = targetWarnings(selected).warnings ?? [];
|
|
564
|
+
if (!targetState.visible || targetState.disabled || !targetState.editable || !targetState.focused)
|
|
565
|
+
warnings.push({ code: 'target_state_changed_after_input',
|
|
566
|
+
message: 'Input was dispatched; the original element state changed afterwards. Inspect targetState before another action.' });
|
|
497
567
|
const sensitive = isSensitiveInput(element);
|
|
498
568
|
return {
|
|
499
569
|
ok: true,
|
|
500
570
|
resolved: selected.ref,
|
|
501
|
-
|
|
571
|
+
inputDispatched: true,
|
|
572
|
+
valueState: 'observed',
|
|
573
|
+
targetState,
|
|
574
|
+
...(warnings.length ? { warnings } : {}),
|
|
502
575
|
sensitive,
|
|
503
576
|
valueLength: (richText ? element.innerText : element.value).length,
|
|
504
577
|
...(sensitive ? {} : { value: richText ? element.innerText : element.value }),
|