@surea11y/core 1.1.0 → 1.1.2

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.
Files changed (78) hide show
  1. package/CHANGELOG.md +25 -2
  2. package/docs/ENGINE_OPTIONS.md +2 -0
  3. package/docs/LIMITATIONS.md +1 -1
  4. package/package.json +1 -1
  5. package/src/checks/automatic/aria-allowed-attr.js +5 -4
  6. package/src/checks/automatic/aria-allowed-role.js +5 -4
  7. package/src/checks/automatic/aria-braille-equivalent.js +3 -4
  8. package/src/checks/automatic/aria-conditional-attr.js +3 -4
  9. package/src/checks/automatic/aria-deprecated-role.js +2 -3
  10. package/src/checks/automatic/aria-prohibited-attr.js +5 -4
  11. package/src/checks/automatic/aria-prohibited-children.js +2 -3
  12. package/src/checks/automatic/aria-required-attr.js +2 -3
  13. package/src/checks/automatic/aria-required-children.js +2 -3
  14. package/src/checks/automatic/aria-required-parent.js +3 -4
  15. package/src/checks/automatic/aria-roles-valid.js +2 -3
  16. package/src/checks/automatic/aria-valid-attr-value.js +5 -4
  17. package/src/checks/automatic/aria-valid-attr.js +5 -4
  18. package/src/checks/automatic/autocomplete-valid.js +2 -3
  19. package/src/checks/automatic/avoid-inline-spacing.js +2 -3
  20. package/src/checks/automatic/binary-control-name-present.js +2 -3
  21. package/src/checks/automatic/button-name-present.js +14 -4
  22. package/src/checks/automatic/bypass-blocks-present.js +35 -7
  23. package/src/checks/automatic/combobox-name-present.js +2 -3
  24. package/src/checks/automatic/definition-list-children-valid.js +2 -3
  25. package/src/checks/automatic/deprecated-elements-not-used.js +5 -4
  26. package/src/checks/automatic/dialog-name-present.js +2 -3
  27. package/src/checks/automatic/dlitem-parent-valid.js +2 -3
  28. package/src/checks/automatic/form-control-single-label.js +2 -3
  29. package/src/checks/automatic/iframe-focusable-content.js +2 -3
  30. package/src/checks/automatic/iframe-name-present.js +2 -3
  31. package/src/checks/automatic/iframe-title-unique.js +5 -5
  32. package/src/checks/automatic/label-in-name.js +7 -8
  33. package/src/checks/automatic/link-in-text-block.js +2 -3
  34. package/src/checks/automatic/link-name-present.js +16 -4
  35. package/src/checks/automatic/list-children-valid.js +2 -3
  36. package/src/checks/automatic/listbox-name-present.js +2 -3
  37. package/src/checks/automatic/listitem-parent-valid.js +2 -3
  38. package/src/checks/automatic/menuitem-name-present.js +2 -3
  39. package/src/checks/automatic/meter-name-present.js +2 -3
  40. package/src/checks/automatic/nested-interactive-controls-absent.js +2 -3
  41. package/src/checks/automatic/option-name-present.js +2 -3
  42. package/src/checks/automatic/progressbar-name-present.js +2 -3
  43. package/src/checks/automatic/searchbox-name-present.js +2 -3
  44. package/src/checks/automatic/server-side-image-map-absent.js +2 -3
  45. package/src/checks/automatic/slider-name-present.js +2 -3
  46. package/src/checks/automatic/spinbutton-name-present.js +2 -3
  47. package/src/checks/automatic/summary-name-present.js +2 -3
  48. package/src/checks/automatic/tab-name-present.js +2 -3
  49. package/src/checks/automatic/table-headers-attr-valid.js +5 -4
  50. package/src/checks/automatic/table-th-has-data-cells.js +5 -4
  51. package/src/checks/automatic/td-has-header.js +2 -3
  52. package/src/checks/automatic/textbox-name-present.js +2 -3
  53. package/src/checks/automatic/tooltip-name-present.js +2 -3
  54. package/src/checks/automatic/treeitem-name-present.js +2 -3
  55. package/src/checks/automatic/valid-lang.js +2 -3
  56. package/src/checks/manual/accesskeys-manual.js +1 -7
  57. package/src/checks/manual/aria-checked-state-mismatch-manual.js +7 -6
  58. package/src/checks/manual/aria-text-manual.js +2 -3
  59. package/src/checks/manual/focus-order-semantics-manual.js +2 -3
  60. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -3
  61. package/src/checks/manual/image-redundant-alt-manual.js +2 -3
  62. package/src/checks/manual/label-title-only-manual.js +2 -3
  63. package/src/checks/manual/link-name-quality-manual.js +2 -3
  64. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -3
  65. package/src/checks/manual/no-autoplay-audio-manual.js +2 -3
  66. package/src/checks/manual/p-as-heading-manual.js +2 -3
  67. package/src/checks/manual/page-has-heading-one-manual.js +31 -2
  68. package/src/checks/manual/presentation-role-conflict-manual.js +3 -4
  69. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -3
  70. package/src/checks/manual/skip-link-manual.js +91 -9
  71. package/src/checks/manual/table-duplicate-name-manual.js +2 -3
  72. package/src/checks/manual/table-fake-caption-manual.js +2 -3
  73. package/src/checks/manual/video-caption-manual.js +2 -3
  74. package/src/core/dom-helpers.js +89 -3
  75. package/src/core/dom-runner.js +19 -0
  76. package/src/core.js +962 -481
  77. package/src/i18n/en.js +4 -2
  78. package/src/i18n/fr.js +4 -2
package/src/core.js CHANGED
@@ -5316,8 +5316,8 @@ const CHECK_DEFS = [
5316
5316
  },
