@surea11y/core 1.1.1 → 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 (76) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +1 -1
  3. package/src/checks/automatic/aria-allowed-attr.js +2 -3
  4. package/src/checks/automatic/aria-allowed-role.js +2 -3
  5. package/src/checks/automatic/aria-braille-equivalent.js +3 -4
  6. package/src/checks/automatic/aria-conditional-attr.js +3 -4
  7. package/src/checks/automatic/aria-deprecated-role.js +2 -3
  8. package/src/checks/automatic/aria-prohibited-attr.js +2 -3
  9. package/src/checks/automatic/aria-prohibited-children.js +2 -3
  10. package/src/checks/automatic/aria-required-attr.js +2 -3
  11. package/src/checks/automatic/aria-required-children.js +2 -3
  12. package/src/checks/automatic/aria-required-parent.js +3 -4
  13. package/src/checks/automatic/aria-roles-valid.js +2 -3
  14. package/src/checks/automatic/aria-valid-attr-value.js +2 -3
  15. package/src/checks/automatic/aria-valid-attr.js +2 -3
  16. package/src/checks/automatic/autocomplete-valid.js +2 -3
  17. package/src/checks/automatic/avoid-inline-spacing.js +2 -3
  18. package/src/checks/automatic/binary-control-name-present.js +2 -3
  19. package/src/checks/automatic/button-name-present.js +14 -4
  20. package/src/checks/automatic/bypass-blocks-present.js +35 -7
  21. package/src/checks/automatic/combobox-name-present.js +2 -3
  22. package/src/checks/automatic/definition-list-children-valid.js +2 -3
  23. package/src/checks/automatic/deprecated-elements-not-used.js +2 -3
  24. package/src/checks/automatic/dialog-name-present.js +2 -3
  25. package/src/checks/automatic/dlitem-parent-valid.js +2 -3
  26. package/src/checks/automatic/form-control-single-label.js +2 -3
  27. package/src/checks/automatic/iframe-focusable-content.js +2 -3
  28. package/src/checks/automatic/iframe-name-present.js +2 -3
  29. package/src/checks/automatic/iframe-title-unique.js +2 -3
  30. package/src/checks/automatic/label-in-name.js +7 -8
  31. package/src/checks/automatic/link-in-text-block.js +2 -3
  32. package/src/checks/automatic/link-name-present.js +16 -4
  33. package/src/checks/automatic/list-children-valid.js +2 -3
  34. package/src/checks/automatic/listbox-name-present.js +2 -3
  35. package/src/checks/automatic/listitem-parent-valid.js +2 -3
  36. package/src/checks/automatic/menuitem-name-present.js +2 -3
  37. package/src/checks/automatic/meter-name-present.js +2 -3
  38. package/src/checks/automatic/nested-interactive-controls-absent.js +2 -3
  39. package/src/checks/automatic/option-name-present.js +2 -3
  40. package/src/checks/automatic/progressbar-name-present.js +2 -3
  41. package/src/checks/automatic/searchbox-name-present.js +2 -3
  42. package/src/checks/automatic/server-side-image-map-absent.js +2 -3
  43. package/src/checks/automatic/slider-name-present.js +2 -3
  44. package/src/checks/automatic/spinbutton-name-present.js +2 -3
  45. package/src/checks/automatic/summary-name-present.js +2 -3
  46. package/src/checks/automatic/tab-name-present.js +2 -3
  47. package/src/checks/automatic/table-headers-attr-valid.js +2 -3
  48. package/src/checks/automatic/table-th-has-data-cells.js +2 -3
  49. package/src/checks/automatic/td-has-header.js +2 -3
  50. package/src/checks/automatic/textbox-name-present.js +2 -3
  51. package/src/checks/automatic/tooltip-name-present.js +2 -3
  52. package/src/checks/automatic/treeitem-name-present.js +2 -3
  53. package/src/checks/automatic/valid-lang.js +2 -3
  54. package/src/checks/manual/accesskeys-manual.js +1 -7
  55. package/src/checks/manual/aria-checked-state-mismatch-manual.js +3 -4
  56. package/src/checks/manual/aria-text-manual.js +2 -3
  57. package/src/checks/manual/focus-order-semantics-manual.js +2 -3
  58. package/src/checks/manual/identical-links-same-purpose-manual.js +2 -3
  59. package/src/checks/manual/image-redundant-alt-manual.js +2 -3
  60. package/src/checks/manual/label-title-only-manual.js +2 -3
  61. package/src/checks/manual/link-name-quality-manual.js +2 -3
  62. package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -3
  63. package/src/checks/manual/no-autoplay-audio-manual.js +2 -3
  64. package/src/checks/manual/p-as-heading-manual.js +2 -3
  65. package/src/checks/manual/page-has-heading-one-manual.js +31 -2
  66. package/src/checks/manual/presentation-role-conflict-manual.js +3 -4
  67. package/src/checks/manual/scrollable-region-focusable-manual.js +2 -3
  68. package/src/checks/manual/skip-link-manual.js +91 -9
  69. package/src/checks/manual/table-duplicate-name-manual.js +2 -3
  70. package/src/checks/manual/table-fake-caption-manual.js +2 -3
  71. package/src/checks/manual/video-caption-manual.js +2 -3
  72. package/src/core/dom-helpers.js +56 -3
  73. package/src/core/dom-runner.js +15 -0
  74. package/src/core.js +851 -481
  75. package/src/i18n/en.js +4 -2
  76. 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.",
@@ -11944,7 +11948,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
11944
11948
  'templateContent',
11945
11949
  'nonRenderedElement',
11946
11950
  'inputHidden',
11947
- 'visibilityHidden'
11951
+ 'visibilityHidden',
11952
+ 'contentVisibilityHidden'
11948
11953
  ]);
11949
11954
 
