@enricai/barnacle 1.12.47 → 1.12.49

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.
@@ -89,6 +89,7 @@ exports.dispatchJqueryChangeEvent = dispatchJqueryChangeEvent;
89
89
  exports.verifyDomEffect = verifyDomEffect;
90
90
  exports.narrowInvalidFormControl = narrowInvalidFormControl;
91
91
  exports.formatStepPrefix = formatStepPrefix;
92
+ exports.tryDeterministicFieldLabelActuation = tryDeterministicFieldLabelActuation;
92
93
  exports.probeStepBeforeAttempts = probeStepBeforeAttempts;
93
94
  exports.extractLinkFromMessage = extractLinkFromMessage;
94
95
  exports.extractCodeFromMessage = extractCodeFromMessage;
@@ -636,6 +637,39 @@ const PROMPT_EMPTY_VALUE_RX_FLAGS = PROMPT_EMPTY_VALUE_RX.flags;
636
637
  * the remaining `textContent`.
637
638
  */
638
639
  const BUTTON_VALUE_EXPR = "((el) => { const c = el.cloneNode(true); for (const n of c.querySelectorAll(\"[role='option'],[role='listbox'],[aria-hidden='true'],abbr,svg\")) n.remove(); return c.textContent || \"\"; })";
640
+ /**
641
+ * Browser-side expression resolving a `role='option'` element's accessible
642
+ * label when its own `textContent`/`data-value` is empty — a widget-kit may
643
+ * carry the visible text on `aria-label`, `aria-labelledby` (standards-first,
644
+ * mirroring {@link labelFor}/`widgetLabel`'s id-resolution), `title`, or a
645
+ * descendant node instead of the option's own text node. Shared by option
646
+ * enumeration and by {@link PROMPT_CURRENT_TEXT_EXPR}'s
647
+ * `aria-activedescendant` readback so a widget can be driven and confirmed
648
+ * committed via the same resolved label regardless of which node carries it.
649
+ */
650
+ const OPTION_ACCESSIBLE_LABEL_EXPR = `((el) => {
651
+ const norm = (s) => (s || "").replace(/\\s+/g, " ").trim();
652
+ const auto = el.getAttribute && el.getAttribute("data-automation-label");
653
+ if (auto && auto.trim()) return norm(auto);
654
+ const alb = el.getAttribute && el.getAttribute("aria-labelledby");
655
+ if (alb) {
656
+ const parts = [];
657
+ for (const id of alb.split(/\\s+/)) { const ref = document.getElementById(id); if (ref && ref.textContent) parts.push(ref.textContent); }
658
+ if (parts.length) return norm(parts.join(" "));
659
+ }
660
+ const al = el.getAttribute && el.getAttribute("aria-label");
661
+ if (al && al.trim()) return norm(al);
662
+ const own = el.textContent || "";
663
+ if (own.trim()) return norm(own);
664
+ const title = el.getAttribute && el.getAttribute("title");
665
+ if (title && title.trim()) return norm(title);
666
+ const desc = el.querySelector && el.querySelector("[aria-label],[title]");
667
+ if (desc) {
668
+ const dal = desc.getAttribute("aria-label") || desc.getAttribute("title");
669
+ if (dal && dal.trim()) return norm(dal);
670
+ }
671
+ return "";
672
+ })`;
639
673
  /**
640
674
  * Browser-side expression reading a prompt-selector widget's OWN committed-value
641
675
  * text — the single source of truth for "is this widget filled?", shared by the
@@ -651,8 +685,9 @@ const BUTTON_VALUE_EXPR = "((el) => { const c = el.cloneNode(true); for (const n
651
685
  const PROMPT_CURRENT_TEXT_EXPR = `((w, valueSel, emptyRx) => {
652
686
  const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
653
687
  const buttonValue = ${BUTTON_VALUE_EXPR};
688
+ const optionLabel = ${OPTION_ACCESSIBLE_LABEL_EXPR};
654
689
  const adid = w.getAttribute && w.getAttribute("aria-activedescendant");
655
- if (adid) { const opt = document.getElementById(adid); if (opt && opt.textContent.trim()) return norm(opt.textContent); }
690
+ if (adid) { const opt = document.getElementById(adid); if (opt && optionLabel(opt)) return norm(optionLabel(opt)); }
656
691
  if (w.tagName === "INPUT" && (w.value || "").trim()) return norm(w.value);
657
692
  if (w.tagName === "BUTTON" && buttonValue(w).trim()) return norm(buttonValue(w));
658
693
  const lbl = w.matches(valueSel) ? w : w.querySelector(valueSel);
@@ -5975,6 +6010,7 @@ async function tryPromptSelectorPrimitive(params) {
5975
6010
  return null;
5976
6011
  }
5977
6012
  const enumerateOptionsExpr = `((widgetMarkAttr, wIdx, markAttr, optionSel, searchSel) => {
6013
+ const optionLabel = ${OPTION_ACCESSIBLE_LABEL_EXPR};
5978
6014
  const w = document.querySelector("[" + widgetMarkAttr + '="' + wIdx + '"]');
5979
6015
  if (!w) return { optionsPresent: false };
5980
6016
  // Clear stale option + filter marks from a PRIOR prompt-selector call on
@@ -6009,8 +6045,8 @@ async function tryPromptSelectorPrimitive(params) {
6009
6045
  const options = [];
6010
6046
  for (let i = 0; i < opts.length; i++) {
6011
6047
  opts[i].setAttribute(markAttr, String(i));
6012
- const label = opts[i].getAttribute("data-automation-label") || opts[i].getAttribute("aria-label") || opts[i].textContent || "";
6013
- options.push({ oIdx: i, text: label.replace(/\\s+/g, " ").trim() });
6048
+ const label = optionLabel(opts[i]);
6049
+ options.push({ oIdx: i, text: label });
6014
6050
  }
6015
6051
  // scopeIsDocument distinguishes a genuinely resolved (portal or inline)
6016
6052
  // popup from the document-wide last-resort fallback in
@@ -6209,6 +6245,7 @@ async function commitPromptOption(params) {
6209
6245
  const isPairedHiddenSelect = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
6210
6246
  const norm = (s) => (s || "").replace(/\\s+/g, " ").trim().toLowerCase();
6211
6247
  const buttonValue = ${BUTTON_VALUE_EXPR};
6248
+ const optionLabel = ${OPTION_ACCESSIBLE_LABEL_EXPR};
6212
6249
  const emptyRx = new RegExp(emptyRxSrc, emptyRxFlags);
6213
6250
  const w = document.querySelector("[" + markAttr + '="' + wIdx + '"]');
6214
6251
  if (!w) return { ok: false, id: "" };
@@ -6216,7 +6253,7 @@ async function commitPromptOption(params) {
6216
6253
  // own value text (popup-pollution-safe, see BUTTON_VALUE_EXPR), or a
6217
6254
  // selection-label node (empty-state phrase treated as no value).
6218
6255
  const adid = w.getAttribute && w.getAttribute("aria-activedescendant");
6219
- const adText = adid && document.getElementById(adid) ? norm(document.getElementById(adid).textContent) : "";
6256
+ const adText = adid && document.getElementById(adid) ? norm(optionLabel(document.getElementById(adid))) : "";
6220
6257
  const own = w.tagName === "INPUT" ? norm(w.value || "") : w.tagName === "BUTTON" ? norm(buttonValue(w)) : "";
6221
6258
  const lbl = w.matches(valueSel) ? w : w.querySelector(valueSel);
6222
6259
  const lblRaw = lbl ? (lbl.textContent || "") : "";
@@ -7142,7 +7179,11 @@ async function resolveDeepLocatorCandidatesWithWidening(page, frameSelector, ins
7142
7179
  * `not-fill-or-select`, never the first attempt for a step that names a
7143
7180
  * field. `triedSelectors` (already-attempted selectors from prior
7144
7181
  * cascade attempts) is excluded so a step that already tried and failed on
7145
- * this candidate doesn't just re-pick it.
7182
+ * this candidate doesn't just re-pick it. A "select" match that resolves to
7183
+ * an opener-paired-hidden `<select>` (see the loop below) is skipped rather
7184
+ * than actuated — that element belongs to `tryPromptSelectorPrimitive`,
7185
+ * which already had first refusal on it, and empty-valued `<option>`s can
7186
+ * never corroborate a write landed on the right one.
7146
7187
  */
