halo-agent 2.4.4 → 2.4.6
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/package.json +1 -1
- package/scanAccessibility.js +47 -1
- package/smartFill.js +63 -25
package/package.json
CHANGED
package/scanAccessibility.js
CHANGED
|
@@ -292,6 +292,25 @@ async function enrichFromDom(page, axFields) {
|
|
|
292
292
|
const ownerFrame = window.frameElement;
|
|
293
293
|
const iframeSrc = ownerFrame ? ownerFrame.src || ownerFrame.name || '' : '';
|
|
294
294
|
|
|
295
|
+
// Disjoint label-for recovery. When the input has no matching
|
|
296
|
+
// <label for=id> partner (e.g. Ashby puts data-field-path on a
|
|
297
|
+
// wrapper <div> while the input itself has no id), Chrome's AX
|
|
298
|
+
// algorithm bottoms out at placeholder. We pre-compute a label
|
|
299
|
+
// from a known ATS field-group wrapper here so pickLabel() can
|
|
300
|
+
// prefer it when rawName turns out to be the placeholder.
|
|
301
|
+
let recoveredLabel = '';
|
|
302
|
+
try {
|
|
303
|
+
const wrapperSel = '[data-field-path], [class*="_fieldEntry_"], .ashby-application-form-field-entry, .application-question, [data-question-id]';
|
|
304
|
+
const wrapper = el.closest && el.closest(wrapperSel);
|
|
305
|
+
if (wrapper) {
|
|
306
|
+
const lbl = wrapper.querySelector('label');
|
|
307
|
+
if (lbl) {
|
|
308
|
+
const t = safeText(lbl.textContent || '').replace(/\s*\*\s*$/, '').trim();
|
|
309
|
+
if (t && t.length < 200) recoveredLabel = t;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {}
|
|
313
|
+
|
|
295
314
|
return {
|
|
296
315
|
mmid,
|
|
297
316
|
tag,
|
|
@@ -301,6 +320,7 @@ async function enrichFromDom(page, axFields) {
|
|
|
301
320
|
id: el.id || '',
|
|
302
321
|
ariaLabel: el.getAttribute('aria-label') || '',
|
|
303
322
|
placeholder: el.placeholder || '',
|
|
323
|
+
recoveredLabel,
|
|
304
324
|
selectorHint,
|
|
305
325
|
currentValue,
|
|
306
326
|
options,
|
|
@@ -349,7 +369,20 @@ function pickLabel(f) {
|
|
|
349
369
|
// Priority: AX name → aria-label → placeholder → text content → field id.
|
|
350
370
|
// (AX name already merges <label for>, aria-labelledby, fieldset/legend
|
|
351
371
|
// walks — we get them all for free.)
|
|
352
|
-
|
|
372
|
+
//
|
|
373
|
+
// Disjoint label-for recovery: when Chrome's AX algorithm bottoms out at
|
|
374
|
+
// the placeholder (because <label for=X> points at a wrapper id and the
|
|
375
|
+
// input itself has no id — Ashby's Location pattern, also any
|
|
376
|
+
// CSS-modules form library), rawName === placeholder. In that case
|
|
377
|
+
// prefer the label we walked from the field-group wrapper.
|
|
378
|
+
const rawName = (f.rawName || '').trim();
|
|
379
|
+
const placeholder = (f.placeholder || '').trim();
|
|
380
|
+
const recovered = (f.recoveredLabel || '').trim();
|
|
381
|
+
if (rawName && placeholder && rawName.toLowerCase() === placeholder.toLowerCase() && recovered) {
|
|
382
|
+
console.log(`[scanAx] recovered label "${recovered}" for placeholder-only field (was: "${rawName}")`);
|
|
383
|
+
return recovered;
|
|
384
|
+
}
|
|
385
|
+
return (rawName || f.ariaLabel || recovered || placeholder || f.textContent || f.id || f.name || '').trim();
|
|
353
386
|
}
|
|
354
387
|
|
|
355
388
|
function pickDescription(f) {
|
|
@@ -394,6 +427,19 @@ async function scanFrameDom(frame, frameUrl) {
|
|
|
394
427
|
const t = alb.split(/\s+/).map((id) => document.getElementById(id)).filter(Boolean).map((e) => safeText(e.textContent)).join(' ').trim();
|
|
395
428
|
if (t) return t;
|
|
396
429
|
}
|
|
430
|
+
// Disjoint label-for recovery via ATS field-group wrapper.
|
|
431
|
+
// Mirrors the recoveredLabel walk in the main-frame DOM evaluator.
|
|
432
|
+
try {
|
|
433
|
+
const wrapperSel = '[data-field-path], [class*="_fieldEntry_"], .ashby-application-form-field-entry, .application-question, [data-question-id]';
|
|
434
|
+
const wrapper = el.closest && el.closest(wrapperSel);
|
|
435
|
+
if (wrapper) {
|
|
436
|
+
const lbl = wrapper.querySelector('label');
|
|
437
|
+
if (lbl && !lbl.contains(el)) {
|
|
438
|
+
const t = safeText(lbl.textContent).replace(/\*$/, '').trim();
|
|
439
|
+
if (t && t.length < 200) return t;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch {}
|
|
397
443
|
let p = el.parentElement, hops = 0;
|
|
398
444
|
while (p && hops < 8) {
|
|
399
445
|
const lbl = p.querySelector('label, legend, h2, h3, h4');
|
package/smartFill.js
CHANGED
|
@@ -123,7 +123,7 @@ async function executePlanItem(page, item, fieldByMmid, ctx) {
|
|
|
123
123
|
// it as click_option.
|
|
124
124
|
if (field.role === 'combobox' || field.role === 'listbox') {
|
|
125
125
|
const result = await openAndPickOption(root, locator, item.value, {
|
|
126
|
-
config: ctx.config, jobId: ctx.jobId, label: field.label,
|
|
126
|
+
config: ctx.config, jobId: ctx.jobId, label: field.label, field,
|
|
127
127
|
});
|
|
128
128
|
if (result.ok) return result;
|
|
129
129
|
// Fall through to text-fill if no option matched at all
|
|
@@ -151,7 +151,7 @@ async function executePlanItem(page, item, fieldByMmid, ctx) {
|
|
|
151
151
|
|
|
152
152
|
case 'click_option': {
|
|
153
153
|
return await openAndPickOption(root, locator, item.value, {
|
|
154
|
-
config: ctx.config, jobId: ctx.jobId, label: field.label,
|
|
154
|
+
config: ctx.config, jobId: ctx.jobId, label: field.label, field,
|
|
155
155
|
});
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -636,6 +636,67 @@ async function openAndPickOption(root, triggerLocator, value, llmCtx) {
|
|
|
636
636
|
// need the Page. For in-frame fields, option menus render inside the
|
|
637
637
|
// frame so root.locator finds them.
|
|
638
638
|
const page = (root && typeof root.page === 'function') ? root.page() : root;
|
|
639
|
+
|
|
640
|
+
// ── Async-listbox typeahead (shape-agnostic): try FIRST. ─────────────
|
|
641
|
+
// Any combobox where options are NOT pre-rendered — Ashby Location,
|
|
642
|
+
// Greenhouse Google Places, Workday city pickers, school/company
|
|
643
|
+
// autocompletes — must be typed into BEFORE options can appear. The
|
|
644
|
+
// generic "click and snapshot" path below opens the menu, waits 350ms,
|
|
645
|
+
// and bails on empty — but Ashby's geo fetch alone takes ~700ms.
|
|
646
|
+
//
|
|
647
|
+
// We probe DOM signals on the located element AND on its descendants AND
|
|
648
|
+
// on its closest field-group, because the planner's mmid may resolve to
|
|
649
|
+
// the hidden input, the wrapper div, or the toggle button — and Ashby's
|
|
650
|
+
// wrapper has no role/aria attrs of its own.
|
|
651
|
+
//
|
|
652
|
+
// Triggers:
|
|
653
|
+
// - role=combobox + (aria-haspopup=listbox | aria-autocomplete=list)
|
|
654
|
+
// - aria-autocomplete=list (canonical ARIA for async listbox)
|
|
655
|
+
// - scanAccessibility-tagged field.isTypeahead === true
|
|
656
|
+
// - Ashby's emotion-hashed _input_ class inside a _fieldEntry_ group
|
|
657
|
+
try {
|
|
658
|
+
const fieldIsTypeahead = !!(llmCtx && llmCtx.field && llmCtx.field.isTypeahead);
|
|
659
|
+
const domIsTypeahead = await triggerLocator.evaluate((el) => {
|
|
660
|
+
const probe = (n) => {
|
|
661
|
+
if (!n || n.nodeType !== 1) return false;
|
|
662
|
+
const role = n.getAttribute('role');
|
|
663
|
+
const ac = n.getAttribute('aria-autocomplete');
|
|
664
|
+
const hp = n.getAttribute('aria-haspopup');
|
|
665
|
+
if (ac === 'list' || ac === 'both') return true;
|
|
666
|
+
if (role === 'combobox' && (hp === 'listbox' || hp === 'true')) return true;
|
|
667
|
+
return false;
|
|
668
|
+
};
|
|
669
|
+
// 1. element itself
|
|
670
|
+
if (probe(el)) return true;
|
|
671
|
+
// 2. descendant input/combobox (when locator is wrapper or toggle button)
|
|
672
|
+
const innerSelectors = 'input[role="combobox"], [aria-autocomplete="list"], [aria-autocomplete="both"], [role="combobox"][aria-haspopup="listbox"]';
|
|
673
|
+
const inner = el.querySelector?.(innerSelectors);
|
|
674
|
+
if (probe(inner)) return true;
|
|
675
|
+
// 3. closest field-group, then re-descend (handles ATS-specific wrappers)
|
|
676
|
+
const groupSel = '[class*="_fieldEntry_"], .ashby-application-form-field-entry, .application-question, [data-question-id], [class*="formField"], .field';
|
|
677
|
+
const group = el.closest?.(groupSel);
|
|
678
|
+
if (group) {
|
|
679
|
+
const groupInner = group.querySelector(innerSelectors);
|
|
680
|
+
if (probe(groupInner)) return true;
|
|
681
|
+
}
|
|
682
|
+
// 4. Ashby's emotion-hashed input class as a supplementary signal
|
|
683
|
+
const cls = String(el.className || '');
|
|
684
|
+
if (/_input_/.test(cls) && el.closest?.('[class*="_fieldEntry_"], .ashby-application-form-field-entry')) return true;
|
|
685
|
+
return false;
|
|
686
|
+
}).catch(() => false);
|
|
687
|
+
|
|
688
|
+
if (fieldIsTypeahead || domIsTypeahead) {
|
|
689
|
+
const why = [];
|
|
690
|
+
if (fieldIsTypeahead) why.push('field.isTypeahead');
|
|
691
|
+
if (domIsTypeahead) why.push('dom signals');
|
|
692
|
+
console.log(`[smartFill] openAndPickOption: detected typeahead (${why.join(', ')}) → typeAndPickSuggestion first`);
|
|
693
|
+
const r = await typeAndPickSuggestion(root, triggerLocator, value);
|
|
694
|
+
if (r && r.ok) return r;
|
|
695
|
+
// Fall through to specialized + generic paths on no-match.
|
|
696
|
+
console.log(`[smartFill] typeahead path did not land (${r && r.reason}); falling through to generic dispatch.`);
|
|
697
|
+
}
|
|
698
|
+
} catch {}
|
|
699
|
+
|
|
639
700
|
try {
|
|
640
701
|
const rs = await tryReactSelect(root, triggerLocator, value);
|
|
641
702
|
if (rs !== null) {
|
|
@@ -652,29 +713,6 @@ async function openAndPickOption(root, triggerLocator, value, llmCtx) {
|
|
|
652
713
|
if (itlResult !== null && itlResult.ok) return itlResult;
|
|
653
714
|
} catch {}
|
|
654
715
|
|
|
655
|
-
try {
|
|
656
|
-
// Ashby async typeahead. Ashby's Location field is role="combobox" with
|
|
657
|
-
// aria-haspopup="listbox" and an input class like `_input_v5ami_28`. It
|
|
658
|
-
// renders NO options until you type, then fetches matches over the
|
|
659
|
-
// network (~700ms) and shows them as [role="option"]. The generic
|
|
660
|
-
// open-and-look path opened it, saw an empty menu, and bailed — that's
|
|
661
|
-
// the Gen Digital "dropdown opened but no visible options found" failure.
|
|
662
|
-
// Detect Ashby's widget and route to type-then-wait-for-async-options.
|
|
663
|
-
const isAshbyCombo = await triggerLocator.evaluate((el) => {
|
|
664
|
-
const role = el.getAttribute('role');
|
|
665
|
-
const hasPopup = el.getAttribute('aria-haspopup');
|
|
666
|
-
const cls = String(el.className || '');
|
|
667
|
-
// Ashby's emotion-hashed input class, or the generic combobox+listbox shape.
|
|
668
|
-
const ashbyClass = /_input_/.test(cls) && !!el.closest('[class*="_fieldEntry_"], .ashby-application-form-field-entry');
|
|
669
|
-
return (role === 'combobox' && (hasPopup === 'listbox' || hasPopup === 'true')) || ashbyClass;
|
|
670
|
-
}).catch(() => false);
|
|
671
|
-
if (isAshbyCombo) {
|
|
672
|
-
const r = await typeAndPickSuggestion(root, triggerLocator, value);
|
|
673
|
-
if (r.ok) return r;
|
|
674
|
-
// If typeahead didn't land, fall through to the generic path below.
|
|
675
|
-
}
|
|
676
|
-
} catch {}
|
|
677
|
-
|
|
678
716
|
try {
|
|
679
717
|
// Workday combobox / multi-select. Workday wraps every form widget in a
|
|
680
718
|
// [data-automation-id^="formField-"] container and its dropdowns mount a
|