5317
5317
  {
5318
5318
  "ruleId": "skip-link",
5319
- "title": "Skip link must have a resolvable target",
5320
- "description": "Checks that a \"skip to ...\" link's href fragment resolves to a real element in the document.",
5319
+ "title": "Skip link must have a resolvable, usable target",
5320
+ "description": "Checks that a \"skip to ...\" link's href fragment resolves to a real, currently usable element in the document.",
5321
5321
  "i18n": {
5322
5322
  "titleKey": "skipLink_title",
5323
5323
  "descriptionKey": "skipLink_description"
@@ -7556,10 +7556,12 @@ const I18N = {
7556
7556
  "region_description": "Checks that direct children of <body> with visible text content are contained within a landmark region.",
7557
7557
  "region_summary_cantTell": "This content is not contained within a landmark region.",
7558
7558
  "region_hint_cantTell": "Move this content inside a landmark region (main, nav, aside, a labeled section, etc.).",
7559
- "skipLink_title": "Skip link must have a resolvable target",
7560
- "skipLink_description": "Checks that a \"skip to ...\" link's href fragment resolves to a real element in the document.",
7559
+ "skipLink_title": "Skip link must have a resolvable, usable target",
7560
+ "skipLink_description": "Checks that a \"skip to ...\" link's href fragment resolves to a real, currently usable element in the document.",
7561
7561
  "skipLink_summary_cantTell": "This skip link's target does not exist.",
7562
7562
  "skipLink_hint_cantTell": "Point the skip link's href at an id that exists in the document, or add the missing target element.",
7563
+ "skipLink_summary_unusableTarget_cantTell": "This skip link points to a target that exists but is not currently usable.",
7564
+ "skipLink_hint_unusableTarget_cantTell": "Point this skip link to a target that is exposed and usable as a navigation destination.",
7563
7565
  "autocompleteValid_title": "autocomplete attribute must be a valid autofill value",
7564
7566
  "autocompleteValid_description": "Checks that a non-empty autocomplete attribute is \"on\"/\"off\" or a well-formed autofill detail token list.",
7565
7567
  "autocompleteValid_summary_fail": "This autocomplete attribute value is not a valid autofill value.",
@@ -8158,10 +8160,12 @@ const I18N = {
8158
8160
  "region_description": "Vérifie que les enfants directs de <body> ayant un contenu textuel visible sont contenus dans un point de repère.",
8159
8161
  "region_summary_cantTell": "Ce contenu n’est contenu dans aucun point de repère.",
8160
8162
  "region_hint_cantTell": "Déplacez ce contenu à l’intérieur d’un point de repère (main, nav, aside, une section nommée, etc.).",
8161
- "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout",
8162
- "skipLink_description": "Vérifie que le fragment href d’un lien « aller au... » se résout vers un élément réel du document.",
8163
+ "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout et qui est utilisable",
8164
+ "skipLink_description": "Vérifie que le fragment href d’un lien « aller au... » se résout vers un élément réel et actuellement utilisable du document.",
8163
8165
  "skipLink_summary_cantTell": "La cible de ce lien d’évitement n’existe pas.",
8164
8166
  "skipLink_hint_cantTell": "Faites pointer le href du lien d’évitement vers un id qui existe dans le document, ou ajoutez l’élément cible manquant.",
8167
+ "skipLink_summary_unusableTarget_cantTell": "Ce lien d’évitement pointe vers une cible qui existe mais qui n’est pas actuellement utilisable.",
8168
+ "skipLink_hint_unusableTarget_cantTell": "Faites pointer ce lien d’évitement vers une cible exposée et utilisable comme destination de navigation.",
8165
8169
  "autocompleteValid_title": "L’attribut autocomplete doit être une valeur d’auto-remplissage valide",
8166
8170
  "autocompleteValid_description": "Vérifie qu’un attribut autocomplete non vide vaut « on »/« off » ou une liste de jetons d’auto-remplissage bien formée.",
8167
8171
  "autocompleteValid_summary_fail": "Cette valeur d’attribut autocomplete n’est pas une valeur d’auto-remplissage valide.",
@@ -10967,6 +10971,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
10967
10971
  })();
10968
10972
  // Default on: opt OUT with `includeShadowDom: false`, not opt in.
10969
10973
  const includeShadowDom = !(opts && opts.includeShadowDom === false);
10974
+ // Default off: by default, helper queries skip structurally/CSS-hidden
10975
+ // subtrees (display:none, [hidden], closed <details>, etc.). Callers can
10976
+ // opt out with includeHiddenElements:true.
10977
+ const includeHiddenElements = !!(opts && opts.includeHiddenElements === true);
10970
10978
  const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
10971
10979
 
10972
10980
  // Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
@@ -11933,8 +11941,55 @@ const createDomHelpers = (function createDomHelpers(opts) {
11933
11941
  return results;
11934
11942
  }
11935
11943
 
11944
+ const HARD_HIDDEN_REASONS = new Set([
11945
+ 'displayNone',
11946
+ 'hiddenAttr',
11947
+ 'detailsClosed',
11948
+ 'templateContent',
11949
+ 'nonRenderedElement',
11950
+ 'inputHidden',
11951
+ 'visibilityHidden',
11952
+ 'contentVisibilityHidden'
11953
+ ]);
11954
+
11936
11955
  function queryAllSmart(sel) {
11937
- const list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
11956
+ let list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
11957
+
11958
+ // Global hidden-content policy: skip nodes that are fully excluded from
11959
+ // rendered visibility by default (unless includeHiddenElements:true).
11960
+ if (!includeHiddenElements) {
11961
+ list = list.filter((el) => {
11962
+ try {
11963
+ const vis = isAccTreeEligible(el);
11964
+ if (!vis || vis.eligible !== false) return true;
11965
+ const reasons = Array.isArray(vis.reasons) ? vis.reasons : [];
11966
+ for (const r of reasons) {
11967
+ if (HARD_HIDDEN_REASONS.has(r)) return false;
11968
+ }
11969
+
11970
+ // `isAccTreeEligible` can short-circuit on an inert ancestor
11971
+ // before it reaches an outer hard-hidden ancestor (e.g.
11972
+ // display:none wrapper). In that case the node is still
11973
+ // structurally hidden and should be excluded by the default
11974
+ // hidden-content policy.
11975
+ if (reasons.includes('inert')) {
11976
+ const domVis = isDomVisibleEligible(el, null, {
11977
+ visibilityMode: 'styleOnly',
11978
+ disableGeometry: true,
11979
+ ignoreOpacity: true
11980
+ });
11981
+ const domReasons = Array.isArray(domVis && domVis.reasons) ? domVis.reasons : [];
11982
+ for (const r of domReasons) {
11983
+ if (HARD_HIDDEN_REASONS.has(r)) return false;
11984
+ }
11985
+ }
11986
+ return true;
11987
+ } catch {
11988
+ return true;
11989
+ }
11990
+ });
11991
+ }
11992
+
11938
11993
  return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
11939
11994
  }
11940
11995
 
@@ -14504,6 +14559,36 @@ const createDomHelpers = (function createDomHelpers(opts) {
14504
14559
  let node = el;
14505
14560
  let safety = 0;
14506
14561
 
14562
+ // Only apply the "stop climbing once we reach a contextSelector-
14563
+ // matched root" shortcut when there's a single (or no) matched
14564
+ // root -- resolveContextRoots() falls back to `[documentElement]`
14565
+ // when no contextSelector is given, so this is the overwhelmingly
14566
+ // common case and behaves exactly as before.
14567
+ //
14568
+ // With MULTIPLE matched roots (multi-region contextSelector
14569
+ // scans), stopping there without recording anything about which
14570
+ // root produced an ambiguous, non-unique selector string for two
14571
+ // structurally-identical regions -- a real, confirmed bug (found
14572
+ // 2026-07-29 via the cross-engine comparisons project): two
14573
+ // wrapper <div>s, each containing two identical ".widget"
14574
+ // sections scanned via `contextSelector: '.widget'`, produced the
14575
+ // *same* selector string ("section:nth-of-type(1) > div > div >
14576
+ // button") for the equivalent button in each wrapper --
14577
+ // resolving to 2 elements instead of 1 when queried, and pointing
14578
+ // at the wrong one for at least one of the two occurrences. The
14579
+ // existing `el.matches(candidate)` safety check below couldn't
14580
+ // catch this: it only verifies THIS element matches the string,
14581
+ // never that the string is unique document-wide.
14582
+ //
14583
+ // Fix: when multiple roots are in play, don't stop early --
14584
+ // keep climbing (same as the always-correct no-contextSelector
14585
+ // path) until finding a genuinely unique anchor or reaching the
14586
+ // true document root, which is always singular. That restores
14587
+ // the invariant the final safety-check comment below relies on,
14588
+ // rather than needing a separate (more expensive) document-wide
14589
+ // uniqueness re-check.
14590
+ const stopAtMatchedRoot = roots.length <= 1;
14591
+
14507
14592
  while (node && node.nodeType === 1 && safety++ < 20) {
14508
14593
  let anchor = null;
14509
14594
 
@@ -14543,7 +14628,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
14543
14628
  parts.unshift(nthOfType(node));
14544
14629
  }
14545
14630
 
14546
- if (!node.parentElement || roots.includes(node)) break;
14631
+ if (!node.parentElement || (stopAtMatchedRoot && roots.includes(node))) break;
14547
14632
  node = node.parentElement;
14548
14633
  }
14549
14634
 
@@ -14562,7 +14647,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
14562
14647
  // position relative to its own parent via `>` (child, not
14563
14648
  // descendant) combinators, so a correctly-matching chain can
14564
14649
  // only resolve to one element short of a malformed document
14565
- // (e.g. two <html> roots). Re-deriving that guarantee via a
14650
+ // (e.g. two <html> roots) -- true as long as the walk above
14651
+ // never stops short of a genuinely unique anchor/root, which is
14652
+ // exactly what `stopAtMatchedRoot` now guarantees (see its own
14653
+ // comment above; a multi-root contextSelector scan stopping
14654
+ // early used to violate this invariant silently). Re-deriving
14655
+ // that guarantee via a
14566
14656
  // document-wide :nth-of-type scan was measured to cost O(total
14567
14657
  // same-tag siblings) per call — pathological on pages with many
14568
14658
  // flat, unidentified siblings (e.g. hundreds of unlabeled
@@ -15129,6 +15219,9 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
15129
15219
 
15130
15220
  // Default on: opt OUT with `includeShadowDom: false`, not opt in.
15131
15221
  const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
15222
+ // Default off: hidden/collapsed content is excluded from rule evaluation
15223
+ // unless the caller explicitly opts in.
15224
+ const includeHiddenElements = !!(engineOptionsResolved && engineOptionsResolved.includeHiddenElements === true);
15132
15225
  const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
15133
15226
 
15134
15227
  const url = pageUrl || (document.location && document.location.href) || null;
@@ -15139,11 +15232,27 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
15139
15232
  ? engineOptionsResolved.timestamp.trim()
15140
15233
  : null;
15141
15234
 
15235
+ // createDomHelpers()/createContrastHelpers() persist their element-keyed
15236
+ // caches (outerHtmlCache, selectorCache, etc.) on window.__a11ycoreSharedCache
15237
+ // so multiple helper instances created *within this run* can share them
15238
+ // deterministically. But a window/document is frequently reused across
15239
+ // SEPARATE runs -- e.g. Jest's jsdom environment creates one window per
15240
+ // test file, and mutating document.body between it() blocks is standard.
15241
+ // Those caches are keyed by element reference, not content, so a run that
15242
+ // reuses an already-cached element (document.body never changes identity)
15243
+ // would otherwise read stale data cached by an earlier, unrelated run on
15244
+ // the same window. Clearing at the start of every run keeps sharing scoped
15245
+ // to "this run" as intended, without leaking across runs.
15246
+ try {
15247
+ if (window && window.__a11ycoreSharedCache) window.__a11ycoreSharedCache = {};
15248
+ } catch {}
15249
+
15142
15250
  const sharedHelpers = createDomHelpers({
15143
15251
  document,
15144
15252
  window,
15145
15253
  root: roots,
15146
15254
  includeShadowDom,
15255
+ includeHiddenElements,
15147
15256
  excludeSelectors,
15148
15257
  // Optional perf counters (bench/debug only). Deterministic and per-run.
15149
15258
  perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)
@@ -21247,8 +21356,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
21247
21356
  },
21248
21357
  {
21249
21358
  "ruleId": "skip-link",
21250
- "title": "Skip link must have a resolvable target",
21251
- "description": "Checks that a \"skip to ...\" link's href fragment resolves to a real element in the document.",
21359
+ "title": "Skip link must have a resolvable, usable target",
21360
+ "description": "Checks that a \"skip to ...\" link's href fragment resolves to a real, currently usable element in the document.",
21252
21361
  "i18n": {
21253
21362
  "titleKey": "skipLink_title",
21254
21363
  "descriptionKey": "skipLink_description"
@@ -22840,13 +22949,7 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
22840
22949
  const RULE_IMPLS = {
22841
22950
  "accesskeys": { run: (function runInPage(ctx) {
22842
22951
  const { document, helpers, rule } = ctx;
22843
-
22844
- let nodes = [];
22845
- try {
22846
- nodes = document.querySelectorAll('[accesskey]');
22847
- } catch {
22848
- nodes = [];
22849
- }
22952
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[accesskey]') : helpers.queryAll('[accesskey]');
22850
22953
 
22851
22954
  const groups = new Map(); // normalized key -> elements[]
22852
22955
  for (const el of nodes) {
@@ -23437,8 +23540,7 @@ if (isAccTreeEligible) {
23437
23540
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
23438
23541
  }), applicability: null },
23439
23542
  "aria-allowed-attr": { run: (function runInPage(ctx) {
23440
- const { document, root, helpers, rule } = ctx;
23441
- const safeRoot = root || document;
23543
+ const { document, helpers, rule } = ctx;
23442
23544
 
23443
23545
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
23444
23546
  if (!ariaHelpers) {
@@ -23502,7 +23604,7 @@ if (isAccTreeEligible) {
23502
23604
  };
23503
23605
 
23504
23606
  const globalSet = new Set(GLOBAL_ATTRS);
23505
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
23607
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
23506
23608
 
23507
23609
  const occurrences = [];
23508
23610
  let applicableCount = 0;
@@ -23567,15 +23669,14 @@ if (isAccTreeEligible) {
23567
23669
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23568
23670
  }), applicability: null },
23569
23671
  "aria-allowed-role": { run: (function runInPage(ctx) {
23570
- const { document, root, helpers, rule } = ctx;
23571
- const safeRoot = root || document;
23672
+ const { document, helpers, rule } = ctx;
23572
23673
 
23573
23674
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
23574
23675
  if (!ariaHelpers) {
23575
23676
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
23576
23677
  }
23577
23678
 
23578
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
23679
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
23579
23680
 
23580
23681
  const occurrences = [];
23581
23682
  let applicableCount = 0;
@@ -23622,8 +23723,7 @@ if (isAccTreeEligible) {
23622
23723
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23623
23724
  }), applicability: null },
23624
23725
  "aria-braille-equivalent": { run: (function runInPage(ctx) {
23625
- const { document, root, helpers, rule } = ctx;
23626
- const safeRoot = root || document;
23726
+ const { document, helpers, rule } = ctx;
23627
23727
 
23628
23728
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
23629
23729
 
@@ -23644,8 +23744,8 @@ if (isAccTreeEligible) {
23644
23744
  }
23645
23745
 
23646
23746
  const nodes = helpers.queryAllSmart
23647
- ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]', safeRoot)
23648
- : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]', safeRoot);
23747
+ ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]')
23748
+ : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]');
23649
23749
 
23650
23750
  const occurrences = [];
23651
23751
  let applicableCount = 0;
@@ -23706,14 +23806,13 @@ if (isAccTreeEligible) {
23706
23806
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23707
23807
  }), applicability: null },
23708
23808
  "aria-checked-state-mismatch": { run: (function runInPage(ctx) {
23709
- const { document, root, helpers, rule } = ctx;
23710
- const safeRoot = root || document;
23809
+ const { document, helpers, rule } = ctx;
23711
23810
 
23712
23811
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
23713
23812
 
23714
23813
  const nodes = helpers.queryAllSmart
23715
- ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]', safeRoot)
23716
- : helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]', safeRoot);
23814
+ ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]')
23815
+ : helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]');
23717
23816
 
23718
23817
  const occurrences = [];
23719
23818
  let applicableCount = 0;
@@ -23775,16 +23874,15 @@ if (isAccTreeEligible) {
23775
23874
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
23776
23875
  }), applicability: null },
23777
23876
  "aria-conditional-attr": { run: (function runInPage(ctx) {
23778
- const { document, root, helpers, rule } = ctx;
23779
- const safeRoot = root || document;
23877
+ const { document, helpers, rule } = ctx;
23780
23878
 
23781
23879
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
23782
23880
 
23783
23881
  const TRUTHY_INVALID_VALUES = new Set(['true', 'grammar', 'spelling']);
23784
23882
 
23785
23883
  const nodes = helpers.queryAllSmart
23786
- ? helpers.queryAllSmart('[aria-errormessage]', safeRoot)
23787
- : helpers.queryAll('[aria-errormessage]', safeRoot);
23884
+ ? helpers.queryAllSmart('[aria-errormessage]')
23885
+ : helpers.queryAll('[aria-errormessage]');
23788
23886
 
23789
23887
  const occurrences = [];
23790
23888
  let applicableCount = 0;
@@ -23829,15 +23927,14 @@ if (isAccTreeEligible) {
23829
23927
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23830
23928
  }), applicability: null },
23831
23929
  "aria-deprecated-role": { run: (function runInPage(ctx) {
23832
- const { document, root, helpers, rule } = ctx;
23833
- const safeRoot = root || document;
23930
+ const { document, helpers, rule } = ctx;
23834
23931
 
23835
23932
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
23836
23933
  if (!ariaHelpers) {
23837
23934
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
23838
23935
  }
23839
23936
 
23840
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
23937
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
23841
23938
 
23842
23939
  const occurrences = [];
23843
23940
  let applicableCount = 0;
@@ -24349,8 +24446,7 @@ if (isAccTreeEligible) {
24349
24446
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
24350
24447
  }), applicability: null },
24351
24448
  "aria-prohibited-attr": { run: (function runInPage(ctx) {
24352
- const { document, root, helpers, rule } = ctx;
24353
- const safeRoot = root || document;
24449
+ const { document, helpers, rule } = ctx;
24354
24450
 
24355
24451
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24356
24452
  if (!ariaHelpers) {
@@ -24370,7 +24466,7 @@ if (isAccTreeEligible) {
24370
24466
 
24371
24467
  const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
24372
24468
 
24373
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24469
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24374
24470
 
24375
24471
  const occurrences = [];
24376
24472
  let applicableCount = 0;
@@ -24421,8 +24517,7 @@ if (isAccTreeEligible) {
24421
24517
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24422
24518
  }), applicability: null },
24423
24519
  "aria-prohibited-children": { run: (function runInPage(ctx) {
24424
- const { document, root, helpers, rule } = ctx;
24425
- const safeRoot = root || document;
24520
+ const { document, helpers, rule } = ctx;
24426
24521
 
24427
24522
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24428
24523
  if (!ariaHelpers) {
@@ -24511,7 +24606,7 @@ if (isAccTreeEligible) {
24511
24606
  }
24512
24607
  }
24513
24608
 
24514
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24609
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24515
24610
 
24516
24611
  const occurrences = [];
24517
24612
  let applicableCount = 0;
@@ -24583,8 +24678,7 @@ if (isAccTreeEligible) {
24583
24678
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24584
24679
  }), applicability: null },
24585
24680
  "aria-required-attr": { run: (function runInPage(ctx) {
24586
- const { document, root, helpers, rule } = ctx;
24587
- const safeRoot = root || document;
24681
+ const { document, helpers, rule } = ctx;
24588
24682
 
24589
24683
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24590
24684
  if (!ariaHelpers) {
@@ -24608,7 +24702,7 @@ if (isAccTreeEligible) {
24608
24702
  return v != null && String(v).trim().toLowerCase() === 'true';
24609
24703
  }
24610
24704
 
24611
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24705
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24612
24706
 
24613
24707
  const occurrences = [];
24614
24708
  let applicableCount = 0;
@@ -24665,8 +24759,7 @@ if (isAccTreeEligible) {
24665
24759
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24666
24760
  }), applicability: null },
24667
24761
  "aria-required-children": { run: (function runInPage(ctx) {
24668
- const { document, root, helpers, rule } = ctx;
24669
- const safeRoot = root || document;
24762
+ const { document, helpers, rule } = ctx;
24670
24763
 
24671
24764
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24672
24765
  if (!ariaHelpers) {
@@ -24697,7 +24790,7 @@ if (isAccTreeEligible) {
24697
24790
  // ("runInPage MUST be self-contained").
24698
24791
  const CANDIDATE_SELECTOR = '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
24699
24792
 
24700
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24793
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24701
24794
 
24702
24795
  const occurrences = [];
24703
24796
  let applicableCount = 0;
@@ -24856,8 +24949,7 @@ if (isAccTreeEligible) {
24856
24949
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24857
24950
  }), applicability: null },
24858
24951
  "aria-required-parent": { run: (function runInPage(ctx) {
24859
- const { document, root, helpers, rule } = ctx;
24860
- const safeRoot = root || document;
24952
+ const { document, helpers, rule } = ctx;
24861
24953
 
24862
24954
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24863
24955
  if (!ariaHelpers) {
@@ -24943,7 +25035,7 @@ if (isAccTreeEligible) {
24943
25035
  const idTok = elId && String(elId).trim();
24944
25036
  if (!idTok) return false;
24945
25037
 
24946
- const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]', safeRoot) : helpers.queryAll('[aria-owns]', safeRoot);
25038
+ const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]') : helpers.queryAll('[aria-owns]');
24947
25039
  for (const owner of owners) {
24948
25040
  if (!owner || !owner.getAttribute) continue;
24949
25041
  const ownsAttr = owner.getAttribute('aria-owns') || '';
@@ -24956,7 +25048,7 @@ if (isAccTreeEligible) {
24956
25048
  return false;
24957
25049
  }
24958
25050
 
24959
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
25051
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24960
25052
 
24961
25053
  const occurrences = [];
24962
25054
  let applicableCount = 0;
@@ -25169,15 +25261,14 @@ if (isAccTreeEligible) {
25169
25261
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
25170
25262
  }), applicability: null },
25171
25263
  "aria-roles-valid": { run: (function runInPage(ctx) {
25172
- const { document, root, helpers, rule } = ctx;
25173
- const safeRoot = root || document;
25264
+ const { document, helpers, rule } = ctx;
25174
25265
 
25175
25266
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
25176
25267
  if (!ariaHelpers) {
25177
25268
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25178
25269
  }
25179
25270
 
25180
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
25271
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
25181
25272
 
25182
25273
  const occurrences = [];
25183
25274
  let applicableCount = 0;
@@ -25232,13 +25323,12 @@ if (isAccTreeEligible) {
25232
25323
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25233
25324
  }), applicability: null },
25234
25325
  "aria-text": { run: (function runInPage(ctx) {
25235
- const { document, root, helpers, rule } = ctx;
25236
- const safeRoot = root || document;
25326
+ const { document, helpers, rule } = ctx;
25237
25327
 
25238
25328
  const FOCUSABLE_DESCENDANT_SELECTOR =
25239
25329
  'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
25240
25330
 
25241
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]', safeRoot) : helpers.queryAll('[role="text"]', safeRoot);
25331
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]') : helpers.queryAll('[role="text"]');
25242
25332
 
25243
25333
  const occurrences = [];
25244
25334
  let applicableCount = 0;
@@ -25292,15 +25382,14 @@ if (isAccTreeEligible) {
25292
25382
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25293
25383
  }), applicability: null },
25294
25384
  "aria-valid-attr": { run: (function runInPage(ctx) {
25295
- const { document, root, helpers, rule } = ctx;
25296
- const safeRoot = root || document;
25385
+ const { document, helpers, rule } = ctx;
25297
25386
 
25298
25387
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
25299
25388
  if (!ariaHelpers) {
25300
25389
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25301
25390
  }
25302
25391
 
25303
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
25392
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
25304
25393
 
25305
25394
  const occurrences = [];
25306
25395
  let applicableCount = 0;
@@ -25354,15 +25443,14 @@ if (isAccTreeEligible) {
25354
25443
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25355
25444
  }), applicability: null },
25356
25445
  "aria-valid-attr-value": { run: (function runInPage(ctx) {
25357
- const { document, root, helpers, rule } = ctx;
25358
- const safeRoot = root || document;
25446
+ const { document, helpers, rule } = ctx;
25359
25447
 
25360
25448
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
25361
25449
  if (!ariaHelpers) {
25362
25450
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25363
25451
  }
25364
25452
 
25365
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
25453
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
25366
25454
 
25367
25455
  const occurrences = [];
25368
25456
  let applicableCount = 0;
@@ -25419,8 +25507,7 @@ if (isAccTreeEligible) {
25419
25507
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25420
25508
  }), applicability: null },
25421
25509
  "autocomplete-valid": { run: (function runInPage(ctx) {
25422
- const { document, root, helpers, rule } = ctx;
25423
- const safeRoot = root || document;
25510
+ const { document, helpers, rule } = ctx;
25424
25511
 
25425
25512
  // Declared inside runInPage — see scripts/build-core.js header
25426
25513
  // ("runInPage MUST be self-contained").
@@ -25455,7 +25542,7 @@ if (isAccTreeEligible) {
25455
25542
  return FIELD_NAMES.has(remaining[0]);
25456
25543
  }
25457
25544
 
25458
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea', safeRoot) : helpers.queryAll('input, select, textarea', safeRoot);
25545
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea') : helpers.queryAll('input, select, textarea');
25459
25546
 
25460
25547
  const occurrences = [];
25461
25548
  let applicableCount = 0;
@@ -25498,12 +25585,11 @@ if (isAccTreeEligible) {
25498
25585
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25499
25586
  }), applicability: null },
25500
25587
  "avoid-inline-spacing": { run: (function runInPage(ctx) {
25501
- const { document, root, helpers, rule } = ctx;
25502
- const safeRoot = root || document;
25588
+ const { document, helpers, rule } = ctx;
25503
25589
 
25504
25590
  const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
25505
25591
 
25506
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]', safeRoot) : helpers.queryAll('[style]', safeRoot);
25592
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]') : helpers.queryAll('[style]');
25507
25593
 
25508
25594
  const occurrences = [];
25509
25595
  let applicableCount = 0;
@@ -25556,11 +25642,10 @@ if (isAccTreeEligible) {
25556
25642
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25557
25643
  }), applicability: null },
25558
25644
  "binary-control-name-present": { run: (function runInPage(ctx) {
25559
- const { document, root, helpers, rule } = ctx;
25645
+ const { document, helpers, rule } = ctx;
25560
25646
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
25561
25647
  ? helpers.getEligibilityInfo
25562
25648
  : null;
25563
- const safeRoot = root || document;
25564
25649
 
25565
25650
 
25566
25651
  function normalizeWs(s) {
@@ -25692,7 +25777,7 @@ if (isAccTreeEligible) {
25692
25777
  let applicableCount = 0;
25693
25778
 
25694
25779
  const selector = 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
25695
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
25780
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
25696
25781
 
25697
25782
 
25698
25783
  // Precompute label[for] associations once for speed/determinism.
@@ -25824,8 +25909,7 @@ if (isAccTreeEligible) {
25824
25909
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25825
25910
  }), applicability: null },
25826
25911
  "button-name-present": { run: (function runInPage(ctx) {
25827
- const { document, root, helpers, rule } = ctx;
25828
- const safeRoot = root || document;
25912
+ const { document, helpers, rule } = ctx;
25829
25913
 
25830
25914
  const occurrences = [];
25831
25915
  let applicableCount = 0;
@@ -25862,7 +25946,7 @@ if (isAccTreeEligible) {
25862
25946
  }
25863
25947
 
25864
25948
  const selector = 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
25865
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
25949
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
25866
25950
 
25867
25951
  for (const el of nodes) {
25868
25952
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -25874,6 +25958,7 @@ if (isAccTreeEligible) {
25874
25958
 
25875
25959
  const tag = (el.tagName || '').toLowerCase();
25876
25960
  const role = el.getAttribute ? el.getAttribute('role') : null;
25961
+ const roleNorm = normalizeWs(role).toLowerCase();
25877
25962
 
25878
25963
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
25879
25964
 
@@ -25889,7 +25974,17 @@ if (isAccTreeEligible) {
25889
25974
  inputValueName = getInputButtonValueName(el);
25890
25975
  }
25891
25976
 
25892
- const isContentNameCandidate = tag === 'button' || role === 'button';
25977
+ // A native <button> (or [role="button"]) whose role has been overridden to
25978
+ // one of these roles is no longer semantically a button — per the WAI-ARIA
25979
+ // Accessible Name and Description Computation spec these roles are
25980
+ // name-from-author-only, and their rendered content represents a VALUE,
25981
+ // not a NAME (mirrors axe-core's controlValueRoles, verified against its
25982
+ // source). Found on a real page (Spotify's "sort by" control): a
25983
+ // <button role="combobox">List</button> where "List" is the combobox's
25984
+ // currently selected value, not a label for what the combobox is —
25985
+ // crediting it as the accessible name masked a real missing-name bug.
25986
+ const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
25987
+ const isContentNameCandidate = (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
25893
25988
  const contentName =
25894
25989
  (!trustedProgrammaticName && !inputValueName && isContentNameCandidate)
25895
25990
  ? getConservativeSubtreeText(el)
@@ -25949,12 +26044,41 @@ if (isAccTreeEligible) {
25949
26044
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25950
26045
  }
25951
26046
 
25952
- function hasMainLandmark() {
26047
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
26048
+
26049
+ function isExposedToAt(el) {
26050
+ if (!isAccTreeEligible) return true;
25953
26051
  try {
25954
- return !!document.querySelector('main, [role="main"]');
26052
+ const r = isAccTreeEligible(el, ctx);
26053
+ if (typeof r === 'boolean') return r;
26054
+ return !!(r && r.eligible);
25955
26055
  } catch {
25956
- return false;
26056
+ return true;
26057
+ }
26058
+ }
26059
+
26060
+ function queryAll(selector) {
26061
+ try {
26062
+ return helpers && typeof helpers.queryAllSmart === 'function'
26063
+ ? helpers.queryAllSmart(selector)
26064
+ : document.querySelectorAll(selector);
26065
+ } catch {
26066
+ return [];
26067
+ }
26068
+ }
26069
+
26070
+ // Filters through isAccTreeEligible (hidden/aria-hidden/display:none/inert
26071
+ // don't count as "a bypass mechanism is present") -- see page-has-heading-
26072
+ // one-manual.js's identical fix for the real-world trigger (CDC's flu
26073
+ // page, 2026-07-30: its only <h1> sits inside a display:none ancestor,
26074
+ // unreachable by sighted and screen reader users alike). A fully
26075
+ // non-rendered <main>/heading was previously credited here too, wrongly
26076
+ // returning `pass` for a page with zero actual bypass mechanisms.
26077
+ function hasMainLandmark() {
26078
+ for (const el of queryAll('main, [role="main"]')) {
26079
+ if (el && isExposedToAt(el)) return true;
25957
26080
  }
26081
+ return false;
25958
26082
  }
25959
26083
 
25960
26084
  function hasWorkingAnchorLink() {
@@ -25996,11 +26120,10 @@ if (isAccTreeEligible) {
25996
26120
  }
25997
26121
 
25998
26122
  function hasHeading() {
25999
- try {
26000
- return !!document.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
26001
- } catch {
26002
- return false;
26123
+ for (const el of queryAll('h1, h2, h3, h4, h5, h6, [role="heading"]')) {
26124
+ if (el && isExposedToAt(el)) return true;
26003
26125
  }
26126
+ return false;
26004
26127
  }
26005
26128
 
26006
26129
  const mainLandmark = hasMainLandmark();
@@ -26258,11 +26381,10 @@ if (isAccTreeEligible) {
26258
26381
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
26259
26382
  }), applicability: null },
26260
26383
  "combobox-name-present": { run: (function runInPage(ctx) {
26261
- const { document, root, helpers, rule } = ctx;
26384
+ const { document, helpers, rule } = ctx;
26262
26385
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
26263
26386
  ? helpers.getEligibilityInfo
26264
26387
  : null;
26265
- const safeRoot = root || document;
26266
26388
 
26267
26389
 
26268
26390
  function normalizeWs(s) {
@@ -26394,7 +26516,7 @@ if (isAccTreeEligible) {
26394
26516
  let applicableCount = 0;
26395
26517
 
26396
26518
  const selector = '[role="combobox"]';
26397
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
26519
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
26398
26520
 
26399
26521
  // Precompute label[for] map for combobox elements that are labelable
26400
26522
  // native form controls (e.g. <input role="combobox">).
@@ -28121,14 +28243,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28121
28243
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
28122
28244
  }), applicability: null },
28123
28245
  "definition-list-children-valid": { run: (function runInPage(ctx) {
28124
- const { document, root, helpers, rule } = ctx;
28125
- const safeRoot = root || document;
28246
+ const { document, helpers, rule } = ctx;
28126
28247
 
28127
28248
  // Declared inside runInPage — see scripts/build-core.js header
28128
28249
  // ("runInPage MUST be self-contained").
28129
28250
  const PASSTHROUGH_TAGS = new Set(['dt', 'dd', 'script', 'template', 'style']);
28130
28251
 
28131
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl', safeRoot) : helpers.queryAll('dl', safeRoot);
28252
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl') : helpers.queryAll('dl');
28132
28253
 
28133
28254
  const occurrences = [];
28134
28255
  let applicableCount = 0;
@@ -28212,10 +28333,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28212
28333
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
28213
28334
  }), applicability: null },
28214
28335
  "deprecated-elements-not-used": { run: (function runInPage(ctx) {
28215
- const { document, root, helpers, rule } = ctx;
28216
- const safeRoot = root || document;
28336
+ const { document, helpers, rule } = ctx;
28217
28337
 
28218
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee', safeRoot) : helpers.queryAll('blink, marquee', safeRoot);
28338
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee') : helpers.queryAll('blink, marquee');
28219
28339
 
28220
28340
  const occurrences = [];
28221
28341
  let applicableCount = 0;
@@ -28254,11 +28374,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28254
28374
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
28255
28375
  }), applicability: null },
28256
28376
  "dialog-name-present": { run: (function runInPage(ctx) {
28257
- const { document, root, helpers, rule } = ctx;
28377
+ const { document, helpers, rule } = ctx;
28258
28378
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28259
28379
  ? helpers.getEligibilityInfo
28260
28380
  : null;
28261
- const safeRoot = root || document;
28262
28381
 
28263
28382
 
28264
28383
  function normalizeWs(s) {
@@ -28372,7 +28491,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28372
28491
  let applicableCount = 0;
28373
28492
 
28374
28493
  const selector = '[role="dialog"],[role="alertdialog"]';
28375
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
28494
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
28376
28495
 
28377
28496
 
28378
28497
  function evaluate(el) {
@@ -28437,10 +28556,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28437
28556
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
28438
28557
  }), applicability: null },
28439
28558
  "dlitem-parent-valid": { run: (function runInPage(ctx) {
28440
- const { document, root, helpers, rule } = ctx;
28441
- const safeRoot = root || document;
28559
+ const { document, helpers, rule } = ctx;
28442
28560
 
28443
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd', safeRoot) : helpers.queryAll('dt, dd', safeRoot);
28561
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd') : helpers.queryAll('dt, dd');
28444
28562
 
28445
28563
  const occurrences = [];
28446
28564
  let applicableCount = 0;
@@ -29092,8 +29210,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
29092
29210
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
29093
29211
  }), applicability: null },
29094
29212
  "focus-order-semantics": { run: (function runInPage(ctx) {
29095
- const { document, root, helpers, rule } = ctx;
29096
- const safeRoot = root || document;
29213
+ const { document, helpers, rule } = ctx;
29097
29214
 
29098
29215
  const NON_INTERACTIVE_ROLES = new Set([
29099
29216
  'generic', 'group', 'text', 'presentation', 'none', 'img', 'heading',
@@ -29103,7 +29220,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
29103
29220
  'definition', 'paragraph', 'caption', 'status', 'alert', 'log', 'tooltip'
29104
29221
  ]);
29105
29222
 
29106
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex][role]', safeRoot) : helpers.queryAll('[tabindex][role]', safeRoot);
29223
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex][role]') : helpers.queryAll('[tabindex][role]');
29107
29224
 
29108
29225
  const occurrences = [];
29109
29226
  let applicableCount = 0;
@@ -29744,11 +29861,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
29744
29861
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [], data: { details: { metrics } } };
29745
29862
  }), applicability: null },
29746
29863
  "form-control-single-label": { run: (function runInPage(ctx) {
29747
- const { document, root, helpers, rule } = ctx;
29748
- const safeRoot = root || document;
29864
+ const { document, helpers, rule } = ctx;
29749
29865
 
29750
29866
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
29751
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
29867
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
29752
29868
 
29753
29869
  // Build a for-value -> label[] map once (avoids per-control dynamic
29754
29870
  // attribute-selector construction / CSS.escape, which is not guaranteed
@@ -30026,14 +30142,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30026
30142
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
30027
30143
  }), applicability: null },
30028
30144
  "identical-links-same-purpose": { run: (function runInPage(ctx) {
30029
- const { document, root, helpers, rule } = ctx;
30030
- const safeRoot = root || document;
30145
+ const { document, helpers, rule } = ctx;
30031
30146
 
30032
30147
  function normName(s) {
30033
30148
  return (s == null ? '' : String(s)).replace(/\s+/g, ' ').trim().toLowerCase();
30034
30149
  }
30035
30150
 
30036
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]', safeRoot) : helpers.queryAll('a[href]', safeRoot);
30151
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]') : helpers.queryAll('a[href]');
30037
30152
 
30038
30153
  const groups = new Map(); // normName -> [{ el, href }]
30039
30154
  let applicableCount = 0;
@@ -30113,8 +30228,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30113
30228
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
30114
30229
  }), applicability: null },
30115
30230
  "iframe-focusable-content": { run: (function runInPage(ctx) {
30116
- const { document, root, helpers, rule } = ctx;
30117
- const safeRoot = root || document;
30231
+ const { document, helpers, rule } = ctx;
30118
30232
 
30119
30233
  function hasFocusableCandidate(doc) {
30120
30234
  if (!doc || !doc.querySelectorAll) return false;
@@ -30146,7 +30260,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30146
30260
  return !Number.isNaN(n) && n < 0;
30147
30261
  }
30148
30262
 
30149
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame', safeRoot) : helpers.queryAll('iframe, frame', safeRoot);
30263
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
30150
30264
 
30151
30265
  const occurrences = [];
30152
30266
  let applicableCount = 0;
@@ -30196,10 +30310,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30196
30310
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
30197
30311
  }), applicability: null },
30198
30312
  "iframe-name-present": { run: (function runInPage(ctx) {
30199
- const { document, root, helpers, rule } = ctx;
30200
- const safeRoot = root || document;
30313
+ const { document, helpers, rule } = ctx;
30201
30314
 
30202
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame', safeRoot) : helpers.queryAll('iframe, frame', safeRoot);
30315
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
30203
30316
 
30204
30317
  const occurrences = [];
30205
30318
  let applicableCount = 0;
@@ -30251,10 +30364,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30251
30364
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
30252
30365
  }), applicability: null },
30253
30366
  "iframe-title-unique": { run: (function runInPage(ctx) {
30254
- const { document, root, helpers, rule } = ctx;
30255
- const safeRoot = root || document;
30367
+ const { document, helpers, rule } = ctx;
30256
30368
 
30257
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame', safeRoot) : helpers.queryAll('iframe, frame', safeRoot);
30369
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
30258
30370
 
30259
30371
  const groups = new Map(); // trimmed title -> elements[]
30260
30372
  let applicableCount = 0;
@@ -30309,8 +30421,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30309
30421
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
30310
30422
  }), applicability: null },
30311
30423
  "image-redundant-alt": { run: (function runInPage(ctx) {
30312
- const { document, root, helpers, rule } = ctx;
30313
- const safeRoot = root || document;
30424
+ const { document, helpers, rule } = ctx;
30314
30425
 
30315
30426
  function normalizeWs(s) {
30316
30427
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -30329,7 +30440,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30329
30440
  return normalizeWs(text);
30330
30441
  }
30331
30442
 
30332
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[alt]', safeRoot) : helpers.queryAll('img[alt]', safeRoot);
30443
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[alt]') : helpers.queryAll('img[alt]');
30333
30444
 
30334
30445
  const occurrences = [];
30335
30446
  let applicableCount = 0;
@@ -31117,8 +31228,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31117
31228
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
31118
31229
  }), applicability: null },
31119
31230
  "label-in-name": { run: (function runInPage(ctx) {
31120
- const { document, root, helpers, rule } = ctx;
31121
- const safeRoot = root || document;
31231
+ const { document, helpers, rule } = ctx;
31122
31232
 
31123
31233
  const occurrences = [];
31124
31234
  let applicableCount = 0;
@@ -31127,7 +31237,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31127
31237
  // NOTE: aria-hidden is intentionally NOT excluded here; it does not affect visual rendering.
31128
31238
  const selector = ':is(button, a[href], summary, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="searchbox"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="treeitem"], [role="gridcell"]):not([hidden]):not([disabled]):not([aria-disabled="true"]):is([aria-label], [aria-labelledby])';
31129
31239
 
31130
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
31240
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
31131
31241
 
31132
31242
  function norm(s) {
31133
31243
  const v = (s == null ? '' : String(s));
@@ -31265,11 +31375,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31265
31375
  }
31266
31376
 
31267
31377
  // Fallback: label[for=id]. Uses `document` directly rather than
31268
- // ctx.root/safeRoot -- label[for] association is a document-wide
31269
- // relationship (IDs are document-unique), not bounded by whatever
31270
- // contextSelector region happens to be scanned, and ctx.root is an
31271
- // array (multi-region contextSelector support), not a single element
31272
- // with its own .querySelector to call directly.
31378
+ // ctx.root -- label[for] association is a document-wide relationship
31379
+ // (IDs are document-unique), not bounded by whatever contextSelector
31380
+ // region happens to be scanned, and ctx.root is an array (multi-region
31381
+ // contextSelector support), not a single element with its own
31382
+ // .querySelector to call directly.
31273
31383
  try {
31274
31384
  const idAttribute = control && control.getAttribute ? (control.getAttribute('id') || '') : '';
31275
31385
  const key = String(idAttribute || '').trim();
@@ -31410,11 +31520,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31410
31520
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
31411
31521
  }), applicability: null },
31412
31522
  "label-title-only": { run: (function runInPage(ctx) {
31413
- const { document, root, helpers, rule } = ctx;
31414
- const safeRoot = root || document;
31523
+ const { document, helpers, rule } = ctx;
31415
31524
 
31416
31525
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
31417
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
31526
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
31418
31527
 
31419
31528
  const labelsByFor = new Map();
31420
31529
  const allLabels = document.getElementsByTagName ? document.getElementsByTagName('label') : [];
@@ -32461,8 +32570,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32461
32570
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
32462
32571
  }), applicability: null },
32463
32572
  "link-in-text-block": { run: (function runInPage(ctx) {
32464
- const { document, root, helpers, rule, engineOptions } = ctx;
32465
- const safeRoot = root || document;
32573
+ const { document, helpers, rule, engineOptions } = ctx;
32466
32574
 
32467
32575
  function safeComputedStyle(el) {
32468
32576
  try {
@@ -32508,7 +32616,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32508
32616
  const c = helpers && helpers.contrast ? helpers.contrast : null;
32509
32617
 
32510
32618
  const selector = 'a[href]';
32511
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
32619
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
32512
32620
 
32513
32621
  const occurrences = [];
32514
32622
  let applicableCount = 0;
@@ -32608,8 +32716,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32608
32716
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
32609
32717
  }), applicability: null },
32610
32718
  "link-name-present": { run: (function runInPage(ctx) {
32611
- const { document, root, helpers, rule } = ctx;
32612
- const safeRoot = root || document;
32719
+ const { document, helpers, rule } = ctx;
32613
32720
 
32614
32721
  const occurrences = [];
32615
32722
  let applicableCount = 0;
@@ -32630,7 +32737,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32630
32737
  }
32631
32738
 
32632
32739
  const selector = 'a[href], area[href], [role="link"]';
32633
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
32740
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
32634
32741
 
32635
32742
  for (const el of nodes) {
32636
32743
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -32643,7 +32750,20 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32643
32750
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
32644
32751
  const programmaticName = (nameInfo && typeof nameInfo.value === 'string') ? nameInfo.value : '';
32645
32752
 
32646
- const contentName = programmaticName.trim().length === 0 ? getConservativeSubtreeText(el) : '';
32753
+ // A native <a href>/<area href> (or [role="link"]) whose role has been
32754
+ // overridden to one of these roles is no longer semantically a link —
32755
+ // per the WAI-ARIA Accessible Name and Description Computation spec
32756
+ // these roles are name-from-author-only, and their rendered content
32757
+ // represents a VALUE, not a NAME (mirrors axe-core's controlValueRoles,
32758
+ // verified against its source; see button-name-present.js's identical
32759
+ // fix for a real-world example — <button role="combobox">List</button> —
32760
+ // of the same class of bug on a different host tag).
32761
+ const role = el.getAttribute ? el.getAttribute('role') : null;
32762
+ const roleNorm = String(role || '').replace(/\s+/g, ' ').trim().toLowerCase();
32763
+ const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
32764
+ const isContentNameCandidate = !VALUE_ROLES.includes(roleNorm);
32765
+
32766
+ const contentName = (programmaticName.trim().length === 0 && isContentNameCandidate) ? getConservativeSubtreeText(el) : '';
32647
32767
  const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
32648
32768
 
32649
32769
  if (finalName.length === 0) {
@@ -32695,8 +32815,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32695
32815
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
32696
32816
  }), applicability: null },
32697
32817
  "link-name-quality": { run: (function runInPage(ctx) {
32698
- const { document, root, helpers, rule } = ctx;
32699
- const safeRoot = root || document;
32818
+ const { document, helpers, rule } = ctx;
32700
32819
 
32701
32820
  const GENERIC_LINK_TEXT = new Set([
32702
32821
  'click here', 'here', 'click', 'more', 'more info', 'more information',
@@ -32715,7 +32834,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32715
32834
  }
32716
32835
 
32717
32836
  const selector = 'a[href]';
32718
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
32837
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
32719
32838
 
32720
32839
  const occurrences = [];
32721
32840
  let applicableCount = 0;
@@ -32775,14 +32894,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32775
32894
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
32776
32895
  }), applicability: null },
32777
32896
  "list-children-valid": { run: (function runInPage(ctx) {
32778
- const { document, root, helpers, rule } = ctx;
32779
- const safeRoot = root || document;
32897
+ const { document, helpers, rule } = ctx;
32780
32898
 
32781
32899
  // Declared inside runInPage — see scripts/build-core.js header
32782
32900
  // ("runInPage MUST be self-contained").
32783
32901
  const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
32784
32902
 
32785
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('ul, ol', safeRoot) : helpers.queryAll('ul, ol', safeRoot);
32903
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('ul, ol') : helpers.queryAll('ul, ol');
32786
32904
 
32787
32905
  const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
32788
32906
 
@@ -32853,11 +32971,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32853
32971
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
32854
32972
  }), applicability: null },
32855
32973
  "listbox-name-present": { run: (function runInPage(ctx) {
32856
- const { document, root, helpers, rule } = ctx;
32974
+ const { document, helpers, rule } = ctx;
32857
32975
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
32858
32976
  ? helpers.getEligibilityInfo
32859
32977
  : null;
32860
- const safeRoot = root || document;
32861
32978
 
32862
32979
 
32863
32980
  function normalizeWs(s) {
@@ -32962,7 +33079,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32962
33079
  let applicableCount = 0;
32963
33080
 
32964
33081
  const selector = "[role=\"listbox\"]";
32965
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
33082
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
32966
33083
 
32967
33084
  // Precompute label[for] map for listbox elements that are labelable
32968
33085
  // native form controls (e.g. <select multiple role="listbox">).
@@ -33063,10 +33180,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33063
33180
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
33064
33181
  }), applicability: null },
33065
33182
  "listitem-parent-valid": { run: (function runInPage(ctx) {
33066
- const { document, root, helpers, rule } = ctx;
33067
- const safeRoot = root || document;
33183
+ const { document, helpers, rule } = ctx;
33068
33184
 
33069
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li', safeRoot) : helpers.queryAll('li', safeRoot);
33185
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li') : helpers.queryAll('li');
33070
33186
 
33071
33187
  const occurrences = [];
33072
33188
  let applicableCount = 0;
@@ -33495,11 +33611,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33495
33611
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
33496
33612
  }), applicability: null },
33497
33613
  "menuitem-name-present": { run: (function runInPage(ctx) {
33498
- const { document, root, helpers, rule } = ctx;
33614
+ const { document, helpers, rule } = ctx;
33499
33615
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
33500
33616
  ? helpers.getEligibilityInfo
33501
33617
  : null;
33502
- const safeRoot = root || document;
33503
33618
 
33504
33619
 
33505
33620
  function normalizeWs(s) {
@@ -33605,7 +33720,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33605
33720
  let applicableCount = 0;
33606
33721
 
33607
33722
  const selector = '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]';
33608
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
33723
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
33609
33724
 
33610
33725
 
33611
33726
  function evaluate(el) {
@@ -33911,11 +34026,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33911
34026
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
33912
34027
  }), applicability: null },
33913
34028
  "meter-name-present": { run: (function runInPage(ctx) {
33914
- const { document, root, helpers, rule } = ctx;
34029
+ const { document, helpers, rule } = ctx;
33915
34030
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
33916
34031
  ? helpers.getEligibilityInfo
33917
34032
  : null;
33918
- const safeRoot = root || document;
33919
34033
 
33920
34034
 
33921
34035
  function normalizeWs(s) {
@@ -33979,7 +34093,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33979
34093
  let applicableCount = 0;
33980
34094
 
33981
34095
  const selector = '[role="meter"]';
33982
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
34096
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
33983
34097
 
33984
34098
 
33985
34099
  function evaluate(el) {
@@ -34045,8 +34159,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34045
34159
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
34046
34160
  }), applicability: null },
34047
34161
  "mouse-only-event-handlers": { run: (function runInPage(ctx) {
34048
- const { document, root, helpers, rule } = ctx;
34049
- const safeRoot = root || document;
34162
+ const { document, helpers, rule } = ctx;
34050
34163
 
34051
34164
  const MOUSE_ONLY_ATTRS = [
34052
34165
  'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup',
@@ -34057,7 +34170,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34057
34170
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
34058
34171
 
34059
34172
  const selector = MOUSE_ONLY_ATTRS.map((a) => `[${a}]`).join(', ');
34060
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
34173
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
34061
34174
 
34062
34175
  const occurrences = [];
34063
34176
  let applicableCount = 0;
@@ -34109,8 +34222,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34109
34222
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
34110
34223
  }), applicability: null },
34111
34224
  "nested-interactive-controls-absent": { run: (function runInPage(ctx) {
34112
- const { document, root, helpers, rule } = ctx;
34113
- const safeRoot = root || document;
34225
+ const { document, helpers, rule } = ctx;
34114
34226
 
34115
34227
  // Declared inside runInPage — see scripts/build-core.js header
34116
34228
  // ("runInPage MUST be self-contained").
@@ -34139,7 +34251,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34139
34251
  '[role="treeitem"]'
34140
34252
  ].join(', ');
34141
34253
 
34142
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(INTERACTIVE_SELECTOR, safeRoot) : helpers.queryAll(INTERACTIVE_SELECTOR, safeRoot);
34254
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(INTERACTIVE_SELECTOR) : helpers.queryAll(INTERACTIVE_SELECTOR);
34143
34255
 
34144
34256
  const occurrences = [];
34145
34257
  let applicableCount = 0;
@@ -34188,10 +34300,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34188
34300
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
34189
34301
  }), applicability: null },
34190
34302
  "no-autoplay-audio": { run: (function runInPage(ctx) {
34191
- const { document, root, helpers, rule } = ctx;
34192
- const safeRoot = root || document;
34303
+ const { document, helpers, rule } = ctx;
34193
34304
 
34194
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('audio[autoplay], video[autoplay]', safeRoot) : helpers.queryAll('audio[autoplay], video[autoplay]', safeRoot);
34305
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('audio[autoplay], video[autoplay]') : helpers.queryAll('audio[autoplay], video[autoplay]');
34195
34306
 
34196
34307
  const occurrences = [];
34197
34308
  let applicableCount = 0;
@@ -34545,11 +34656,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34545
34656
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
34546
34657
  }), applicability: null },
34547
34658
  "option-name-present": { run: (function runInPage(ctx) {
34548
- const { document, root, helpers, rule } = ctx;
34659
+ const { document, helpers, rule } = ctx;
34549
34660
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
34550
34661
  ? helpers.getEligibilityInfo
34551
34662
  : null;
34552
- const safeRoot = root || document;
34553
34663
 
34554
34664
 
34555
34665
  function normalizeWs(s) {
@@ -34613,7 +34723,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34613
34723
  let applicableCount = 0;
34614
34724
 
34615
34725
  const selector = "[role=\"option\"]";
34616
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
34726
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
34617
34727
 
34618
34728
 
34619
34729
  function hasName(el) {
@@ -34676,8 +34786,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34676
34786
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
34677
34787
  }), applicability: null },
34678
34788
  "p-as-heading": { run: (function runInPage(ctx) {
34679
- const { document, root, helpers, rule } = ctx;
34680
- const safeRoot = root || document;
34789
+ const { document, helpers, rule } = ctx;
34681
34790
 
34682
34791
  const MAX_HEADING_LIKE_CHARS = 120;
34683
34792
  const MIN_FONT_SIZE_PX = 18;
@@ -34729,7 +34838,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34729
34838
  return Number.isFinite(px) ? px : 0;
34730
34839
  }
34731
34840
 
34732
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p', safeRoot) : helpers.queryAll('p', safeRoot);
34841
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p') : helpers.queryAll('p');
34733
34842
 
34734
34843
  const occurrences = [];
34735
34844
  let applicableCount = 0;
@@ -34811,14 +34920,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34811
34920
  return tag === 'h1';
34812
34921
  }
34813
34922
 
34923
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
34924
+
34925
+ function isExposedToAt(el) {
34926
+ if (!isAccTreeEligible) return true;
34927
+ try {
34928
+ const r = isAccTreeEligible(el, ctx);
34929
+ if (typeof r === 'boolean') return r;
34930
+ return !!(r && r.eligible);
34931
+ } catch {
34932
+ return true;
34933
+ }
34934
+ }
34935
+
34936
+ // queryAllSmart (shadow-DOM-aware) instead of plain document.querySelectorAll -- see
34937
+ // landmark-one-main-manual.js's identical precedent.
34814
34938
  let nodes = [];
34815
34939
  try {
34816
- nodes = document.querySelectorAll('h1, [role]');
34940
+ nodes = helpers && typeof helpers.queryAllSmart === 'function'
34941
+ ? helpers.queryAllSmart('h1, [role]')
34942
+ : document.querySelectorAll('h1, [role]');
34817
34943
  } catch {
34818
34944
  nodes = [];
34819
34945
  }
34820
34946
 
34821
- const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el));
34947
+ const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el) && isExposedToAt(el));
34822
34948
 
34823
34949
  if (hasH1) {
34824
34950
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -35142,8 +35268,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35142
35268
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
35143
35269
  }), applicability: null },
35144
35270
  "presentation-role-conflict": { run: (function runInPage(ctx) {
35145
- const { document, root, helpers, rule } = ctx;
35146
- const safeRoot = root || document;
35271
+ const { document, helpers, rule } = ctx;
35147
35272
 
35148
35273
  // The full set of ARIA attributes marked `global: true` per the WAI-ARIA
35149
35274
  // spec (confirmed against a widely-used reference engine's own
@@ -35160,8 +35285,8 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35160
35285
  const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
35161
35286
 
35162
35287
  const nodes = helpers.queryAllSmart
35163
- ? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]', safeRoot)
35164
- : helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]', safeRoot);
35288
+ ? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]')
35289
+ : helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]');
35165
35290
 
35166
35291
  const occurrences = [];
35167
35292
  let applicableCount = 0;
@@ -35225,11 +35350,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35225
35350
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
35226
35351
  }), applicability: null },
35227
35352
  "progressbar-name-present": { run: (function runInPage(ctx) {
35228
- const { document, root, helpers, rule } = ctx;
35353
+ const { document, helpers, rule } = ctx;
35229
35354
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
35230
35355
  ? helpers.getEligibilityInfo
35231
35356
  : null;
35232
- const safeRoot = root || document;
35233
35357
 
35234
35358
 
35235
35359
  function normalizeWs(s) {
@@ -35293,7 +35417,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35293
35417
  let applicableCount = 0;
35294
35418
 
35295
35419
  const selector = '[role="progressbar"]';
35296
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
35420
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
35297
35421
 
35298
35422
 
35299
35423
  function evaluate(el) {
@@ -35705,8 +35829,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35705
35829
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
35706
35830
  }), applicability: null },
35707
35831
  "scrollable-region-focusable": { run: (function runInPage(ctx) {
35708
- const { document, root, helpers, rule } = ctx;
35709
- const safeRoot = root || document;
35832
+ const { document, helpers, rule } = ctx;
35710
35833
 
35711
35834
  function safeComputedStyle(el) {
35712
35835
  try {
@@ -35751,7 +35874,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35751
35874
  }
35752
35875
 
35753
35876
  const CANDIDATE_SELECTOR = 'div, section, article, aside, main, nav, pre, table, blockquote, ul, ol, textarea';
35754
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(CANDIDATE_SELECTOR, safeRoot) : helpers.queryAll(CANDIDATE_SELECTOR, safeRoot);
35877
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(CANDIDATE_SELECTOR) : helpers.queryAll(CANDIDATE_SELECTOR);
35755
35878
 
35756
35879
  const occurrences = [];
35757
35880
  let applicableCount = 0;
@@ -35806,11 +35929,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35806
35929
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
35807
35930
  }), applicability: null },
35808
35931
  "searchbox-name-present": { run: (function runInPage(ctx) {
35809
- const { document, root, helpers, rule } = ctx;
35932
+ const { document, helpers, rule } = ctx;
35810
35933
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
35811
35934
  ? helpers.getEligibilityInfo
35812
35935
  : null;
35813
- const safeRoot = root || document;
35814
35936
 
35815
35937
 
35816
35938
  function normalizeWs(s) {
@@ -35915,7 +36037,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35915
36037
  let applicableCount = 0;
35916
36038
 
35917
36039
  const selector = "[role=\"searchbox\"]";
35918
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
36040
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
35919
36041
 
35920
36042
  // Precompute label[for] map for searchbox elements that are labelable
35921
36043
  // native form controls (e.g. <input role="searchbox">).
@@ -36016,10 +36138,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36016
36138
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36017
36139
  }), applicability: null },
36018
36140
  "server-side-image-map-absent": { run: (function runInPage(ctx) {
36019
- const { document, root, helpers, rule } = ctx;
36020
- const safeRoot = root || document;
36141
+ const { document, helpers, rule } = ctx;
36021
36142
 
36022
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[ismap]', safeRoot) : helpers.queryAll('img[ismap]', safeRoot);
36143
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[ismap]') : helpers.queryAll('img[ismap]');
36023
36144
 
36024
36145
  const occurrences = [];
36025
36146
  let applicableCount = 0;
@@ -36057,8 +36178,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36057
36178
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36058
36179
  }), applicability: null },
36059
36180
  "skip-link": { run: (function runInPage(ctx) {
36060
- const { document, root, helpers, rule } = ctx;
36061
- const safeRoot = root || document;
36181
+ const { document, helpers, rule } = ctx;
36062
36182
 
36063
36183
  function normalizeWs(s) {
36064
36184
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -36085,7 +36205,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36085
36205
  return normalizeWs(el.textContent);
36086
36206
  }
36087
36207
 
36088
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]', safeRoot) : helpers.queryAll('a[href]', safeRoot);
36208
+ function hasReliableGeometrySupport() {
36209
+ const probe = document.documentElement || document.body || null;
36210
+ if (!probe || !probe.getClientRects || !probe.getBoundingClientRect) return false;
36211
+ try {
36212
+ const rects = probe.getClientRects();
36213
+ const rectCount = rects ? rects.length : 0;
36214
+ const r = probe.getBoundingClientRect();
36215
+ const w = r && Number.isFinite(r.width) ? r.width : 0;
36216
+ const h = r && Number.isFinite(r.height) ? r.height : 0;
36217
+ return rectCount > 0 && (w > 0 || h > 0);
36218
+ } catch {
36219
+ return false;
36220
+ }
36221
+ }
36222
+
36223
+ function toEligibility(info) {
36224
+ return {
36225
+ eligible: !!(info && info.eligible),
36226
+ reasons: info && Array.isArray(info.reasons) ? info.reasons.slice(0) : []
36227
+ };
36228
+ }
36229
+
36230
+ const geometrySupported = hasReliableGeometrySupport();
36231
+
36232
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]') : helpers.queryAll('a[href]');
36089
36233
 
36090
36234
  const occurrences = [];
36091
36235
  let applicableCount = 0;
@@ -36123,7 +36267,63 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36123
36267
  }
36124
36268
  }
36125
36269
 
36126
- if (target) continue;
36270
+ if (target) {
36271
+ const accEligibility = toEligibility(
36272
+ helpers.getEligibilityInfo
36273
+ ? helpers.getEligibilityInfo(target, ctx, { targetSet: 'acc' })
36274
+ : (helpers.isAccTreeEligible ? helpers.isAccTreeEligible(target, ctx) : { eligible: true, reasons: [] })
36275
+ );
36276
+
36277
+ let geometryEligibility = null;
36278
+ let geometryReasonCode = null;
36279
+ if (geometrySupported && helpers.isDomVisibleEligible) {
36280
+ geometryEligibility = toEligibility(
36281
+ helpers.isDomVisibleEligible(target, ctx, { visibilityMode: 'styleAndGeometry', ignoreOpacity: true })
36282
+ );
36283
+ if (!geometryEligibility.eligible && geometryEligibility.reasons.includes('noClientRects')) {
36284
+ geometryReasonCode = 'NO_CLIENT_RECTS';
36285
+ } else if (!geometryEligibility.eligible && geometryEligibility.reasons.includes('zeroArea')) {
36286
+ geometryReasonCode = 'ZERO_AREA_TARGET';
36287
+ }
36288
+ }
36289
+
36290
+ const unusableByAcc = !accEligibility.eligible;
36291
+ const unusableByGeometry = !!geometryReasonCode;
36292
+ if (!unusableByAcc && !unusableByGeometry) continue;
36293
+
36294
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
36295
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
36296
+
36297
+ occurrences.push({
36298
+ selector: stableSelector,
36299
+ html,
36300
+ summary: 'This skip link points to a target that exists but is not currently usable.',
36301
+ hint: 'Point this skip link to a target that is exposed and usable as a navigation destination.',
36302
+ i18n: {
36303
+ summaryKey: 'skipLink_summary_unusableTarget_cantTell',
36304
+ hintKey: 'skipLink_hint_unusableTarget_cantTell',
36305
+ params: { href }
36306
+ },
36307
+ data: {
36308
+ details: {
36309
+ reasonCode: 'SKIP_LINK_TARGET_UNUSABLE',
36310
+ href,
36311
+ unusableReasonCode: unusableByAcc ? 'ACC_TREE_INELIGIBLE' : geometryReasonCode,
36312
+ targetSelector: helpers.buildSelector ? helpers.buildSelector(target) : null,
36313
+ geometryCheckEnabled: geometrySupported
36314
+ },
36315
+ visibilityFilter: {
36316
+ targetSet: 'acc',
36317
+ accEligible: accEligibility.eligible,
36318
+ reasons: accEligibility.reasons
36319
+ },
36320
+ targetGeometry: geometryEligibility
36321
+ ? { eligible: geometryEligibility.eligible, reasons: geometryEligibility.reasons }
36322
+ : { eligible: null, reasons: [] }
36323
+ }
36324
+ });
36325
+ continue;
36326
+ }
36127
36327
 
36128
36328
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
36129
36329
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
@@ -36153,11 +36353,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36153
36353
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
36154
36354
  }), applicability: null },
36155
36355
  "slider-name-present": { run: (function runInPage(ctx) {
36156
- const { document, root, helpers, rule } = ctx;
36356
+ const { document, helpers, rule } = ctx;
36157
36357
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
36158
36358
  ? helpers.getEligibilityInfo
36159
36359
  : null;
36160
- const safeRoot = root || document;
36161
36360
 
36162
36361
 
36163
36362
  function normalizeWs(s) {
@@ -36289,7 +36488,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36289
36488
  let applicableCount = 0;
36290
36489
 
36291
36490
  const selector = 'input[type="range"], [role="slider"]';
36292
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
36491
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
36293
36492
 
36294
36493
 
36295
36494
  // Precompute label[for] map for native range inputs.
@@ -36403,11 +36602,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36403
36602
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36404
36603
  }), applicability: null },
36405
36604
  "spinbutton-name-present": { run: (function runInPage(ctx) {
36406
- const { document, root, helpers, rule } = ctx;
36605
+ const { document, helpers, rule } = ctx;
36407
36606
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
36408
36607
  ? helpers.getEligibilityInfo
36409
36608
  : null;
36410
- const safeRoot = root || document;
36411
36609
 
36412
36610
 
36413
36611
  function normalizeWs(s) {
@@ -36512,7 +36710,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36512
36710
  let applicableCount = 0;
36513
36711
 
36514
36712
  const selector = "[role=\"spinbutton\"]";
36515
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
36713
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
36516
36714
 
36517
36715
  // Precompute label[for] map for spinbutton elements that are labelable
36518
36716
  // native form controls (e.g. <input role="spinbutton">).
@@ -36613,11 +36811,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36613
36811
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36614
36812
  }), applicability: null },
36615
36813
  "summary-name-present": { run: (function runInPage(ctx) {
36616
- const { document, root, helpers, rule } = ctx;
36814
+ const { document, helpers, rule } = ctx;
36617
36815
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
36618
36816
  ? helpers.getEligibilityInfo
36619
36817
  : null;
36620
- const safeRoot = root || document;
36621
36818
 
36622
36819
 
36623
36820
  function normalizeWs(s) {
@@ -36680,7 +36877,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36680
36877
  const occurrences = [];
36681
36878
  let applicableCount = 0;
36682
36879
 
36683
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary', safeRoot) : helpers.queryAll('summary', safeRoot);
36880
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary') : helpers.queryAll('summary');
36684
36881
 
36685
36882
  function evaluate(el) {
36686
36883
  const ariaLabel = getAttr(el, 'aria-label');
@@ -37319,11 +37516,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37319
37516
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
37320
37517
  }), applicability: null },
37321
37518
  "tab-name-present": { run: (function runInPage(ctx) {
37322
- const { document, root, helpers, rule } = ctx;
37519
+ const { document, helpers, rule } = ctx;
37323
37520
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
37324
37521
  ? helpers.getEligibilityInfo
37325
37522
  : null;
37326
- const safeRoot = root || document;
37327
37523
 
37328
37524
 
37329
37525
  function normalizeWs(s) {
@@ -37429,7 +37625,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37429
37625
  let applicableCount = 0;
37430
37626
 
37431
37627
  const selector = '[role="tab"]';
37432
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
37628
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
37433
37629
 
37434
37630
 
37435
37631
  function evaluate(el) {
@@ -37544,14 +37740,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37544
37740
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
37545
37741
  }), applicability: null },
37546
37742
  "table-duplicate-name": { run: (function runInPage(ctx) {
37547
- const { document, root, helpers, rule } = ctx;
37548
- const safeRoot = root || document;
37743
+ const { document, helpers, rule } = ctx;
37549
37744
 
37550
37745
  function normalizeWs(s) {
37551
37746
  return String(s || '').replace(/\s+/g, ' ').trim();
37552
37747
  }
37553
37748
 
37554
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]', safeRoot) : helpers.queryAll('table[summary]', safeRoot);
37749
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]') : helpers.queryAll('table[summary]');
37555
37750
 
37556
37751
  const occurrences = [];
37557
37752
  let applicableCount = 0;
@@ -37597,12 +37792,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37597
37792
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
37598
37793
  }), applicability: null },
37599
37794
  "table-fake-caption": { run: (function runInPage(ctx) {
37600
- const { document, root, helpers, rule } = ctx;
37601
- const safeRoot = root || document;
37795
+ const { document, helpers, rule } = ctx;
37602
37796
 
37603
37797
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
37604
37798
 
37605
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
37799
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
37606
37800
 
37607
37801
  const occurrences = [];
37608
37802
  let applicableCount = 0;
@@ -37665,10 +37859,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37665
37859
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
37666
37860
  }), applicability: null },
37667
37861
  "table-headers-attr-valid": { run: (function runInPage(ctx) {
37668
- const { document, root, helpers, rule } = ctx;
37669
- const safeRoot = root || document;
37862
+ const { document, helpers, rule } = ctx;
37670
37863
 
37671
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('td[headers], th[headers]', safeRoot) : helpers.queryAll('td[headers], th[headers]', safeRoot);
37864
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('td[headers], th[headers]') : helpers.queryAll('td[headers], th[headers]');
37672
37865
 
37673
37866
  const occurrences = [];
37674
37867
  let applicableCount = 0;
@@ -37742,10 +37935,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37742
37935
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
37743
37936
  }), applicability: null },
37744
37937
  "table-th-has-data-cells": { run: (function runInPage(ctx) {
37745
- const { document, root, helpers, rule } = ctx;
37746
- const safeRoot = root || document;
37938
+ const { document, helpers, rule } = ctx;
37747
37939
 
37748
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
37940
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
37749
37941
 
37750
37942
  const occurrences = [];
37751
37943
  let applicableCount = 0;
@@ -38304,14 +38496,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38304
38496
  return { ruleId: RULE_ID, outcome: 'pass', severity: 'minor', occurrences: [] };
38305
38497
  }), applicability: null },
38306
38498
  "td-has-header": { run: (function runInPage(ctx) {
38307
- const { document, root, helpers, rule } = ctx;
38308
- const safeRoot = root || document;
38499
+ const { document, helpers, rule } = ctx;
38309
38500
 
38310
38501
  const MIN_SIZE = 4;
38311
38502
 
38312
38503
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
38313
38504
 
38314
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
38505
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
38315
38506
 
38316
38507
  const occurrences = [];
38317
38508
  let applicableCount = 0;
@@ -38397,11 +38588,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38397
38588
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38398
38589
  }), applicability: null },
38399
38590
  "textbox-name-present": { run: (function runInPage(ctx) {
38400
- const { document, root, helpers, rule } = ctx;
38591
+ const { document, helpers, rule } = ctx;
38401
38592
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
38402
38593
  ? helpers.getEligibilityInfo
38403
38594
  : null;
38404
- const safeRoot = root || document;
38405
38595
 
38406
38596
 
38407
38597
  function normalizeWs(s) {
@@ -38506,7 +38696,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38506
38696
  let applicableCount = 0;
38507
38697
 
38508
38698
  const selector = "[role=\"textbox\"]";
38509
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
38699
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
38510
38700
 
38511
38701
  // Precompute label[for] map for textbox elements that are labelable
38512
38702
  // native form controls (e.g. <input role="textbox">).
@@ -38607,11 +38797,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38607
38797
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38608
38798
  }), applicability: null },
38609
38799
  "tooltip-name-present": { run: (function runInPage(ctx) {
38610
- const { document, root, helpers, rule } = ctx;
38800
+ const { document, helpers, rule } = ctx;
38611
38801
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
38612
38802
  ? helpers.getEligibilityInfo
38613
38803
  : null;
38614
- const safeRoot = root || document;
38615
38804
 
38616
38805
 
38617
38806
  function normalizeWs(s) {
@@ -38675,7 +38864,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38675
38864
  let applicableCount = 0;
38676
38865
 
38677
38866
  const selector = '[role="tooltip"]';
38678
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
38867
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
38679
38868
 
38680
38869
 
38681
38870
  function evaluate(el) {
@@ -38739,11 +38928,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38739
38928
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38740
38929
  }), applicability: null },
38741
38930
  "treeitem-name-present": { run: (function runInPage(ctx) {
38742
- const { document, root, helpers, rule } = ctx;
38931
+ const { document, helpers, rule } = ctx;
38743
38932
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
38744
38933
  ? helpers.getEligibilityInfo
38745
38934
  : null;
38746
- const safeRoot = root || document;
38747
38935
 
38748
38936
 
38749
38937
  function normalizeWs(s) {
@@ -38807,7 +38995,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38807
38995
  let applicableCount = 0;
38808
38996
 
38809
38997
  const selector = "[role=\"treeitem\"]";
38810
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
38998
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
38811
38999
 
38812
39000
 
38813
39001
  function hasName(el) {
@@ -38870,12 +39058,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38870
39058
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38871
39059
  }), applicability: null },
38872
39060
  "valid-lang": { run: (function runInPage(ctx) {
38873
- const { document, root, helpers, rule } = ctx;
38874
- const safeRoot = root || document;
39061
+ const { document, helpers, rule } = ctx;
38875
39062
 
38876
39063
  const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
38877
39064
 
38878
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]', safeRoot) : helpers.queryAll('[lang]', safeRoot);
39065
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]') : helpers.queryAll('[lang]');
38879
39066
 
38880
39067
  const occurrences = [];
38881
39068
  let applicableCount = 0;
@@ -38920,10 +39107,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38920
39107
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38921
39108
  }), applicability: null },
38922
39109
  "video-caption": { run: (function runInPage(ctx) {
38923
- const { document, root, helpers, rule } = ctx;
38924
- const safeRoot = root || document;
39110
+ const { document, helpers, rule } = ctx;
38925
39111
 
38926
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video', safeRoot) : helpers.queryAll('video', safeRoot);
39112
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video') : helpers.queryAll('video');
38927
39113
 
38928
39114
  const occurrences = [];
38929
39115
  let applicableCount = 0;
@@ -39645,10 +39831,12 @@ const I18N = {
39645
39831
  "region_description": "Checks that direct children of <body> with visible text content are contained within a landmark region.",
39646
39832
  "region_summary_cantTell": "This content is not contained within a landmark region.",
39647
39833
  "region_hint_cantTell": "Move this content inside a landmark region (main, nav, aside, a labeled section, etc.).",
39648
- "skipLink_title": "Skip link must have a resolvable target",
39649
- "skipLink_description": "Checks that a \"skip to ...\" link's href fragment resolves to a real element in the document.",
39834
+ "skipLink_title": "Skip link must have a resolvable, usable target",
39835
+ "skipLink_description": "Checks that a \"skip to ...\" link's href fragment resolves to a real, currently usable element in the document.",
39650
39836
  "skipLink_summary_cantTell": "This skip link's target does not exist.",
39651
39837
  "skipLink_hint_cantTell": "Point the skip link's href at an id that exists in the document, or add the missing target element.",
39838
+ "skipLink_summary_unusableTarget_cantTell": "This skip link points to a target that exists but is not currently usable.",
39839
+ "skipLink_hint_unusableTarget_cantTell": "Point this skip link to a target that is exposed and usable as a navigation destination.",
39652
39840
  "autocompleteValid_title": "autocomplete attribute must be a valid autofill value",
39653
39841
  "autocompleteValid_description": "Checks that a non-empty autocomplete attribute is \"on\"/\"off\" or a well-formed autofill detail token list.",
39654
39842
  "autocompleteValid_summary_fail": "This autocomplete attribute value is not a valid autofill value.",
@@ -40247,10 +40435,12 @@ const I18N = {
40247
40435
  "region_description": "Vérifie que les enfants directs de <body> ayant un contenu textuel visible sont contenus dans un point de repère.",
40248
40436
  "region_summary_cantTell": "Ce contenu n’est contenu dans aucun point de repère.",
40249
40437
  "region_hint_cantTell": "Déplacez ce contenu à l’intérieur d’un point de repère (main, nav, aside, une section nommée, etc.).",
40250
- "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout",
40251
- "skipLink_description": "Vérifie que le fragment href d’un lien « aller au... » se résout vers un élément réel du document.",
40438
+ "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout et qui est utilisable",
40439
+ "skipLink_description": "Vérifie que le fragment href d’un lien « aller au... » se résout vers un élément réel et actuellement utilisable du document.",
40252
40440
  "skipLink_summary_cantTell": "La cible de ce lien d’évitement n’existe pas.",
40253
40441
  "skipLink_hint_cantTell": "Faites pointer le href du lien d’évitement vers un id qui existe dans le document, ou ajoutez l’élément cible manquant.",
40442
+ "skipLink_summary_unusableTarget_cantTell": "Ce lien d’évitement pointe vers une cible qui existe mais qui n’est pas actuellement utilisable.",
40443
+ "skipLink_hint_unusableTarget_cantTell": "Faites pointer ce lien d’évitement vers une cible exposée et utilisable comme destination de navigation.",
40254
40444
  "autocompleteValid_title": "L’attribut autocomplete doit être une valeur d’auto-remplissage valide",
40255
40445
  "autocompleteValid_description": "Vérifie qu’un attribut autocomplete non vide vaut « on »/« off » ou une liste de jetons d’auto-remplissage bien formée.",
40256
40446
  "autocompleteValid_summary_fail": "Cette valeur d’attribut autocomplete n’est pas une valeur d’auto-remplissage valide.",
@@ -43056,6 +43246,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
43056
43246
  })();
43057
43247
  // Default on: opt OUT with `includeShadowDom: false`, not opt in.
43058
43248
  const includeShadowDom = !(opts && opts.includeShadowDom === false);
43249
+ // Default off: by default, helper queries skip structurally/CSS-hidden
43250
+ // subtrees (display:none, [hidden], closed <details>, etc.). Callers can
43251
+ // opt out with includeHiddenElements:true.
43252
+ const includeHiddenElements = !!(opts && opts.includeHiddenElements === true);
43059
43253
  const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
43060
43254
 
43061
43255
  // Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
@@ -44022,8 +44216,55 @@ const createDomHelpers = (function createDomHelpers(opts) {
44022
44216
  return results;
44023
44217
  }
44024
44218
 
44219
+ const HARD_HIDDEN_REASONS = new Set([
44220
+ 'displayNone',
44221
+ 'hiddenAttr',
44222
+ 'detailsClosed',
44223
+ 'templateContent',
44224
+ 'nonRenderedElement',
44225
+ 'inputHidden',
44226
+ 'visibilityHidden',
44227
+ 'contentVisibilityHidden'
44228
+ ]);
44229
+
44025
44230
  function queryAllSmart(sel) {
44026
- const list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
44231
+ let list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
44232
+
44233
+ // Global hidden-content policy: skip nodes that are fully excluded from
44234
+ // rendered visibility by default (unless includeHiddenElements:true).
44235
+ if (!includeHiddenElements) {
44236
+ list = list.filter((el) => {
44237
+ try {
44238
+ const vis = isAccTreeEligible(el);
44239
+ if (!vis || vis.eligible !== false) return true;
44240
+ const reasons = Array.isArray(vis.reasons) ? vis.reasons : [];
44241
+ for (const r of reasons) {
44242
+ if (HARD_HIDDEN_REASONS.has(r)) return false;
44243
+ }
44244
+
44245
+ // `isAccTreeEligible` can short-circuit on an inert ancestor
44246
+ // before it reaches an outer hard-hidden ancestor (e.g.
44247
+ // display:none wrapper). In that case the node is still
44248
+ // structurally hidden and should be excluded by the default
44249
+ // hidden-content policy.
44250
+ if (reasons.includes('inert')) {
44251
+ const domVis = isDomVisibleEligible(el, null, {
44252
+ visibilityMode: 'styleOnly',
44253
+ disableGeometry: true,
44254
+ ignoreOpacity: true
44255
+ });
44256
+ const domReasons = Array.isArray(domVis && domVis.reasons) ? domVis.reasons : [];
44257
+ for (const r of domReasons) {
44258
+ if (HARD_HIDDEN_REASONS.has(r)) return false;
44259
+ }
44260
+ }
44261
+ return true;
44262
+ } catch {
44263
+ return true;
44264
+ }
44265
+ });
44266
+ }
44267
+
44027
44268
  return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
44028
44269
  }
44029
44270
 
@@ -46593,6 +46834,36 @@ const createDomHelpers = (function createDomHelpers(opts) {
46593
46834
  let node = el;
46594
46835
  let safety = 0;
46595
46836
 
46837
+ // Only apply the "stop climbing once we reach a contextSelector-
46838
+ // matched root" shortcut when there's a single (or no) matched
46839
+ // root -- resolveContextRoots() falls back to `[documentElement]`
46840
+ // when no contextSelector is given, so this is the overwhelmingly
46841
+ // common case and behaves exactly as before.
46842
+ //
46843
+ // With MULTIPLE matched roots (multi-region contextSelector
46844
+ // scans), stopping there without recording anything about which
46845
+ // root produced an ambiguous, non-unique selector string for two
46846
+ // structurally-identical regions -- a real, confirmed bug (found
46847
+ // 2026-07-29 via the cross-engine comparisons project): two
46848
+ // wrapper <div>s, each containing two identical ".widget"
46849
+ // sections scanned via `contextSelector: '.widget'`, produced the
46850
+ // *same* selector string ("section:nth-of-type(1) > div > div >
46851
+ // button") for the equivalent button in each wrapper --
46852
+ // resolving to 2 elements instead of 1 when queried, and pointing
46853
+ // at the wrong one for at least one of the two occurrences. The
46854
+ // existing `el.matches(candidate)` safety check below couldn't
46855
+ // catch this: it only verifies THIS element matches the string,
46856
+ // never that the string is unique document-wide.
46857
+ //
46858
+ // Fix: when multiple roots are in play, don't stop early --
46859
+ // keep climbing (same as the always-correct no-contextSelector
46860
+ // path) until finding a genuinely unique anchor or reaching the
46861
+ // true document root, which is always singular. That restores
46862
+ // the invariant the final safety-check comment below relies on,
46863
+ // rather than needing a separate (more expensive) document-wide
46864
+ // uniqueness re-check.
46865
+ const stopAtMatchedRoot = roots.length <= 1;
46866
+
46596
46867
  while (node && node.nodeType === 1 && safety++ < 20) {
46597
46868
  let anchor = null;
46598
46869
 
@@ -46632,7 +46903,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
46632
46903
  parts.unshift(nthOfType(node));
46633
46904
  }
46634
46905
 
46635
- if (!node.parentElement || roots.includes(node)) break;
46906
+ if (!node.parentElement || (stopAtMatchedRoot && roots.includes(node))) break;
46636
46907
  node = node.parentElement;
46637
46908
  }
46638
46909
 
@@ -46651,7 +46922,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
46651
46922
  // position relative to its own parent via `>` (child, not
46652
46923
  // descendant) combinators, so a correctly-matching chain can
46653
46924
  // only resolve to one element short of a malformed document
46654
- // (e.g. two <html> roots). Re-deriving that guarantee via a
46925
+ // (e.g. two <html> roots) -- true as long as the walk above
46926
+ // never stops short of a genuinely unique anchor/root, which is
46927
+ // exactly what `stopAtMatchedRoot` now guarantees (see its own
46928
+ // comment above; a multi-root contextSelector scan stopping
46929
+ // early used to violate this invariant silently). Re-deriving
46930
+ // that guarantee via a
46655
46931
  // document-wide :nth-of-type scan was measured to cost O(total
46656
46932
  // same-tag siblings) per call — pathological on pages with many
46657
46933
  // flat, unidentified siblings (e.g. hundreds of unlabeled
@@ -47218,6 +47494,9 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
47218
47494
 
47219
47495
  // Default on: opt OUT with `includeShadowDom: false`, not opt in.
47220
47496
  const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
47497
+ // Default off: hidden/collapsed content is excluded from rule evaluation
47498
+ // unless the caller explicitly opts in.
47499
+ const includeHiddenElements = !!(engineOptionsResolved && engineOptionsResolved.includeHiddenElements === true);
47221
47500
  const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
47222
47501
 
47223
47502
  const url = pageUrl || (document.location && document.location.href) || null;
@@ -47228,11 +47507,27 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
47228
47507
  ? engineOptionsResolved.timestamp.trim()
47229
47508
  : null;
47230
47509
 
47510
+ // createDomHelpers()/createContrastHelpers() persist their element-keyed
47511
+ // caches (outerHtmlCache, selectorCache, etc.) on window.__a11ycoreSharedCache
47512
+ // so multiple helper instances created *within this run* can share them
47513
+ // deterministically. But a window/document is frequently reused across
47514
+ // SEPARATE runs -- e.g. Jest's jsdom environment creates one window per
47515
+ // test file, and mutating document.body between it() blocks is standard.
47516
+ // Those caches are keyed by element reference, not content, so a run that
47517
+ // reuses an already-cached element (document.body never changes identity)
47518
+ // would otherwise read stale data cached by an earlier, unrelated run on
47519
+ // the same window. Clearing at the start of every run keeps sharing scoped
47520
+ // to "this run" as intended, without leaking across runs.
47521
+ try {
47522
+ if (window && window.__a11ycoreSharedCache) window.__a11ycoreSharedCache = {};
47523
+ } catch {}
47524
+
47231
47525
  const sharedHelpers = createDomHelpers({
47232
47526
  document,
47233
47527
  window,
47234
47528
  root: roots,
47235
47529
  includeShadowDom,
47530
+ includeHiddenElements,
47236
47531
  excludeSelectors,
47237
47532
  // Optional perf counters (bench/debug only). Deterministic and per-run.
47238
47533
  perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)
@@ -53296,8 +53591,8 @@ const __a11yCoreCrossFrameApi = (function () {
53296
53591
  },
53297
53592
  {
53298
53593
  "ruleId": "skip-link",
53299
- "title": "Skip link must have a resolvable target",
53300
- "description": "Checks that a \"skip to ...\" link's href fragment resolves to a real element in the document.",
53594
+ "title": "Skip link must have a resolvable, usable target",
53595
+ "description": "Checks that a \"skip to ...\" link's href fragment resolves to a real, currently usable element in the document.",
53301
53596
  "i18n": {
53302
53597
  "titleKey": "skipLink_title",
53303
53598
  "descriptionKey": "skipLink_description"
@@ -54884,13 +55179,7 @@ const __a11yCoreCrossFrameApi = (function () {
54884
55179
  const RULE_IMPLS = {
54885
55180
  "accesskeys": { run: (function runInPage(ctx) {
54886
55181
  const { document, helpers, rule } = ctx;
54887
-
54888
- let nodes = [];
54889
- try {
54890
- nodes = document.querySelectorAll('[accesskey]');
54891
- } catch {
54892
- nodes = [];
54893
- }
55182
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[accesskey]') : helpers.queryAll('[accesskey]');
54894
55183
 
54895
55184
  const groups = new Map(); // normalized key -> elements[]
54896
55185
  for (const el of nodes) {
@@ -55481,8 +55770,7 @@ if (isAccTreeEligible) {
55481
55770
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
55482
55771
  }), applicability: null },
55483
55772
  "aria-allowed-attr": { run: (function runInPage(ctx) {
55484
- const { document, root, helpers, rule } = ctx;
55485
- const safeRoot = root || document;
55773
+ const { document, helpers, rule } = ctx;
55486
55774
 
55487
55775
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
55488
55776
  if (!ariaHelpers) {
@@ -55546,7 +55834,7 @@ if (isAccTreeEligible) {
55546
55834
  };
55547
55835
 
55548
55836
  const globalSet = new Set(GLOBAL_ATTRS);
55549
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
55837
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
55550
55838
 
55551
55839
  const occurrences = [];
55552
55840
  let applicableCount = 0;
@@ -55611,15 +55899,14 @@ if (isAccTreeEligible) {
55611
55899
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55612
55900
  }), applicability: null },
55613
55901
  "aria-allowed-role": { run: (function runInPage(ctx) {
55614
- const { document, root, helpers, rule } = ctx;
55615
- const safeRoot = root || document;
55902
+ const { document, helpers, rule } = ctx;
55616
55903
 
55617
55904
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
55618
55905
  if (!ariaHelpers) {
55619
55906
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
55620
55907
  }
55621
55908
 
55622
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
55909
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
55623
55910
 
55624
55911
  const occurrences = [];
55625
55912
  let applicableCount = 0;
@@ -55666,8 +55953,7 @@ if (isAccTreeEligible) {
55666
55953
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55667
55954
  }), applicability: null },
55668
55955
  "aria-braille-equivalent": { run: (function runInPage(ctx) {
55669
- const { document, root, helpers, rule } = ctx;
55670
- const safeRoot = root || document;
55956
+ const { document, helpers, rule } = ctx;
55671
55957
 
55672
55958
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
55673
55959
 
@@ -55688,8 +55974,8 @@ if (isAccTreeEligible) {
55688
55974
  }
55689
55975
 
55690
55976
  const nodes = helpers.queryAllSmart
55691
- ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]', safeRoot)
55692
- : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]', safeRoot);
55977
+ ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]')
55978
+ : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]');
55693
55979
 
55694
55980
  const occurrences = [];
55695
55981
  let applicableCount = 0;
@@ -55750,14 +56036,13 @@ if (isAccTreeEligible) {
55750
56036
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55751
56037
  }), applicability: null },
55752
56038
  "aria-checked-state-mismatch": { run: (function runInPage(ctx) {
55753
- const { document, root, helpers, rule } = ctx;
55754
- const safeRoot = root || document;
56039
+ const { document, helpers, rule } = ctx;
55755
56040
 
55756
56041
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
55757
56042
 
55758
56043
  const nodes = helpers.queryAllSmart
55759
- ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]', safeRoot)
55760
- : helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]', safeRoot);
56044
+ ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]')
56045
+ : helpers.queryAll('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]');
55761
56046
 
55762
56047
  const occurrences = [];
55763
56048
  let applicableCount = 0;
@@ -55819,16 +56104,15 @@ if (isAccTreeEligible) {
55819
56104
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
55820
56105
  }), applicability: null },
55821
56106
  "aria-conditional-attr": { run: (function runInPage(ctx) {
55822
- const { document, root, helpers, rule } = ctx;
55823
- const safeRoot = root || document;
56107
+ const { document, helpers, rule } = ctx;
55824
56108
 
55825
56109
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
55826
56110
 
55827
56111
  const TRUTHY_INVALID_VALUES = new Set(['true', 'grammar', 'spelling']);
55828
56112
 
55829
56113
  const nodes = helpers.queryAllSmart
55830
- ? helpers.queryAllSmart('[aria-errormessage]', safeRoot)
55831
- : helpers.queryAll('[aria-errormessage]', safeRoot);
56114
+ ? helpers.queryAllSmart('[aria-errormessage]')
56115
+ : helpers.queryAll('[aria-errormessage]');
55832
56116
 
55833
56117
  const occurrences = [];
55834
56118
  let applicableCount = 0;
@@ -55873,15 +56157,14 @@ if (isAccTreeEligible) {
55873
56157
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55874
56158
  }), applicability: null },
55875
56159
  "aria-deprecated-role": { run: (function runInPage(ctx) {
55876
- const { document, root, helpers, rule } = ctx;
55877
- const safeRoot = root || document;
56160
+ const { document, helpers, rule } = ctx;
55878
56161
 
55879
56162
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
55880
56163
  if (!ariaHelpers) {
55881
56164
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
55882
56165
  }
55883
56166
 
55884
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56167
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
55885
56168
 
55886
56169
  const occurrences = [];
55887
56170
  let applicableCount = 0;
@@ -56393,8 +56676,7 @@ if (isAccTreeEligible) {
56393
56676
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
56394
56677
  }), applicability: null },
56395
56678
  "aria-prohibited-attr": { run: (function runInPage(ctx) {
56396
- const { document, root, helpers, rule } = ctx;
56397
- const safeRoot = root || document;
56679
+ const { document, helpers, rule } = ctx;
56398
56680
 
56399
56681
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56400
56682
  if (!ariaHelpers) {
@@ -56414,7 +56696,7 @@ if (isAccTreeEligible) {
56414
56696
 
56415
56697
  const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
56416
56698
 
56417
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56699
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56418
56700
 
56419
56701
  const occurrences = [];
56420
56702
  let applicableCount = 0;
@@ -56465,8 +56747,7 @@ if (isAccTreeEligible) {
56465
56747
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56466
56748
  }), applicability: null },
56467
56749
  "aria-prohibited-children": { run: (function runInPage(ctx) {
56468
- const { document, root, helpers, rule } = ctx;
56469
- const safeRoot = root || document;
56750
+ const { document, helpers, rule } = ctx;
56470
56751
 
56471
56752
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56472
56753
  if (!ariaHelpers) {
@@ -56555,7 +56836,7 @@ if (isAccTreeEligible) {
56555
56836
  }
56556
56837
  }
56557
56838
 
56558
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56839
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56559
56840
 
56560
56841
  const occurrences = [];
56561
56842
  let applicableCount = 0;
@@ -56627,8 +56908,7 @@ if (isAccTreeEligible) {
56627
56908
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56628
56909
  }), applicability: null },
56629
56910
  "aria-required-attr": { run: (function runInPage(ctx) {
56630
- const { document, root, helpers, rule } = ctx;
56631
- const safeRoot = root || document;
56911
+ const { document, helpers, rule } = ctx;
56632
56912
 
56633
56913
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56634
56914
  if (!ariaHelpers) {
@@ -56652,7 +56932,7 @@ if (isAccTreeEligible) {
56652
56932
  return v != null && String(v).trim().toLowerCase() === 'true';
56653
56933
  }
56654
56934
 
56655
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56935
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56656
56936
 
56657
56937
  const occurrences = [];
56658
56938
  let applicableCount = 0;
@@ -56709,8 +56989,7 @@ if (isAccTreeEligible) {
56709
56989
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56710
56990
  }), applicability: null },
56711
56991
  "aria-required-children": { run: (function runInPage(ctx) {
56712
- const { document, root, helpers, rule } = ctx;
56713
- const safeRoot = root || document;
56992
+ const { document, helpers, rule } = ctx;
56714
56993
 
56715
56994
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56716
56995
  if (!ariaHelpers) {
@@ -56741,7 +57020,7 @@ if (isAccTreeEligible) {
56741
57020
  // ("runInPage MUST be self-contained").
56742
57021
  const CANDIDATE_SELECTOR = '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
56743
57022
 
56744
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
57023
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56745
57024
 
56746
57025
  const occurrences = [];
56747
57026
  let applicableCount = 0;
@@ -56900,8 +57179,7 @@ if (isAccTreeEligible) {
56900
57179
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56901
57180
  }), applicability: null },
56902
57181
  "aria-required-parent": { run: (function runInPage(ctx) {
56903
- const { document, root, helpers, rule } = ctx;
56904
- const safeRoot = root || document;
57182
+ const { document, helpers, rule } = ctx;
56905
57183
 
56906
57184
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56907
57185
  if (!ariaHelpers) {
@@ -56987,7 +57265,7 @@ if (isAccTreeEligible) {
56987
57265
  const idTok = elId && String(elId).trim();
56988
57266
  if (!idTok) return false;
56989
57267
 
56990
- const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]', safeRoot) : helpers.queryAll('[aria-owns]', safeRoot);
57268
+ const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]') : helpers.queryAll('[aria-owns]');
56991
57269
  for (const owner of owners) {
56992
57270
  if (!owner || !owner.getAttribute) continue;
56993
57271
  const ownsAttr = owner.getAttribute('aria-owns') || '';
@@ -57000,7 +57278,7 @@ if (isAccTreeEligible) {
57000
57278
  return false;
57001
57279
  }
57002
57280
 
57003
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
57281
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
57004
57282
 
57005
57283
  const occurrences = [];
57006
57284
  let applicableCount = 0;
@@ -57213,15 +57491,14 @@ if (isAccTreeEligible) {
57213
57491
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
57214
57492
  }), applicability: null },
57215
57493
  "aria-roles-valid": { run: (function runInPage(ctx) {
57216
- const { document, root, helpers, rule } = ctx;
57217
- const safeRoot = root || document;
57494
+ const { document, helpers, rule } = ctx;
57218
57495
 
57219
57496
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
57220
57497
  if (!ariaHelpers) {
57221
57498
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57222
57499
  }
57223
57500
 
57224
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
57501
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
57225
57502
 
57226
57503
  const occurrences = [];
57227
57504
  let applicableCount = 0;
@@ -57276,13 +57553,12 @@ if (isAccTreeEligible) {
57276
57553
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57277
57554
  }), applicability: null },
57278
57555
  "aria-text": { run: (function runInPage(ctx) {
57279
- const { document, root, helpers, rule } = ctx;
57280
- const safeRoot = root || document;
57556
+ const { document, helpers, rule } = ctx;
57281
57557
 
57282
57558
  const FOCUSABLE_DESCENDANT_SELECTOR =
57283
57559
  'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
57284
57560
 
57285
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]', safeRoot) : helpers.queryAll('[role="text"]', safeRoot);
57561
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role="text"]') : helpers.queryAll('[role="text"]');
57286
57562
 
57287
57563
  const occurrences = [];
57288
57564
  let applicableCount = 0;
@@ -57336,15 +57612,14 @@ if (isAccTreeEligible) {
57336
57612
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57337
57613
  }), applicability: null },
57338
57614
  "aria-valid-attr": { run: (function runInPage(ctx) {
57339
- const { document, root, helpers, rule } = ctx;
57340
- const safeRoot = root || document;
57615
+ const { document, helpers, rule } = ctx;
57341
57616
 
57342
57617
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
57343
57618
  if (!ariaHelpers) {
57344
57619
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57345
57620
  }
57346
57621
 
57347
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
57622
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
57348
57623
 
57349
57624
  const occurrences = [];
57350
57625
  let applicableCount = 0;
@@ -57398,15 +57673,14 @@ if (isAccTreeEligible) {
57398
57673
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57399
57674
  }), applicability: null },
57400
57675
  "aria-valid-attr-value": { run: (function runInPage(ctx) {
57401
- const { document, root, helpers, rule } = ctx;
57402
- const safeRoot = root || document;
57676
+ const { document, helpers, rule } = ctx;
57403
57677
 
57404
57678
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
57405
57679
  if (!ariaHelpers) {
57406
57680
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57407
57681
  }
57408
57682
 
57409
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
57683
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
57410
57684
 
57411
57685
  const occurrences = [];
57412
57686
  let applicableCount = 0;
@@ -57463,8 +57737,7 @@ if (isAccTreeEligible) {
57463
57737
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57464
57738
  }), applicability: null },
57465
57739
  "autocomplete-valid": { run: (function runInPage(ctx) {
57466
- const { document, root, helpers, rule } = ctx;
57467
- const safeRoot = root || document;
57740
+ const { document, helpers, rule } = ctx;
57468
57741
 
57469
57742
  // Declared inside runInPage — see scripts/build-core.js header
57470
57743
  // ("runInPage MUST be self-contained").
@@ -57499,7 +57772,7 @@ if (isAccTreeEligible) {
57499
57772
  return FIELD_NAMES.has(remaining[0]);
57500
57773
  }
57501
57774
 
57502
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea', safeRoot) : helpers.queryAll('input, select, textarea', safeRoot);
57775
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea') : helpers.queryAll('input, select, textarea');
57503
57776
 
57504
57777
  const occurrences = [];
57505
57778
  let applicableCount = 0;
@@ -57542,12 +57815,11 @@ if (isAccTreeEligible) {
57542
57815
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57543
57816
  }), applicability: null },
57544
57817
  "avoid-inline-spacing": { run: (function runInPage(ctx) {
57545
- const { document, root, helpers, rule } = ctx;
57546
- const safeRoot = root || document;
57818
+ const { document, helpers, rule } = ctx;
57547
57819
 
57548
57820
  const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
57549
57821
 
57550
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]', safeRoot) : helpers.queryAll('[style]', safeRoot);
57822
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]') : helpers.queryAll('[style]');
57551
57823
 
57552
57824
  const occurrences = [];
57553
57825
  let applicableCount = 0;
@@ -57600,11 +57872,10 @@ if (isAccTreeEligible) {
57600
57872
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57601
57873
  }), applicability: null },
57602
57874
  "binary-control-name-present": { run: (function runInPage(ctx) {
57603
- const { document, root, helpers, rule } = ctx;
57875
+ const { document, helpers, rule } = ctx;
57604
57876
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
57605
57877
  ? helpers.getEligibilityInfo
57606
57878
  : null;
57607
- const safeRoot = root || document;
57608
57879
 
57609
57880
 
57610
57881
  function normalizeWs(s) {
@@ -57736,7 +58007,7 @@ if (isAccTreeEligible) {
57736
58007
  let applicableCount = 0;
57737
58008
 
57738
58009
  const selector = 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
57739
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
58010
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
57740
58011
 
57741
58012
 
57742
58013
  // Precompute label[for] associations once for speed/determinism.
@@ -57868,8 +58139,7 @@ if (isAccTreeEligible) {
57868
58139
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57869
58140
  }), applicability: null },
57870
58141
  "button-name-present": { run: (function runInPage(ctx) {
57871
- const { document, root, helpers, rule } = ctx;
57872
- const safeRoot = root || document;
58142
+ const { document, helpers, rule } = ctx;
57873
58143
 
57874
58144
  const occurrences = [];
57875
58145
  let applicableCount = 0;
@@ -57906,7 +58176,7 @@ if (isAccTreeEligible) {
57906
58176
  }
57907
58177
 
57908
58178
  const selector = 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
57909
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
58179
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
57910
58180
 
57911
58181
  for (const el of nodes) {
57912
58182
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -57918,6 +58188,7 @@ if (isAccTreeEligible) {
57918
58188
 
57919
58189
  const tag = (el.tagName || '').toLowerCase();
57920
58190
  const role = el.getAttribute ? el.getAttribute('role') : null;
58191
+ const roleNorm = normalizeWs(role).toLowerCase();
57921
58192
 
57922
58193
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
57923
58194
 
@@ -57933,7 +58204,17 @@ if (isAccTreeEligible) {
57933
58204
  inputValueName = getInputButtonValueName(el);
57934
58205
  }
57935
58206
 
57936
- const isContentNameCandidate = tag === 'button' || role === 'button';
58207
+ // A native <button> (or [role="button"]) whose role has been overridden to
58208
+ // one of these roles is no longer semantically a button — per the WAI-ARIA
58209
+ // Accessible Name and Description Computation spec these roles are
58210
+ // name-from-author-only, and their rendered content represents a VALUE,
58211
+ // not a NAME (mirrors axe-core's controlValueRoles, verified against its
58212
+ // source). Found on a real page (Spotify's "sort by" control): a
58213
+ // <button role="combobox">List</button> where "List" is the combobox's
58214
+ // currently selected value, not a label for what the combobox is —
58215
+ // crediting it as the accessible name masked a real missing-name bug.
58216
+ const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
58217
+ const isContentNameCandidate = (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
57937
58218
  const contentName =
57938
58219
  (!trustedProgrammaticName && !inputValueName && isContentNameCandidate)
57939
58220
  ? getConservativeSubtreeText(el)
@@ -57993,12 +58274,41 @@ if (isAccTreeEligible) {
57993
58274
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57994
58275
  }
57995
58276
 
57996
- function hasMainLandmark() {
58277
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
58278
+
58279
+ function isExposedToAt(el) {
58280
+ if (!isAccTreeEligible) return true;
57997
58281
  try {
57998
- return !!document.querySelector('main, [role="main"]');
58282
+ const r = isAccTreeEligible(el, ctx);
58283
+ if (typeof r === 'boolean') return r;
58284
+ return !!(r && r.eligible);
57999
58285
  } catch {
58000
- return false;
58286
+ return true;
58287
+ }
58288
+ }
58289
+
58290
+ function queryAll(selector) {
58291
+ try {
58292
+ return helpers && typeof helpers.queryAllSmart === 'function'
58293
+ ? helpers.queryAllSmart(selector)
58294
+ : document.querySelectorAll(selector);
58295
+ } catch {
58296
+ return [];
58297
+ }
58298
+ }
58299
+
58300
+ // Filters through isAccTreeEligible (hidden/aria-hidden/display:none/inert
58301
+ // don't count as "a bypass mechanism is present") -- see page-has-heading-
58302
+ // one-manual.js's identical fix for the real-world trigger (CDC's flu
58303
+ // page, 2026-07-30: its only <h1> sits inside a display:none ancestor,
58304
+ // unreachable by sighted and screen reader users alike). A fully
58305
+ // non-rendered <main>/heading was previously credited here too, wrongly
58306
+ // returning `pass` for a page with zero actual bypass mechanisms.
58307
+ function hasMainLandmark() {
58308
+ for (const el of queryAll('main, [role="main"]')) {
58309
+ if (el && isExposedToAt(el)) return true;
58001
58310
  }
58311
+ return false;
58002
58312
  }
58003
58313
 
58004
58314
  function hasWorkingAnchorLink() {
@@ -58040,11 +58350,10 @@ if (isAccTreeEligible) {
58040
58350
  }
58041
58351
 
58042
58352
  function hasHeading() {
58043
- try {
58044
- return !!document.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
58045
- } catch {
58046
- return false;
58353
+ for (const el of queryAll('h1, h2, h3, h4, h5, h6, [role="heading"]')) {
58354
+ if (el && isExposedToAt(el)) return true;
58047
58355
  }
58356
+ return false;
58048
58357
  }
58049
58358
 
58050
58359
  const mainLandmark = hasMainLandmark();
@@ -58302,11 +58611,10 @@ if (isAccTreeEligible) {
58302
58611
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
58303
58612
  }), applicability: null },
58304
58613
  "combobox-name-present": { run: (function runInPage(ctx) {
58305
- const { document, root, helpers, rule } = ctx;
58614
+ const { document, helpers, rule } = ctx;
58306
58615
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
58307
58616
  ? helpers.getEligibilityInfo
58308
58617
  : null;
58309
- const safeRoot = root || document;
58310
58618
 
58311
58619
 
58312
58620
  function normalizeWs(s) {
@@ -58438,7 +58746,7 @@ if (isAccTreeEligible) {
58438
58746
  let applicableCount = 0;
58439
58747
 
58440
58748
  const selector = '[role="combobox"]';
58441
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
58749
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
58442
58750
 
58443
58751
  // Precompute label[for] map for combobox elements that are labelable
58444
58752
  // native form controls (e.g. <input role="combobox">).
@@ -60165,14 +60473,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60165
60473
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
60166
60474
  }), applicability: null },
60167
60475
  "definition-list-children-valid": { run: (function runInPage(ctx) {
60168
- const { document, root, helpers, rule } = ctx;
60169
- const safeRoot = root || document;
60476
+ const { document, helpers, rule } = ctx;
60170
60477
 
60171
60478
  // Declared inside runInPage — see scripts/build-core.js header
60172
60479
  // ("runInPage MUST be self-contained").
60173
60480
  const PASSTHROUGH_TAGS = new Set(['dt', 'dd', 'script', 'template', 'style']);
60174
60481
 
60175
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl', safeRoot) : helpers.queryAll('dl', safeRoot);
60482
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl') : helpers.queryAll('dl');
60176
60483
 
60177
60484
  const occurrences = [];
60178
60485
  let applicableCount = 0;
@@ -60256,10 +60563,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60256
60563
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
60257
60564
  }), applicability: null },
60258
60565
  "deprecated-elements-not-used": { run: (function runInPage(ctx) {
60259
- const { document, root, helpers, rule } = ctx;
60260
- const safeRoot = root || document;
60566
+ const { document, helpers, rule } = ctx;
60261
60567
 
60262
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee', safeRoot) : helpers.queryAll('blink, marquee', safeRoot);
60568
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee') : helpers.queryAll('blink, marquee');
60263
60569
 
60264
60570
  const occurrences = [];
60265
60571
  let applicableCount = 0;
@@ -60298,11 +60604,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60298
60604
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
60299
60605
  }), applicability: null },
60300
60606
  "dialog-name-present": { run: (function runInPage(ctx) {
60301
- const { document, root, helpers, rule } = ctx;
60607
+ const { document, helpers, rule } = ctx;
60302
60608
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
60303
60609
  ? helpers.getEligibilityInfo
60304
60610
  : null;
60305
- const safeRoot = root || document;
60306
60611
 
60307
60612
 
60308
60613
  function normalizeWs(s) {
@@ -60416,7 +60721,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60416
60721
  let applicableCount = 0;
60417
60722
 
60418
60723
  const selector = '[role="dialog"],[role="alertdialog"]';
60419
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
60724
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
60420
60725
 
60421
60726
 
60422
60727
  function evaluate(el) {
@@ -60481,10 +60786,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60481
60786
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
60482
60787
  }), applicability: null },
60483
60788
  "dlitem-parent-valid": { run: (function runInPage(ctx) {
60484
- const { document, root, helpers, rule } = ctx;
60485
- const safeRoot = root || document;
60789
+ const { document, helpers, rule } = ctx;
60486
60790
 
60487
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd', safeRoot) : helpers.queryAll('dt, dd', safeRoot);
60791
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd') : helpers.queryAll('dt, dd');
60488
60792
 
60489
60793
  const occurrences = [];
60490
60794
  let applicableCount = 0;
@@ -61136,8 +61440,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
61136
61440
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
61137
61441
  }), applicability: null },
61138
61442
  "focus-order-semantics": { run: (function runInPage(ctx) {
61139
- const { document, root, helpers, rule } = ctx;
61140
- const safeRoot = root || document;
61443
+ const { document, helpers, rule } = ctx;
61141
61444
 
61142
61445
  const NON_INTERACTIVE_ROLES = new Set([
61143
61446
  'generic', 'group', 'text', 'presentation', 'none', 'img', 'heading',
@@ -61147,7 +61450,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
61147
61450
  'definition', 'paragraph', 'caption', 'status', 'alert', 'log', 'tooltip'
61148
61451
  ]);
61149
61452
 
61150
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex][role]', safeRoot) : helpers.queryAll('[tabindex][role]', safeRoot);
61453
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[tabindex][role]') : helpers.queryAll('[tabindex][role]');
61151
61454
 
61152
61455
  const occurrences = [];
61153
61456
  let applicableCount = 0;
@@ -61788,11 +62091,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
61788
62091
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [], data: { details: { metrics } } };
61789
62092
  }), applicability: null },
61790
62093
  "form-control-single-label": { run: (function runInPage(ctx) {
61791
- const { document, root, helpers, rule } = ctx;
61792
- const safeRoot = root || document;
62094
+ const { document, helpers, rule } = ctx;
61793
62095
 
61794
62096
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
61795
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
62097
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
61796
62098
 
61797
62099
  // Build a for-value -> label[] map once (avoids per-control dynamic
61798
62100
  // attribute-selector construction / CSS.escape, which is not guaranteed
@@ -62070,14 +62372,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62070
62372
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
62071
62373
  }), applicability: null },
62072
62374
  "identical-links-same-purpose": { run: (function runInPage(ctx) {
62073
- const { document, root, helpers, rule } = ctx;
62074
- const safeRoot = root || document;
62375
+ const { document, helpers, rule } = ctx;
62075
62376
 
62076
62377
  function normName(s) {
62077
62378
  return (s == null ? '' : String(s)).replace(/\s+/g, ' ').trim().toLowerCase();
62078
62379
  }
62079
62380
 
62080
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]', safeRoot) : helpers.queryAll('a[href]', safeRoot);
62381
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]') : helpers.queryAll('a[href]');
62081
62382
 
62082
62383
  const groups = new Map(); // normName -> [{ el, href }]
62083
62384
  let applicableCount = 0;
@@ -62157,8 +62458,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62157
62458
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
62158
62459
  }), applicability: null },
62159
62460
  "iframe-focusable-content": { run: (function runInPage(ctx) {
62160
- const { document, root, helpers, rule } = ctx;
62161
- const safeRoot = root || document;
62461
+ const { document, helpers, rule } = ctx;
62162
62462
 
62163
62463
  function hasFocusableCandidate(doc) {
62164
62464
  if (!doc || !doc.querySelectorAll) return false;
@@ -62190,7 +62490,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62190
62490
  return !Number.isNaN(n) && n < 0;
62191
62491
  }
62192
62492
 
62193
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame', safeRoot) : helpers.queryAll('iframe, frame', safeRoot);
62493
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
62194
62494
 
62195
62495
  const occurrences = [];
62196
62496
  let applicableCount = 0;
@@ -62240,10 +62540,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62240
62540
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
62241
62541
  }), applicability: null },
62242
62542
  "iframe-name-present": { run: (function runInPage(ctx) {
62243
- const { document, root, helpers, rule } = ctx;
62244
- const safeRoot = root || document;
62543
+ const { document, helpers, rule } = ctx;
62245
62544
 
62246
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame', safeRoot) : helpers.queryAll('iframe, frame', safeRoot);
62545
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
62247
62546
 
62248
62547
  const occurrences = [];
62249
62548
  let applicableCount = 0;
@@ -62295,10 +62594,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62295
62594
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
62296
62595
  }), applicability: null },
62297
62596
  "iframe-title-unique": { run: (function runInPage(ctx) {
62298
- const { document, root, helpers, rule } = ctx;
62299
- const safeRoot = root || document;
62597
+ const { document, helpers, rule } = ctx;
62300
62598
 
62301
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame', safeRoot) : helpers.queryAll('iframe, frame', safeRoot);
62599
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
62302
62600
 
62303
62601
  const groups = new Map(); // trimmed title -> elements[]
62304
62602
  let applicableCount = 0;
@@ -62353,8 +62651,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62353
62651
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
62354
62652
  }), applicability: null },
62355
62653
  "image-redundant-alt": { run: (function runInPage(ctx) {
62356
- const { document, root, helpers, rule } = ctx;
62357
- const safeRoot = root || document;
62654
+ const { document, helpers, rule } = ctx;
62358
62655
 
62359
62656
  function normalizeWs(s) {
62360
62657
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -62373,7 +62670,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62373
62670
  return normalizeWs(text);
62374
62671
  }
62375
62672
 
62376
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[alt]', safeRoot) : helpers.queryAll('img[alt]', safeRoot);
62673
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[alt]') : helpers.queryAll('img[alt]');
62377
62674
 
62378
62675
  const occurrences = [];
62379
62676
  let applicableCount = 0;
@@ -63161,8 +63458,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63161
63458
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
63162
63459
  }), applicability: null },
63163
63460
  "label-in-name": { run: (function runInPage(ctx) {
63164
- const { document, root, helpers, rule } = ctx;
63165
- const safeRoot = root || document;
63461
+ const { document, helpers, rule } = ctx;
63166
63462
 
63167
63463
  const occurrences = [];
63168
63464
  let applicableCount = 0;
@@ -63171,7 +63467,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63171
63467
  // NOTE: aria-hidden is intentionally NOT excluded here; it does not affect visual rendering.
63172
63468
  const selector = ':is(button, a[href], summary, input:not([type="hidden"]), textarea, select, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="searchbox"], [role="tab"], [role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"], [role="option"], [role="treeitem"], [role="gridcell"]):not([hidden]):not([disabled]):not([aria-disabled="true"]):is([aria-label], [aria-labelledby])';
63173
63469
 
63174
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
63470
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
63175
63471
 
63176
63472
  function norm(s) {
63177
63473
  const v = (s == null ? '' : String(s));
@@ -63309,11 +63605,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63309
63605
  }
63310
63606
 
63311
63607
  // Fallback: label[for=id]. Uses `document` directly rather than
63312
- // ctx.root/safeRoot -- label[for] association is a document-wide
63313
- // relationship (IDs are document-unique), not bounded by whatever
63314
- // contextSelector region happens to be scanned, and ctx.root is an
63315
- // array (multi-region contextSelector support), not a single element
63316
- // with its own .querySelector to call directly.
63608
+ // ctx.root -- label[for] association is a document-wide relationship
63609
+ // (IDs are document-unique), not bounded by whatever contextSelector
63610
+ // region happens to be scanned, and ctx.root is an array (multi-region
63611
+ // contextSelector support), not a single element with its own
63612
+ // .querySelector to call directly.
63317
63613
  try {
63318
63614
  const idAttribute = control && control.getAttribute ? (control.getAttribute('id') || '') : '';
63319
63615
  const key = String(idAttribute || '').trim();
@@ -63454,11 +63750,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63454
63750
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
63455
63751
  }), applicability: null },
63456
63752
  "label-title-only": { run: (function runInPage(ctx) {
63457
- const { document, root, helpers, rule } = ctx;
63458
- const safeRoot = root || document;
63753
+ const { document, helpers, rule } = ctx;
63459
63754
 
63460
63755
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
63461
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
63756
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
63462
63757
 
63463
63758
  const labelsByFor = new Map();
63464
63759
  const allLabels = document.getElementsByTagName ? document.getElementsByTagName('label') : [];
@@ -64505,8 +64800,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64505
64800
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
64506
64801
  }), applicability: null },
64507
64802
  "link-in-text-block": { run: (function runInPage(ctx) {
64508
- const { document, root, helpers, rule, engineOptions } = ctx;
64509
- const safeRoot = root || document;
64803
+ const { document, helpers, rule, engineOptions } = ctx;
64510
64804
 
64511
64805
  function safeComputedStyle(el) {
64512
64806
  try {
@@ -64552,7 +64846,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64552
64846
  const c = helpers && helpers.contrast ? helpers.contrast : null;
64553
64847
 
64554
64848
  const selector = 'a[href]';
64555
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
64849
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
64556
64850
 
64557
64851
  const occurrences = [];
64558
64852
  let applicableCount = 0;
@@ -64652,8 +64946,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64652
64946
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
64653
64947
  }), applicability: null },
64654
64948
  "link-name-present": { run: (function runInPage(ctx) {
64655
- const { document, root, helpers, rule } = ctx;
64656
- const safeRoot = root || document;
64949
+ const { document, helpers, rule } = ctx;
64657
64950
 
64658
64951
  const occurrences = [];
64659
64952
  let applicableCount = 0;
@@ -64674,7 +64967,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64674
64967
  }
64675
64968
 
64676
64969
  const selector = 'a[href], area[href], [role="link"]';
64677
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
64970
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
64678
64971
 
64679
64972
  for (const el of nodes) {
64680
64973
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -64687,7 +64980,20 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64687
64980
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
64688
64981
  const programmaticName = (nameInfo && typeof nameInfo.value === 'string') ? nameInfo.value : '';
64689
64982
 
64690
- const contentName = programmaticName.trim().length === 0 ? getConservativeSubtreeText(el) : '';
64983
+ // A native <a href>/<area href> (or [role="link"]) whose role has been
64984
+ // overridden to one of these roles is no longer semantically a link —
64985
+ // per the WAI-ARIA Accessible Name and Description Computation spec
64986
+ // these roles are name-from-author-only, and their rendered content
64987
+ // represents a VALUE, not a NAME (mirrors axe-core's controlValueRoles,
64988
+ // verified against its source; see button-name-present.js's identical
64989
+ // fix for a real-world example — <button role="combobox">List</button> —
64990
+ // of the same class of bug on a different host tag).
64991
+ const role = el.getAttribute ? el.getAttribute('role') : null;
64992
+ const roleNorm = String(role || '').replace(/\s+/g, ' ').trim().toLowerCase();
64993
+ const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
64994
+ const isContentNameCandidate = !VALUE_ROLES.includes(roleNorm);
64995
+
64996
+ const contentName = (programmaticName.trim().length === 0 && isContentNameCandidate) ? getConservativeSubtreeText(el) : '';
64691
64997
  const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
64692
64998
 
64693
64999
  if (finalName.length === 0) {
@@ -64739,8 +65045,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64739
65045
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
64740
65046
  }), applicability: null },
64741
65047
  "link-name-quality": { run: (function runInPage(ctx) {
64742
- const { document, root, helpers, rule } = ctx;
64743
- const safeRoot = root || document;
65048
+ const { document, helpers, rule } = ctx;
64744
65049
 
64745
65050
  const GENERIC_LINK_TEXT = new Set([
64746
65051
  'click here', 'here', 'click', 'more', 'more info', 'more information',
@@ -64759,7 +65064,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64759
65064
  }
64760
65065
 
64761
65066
  const selector = 'a[href]';
64762
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
65067
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
64763
65068
 
64764
65069
  const occurrences = [];
64765
65070
  let applicableCount = 0;
@@ -64819,14 +65124,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64819
65124
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
64820
65125
  }), applicability: null },
64821
65126
  "list-children-valid": { run: (function runInPage(ctx) {
64822
- const { document, root, helpers, rule } = ctx;
64823
- const safeRoot = root || document;
65127
+ const { document, helpers, rule } = ctx;
64824
65128
 
64825
65129
  // Declared inside runInPage — see scripts/build-core.js header
64826
65130
  // ("runInPage MUST be self-contained").
64827
65131
  const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
64828
65132
 
64829
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('ul, ol', safeRoot) : helpers.queryAll('ul, ol', safeRoot);
65133
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('ul, ol') : helpers.queryAll('ul, ol');
64830
65134
 
64831
65135
  const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
64832
65136
 
@@ -64897,11 +65201,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64897
65201
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
64898
65202
  }), applicability: null },
64899
65203
  "listbox-name-present": { run: (function runInPage(ctx) {
64900
- const { document, root, helpers, rule } = ctx;
65204
+ const { document, helpers, rule } = ctx;
64901
65205
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
64902
65206
  ? helpers.getEligibilityInfo
64903
65207
  : null;
64904
- const safeRoot = root || document;
64905
65208
 
64906
65209
 
64907
65210
  function normalizeWs(s) {
@@ -65006,7 +65309,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65006
65309
  let applicableCount = 0;
65007
65310
 
65008
65311
  const selector = "[role=\"listbox\"]";
65009
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
65312
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
65010
65313
 
65011
65314
  // Precompute label[for] map for listbox elements that are labelable
65012
65315
  // native form controls (e.g. <select multiple role="listbox">).
@@ -65107,10 +65410,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65107
65410
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
65108
65411
  }), applicability: null },
65109
65412
  "listitem-parent-valid": { run: (function runInPage(ctx) {
65110
- const { document, root, helpers, rule } = ctx;
65111
- const safeRoot = root || document;
65413
+ const { document, helpers, rule } = ctx;
65112
65414
 
65113
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li', safeRoot) : helpers.queryAll('li', safeRoot);
65415
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li') : helpers.queryAll('li');
65114
65416
 
65115
65417
  const occurrences = [];
65116
65418
  let applicableCount = 0;
@@ -65539,11 +65841,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65539
65841
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
65540
65842
  }), applicability: null },
65541
65843
  "menuitem-name-present": { run: (function runInPage(ctx) {
65542
- const { document, root, helpers, rule } = ctx;
65844
+ const { document, helpers, rule } = ctx;
65543
65845
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
65544
65846
  ? helpers.getEligibilityInfo
65545
65847
  : null;
65546
- const safeRoot = root || document;
65547
65848
 
65548
65849
 
65549
65850
  function normalizeWs(s) {
@@ -65649,7 +65950,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65649
65950
  let applicableCount = 0;
65650
65951
 
65651
65952
  const selector = '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]';
65652
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
65953
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
65653
65954
 
65654
65955
 
65655
65956
  function evaluate(el) {
@@ -65955,11 +66256,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65955
66256
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
65956
66257
  }), applicability: null },
65957
66258
  "meter-name-present": { run: (function runInPage(ctx) {
65958
- const { document, root, helpers, rule } = ctx;
66259
+ const { document, helpers, rule } = ctx;
65959
66260
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
65960
66261
  ? helpers.getEligibilityInfo
65961
66262
  : null;
65962
- const safeRoot = root || document;
65963
66263
 
65964
66264
 
65965
66265
  function normalizeWs(s) {
@@ -66023,7 +66323,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66023
66323
  let applicableCount = 0;
66024
66324
 
66025
66325
  const selector = '[role="meter"]';
66026
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
66326
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
66027
66327
 
66028
66328
 
66029
66329
  function evaluate(el) {
@@ -66089,8 +66389,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66089
66389
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
66090
66390
  }), applicability: null },
66091
66391
  "mouse-only-event-handlers": { run: (function runInPage(ctx) {
66092
- const { document, root, helpers, rule } = ctx;
66093
- const safeRoot = root || document;
66392
+ const { document, helpers, rule } = ctx;
66094
66393
 
66095
66394
  const MOUSE_ONLY_ATTRS = [
66096
66395
  'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup',
@@ -66101,7 +66400,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66101
66400
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
66102
66401
 
66103
66402
  const selector = MOUSE_ONLY_ATTRS.map((a) => `[${a}]`).join(', ');
66104
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
66403
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
66105
66404
 
66106
66405
  const occurrences = [];
66107
66406
  let applicableCount = 0;
@@ -66153,8 +66452,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66153
66452
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
66154
66453
  }), applicability: null },
66155
66454
  "nested-interactive-controls-absent": { run: (function runInPage(ctx) {
66156
- const { document, root, helpers, rule } = ctx;
66157
- const safeRoot = root || document;
66455
+ const { document, helpers, rule } = ctx;
66158
66456
 
66159
66457
  // Declared inside runInPage — see scripts/build-core.js header
66160
66458
  // ("runInPage MUST be self-contained").
@@ -66183,7 +66481,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66183
66481
  '[role="treeitem"]'
66184
66482
  ].join(', ');
66185
66483
 
66186
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(INTERACTIVE_SELECTOR, safeRoot) : helpers.queryAll(INTERACTIVE_SELECTOR, safeRoot);
66484
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(INTERACTIVE_SELECTOR) : helpers.queryAll(INTERACTIVE_SELECTOR);
66187
66485
 
66188
66486
  const occurrences = [];
66189
66487
  let applicableCount = 0;
@@ -66232,10 +66530,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66232
66530
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
66233
66531
  }), applicability: null },
66234
66532
  "no-autoplay-audio": { run: (function runInPage(ctx) {
66235
- const { document, root, helpers, rule } = ctx;
66236
- const safeRoot = root || document;
66533
+ const { document, helpers, rule } = ctx;
66237
66534
 
66238
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('audio[autoplay], video[autoplay]', safeRoot) : helpers.queryAll('audio[autoplay], video[autoplay]', safeRoot);
66535
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('audio[autoplay], video[autoplay]') : helpers.queryAll('audio[autoplay], video[autoplay]');
66239
66536
 
66240
66537
  const occurrences = [];
66241
66538
  let applicableCount = 0;
@@ -66589,11 +66886,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66589
66886
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
66590
66887
  }), applicability: null },
66591
66888
  "option-name-present": { run: (function runInPage(ctx) {
66592
- const { document, root, helpers, rule } = ctx;
66889
+ const { document, helpers, rule } = ctx;
66593
66890
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
66594
66891
  ? helpers.getEligibilityInfo
66595
66892
  : null;
66596
- const safeRoot = root || document;
66597
66893
 
66598
66894
 
66599
66895
  function normalizeWs(s) {
@@ -66657,7 +66953,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66657
66953
  let applicableCount = 0;
66658
66954
 
66659
66955
  const selector = "[role=\"option\"]";
66660
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
66956
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
66661
66957
 
66662
66958
 
66663
66959
  function hasName(el) {
@@ -66720,8 +67016,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66720
67016
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
66721
67017
  }), applicability: null },
66722
67018
  "p-as-heading": { run: (function runInPage(ctx) {
66723
- const { document, root, helpers, rule } = ctx;
66724
- const safeRoot = root || document;
67019
+ const { document, helpers, rule } = ctx;
66725
67020
 
66726
67021
  const MAX_HEADING_LIKE_CHARS = 120;
66727
67022
  const MIN_FONT_SIZE_PX = 18;
@@ -66773,7 +67068,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66773
67068
  return Number.isFinite(px) ? px : 0;
66774
67069
  }
66775
67070
 
66776
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p', safeRoot) : helpers.queryAll('p', safeRoot);
67071
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p') : helpers.queryAll('p');
66777
67072
 
66778
67073
  const occurrences = [];
66779
67074
  let applicableCount = 0;
@@ -66855,14 +67150,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66855
67150
  return tag === 'h1';
66856
67151
  }
66857
67152
 
67153
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
67154
+
67155
+ function isExposedToAt(el) {
67156
+ if (!isAccTreeEligible) return true;
67157
+ try {
67158
+ const r = isAccTreeEligible(el, ctx);
67159
+ if (typeof r === 'boolean') return r;
67160
+ return !!(r && r.eligible);
67161
+ } catch {
67162
+ return true;
67163
+ }
67164
+ }
67165
+
67166
+ // queryAllSmart (shadow-DOM-aware) instead of plain document.querySelectorAll -- see
67167
+ // landmark-one-main-manual.js's identical precedent.
66858
67168
  let nodes = [];
66859
67169
  try {
66860
- nodes = document.querySelectorAll('h1, [role]');
67170
+ nodes = helpers && typeof helpers.queryAllSmart === 'function'
67171
+ ? helpers.queryAllSmart('h1, [role]')
67172
+ : document.querySelectorAll('h1, [role]');
66861
67173
  } catch {
66862
67174
  nodes = [];
66863
67175
  }
66864
67176
 
66865
- const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el));
67177
+ const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el) && isExposedToAt(el));
66866
67178
 
66867
67179
  if (hasH1) {
66868
67180
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -67186,8 +67498,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67186
67498
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
67187
67499
  }), applicability: null },
67188
67500
  "presentation-role-conflict": { run: (function runInPage(ctx) {
67189
- const { document, root, helpers, rule } = ctx;
67190
- const safeRoot = root || document;
67501
+ const { document, helpers, rule } = ctx;
67191
67502
 
67192
67503
  // The full set of ARIA attributes marked `global: true` per the WAI-ARIA
67193
67504
  // spec (confirmed against a widely-used reference engine's own
@@ -67204,8 +67515,8 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67204
67515
  const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
67205
67516
 
67206
67517
  const nodes = helpers.queryAllSmart
67207
- ? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]', safeRoot)
67208
- : helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]', safeRoot);
67518
+ ? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]')
67519
+ : helpers.queryAll('[role="presentation"], [role="none"], img[alt=""]');
67209
67520
 
67210
67521
  const occurrences = [];
67211
67522
  let applicableCount = 0;
@@ -67269,11 +67580,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67269
67580
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
67270
67581
  }), applicability: null },
67271
67582
  "progressbar-name-present": { run: (function runInPage(ctx) {
67272
- const { document, root, helpers, rule } = ctx;
67583
+ const { document, helpers, rule } = ctx;
67273
67584
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
67274
67585
  ? helpers.getEligibilityInfo
67275
67586
  : null;
67276
- const safeRoot = root || document;
67277
67587
 
67278
67588
 
67279
67589
  function normalizeWs(s) {
@@ -67337,7 +67647,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67337
67647
  let applicableCount = 0;
67338
67648
 
67339
67649
  const selector = '[role="progressbar"]';
67340
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
67650
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
67341
67651
 
67342
67652
 
67343
67653
  function evaluate(el) {
@@ -67749,8 +68059,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67749
68059
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
67750
68060
  }), applicability: null },
67751
68061
  "scrollable-region-focusable": { run: (function runInPage(ctx) {
67752
- const { document, root, helpers, rule } = ctx;
67753
- const safeRoot = root || document;
68062
+ const { document, helpers, rule } = ctx;
67754
68063
 
67755
68064
  function safeComputedStyle(el) {
67756
68065
  try {
@@ -67795,7 +68104,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67795
68104
  }
67796
68105
 
67797
68106
  const CANDIDATE_SELECTOR = 'div, section, article, aside, main, nav, pre, table, blockquote, ul, ol, textarea';
67798
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(CANDIDATE_SELECTOR, safeRoot) : helpers.queryAll(CANDIDATE_SELECTOR, safeRoot);
68107
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(CANDIDATE_SELECTOR) : helpers.queryAll(CANDIDATE_SELECTOR);
67799
68108
 
67800
68109
  const occurrences = [];
67801
68110
  let applicableCount = 0;
@@ -67850,11 +68159,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67850
68159
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
67851
68160
  }), applicability: null },
67852
68161
  "searchbox-name-present": { run: (function runInPage(ctx) {
67853
- const { document, root, helpers, rule } = ctx;
68162
+ const { document, helpers, rule } = ctx;
67854
68163
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
67855
68164
  ? helpers.getEligibilityInfo
67856
68165
  : null;
67857
- const safeRoot = root || document;
67858
68166
 
67859
68167
 
67860
68168
  function normalizeWs(s) {
@@ -67959,7 +68267,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67959
68267
  let applicableCount = 0;
67960
68268
 
67961
68269
  const selector = "[role=\"searchbox\"]";
67962
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
68270
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
67963
68271
 
67964
68272
  // Precompute label[for] map for searchbox elements that are labelable
67965
68273
  // native form controls (e.g. <input role="searchbox">).
@@ -68060,10 +68368,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68060
68368
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68061
68369
  }), applicability: null },
68062
68370
  "server-side-image-map-absent": { run: (function runInPage(ctx) {
68063
- const { document, root, helpers, rule } = ctx;
68064
- const safeRoot = root || document;
68371
+ const { document, helpers, rule } = ctx;
68065
68372
 
68066
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[ismap]', safeRoot) : helpers.queryAll('img[ismap]', safeRoot);
68373
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('img[ismap]') : helpers.queryAll('img[ismap]');
68067
68374
 
68068
68375
  const occurrences = [];
68069
68376
  let applicableCount = 0;
@@ -68101,8 +68408,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68101
68408
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68102
68409
  }), applicability: null },
68103
68410
  "skip-link": { run: (function runInPage(ctx) {
68104
- const { document, root, helpers, rule } = ctx;
68105
- const safeRoot = root || document;
68411
+ const { document, helpers, rule } = ctx;
68106
68412
 
68107
68413
  function normalizeWs(s) {
68108
68414
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -68129,7 +68435,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68129
68435
  return normalizeWs(el.textContent);
68130
68436
  }
68131
68437
 
68132
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]', safeRoot) : helpers.queryAll('a[href]', safeRoot);
68438
+ function hasReliableGeometrySupport() {
68439
+ const probe = document.documentElement || document.body || null;
68440
+ if (!probe || !probe.getClientRects || !probe.getBoundingClientRect) return false;
68441
+ try {
68442
+ const rects = probe.getClientRects();
68443
+ const rectCount = rects ? rects.length : 0;
68444
+ const r = probe.getBoundingClientRect();
68445
+ const w = r && Number.isFinite(r.width) ? r.width : 0;
68446
+ const h = r && Number.isFinite(r.height) ? r.height : 0;
68447
+ return rectCount > 0 && (w > 0 || h > 0);
68448
+ } catch {
68449
+ return false;
68450
+ }
68451
+ }
68452
+
68453
+ function toEligibility(info) {
68454
+ return {
68455
+ eligible: !!(info && info.eligible),
68456
+ reasons: info && Array.isArray(info.reasons) ? info.reasons.slice(0) : []
68457
+ };
68458
+ }
68459
+
68460
+ const geometrySupported = hasReliableGeometrySupport();
68461
+
68462
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('a[href]') : helpers.queryAll('a[href]');
68133
68463
 
68134
68464
  const occurrences = [];
68135
68465
  let applicableCount = 0;
@@ -68167,7 +68497,63 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68167
68497
  }
68168
68498
  }
68169
68499
 
68170
- if (target) continue;
68500
+ if (target) {
68501
+ const accEligibility = toEligibility(
68502
+ helpers.getEligibilityInfo
68503
+ ? helpers.getEligibilityInfo(target, ctx, { targetSet: 'acc' })
68504
+ : (helpers.isAccTreeEligible ? helpers.isAccTreeEligible(target, ctx) : { eligible: true, reasons: [] })
68505
+ );
68506
+
68507
+ let geometryEligibility = null;
68508
+ let geometryReasonCode = null;
68509
+ if (geometrySupported && helpers.isDomVisibleEligible) {
68510
+ geometryEligibility = toEligibility(
68511
+ helpers.isDomVisibleEligible(target, ctx, { visibilityMode: 'styleAndGeometry', ignoreOpacity: true })
68512
+ );
68513
+ if (!geometryEligibility.eligible && geometryEligibility.reasons.includes('noClientRects')) {
68514
+ geometryReasonCode = 'NO_CLIENT_RECTS';
68515
+ } else if (!geometryEligibility.eligible && geometryEligibility.reasons.includes('zeroArea')) {
68516
+ geometryReasonCode = 'ZERO_AREA_TARGET';
68517
+ }
68518
+ }
68519
+
68520
+ const unusableByAcc = !accEligibility.eligible;
68521
+ const unusableByGeometry = !!geometryReasonCode;
68522
+ if (!unusableByAcc && !unusableByGeometry) continue;
68523
+
68524
+ const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
68525
+ const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
68526
+
68527
+ occurrences.push({
68528
+ selector: stableSelector,
68529
+ html,
68530
+ summary: 'This skip link points to a target that exists but is not currently usable.',
68531
+ hint: 'Point this skip link to a target that is exposed and usable as a navigation destination.',
68532
+ i18n: {
68533
+ summaryKey: 'skipLink_summary_unusableTarget_cantTell',
68534
+ hintKey: 'skipLink_hint_unusableTarget_cantTell',
68535
+ params: { href }
68536
+ },
68537
+ data: {
68538
+ details: {
68539
+ reasonCode: 'SKIP_LINK_TARGET_UNUSABLE',
68540
+ href,
68541
+ unusableReasonCode: unusableByAcc ? 'ACC_TREE_INELIGIBLE' : geometryReasonCode,
68542
+ targetSelector: helpers.buildSelector ? helpers.buildSelector(target) : null,
68543
+ geometryCheckEnabled: geometrySupported
68544
+ },
68545
+ visibilityFilter: {
68546
+ targetSet: 'acc',
68547
+ accEligible: accEligibility.eligible,
68548
+ reasons: accEligibility.reasons
68549
+ },
68550
+ targetGeometry: geometryEligibility
68551
+ ? { eligible: geometryEligibility.eligible, reasons: geometryEligibility.reasons }
68552
+ : { eligible: null, reasons: [] }
68553
+ }
68554
+ });
68555
+ continue;
68556
+ }
68171
68557
 
68172
68558
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
68173
68559
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
@@ -68197,11 +68583,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68197
68583
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
68198
68584
  }), applicability: null },
68199
68585
  "slider-name-present": { run: (function runInPage(ctx) {
68200
- const { document, root, helpers, rule } = ctx;
68586
+ const { document, helpers, rule } = ctx;
68201
68587
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
68202
68588
  ? helpers.getEligibilityInfo
68203
68589
  : null;
68204
- const safeRoot = root || document;
68205
68590
 
68206
68591
 
68207
68592
  function normalizeWs(s) {
@@ -68333,7 +68718,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68333
68718
  let applicableCount = 0;
68334
68719
 
68335
68720
  const selector = 'input[type="range"], [role="slider"]';
68336
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
68721
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
68337
68722
 
68338
68723
 
68339
68724
  // Precompute label[for] map for native range inputs.
@@ -68447,11 +68832,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68447
68832
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68448
68833
  }), applicability: null },
68449
68834
  "spinbutton-name-present": { run: (function runInPage(ctx) {
68450
- const { document, root, helpers, rule } = ctx;
68835
+ const { document, helpers, rule } = ctx;
68451
68836
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
68452
68837
  ? helpers.getEligibilityInfo
68453
68838
  : null;
68454
- const safeRoot = root || document;
68455
68839
 
68456
68840
 
68457
68841
  function normalizeWs(s) {
@@ -68556,7 +68940,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68556
68940
  let applicableCount = 0;
68557
68941
 
68558
68942
  const selector = "[role=\"spinbutton\"]";
68559
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
68943
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
68560
68944
 
68561
68945
  // Precompute label[for] map for spinbutton elements that are labelable
68562
68946
  // native form controls (e.g. <input role="spinbutton">).
@@ -68657,11 +69041,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68657
69041
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68658
69042
  }), applicability: null },
68659
69043
  "summary-name-present": { run: (function runInPage(ctx) {
68660
- const { document, root, helpers, rule } = ctx;
69044
+ const { document, helpers, rule } = ctx;
68661
69045
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
68662
69046
  ? helpers.getEligibilityInfo
68663
69047
  : null;
68664
- const safeRoot = root || document;
68665
69048
 
68666
69049
 
68667
69050
  function normalizeWs(s) {
@@ -68724,7 +69107,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68724
69107
  const occurrences = [];
68725
69108
  let applicableCount = 0;
68726
69109
 
68727
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary', safeRoot) : helpers.queryAll('summary', safeRoot);
69110
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary') : helpers.queryAll('summary');
68728
69111
 
68729
69112
  function evaluate(el) {
68730
69113
  const ariaLabel = getAttr(el, 'aria-label');
@@ -69363,11 +69746,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69363
69746
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
69364
69747
  }), applicability: null },
69365
69748
  "tab-name-present": { run: (function runInPage(ctx) {
69366
- const { document, root, helpers, rule } = ctx;
69749
+ const { document, helpers, rule } = ctx;
69367
69750
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
69368
69751
  ? helpers.getEligibilityInfo
69369
69752
  : null;
69370
- const safeRoot = root || document;
69371
69753
 
69372
69754
 
69373
69755
  function normalizeWs(s) {
@@ -69473,7 +69855,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69473
69855
  let applicableCount = 0;
69474
69856
 
69475
69857
  const selector = '[role="tab"]';
69476
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
69858
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
69477
69859
 
69478
69860
 
69479
69861
  function evaluate(el) {
@@ -69588,14 +69970,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69588
69970
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
69589
69971
  }), applicability: null },
69590
69972
  "table-duplicate-name": { run: (function runInPage(ctx) {
69591
- const { document, root, helpers, rule } = ctx;
69592
- const safeRoot = root || document;
69973
+ const { document, helpers, rule } = ctx;
69593
69974
 
69594
69975
  function normalizeWs(s) {
69595
69976
  return String(s || '').replace(/\s+/g, ' ').trim();
69596
69977
  }
69597
69978
 
69598
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]', safeRoot) : helpers.queryAll('table[summary]', safeRoot);
69979
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table[summary]') : helpers.queryAll('table[summary]');
69599
69980
 
69600
69981
  const occurrences = [];
69601
69982
  let applicableCount = 0;
@@ -69641,12 +70022,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69641
70022
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
69642
70023
  }), applicability: null },
69643
70024
  "table-fake-caption": { run: (function runInPage(ctx) {
69644
- const { document, root, helpers, rule } = ctx;
69645
- const safeRoot = root || document;
70025
+ const { document, helpers, rule } = ctx;
69646
70026
 
69647
70027
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
69648
70028
 
69649
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
70029
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
69650
70030
 
69651
70031
  const occurrences = [];
69652
70032
  let applicableCount = 0;
@@ -69709,10 +70089,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69709
70089
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
69710
70090
  }), applicability: null },
69711
70091
  "table-headers-attr-valid": { run: (function runInPage(ctx) {
69712
- const { document, root, helpers, rule } = ctx;
69713
- const safeRoot = root || document;
70092
+ const { document, helpers, rule } = ctx;
69714
70093
 
69715
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('td[headers], th[headers]', safeRoot) : helpers.queryAll('td[headers], th[headers]', safeRoot);
70094
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('td[headers], th[headers]') : helpers.queryAll('td[headers], th[headers]');
69716
70095
 
69717
70096
  const occurrences = [];
69718
70097
  let applicableCount = 0;
@@ -69786,10 +70165,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69786
70165
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
69787
70166
  }), applicability: null },
69788
70167
  "table-th-has-data-cells": { run: (function runInPage(ctx) {
69789
- const { document, root, helpers, rule } = ctx;
69790
- const safeRoot = root || document;
70168
+ const { document, helpers, rule } = ctx;
69791
70169
 
69792
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
70170
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
69793
70171
 
69794
70172
  const occurrences = [];
69795
70173
  let applicableCount = 0;
@@ -70348,14 +70726,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70348
70726
  return { ruleId: RULE_ID, outcome: 'pass', severity: 'minor', occurrences: [] };
70349
70727
  }), applicability: null },
70350
70728
  "td-has-header": { run: (function runInPage(ctx) {
70351
- const { document, root, helpers, rule } = ctx;
70352
- const safeRoot = root || document;
70729
+ const { document, helpers, rule } = ctx;
70353
70730
 
70354
70731
  const MIN_SIZE = 4;
70355
70732
 
70356
70733
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
70357
70734
 
70358
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
70735
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
70359
70736
 
70360
70737
  const occurrences = [];
70361
70738
  let applicableCount = 0;
@@ -70441,11 +70818,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70441
70818
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70442
70819
  }), applicability: null },
70443
70820
  "textbox-name-present": { run: (function runInPage(ctx) {
70444
- const { document, root, helpers, rule } = ctx;
70821
+ const { document, helpers, rule } = ctx;
70445
70822
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
70446
70823
  ? helpers.getEligibilityInfo
70447
70824
  : null;
70448
- const safeRoot = root || document;
70449
70825
 
70450
70826
 
70451
70827
  function normalizeWs(s) {
@@ -70550,7 +70926,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70550
70926
  let applicableCount = 0;
70551
70927
 
70552
70928
  const selector = "[role=\"textbox\"]";
70553
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
70929
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
70554
70930
 
70555
70931
  // Precompute label[for] map for textbox elements that are labelable
70556
70932
  // native form controls (e.g. <input role="textbox">).
@@ -70651,11 +71027,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70651
71027
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70652
71028
  }), applicability: null },
70653
71029
  "tooltip-name-present": { run: (function runInPage(ctx) {
70654
- const { document, root, helpers, rule } = ctx;
71030
+ const { document, helpers, rule } = ctx;
70655
71031
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
70656
71032
  ? helpers.getEligibilityInfo
70657
71033
  : null;
70658
- const safeRoot = root || document;
70659
71034
 
70660
71035
 
70661
71036
  function normalizeWs(s) {
@@ -70719,7 +71094,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70719
71094
  let applicableCount = 0;
70720
71095
 
70721
71096
  const selector = '[role="tooltip"]';
70722
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
71097
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
70723
71098
 
70724
71099
 
70725
71100
  function evaluate(el) {
@@ -70783,11 +71158,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70783
71158
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70784
71159
  }), applicability: null },
70785
71160
  "treeitem-name-present": { run: (function runInPage(ctx) {
70786
- const { document, root, helpers, rule } = ctx;
71161
+ const { document, helpers, rule } = ctx;
70787
71162
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
70788
71163
  ? helpers.getEligibilityInfo
70789
71164
  : null;
70790
- const safeRoot = root || document;
70791
71165
 
70792
71166
 
70793
71167
  function normalizeWs(s) {
@@ -70851,7 +71225,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70851
71225
  let applicableCount = 0;
70852
71226
 
70853
71227
  const selector = "[role=\"treeitem\"]";
70854
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
71228
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
70855
71229
 
70856
71230
 
70857
71231
  function hasName(el) {
@@ -70914,12 +71288,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70914
71288
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70915
71289
  }), applicability: null },
70916
71290
  "valid-lang": { run: (function runInPage(ctx) {
70917
- const { document, root, helpers, rule } = ctx;
70918
- const safeRoot = root || document;
71291
+ const { document, helpers, rule } = ctx;
70919
71292
 
70920
71293
  const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
70921
71294
 
70922
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]', safeRoot) : helpers.queryAll('[lang]', safeRoot);
71295
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]') : helpers.queryAll('[lang]');
70923
71296
 
70924
71297
  const occurrences = [];
70925
71298
  let applicableCount = 0;
@@ -70964,10 +71337,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70964
71337
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70965
71338
  }), applicability: null },
70966
71339
  "video-caption": { run: (function runInPage(ctx) {
70967
- const { document, root, helpers, rule } = ctx;
70968
- const safeRoot = root || document;
71340
+ const { document, helpers, rule } = ctx;
70969
71341
 
70970
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video', safeRoot) : helpers.queryAll('video', safeRoot);
71342
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video') : helpers.queryAll('video');
70971
71343
 
70972
71344
  const occurrences = [];
70973
71345
  let applicableCount = 0;
@@ -71689,10 +72061,12 @@ const I18N = {
71689
72061
  "region_description": "Checks that direct children of <body> with visible text content are contained within a landmark region.",
71690
72062
  "region_summary_cantTell": "This content is not contained within a landmark region.",
71691
72063
  "region_hint_cantTell": "Move this content inside a landmark region (main, nav, aside, a labeled section, etc.).",
71692
- "skipLink_title": "Skip link must have a resolvable target",
71693
- "skipLink_description": "Checks that a \"skip to ...\" link's href fragment resolves to a real element in the document.",
72064
+ "skipLink_title": "Skip link must have a resolvable, usable target",
72065
+ "skipLink_description": "Checks that a \"skip to ...\" link's href fragment resolves to a real, currently usable element in the document.",
71694
72066
  "skipLink_summary_cantTell": "This skip link's target does not exist.",
71695
72067
  "skipLink_hint_cantTell": "Point the skip link's href at an id that exists in the document, or add the missing target element.",
72068
+ "skipLink_summary_unusableTarget_cantTell": "This skip link points to a target that exists but is not currently usable.",
72069
+ "skipLink_hint_unusableTarget_cantTell": "Point this skip link to a target that is exposed and usable as a navigation destination.",
71696
72070
  "autocompleteValid_title": "autocomplete attribute must be a valid autofill value",
71697
72071
  "autocompleteValid_description": "Checks that a non-empty autocomplete attribute is \"on\"/\"off\" or a well-formed autofill detail token list.",
71698
72072
  "autocompleteValid_summary_fail": "This autocomplete attribute value is not a valid autofill value.",
@@ -72291,10 +72665,12 @@ const I18N = {
72291
72665
  "region_description": "Vérifie que les enfants directs de <body> ayant un contenu textuel visible sont contenus dans un point de repère.",
72292
72666
  "region_summary_cantTell": "Ce contenu n’est contenu dans aucun point de repère.",
72293
72667
  "region_hint_cantTell": "Déplacez ce contenu à l’intérieur d’un point de repère (main, nav, aside, une section nommée, etc.).",
72294
- "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout",
72295
- "skipLink_description": "Vérifie que le fragment href d’un lien « aller au... » se résout vers un élément réel du document.",
72668
+ "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout et qui est utilisable",
72669
+ "skipLink_description": "Vérifie que le fragment href d’un lien « aller au... » se résout vers un élément réel et actuellement utilisable du document.",
72296
72670
  "skipLink_summary_cantTell": "La cible de ce lien d’évitement n’existe pas.",
72297
72671
  "skipLink_hint_cantTell": "Faites pointer le href du lien d’évitement vers un id qui existe dans le document, ou ajoutez l’élément cible manquant.",
72672
+ "skipLink_summary_unusableTarget_cantTell": "Ce lien d’évitement pointe vers une cible qui existe mais qui n’est pas actuellement utilisable.",
72673
+ "skipLink_hint_unusableTarget_cantTell": "Faites pointer ce lien d’évitement vers une cible exposée et utilisable comme destination de navigation.",
72298
72674
  "autocompleteValid_title": "L’attribut autocomplete doit être une valeur d’auto-remplissage valide",
72299
72675
  "autocompleteValid_description": "Vérifie qu’un attribut autocomplete non vide vaut « on »/« off » ou une liste de jetons d’auto-remplissage bien formée.",
72300
72676
  "autocompleteValid_summary_fail": "Cette valeur d’attribut autocomplete n’est pas une valeur d’auto-remplissage valide.",
@@ -75100,6 +75476,10 @@ const createDomHelpers = (function createDomHelpers(opts) {
75100
75476
  })();
75101
75477
  // Default on: opt OUT with `includeShadowDom: false`, not opt in.
75102
75478
  const includeShadowDom = !(opts && opts.includeShadowDom === false);
75479
+ // Default off: by default, helper queries skip structurally/CSS-hidden
75480
+ // subtrees (display:none, [hidden], closed <details>, etc.). Callers can
75481
+ // opt out with includeHiddenElements:true.
75482
+ const includeHiddenElements = !!(opts && opts.includeHiddenElements === true);
75103
75483
  const excludeSelectors = Array.isArray(opts && opts.excludeSelectors) ? opts.excludeSelectors : [];
75104
75484
 
75105
75485
  // Rule-scoped excludes (engineOptions.rules[ruleId].excludeSelectors), set
@@ -76066,8 +76446,55 @@ const createDomHelpers = (function createDomHelpers(opts) {
76066
76446
  return results;
76067
76447
  }
76068
76448
 
76449
+ const HARD_HIDDEN_REASONS = new Set([
76450
+ 'displayNone',
76451
+ 'hiddenAttr',
76452
+ 'detailsClosed',
76453
+ 'templateContent',
76454
+ 'nonRenderedElement',
76455
+ 'inputHidden',
76456
+ 'visibilityHidden',
76457
+ 'contentVisibilityHidden'
76458
+ ]);
76459
+
76069
76460
  function queryAllSmart(sel) {
76070
- const list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
76461
+ let list = includeShadowDom ? queryAllDeep(sel) : queryAll(sel);
76462
+
76463
+ // Global hidden-content policy: skip nodes that are fully excluded from
76464
+ // rendered visibility by default (unless includeHiddenElements:true).
76465
+ if (!includeHiddenElements) {
76466
+ list = list.filter((el) => {
76467
+ try {
76468
+ const vis = isAccTreeEligible(el);
76469
+ if (!vis || vis.eligible !== false) return true;
76470
+ const reasons = Array.isArray(vis.reasons) ? vis.reasons : [];
76471
+ for (const r of reasons) {
76472
+ if (HARD_HIDDEN_REASONS.has(r)) return false;
76473
+ }
76474
+
76475
+ // `isAccTreeEligible` can short-circuit on an inert ancestor
76476
+ // before it reaches an outer hard-hidden ancestor (e.g.
76477
+ // display:none wrapper). In that case the node is still
76478
+ // structurally hidden and should be excluded by the default
76479
+ // hidden-content policy.
76480
+ if (reasons.includes('inert')) {
76481
+ const domVis = isDomVisibleEligible(el, null, {
76482
+ visibilityMode: 'styleOnly',
76483
+ disableGeometry: true,
76484
+ ignoreOpacity: true
76485
+ });
76486
+ const domReasons = Array.isArray(domVis && domVis.reasons) ? domVis.reasons : [];
76487
+ for (const r of domReasons) {
76488
+ if (HARD_HIDDEN_REASONS.has(r)) return false;
76489
+ }
76490
+ }
76491
+ return true;
76492
+ } catch {
76493
+ return true;
76494
+ }
76495
+ });
76496
+ }
76497
+
76071
76498
  return __getEffectiveExcludeSelectors().length ? list.filter((el) => !isExcluded(el)) : list;
76072
76499
  }
76073
76500
 
@@ -78637,6 +79064,36 @@ const createDomHelpers = (function createDomHelpers(opts) {
78637
79064
  let node = el;
78638
79065
  let safety = 0;
78639
79066
 
79067
+ // Only apply the "stop climbing once we reach a contextSelector-
79068
+ // matched root" shortcut when there's a single (or no) matched
79069
+ // root -- resolveContextRoots() falls back to `[documentElement]`
79070
+ // when no contextSelector is given, so this is the overwhelmingly
79071
+ // common case and behaves exactly as before.
79072
+ //
79073
+ // With MULTIPLE matched roots (multi-region contextSelector
79074
+ // scans), stopping there without recording anything about which
79075
+ // root produced an ambiguous, non-unique selector string for two
79076
+ // structurally-identical regions -- a real, confirmed bug (found
79077
+ // 2026-07-29 via the cross-engine comparisons project): two
79078
+ // wrapper <div>s, each containing two identical ".widget"
79079
+ // sections scanned via `contextSelector: '.widget'`, produced the
79080
+ // *same* selector string ("section:nth-of-type(1) > div > div >
79081
+ // button") for the equivalent button in each wrapper --
79082
+ // resolving to 2 elements instead of 1 when queried, and pointing
79083
+ // at the wrong one for at least one of the two occurrences. The
79084
+ // existing `el.matches(candidate)` safety check below couldn't
79085
+ // catch this: it only verifies THIS element matches the string,
79086
+ // never that the string is unique document-wide.
79087
+ //
79088
+ // Fix: when multiple roots are in play, don't stop early --
79089
+ // keep climbing (same as the always-correct no-contextSelector
79090
+ // path) until finding a genuinely unique anchor or reaching the
79091
+ // true document root, which is always singular. That restores
79092
+ // the invariant the final safety-check comment below relies on,
79093
+ // rather than needing a separate (more expensive) document-wide
79094
+ // uniqueness re-check.
79095
+ const stopAtMatchedRoot = roots.length <= 1;
79096
+
78640
79097
  while (node && node.nodeType === 1 && safety++ < 20) {
78641
79098
  let anchor = null;
78642
79099
 
@@ -78676,7 +79133,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
78676
79133
  parts.unshift(nthOfType(node));
78677
79134
  }
78678
79135
 
78679
- if (!node.parentElement || roots.includes(node)) break;
79136
+ if (!node.parentElement || (stopAtMatchedRoot && roots.includes(node))) break;
78680
79137
  node = node.parentElement;
78681
79138
  }
78682
79139
 
@@ -78695,7 +79152,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
78695
79152
  // position relative to its own parent via `>` (child, not
78696
79153
  // descendant) combinators, so a correctly-matching chain can
78697
79154
  // only resolve to one element short of a malformed document
78698
- // (e.g. two <html> roots). Re-deriving that guarantee via a
79155
+ // (e.g. two <html> roots) -- true as long as the walk above
79156
+ // never stops short of a genuinely unique anchor/root, which is
79157
+ // exactly what `stopAtMatchedRoot` now guarantees (see its own
79158
+ // comment above; a multi-root contextSelector scan stopping
79159
+ // early used to violate this invariant silently). Re-deriving
79160
+ // that guarantee via a
78699
79161
  // document-wide :nth-of-type scan was measured to cost O(total
78700
79162
  // same-tag siblings) per call — pathological on pages with many
78701
79163
  // flat, unidentified siblings (e.g. hundreds of unlabeled
@@ -79262,6 +79724,9 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
79262
79724
 
79263
79725
  // Default on: opt OUT with `includeShadowDom: false`, not opt in.
79264
79726
  const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
79727
+ // Default off: hidden/collapsed content is excluded from rule evaluation
79728
+ // unless the caller explicitly opts in.
79729
+ const includeHiddenElements = !!(engineOptionsResolved && engineOptionsResolved.includeHiddenElements === true);
79265
79730
  const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
79266
79731
 
79267
79732
  const url = pageUrl || (document.location && document.location.href) || null;
@@ -79272,11 +79737,27 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
79272
79737
  ? engineOptionsResolved.timestamp.trim()
79273
79738
  : null;
79274
79739
 
79740
+ // createDomHelpers()/createContrastHelpers() persist their element-keyed
79741
+ // caches (outerHtmlCache, selectorCache, etc.) on window.__a11ycoreSharedCache
79742
+ // so multiple helper instances created *within this run* can share them
79743
+ // deterministically. But a window/document is frequently reused across
79744
+ // SEPARATE runs -- e.g. Jest's jsdom environment creates one window per
79745
+ // test file, and mutating document.body between it() blocks is standard.
79746
+ // Those caches are keyed by element reference, not content, so a run that
79747
+ // reuses an already-cached element (document.body never changes identity)
79748
+ // would otherwise read stale data cached by an earlier, unrelated run on
79749
+ // the same window. Clearing at the start of every run keeps sharing scoped
79750
+ // to "this run" as intended, without leaking across runs.
79751
+ try {
79752
+ if (window && window.__a11ycoreSharedCache) window.__a11ycoreSharedCache = {};
79753
+ } catch {}
79754
+
79275
79755
  const sharedHelpers = createDomHelpers({
79276
79756
  document,
79277
79757
  window,
79278
79758
  root: roots,
79279
79759
  includeShadowDom,
79760
+ includeHiddenElements,
79280
79761
  excludeSelectors,
79281
79762
  // Optional perf counters (bench/debug only). Deterministic and per-run.
79282
79763
  perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)