@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.
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/checks/automatic/aria-allowed-attr.js +2 -3
- package/src/checks/automatic/aria-allowed-role.js +2 -3
- package/src/checks/automatic/aria-braille-equivalent.js +3 -4
- package/src/checks/automatic/aria-conditional-attr.js +3 -4
- package/src/checks/automatic/aria-deprecated-role.js +2 -3
- package/src/checks/automatic/aria-prohibited-attr.js +2 -3
- package/src/checks/automatic/aria-prohibited-children.js +2 -3
- package/src/checks/automatic/aria-required-attr.js +2 -3
- package/src/checks/automatic/aria-required-children.js +2 -3
- package/src/checks/automatic/aria-required-parent.js +3 -4
- package/src/checks/automatic/aria-roles-valid.js +2 -3
- package/src/checks/automatic/aria-valid-attr-value.js +2 -3
- package/src/checks/automatic/aria-valid-attr.js +2 -3
- package/src/checks/automatic/autocomplete-valid.js +2 -3
- package/src/checks/automatic/avoid-inline-spacing.js +2 -3
- package/src/checks/automatic/binary-control-name-present.js +2 -3
- package/src/checks/automatic/button-name-present.js +14 -4
- package/src/checks/automatic/bypass-blocks-present.js +35 -7
- package/src/checks/automatic/combobox-name-present.js +2 -3
- package/src/checks/automatic/definition-list-children-valid.js +2 -3
- package/src/checks/automatic/deprecated-elements-not-used.js +2 -3
- package/src/checks/automatic/dialog-name-present.js +2 -3
- package/src/checks/automatic/dlitem-parent-valid.js +2 -3
- package/src/checks/automatic/form-control-single-label.js +2 -3
- package/src/checks/automatic/iframe-focusable-content.js +2 -3
- package/src/checks/automatic/iframe-name-present.js +2 -3
- package/src/checks/automatic/iframe-title-unique.js +2 -3
- package/src/checks/automatic/label-in-name.js +7 -8
- package/src/checks/automatic/link-in-text-block.js +2 -3
- package/src/checks/automatic/link-name-present.js +16 -4
- package/src/checks/automatic/list-children-valid.js +2 -3
- package/src/checks/automatic/listbox-name-present.js +2 -3
- package/src/checks/automatic/listitem-parent-valid.js +2 -3
- package/src/checks/automatic/menuitem-name-present.js +2 -3
- package/src/checks/automatic/meter-name-present.js +2 -3
- package/src/checks/automatic/nested-interactive-controls-absent.js +2 -3
- package/src/checks/automatic/option-name-present.js +2 -3
- package/src/checks/automatic/progressbar-name-present.js +2 -3
- package/src/checks/automatic/searchbox-name-present.js +2 -3
- package/src/checks/automatic/server-side-image-map-absent.js +2 -3
- package/src/checks/automatic/slider-name-present.js +2 -3
- package/src/checks/automatic/spinbutton-name-present.js +2 -3
- package/src/checks/automatic/summary-name-present.js +2 -3
- package/src/checks/automatic/tab-name-present.js +2 -3
- package/src/checks/automatic/table-headers-attr-valid.js +2 -3
- package/src/checks/automatic/table-th-has-data-cells.js +2 -3
- package/src/checks/automatic/td-has-header.js +2 -3
- package/src/checks/automatic/textbox-name-present.js +2 -3
- package/src/checks/automatic/tooltip-name-present.js +2 -3
- package/src/checks/automatic/treeitem-name-present.js +2 -3
- package/src/checks/automatic/valid-lang.js +2 -3
- package/src/checks/manual/accesskeys-manual.js +1 -7
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +3 -4
- package/src/checks/manual/aria-text-manual.js +2 -3
- package/src/checks/manual/focus-order-semantics-manual.js +2 -3
- package/src/checks/manual/identical-links-same-purpose-manual.js +2 -3
- package/src/checks/manual/image-redundant-alt-manual.js +2 -3
- package/src/checks/manual/label-title-only-manual.js +2 -3
- package/src/checks/manual/link-name-quality-manual.js +2 -3
- package/src/checks/manual/mouse-only-event-handlers-manual.js +2 -3
- package/src/checks/manual/no-autoplay-audio-manual.js +2 -3
- package/src/checks/manual/p-as-heading-manual.js +2 -3
- package/src/checks/manual/page-has-heading-one-manual.js +31 -2
- package/src/checks/manual/presentation-role-conflict-manual.js +3 -4
- package/src/checks/manual/scrollable-region-focusable-manual.js +2 -3
- package/src/checks/manual/skip-link-manual.js +91 -9
- package/src/checks/manual/table-duplicate-name-manual.js +2 -3
- package/src/checks/manual/table-fake-caption-manual.js +2 -3
- package/src/checks/manual/video-caption-manual.js +2 -3
- package/src/core/dom-helpers.js +56 -3
- package/src/core/dom-runner.js +15 -0
- package/src/core.js +851 -481
- package/src/i18n/en.js +4 -2
- package/src/i18n/fr.js +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to this project are documented here, in [Keep a Changelog](h
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.1.2] - 2026-07-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `accesskeys` no longer over-reports duplicate `accesskey` values when one copy is structurally/CSS hidden by default (for example collapsed or `display:none` menu replicas). Candidate collection now follows the shared helper visibility policy, so only currently eligible elements are grouped unless `engineOptions.includeHiddenElements: true` is explicitly set.
|
|
11
|
+
- `skip-link` no longer treats fragment-target existence alone as sufficient. It now also flags skip links whose target exists but is currently unusable (hidden from the accessibility tree), while keeping geometry-based target checks gated to environments that expose reliable layout metrics.
|
|
12
|
+
- `page-has-heading-one` and `bypass-blocks-present` credited a fully non-rendered `<h1>`/`<main>`/heading (inside a `display:none` ancestor, or otherwise removed from the accessibility tree via `visibility:hidden`/`[hidden]`/`aria-hidden`/`inert`) as satisfying the check, since both queried the raw DOM (`document.querySelectorAll`) with no visibility/accessibility-tree filtering. Found via the cross-engine comparisons project on CDC's flu page: its only `<h1>` sits inside a `display:none` ancestor — unreachable by sighted and screen reader users alike — and `page-has-heading-one` reported `notApplicable` where axe-core correctly fails. `bypass-blocks-present`'s `<main>`/heading conditions had the identical gap, wrongly returning `pass` for a page with zero actual bypass mechanisms. Both now filter candidates through the existing `isAccTreeEligible` helper, matching `landmark-one-main`'s established precedent; confirmed this does not regress genuinely screen-reader-accessible but visually-clipped/off-screen headings and landmarks (e.g. eBay's homepage `<h1>`, hidden via clip-path with no `aria-hidden`), which `isAccTreeEligible` correctly continues to credit.
|
|
13
|
+
- `button-name-present` and `link-name-present` credited a `<button>`/`<a href>` element's rendered content as its accessible name even when an explicit `role` overrode it to a role whose content represents a VALUE, not a NAME (`combobox`, `listbox`, `textbox`, `slider`, `spinbutton`, `progressbar`, `scrollbar` — name-from-author-only per the WAI-ARIA Accessible Name and Description Computation spec; mirrors axe-core's `controlValueRoles`, verified against its source). Both checks gated "is this a name-from-content candidate" on the native host tag alone, never checking whether `role` had overridden it. Found via the cross-engine comparisons project on Spotify's "Today's Top Hits" playlist page: `<button role="combobox">List</button>` (a "sort by" control, no `aria-label`/`aria-labelledby`) was credited with the name "List" — the combobox's currently selected *value*, not a label for what it is — and reported no issue at all, while axe-core's `button-name` correctly failed it. Both rules now exclude these value-roles from name-from-content; a programmatic name (`aria-label`/`aria-labelledby`/`title`/native `<label>`) still works normally. `combobox-name-present`, `listbox-name-present`, `textbox-name-present`, `spinbutton-name-present`, `progressbar-name-present`, `meter-name-present`, `searchbox-name-present`, `slider-name-present`, and `dialog-name-present` were audited against the same gap and were already correctly name-from-author-only.
|
|
14
|
+
- `createDomHelpers()`'s element-keyed caches (`outerHtmlCache`, `selectorCache`, etc.) were persisted on `window.__a11ycoreSharedCache` and only initialized once per `window`/`document`, not once per run. A window/document reused across separate `runDomRulesInPage()`/`runa11yCoreInPage()` calls — e.g. Jest's `jsdom` environment, which creates one `window` per test file — could read back a previous run's stale cached value for an element that persists by reference across runs (like `document.body`) while its content changed via an in-place mutation (`innerHTML = ...`) in between. Rule pass/fail outcomes were always computed correctly against the live DOM; only cached diagnostic data such as `occurrences[].html` (via `bypass-blocks-present`, reported in #2) could go stale. `runCore()` (`src/core/dom-runner.js`) now resets `window.__a11ycoreSharedCache` at the start of every run, keeping the intended within-a-run sharing while preventing leakage across runs.
|
|
15
|
+
- `buildSelector` could emit ambiguous selectors in multi-region scans when the target element was the last same-tag sibling, because the `:nth-of-type()` disambiguator could be omitted on that segment. Selector construction now consistently disambiguates those cases, so each occurrence maps back to the intended node.
|
|
16
|
+
- `queryAllSmart` could retain elements that are structurally hard-hidden when `isAccTreeEligible` short-circuited on an `inert` ancestor before reaching an outer `display:none`/`visibility:hidden`/`content-visibility:hidden` ancestor. It now performs a style-only DOM visibility fallback in that path and excludes these hard-hidden nodes by default, preserving `includeHiddenElements: true` opt-in behavior.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- Test coverage tightened for this release cycle: re-enabled and stabilized the previously skipped `role-img-text-alternative-present` i18n assertions (EN/FR exact strings and keys), and added regression coverage for the inert + outer hard-hidden filtering path in `queryAllSmart`.
|
|
20
|
+
- Removed a dead `root`/`safeRoot` second argument passed to `helpers.queryAllSmart`/`helpers.queryAll` across 67 rule files: both helpers only ever accepted a single selector argument and scope internally via the run's resolved `contextSelector` roots, so the extra argument was silently ignored in every call. No behavior change — `ctx.root` was always identical to the roots already baked into `helpers` at run start. Also fixed `docs/RULE_TEMPLATE.md`, the copy/paste template most likely responsible for the pattern spreading, which declared `safeRoot` but never even referenced it.
|
|
21
|
+
|
|
7
22
|
## [1.1.1] - 2026-07-29
|
|
8
23
|
|
|
9
24
|
### Changed
|
package/package.json
CHANGED
|
@@ -98,8 +98,7 @@ const meta = {
|
|
|
98
98
|
};
|
|
99
99
|
|
|
100
100
|
function runInPage(ctx) {
|
|
101
|
-
const { document,
|
|
102
|
-
const safeRoot = root || document;
|
|
101
|
+
const { document, helpers, rule } = ctx;
|
|
103
102
|
|
|
104
103
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
105
104
|
if (!ariaHelpers) {
|
|
@@ -163,7 +162,7 @@ function runInPage(ctx) {
|
|
|
163
162
|
};
|
|
164
163
|
|
|
165
164
|
const globalSet = new Set(GLOBAL_ATTRS);
|
|
166
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
165
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
167
166
|
|
|
168
167
|
const occurrences = [];
|
|
169
168
|
let applicableCount = 0;
|
|
@@ -46,15 +46,14 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
53
52
|
if (!ariaHelpers) {
|
|
54
53
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
56
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
58
57
|
|
|
59
58
|
const occurrences = [];
|
|
60
59
|
let applicableCount = 0;
|
|
@@ -52,8 +52,7 @@ const meta = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
function runInPage(ctx) {
|
|
55
|
-
const { document,
|
|
56
|
-
const safeRoot = root || document;
|
|
55
|
+
const { document, helpers, rule } = ctx;
|
|
57
56
|
|
|
58
57
|
function trim(v) { return (v == null ? '' : String(v)).trim(); }
|
|
59
58
|
|
|
@@ -74,8 +73,8 @@ function runInPage(ctx) {
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
const nodes = helpers.queryAllSmart
|
|
77
|
-
? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]'
|
|
78
|
-
: helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]'
|
|
76
|
+
? helpers.queryAllSmart('[aria-braillelabel], [aria-brailleroledescription]')
|
|
77
|
+
: helpers.queryAll('[aria-braillelabel], [aria-brailleroledescription]');
|
|
79
78
|
|
|
80
79
|
const occurrences = [];
|
|
81
80
|
let applicableCount = 0;
|
|
@@ -53,16 +53,15 @@ const meta = {
|
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
function runInPage(ctx) {
|
|
56
|
-
const { document,
|
|
57
|
-
const safeRoot = root || document;
|
|
56
|
+
const { document, helpers, rule } = ctx;
|
|
58
57
|
|
|
59
58
|
function trim(v) { return (v == null ? '' : String(v)).trim(); }
|
|
60
59
|
|
|
61
60
|
const TRUTHY_INVALID_VALUES = new Set(['true', 'grammar', 'spelling']);
|
|
62
61
|
|
|
63
62
|
const nodes = helpers.queryAllSmart
|
|
64
|
-
? helpers.queryAllSmart('[aria-errormessage]'
|
|
65
|
-
: helpers.queryAll('[aria-errormessage]'
|
|
63
|
+
? helpers.queryAllSmart('[aria-errormessage]')
|
|
64
|
+
: helpers.queryAll('[aria-errormessage]');
|
|
66
65
|
|
|
67
66
|
const occurrences = [];
|
|
68
67
|
let applicableCount = 0;
|
|
@@ -45,15 +45,14 @@ const meta = {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
function runInPage(ctx) {
|
|
48
|
-
const { document,
|
|
49
|
-
const safeRoot = root || document;
|
|
48
|
+
const { document, helpers, rule } = ctx;
|
|
50
49
|
|
|
51
50
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
52
51
|
if (!ariaHelpers) {
|
|
53
52
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
55
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
57
56
|
|
|
58
57
|
const occurrences = [];
|
|
59
58
|
let applicableCount = 0;
|
|
@@ -83,8 +83,7 @@ const meta = {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
function runInPage(ctx) {
|
|
86
|
-
const { document,
|
|
87
|
-
const safeRoot = root || document;
|
|
86
|
+
const { document, helpers, rule } = ctx;
|
|
88
87
|
|
|
89
88
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
90
89
|
if (!ariaHelpers) {
|
|
@@ -104,7 +103,7 @@ function runInPage(ctx) {
|
|
|
104
103
|
|
|
105
104
|
const PROHIBITED_NAMING_ATTRS = ['aria-label', 'aria-labelledby'];
|
|
106
105
|
|
|
107
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
106
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
108
107
|
|
|
109
108
|
const occurrences = [];
|
|
110
109
|
let applicableCount = 0;
|
|
@@ -100,8 +100,7 @@ const meta = {
|
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
function runInPage(ctx) {
|
|
103
|
-
const { document,
|
|
104
|
-
const safeRoot = root || document;
|
|
103
|
+
const { document, helpers, rule } = ctx;
|
|
105
104
|
|
|
106
105
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
107
106
|
if (!ariaHelpers) {
|
|
@@ -190,7 +189,7 @@ function runInPage(ctx) {
|
|
|
190
189
|
}
|
|
191
190
|
}
|
|
192
191
|
|
|
193
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
192
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
194
193
|
|
|
195
194
|
const occurrences = [];
|
|
196
195
|
let applicableCount = 0;
|
|
@@ -69,8 +69,7 @@ const meta = {
|
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
function runInPage(ctx) {
|
|
72
|
-
const { document,
|
|
73
|
-
const safeRoot = root || document;
|
|
72
|
+
const { document, helpers, rule } = ctx;
|
|
74
73
|
|
|
75
74
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
76
75
|
if (!ariaHelpers) {
|
|
@@ -94,7 +93,7 @@ function runInPage(ctx) {
|
|
|
94
93
|
return v != null && String(v).trim().toLowerCase() === 'true';
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
96
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
98
97
|
|
|
99
98
|
const occurrences = [];
|
|
100
99
|
let applicableCount = 0;
|
|
@@ -80,8 +80,7 @@ const meta = {
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
function runInPage(ctx) {
|
|
83
|
-
const { document,
|
|
84
|
-
const safeRoot = root || document;
|
|
83
|
+
const { document, helpers, rule } = ctx;
|
|
85
84
|
|
|
86
85
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
87
86
|
if (!ariaHelpers) {
|
|
@@ -112,7 +111,7 @@ function runInPage(ctx) {
|
|
|
112
111
|
// ("runInPage MUST be self-contained").
|
|
113
112
|
const CANDIDATE_SELECTOR = '[role], li, option, tr, td, th, thead, tbody, tfoot, ul, ol, table, select, input[type="radio"]';
|
|
114
113
|
|
|
115
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
114
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
116
115
|
|
|
117
116
|
const occurrences = [];
|
|
118
117
|
let applicableCount = 0;
|
|
@@ -66,8 +66,7 @@ const meta = {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
function runInPage(ctx) {
|
|
69
|
-
const { document,
|
|
70
|
-
const safeRoot = root || document;
|
|
69
|
+
const { document, helpers, rule } = ctx;
|
|
71
70
|
|
|
72
71
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
73
72
|
if (!ariaHelpers) {
|
|
@@ -153,7 +152,7 @@ function runInPage(ctx) {
|
|
|
153
152
|
const idTok = elId && String(elId).trim();
|
|
154
153
|
if (!idTok) return false;
|
|
155
154
|
|
|
156
|
-
const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]'
|
|
155
|
+
const owners = helpers.queryAllSmart ? helpers.queryAllSmart('[aria-owns]') : helpers.queryAll('[aria-owns]');
|
|
157
156
|
for (const owner of owners) {
|
|
158
157
|
if (!owner || !owner.getAttribute) continue;
|
|
159
158
|
const ownsAttr = owner.getAttribute('aria-owns') || '';
|
|
@@ -166,7 +165,7 @@ function runInPage(ctx) {
|
|
|
166
165
|
return false;
|
|
167
166
|
}
|
|
168
167
|
|
|
169
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
168
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
170
169
|
|
|
171
170
|
const occurrences = [];
|
|
172
171
|
let applicableCount = 0;
|
|
@@ -44,15 +44,14 @@ const meta = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
function runInPage(ctx) {
|
|
47
|
-
const { document,
|
|
48
|
-
const safeRoot = root || document;
|
|
47
|
+
const { document, helpers, rule } = ctx;
|
|
49
48
|
|
|
50
49
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
51
50
|
if (!ariaHelpers) {
|
|
52
51
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]'
|
|
54
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[role]') : helpers.queryAll('[role]');
|
|
56
55
|
|
|
57
56
|
const occurrences = [];
|
|
58
57
|
let applicableCount = 0;
|
|
@@ -57,15 +57,14 @@ const meta = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
function runInPage(ctx) {
|
|
60
|
-
const { document,
|
|
61
|
-
const safeRoot = root || document;
|
|
60
|
+
const { document, helpers, rule } = ctx;
|
|
62
61
|
|
|
63
62
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
64
63
|
if (!ariaHelpers) {
|
|
65
64
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*'
|
|
67
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
|
|
69
68
|
|
|
70
69
|
const occurrences = [];
|
|
71
70
|
let applicableCount = 0;
|
|
@@ -46,15 +46,14 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const ariaHelpers = helpers && helpers.aria ? helpers.aria : null;
|
|
53
52
|
if (!ariaHelpers) {
|
|
54
53
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*'
|
|
56
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('*') : helpers.queryAll('*');
|
|
58
57
|
|
|
59
58
|
const occurrences = [];
|
|
60
59
|
let applicableCount = 0;
|
|
@@ -52,8 +52,7 @@ const meta = {
|
|
|
52
52
|
};
|
|
53
53
|
|
|
54
54
|
function runInPage(ctx) {
|
|
55
|
-
const { document,
|
|
56
|
-
const safeRoot = root || document;
|
|
55
|
+
const { document, helpers, rule } = ctx;
|
|
57
56
|
|
|
58
57
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
59
58
|
// ("runInPage MUST be self-contained").
|
|
@@ -88,7 +87,7 @@ function runInPage(ctx) {
|
|
|
88
87
|
return FIELD_NAMES.has(remaining[0]);
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea'
|
|
90
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('input, select, textarea') : helpers.queryAll('input, select, textarea');
|
|
92
91
|
|
|
93
92
|
const occurrences = [];
|
|
94
93
|
let applicableCount = 0;
|
|
@@ -46,12 +46,11 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const SPACING_PROPS = ['line-height', 'letter-spacing', 'word-spacing'];
|
|
53
52
|
|
|
54
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]'
|
|
53
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('[style]') : helpers.queryAll('[style]');
|
|
55
54
|
|
|
56
55
|
const occurrences = [];
|
|
57
56
|
let applicableCount = 0;
|
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -159,7 +158,7 @@ function runInPage(ctx) {
|
|
|
159
158
|
let applicableCount = 0;
|
|
160
159
|
|
|
161
160
|
const selector = 'input[type="checkbox"], input[type="radio"], [role="checkbox"], [role="radio"], [role="switch"]';
|
|
162
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
161
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
163
162
|
|
|
164
163
|
|
|
165
164
|
// Precompute label[for] associations once for speed/determinism.
|
|
@@ -25,8 +25,7 @@ const meta = {
|
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
function runInPage(ctx) {
|
|
28
|
-
const { document,
|
|
29
|
-
const safeRoot = root || document;
|
|
28
|
+
const { document, helpers, rule } = ctx;
|
|
30
29
|
|
|
31
30
|
const occurrences = [];
|
|
32
31
|
let applicableCount = 0;
|
|
@@ -63,7 +62,7 @@ function runInPage(ctx) {
|
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
const selector = 'button, input[type="button"], input[type="submit"], input[type="reset"], [role="button"]';
|
|
66
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
65
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
67
66
|
|
|
68
67
|
for (const el of nodes) {
|
|
69
68
|
// isAccTreeEligible returns { eligible, reasons }, not a boolean.
|
|
@@ -75,6 +74,7 @@ function runInPage(ctx) {
|
|
|
75
74
|
|
|
76
75
|
const tag = (el.tagName || '').toLowerCase();
|
|
77
76
|
const role = el.getAttribute ? el.getAttribute('role') : null;
|
|
77
|
+
const roleNorm = normalizeWs(role).toLowerCase();
|
|
78
78
|
|
|
79
79
|
const nameInfo = helpers.getAccessibleNameInfo ? helpers.getAccessibleNameInfo(el, ctx) : null;
|
|
80
80
|
|
|
@@ -90,7 +90,17 @@ function runInPage(ctx) {
|
|
|
90
90
|
inputValueName = getInputButtonValueName(el);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
// A native <button> (or [role="button"]) whose role has been overridden to
|
|
94
|
+
// one of these roles is no longer semantically a button — per the WAI-ARIA
|
|
95
|
+
// Accessible Name and Description Computation spec these roles are
|
|
96
|
+
// name-from-author-only, and their rendered content represents a VALUE,
|
|
97
|
+
// not a NAME (mirrors axe-core's controlValueRoles, verified against its
|
|
98
|
+
// source). Found on a real page (Spotify's "sort by" control): a
|
|
99
|
+
// <button role="combobox">List</button> where "List" is the combobox's
|
|
100
|
+
// currently selected value, not a label for what the combobox is —
|
|
101
|
+
// crediting it as the accessible name masked a real missing-name bug.
|
|
102
|
+
const VALUE_ROLES = ['textbox', 'progressbar', 'scrollbar', 'slider', 'spinbutton', 'combobox', 'listbox'];
|
|
103
|
+
const isContentNameCandidate = (tag === 'button' || role === 'button') && !VALUE_ROLES.includes(roleNorm);
|
|
94
104
|
const contentName =
|
|
95
105
|
(!trustedProgrammaticName && !inputValueName && isContentNameCandidate)
|
|
96
106
|
? getConservativeSubtreeText(el)
|
|
@@ -75,14 +75,43 @@ function runInPage(ctx) {
|
|
|
75
75
|
return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function
|
|
78
|
+
const isAccTreeEligible = helpers && typeof helpers.isAccTreeEligible === 'function' ? helpers.isAccTreeEligible : null;
|
|
79
|
+
|
|
80
|
+
function isExposedToAt(el) {
|
|
81
|
+
if (!isAccTreeEligible) return true;
|
|
82
|
+
try {
|
|
83
|
+
const r = isAccTreeEligible(el, ctx);
|
|
84
|
+
if (typeof r === 'boolean') return r;
|
|
85
|
+
return !!(r && r.eligible);
|
|
86
|
+
} catch {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function queryAll(selector) {
|
|
79
92
|
try {
|
|
80
|
-
return
|
|
93
|
+
return helpers && typeof helpers.queryAllSmart === 'function'
|
|
94
|
+
? helpers.queryAllSmart(selector)
|
|
95
|
+
: document.querySelectorAll(selector);
|
|
81
96
|
} catch {
|
|
82
|
-
return
|
|
97
|
+
return [];
|
|
83
98
|
}
|
|
84
99
|
}
|
|
85
100
|
|
|
101
|
+
// Filters through isAccTreeEligible (hidden/aria-hidden/display:none/inert
|
|
102
|
+
// don't count as "a bypass mechanism is present") -- see page-has-heading-
|
|
103
|
+
// one-manual.js's identical fix for the real-world trigger (CDC's flu
|
|
104
|
+
// page, 2026-07-30: its only <h1> sits inside a display:none ancestor,
|
|
105
|
+
// unreachable by sighted and screen reader users alike). A fully
|
|
106
|
+
// non-rendered <main>/heading was previously credited here too, wrongly
|
|
107
|
+
// returning `pass` for a page with zero actual bypass mechanisms.
|
|
108
|
+
function hasMainLandmark() {
|
|
109
|
+
for (const el of queryAll('main, [role="main"]')) {
|
|
110
|
+
if (el && isExposedToAt(el)) return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
function hasWorkingAnchorLink() {
|
|
87
116
|
let links = [];
|
|
88
117
|
try {
|
|
@@ -122,11 +151,10 @@ function runInPage(ctx) {
|
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
function hasHeading() {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
} catch {
|
|
128
|
-
return false;
|
|
154
|
+
for (const el of queryAll('h1, h2, h3, h4, h5, h6, [role="heading"]')) {
|
|
155
|
+
if (el && isExposedToAt(el)) return true;
|
|
129
156
|
}
|
|
157
|
+
return false;
|
|
130
158
|
}
|
|
131
159
|
|
|
132
160
|
const mainLandmark = hasMainLandmark();
|
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -159,7 +158,7 @@ function runInPage(ctx) {
|
|
|
159
158
|
let applicableCount = 0;
|
|
160
159
|
|
|
161
160
|
const selector = '[role="combobox"]';
|
|
162
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
161
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
163
162
|
|
|
164
163
|
// Precompute label[for] map for combobox elements that are labelable
|
|
165
164
|
// native form controls (e.g. <input role="combobox">).
|
|
@@ -54,14 +54,13 @@ const meta = {
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
function runInPage(ctx) {
|
|
57
|
-
const { document,
|
|
58
|
-
const safeRoot = root || document;
|
|
57
|
+
const { document, helpers, rule } = ctx;
|
|
59
58
|
|
|
60
59
|
// Declared inside runInPage — see scripts/build-core.js header
|
|
61
60
|
// ("runInPage MUST be self-contained").
|
|
62
61
|
const PASSTHROUGH_TAGS = new Set(['dt', 'dd', 'script', 'template', 'style']);
|
|
63
62
|
|
|
64
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl'
|
|
63
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dl') : helpers.queryAll('dl');
|
|
65
64
|
|
|
66
65
|
const occurrences = [];
|
|
67
66
|
let applicableCount = 0;
|
|
@@ -48,10 +48,9 @@ const meta = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
function runInPage(ctx) {
|
|
51
|
-
const { document,
|
|
52
|
-
const safeRoot = root || document;
|
|
51
|
+
const { document, helpers, rule } = ctx;
|
|
53
52
|
|
|
54
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee'
|
|
53
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('blink, marquee') : helpers.queryAll('blink, marquee');
|
|
55
54
|
|
|
56
55
|
const occurrences = [];
|
|
57
56
|
let applicableCount = 0;
|
|
@@ -23,11 +23,10 @@ const meta = {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
function runInPage(ctx) {
|
|
26
|
-
const { document,
|
|
26
|
+
const { document, helpers, rule } = ctx;
|
|
27
27
|
const getEligibilityInfo = helpers && typeof helpers.getEligibilityInfo === 'function'
|
|
28
28
|
? helpers.getEligibilityInfo
|
|
29
29
|
: null;
|
|
30
|
-
const safeRoot = root || document;
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
function normalizeWs(s) {
|
|
@@ -141,7 +140,7 @@ function runInPage(ctx) {
|
|
|
141
140
|
let applicableCount = 0;
|
|
142
141
|
|
|
143
142
|
const selector = '[role="dialog"],[role="alertdialog"]';
|
|
144
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
143
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
145
144
|
|
|
146
145
|
|
|
147
146
|
function evaluate(el) {
|
|
@@ -42,10 +42,9 @@ const meta = {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
function runInPage(ctx) {
|
|
45
|
-
const { document,
|
|
46
|
-
const safeRoot = root || document;
|
|
45
|
+
const { document, helpers, rule } = ctx;
|
|
47
46
|
|
|
48
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd'
|
|
47
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('dt, dd') : helpers.queryAll('dt, dd');
|
|
49
48
|
|
|
50
49
|
const occurrences = [];
|
|
51
50
|
let applicableCount = 0;
|
|
@@ -46,11 +46,10 @@ const meta = {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
function runInPage(ctx) {
|
|
49
|
-
const { document,
|
|
50
|
-
const safeRoot = root || document;
|
|
49
|
+
const { document, helpers, rule } = ctx;
|
|
51
50
|
|
|
52
51
|
const selector = 'input:not([type="hidden"]):not([type="submit"]):not([type="reset"]):not([type="button"]):not([type="image"]),select,textarea';
|
|
53
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector
|
|
52
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart(selector) : helpers.queryAll(selector);
|
|
54
53
|
|
|
55
54
|
// Build a for-value -> label[] map once (avoids per-control dynamic
|
|
56
55
|
// attribute-selector construction / CSS.escape, which is not guaranteed
|
|
@@ -55,8 +55,7 @@ const meta = {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
function runInPage(ctx) {
|
|
58
|
-
const { document,
|
|
59
|
-
const safeRoot = root || document;
|
|
58
|
+
const { document, helpers, rule } = ctx;
|
|
60
59
|
|
|
61
60
|
function hasFocusableCandidate(doc) {
|
|
62
61
|
if (!doc || !doc.querySelectorAll) return false;
|
|
@@ -88,7 +87,7 @@ function runInPage(ctx) {
|
|
|
88
87
|
return !Number.isNaN(n) && n < 0;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame'
|
|
90
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
|
|
92
91
|
|
|
93
92
|
const occurrences = [];
|
|
94
93
|
let applicableCount = 0;
|
|
@@ -44,10 +44,9 @@ const meta = {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
function runInPage(ctx) {
|
|
47
|
-
const { document,
|
|
48
|
-
const safeRoot = root || document;
|
|
47
|
+
const { document, helpers, rule } = ctx;
|
|
49
48
|
|
|
50
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame'
|
|
49
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
|
|
51
50
|
|
|
52
51
|
const occurrences = [];
|
|
53
52
|
let applicableCount = 0;
|
|
@@ -47,10 +47,9 @@ const meta = {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
function runInPage(ctx) {
|
|
50
|
-
const { document,
|
|
51
|
-
const safeRoot = root || document;
|
|
50
|
+
const { document, helpers, rule } = ctx;
|
|
52
51
|
|
|
53
|
-
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame'
|
|
52
|
+
const nodes = helpers.queryAllSmart ? helpers.queryAllSmart('iframe, frame') : helpers.queryAll('iframe, frame');
|
|
54
53
|
|
|
55
54
|
const groups = new Map(); // trimmed title -> elements[]
|
|
56
55
|
let applicableCount = 0;
|