@surea11y/core 1.1.2 → 1.3.0

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 (160) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/LICENSE +373 -21
  3. package/README.md +70 -1
  4. package/bin/core.js +240 -11
  5. package/docs/API_STABILITY.md +61 -0
  6. package/docs/BASELINE.md +66 -0
  7. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  8. package/docs/CI_INTEGRATIONS.md +103 -0
  9. package/docs/CLI.md +80 -1
  10. package/docs/ENGINE_OPTIONS.md +4 -0
  11. package/docs/INTEGRATION.md +19 -1
  12. package/docs/OUTPUT_SCHEMA.md +2 -2
  13. package/docs/REPORT.md +33 -0
  14. package/docs/RULE_AUTHORING.md +31 -0
  15. package/docs/RULE_CATALOG.md +1 -1
  16. package/docs/SARIF.md +59 -0
  17. package/package.json +15 -4
  18. package/src/baseline.js +0 -0
  19. package/src/catalogs/composites.wcag.js +414 -450
  20. package/src/checks/automatic/area-alt-present.js +59 -25
  21. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  22. package/src/checks/automatic/aria-allowed-role.js +21 -7
  23. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  24. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  25. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  26. package/src/checks/automatic/aria-hidden-body.js +50 -19
  27. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  28. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  29. package/src/checks/automatic/aria-prohibited-children.js +125 -29
  30. package/src/checks/automatic/aria-required-attr.js +23 -8
  31. package/src/checks/automatic/aria-required-children.js +37 -14
  32. package/src/checks/automatic/aria-required-parent.js +48 -14
  33. package/src/checks/automatic/aria-role-name-present.js +47 -21
  34. package/src/checks/automatic/aria-roles-valid.js +22 -12
  35. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  36. package/src/checks/automatic/aria-valid-attr.js +17 -5
  37. package/src/checks/automatic/autocomplete-valid.js +74 -16
  38. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  39. package/src/checks/automatic/binary-control-name-present.js +60 -50
  40. package/src/checks/automatic/button-name-present.js +48 -18
  41. package/src/checks/automatic/bypass-blocks-present.js +50 -25
  42. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  43. package/src/checks/automatic/combobox-name-present.js +38 -45
  44. package/src/checks/automatic/contrast-computable.js +361 -341
  45. package/src/checks/automatic/contrast-enhanced.js +487 -466
  46. package/src/checks/automatic/contrast-minimum.js +486 -465
  47. package/src/checks/automatic/css-orientation-lock.js +39 -9
  48. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  49. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  50. package/src/checks/automatic/dialog-name-present.js +37 -75
  51. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  52. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  53. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  55. package/src/checks/automatic/form-control-single-label.js +47 -10
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
  57. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  58. package/src/checks/automatic/iframe-name-present.js +31 -9
  59. package/src/checks/automatic/iframe-title-unique.js +29 -8
  60. package/src/checks/automatic/img-alt-present.js +47 -43
  61. package/src/checks/automatic/input-image-alt-present.js +143 -112
  62. package/src/checks/automatic/label-in-name.js +50 -22
  63. package/src/checks/automatic/language-page-present.js +117 -109
  64. package/src/checks/automatic/link-in-text-block.js +59 -19
  65. package/src/checks/automatic/link-name-present.js +45 -14
  66. package/src/checks/automatic/list-children-valid.js +26 -9
  67. package/src/checks/automatic/listbox-name-present.js +39 -19
  68. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  69. package/src/checks/automatic/menuitem-name-present.js +39 -61
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
  73. package/src/checks/automatic/meter-name-present.js +36 -33
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  75. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  76. package/src/checks/automatic/option-name-present.js +38 -21
  77. package/src/checks/automatic/page-title-present.js +26 -7
  78. package/src/checks/automatic/progressbar-name-present.js +41 -34
  79. package/src/checks/automatic/role-img-alt-present.js +209 -157
  80. package/src/checks/automatic/searchbox-name-present.js +39 -19
  81. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  82. package/src/checks/automatic/slider-name-present.js +40 -47
  83. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  84. package/src/checks/automatic/summary-name-present.js +37 -17
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  86. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  87. package/src/checks/automatic/tab-name-present.js +37 -60
  88. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  89. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  90. package/src/checks/automatic/target-size-minimum.js +118 -48
  91. package/src/checks/automatic/td-has-header.js +29 -11
  92. package/src/checks/automatic/textbox-name-present.js +39 -19
  93. package/src/checks/automatic/tooltip-name-present.js +37 -18
  94. package/src/checks/automatic/treeitem-name-present.js +38 -21
  95. package/src/checks/automatic/valid-lang.js +20 -6
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  97. package/src/checks/manual/accesskeys-manual.js +14 -5
  98. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  99. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  101. package/src/checks/manual/aria-text-manual.js +14 -6
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  103. package/src/checks/manual/css-hidden-focus.js +196 -165
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  105. package/src/checks/manual/empty-heading-manual.js +24 -12
  106. package/src/checks/manual/empty-table-header-manual.js +21 -14
  107. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  109. package/src/checks/manual/heading-order-manual.js +22 -12
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  112. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  113. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  115. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  116. package/src/checks/manual/label-title-only-manual.js +14 -5
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  123. package/src/checks/manual/landmark-one-main-manual.js +35 -21
  124. package/src/checks/manual/landmark-unique-manual.js +73 -45
  125. package/src/checks/manual/link-name-quality-manual.js +43 -12
  126. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  127. package/src/checks/manual/meta-viewport-large-manual.js +25 -6
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  129. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  131. package/src/checks/manual/p-as-heading-manual.js +22 -7
  132. package/src/checks/manual/page-has-heading-one-manual.js +40 -23
  133. package/src/checks/manual/page-title-patterns-manual.js +87 -51
  134. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  135. package/src/checks/manual/region-manual.js +275 -62
  136. package/src/checks/manual/scope-attr-valid-manual.js +11 -9
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  138. package/src/checks/manual/skip-link-manual.js +35 -12
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  140. package/src/checks/manual/tabindex-manual.js +11 -9
  141. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  142. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  143. package/src/checks/manual/video-caption-manual.js +15 -4
  144. package/src/checks/manual-review.js +56 -12
  145. package/src/core/aria-helpers.js +1128 -823
  146. package/src/core/contrast-helpers.js +1217 -1062
  147. package/src/core/dom-helpers.js +4176 -3886
  148. package/src/core/dom-runner.js +720 -596
  149. package/src/core/frame-messaging.js +189 -138
  150. package/src/core/frame-scan.js +94 -82
  151. package/src/core/rollup-composites.js +94 -102
  152. package/src/core/rule-meta.js +71 -35
  153. package/src/core.js +37939 -29121
  154. package/src/i18n/en.js +1194 -887
  155. package/src/i18n/fr.js +1136 -793
  156. package/src/policy/contracts.js +13 -13
  157. package/src/policy/resolvePolicy.js +48 -44
  158. package/src/report.js +502 -0
  159. package/src/sarif.js +175 -0
  160. package/surea11y.browser.js +36042 -0