7147
7188
  async function tryDeterministicFieldLabelActuation(params) {
7148
7189
  const { page, frameTarget, step, triedSelectors, timeoutOptions } = params;
@@ -7150,8 +7191,32 @@ async function tryDeterministicFieldLabelActuation(params) {
7150
7191
  if (!fieldTarget)
7151
7192
  return { kind: "not-fill-or-select" };
7152
7193
  const { candidates, innerSelector } = await resolveDeepLocatorCandidatesWithWidening(page, frameTarget.frameSelector, step, timeoutOptions);
7153
- const unexcluded = candidates.filter((c) => !triedSelectors.includes(c.selector));
7154
- const matched = findDeepLocatorCandidateByFieldLabel(unexcluded, fieldTarget.fieldLabel);
7194
+ let unexcluded = candidates.filter((c) => !triedSelectors.includes(c.selector));
7195
+ let matched = findDeepLocatorCandidateByFieldLabel(unexcluded, fieldTarget.fieldLabel);
7196
+ // A "select" field-label match can resolve to a design-system combobox
7197
+ // opener's paired-but-hidden shadow <select> (Base Web etc.) — the same
7198
+ // element `trySelectPrimitive`/`applySelectValue` already refuse to write
7199
+ // via `OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR`, because `tryPromptSelectorPrimitive`
7200
+ // is the primitive that owns that opener widget and already had first
7201
+ // refusal on it. Writing here instead bypasses that ownership: the opener's
7202
+ // widget never observes the write, and when every <option> carries an empty
7203
+ // `value` attribute the write can't even corroborate against the select's
7204
+ // OWN state (`sel.value` stays "" regardless of which option got picked).
7205
+ // Skip a matched candidate that resolves to one, falling back to the next
7206
+ // named candidate (or no-match) instead of ever attempting that write.
7207
+ while (matched && fieldTarget.kind === "select") {
7208
+ const matchedIsPairedHiddenSelect = await frameTarget
7209
+ .evaluate(`((innerSelector, index) => {
7210
+ const isOpenerPairedHidden = ${exports.OPENER_PAIRED_HIDDEN_SELECT_EL_EXPR};
7211
+ const el = Array.from(document.querySelectorAll(innerSelector))[index];
7212
+ return !!el && isOpenerPairedHidden(el);
7213
+ })(${JSON.stringify(innerSelector)}, ${JSON.stringify(matched.index)})`)
7214
+ .catch(() => false);
7215
+ if (!matchedIsPairedHiddenSelect)
7216
+ break;
7217
+ unexcluded = unexcluded.filter((c) => c.selector !== matched?.selector);
7218
+ matched = findDeepLocatorCandidateByFieldLabel(unexcluded, fieldTarget.fieldLabel);
7219
+ }
7155
7220
  if (!matched)
7156
7221
  return { kind: "no-match", fieldTarget };
7157
7222
  const actuated = fieldTarget.kind === "fill"