11950
11955
  function queryAllSmart(sel) {
@@ -11961,6 +11966,23 @@ const createDomHelpers = (function createDomHelpers(opts) {
11961
11966
  for (const r of reasons) {
11962
11967
  if (HARD_HIDDEN_REASONS.has(r)) return false;
11963
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
+ }
11964
11986
  return true;
11965
11987
  } catch {
11966
11988
  return true;
@@ -14537,6 +14559,36 @@ const createDomHelpers = (function createDomHelpers(opts) {
14537
14559
  let node = el;
14538
14560
  let safety = 0;
14539
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
+
14540
14592
  while (node && node.nodeType === 1 && safety++ < 20) {
14541
14593
  let anchor = null;
14542
14594
 
@@ -14576,7 +14628,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
14576
14628
  parts.unshift(nthOfType(node));
14577
14629
  }
14578
14630
 
14579
- if (!node.parentElement || roots.includes(node)) break;
14631
+ if (!node.parentElement || (stopAtMatchedRoot && roots.includes(node))) break;
14580
14632
  node = node.parentElement;
14581
14633
  }
14582
14634
 
@@ -14595,7 +14647,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
14595
14647
  // position relative to its own parent via `>` (child, not
14596
14648
  // descendant) combinators, so a correctly-matching chain can
14597
14649
  // only resolve to one element short of a malformed document
14598
- // (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
14599
14656
  // document-wide :nth-of-type scan was measured to cost O(total
14600
14657
  // same-tag siblings) per call — pathological on pages with many
14601
14658
  // flat, unidentified siblings (e.g. hundreds of unlabeled
@@ -15175,6 +15232,21 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
15175
15232
  ? engineOptionsResolved.timestamp.trim()
15176
15233
  : null;
15177
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
+
15178
15250
  const sharedHelpers = createDomHelpers({
15179
15251
  document,
15180
15252
  window,
@@ -21284,8 +21356,8 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
21284
21356
  },
21285
21357
  {
21286
21358
  "ruleId": "skip-link",
21287
- "title": "Skip link must have a resolvable target",
21288
- "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.",
21289
21361
  "i18n": {
21290
21362
  "titleKey": "skipLink_title",
21291
21363
  "descriptionKey": "skipLink_description"
@@ -22877,13 +22949,7 @@ function runa11yCoreInPage(pageUrl, contextSelector, engineOptions, runOnly) {
22877
22949
  const RULE_IMPLS = {
22878
22950
  "accesskeys": { run: (function runInPage(ctx) {
22879
22951
  const { document, helpers, rule } = ctx;
22880
-
22881
- let nodes = [];
22882
- try {
22883
- nodes = document.querySelectorAll('[accesskey]');
22884
- } catch {
22885
- nodes = [];
22886
- }
22952
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[accesskey]') : helpers.queryAll('[accesskey]');
22887
22953
 
22888
22954
  const groups = new Map(); // normalized key -> elements[]
22889
22955
  for (const el of nodes) {
@@ -23474,8 +23540,7 @@ if (isAccTreeEligible) {
23474
23540
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
23475
23541
  }), applicability: null },
23476
23542
  "aria-allowed-attr": { run: (function runInPage(ctx) {
23477
- const { document, root, helpers, rule } = ctx;
23478
- const safeRoot = root || document;
23543
+ const { document, helpers, rule } = ctx;
23479
23544
 
23480
23545
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
23481
23546
  if (!ariaHelpers) {
@@ -23539,7 +23604,7 @@ if (isAccTreeEligible) {
23539
23604
  };
23540
23605
 
23541
23606
  const globalSet = new Set(GLOBAL_ATTRS);
23542
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
23607
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
23543
23608
 
23544
23609
  const occurrences = [];
23545
23610
  let applicableCount = 0;
@@ -23604,15 +23669,14 @@ if (isAccTreeEligible) {
23604
23669
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23605
23670
  }), applicability: null },
23606
23671
  "aria-allowed-role": { run: (function runInPage(ctx) {
23607
- const { document, root, helpers, rule } = ctx;
23608
- const safeRoot = root || document;
23672
+ const { document, helpers, rule } = ctx;
23609
23673
 
23610
23674
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
23611
23675
  if (!ariaHelpers) {
23612
23676
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
23613
23677
  }
23614
23678
 
23615
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
23679
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
23616
23680
 
23617
23681
  const occurrences = [];
23618
23682
  let applicableCount = 0;
@@ -23659,8 +23723,7 @@ if (isAccTreeEligible) {
23659
23723
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23660
23724
  }), applicability: null },
23661
23725
  "aria-braille-equivalent": { run: (function runInPage(ctx) {
23662
- const { document, root, helpers, rule } = ctx;
23663
- const safeRoot = root || document;
23726
+ const { document, helpers, rule } = ctx;
23664
23727
 
23665
23728
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
23666
23729
 
@@ -23681,8 +23744,8 @@ if (isAccTreeEligible) {
23681
23744
  }
23682
23745
 
23683
23746
  const nodes = helpers.queryAllSmart
23684
- ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]', safeRoot)
23685
- : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]', safeRoot);
23747
+ ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]')
23748
+ : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]');
23686
23749
 
23687
23750
  const occurrences = [];
23688
23751
  let applicableCount = 0;
@@ -23743,14 +23806,13 @@ if (isAccTreeEligible) {
23743
23806
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23744
23807
  }), applicability: null },
23745
23808
  "aria-checked-state-mismatch": { run: (function runInPage(ctx) {
23746
- const { document, root, helpers, rule } = ctx;
23747
- const safeRoot = root || document;
23809
+ const { document, helpers, rule } = ctx;
23748
23810
 
23749
23811
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
23750
23812
 
23751
23813
  const nodes = helpers.queryAllSmart
23752
- ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]', safeRoot)
23753
- : 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]');
23754
23816
 
23755
23817
  const occurrences = [];
23756
23818
  let applicableCount = 0;
@@ -23812,16 +23874,15 @@ if (isAccTreeEligible) {
23812
23874
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
23813
23875
  }), applicability: null },
23814
23876
  "aria-conditional-attr": { run: (function runInPage(ctx) {
23815
- const { document, root, helpers, rule } = ctx;
23816
- const safeRoot = root || document;
23877
+ const { document, helpers, rule } = ctx;
23817
23878
 
23818
23879
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
23819
23880
 
23820
23881
  const TRUTHY_INVALID_VALUES = new Set(['true', 'grammar', 'spelling']);
23821
23882
 
23822
23883
  const nodes = helpers.queryAllSmart
23823
- ? helpers.queryAllSmart('[aria-errormessage]', safeRoot)
23824
- : helpers.queryAll('[aria-errormessage]', safeRoot);
23884
+ ? helpers.queryAllSmart('[aria-errormessage]')
23885
+ : helpers.queryAll('[aria-errormessage]');
23825
23886
 
23826
23887
  const occurrences = [];
23827
23888
  let applicableCount = 0;
@@ -23866,15 +23927,14 @@ if (isAccTreeEligible) {
23866
23927
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
23867
23928
  }), applicability: null },
23868
23929
  "aria-deprecated-role": { run: (function runInPage(ctx) {
23869
- const { document, root, helpers, rule } = ctx;
23870
- const safeRoot = root || document;
23930
+ const { document, helpers, rule } = ctx;
23871
23931
 
23872
23932
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
23873
23933
  if (!ariaHelpers) {
23874
23934
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
23875
23935
  }
23876
23936
 
23877
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
23937
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
23878
23938
 
23879
23939
  const occurrences = [];
23880
23940
  let applicableCount = 0;
@@ -24386,8 +24446,7 @@ if (isAccTreeEligible) {
24386
24446
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
24387
24447
  }), applicability: null },
24388
24448
  "aria-prohibited-attr": { run: (function runInPage(ctx) {
24389
- const { document, root, helpers, rule } = ctx;
24390
- const safeRoot = root || document;
24449
+ const { document, helpers, rule } = ctx;
24391
24450
 
24392
24451
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24393
24452
  if (!ariaHelpers) {
@@ -24407,7 +24466,7 @@ if (isAccTreeEligible) {
24407
24466
 
24408
24467
  const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
24409
24468
 
24410
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24469
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24411
24470
 
24412
24471
  const occurrences = [];
24413
24472
  let applicableCount = 0;
@@ -24458,8 +24517,7 @@ if (isAccTreeEligible) {
24458
24517
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24459
24518
  }), applicability: null },
24460
24519
  "aria-prohibited-children": { run: (function runInPage(ctx) {
24461
- const { document, root, helpers, rule } = ctx;
24462
- const safeRoot = root || document;
24520
+ const { document, helpers, rule } = ctx;
24463
24521
 
24464
24522
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24465
24523
  if (!ariaHelpers) {
@@ -24548,7 +24606,7 @@ if (isAccTreeEligible) {
24548
24606
  }
24549
24607
  }
24550
24608
 
24551
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24609
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24552
24610
 
24553
24611
  const occurrences = [];
24554
24612
  let applicableCount = 0;
@@ -24620,8 +24678,7 @@ if (isAccTreeEligible) {
24620
24678
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24621
24679
  }), applicability: null },
24622
24680
  "aria-required-attr": { run: (function runInPage(ctx) {
24623
- const { document, root, helpers, rule } = ctx;
24624
- const safeRoot = root || document;
24681
+ const { document, helpers, rule } = ctx;
24625
24682
 
24626
24683
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24627
24684
  if (!ariaHelpers) {
@@ -24645,7 +24702,7 @@ if (isAccTreeEligible) {
24645
24702
  return v != null && String(v).trim().toLowerCase() === 'true';
24646
24703
  }
24647
24704
 
24648
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24705
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24649
24706
 
24650
24707
  const occurrences = [];
24651
24708
  let applicableCount = 0;
@@ -24702,8 +24759,7 @@ if (isAccTreeEligible) {
24702
24759
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24703
24760
  }), applicability: null },
24704
24761
  "aria-required-children": { run: (function runInPage(ctx) {
24705
- const { document, root, helpers, rule } = ctx;
24706
- const safeRoot = root || document;
24762
+ const { document, helpers, rule } = ctx;
24707
24763
 
24708
24764
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24709
24765
  if (!ariaHelpers) {
@@ -24734,7 +24790,7 @@ if (isAccTreeEligible) {
24734
24790
  // ("runInPage MUST be self-contained").
24735
24791
  const CANDIDATE_SELECTOR = '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
24736
24792
 
24737
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
24793
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24738
24794
 
24739
24795
  const occurrences = [];
24740
24796
  let applicableCount = 0;
@@ -24893,8 +24949,7 @@ if (isAccTreeEligible) {
24893
24949
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
24894
24950
  }), applicability: null },
24895
24951
  "aria-required-parent": { run: (function runInPage(ctx) {
24896
- const { document, root, helpers, rule } = ctx;
24897
- const safeRoot = root || document;
24952
+ const { document, helpers, rule } = ctx;
24898
24953
 
24899
24954
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
24900
24955
  if (!ariaHelpers) {
@@ -24980,7 +25035,7 @@ if (isAccTreeEligible) {
24980
25035
  const idTok = elId && String(elId).trim();
24981
25036
  if (!idTok) return false;
24982
25037
 
24983
- 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]');
24984
25039
  for (const owner of owners) {
24985
25040
  if (!owner || !owner.getAttribute) continue;
24986
25041
  const ownsAttr = owner.getAttribute('aria-owns') || '';
@@ -24993,7 +25048,7 @@ if (isAccTreeEligible) {
24993
25048
  return false;
24994
25049
  }
24995
25050
 
24996
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
25051
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
24997
25052
 
24998
25053
  const occurrences = [];
24999
25054
  let applicableCount = 0;
@@ -25206,15 +25261,14 @@ if (isAccTreeEligible) {
25206
25261
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
25207
25262
  }), applicability: null },
25208
25263
  "aria-roles-valid": { run: (function runInPage(ctx) {
25209
- const { document, root, helpers, rule } = ctx;
25210
- const safeRoot = root || document;
25264
+ const { document, helpers, rule } = ctx;
25211
25265
 
25212
25266
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
25213
25267
  if (!ariaHelpers) {
25214
25268
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25215
25269
  }
25216
25270
 
25217
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
25271
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
25218
25272
 
25219
25273
  const occurrences = [];
25220
25274
  let applicableCount = 0;
@@ -25269,13 +25323,12 @@ if (isAccTreeEligible) {
25269
25323
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25270
25324
  }), applicability: null },
25271
25325
  "aria-text": { run: (function runInPage(ctx) {
25272
- const { document, root, helpers, rule } = ctx;
25273
- const safeRoot = root || document;
25326
+ const { document, helpers, rule } = ctx;
25274
25327
 
25275
25328
  const FOCUSABLE_DESCENDANT_SELECTOR =
25276
25329
  'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
25277
25330
 
25278
- 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"]');
25279
25332
 
25280
25333
  const occurrences = [];
25281
25334
  let applicableCount = 0;
@@ -25329,15 +25382,14 @@ if (isAccTreeEligible) {
25329
25382
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25330
25383
  }), applicability: null },
25331
25384
  "aria-valid-attr": { run: (function runInPage(ctx) {
25332
- const { document, root, helpers, rule } = ctx;
25333
- const safeRoot = root || document;
25385
+ const { document, helpers, rule } = ctx;
25334
25386
 
25335
25387
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
25336
25388
  if (!ariaHelpers) {
25337
25389
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25338
25390
  }
25339
25391
 
25340
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
25392
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
25341
25393
 
25342
25394
  const occurrences = [];
25343
25395
  let applicableCount = 0;
@@ -25391,15 +25443,14 @@ if (isAccTreeEligible) {
25391
25443
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25392
25444
  }), applicability: null },
25393
25445
  "aria-valid-attr-value": { run: (function runInPage(ctx) {
25394
- const { document, root, helpers, rule } = ctx;
25395
- const safeRoot = root || document;
25446
+ const { document, helpers, rule } = ctx;
25396
25447
 
25397
25448
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
25398
25449
  if (!ariaHelpers) {
25399
25450
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25400
25451
  }
25401
25452
 
25402
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
25453
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
25403
25454
 
25404
25455
  const occurrences = [];
25405
25456
  let applicableCount = 0;
@@ -25456,8 +25507,7 @@ if (isAccTreeEligible) {
25456
25507
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25457
25508
  }), applicability: null },
25458
25509
  "autocomplete-valid": { run: (function runInPage(ctx) {
25459
- const { document, root, helpers, rule } = ctx;
25460
- const safeRoot = root || document;
25510
+ const { document, helpers, rule } = ctx;
25461
25511
 
25462
25512
  // Declared inside runInPage — see scripts/build-core.js header
25463
25513
  // ("runInPage MUST be self-contained").
@@ -25492,7 +25542,7 @@ if (isAccTreeEligible) {
25492
25542
  return FIELD_NAMES.has(remaining[0]);
25493
25543
  }
25494
25544
 
25495
- 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');
25496
25546
 
25497
25547
  const occurrences = [];
25498
25548
  let applicableCount = 0;
@@ -25535,12 +25585,11 @@ if (isAccTreeEligible) {
25535
25585
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25536
25586
  }), applicability: null },
25537
25587
  "avoid-inline-spacing": { run: (function runInPage(ctx) {
25538
- const { document, root, helpers, rule } = ctx;
25539
- const safeRoot = root || document;
25588
+ const { document, helpers, rule } = ctx;
25540
25589
 
25541
25590
  const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
25542
25591
 
25543
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]', safeRoot) : helpers.queryAll('[style]', safeRoot);
25592
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]') : helpers.queryAll('[style]');
25544
25593
 
25545
25594
  const occurrences = [];
25546
25595
  let applicableCount = 0;
@@ -25593,11 +25642,10 @@ if (isAccTreeEligible) {
25593
25642
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25594
25643
  }), applicability: null },
25595
25644
  "binary-control-name-present": { run: (function runInPage(ctx) {
25596
- const { document, root, helpers, rule } = ctx;
25645
+ const { document, helpers, rule } = ctx;
25597
25646
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
25598
25647
  ? helpers.getEligibilityInfo
25599
25648
  : null;
25600
- const safeRoot = root || document;
25601
25649
 
25602
25650
 
25603
25651
  function normalizeWs(s) {
@@ -25729,7 +25777,7 @@ if (isAccTreeEligible) {
25729
25777
  let applicableCount = 0;
25730
25778
 
25731
25779
  const selector = 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
25732
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
25780
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
25733
25781
 
25734
25782
 
25735
25783
  // Precompute label[for] associations once for speed/determinism.
@@ -25861,8 +25909,7 @@ if (isAccTreeEligible) {
25861
25909
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
25862
25910
  }), applicability: null },
25863
25911
  "button-name-present": { run: (function runInPage(ctx) {
25864
- const { document, root, helpers, rule } = ctx;
25865
- const safeRoot = root || document;
25912
+ const { document, helpers, rule } = ctx;
25866
25913
 
25867
25914
  const occurrences = [];
25868
25915
  let applicableCount = 0;
@@ -25899,7 +25946,7 @@ if (isAccTreeEligible) {
25899
25946
  }
25900
25947
 
25901
25948
  const selector = 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
25902
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
25949
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
25903
25950
 
25904
25951
  for (const el of nodes) {
25905
25952
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -25911,6 +25958,7 @@ if (isAccTreeEligible) {
25911
25958
 
25912
25959
  const tag = (el.tagName || '').toLowerCase();
25913
25960
  const role = el.getAttribute ? el.getAttribute('role') : null;
25961
+ const roleNorm = normalizeWs(role).toLowerCase();
25914
25962
 
25915
25963
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
25916
25964
 
@@ -25926,7 +25974,17 @@ if (isAccTreeEligible) {
25926
25974
  inputValueName = getInputButtonValueName(el);
25927
25975
  }
25928
25976
 
25929
- 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);
25930
25988
  const contentName =
25931
25989
  (!trustedProgrammaticName && !inputValueName && isContentNameCandidate)
25932
25990
  ? getConservativeSubtreeText(el)
@@ -25986,12 +26044,41 @@ if (isAccTreeEligible) {
25986
26044
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
25987
26045
  }
25988
26046
 
25989
- function hasMainLandmark() {
26047
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
26048
+
26049
+ function isExposedToAt(el) {
26050
+ if (!isAccTreeEligible) return true;
25990
26051
  try {
25991
- return !!document.querySelector('main, [role="main"]');
26052
+ const r = isAccTreeEligible(el, ctx);
26053
+ if (typeof r === 'boolean') return r;
26054
+ return !!(r && r.eligible);
25992
26055
  } catch {
25993
- 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;
25994
26080
  }
26081
+ return false;
25995
26082
  }
25996
26083
 
25997
26084
  function hasWorkingAnchorLink() {
@@ -26033,11 +26120,10 @@ if (isAccTreeEligible) {
26033
26120
  }
26034
26121
 
26035
26122
  function hasHeading() {
26036
- try {
26037
- return !!document.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
26038
- } catch {
26039
- return false;
26123
+ for (const el of queryAll('h1, h2, h3, h4, h5, h6, [role="heading"]')) {
26124
+ if (el && isExposedToAt(el)) return true;
26040
26125
  }
26126
+ return false;
26041
26127
  }
26042
26128
 
26043
26129
  const mainLandmark = hasMainLandmark();
@@ -26295,11 +26381,10 @@ if (isAccTreeEligible) {
26295
26381
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
26296
26382
  }), applicability: null },
26297
26383
  "combobox-name-present": { run: (function runInPage(ctx) {
26298
- const { document, root, helpers, rule } = ctx;
26384
+ const { document, helpers, rule } = ctx;
26299
26385
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
26300
26386
  ? helpers.getEligibilityInfo
26301
26387
  : null;
26302
- const safeRoot = root || document;
26303
26388
 
26304
26389
 
26305
26390
  function normalizeWs(s) {
@@ -26431,7 +26516,7 @@ if (isAccTreeEligible) {
26431
26516
  let applicableCount = 0;
26432
26517
 
26433
26518
  const selector = '[role="combobox"]';
26434
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
26519
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
26435
26520
 
26436
26521
  // Precompute label[for] map for combobox elements that are labelable
26437
26522
  // native form controls (e.g. <input role="combobox">).
@@ -28158,14 +28243,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28158
28243
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
28159
28244
  }), applicability: null },
28160
28245
  "definition-list-children-valid": { run: (function runInPage(ctx) {
28161
- const { document, root, helpers, rule } = ctx;
28162
- const safeRoot = root || document;
28246
+ const { document, helpers, rule } = ctx;
28163
28247
 
28164
28248
  // Declared inside runInPage — see scripts/build-core.js header
28165
28249
  // ("runInPage MUST be self-contained").
28166
28250
  const PASSTHROUGH_TAGS = new Set(['dt', 'dd', 'script', 'template', 'style']);
28167
28251
 
28168
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl', safeRoot) : helpers.queryAll('dl', safeRoot);
28252
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl') : helpers.queryAll('dl');
28169
28253
 
28170
28254
  const occurrences = [];
28171
28255
  let applicableCount = 0;
@@ -28249,10 +28333,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28249
28333
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
28250
28334
  }), applicability: null },
28251
28335
  "deprecated-elements-not-used": { run: (function runInPage(ctx) {
28252
- const { document, root, helpers, rule } = ctx;
28253
- const safeRoot = root || document;
28336
+ const { document, helpers, rule } = ctx;
28254
28337
 
28255
- 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');
28256
28339
 
28257
28340
  const occurrences = [];
28258
28341
  let applicableCount = 0;
@@ -28291,11 +28374,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28291
28374
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
28292
28375
  }), applicability: null },
28293
28376
  "dialog-name-present": { run: (function runInPage(ctx) {
28294
- const { document, root, helpers, rule } = ctx;
28377
+ const { document, helpers, rule } = ctx;
28295
28378
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
28296
28379
  ? helpers.getEligibilityInfo
28297
28380
  : null;
28298
- const safeRoot = root || document;
28299
28381
 
28300
28382
 
28301
28383
  function normalizeWs(s) {
@@ -28409,7 +28491,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28409
28491
  let applicableCount = 0;
28410
28492
 
28411
28493
  const selector = '[role="dialog"],[role="alertdialog"]';
28412
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
28494
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
28413
28495
 
28414
28496
 
28415
28497
  function evaluate(el) {
@@ -28474,10 +28556,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
28474
28556
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
28475
28557
  }), applicability: null },
28476
28558
  "dlitem-parent-valid": { run: (function runInPage(ctx) {
28477
- const { document, root, helpers, rule } = ctx;
28478
- const safeRoot = root || document;
28559
+ const { document, helpers, rule } = ctx;
28479
28560
 
28480
- 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');
28481
28562
 
28482
28563
  const occurrences = [];
28483
28564
  let applicableCount = 0;
@@ -29129,8 +29210,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
29129
29210
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
29130
29211
  }), applicability: null },
29131
29212
  "focus-order-semantics": { run: (function runInPage(ctx) {
29132
- const { document, root, helpers, rule } = ctx;
29133
- const safeRoot = root || document;
29213
+ const { document, helpers, rule } = ctx;
29134
29214
 
29135
29215
  const NON_INTERACTIVE_ROLES = new Set([
29136
29216
  'generic', 'group', 'text', 'presentation', 'none', 'img', 'heading',
@@ -29140,7 +29220,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
29140
29220
  'definition', 'paragraph', 'caption', 'status', 'alert', 'log', 'tooltip'
29141
29221
  ]);
29142
29222
 
29143
- 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]');
29144
29224
 
29145
29225
  const occurrences = [];
29146
29226
  let applicableCount = 0;
@@ -29781,11 +29861,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
29781
29861
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [], data: { details: { metrics } } };
29782
29862
  }), applicability: null },
29783
29863
  "form-control-single-label": { run: (function runInPage(ctx) {
29784
- const { document, root, helpers, rule } = ctx;
29785
- const safeRoot = root || document;
29864
+ const { document, helpers, rule } = ctx;
29786
29865
 
29787
29866
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
29788
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
29867
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
29789
29868
 
29790
29869
  // Build a for-value -> label[] map once (avoids per-control dynamic
29791
29870
  // attribute-selector construction / CSS.escape, which is not guaranteed
@@ -30063,14 +30142,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30063
30142
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
30064
30143
  }), applicability: null },
30065
30144
  "identical-links-same-purpose": { run: (function runInPage(ctx) {
30066
- const { document, root, helpers, rule } = ctx;
30067
- const safeRoot = root || document;
30145
+ const { document, helpers, rule } = ctx;
30068
30146
 
30069
30147
  function normName(s) {
30070
30148
  return (s == null ? '' : String(s)).replace(/\s+/g, ' ').trim().toLowerCase();
30071
30149
  }
30072
30150
 
30073
- 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]');
30074
30152
 
30075
30153
  const groups = new Map(); // normName -> [{ el, href }]
30076
30154
  let applicableCount = 0;
@@ -30150,8 +30228,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30150
30228
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
30151
30229
  }), applicability: null },
30152
30230
  "iframe-focusable-content": { run: (function runInPage(ctx) {
30153
- const { document, root, helpers, rule } = ctx;
30154
- const safeRoot = root || document;
30231
+ const { document, helpers, rule } = ctx;
30155
30232
 
30156
30233
  function hasFocusableCandidate(doc) {
30157
30234
  if (!doc || !doc.querySelectorAll) return false;
@@ -30183,7 +30260,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30183
30260
  return !Number.isNaN(n) && n < 0;
30184
30261
  }
30185
30262
 
30186
- 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');
30187
30264
 
30188
30265
  const occurrences = [];
30189
30266
  let applicableCount = 0;
@@ -30233,10 +30310,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30233
30310
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
30234
30311
  }), applicability: null },
30235
30312
  "iframe-name-present": { run: (function runInPage(ctx) {
30236
- const { document, root, helpers, rule } = ctx;
30237
- const safeRoot = root || document;
30313
+ const { document, helpers, rule } = ctx;
30238
30314
 
30239
- 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');
30240
30316
 
30241
30317
  const occurrences = [];
30242
30318
  let applicableCount = 0;
@@ -30288,10 +30364,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30288
30364
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
30289
30365
  }), applicability: null },
30290
30366
  "iframe-title-unique": { run: (function runInPage(ctx) {
30291
- const { document, root, helpers, rule } = ctx;
30292
- const safeRoot = root || document;
30367
+ const { document, helpers, rule } = ctx;
30293
30368
 
30294
- 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');
30295
30370
 
30296
30371
  const groups = new Map(); // trimmed title -> elements[]
30297
30372
  let applicableCount = 0;
@@ -30346,8 +30421,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30346
30421
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
30347
30422
  }), applicability: null },
30348
30423
  "image-redundant-alt": { run: (function runInPage(ctx) {
30349
- const { document, root, helpers, rule } = ctx;
30350
- const safeRoot = root || document;
30424
+ const { document, helpers, rule } = ctx;
30351
30425
 
30352
30426
  function normalizeWs(s) {
30353
30427
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -30366,7 +30440,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
30366
30440
  return normalizeWs(text);
30367
30441
  }
30368
30442
 
30369
- 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]');
30370
30444
 
30371
30445
  const occurrences = [];
30372
30446
  let applicableCount = 0;
@@ -31154,8 +31228,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31154
31228
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
31155
31229
  }), applicability: null },
31156
31230
  "label-in-name": { run: (function runInPage(ctx) {
31157
- const { document, root, helpers, rule } = ctx;
31158
- const safeRoot = root || document;
31231
+ const { document, helpers, rule } = ctx;
31159
31232
 
31160
31233
  const occurrences = [];
31161
31234
  let applicableCount = 0;
@@ -31164,7 +31237,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31164
31237
  // NOTE: aria-hidden is intentionally NOT excluded here; it does not affect visual rendering.
31165
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])';
31166
31239
 
31167
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
31240
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
31168
31241
 
31169
31242
  function norm(s) {
31170
31243
  const v = (s == null ? '' : String(s));
@@ -31302,11 +31375,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31302
31375
  }
31303
31376
 
31304
31377
  // Fallback: label[for=id]. Uses `document` directly rather than
31305
- // ctx.root/safeRoot -- label[for] association is a document-wide
31306
- // relationship (IDs are document-unique), not bounded by whatever
31307
- // contextSelector region happens to be scanned, and ctx.root is an
31308
- // array (multi-region contextSelector support), not a single element
31309
- // 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.
31310
31383
  try {
31311
31384
  const idAttribute = control && control.getAttribute ? (control.getAttribute('id') || '') : '';
31312
31385
  const key = String(idAttribute || '').trim();
@@ -31447,11 +31520,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
31447
31520
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
31448
31521
  }), applicability: null },
31449
31522
  "label-title-only": { run: (function runInPage(ctx) {
31450
- const { document, root, helpers, rule } = ctx;
31451
- const safeRoot = root || document;
31523
+ const { document, helpers, rule } = ctx;
31452
31524
 
31453
31525
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
31454
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
31526
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
31455
31527
 
31456
31528
  const labelsByFor = new Map();
31457
31529
  const allLabels = document.getElementsByTagName ? document.getElementsByTagName('label') : [];
@@ -32498,8 +32570,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32498
32570
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
32499
32571
  }), applicability: null },
32500
32572
  "link-in-text-block": { run: (function runInPage(ctx) {
32501
- const { document, root, helpers, rule, engineOptions } = ctx;
32502
- const safeRoot = root || document;
32573
+ const { document, helpers, rule, engineOptions } = ctx;
32503
32574
 
32504
32575
  function safeComputedStyle(el) {
32505
32576
  try {
@@ -32545,7 +32616,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32545
32616
  const c = helpers && helpers.contrast ? helpers.contrast : null;
32546
32617
 
32547
32618
  const selector = 'a[href]';
32548
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
32619
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
32549
32620
 
32550
32621
  const occurrences = [];
32551
32622
  let applicableCount = 0;
@@ -32645,8 +32716,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32645
32716
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
32646
32717
  }), applicability: null },
32647
32718
  "link-name-present": { run: (function runInPage(ctx) {
32648
- const { document, root, helpers, rule } = ctx;
32649
- const safeRoot = root || document;
32719
+ const { document, helpers, rule } = ctx;
32650
32720
 
32651
32721
  const occurrences = [];
32652
32722
  let applicableCount = 0;
@@ -32667,7 +32737,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32667
32737
  }
32668
32738
 
32669
32739
  const selector = 'a[href], area[href], [role="link"]';
32670
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
32740
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
32671
32741
 
32672
32742
  for (const el of nodes) {
32673
32743
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -32680,7 +32750,20 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32680
32750
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
32681
32751
  const programmaticName = (nameInfo && typeof nameInfo.value === 'string') ? nameInfo.value : '';
32682
32752
 
32683
- 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) : '';
32684
32767
  const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
32685
32768
 
32686
32769
  if (finalName.length === 0) {
@@ -32732,8 +32815,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32732
32815
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
32733
32816
  }), applicability: null },
32734
32817
  "link-name-quality": { run: (function runInPage(ctx) {
32735
- const { document, root, helpers, rule } = ctx;
32736
- const safeRoot = root || document;
32818
+ const { document, helpers, rule } = ctx;
32737
32819
 
32738
32820
  const GENERIC_LINK_TEXT = new Set([
32739
32821
  'click here', 'here', 'click', 'more', 'more info', 'more information',
@@ -32752,7 +32834,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32752
32834
  }
32753
32835
 
32754
32836
  const selector = 'a[href]';
32755
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
32837
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
32756
32838
 
32757
32839
  const occurrences = [];
32758
32840
  let applicableCount = 0;
@@ -32812,14 +32894,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32812
32894
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
32813
32895
  }), applicability: null },
32814
32896
  "list-children-valid": { run: (function runInPage(ctx) {
32815
- const { document, root, helpers, rule } = ctx;
32816
- const safeRoot = root || document;
32897
+ const { document, helpers, rule } = ctx;
32817
32898
 
32818
32899
  // Declared inside runInPage — see scripts/build-core.js header
32819
32900
  // ("runInPage MUST be self-contained").
32820
32901
  const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
32821
32902
 
32822
- 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');
32823
32904
 
32824
32905
  const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
32825
32906
 
@@ -32890,11 +32971,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32890
32971
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
32891
32972
  }), applicability: null },
32892
32973
  "listbox-name-present": { run: (function runInPage(ctx) {
32893
- const { document, root, helpers, rule } = ctx;
32974
+ const { document, helpers, rule } = ctx;
32894
32975
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
32895
32976
  ? helpers.getEligibilityInfo
32896
32977
  : null;
32897
- const safeRoot = root || document;
32898
32978
 
32899
32979
 
32900
32980
  function normalizeWs(s) {
@@ -32999,7 +33079,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
32999
33079
  let applicableCount = 0;
33000
33080
 
33001
33081
  const selector = "[role=\"listbox\"]";
33002
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
33082
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
33003
33083
 
33004
33084
  // Precompute label[for] map for listbox elements that are labelable
33005
33085
  // native form controls (e.g. <select multiple role="listbox">).
@@ -33100,10 +33180,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33100
33180
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
33101
33181
  }), applicability: null },
33102
33182
  "listitem-parent-valid": { run: (function runInPage(ctx) {
33103
- const { document, root, helpers, rule } = ctx;
33104
- const safeRoot = root || document;
33183
+ const { document, helpers, rule } = ctx;
33105
33184
 
33106
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li', safeRoot) : helpers.queryAll('li', safeRoot);
33185
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li') : helpers.queryAll('li');
33107
33186
 
33108
33187
  const occurrences = [];
33109
33188
  let applicableCount = 0;
@@ -33532,11 +33611,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33532
33611
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
33533
33612
  }), applicability: null },
33534
33613
  "menuitem-name-present": { run: (function runInPage(ctx) {
33535
- const { document, root, helpers, rule } = ctx;
33614
+ const { document, helpers, rule } = ctx;
33536
33615
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
33537
33616
  ? helpers.getEligibilityInfo
33538
33617
  : null;
33539
- const safeRoot = root || document;
33540
33618
 
33541
33619
 
33542
33620
  function normalizeWs(s) {
@@ -33642,7 +33720,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33642
33720
  let applicableCount = 0;
33643
33721
 
33644
33722
  const selector = '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]';
33645
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
33723
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
33646
33724
 
33647
33725
 
33648
33726
  function evaluate(el) {
@@ -33948,11 +34026,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
33948
34026
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
33949
34027
  }), applicability: null },
33950
34028
  "meter-name-present": { run: (function runInPage(ctx) {
33951
- const { document, root, helpers, rule } = ctx;
34029
+ const { document, helpers, rule } = ctx;
33952
34030
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
33953
34031
  ? helpers.getEligibilityInfo
33954
34032
  : null;
33955
- const safeRoot = root || document;
33956
34033
 
33957
34034
 
33958
34035
  function normalizeWs(s) {
@@ -34016,7 +34093,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34016
34093
  let applicableCount = 0;
34017
34094
 
34018
34095
  const selector = '[role="meter"]';
34019
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
34096
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
34020
34097
 
34021
34098
 
34022
34099
  function evaluate(el) {
@@ -34082,8 +34159,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34082
34159
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
34083
34160
  }), applicability: null },
34084
34161
  "mouse-only-event-handlers": { run: (function runInPage(ctx) {
34085
- const { document, root, helpers, rule } = ctx;
34086
- const safeRoot = root || document;
34162
+ const { document, helpers, rule } = ctx;
34087
34163
 
34088
34164
  const MOUSE_ONLY_ATTRS = [
34089
34165
  'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup',
@@ -34094,7 +34170,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34094
34170
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
34095
34171
 
34096
34172
  const selector = MOUSE_ONLY_ATTRS.map((a) => `[${a}]`).join(', ');
34097
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
34173
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
34098
34174
 
34099
34175
  const occurrences = [];
34100
34176
  let applicableCount = 0;
@@ -34146,8 +34222,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34146
34222
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
34147
34223
  }), applicability: null },
34148
34224
  "nested-interactive-controls-absent": { run: (function runInPage(ctx) {
34149
- const { document, root, helpers, rule } = ctx;
34150
- const safeRoot = root || document;
34225
+ const { document, helpers, rule } = ctx;
34151
34226
 
34152
34227
  // Declared inside runInPage — see scripts/build-core.js header
34153
34228
  // ("runInPage MUST be self-contained").
@@ -34176,7 +34251,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34176
34251
  '[role="treeitem"]'
34177
34252
  ].join(', ');
34178
34253
 
34179
- 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);
34180
34255
 
34181
34256
  const occurrences = [];
34182
34257
  let applicableCount = 0;
@@ -34225,10 +34300,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34225
34300
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
34226
34301
  }), applicability: null },
34227
34302
  "no-autoplay-audio": { run: (function runInPage(ctx) {
34228
- const { document, root, helpers, rule } = ctx;
34229
- const safeRoot = root || document;
34303
+ const { document, helpers, rule } = ctx;
34230
34304
 
34231
- 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]');
34232
34306
 
34233
34307
  const occurrences = [];
34234
34308
  let applicableCount = 0;
@@ -34582,11 +34656,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34582
34656
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
34583
34657
  }), applicability: null },
34584
34658
  "option-name-present": { run: (function runInPage(ctx) {
34585
- const { document, root, helpers, rule } = ctx;
34659
+ const { document, helpers, rule } = ctx;
34586
34660
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
34587
34661
  ? helpers.getEligibilityInfo
34588
34662
  : null;
34589
- const safeRoot = root || document;
34590
34663
 
34591
34664
 
34592
34665
  function normalizeWs(s) {
@@ -34650,7 +34723,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34650
34723
  let applicableCount = 0;
34651
34724
 
34652
34725
  const selector = "[role=\"option\"]";
34653
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
34726
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
34654
34727
 
34655
34728
 
34656
34729
  function hasName(el) {
@@ -34713,8 +34786,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34713
34786
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
34714
34787
  }), applicability: null },
34715
34788
  "p-as-heading": { run: (function runInPage(ctx) {
34716
- const { document, root, helpers, rule } = ctx;
34717
- const safeRoot = root || document;
34789
+ const { document, helpers, rule } = ctx;
34718
34790
 
34719
34791
  const MAX_HEADING_LIKE_CHARS = 120;
34720
34792
  const MIN_FONT_SIZE_PX = 18;
@@ -34766,7 +34838,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34766
34838
  return Number.isFinite(px) ? px : 0;
34767
34839
  }
34768
34840
 
34769
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p', safeRoot) : helpers.queryAll('p', safeRoot);
34841
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p') : helpers.queryAll('p');
34770
34842
 
34771
34843
  const occurrences = [];
34772
34844
  let applicableCount = 0;
@@ -34848,14 +34920,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
34848
34920
  return tag === 'h1';
34849
34921
  }
34850
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.
34851
34938
  let nodes = [];
34852
34939
  try {
34853
- nodes = document.querySelectorAll('h1, [role]');
34940
+ nodes = helpers && typeof helpers.queryAllSmart === 'function'
34941
+ ? helpers.queryAllSmart('h1, [role]')
34942
+ : document.querySelectorAll('h1, [role]');
34854
34943
  } catch {
34855
34944
  nodes = [];
34856
34945
  }
34857
34946
 
34858
- const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el));
34947
+ const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el) && isExposedToAt(el));
34859
34948
 
34860
34949
  if (hasH1) {
34861
34950
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -35179,8 +35268,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35179
35268
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
35180
35269
  }), applicability: null },
35181
35270
  "presentation-role-conflict": { run: (function runInPage(ctx) {
35182
- const { document, root, helpers, rule } = ctx;
35183
- const safeRoot = root || document;
35271
+ const { document, helpers, rule } = ctx;
35184
35272
 
35185
35273
  // The full set of ARIA attributes marked `global: true` per the WAI-ARIA
35186
35274
  // spec (confirmed against a widely-used reference engine's own
@@ -35197,8 +35285,8 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35197
35285
  const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
35198
35286
 
35199
35287
  const nodes = helpers.queryAllSmart
35200
- ? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]', safeRoot)
35201
- : 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=""]');
35202
35290
 
35203
35291
  const occurrences = [];
35204
35292
  let applicableCount = 0;
@@ -35262,11 +35350,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35262
35350
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
35263
35351
  }), applicability: null },
35264
35352
  "progressbar-name-present": { run: (function runInPage(ctx) {
35265
- const { document, root, helpers, rule } = ctx;
35353
+ const { document, helpers, rule } = ctx;
35266
35354
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
35267
35355
  ? helpers.getEligibilityInfo
35268
35356
  : null;
35269
- const safeRoot = root || document;
35270
35357
 
35271
35358
 
35272
35359
  function normalizeWs(s) {
@@ -35330,7 +35417,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35330
35417
  let applicableCount = 0;
35331
35418
 
35332
35419
  const selector = '[role="progressbar"]';
35333
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
35420
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
35334
35421
 
35335
35422
 
35336
35423
  function evaluate(el) {
@@ -35742,8 +35829,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35742
35829
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
35743
35830
  }), applicability: null },
35744
35831
  "scrollable-region-focusable": { run: (function runInPage(ctx) {
35745
- const { document, root, helpers, rule } = ctx;
35746
- const safeRoot = root || document;
35832
+ const { document, helpers, rule } = ctx;
35747
35833
 
35748
35834
  function safeComputedStyle(el) {
35749
35835
  try {
@@ -35788,7 +35874,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35788
35874
  }
35789
35875
 
35790
35876
  const CANDIDATE_SELECTOR = 'div, section, article, aside, main, nav, pre, table, blockquote, ul, ol, textarea';
35791
- 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);
35792
35878
 
35793
35879
  const occurrences = [];
35794
35880
  let applicableCount = 0;
@@ -35843,11 +35929,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35843
35929
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
35844
35930
  }), applicability: null },
35845
35931
  "searchbox-name-present": { run: (function runInPage(ctx) {
35846
- const { document, root, helpers, rule } = ctx;
35932
+ const { document, helpers, rule } = ctx;
35847
35933
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
35848
35934
  ? helpers.getEligibilityInfo
35849
35935
  : null;
35850
- const safeRoot = root || document;
35851
35936
 
35852
35937
 
35853
35938
  function normalizeWs(s) {
@@ -35952,7 +36037,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
35952
36037
  let applicableCount = 0;
35953
36038
 
35954
36039
  const selector = "[role=\"searchbox\"]";
35955
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
36040
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
35956
36041
 
35957
36042
  // Precompute label[for] map for searchbox elements that are labelable
35958
36043
  // native form controls (e.g. <input role="searchbox">).
@@ -36053,10 +36138,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36053
36138
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36054
36139
  }), applicability: null },
36055
36140
  "server-side-image-map-absent": { run: (function runInPage(ctx) {
36056
- const { document, root, helpers, rule } = ctx;
36057
- const safeRoot = root || document;
36141
+ const { document, helpers, rule } = ctx;
36058
36142
 
36059
- 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]');
36060
36144
 
36061
36145
  const occurrences = [];
36062
36146
  let applicableCount = 0;
@@ -36094,8 +36178,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36094
36178
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36095
36179
  }), applicability: null },
36096
36180
  "skip-link": { run: (function runInPage(ctx) {
36097
- const { document, root, helpers, rule } = ctx;
36098
- const safeRoot = root || document;
36181
+ const { document, helpers, rule } = ctx;
36099
36182
 
36100
36183
  function normalizeWs(s) {
36101
36184
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -36122,7 +36205,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36122
36205
  return normalizeWs(el.textContent);
36123
36206
  }
36124
36207
 
36125
- 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]');
36126
36233
 
36127
36234
  const occurrences = [];
36128
36235
  let applicableCount = 0;
@@ -36160,7 +36267,63 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36160
36267
  }
36161
36268
  }
36162
36269
 
36163
- 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
+ }
36164
36327
 
36165
36328
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
36166
36329
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
@@ -36190,11 +36353,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36190
36353
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
36191
36354
  }), applicability: null },
36192
36355
  "slider-name-present": { run: (function runInPage(ctx) {
36193
- const { document, root, helpers, rule } = ctx;
36356
+ const { document, helpers, rule } = ctx;
36194
36357
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
36195
36358
  ? helpers.getEligibilityInfo
36196
36359
  : null;
36197
- const safeRoot = root || document;
36198
36360
 
36199
36361
 
36200
36362
  function normalizeWs(s) {
@@ -36326,7 +36488,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36326
36488
  let applicableCount = 0;
36327
36489
 
36328
36490
  const selector = 'input[type="range"], [role="slider"]';
36329
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
36491
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
36330
36492
 
36331
36493
 
36332
36494
  // Precompute label[for] map for native range inputs.
@@ -36440,11 +36602,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36440
36602
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36441
36603
  }), applicability: null },
36442
36604
  "spinbutton-name-present": { run: (function runInPage(ctx) {
36443
- const { document, root, helpers, rule } = ctx;
36605
+ const { document, helpers, rule } = ctx;
36444
36606
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
36445
36607
  ? helpers.getEligibilityInfo
36446
36608
  : null;
36447
- const safeRoot = root || document;
36448
36609
 
36449
36610
 
36450
36611
  function normalizeWs(s) {
@@ -36549,7 +36710,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36549
36710
  let applicableCount = 0;
36550
36711
 
36551
36712
  const selector = "[role=\"spinbutton\"]";
36552
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
36713
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
36553
36714
 
36554
36715
  // Precompute label[for] map for spinbutton elements that are labelable
36555
36716
  // native form controls (e.g. <input role="spinbutton">).
@@ -36650,11 +36811,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36650
36811
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
36651
36812
  }), applicability: null },
36652
36813
  "summary-name-present": { run: (function runInPage(ctx) {
36653
- const { document, root, helpers, rule } = ctx;
36814
+ const { document, helpers, rule } = ctx;
36654
36815
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
36655
36816
  ? helpers.getEligibilityInfo
36656
36817
  : null;
36657
- const safeRoot = root || document;
36658
36818
 
36659
36819
 
36660
36820
  function normalizeWs(s) {
@@ -36717,7 +36877,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
36717
36877
  const occurrences = [];
36718
36878
  let applicableCount = 0;
36719
36879
 
36720
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary', safeRoot) : helpers.queryAll('summary', safeRoot);
36880
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary') : helpers.queryAll('summary');
36721
36881
 
36722
36882
  function evaluate(el) {
36723
36883
  const ariaLabel = getAttr(el, 'aria-label');
@@ -37356,11 +37516,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37356
37516
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
37357
37517
  }), applicability: null },
37358
37518
  "tab-name-present": { run: (function runInPage(ctx) {
37359
- const { document, root, helpers, rule } = ctx;
37519
+ const { document, helpers, rule } = ctx;
37360
37520
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
37361
37521
  ? helpers.getEligibilityInfo
37362
37522
  : null;
37363
- const safeRoot = root || document;
37364
37523
 
37365
37524
 
37366
37525
  function normalizeWs(s) {
@@ -37466,7 +37625,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37466
37625
  let applicableCount = 0;
37467
37626
 
37468
37627
  const selector = '[role="tab"]';
37469
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
37628
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
37470
37629
 
37471
37630
 
37472
37631
  function evaluate(el) {
@@ -37581,14 +37740,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37581
37740
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
37582
37741
  }), applicability: null },
37583
37742
  "table-duplicate-name": { run: (function runInPage(ctx) {
37584
- const { document, root, helpers, rule } = ctx;
37585
- const safeRoot = root || document;
37743
+ const { document, helpers, rule } = ctx;
37586
37744
 
37587
37745
  function normalizeWs(s) {
37588
37746
  return String(s || '').replace(/\s+/g, ' ').trim();
37589
37747
  }
37590
37748
 
37591
- 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]');
37592
37750
 
37593
37751
  const occurrences = [];
37594
37752
  let applicableCount = 0;
@@ -37634,12 +37792,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37634
37792
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
37635
37793
  }), applicability: null },
37636
37794
  "table-fake-caption": { run: (function runInPage(ctx) {
37637
- const { document, root, helpers, rule } = ctx;
37638
- const safeRoot = root || document;
37795
+ const { document, helpers, rule } = ctx;
37639
37796
 
37640
37797
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
37641
37798
 
37642
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
37799
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
37643
37800
 
37644
37801
  const occurrences = [];
37645
37802
  let applicableCount = 0;
@@ -37702,10 +37859,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37702
37859
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
37703
37860
  }), applicability: null },
37704
37861
  "table-headers-attr-valid": { run: (function runInPage(ctx) {
37705
- const { document, root, helpers, rule } = ctx;
37706
- const safeRoot = root || document;
37862
+ const { document, helpers, rule } = ctx;
37707
37863
 
37708
- 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]');
37709
37865
 
37710
37866
  const occurrences = [];
37711
37867
  let applicableCount = 0;
@@ -37779,10 +37935,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
37779
37935
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
37780
37936
  }), applicability: null },
37781
37937
  "table-th-has-data-cells": { run: (function runInPage(ctx) {
37782
- const { document, root, helpers, rule } = ctx;
37783
- const safeRoot = root || document;
37938
+ const { document, helpers, rule } = ctx;
37784
37939
 
37785
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
37940
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
37786
37941
 
37787
37942
  const occurrences = [];
37788
37943
  let applicableCount = 0;
@@ -38341,14 +38496,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38341
38496
  return { ruleId: RULE_ID, outcome: 'pass', severity: 'minor', occurrences: [] };
38342
38497
  }), applicability: null },
38343
38498
  "td-has-header": { run: (function runInPage(ctx) {
38344
- const { document, root, helpers, rule } = ctx;
38345
- const safeRoot = root || document;
38499
+ const { document, helpers, rule } = ctx;
38346
38500
 
38347
38501
  const MIN_SIZE = 4;
38348
38502
 
38349
38503
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
38350
38504
 
38351
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
38505
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
38352
38506
 
38353
38507
  const occurrences = [];
38354
38508
  let applicableCount = 0;
@@ -38434,11 +38588,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38434
38588
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38435
38589
  }), applicability: null },
38436
38590
  "textbox-name-present": { run: (function runInPage(ctx) {
38437
- const { document, root, helpers, rule } = ctx;
38591
+ const { document, helpers, rule } = ctx;
38438
38592
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
38439
38593
  ? helpers.getEligibilityInfo
38440
38594
  : null;
38441
- const safeRoot = root || document;
38442
38595
 
38443
38596
 
38444
38597
  function normalizeWs(s) {
@@ -38543,7 +38696,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38543
38696
  let applicableCount = 0;
38544
38697
 
38545
38698
  const selector = "[role=\"textbox\"]";
38546
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
38699
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
38547
38700
 
38548
38701
  // Precompute label[for] map for textbox elements that are labelable
38549
38702
  // native form controls (e.g. <input role="textbox">).
@@ -38644,11 +38797,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38644
38797
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38645
38798
  }), applicability: null },
38646
38799
  "tooltip-name-present": { run: (function runInPage(ctx) {
38647
- const { document, root, helpers, rule } = ctx;
38800
+ const { document, helpers, rule } = ctx;
38648
38801
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
38649
38802
  ? helpers.getEligibilityInfo
38650
38803
  : null;
38651
- const safeRoot = root || document;
38652
38804
 
38653
38805
 
38654
38806
  function normalizeWs(s) {
@@ -38712,7 +38864,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38712
38864
  let applicableCount = 0;
38713
38865
 
38714
38866
  const selector = '[role="tooltip"]';
38715
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
38867
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
38716
38868
 
38717
38869
 
38718
38870
  function evaluate(el) {
@@ -38776,11 +38928,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38776
38928
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38777
38929
  }), applicability: null },
38778
38930
  "treeitem-name-present": { run: (function runInPage(ctx) {
38779
- const { document, root, helpers, rule } = ctx;
38931
+ const { document, helpers, rule } = ctx;
38780
38932
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
38781
38933
  ? helpers.getEligibilityInfo
38782
38934
  : null;
38783
- const safeRoot = root || document;
38784
38935
 
38785
38936
 
38786
38937
  function normalizeWs(s) {
@@ -38844,7 +38995,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38844
38995
  let applicableCount = 0;
38845
38996
 
38846
38997
  const selector = "[role=\"treeitem\"]";
38847
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
38998
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
38848
38999
 
38849
39000
 
38850
39001
  function hasName(el) {
@@ -38907,12 +39058,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38907
39058
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38908
39059
  }), applicability: null },
38909
39060
  "valid-lang": { run: (function runInPage(ctx) {
38910
- const { document, root, helpers, rule } = ctx;
38911
- const safeRoot = root || document;
39061
+ const { document, helpers, rule } = ctx;
38912
39062
 
38913
39063
  const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
38914
39064
 
38915
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]', safeRoot) : helpers.queryAll('[lang]', safeRoot);
39065
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]') : helpers.queryAll('[lang]');
38916
39066
 
38917
39067
  const occurrences = [];
38918
39068
  let applicableCount = 0;
@@ -38957,10 +39107,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
38957
39107
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
38958
39108
  }), applicability: null },
38959
39109
  "video-caption": { run: (function runInPage(ctx) {
38960
- const { document, root, helpers, rule } = ctx;
38961
- const safeRoot = root || document;
39110
+ const { document, helpers, rule } = ctx;
38962
39111
 
38963
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video', safeRoot) : helpers.queryAll('video', safeRoot);
39112
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video') : helpers.queryAll('video');
38964
39113
 
38965
39114
  const occurrences = [];
38966
39115
  let applicableCount = 0;
@@ -39682,10 +39831,12 @@ const I18N = {
39682
39831
  "region_description": "Checks that direct children of <body> with visible text content are contained within a landmark region.",
39683
39832
  "region_summary_cantTell": "This content is not contained within a landmark region.",
39684
39833
  "region_hint_cantTell": "Move this content inside a landmark region (main, nav, aside, a labeled section, etc.).",
39685
- "skipLink_title": "Skip link must have a resolvable target",
39686
- "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.",
39687
39836
  "skipLink_summary_cantTell": "This skip link's target does not exist.",
39688
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.",
39689
39840
  "autocompleteValid_title": "autocomplete attribute must be a valid autofill value",
39690
39841
  "autocompleteValid_description": "Checks that a non-empty autocomplete attribute is \"on\"/\"off\" or a well-formed autofill detail token list.",
39691
39842
  "autocompleteValid_summary_fail": "This autocomplete attribute value is not a valid autofill value.",
@@ -40284,10 +40435,12 @@ const I18N = {
40284
40435
  "region_description": "Vérifie que les enfants directs de <body> ayant un contenu textuel visible sont contenus dans un point de repère.",
40285
40436
  "region_summary_cantTell": "Ce contenu n’est contenu dans aucun point de repère.",
40286
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.).",
40287
- "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout",
40288
- "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.",
40289
40440
  "skipLink_summary_cantTell": "La cible de ce lien d’évitement n’existe pas.",
40290
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.",
40291
40444
  "autocompleteValid_title": "L’attribut autocomplete doit être une valeur d’auto-remplissage valide",
40292
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.",
40293
40446
  "autocompleteValid_summary_fail": "Cette valeur d’attribut autocomplete n’est pas une valeur d’auto-remplissage valide.",
@@ -44070,7 +44223,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
44070
44223
  'templateContent',
44071
44224
  'nonRenderedElement',
44072
44225
  'inputHidden',
44073
- 'visibilityHidden'
44226
+ 'visibilityHidden',
44227
+ 'contentVisibilityHidden'
44074
44228
  ]);
44075
44229
 
44076
44230
  function queryAllSmart(sel) {
@@ -44087,6 +44241,23 @@ const createDomHelpers = (function createDomHelpers(opts) {
44087
44241
  for (const r of reasons) {
44088
44242
  if (HARD_HIDDEN_REASONS.has(r)) return false;
44089
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
+ }
44090
44261
  return true;
44091
44262
  } catch {
44092
44263
  return true;
@@ -46663,6 +46834,36 @@ const createDomHelpers = (function createDomHelpers(opts) {
46663
46834
  let node = el;
46664
46835
  let safety = 0;
46665
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
+
46666
46867
  while (node && node.nodeType === 1 && safety++ < 20) {
46667
46868
  let anchor = null;
46668
46869
 
@@ -46702,7 +46903,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
46702
46903
  parts.unshift(nthOfType(node));
46703
46904
  }
46704
46905
 
46705
- if (!node.parentElement || roots.includes(node)) break;
46906
+ if (!node.parentElement || (stopAtMatchedRoot && roots.includes(node))) break;
46706
46907
  node = node.parentElement;
46707
46908
  }
46708
46909
 
@@ -46721,7 +46922,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
46721
46922
  // position relative to its own parent via `>` (child, not
46722
46923
  // descendant) combinators, so a correctly-matching chain can
46723
46924
  // only resolve to one element short of a malformed document
46724
- // (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
46725
46931
  // document-wide :nth-of-type scan was measured to cost O(total
46726
46932
  // same-tag siblings) per call — pathological on pages with many
46727
46933
  // flat, unidentified siblings (e.g. hundreds of unlabeled
@@ -47301,6 +47507,21 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
47301
47507
  ? engineOptionsResolved.timestamp.trim()
47302
47508
  : null;
47303
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
+
47304
47525
  const sharedHelpers = createDomHelpers({
47305
47526
  document,
47306
47527
  window,
@@ -53370,8 +53591,8 @@ const __a11yCoreCrossFrameApi = (function () {
53370
53591
  },
53371
53592
  {
53372
53593
  "ruleId": "skip-link",
53373
- "title": "Skip link must have a resolvable target",
53374
- "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.",
53375
53596
  "i18n": {
53376
53597
  "titleKey": "skipLink_title",
53377
53598
  "descriptionKey": "skipLink_description"
@@ -54958,13 +55179,7 @@ const __a11yCoreCrossFrameApi = (function () {
54958
55179
  const RULE_IMPLS = {
54959
55180
  "accesskeys": { run: (function runInPage(ctx) {
54960
55181
  const { document, helpers, rule } = ctx;
54961
-
54962
- let nodes = [];
54963
- try {
54964
- nodes = document.querySelectorAll('[accesskey]');
54965
- } catch {
54966
- nodes = [];
54967
- }
55182
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[accesskey]') : helpers.queryAll('[accesskey]');
54968
55183
 
54969
55184
  const groups = new Map(); // normalized key -> elements[]
54970
55185
  for (const el of nodes) {
@@ -55555,8 +55770,7 @@ if (isAccTreeEligible) {
55555
55770
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
55556
55771
  }), applicability: null },
55557
55772
  "aria-allowed-attr": { run: (function runInPage(ctx) {
55558
- const { document, root, helpers, rule } = ctx;
55559
- const safeRoot = root || document;
55773
+ const { document, helpers, rule } = ctx;
55560
55774
 
55561
55775
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
55562
55776
  if (!ariaHelpers) {
@@ -55620,7 +55834,7 @@ if (isAccTreeEligible) {
55620
55834
  };
55621
55835
 
55622
55836
  const globalSet = new Set(GLOBAL_ATTRS);
55623
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
55837
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
55624
55838
 
55625
55839
  const occurrences = [];
55626
55840
  let applicableCount = 0;
@@ -55685,15 +55899,14 @@ if (isAccTreeEligible) {
55685
55899
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55686
55900
  }), applicability: null },
55687
55901
  "aria-allowed-role": { run: (function runInPage(ctx) {
55688
- const { document, root, helpers, rule } = ctx;
55689
- const safeRoot = root || document;
55902
+ const { document, helpers, rule } = ctx;
55690
55903
 
55691
55904
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
55692
55905
  if (!ariaHelpers) {
55693
55906
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
55694
55907
  }
55695
55908
 
55696
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
55909
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
55697
55910
 
55698
55911
  const occurrences = [];
55699
55912
  let applicableCount = 0;
@@ -55740,8 +55953,7 @@ if (isAccTreeEligible) {
55740
55953
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55741
55954
  }), applicability: null },
55742
55955
  "aria-braille-equivalent": { run: (function runInPage(ctx) {
55743
- const { document, root, helpers, rule } = ctx;
55744
- const safeRoot = root || document;
55956
+ const { document, helpers, rule } = ctx;
55745
55957
 
55746
55958
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
55747
55959
 
@@ -55762,8 +55974,8 @@ if (isAccTreeEligible) {
55762
55974
  }
55763
55975
 
55764
55976
  const nodes = helpers.queryAllSmart
55765
- ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]', safeRoot)
55766
- : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]', safeRoot);
55977
+ ? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]')
55978
+ : helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]');
55767
55979
 
55768
55980
  const occurrences = [];
55769
55981
  let applicableCount = 0;
@@ -55824,14 +56036,13 @@ if (isAccTreeEligible) {
55824
56036
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55825
56037
  }), applicability: null },
55826
56038
  "aria-checked-state-mismatch": { run: (function runInPage(ctx) {
55827
- const { document, root, helpers, rule } = ctx;
55828
- const safeRoot = root || document;
56039
+ const { document, helpers, rule } = ctx;
55829
56040
 
55830
56041
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
55831
56042
 
55832
56043
  const nodes = helpers.queryAllSmart
55833
- ? helpers.queryAllSmart('input[type="checkbox"][aria-checked], input[type="radio"][aria-checked]', safeRoot)
55834
- : 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]');
55835
56046
 
55836
56047
  const occurrences = [];
55837
56048
  let applicableCount = 0;
@@ -55893,16 +56104,15 @@ if (isAccTreeEligible) {
55893
56104
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
55894
56105
  }), applicability: null },
55895
56106
  "aria-conditional-attr": { run: (function runInPage(ctx) {
55896
- const { document, root, helpers, rule } = ctx;
55897
- const safeRoot = root || document;
56107
+ const { document, helpers, rule } = ctx;
55898
56108
 
55899
56109
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
55900
56110
 
55901
56111
  const TRUTHY_INVALID_VALUES = new Set(['true', 'grammar', 'spelling']);
55902
56112
 
55903
56113
  const nodes = helpers.queryAllSmart
55904
- ? helpers.queryAllSmart('[aria-errormessage]', safeRoot)
55905
- : helpers.queryAll('[aria-errormessage]', safeRoot);
56114
+ ? helpers.queryAllSmart('[aria-errormessage]')
56115
+ : helpers.queryAll('[aria-errormessage]');
55906
56116
 
55907
56117
  const occurrences = [];
55908
56118
  let applicableCount = 0;
@@ -55947,15 +56157,14 @@ if (isAccTreeEligible) {
55947
56157
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
55948
56158
  }), applicability: null },
55949
56159
  "aria-deprecated-role": { run: (function runInPage(ctx) {
55950
- const { document, root, helpers, rule } = ctx;
55951
- const safeRoot = root || document;
56160
+ const { document, helpers, rule } = ctx;
55952
56161
 
55953
56162
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
55954
56163
  if (!ariaHelpers) {
55955
56164
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
55956
56165
  }
55957
56166
 
55958
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56167
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
55959
56168
 
55960
56169
  const occurrences = [];
55961
56170
  let applicableCount = 0;
@@ -56467,8 +56676,7 @@ if (isAccTreeEligible) {
56467
56676
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
56468
56677
  }), applicability: null },
56469
56678
  "aria-prohibited-attr": { run: (function runInPage(ctx) {
56470
- const { document, root, helpers, rule } = ctx;
56471
- const safeRoot = root || document;
56679
+ const { document, helpers, rule } = ctx;
56472
56680
 
56473
56681
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56474
56682
  if (!ariaHelpers) {
@@ -56488,7 +56696,7 @@ if (isAccTreeEligible) {
56488
56696
 
56489
56697
  const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
56490
56698
 
56491
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56699
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56492
56700
 
56493
56701
  const occurrences = [];
56494
56702
  let applicableCount = 0;
@@ -56539,8 +56747,7 @@ if (isAccTreeEligible) {
56539
56747
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56540
56748
  }), applicability: null },
56541
56749
  "aria-prohibited-children": { run: (function runInPage(ctx) {
56542
- const { document, root, helpers, rule } = ctx;
56543
- const safeRoot = root || document;
56750
+ const { document, helpers, rule } = ctx;
56544
56751
 
56545
56752
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56546
56753
  if (!ariaHelpers) {
@@ -56629,7 +56836,7 @@ if (isAccTreeEligible) {
56629
56836
  }
56630
56837
  }
56631
56838
 
56632
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56839
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56633
56840
 
56634
56841
  const occurrences = [];
56635
56842
  let applicableCount = 0;
@@ -56701,8 +56908,7 @@ if (isAccTreeEligible) {
56701
56908
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56702
56909
  }), applicability: null },
56703
56910
  "aria-required-attr": { run: (function runInPage(ctx) {
56704
- const { document, root, helpers, rule } = ctx;
56705
- const safeRoot = root || document;
56911
+ const { document, helpers, rule } = ctx;
56706
56912
 
56707
56913
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56708
56914
  if (!ariaHelpers) {
@@ -56726,7 +56932,7 @@ if (isAccTreeEligible) {
56726
56932
  return v != null && String(v).trim().toLowerCase() === 'true';
56727
56933
  }
56728
56934
 
56729
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
56935
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56730
56936
 
56731
56937
  const occurrences = [];
56732
56938
  let applicableCount = 0;
@@ -56783,8 +56989,7 @@ if (isAccTreeEligible) {
56783
56989
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56784
56990
  }), applicability: null },
56785
56991
  "aria-required-children": { run: (function runInPage(ctx) {
56786
- const { document, root, helpers, rule } = ctx;
56787
- const safeRoot = root || document;
56992
+ const { document, helpers, rule } = ctx;
56788
56993
 
56789
56994
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56790
56995
  if (!ariaHelpers) {
@@ -56815,7 +57020,7 @@ if (isAccTreeEligible) {
56815
57020
  // ("runInPage MUST be self-contained").
56816
57021
  const CANDIDATE_SELECTOR = '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
56817
57022
 
56818
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
57023
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
56819
57024
 
56820
57025
  const occurrences = [];
56821
57026
  let applicableCount = 0;
@@ -56974,8 +57179,7 @@ if (isAccTreeEligible) {
56974
57179
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
56975
57180
  }), applicability: null },
56976
57181
  "aria-required-parent": { run: (function runInPage(ctx) {
56977
- const { document, root, helpers, rule } = ctx;
56978
- const safeRoot = root || document;
57182
+ const { document, helpers, rule } = ctx;
56979
57183
 
56980
57184
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
56981
57185
  if (!ariaHelpers) {
@@ -57061,7 +57265,7 @@ if (isAccTreeEligible) {
57061
57265
  const idTok = elId && String(elId).trim();
57062
57266
  if (!idTok) return false;
57063
57267
 
57064
- 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]');
57065
57269
  for (const owner of owners) {
57066
57270
  if (!owner || !owner.getAttribute) continue;
57067
57271
  const ownsAttr = owner.getAttribute('aria-owns') || '';
@@ -57074,7 +57278,7 @@ if (isAccTreeEligible) {
57074
57278
  return false;
57075
57279
  }
57076
57280
 
57077
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
57281
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
57078
57282
 
57079
57283
  const occurrences = [];
57080
57284
  let applicableCount = 0;
@@ -57287,15 +57491,14 @@ if (isAccTreeEligible) {
57287
57491
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'minor', occurrences };
57288
57492
  }), applicability: null },
57289
57493
  "aria-roles-valid": { run: (function runInPage(ctx) {
57290
- const { document, root, helpers, rule } = ctx;
57291
- const safeRoot = root || document;
57494
+ const { document, helpers, rule } = ctx;
57292
57495
 
57293
57496
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
57294
57497
  if (!ariaHelpers) {
57295
57498
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57296
57499
  }
57297
57500
 
57298
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]', safeRoot) : helpers.queryAll('[role]', safeRoot);
57501
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
57299
57502
 
57300
57503
  const occurrences = [];
57301
57504
  let applicableCount = 0;
@@ -57350,13 +57553,12 @@ if (isAccTreeEligible) {
57350
57553
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57351
57554
  }), applicability: null },
57352
57555
  "aria-text": { run: (function runInPage(ctx) {
57353
- const { document, root, helpers, rule } = ctx;
57354
- const safeRoot = root || document;
57556
+ const { document, helpers, rule } = ctx;
57355
57557
 
57356
57558
  const FOCUSABLE_DESCENDANT_SELECTOR =
57357
57559
  'a[href], button, input, select, textarea, [tabindex], iframe, [contenteditable]:not([contenteditable="false"])';
57358
57560
 
57359
- 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"]');
57360
57562
 
57361
57563
  const occurrences = [];
57362
57564
  let applicableCount = 0;
@@ -57410,15 +57612,14 @@ if (isAccTreeEligible) {
57410
57612
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57411
57613
  }), applicability: null },
57412
57614
  "aria-valid-attr": { run: (function runInPage(ctx) {
57413
- const { document, root, helpers, rule } = ctx;
57414
- const safeRoot = root || document;
57615
+ const { document, helpers, rule } = ctx;
57415
57616
 
57416
57617
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
57417
57618
  if (!ariaHelpers) {
57418
57619
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57419
57620
  }
57420
57621
 
57421
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
57622
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
57422
57623
 
57423
57624
  const occurrences = [];
57424
57625
  let applicableCount = 0;
@@ -57472,15 +57673,14 @@ if (isAccTreeEligible) {
57472
57673
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57473
57674
  }), applicability: null },
57474
57675
  "aria-valid-attr-value": { run: (function runInPage(ctx) {
57475
- const { document, root, helpers, rule } = ctx;
57476
- const safeRoot = root || document;
57676
+ const { document, helpers, rule } = ctx;
57477
57677
 
57478
57678
  const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
57479
57679
  if (!ariaHelpers) {
57480
57680
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
57481
57681
  }
57482
57682
 
57483
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*', safeRoot) : helpers.queryAll('*', safeRoot);
57683
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
57484
57684
 
57485
57685
  const occurrences = [];
57486
57686
  let applicableCount = 0;
@@ -57537,8 +57737,7 @@ if (isAccTreeEligible) {
57537
57737
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57538
57738
  }), applicability: null },
57539
57739
  "autocomplete-valid": { run: (function runInPage(ctx) {
57540
- const { document, root, helpers, rule } = ctx;
57541
- const safeRoot = root || document;
57740
+ const { document, helpers, rule } = ctx;
57542
57741
 
57543
57742
  // Declared inside runInPage — see scripts/build-core.js header
57544
57743
  // ("runInPage MUST be self-contained").
@@ -57573,7 +57772,7 @@ if (isAccTreeEligible) {
57573
57772
  return FIELD_NAMES.has(remaining[0]);
57574
57773
  }
57575
57774
 
57576
- 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');
57577
57776
 
57578
57777
  const occurrences = [];
57579
57778
  let applicableCount = 0;
@@ -57616,12 +57815,11 @@ if (isAccTreeEligible) {
57616
57815
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57617
57816
  }), applicability: null },
57618
57817
  "avoid-inline-spacing": { run: (function runInPage(ctx) {
57619
- const { document, root, helpers, rule } = ctx;
57620
- const safeRoot = root || document;
57818
+ const { document, helpers, rule } = ctx;
57621
57819
 
57622
57820
  const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
57623
57821
 
57624
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]', safeRoot) : helpers.queryAll('[style]', safeRoot);
57822
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]') : helpers.queryAll('[style]');
57625
57823
 
57626
57824
  const occurrences = [];
57627
57825
  let applicableCount = 0;
@@ -57674,11 +57872,10 @@ if (isAccTreeEligible) {
57674
57872
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57675
57873
  }), applicability: null },
57676
57874
  "binary-control-name-present": { run: (function runInPage(ctx) {
57677
- const { document, root, helpers, rule } = ctx;
57875
+ const { document, helpers, rule } = ctx;
57678
57876
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
57679
57877
  ? helpers.getEligibilityInfo
57680
57878
  : null;
57681
- const safeRoot = root || document;
57682
57879
 
57683
57880
 
57684
57881
  function normalizeWs(s) {
@@ -57810,7 +58007,7 @@ if (isAccTreeEligible) {
57810
58007
  let applicableCount = 0;
57811
58008
 
57812
58009
  const selector = 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
57813
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
58010
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
57814
58011
 
57815
58012
 
57816
58013
  // Precompute label[for] associations once for speed/determinism.
@@ -57942,8 +58139,7 @@ if (isAccTreeEligible) {
57942
58139
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
57943
58140
  }), applicability: null },
57944
58141
  "button-name-present": { run: (function runInPage(ctx) {
57945
- const { document, root, helpers, rule } = ctx;
57946
- const safeRoot = root || document;
58142
+ const { document, helpers, rule } = ctx;
57947
58143
 
57948
58144
  const occurrences = [];
57949
58145
  let applicableCount = 0;
@@ -57980,7 +58176,7 @@ if (isAccTreeEligible) {
57980
58176
  }
57981
58177
 
57982
58178
  const selector = 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
57983
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
58179
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
57984
58180
 
57985
58181
  for (const el of nodes) {
57986
58182
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -57992,6 +58188,7 @@ if (isAccTreeEligible) {
57992
58188
 
57993
58189
  const tag = (el.tagName || '').toLowerCase();
57994
58190
  const role = el.getAttribute ? el.getAttribute('role') : null;
58191
+ const roleNorm = normalizeWs(role).toLowerCase();
57995
58192
 
57996
58193
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
57997
58194
 
@@ -58007,7 +58204,17 @@ if (isAccTreeEligible) {
58007
58204
  inputValueName = getInputButtonValueName(el);
58008
58205
  }
58009
58206
 
58010
- 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);
58011
58218
  const contentName =
58012
58219
  (!trustedProgrammaticName && !inputValueName && isContentNameCandidate)
58013
58220
  ? getConservativeSubtreeText(el)
@@ -58067,14 +58274,43 @@ if (isAccTreeEligible) {
58067
58274
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
58068
58275
  }
58069
58276
 
58070
- function hasMainLandmark() {
58277
+ const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
58278
+
58279
+ function isExposedToAt(el) {
58280
+ if (!isAccTreeEligible) return true;
58071
58281
  try {
58072
- return !!document.querySelector('main, [role="main"]');
58282
+ const r = isAccTreeEligible(el, ctx);
58283
+ if (typeof r === 'boolean') return r;
58284
+ return !!(r && r.eligible);
58073
58285
  } catch {
58074
- 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 [];
58075
58297
  }
58076
58298
  }
58077
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;
58310
+ }
58311
+ return false;
58312
+ }
58313
+
58078
58314
  function hasWorkingAnchorLink() {
58079
58315
  let links = [];
58080
58316
  try {
@@ -58114,11 +58350,10 @@ if (isAccTreeEligible) {
58114
58350
  }
58115
58351
 
58116
58352
  function hasHeading() {
58117
- try {
58118
- return !!document.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]');
58119
- } catch {
58120
- return false;
58353
+ for (const el of queryAll('h1, h2, h3, h4, h5, h6, [role="heading"]')) {
58354
+ if (el && isExposedToAt(el)) return true;
58121
58355
  }
58356
+ return false;
58122
58357
  }
58123
58358
 
58124
58359
  const mainLandmark = hasMainLandmark();
@@ -58376,11 +58611,10 @@ if (isAccTreeEligible) {
58376
58611
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
58377
58612
  }), applicability: null },
58378
58613
  "combobox-name-present": { run: (function runInPage(ctx) {
58379
- const { document, root, helpers, rule } = ctx;
58614
+ const { document, helpers, rule } = ctx;
58380
58615
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
58381
58616
  ? helpers.getEligibilityInfo
58382
58617
  : null;
58383
- const safeRoot = root || document;
58384
58618
 
58385
58619
 
58386
58620
  function normalizeWs(s) {
@@ -58512,7 +58746,7 @@ if (isAccTreeEligible) {
58512
58746
  let applicableCount = 0;
58513
58747
 
58514
58748
  const selector = '[role="combobox"]';
58515
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
58749
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
58516
58750
 
58517
58751
  // Precompute label[for] map for combobox elements that are labelable
58518
58752
  // native form controls (e.g. <input role="combobox">).
@@ -60239,14 +60473,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60239
60473
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
60240
60474
  }), applicability: null },
60241
60475
  "definition-list-children-valid": { run: (function runInPage(ctx) {
60242
- const { document, root, helpers, rule } = ctx;
60243
- const safeRoot = root || document;
60476
+ const { document, helpers, rule } = ctx;
60244
60477
 
60245
60478
  // Declared inside runInPage — see scripts/build-core.js header
60246
60479
  // ("runInPage MUST be self-contained").
60247
60480
  const PASSTHROUGH_TAGS = new Set(['dt', 'dd', 'script', 'template', 'style']);
60248
60481
 
60249
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl', safeRoot) : helpers.queryAll('dl', safeRoot);
60482
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl') : helpers.queryAll('dl');
60250
60483
 
60251
60484
  const occurrences = [];
60252
60485
  let applicableCount = 0;
@@ -60330,10 +60563,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60330
60563
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
60331
60564
  }), applicability: null },
60332
60565
  "deprecated-elements-not-used": { run: (function runInPage(ctx) {
60333
- const { document, root, helpers, rule } = ctx;
60334
- const safeRoot = root || document;
60566
+ const { document, helpers, rule } = ctx;
60335
60567
 
60336
- 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');
60337
60569
 
60338
60570
  const occurrences = [];
60339
60571
  let applicableCount = 0;
@@ -60372,11 +60604,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60372
60604
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
60373
60605
  }), applicability: null },
60374
60606
  "dialog-name-present": { run: (function runInPage(ctx) {
60375
- const { document, root, helpers, rule } = ctx;
60607
+ const { document, helpers, rule } = ctx;
60376
60608
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
60377
60609
  ? helpers.getEligibilityInfo
60378
60610
  : null;
60379
- const safeRoot = root || document;
60380
60611
 
60381
60612
 
60382
60613
  function normalizeWs(s) {
@@ -60490,7 +60721,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60490
60721
  let applicableCount = 0;
60491
60722
 
60492
60723
  const selector = '[role="dialog"],[role="alertdialog"]';
60493
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
60724
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
60494
60725
 
60495
60726
 
60496
60727
  function evaluate(el) {
@@ -60555,10 +60786,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
60555
60786
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
60556
60787
  }), applicability: null },
60557
60788
  "dlitem-parent-valid": { run: (function runInPage(ctx) {
60558
- const { document, root, helpers, rule } = ctx;
60559
- const safeRoot = root || document;
60789
+ const { document, helpers, rule } = ctx;
60560
60790
 
60561
- 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');
60562
60792
 
60563
60793
  const occurrences = [];
60564
60794
  let applicableCount = 0;
@@ -61210,8 +61440,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
61210
61440
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
61211
61441
  }), applicability: null },
61212
61442
  "focus-order-semantics": { run: (function runInPage(ctx) {
61213
- const { document, root, helpers, rule } = ctx;
61214
- const safeRoot = root || document;
61443
+ const { document, helpers, rule } = ctx;
61215
61444
 
61216
61445
  const NON_INTERACTIVE_ROLES = new Set([
61217
61446
  'generic', 'group', 'text', 'presentation', 'none', 'img', 'heading',
@@ -61221,7 +61450,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
61221
61450
  'definition', 'paragraph', 'caption', 'status', 'alert', 'log', 'tooltip'
61222
61451
  ]);
61223
61452
 
61224
- 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]');
61225
61454
 
61226
61455
  const occurrences = [];
61227
61456
  let applicableCount = 0;
@@ -61862,11 +62091,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
61862
62091
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [], data: { details: { metrics } } };
61863
62092
  }), applicability: null },
61864
62093
  "form-control-single-label": { run: (function runInPage(ctx) {
61865
- const { document, root, helpers, rule } = ctx;
61866
- const safeRoot = root || document;
62094
+ const { document, helpers, rule } = ctx;
61867
62095
 
61868
62096
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
61869
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
62097
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
61870
62098
 
61871
62099
  // Build a for-value -> label[] map once (avoids per-control dynamic
61872
62100
  // attribute-selector construction / CSS.escape, which is not guaranteed
@@ -62144,14 +62372,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62144
62372
  return { ruleId: rule.ruleId, outcome: 'fail', severity: rule.defaultSeverity || 'serious', occurrences };
62145
62373
  }), applicability: null },
62146
62374
  "identical-links-same-purpose": { run: (function runInPage(ctx) {
62147
- const { document, root, helpers, rule } = ctx;
62148
- const safeRoot = root || document;
62375
+ const { document, helpers, rule } = ctx;
62149
62376
 
62150
62377
  function normName(s) {
62151
62378
  return (s == null ? '' : String(s)).replace(/\s+/g, ' ').trim().toLowerCase();
62152
62379
  }
62153
62380
 
62154
- 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]');
62155
62382
 
62156
62383
  const groups = new Map(); // normName -> [{ el, href }]
62157
62384
  let applicableCount = 0;
@@ -62231,8 +62458,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62231
62458
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
62232
62459
  }), applicability: null },
62233
62460
  "iframe-focusable-content": { run: (function runInPage(ctx) {
62234
- const { document, root, helpers, rule } = ctx;
62235
- const safeRoot = root || document;
62461
+ const { document, helpers, rule } = ctx;
62236
62462
 
62237
62463
  function hasFocusableCandidate(doc) {
62238
62464
  if (!doc || !doc.querySelectorAll) return false;
@@ -62264,7 +62490,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62264
62490
  return !Number.isNaN(n) && n < 0;
62265
62491
  }
62266
62492
 
62267
- 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');
62268
62494
 
62269
62495
  const occurrences = [];
62270
62496
  let applicableCount = 0;
@@ -62314,10 +62540,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62314
62540
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
62315
62541
  }), applicability: null },
62316
62542
  "iframe-name-present": { run: (function runInPage(ctx) {
62317
- const { document, root, helpers, rule } = ctx;
62318
- const safeRoot = root || document;
62543
+ const { document, helpers, rule } = ctx;
62319
62544
 
62320
- 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');
62321
62546
 
62322
62547
  const occurrences = [];
62323
62548
  let applicableCount = 0;
@@ -62369,10 +62594,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62369
62594
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
62370
62595
  }), applicability: null },
62371
62596
  "iframe-title-unique": { run: (function runInPage(ctx) {
62372
- const { document, root, helpers, rule } = ctx;
62373
- const safeRoot = root || document;
62597
+ const { document, helpers, rule } = ctx;
62374
62598
 
62375
- 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');
62376
62600
 
62377
62601
  const groups = new Map(); // trimmed title -> elements[]
62378
62602
  let applicableCount = 0;
@@ -62427,8 +62651,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62427
62651
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
62428
62652
  }), applicability: null },
62429
62653
  "image-redundant-alt": { run: (function runInPage(ctx) {
62430
- const { document, root, helpers, rule } = ctx;
62431
- const safeRoot = root || document;
62654
+ const { document, helpers, rule } = ctx;
62432
62655
 
62433
62656
  function normalizeWs(s) {
62434
62657
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -62447,7 +62670,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
62447
62670
  return normalizeWs(text);
62448
62671
  }
62449
62672
 
62450
- 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]');
62451
62674
 
62452
62675
  const occurrences = [];
62453
62676
  let applicableCount = 0;
@@ -63235,8 +63458,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63235
63458
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences };
63236
63459
  }), applicability: null },
63237
63460
  "label-in-name": { run: (function runInPage(ctx) {
63238
- const { document, root, helpers, rule } = ctx;
63239
- const safeRoot = root || document;
63461
+ const { document, helpers, rule } = ctx;
63240
63462
 
63241
63463
  const occurrences = [];
63242
63464
  let applicableCount = 0;
@@ -63245,7 +63467,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63245
63467
  // NOTE: aria-hidden is intentionally NOT excluded here; it does not affect visual rendering.
63246
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])';
63247
63469
 
63248
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
63470
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
63249
63471
 
63250
63472
  function norm(s) {
63251
63473
  const v = (s == null ? '' : String(s));
@@ -63383,11 +63605,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63383
63605
  }
63384
63606
 
63385
63607
  // Fallback: label[for=id]. Uses `document` directly rather than
63386
- // ctx.root/safeRoot -- label[for] association is a document-wide
63387
- // relationship (IDs are document-unique), not bounded by whatever
63388
- // contextSelector region happens to be scanned, and ctx.root is an
63389
- // array (multi-region contextSelector support), not a single element
63390
- // 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.
63391
63613
  try {
63392
63614
  const idAttribute = control && control.getAttribute ? (control.getAttribute('id') || '') : '';
63393
63615
  const key = String(idAttribute || '').trim();
@@ -63528,11 +63750,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
63528
63750
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
63529
63751
  }), applicability: null },
63530
63752
  "label-title-only": { run: (function runInPage(ctx) {
63531
- const { document, root, helpers, rule } = ctx;
63532
- const safeRoot = root || document;
63753
+ const { document, helpers, rule } = ctx;
63533
63754
 
63534
63755
  const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
63535
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
63756
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
63536
63757
 
63537
63758
  const labelsByFor = new Map();
63538
63759
  const allLabels = document.getElementsByTagName ? document.getElementsByTagName('label') : [];
@@ -64579,8 +64800,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64579
64800
  return { ruleId: rule.ruleId, outcome: 'cantTell', severity: rule.defaultSeverity || 'minor', occurrences };
64580
64801
  }), applicability: null },
64581
64802
  "link-in-text-block": { run: (function runInPage(ctx) {
64582
- const { document, root, helpers, rule, engineOptions } = ctx;
64583
- const safeRoot = root || document;
64803
+ const { document, helpers, rule, engineOptions } = ctx;
64584
64804
 
64585
64805
  function safeComputedStyle(el) {
64586
64806
  try {
@@ -64626,7 +64846,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64626
64846
  const c = helpers && helpers.contrast ? helpers.contrast : null;
64627
64847
 
64628
64848
  const selector = 'a[href]';
64629
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
64849
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
64630
64850
 
64631
64851
  const occurrences = [];
64632
64852
  let applicableCount = 0;
@@ -64726,8 +64946,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64726
64946
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
64727
64947
  }), applicability: null },
64728
64948
  "link-name-present": { run: (function runInPage(ctx) {
64729
- const { document, root, helpers, rule } = ctx;
64730
- const safeRoot = root || document;
64949
+ const { document, helpers, rule } = ctx;
64731
64950
 
64732
64951
  const occurrences = [];
64733
64952
  let applicableCount = 0;
@@ -64748,7 +64967,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64748
64967
  }
64749
64968
 
64750
64969
  const selector = 'a[href], area[href], [role="link"]';
64751
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
64970
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
64752
64971
 
64753
64972
  for (const el of nodes) {
64754
64973
  // isAccTreeEligible returns { eligible, reasons }, not a boolean.
@@ -64761,7 +64980,20 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64761
64980
  const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
64762
64981
  const programmaticName = (nameInfo && typeof nameInfo.value === 'string') ? nameInfo.value : '';
64763
64982
 
64764
- 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) : '';
64765
64997
  const finalName = (programmaticName.trim().length ? programmaticName : contentName).trim();
64766
64998
 
64767
64999
  if (finalName.length === 0) {
@@ -64813,8 +65045,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64813
65045
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
64814
65046
  }), applicability: null },
64815
65047
  "link-name-quality": { run: (function runInPage(ctx) {
64816
- const { document, root, helpers, rule } = ctx;
64817
- const safeRoot = root || document;
65048
+ const { document, helpers, rule } = ctx;
64818
65049
 
64819
65050
  const GENERIC_LINK_TEXT = new Set([
64820
65051
  'click here', 'here', 'click', 'more', 'more info', 'more information',
@@ -64833,7 +65064,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64833
65064
  }
64834
65065
 
64835
65066
  const selector = 'a[href]';
64836
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
65067
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
64837
65068
 
64838
65069
  const occurrences = [];
64839
65070
  let applicableCount = 0;
@@ -64893,14 +65124,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64893
65124
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
64894
65125
  }), applicability: null },
64895
65126
  "list-children-valid": { run: (function runInPage(ctx) {
64896
- const { document, root, helpers, rule } = ctx;
64897
- const safeRoot = root || document;
65127
+ const { document, helpers, rule } = ctx;
64898
65128
 
64899
65129
  // Declared inside runInPage — see scripts/build-core.js header
64900
65130
  // ("runInPage MUST be self-contained").
64901
65131
  const ALLOWED_CHILD_TAGS = new Set(['li', 'script', 'template']);
64902
65132
 
64903
- 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');
64904
65134
 
64905
65135
  const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
64906
65136
 
@@ -64971,11 +65201,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
64971
65201
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
64972
65202
  }), applicability: null },
64973
65203
  "listbox-name-present": { run: (function runInPage(ctx) {
64974
- const { document, root, helpers, rule } = ctx;
65204
+ const { document, helpers, rule } = ctx;
64975
65205
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
64976
65206
  ? helpers.getEligibilityInfo
64977
65207
  : null;
64978
- const safeRoot = root || document;
64979
65208
 
64980
65209
 
64981
65210
  function normalizeWs(s) {
@@ -65080,7 +65309,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65080
65309
  let applicableCount = 0;
65081
65310
 
65082
65311
  const selector = "[role=\"listbox\"]";
65083
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
65312
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
65084
65313
 
65085
65314
  // Precompute label[for] map for listbox elements that are labelable
65086
65315
  // native form controls (e.g. <select multiple role="listbox">).
@@ -65181,10 +65410,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65181
65410
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
65182
65411
  }), applicability: null },
65183
65412
  "listitem-parent-valid": { run: (function runInPage(ctx) {
65184
- const { document, root, helpers, rule } = ctx;
65185
- const safeRoot = root || document;
65413
+ const { document, helpers, rule } = ctx;
65186
65414
 
65187
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li', safeRoot) : helpers.queryAll('li', safeRoot);
65415
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('li') : helpers.queryAll('li');
65188
65416
 
65189
65417
  const occurrences = [];
65190
65418
  let applicableCount = 0;
@@ -65613,11 +65841,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65613
65841
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
65614
65842
  }), applicability: null },
65615
65843
  "menuitem-name-present": { run: (function runInPage(ctx) {
65616
- const { document, root, helpers, rule } = ctx;
65844
+ const { document, helpers, rule } = ctx;
65617
65845
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
65618
65846
  ? helpers.getEligibilityInfo
65619
65847
  : null;
65620
- const safeRoot = root || document;
65621
65848
 
65622
65849
 
65623
65850
  function normalizeWs(s) {
@@ -65723,7 +65950,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
65723
65950
  let applicableCount = 0;
65724
65951
 
65725
65952
  const selector = '[role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]';
65726
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
65953
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
65727
65954
 
65728
65955
 
65729
65956
  function evaluate(el) {
@@ -66029,11 +66256,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66029
66256
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
66030
66257
  }), applicability: null },
66031
66258
  "meter-name-present": { run: (function runInPage(ctx) {
66032
- const { document, root, helpers, rule } = ctx;
66259
+ const { document, helpers, rule } = ctx;
66033
66260
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
66034
66261
  ? helpers.getEligibilityInfo
66035
66262
  : null;
66036
- const safeRoot = root || document;
66037
66263
 
66038
66264
 
66039
66265
  function normalizeWs(s) {
@@ -66097,7 +66323,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66097
66323
  let applicableCount = 0;
66098
66324
 
66099
66325
  const selector = '[role="meter"]';
66100
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
66326
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
66101
66327
 
66102
66328
 
66103
66329
  function evaluate(el) {
@@ -66163,8 +66389,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66163
66389
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
66164
66390
  }), applicability: null },
66165
66391
  "mouse-only-event-handlers": { run: (function runInPage(ctx) {
66166
- const { document, root, helpers, rule } = ctx;
66167
- const safeRoot = root || document;
66392
+ const { document, helpers, rule } = ctx;
66168
66393
 
66169
66394
  const MOUSE_ONLY_ATTRS = [
66170
66395
  'onmouseover', 'onmouseout', 'onmousedown', 'onmouseup',
@@ -66175,7 +66400,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66175
66400
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
66176
66401
 
66177
66402
  const selector = MOUSE_ONLY_ATTRS.map((a) => `[${a}]`).join(', ');
66178
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
66403
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
66179
66404
 
66180
66405
  const occurrences = [];
66181
66406
  let applicableCount = 0;
@@ -66227,8 +66452,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66227
66452
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
66228
66453
  }), applicability: null },
66229
66454
  "nested-interactive-controls-absent": { run: (function runInPage(ctx) {
66230
- const { document, root, helpers, rule } = ctx;
66231
- const safeRoot = root || document;
66455
+ const { document, helpers, rule } = ctx;
66232
66456
 
66233
66457
  // Declared inside runInPage — see scripts/build-core.js header
66234
66458
  // ("runInPage MUST be self-contained").
@@ -66257,7 +66481,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66257
66481
  '[role="treeitem"]'
66258
66482
  ].join(', ');
66259
66483
 
66260
- 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);
66261
66485
 
66262
66486
  const occurrences = [];
66263
66487
  let applicableCount = 0;
@@ -66306,10 +66530,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66306
66530
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
66307
66531
  }), applicability: null },
66308
66532
  "no-autoplay-audio": { run: (function runInPage(ctx) {
66309
- const { document, root, helpers, rule } = ctx;
66310
- const safeRoot = root || document;
66533
+ const { document, helpers, rule } = ctx;
66311
66534
 
66312
- 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]');
66313
66536
 
66314
66537
  const occurrences = [];
66315
66538
  let applicableCount = 0;
@@ -66663,11 +66886,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66663
66886
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
66664
66887
  }), applicability: null },
66665
66888
  "option-name-present": { run: (function runInPage(ctx) {
66666
- const { document, root, helpers, rule } = ctx;
66889
+ const { document, helpers, rule } = ctx;
66667
66890
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
66668
66891
  ? helpers.getEligibilityInfo
66669
66892
  : null;
66670
- const safeRoot = root || document;
66671
66893
 
66672
66894
 
66673
66895
  function normalizeWs(s) {
@@ -66731,7 +66953,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66731
66953
  let applicableCount = 0;
66732
66954
 
66733
66955
  const selector = "[role=\"option\"]";
66734
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
66956
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
66735
66957
 
66736
66958
 
66737
66959
  function hasName(el) {
@@ -66794,8 +67016,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66794
67016
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
66795
67017
  }), applicability: null },
66796
67018
  "p-as-heading": { run: (function runInPage(ctx) {
66797
- const { document, root, helpers, rule } = ctx;
66798
- const safeRoot = root || document;
67019
+ const { document, helpers, rule } = ctx;
66799
67020
 
66800
67021
  const MAX_HEADING_LIKE_CHARS = 120;
66801
67022
  const MIN_FONT_SIZE_PX = 18;
@@ -66847,7 +67068,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66847
67068
  return Number.isFinite(px) ? px : 0;
66848
67069
  }
66849
67070
 
66850
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p', safeRoot) : helpers.queryAll('p', safeRoot);
67071
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('p') : helpers.queryAll('p');
66851
67072
 
66852
67073
  const occurrences = [];
66853
67074
  let applicableCount = 0;
@@ -66929,14 +67150,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
66929
67150
  return tag === 'h1';
66930
67151
  }
66931
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.
66932
67168
  let nodes = [];
66933
67169
  try {
66934
- nodes = document.querySelectorAll('h1, [role]');
67170
+ nodes = helpers && typeof helpers.queryAllSmart === 'function'
67171
+ ? helpers.queryAllSmart('h1, [role]')
67172
+ : document.querySelectorAll('h1, [role]');
66935
67173
  } catch {
66936
67174
  nodes = [];
66937
67175
  }
66938
67176
 
66939
- const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el));
67177
+ const hasH1 = Array.from(nodes).some((el) => el && isLevelOneHeading(el) && isExposedToAt(el));
66940
67178
 
66941
67179
  if (hasH1) {
66942
67180
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
@@ -67260,8 +67498,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67260
67498
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
67261
67499
  }), applicability: null },
67262
67500
  "presentation-role-conflict": { run: (function runInPage(ctx) {
67263
- const { document, root, helpers, rule } = ctx;
67264
- const safeRoot = root || document;
67501
+ const { document, helpers, rule } = ctx;
67265
67502
 
67266
67503
  // The full set of ARIA attributes marked `global: true` per the WAI-ARIA
67267
67504
  // spec (confirmed against a widely-used reference engine's own
@@ -67278,8 +67515,8 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67278
67515
  const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
67279
67516
 
67280
67517
  const nodes = helpers.queryAllSmart
67281
- ? helpers.queryAllSmart('[role="presentation"], [role="none"], img[alt=""]', safeRoot)
67282
- : 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=""]');
67283
67520
 
67284
67521
  const occurrences = [];
67285
67522
  let applicableCount = 0;
@@ -67343,11 +67580,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67343
67580
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
67344
67581
  }), applicability: null },
67345
67582
  "progressbar-name-present": { run: (function runInPage(ctx) {
67346
- const { document, root, helpers, rule } = ctx;
67583
+ const { document, helpers, rule } = ctx;
67347
67584
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
67348
67585
  ? helpers.getEligibilityInfo
67349
67586
  : null;
67350
- const safeRoot = root || document;
67351
67587
 
67352
67588
 
67353
67589
  function normalizeWs(s) {
@@ -67411,7 +67647,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67411
67647
  let applicableCount = 0;
67412
67648
 
67413
67649
  const selector = '[role="progressbar"]';
67414
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
67650
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
67415
67651
 
67416
67652
 
67417
67653
  function evaluate(el) {
@@ -67823,8 +68059,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67823
68059
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
67824
68060
  }), applicability: null },
67825
68061
  "scrollable-region-focusable": { run: (function runInPage(ctx) {
67826
- const { document, root, helpers, rule } = ctx;
67827
- const safeRoot = root || document;
68062
+ const { document, helpers, rule } = ctx;
67828
68063
 
67829
68064
  function safeComputedStyle(el) {
67830
68065
  try {
@@ -67869,7 +68104,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67869
68104
  }
67870
68105
 
67871
68106
  const CANDIDATE_SELECTOR = 'div, section, article, aside, main, nav, pre, table, blockquote, ul, ol, textarea';
67872
- 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);
67873
68108
 
67874
68109
  const occurrences = [];
67875
68110
  let applicableCount = 0;
@@ -67924,11 +68159,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
67924
68159
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
67925
68160
  }), applicability: null },
67926
68161
  "searchbox-name-present": { run: (function runInPage(ctx) {
67927
- const { document, root, helpers, rule } = ctx;
68162
+ const { document, helpers, rule } = ctx;
67928
68163
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
67929
68164
  ? helpers.getEligibilityInfo
67930
68165
  : null;
67931
- const safeRoot = root || document;
67932
68166
 
67933
68167
 
67934
68168
  function normalizeWs(s) {
@@ -68033,7 +68267,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68033
68267
  let applicableCount = 0;
68034
68268
 
68035
68269
  const selector = "[role=\"searchbox\"]";
68036
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
68270
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
68037
68271
 
68038
68272
  // Precompute label[for] map for searchbox elements that are labelable
68039
68273
  // native form controls (e.g. <input role="searchbox">).
@@ -68134,10 +68368,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68134
68368
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68135
68369
  }), applicability: null },
68136
68370
  "server-side-image-map-absent": { run: (function runInPage(ctx) {
68137
- const { document, root, helpers, rule } = ctx;
68138
- const safeRoot = root || document;
68371
+ const { document, helpers, rule } = ctx;
68139
68372
 
68140
- 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]');
68141
68374
 
68142
68375
  const occurrences = [];
68143
68376
  let applicableCount = 0;
@@ -68175,8 +68408,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68175
68408
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68176
68409
  }), applicability: null },
68177
68410
  "skip-link": { run: (function runInPage(ctx) {
68178
- const { document, root, helpers, rule } = ctx;
68179
- const safeRoot = root || document;
68411
+ const { document, helpers, rule } = ctx;
68180
68412
 
68181
68413
  function normalizeWs(s) {
68182
68414
  return String(s || '').replace(/\s+/g, ' ').trim();
@@ -68203,7 +68435,31 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68203
68435
  return normalizeWs(el.textContent);
68204
68436
  }
68205
68437
 
68206
- 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]');
68207
68463
 
68208
68464
  const occurrences = [];
68209
68465
  let applicableCount = 0;
@@ -68241,7 +68497,63 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68241
68497
  }
68242
68498
  }
68243
68499
 
68244
- 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
+ }
68245
68557
 
68246
68558
  const stableSelector = helpers.buildSelector ? helpers.buildSelector(el) : 'html';
68247
68559
  const html = helpers.getOuterHtmlSnippet ? helpers.getOuterHtmlSnippet(el) : (el.outerHTML || '');
@@ -68271,11 +68583,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68271
68583
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
68272
68584
  }), applicability: null },
68273
68585
  "slider-name-present": { run: (function runInPage(ctx) {
68274
- const { document, root, helpers, rule } = ctx;
68586
+ const { document, helpers, rule } = ctx;
68275
68587
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
68276
68588
  ? helpers.getEligibilityInfo
68277
68589
  : null;
68278
- const safeRoot = root || document;
68279
68590
 
68280
68591
 
68281
68592
  function normalizeWs(s) {
@@ -68407,7 +68718,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68407
68718
  let applicableCount = 0;
68408
68719
 
68409
68720
  const selector = 'input[type="range"], [role="slider"]';
68410
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
68721
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
68411
68722
 
68412
68723
 
68413
68724
  // Precompute label[for] map for native range inputs.
@@ -68521,11 +68832,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68521
68832
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68522
68833
  }), applicability: null },
68523
68834
  "spinbutton-name-present": { run: (function runInPage(ctx) {
68524
- const { document, root, helpers, rule } = ctx;
68835
+ const { document, helpers, rule } = ctx;
68525
68836
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
68526
68837
  ? helpers.getEligibilityInfo
68527
68838
  : null;
68528
- const safeRoot = root || document;
68529
68839
 
68530
68840
 
68531
68841
  function normalizeWs(s) {
@@ -68630,7 +68940,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68630
68940
  let applicableCount = 0;
68631
68941
 
68632
68942
  const selector = "[role=\"spinbutton\"]";
68633
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
68943
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
68634
68944
 
68635
68945
  // Precompute label[for] map for spinbutton elements that are labelable
68636
68946
  // native form controls (e.g. <input role="spinbutton">).
@@ -68731,11 +69041,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68731
69041
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
68732
69042
  }), applicability: null },
68733
69043
  "summary-name-present": { run: (function runInPage(ctx) {
68734
- const { document, root, helpers, rule } = ctx;
69044
+ const { document, helpers, rule } = ctx;
68735
69045
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
68736
69046
  ? helpers.getEligibilityInfo
68737
69047
  : null;
68738
- const safeRoot = root || document;
68739
69048
 
68740
69049
 
68741
69050
  function normalizeWs(s) {
@@ -68798,7 +69107,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
68798
69107
  const occurrences = [];
68799
69108
  let applicableCount = 0;
68800
69109
 
68801
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary', safeRoot) : helpers.queryAll('summary', safeRoot);
69110
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('summary') : helpers.queryAll('summary');
68802
69111
 
68803
69112
  function evaluate(el) {
68804
69113
  const ariaLabel = getAttr(el, 'aria-label');
@@ -69437,11 +69746,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69437
69746
  return {ruleId: rule.ruleId, outcome: 'cantTell', severity: 'minor', occurrences};
69438
69747
  }), applicability: null },
69439
69748
  "tab-name-present": { run: (function runInPage(ctx) {
69440
- const { document, root, helpers, rule } = ctx;
69749
+ const { document, helpers, rule } = ctx;
69441
69750
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
69442
69751
  ? helpers.getEligibilityInfo
69443
69752
  : null;
69444
- const safeRoot = root || document;
69445
69753
 
69446
69754
 
69447
69755
  function normalizeWs(s) {
@@ -69547,7 +69855,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69547
69855
  let applicableCount = 0;
69548
69856
 
69549
69857
  const selector = '[role="tab"]';
69550
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
69858
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
69551
69859
 
69552
69860
 
69553
69861
  function evaluate(el) {
@@ -69662,14 +69970,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69662
69970
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
69663
69971
  }), applicability: null },
69664
69972
  "table-duplicate-name": { run: (function runInPage(ctx) {
69665
- const { document, root, helpers, rule } = ctx;
69666
- const safeRoot = root || document;
69973
+ const { document, helpers, rule } = ctx;
69667
69974
 
69668
69975
  function normalizeWs(s) {
69669
69976
  return String(s || '').replace(/\s+/g, ' ').trim();
69670
69977
  }
69671
69978
 
69672
- 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]');
69673
69980
 
69674
69981
  const occurrences = [];
69675
69982
  let applicableCount = 0;
@@ -69715,12 +70022,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69715
70022
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
69716
70023
  }), applicability: null },
69717
70024
  "table-fake-caption": { run: (function runInPage(ctx) {
69718
- const { document, root, helpers, rule } = ctx;
69719
- const safeRoot = root || document;
70025
+ const { document, helpers, rule } = ctx;
69720
70026
 
69721
70027
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
69722
70028
 
69723
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
70029
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
69724
70030
 
69725
70031
  const occurrences = [];
69726
70032
  let applicableCount = 0;
@@ -69783,10 +70089,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69783
70089
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
69784
70090
  }), applicability: null },
69785
70091
  "table-headers-attr-valid": { run: (function runInPage(ctx) {
69786
- const { document, root, helpers, rule } = ctx;
69787
- const safeRoot = root || document;
70092
+ const { document, helpers, rule } = ctx;
69788
70093
 
69789
- 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]');
69790
70095
 
69791
70096
  const occurrences = [];
69792
70097
  let applicableCount = 0;
@@ -69860,10 +70165,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
69860
70165
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
69861
70166
  }), applicability: null },
69862
70167
  "table-th-has-data-cells": { run: (function runInPage(ctx) {
69863
- const { document, root, helpers, rule } = ctx;
69864
- const safeRoot = root || document;
70168
+ const { document, helpers, rule } = ctx;
69865
70169
 
69866
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
70170
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
69867
70171
 
69868
70172
  const occurrences = [];
69869
70173
  let applicableCount = 0;
@@ -70422,14 +70726,13 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70422
70726
  return { ruleId: RULE_ID, outcome: 'pass', severity: 'minor', occurrences: [] };
70423
70727
  }), applicability: null },
70424
70728
  "td-has-header": { run: (function runInPage(ctx) {
70425
- const { document, root, helpers, rule } = ctx;
70426
- const safeRoot = root || document;
70729
+ const { document, helpers, rule } = ctx;
70427
70730
 
70428
70731
  const MIN_SIZE = 4;
70429
70732
 
70430
70733
  function trim(v) { return (v == null ? '' : String(v)).trim(); }
70431
70734
 
70432
- const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table', safeRoot) : helpers.queryAll('table', safeRoot);
70735
+ const tables = helpers.queryAllSmart ? helpers.queryAllSmart('table') : helpers.queryAll('table');
70433
70736
 
70434
70737
  const occurrences = [];
70435
70738
  let applicableCount = 0;
@@ -70515,11 +70818,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70515
70818
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70516
70819
  }), applicability: null },
70517
70820
  "textbox-name-present": { run: (function runInPage(ctx) {
70518
- const { document, root, helpers, rule } = ctx;
70821
+ const { document, helpers, rule } = ctx;
70519
70822
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
70520
70823
  ? helpers.getEligibilityInfo
70521
70824
  : null;
70522
- const safeRoot = root || document;
70523
70825
 
70524
70826
 
70525
70827
  function normalizeWs(s) {
@@ -70624,7 +70926,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70624
70926
  let applicableCount = 0;
70625
70927
 
70626
70928
  const selector = "[role=\"textbox\"]";
70627
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
70929
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
70628
70930
 
70629
70931
  // Precompute label[for] map for textbox elements that are labelable
70630
70932
  // native form controls (e.g. <input role="textbox">).
@@ -70725,11 +71027,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70725
71027
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70726
71028
  }), applicability: null },
70727
71029
  "tooltip-name-present": { run: (function runInPage(ctx) {
70728
- const { document, root, helpers, rule } = ctx;
71030
+ const { document, helpers, rule } = ctx;
70729
71031
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
70730
71032
  ? helpers.getEligibilityInfo
70731
71033
  : null;
70732
- const safeRoot = root || document;
70733
71034
 
70734
71035
 
70735
71036
  function normalizeWs(s) {
@@ -70793,7 +71094,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70793
71094
  let applicableCount = 0;
70794
71095
 
70795
71096
  const selector = '[role="tooltip"]';
70796
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
71097
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
70797
71098
 
70798
71099
 
70799
71100
  function evaluate(el) {
@@ -70857,11 +71158,10 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70857
71158
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70858
71159
  }), applicability: null },
70859
71160
  "treeitem-name-present": { run: (function runInPage(ctx) {
70860
- const { document, root, helpers, rule } = ctx;
71161
+ const { document, helpers, rule } = ctx;
70861
71162
  const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
70862
71163
  ? helpers.getEligibilityInfo
70863
71164
  : null;
70864
- const safeRoot = root || document;
70865
71165
 
70866
71166
 
70867
71167
  function normalizeWs(s) {
@@ -70925,7 +71225,7 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70925
71225
  let applicableCount = 0;
70926
71226
 
70927
71227
  const selector = "[role=\"treeitem\"]";
70928
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector, safeRoot) : helpers.queryAll(selector, safeRoot);
71228
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
70929
71229
 
70930
71230
 
70931
71231
  function hasName(el) {
@@ -70988,12 +71288,11 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
70988
71288
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
70989
71289
  }), applicability: null },
70990
71290
  "valid-lang": { run: (function runInPage(ctx) {
70991
- const { document, root, helpers, rule } = ctx;
70992
- const safeRoot = root || document;
71291
+ const { document, helpers, rule } = ctx;
70993
71292
 
70994
71293
  const BCP47_RE = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
70995
71294
 
70996
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]', safeRoot) : helpers.queryAll('[lang]', safeRoot);
71295
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[lang]') : helpers.queryAll('[lang]');
70997
71296
 
70998
71297
  const occurrences = [];
70999
71298
  let applicableCount = 0;
@@ -71038,10 +71337,9 @@ if (scan && scan.elements && Array.isArray(scan.elements)) {
71038
71337
  return { ruleId: rule.ruleId, outcome: 'pass', severity: 'minor', occurrences: [] };
71039
71338
  }), applicability: null },
71040
71339
  "video-caption": { run: (function runInPage(ctx) {
71041
- const { document, root, helpers, rule } = ctx;
71042
- const safeRoot = root || document;
71340
+ const { document, helpers, rule } = ctx;
71043
71341
 
71044
- const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video', safeRoot) : helpers.queryAll('video', safeRoot);
71342
+ const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('video') : helpers.queryAll('video');
71045
71343
 
71046
71344
  const occurrences = [];
71047
71345
  let applicableCount = 0;
@@ -71763,10 +72061,12 @@ const I18N = {
71763
72061
  "region_description": "Checks that direct children of <body> with visible text content are contained within a landmark region.",
71764
72062
  "region_summary_cantTell": "This content is not contained within a landmark region.",
71765
72063
  "region_hint_cantTell": "Move this content inside a landmark region (main, nav, aside, a labeled section, etc.).",
71766
- "skipLink_title": "Skip link must have a resolvable target",
71767
- "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.",
71768
72066
  "skipLink_summary_cantTell": "This skip link's target does not exist.",
71769
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.",
71770
72070
  "autocompleteValid_title": "autocomplete attribute must be a valid autofill value",
71771
72071
  "autocompleteValid_description": "Checks that a non-empty autocomplete attribute is \"on\"/\"off\" or a well-formed autofill detail token list.",
71772
72072
  "autocompleteValid_summary_fail": "This autocomplete attribute value is not a valid autofill value.",
@@ -72365,10 +72665,12 @@ const I18N = {
72365
72665
  "region_description": "Vérifie que les enfants directs de <body> ayant un contenu textuel visible sont contenus dans un point de repère.",
72366
72666
  "region_summary_cantTell": "Ce contenu n’est contenu dans aucun point de repère.",
72367
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.).",
72368
- "skipLink_title": "Un lien d’évitement doit avoir une cible qui se résout",
72369
- "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.",
72370
72670
  "skipLink_summary_cantTell": "La cible de ce lien d’évitement n’existe pas.",
72371
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.",
72372
72674
  "autocompleteValid_title": "L’attribut autocomplete doit être une valeur d’auto-remplissage valide",
72373
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.",
72374
72676
  "autocompleteValid_summary_fail": "Cette valeur d’attribut autocomplete n’est pas une valeur d’auto-remplissage valide.",
@@ -76151,7 +76453,8 @@ const createDomHelpers = (function createDomHelpers(opts) {
76151
76453
  'templateContent',
76152
76454
  'nonRenderedElement',
76153
76455
  'inputHidden',
76154
- 'visibilityHidden'
76456
+ 'visibilityHidden',
76457
+ 'contentVisibilityHidden'
76155
76458
  ]);
76156
76459
 
76157
76460
  function queryAllSmart(sel) {
@@ -76168,6 +76471,23 @@ const createDomHelpers = (function createDomHelpers(opts) {
76168
76471
  for (const r of reasons) {
76169
76472
  if (HARD_HIDDEN_REASONS.has(r)) return false;
76170
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
+ }
76171
76491
  return true;
76172
76492
  } catch {
76173
76493
  return true;
@@ -78744,6 +79064,36 @@ const createDomHelpers = (function createDomHelpers(opts) {
78744
79064
  let node = el;
78745
79065
  let safety = 0;
78746
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
+
78747
79097
  while (node && node.nodeType === 1 && safety++ < 20) {
78748
79098
  let anchor = null;
78749
79099
 
@@ -78783,7 +79133,7 @@ const createDomHelpers = (function createDomHelpers(opts) {
78783
79133
  parts.unshift(nthOfType(node));
78784
79134
  }
78785
79135
 
78786
- if (!node.parentElement || roots.includes(node)) break;
79136
+ if (!node.parentElement || (stopAtMatchedRoot && roots.includes(node))) break;
78787
79137
  node = node.parentElement;
78788
79138
  }
78789
79139
 
@@ -78802,7 +79152,12 @@ const createDomHelpers = (function createDomHelpers(opts) {
78802
79152
  // position relative to its own parent via `>` (child, not
78803
79153
  // descendant) combinators, so a correctly-matching chain can
78804
79154
  // only resolve to one element short of a malformed document
78805
- // (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
78806
79161
  // document-wide :nth-of-type scan was measured to cost O(total
78807
79162
  // same-tag siblings) per call — pathological on pages with many
78808
79163
  // flat, unidentified siblings (e.g. hundreds of unlabeled
@@ -79382,6 +79737,21 @@ const runCore = (function runCore(pageUrl, contextSelector, engineOptions, runOn
79382
79737
  ? engineOptionsResolved.timestamp.trim()
79383
79738
  : null;
79384
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
+
79385
79755
  const sharedHelpers = createDomHelpers({
79386
79756
  document,
79387
79757
  window,