@@ -21,183 +21,206 @@
21
21
  const id = 'css-hidden-focus';
22
22
 
23
23
  const meta = {
24
- title: 'Focusable elements must not be visually hidden',
25
- description:
26
- 'Checks that keyboard-focusable elements are not visually hidden by CSS techniques that can leave them in the tab order.',
27
- i18n: {
28
- titleKey: 'cssHidden_focus_title',
29
- descriptionKey: 'cssHidden_focus_description'
30
- },
31
- helpUrl: null,
32
- tags: ['wcag2aa', 'wcag247', 'navigation', 'focus', 'css', 'atomic', 'manual'],
33
- wcagSc: ['2.4.7'],
34
- normativeMappings: [
35
- {standard: 'WCAG', version: '2.2', requirement: '2.4.7', title: 'Focus Visible', conformanceLevel: 'AA'}
36
- ],
37
- defaultSeverity: 'serious',
38
- category: 'operable',
39
- type: 'manual',
40
- defaultConfidence: 'low',
41
- coverage: {
42
- facetsBySc: {
43
- '2.4.7': ['css-hidden-focusable']
44
- }
24
+ title: 'Focusable elements must not be visually hidden',
25
+ description:
26
+ 'Checks that keyboard-focusable elements are not visually hidden by CSS techniques that can leave them in the tab order.',
27
+ i18n: {
28
+ titleKey: 'cssHidden_focus_title',
29
+ descriptionKey: 'cssHidden_focus_description'
30
+ },
31
+ helpUrl: null,
32
+ tags: ['wcag2aa', 'wcag247', 'navigation', 'focus', 'css', 'atomic', 'manual'],
33
+ wcagSc: ['2.4.7'],
34
+ normativeMappings: [
35
+ {
36
+ standard: 'WCAG',
37
+ version: '2.2',
38
+ requirement: '2.4.7',
39
+ title: 'Focus Visible',
40
+ conformanceLevel: 'AA'
41
+ }
42
+ ],
43
+ defaultSeverity: 'serious',
44
+ category: 'operable',
45
+ type: 'manual',
46
+ defaultConfidence: 'low',
47
+ coverage: {
48
+ facetsBySc: {
49
+ '2.4.7': ['css-hidden-focusable']
45
50
  }
51
+ }
46
52
  };
47
53
 
48
54
  function runInPage(ctx) {
49
- const {document, root, helpers, rule} = ctx;
50
- const safeRoot = root || document;
51
-
52
- const queryAllSmart = helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
53
- const getFocusableInfo = helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
54
- const isDomVisibleEligible = helpers && typeof helpers.isDomVisibleEligible === 'function' ? helpers.isDomVisibleEligible : null;
55
- const reportOccurrence = helpers && typeof helpers.reportOccurrence === 'function' ? helpers.reportOccurrence : null;
56
-
57
- const trim = (v) => (v == null ? '' : String(v)).trim();
58
- const lower = (v) => trim(v).toLowerCase();
59
-
60
- function qAll(sel) {
61
- try {
62
- if (queryAllSmart) {
63
- const r = queryAllSmart(sel);
64
- if (Array.isArray(r)) return r;
65
- return Array.from(r || []);
66
- }
67
- } catch {
68
- // fall through
69
- }
70
- try {
71
- if (safeRoot && typeof safeRoot.querySelectorAll === 'function') return Array.from(safeRoot.querySelectorAll(sel));
72
- } catch {
73
- // fall through
74
- }
75
- return [];
55
+ const { document, root, helpers, rule } = ctx;
56
+ const safeRoot = root || document;
57
+
58
+ const queryAllSmart =
59
+ helpers && typeof helpers.queryAllSmart === 'function' ? helpers.queryAllSmart : null;
60
+ const getFocusableInfo =
61
+ helpers && typeof helpers.getFocusableInfo === 'function' ? helpers.getFocusableInfo : null;
62
+ const isDomVisibleEligible =
63
+ helpers && typeof helpers.isDomVisibleEligible === 'function'
64
+ ? helpers.isDomVisibleEligible
65
+ : null;
66
+ const reportOccurrence =
67
+ helpers && typeof helpers.reportOccurrence === 'function' ? helpers.reportOccurrence : null;
68
+
69
+ const trim = (v) => (v == null ? '' : String(v)).trim();
70
+ const lower = (v) => trim(v).toLowerCase();
71
+
72
+ function qAll(sel) {
73
+ try {
74
+ if (queryAllSmart) {
75
+ const r = queryAllSmart(sel);
76
+ if (Array.isArray(r)) return r;
77
+ return Array.from(r || []);
78
+ }
79
+ } catch {
80
+ // fall through
76
81
  }
77
-
78
- function getComputedStyleSafe(el) {
79
- try {
80
- const w = (document && document.defaultView) ? document.defaultView : (typeof window !== 'undefined' ? window : null);
81
- return w && w.getComputedStyle ? w.getComputedStyle(el) : null;
82
- } catch {
83
- return null;
84
- }
82
+ try {
83
+ if (safeRoot && typeof safeRoot.querySelectorAll === 'function')
84
+ return Array.from(safeRoot.querySelectorAll(sel));
85
+ } catch {
86
+ // fall through
85
87
  }
88
+ return [];
89
+ }
86
90
 
87
- // Returns deterministic "visually hidden but can remain focusable" hints.
88
- function getVisibilityHints(el) {
89
- const out = [];
90
- if (!el) return out;
91
- const cs = getComputedStyleSafe(el);
92
-
93
- // opacity:0
94
- try {
95
- const rawOp = cs && cs.opacity != null ? String(cs.opacity).trim() : '';
96
- const op = rawOp ? Number.parseFloat(rawOp) : 1;
97
- if (Number.isFinite(op) && op <= 0.0001) out.push('opacityZero');
98
- } catch {
99
- }
100
-
101
- // clip / clip-path
102
- try {
103
- const clip = cs && cs.clip != null ? String(cs.clip).trim() : '';
104
- const clipPath = cs && cs.clipPath != null ? String(cs.clipPath).trim() : '';
105
- const clipLow = (clip || '').toLowerCase();
106
- const clipPathLow = (clipPath || '').toLowerCase();
107
-
108
- if (clipLow && clipLow !== 'auto') {
109
- if (clipLow.indexOf('rect(') !== -1 && clipLow.replace(/\s+/g, '').indexOf('rect(0') !== -1) out.push('clipped');
110
- }
111
- if (clipPathLow && clipPathLow !== 'none') {
112
- if (clipPathLow.indexOf('inset(') !== -1 && (clipPathLow.indexOf('100%') !== -1 || clipPathLow.indexOf('50%') !== -1)) {
113
- out.push('clipped');
114
- }
115
- }
116
- } catch {
117
- }
118
-
119
- // zero-size + overflow hidden/clip
120
- try {
121
- const wv = cs && cs.width != null ? String(cs.width).trim() : '';
122
- const hv = cs && cs.height != null ? String(cs.height).trim() : '';
123
- const ov = cs && cs.overflow != null ? String(cs.overflow).trim().toLowerCase() : '';
124
- const isZeroW = wv === '0px' || wv === '0';
125
- const isZeroH = hv === '0px' || hv === '0';
126
- const hidesOverflow = ov === 'hidden' || ov === 'clip';
127
- if ((isZeroW || isZeroH) && hidesOverflow) out.push('zeroSizeOverflowHidden');
128
- } catch {
129
- }
130
-
131
- // off-screen heuristic (absolute/fixed + left/top <= -5000 OR text-indent <= -5000)
132
- try {
133
- const pos = cs && cs.position != null ? String(cs.position).trim().toLowerCase() : '';
134
- const left = cs && cs.left != null ? String(cs.left).trim().toLowerCase() : '';
135
- const top = cs && cs.top != null ? String(cs.top).trim().toLowerCase() : '';
136
- const ti = cs && cs.textIndent != null ? String(cs.textIndent).trim().toLowerCase() : '';
137
-
138
- const parsePx = (s) => {
139
- if (!s || s === 'auto') return null;
140
- const m = String(s).match(/-?\d+(\.\d+)?/);
141
- if (!m) return null;
142
- const n = Number.parseFloat(m[0]);
143
- return Number.isFinite(n) ? n : null;
144
- };
145
-
146
- const l = parsePx(left);
147
- const t = parsePx(top);
148
- const ind = parsePx(ti);
149
-
150
- if (pos === 'absolute' || pos === 'fixed') {
151
- if ((l != null && l <= -5000) || (t != null && t <= -5000)) out.push('offscreen');
152
- }
153
- if (ind != null && ind <= -5000) out.push('offscreen');
154
- } catch {
155
- }
156
-
157
- // Dedup
158
- const seen = new Set();
159
- const uniq = [];
160
- for (const k of out) {
161
- const kk = String(k);
162
- if (!seen.has(kk)) {
163
- seen.add(kk);
164
- uniq.push(kk);
165
- }
166
- }
167
- return uniq;
91
+ function getComputedStyleSafe(el) {
92
+ try {
93
+ const w =
94
+ document && document.defaultView
95
+ ? document.defaultView
96
+ : typeof window !== 'undefined'
97
+ ? window
98
+ : null;
99
+ return w && w.getComputedStyle ? w.getComputedStyle(el) : null;
100
+ } catch {
101
+ return null;
168
102
  }
103
+ }
169
104
 
170
- function getFocusableInfoSafe(el) {
171
- if (!getFocusableInfo) return null;
172
- try {
173
- return getFocusableInfo(el, ctx);
174
- } catch {
175
- return null;
105
+ // Returns deterministic "visually hidden but can remain focusable" hints.
106
+ function getVisibilityHints(el) {
107
+ const out = [];
108
+ if (!el) return out;
109
+ const cs = getComputedStyleSafe(el);
110
+
111
+ // opacity:0
112
+ try {
113
+ const rawOp = cs && cs.opacity != null ? String(cs.opacity).trim() : '';
114
+ const op = rawOp ? Number.parseFloat(rawOp) : 1;
115
+ if (Number.isFinite(op) && op <= 0.0001) out.push('opacityZero');
116
+ } catch {}
117
+
118
+ // clip / clip-path
119
+ try {
120
+ const clip = cs && cs.clip != null ? String(cs.clip).trim() : '';
121
+ const clipPath = cs && cs.clipPath != null ? String(cs.clipPath).trim() : '';
122
+ const clipLow = (clip || '').toLowerCase();
123
+ const clipPathLow = (clipPath || '').toLowerCase();
124
+
125
+ if (clipLow && clipLow !== 'auto') {
126
+ if (clipLow.indexOf('rect(') !== -1 && clipLow.replace(/\s+/g, '').indexOf('rect(0') !== -1)
127
+ out.push('clipped');
128
+ }
129
+ if (clipPathLow && clipPathLow !== 'none') {
130
+ if (
131
+ clipPathLow.indexOf('inset(') !== -1 &&
132
+ (clipPathLow.indexOf('100%') !== -1 || clipPathLow.indexOf('50%') !== -1)
133
+ ) {
134
+ out.push('clipped');
176
135
  }
136
+ }
137
+ } catch {}
138
+
139
+ // zero-size + overflow hidden/clip
140
+ try {
141
+ const wv = cs && cs.width != null ? String(cs.width).trim() : '';
142
+ const hv = cs && cs.height != null ? String(cs.height).trim() : '';
143
+ const ov = cs && cs.overflow != null ? String(cs.overflow).trim().toLowerCase() : '';
144
+ const isZeroW = wv === '0px' || wv === '0';
145
+ const isZeroH = hv === '0px' || hv === '0';
146
+ const hidesOverflow = ov === 'hidden' || ov === 'clip';
147
+ if ((isZeroW || isZeroH) && hidesOverflow) out.push('zeroSizeOverflowHidden');
148
+ } catch {}
149
+
150
+ // off-screen heuristic (absolute/fixed + left/top <= -5000 OR text-indent <= -5000)
151
+ try {
152
+ const pos = cs && cs.position != null ? String(cs.position).trim().toLowerCase() : '';
153
+ const left = cs && cs.left != null ? String(cs.left).trim().toLowerCase() : '';
154
+ const top = cs && cs.top != null ? String(cs.top).trim().toLowerCase() : '';
155
+ const ti = cs && cs.textIndent != null ? String(cs.textIndent).trim().toLowerCase() : '';
156
+
157
+ const parsePx = (s) => {
158
+ if (!s || s === 'auto') return null;
159
+ const m = String(s).match(/-?\d+(\.\d+)?/);
160
+ if (!m) return null;
161
+ const n = Number.parseFloat(m[0]);
162
+ return Number.isFinite(n) ? n : null;
163
+ };
164
+
165
+ const l = parsePx(left);
166
+ const t = parsePx(top);
167
+ const ind = parsePx(ti);
168
+
169
+ if (pos === 'absolute' || pos === 'fixed') {
170
+ if ((l != null && l <= -5000) || (t != null && t <= -5000)) out.push('offscreen');
171
+ }
172
+ if (ind != null && ind <= -5000) out.push('offscreen');
173
+ } catch {}
174
+
175
+ // Dedup
176
+ const seen = new Set();
177
+ const uniq = [];
178
+ for (const k of out) {
179
+ const kk = String(k);
180
+ if (!seen.has(kk)) {
181
+ seen.add(kk);
182
+ uniq.push(kk);
183
+ }
177
184
  }
185
+ return uniq;
186
+ }
178
187
 
179
- function isTabbable(el, info) {
180
- const f = info || getFocusableInfoSafe(el);
181
- return !!(f && f.focusable && f.tabbable);
188
+ function getFocusableInfoSafe(el) {
189
+ if (!getFocusableInfo) return null;
190
+ try {
191
+ return getFocusableInfo(el, ctx);
192
+ } catch {
193
+ return null;
182
194
  }
195
+ }
183
196
 
184
- // Exclude elements that are not rendered / not in the visual rendering tree.
185
- function isRendered(el) {
186
- if (!isDomVisibleEligible) return true;
187
- try {
188
- const vis = isDomVisibleEligible(el, ctx, {visibilityMode: 'styleOnly', disableGeometry: true});
189
- if (vis && vis.eligible === false) {
190
- const rs = Array.isArray(vis.reasons) ? vis.reasons : [];
191
- // We *include* opacityZero in this rule. Everything else is treated as not rendered for this purpose.
192
- const nonOpacity = rs.filter((r) => String(r) !== 'opacityZero');
193
- if (nonOpacity.length) return false;
194
- }
195
- } catch {
196
- // If in doubt, keep deterministic behavior and consider it rendered.
197
- }
198
- return true;
197
+ function isTabbable(el, info) {
198
+ const f = info || getFocusableInfoSafe(el);
199
+ return !!(f && f.focusable && f.tabbable);
200
+ }
201
+
202
+ // Exclude elements that are not rendered / not in the visual rendering tree.
203
+ function isRendered(el) {
204
+ if (!isDomVisibleEligible) return true;
205
+ try {
206
+ const vis = isDomVisibleEligible(el, ctx, {
207
+ visibilityMode: 'styleOnly',
208
+ disableGeometry: true
209
+ });
210
+ if (vis && vis.eligible === false) {
211
+ const rs = Array.isArray(vis.reasons) ? vis.reasons : [];
212
+ // We *include* opacityZero in this rule. Everything else is treated as not rendered for this purpose.
213
+ const nonOpacity = rs.filter((r) => String(r) !== 'opacityZero');
214
+ if (nonOpacity.length) return false;
215
+ }
216
+ } catch {
217
+ // If in doubt, keep deterministic behavior and consider it rendered.
199
218
  }
200
- const candidates = qAll('a[href],area[href],button,input,select,textarea,summary,[tabindex],[contenteditable]');
219
+ return true;
220
+ }
221
+ const candidates = qAll(
222
+ 'a[href],area[href],button,input,select,textarea,summary,[tabindex],[contenteditable]'
223
+ );
201
224
  if (!candidates.length) {
202
225
  return { ruleId: rule.ruleId, outcome: 'notApplicable', severity: 'minor', occurrences: [] };
203
226
  }
@@ -216,7 +239,13 @@ function runInPage(ctx) {
216
239
  const hints = getVisibilityHints(el);
217
240
  if (!hints.length) continue; // <-- applicability gate
218
241
 
219
- const tagName = (() => { try { return lower(el.tagName || ''); } catch { return ''; } })();
242
+ const tagName = (() => {
243
+ try {
244
+ return lower(el.tagName || '');
245
+ } catch {
246
+ return '';
247
+ }
248
+ })();
220
249
 
221
250
  const hintOrder = ['opacityZero', 'offscreen', 'clipped', 'zeroSizeOverflowHidden'];
222
251
  const hintsArr = [];
@@ -239,7 +268,9 @@ function runInPage(ctx) {
239
268
  };
240
269
 
241
270
  occurrences.push(
242
- reportOccurrence ? reportOccurrence(el, baseOccurrence) : { __node: el, selector: '', html: '', ...baseOccurrence }
271
+ reportOccurrence
272
+ ? reportOccurrence(el, baseOccurrence)
273
+ : { __node: el, selector: '', html: '', ...baseOccurrence }
243
274
  );
244
275
  }
245
276
 
@@ -256,4 +287,4 @@ function runInPage(ctx) {
256
287
  };
257
288
  }
258
289
 
259
- module.exports = {id, meta, runInPage};
290
+ module.exports = { id, meta, runInPage };