@a11y-oracle/audit-formatter 1.0.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.
@@ -0,0 +1,317 @@
1
+ /**
2
+ * @module formatters
3
+ *
4
+ * Pure functions that convert A11y-Oracle data into OracleIssue objects.
5
+ *
6
+ * Each function takes A11y-Oracle findings + an AuditContext and returns
7
+ * an array of OracleIssue objects (empty if no issues found). The functions
8
+ * have no side effects and do not require a CDP session or orchestrator.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { formatFocusIssues } from '@a11y-oracle/audit-formatter';
13
+ *
14
+ * const state = await a11y.pressKey('Tab');
15
+ * const issues = formatFocusIssues(state, { project: 'my-app', specName: 'nav.spec.ts' });
16
+ * expect(issues).toHaveLength(0); // No focus issues
17
+ * ```
18
+ */
19
+ import { RULES, matchesWcagLevel } from './rules.js';
20
+ import { selectorFromFocusedElement, selectorFromTabOrderEntry, htmlSnippetFromFocusedElement, htmlSnippetFromTabOrderEntry, } from './selector.js';
21
+ /**
22
+ * Analyze an A11yState for focus-related issues.
23
+ *
24
+ * Checks two rules (only one fires per state — `focus-not-visible` takes priority):
25
+ * - `oracle/focus-not-visible` — `focusIndicator.isVisible === false`
26
+ * - `oracle/focus-low-contrast` — `isVisible` but `meetsWCAG_AA === false`
27
+ *
28
+ * Returns an empty array if no element is focused or focus indicator passes.
29
+ *
30
+ * @param state - Unified accessibility state from pressKey() or getState()
31
+ * @param context - Audit context (project, specName)
32
+ * @returns Array of 0 or 1 OracleIssue
33
+ */
34
+ export function formatFocusIssues(state, context) {
35
+ if (!state.focusedElement) {
36
+ return [];
37
+ }
38
+ const el = state.focusedElement;
39
+ const indicator = state.focusIndicator;
40
+ // focus-not-visible takes priority over focus-low-contrast
41
+ if (!indicator.isVisible) {
42
+ return [
43
+ buildFocusIssue('oracle/focus-not-visible', el, indicator.contrastRatio, context),
44
+ ];
45
+ }
46
+ if (!indicator.meetsWCAG_AA) {
47
+ return [
48
+ buildFocusIssue('oracle/focus-low-contrast', el, indicator.contrastRatio, context),
49
+ ];
50
+ }
51
+ return [];
52
+ }
53
+ /**
54
+ * Analyze a TraversalResult for keyboard trap issues.
55
+ *
56
+ * Checks:
57
+ * - `oracle/keyboard-trap` — `isTrapped === true`
58
+ *
59
+ * @param result - Traversal result from traverseSubTree()
60
+ * @param containerSelector - The CSS selector of the container tested for trapping
61
+ * @param context - Audit context
62
+ * @returns Array of 0 or 1 OracleIssue
63
+ */
64
+ export function formatTrapIssue(result, containerSelector, context) {
65
+ if (!result.isTrapped) {
66
+ return [];
67
+ }
68
+ const rule = RULES['oracle/keyboard-trap'];
69
+ // Build node entries from visited elements
70
+ const nodeEntries = result.visitedElements.map((entry) => ({
71
+ impact: rule.impact,
72
+ html: htmlSnippetFromTabOrderEntry(entry),
73
+ target: [selectorFromTabOrderEntry(entry)],
74
+ any: [],
75
+ all: [],
76
+ none: [
77
+ {
78
+ id: rule.ruleId,
79
+ data: null,
80
+ relatedNodes: [],
81
+ impact: rule.impact,
82
+ message: `Element is trapped inside ${containerSelector} (${result.tabCount} tabs attempted)`,
83
+ },
84
+ ],
85
+ failureSummary: rule.failureSummary,
86
+ }));
87
+ // If no visited elements, create a single node for the container
88
+ if (nodeEntries.length === 0) {
89
+ nodeEntries.push({
90
+ impact: rule.impact,
91
+ html: `<div><!-- container: ${containerSelector} --></div>`,
92
+ target: [containerSelector],
93
+ any: [],
94
+ all: [],
95
+ none: [
96
+ {
97
+ id: rule.ruleId,
98
+ data: null,
99
+ relatedNodes: [],
100
+ impact: rule.impact,
101
+ message: `Keyboard focus is trapped within ${containerSelector}`,
102
+ },
103
+ ],
104
+ failureSummary: rule.failureSummary,
105
+ });
106
+ }
107
+ return [
108
+ {
109
+ ruleId: rule.ruleId,
110
+ impact: rule.impact,
111
+ description: rule.description,
112
+ help: rule.help,
113
+ failureSummary: rule.failureSummary,
114
+ htmlSnippet: nodeEntries[0].html,
115
+ selector: containerSelector,
116
+ specName: context.specName,
117
+ project: context.project,
118
+ helpUrl: rule.helpUrl,
119
+ tags: [...rule.tags],
120
+ nodes: nodeEntries,
121
+ resultType: 'oracle',
122
+ },
123
+ ];
124
+ }
125
+ /** Roles that provide no semantic information to assistive technologies. */
126
+ const GENERIC_ROLES = new Set([
127
+ 'generic',
128
+ 'none',
129
+ 'presentation',
130
+ '',
131
+ ]);
132
+ /**
133
+ * Analyze an A11yState for missing accessible name issues.
134
+ *
135
+ * Checks:
136
+ * - `oracle/focus-missing-name` — focused element has no computed name
137
+ *
138
+ * Only fires when an element is focused AND has a meaningful role
139
+ * (elements with generic/none/presentation roles fire
140
+ * `oracle/focus-generic-role` instead).
141
+ *
142
+ * @param state - Unified accessibility state from pressKey() or getState()
143
+ * @param context - Audit context (project, specName)
144
+ * @returns Array of 0 or 1 OracleIssue
145
+ */
146
+ export function formatNameIssues(state, context) {
147
+ if (!state.focusedElement || !state.speechResult) {
148
+ return [];
149
+ }
150
+ // Only check elements with meaningful roles — generic/none handled separately
151
+ const rawRole = state.speechResult.rawNode?.role?.value ?? '';
152
+ if (GENERIC_ROLES.has(rawRole)) {
153
+ return [];
154
+ }
155
+ // Check if the computed name is empty
156
+ if (state.speechResult.name && state.speechResult.name.trim() !== '') {
157
+ return [];
158
+ }
159
+ return [buildElementIssue('oracle/focus-missing-name', state.focusedElement, null, context)];
160
+ }
161
+ /**
162
+ * Analyze an A11yState for generic/presentational role issues.
163
+ *
164
+ * Checks:
165
+ * - `oracle/focus-generic-role` — focused element has generic, none, or
166
+ * presentation role
167
+ *
168
+ * Only fires when an element is focused AND the AXNode role is one of the
169
+ * semantically empty roles. This indicates an element that receives keyboard
170
+ * focus but provides no role information to assistive technologies.
171
+ *
172
+ * @param state - Unified accessibility state from pressKey() or getState()
173
+ * @param context - Audit context (project, specName)
174
+ * @returns Array of 0 or 1 OracleIssue
175
+ */
176
+ export function formatRoleIssues(state, context) {
177
+ if (!state.focusedElement || !state.speechResult) {
178
+ return [];
179
+ }
180
+ const rawRole = state.speechResult.rawNode?.role?.value ?? '';
181
+ if (rawRole === '' || !GENERIC_ROLES.has(rawRole)) {
182
+ return [];
183
+ }
184
+ return [
185
+ buildElementIssue('oracle/focus-generic-role', state.focusedElement, { role: rawRole }, context),
186
+ ];
187
+ }
188
+ /**
189
+ * Analyze an A11yState for positive tabindex issues.
190
+ *
191
+ * Checks:
192
+ * - `oracle/positive-tabindex` — focused element has `tabIndex > 0`
193
+ *
194
+ * Positive tabindex values create an unpredictable focus order that
195
+ * diverges from the visual reading order.
196
+ *
197
+ * @param state - Unified accessibility state from pressKey() or getState()
198
+ * @param context - Audit context (project, specName)
199
+ * @returns Array of 0 or 1 OracleIssue
200
+ */
201
+ export function formatTabIndexIssues(state, context) {
202
+ if (!state.focusedElement) {
203
+ return [];
204
+ }
205
+ if (state.focusedElement.tabIndex <= 0) {
206
+ return [];
207
+ }
208
+ return [
209
+ buildElementIssue('oracle/positive-tabindex', state.focusedElement, { tabIndex: state.focusedElement.tabIndex }, context),
210
+ ];
211
+ }
212
+ /**
213
+ * Convenience: analyze an A11yState for ALL state-based rules.
214
+ *
215
+ * Runs focus indicator checks, name/role checks, and tabindex checks.
216
+ * Trap detection requires a separate TraversalResult — use
217
+ * `formatTrapIssue()` for that.
218
+ *
219
+ * @param state - Unified accessibility state
220
+ * @param context - Audit context
221
+ * @returns Array of OracleIssue objects
222
+ */
223
+ export function formatAllIssues(state, context) {
224
+ const level = context.wcagLevel ?? 'wcag22aa';
225
+ const disabledSet = context.disabledRules
226
+ ? new Set(context.disabledRules)
227
+ : null;
228
+ const allIssues = [
229
+ ...formatFocusIssues(state, context),
230
+ ...formatNameIssues(state, context),
231
+ ...formatRoleIssues(state, context),
232
+ ...formatTabIndexIssues(state, context),
233
+ ];
234
+ return allIssues.filter((issue) => {
235
+ if (disabledSet?.has(issue.ruleId))
236
+ return false;
237
+ return matchesWcagLevel(RULES[issue.ruleId], level);
238
+ });
239
+ }
240
+ // ---- Internal helpers ----
241
+ function buildElementIssue(ruleId, el, data, context) {
242
+ const rule = RULES[ruleId];
243
+ const selector = selectorFromFocusedElement(el);
244
+ const html = htmlSnippetFromFocusedElement(el);
245
+ const node = {
246
+ impact: rule.impact,
247
+ html,
248
+ target: [selector],
249
+ any: [],
250
+ all: [],
251
+ none: [
252
+ {
253
+ id: ruleId,
254
+ data,
255
+ relatedNodes: [],
256
+ impact: rule.impact,
257
+ message: rule.help,
258
+ },
259
+ ],
260
+ failureSummary: rule.failureSummary,
261
+ };
262
+ return {
263
+ ruleId,
264
+ impact: rule.impact,
265
+ description: rule.description,
266
+ help: rule.help,
267
+ failureSummary: rule.failureSummary,
268
+ htmlSnippet: html,
269
+ selector,
270
+ specName: context.specName,
271
+ project: context.project,
272
+ helpUrl: rule.helpUrl,
273
+ tags: [...rule.tags],
274
+ nodes: [node],
275
+ resultType: 'oracle',
276
+ };
277
+ }
278
+ function buildFocusIssue(ruleId, el, contrastRatio, context) {
279
+ const rule = RULES[ruleId];
280
+ const selector = selectorFromFocusedElement(el);
281
+ const html = htmlSnippetFromFocusedElement(el);
282
+ const contrastInfo = contrastRatio !== null
283
+ ? ` (contrast ratio: ${contrastRatio.toFixed(2)}:1)`
284
+ : '';
285
+ const node = {
286
+ impact: rule.impact,
287
+ html,
288
+ target: [selector],
289
+ any: [],
290
+ all: [],
291
+ none: [
292
+ {
293
+ id: ruleId,
294
+ data: contrastRatio !== null ? { contrastRatio } : null,
295
+ relatedNodes: [],
296
+ impact: rule.impact,
297
+ message: `${rule.help}${contrastInfo}`,
298
+ },
299
+ ],
300
+ failureSummary: rule.failureSummary,
301
+ };
302
+ return {
303
+ ruleId,
304
+ impact: rule.impact,
305
+ description: rule.description,
306
+ help: rule.help,
307
+ failureSummary: rule.failureSummary,
308
+ htmlSnippet: html,
309
+ selector,
310
+ specName: context.specName,
311
+ project: context.project,
312
+ helpUrl: rule.helpUrl,
313
+ tags: [...rule.tags],
314
+ nodes: [node],
315
+ resultType: 'oracle',
316
+ };
317
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * @module oracle-auditor
3
+ *
4
+ * Convenience wrapper that wraps an orchestrator-like object and
5
+ * automatically audits each interaction for accessibility issues.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Playwright
10
+ * const auditor = new OracleAuditor(a11y, {
11
+ * project: 'my-app',
12
+ * specName: 'nav.spec.ts',
13
+ * });
14
+ * await auditor.pressKey('Tab');
15
+ * await auditor.pressKey('Tab');
16
+ * await auditor.checkTrap('#modal');
17
+ * const issues = auditor.getIssues();
18
+ * ```
19
+ */
20
+ import type { A11yState } from '@a11y-oracle/core-engine';
21
+ import type { TraversalResult } from '@a11y-oracle/focus-analyzer';
22
+ import type { OracleIssue, AuditContext } from './types.js';
23
+ /**
24
+ * Minimal interface satisfied by A11yOrchestrator and A11yOracle.
25
+ *
26
+ * Defined locally so the audit-formatter doesn't require a hard
27
+ * dependency on any specific orchestrator implementation.
28
+ */
29
+ export interface OrchestratorLike {
30
+ pressKey(key: string, modifiers?: Record<string, boolean>): Promise<A11yState>;
31
+ getState(): Promise<A11yState>;
32
+ traverseSubTree(selector: string, maxTabs?: number): Promise<TraversalResult>;
33
+ }
34
+ /**
35
+ * Convenience wrapper that accumulates Oracle issues across multiple
36
+ * keyboard interactions and trap checks.
37
+ *
38
+ * Each call to `pressKey()`, `getState()`, or `checkTrap()` automatically
39
+ * runs the relevant audit rules and accumulates any issues found. Call
40
+ * `getIssues()` at the end to retrieve all accumulated issues.
41
+ */
42
+ export declare class OracleAuditor {
43
+ private orchestrator;
44
+ private context;
45
+ private issues;
46
+ private previousKeys;
47
+ constructor(orchestrator: OrchestratorLike, context: AuditContext);
48
+ /**
49
+ * Press a key via the orchestrator and analyze the resulting state
50
+ * for focus issues. Returns the A11yState for further assertions.
51
+ *
52
+ * Deduplicates issues that match the previous `pressKey` call
53
+ * (same ruleId + selector), preventing noise when Tab doesn't
54
+ * move focus (e.g., end of page, trapped element).
55
+ */
56
+ pressKey(key: string, modifiers?: Record<string, boolean>): Promise<A11yState>;
57
+ /**
58
+ * Get the current state and analyze it for focus issues.
59
+ * Useful after programmatic focus changes or click handlers.
60
+ */
61
+ getState(): Promise<A11yState>;
62
+ /**
63
+ * Check a container for keyboard traps (WCAG 2.1.2).
64
+ */
65
+ checkTrap(selector: string, maxTabs?: number): Promise<TraversalResult>;
66
+ /**
67
+ * Return all issues accumulated during the session.
68
+ * Does not clear the internal list — call `clear()` to reset.
69
+ */
70
+ getIssues(): ReadonlyArray<OracleIssue>;
71
+ /**
72
+ * Clear all accumulated issues.
73
+ */
74
+ clear(): void;
75
+ /**
76
+ * The number of accumulated issues.
77
+ */
78
+ get issueCount(): number;
79
+ }
80
+ //# sourceMappingURL=oracle-auditor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oracle-auditor.d.ts","sourceRoot":"","sources":["../../src/lib/oracle-auditor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG5D;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CACN,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,SAAS,CAAC,CAAC;IACtB,QAAQ,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC;IAC/B,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,CAAC,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,YAAY,CAA0B;gBAElC,YAAY,EAAE,gBAAgB,EAAE,OAAO,EAAE,YAAY;IAKjE;;;;;;;OAOG;IACG,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,CAAC,SAAS,CAAC;IAiBrB;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,SAAS,CAAC;IAMpC;;OAEG;IACG,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,CAAC;IAW3B;;;OAGG;IACH,SAAS,IAAI,aAAa,CAAC,WAAW,CAAC;IAIvC;;OAEG;IACH,KAAK,IAAI,IAAI;IAKb;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAEvB;CACF"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @module oracle-auditor
3
+ *
4
+ * Convenience wrapper that wraps an orchestrator-like object and
5
+ * automatically audits each interaction for accessibility issues.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Playwright
10
+ * const auditor = new OracleAuditor(a11y, {
11
+ * project: 'my-app',
12
+ * specName: 'nav.spec.ts',
13
+ * });
14
+ * await auditor.pressKey('Tab');
15
+ * await auditor.pressKey('Tab');
16
+ * await auditor.checkTrap('#modal');
17
+ * const issues = auditor.getIssues();
18
+ * ```
19
+ */
20
+ import { formatAllIssues, formatTrapIssue } from './formatters.js';
21
+ /**
22
+ * Convenience wrapper that accumulates Oracle issues across multiple
23
+ * keyboard interactions and trap checks.
24
+ *
25
+ * Each call to `pressKey()`, `getState()`, or `checkTrap()` automatically
26
+ * runs the relevant audit rules and accumulates any issues found. Call
27
+ * `getIssues()` at the end to retrieve all accumulated issues.
28
+ */
29
+ export class OracleAuditor {
30
+ orchestrator;
31
+ context;
32
+ issues = [];
33
+ previousKeys = new Set();
34
+ constructor(orchestrator, context) {
35
+ this.orchestrator = orchestrator;
36
+ this.context = context;
37
+ }
38
+ /**
39
+ * Press a key via the orchestrator and analyze the resulting state
40
+ * for focus issues. Returns the A11yState for further assertions.
41
+ *
42
+ * Deduplicates issues that match the previous `pressKey` call
43
+ * (same ruleId + selector), preventing noise when Tab doesn't
44
+ * move focus (e.g., end of page, trapped element).
45
+ */
46
+ async pressKey(key, modifiers) {
47
+ const state = await this.orchestrator.pressKey(key, modifiers);
48
+ const newIssues = formatAllIssues(state, this.context);
49
+ const currentKeys = new Set();
50
+ for (const issue of newIssues) {
51
+ const dedupKey = `${issue.ruleId}::${issue.selector}`;
52
+ currentKeys.add(dedupKey);
53
+ if (!this.previousKeys.has(dedupKey)) {
54
+ this.issues.push(issue);
55
+ }
56
+ }
57
+ this.previousKeys = currentKeys;
58
+ return state;
59
+ }
60
+ /**
61
+ * Get the current state and analyze it for focus issues.
62
+ * Useful after programmatic focus changes or click handlers.
63
+ */
64
+ async getState() {
65
+ const state = await this.orchestrator.getState();
66
+ this.issues.push(...formatAllIssues(state, this.context));
67
+ return state;
68
+ }
69
+ /**
70
+ * Check a container for keyboard traps (WCAG 2.1.2).
71
+ */
72
+ async checkTrap(selector, maxTabs) {
73
+ const result = await this.orchestrator.traverseSubTree(selector, maxTabs);
74
+ this.issues.push(...formatTrapIssue(result, selector, this.context));
75
+ return result;
76
+ }
77
+ /**
78
+ * Return all issues accumulated during the session.
79
+ * Does not clear the internal list — call `clear()` to reset.
80
+ */
81
+ getIssues() {
82
+ return [...this.issues];
83
+ }
84
+ /**
85
+ * Clear all accumulated issues.
86
+ */
87
+ clear() {
88
+ this.issues = [];
89
+ this.previousKeys = new Set();
90
+ }
91
+ /**
92
+ * The number of accumulated issues.
93
+ */
94
+ get issueCount() {
95
+ return this.issues.length;
96
+ }
97
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @module rules
3
+ *
4
+ * Oracle audit rule definitions with WCAG metadata.
5
+ *
6
+ * Each rule maps to a specific WCAG criterion and carries all the metadata
7
+ * needed to produce a fully populated OracleIssue. Rules are keyed by their
8
+ * `ruleId` (e.g., `oracle/focus-not-visible`).
9
+ */
10
+ import type { OracleRule, WcagLevel } from './types.js';
11
+ /**
12
+ * All defined Oracle audit rules.
13
+ *
14
+ * Focus indicator rules:
15
+ * - `oracle/focus-not-visible` — WCAG 2.4.7 Focus Visible (Level AA)
16
+ * - `oracle/focus-low-contrast` — WCAG 2.4.12 Focus Appearance (Level AA, WCAG 2.2)
17
+ *
18
+ * Keyboard navigation rules:
19
+ * - `oracle/keyboard-trap` — WCAG 2.1.2 No Keyboard Trap (Level A)
20
+ * - `oracle/focus-missing-name` — WCAG 4.1.2 Name, Role, Value (Level A)
21
+ * - `oracle/focus-generic-role` — WCAG 4.1.2 Name, Role, Value (Level A)
22
+ * - `oracle/positive-tabindex` — WCAG 2.4.3 Focus Order (Level A)
23
+ */
24
+ export declare const RULES: Record<string, OracleRule>;
25
+ /** Get a rule by ID, or throw if unknown. */
26
+ export declare function getRule(ruleId: string): OracleRule;
27
+ /**
28
+ * Check if a rule applies at the given WCAG standard.
29
+ *
30
+ * A rule matches if any of its tags appear in the set of tags
31
+ * covered by the given standard. For example, a rule tagged
32
+ * `wcag22aa` matches `'wcag22aa'` but not `'wcag21aa'`.
33
+ */
34
+ export declare function matchesWcagLevel(rule: OracleRule, level: WcagLevel): boolean;
35
+ /** All defined rule IDs. */
36
+ export declare const RULE_IDS: string[];
37
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/lib/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAiG5C,CAAC;AAEF,6CAA6C;AAC7C,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAMlD;AAkBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAG5E;AAED,4BAA4B;AAC5B,eAAO,MAAM,QAAQ,UAAqB,CAAC"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @module rules
3
+ *
4
+ * Oracle audit rule definitions with WCAG metadata.
5
+ *
6
+ * Each rule maps to a specific WCAG criterion and carries all the metadata
7
+ * needed to produce a fully populated OracleIssue. Rules are keyed by their
8
+ * `ruleId` (e.g., `oracle/focus-not-visible`).
9
+ */
10
+ /**
11
+ * All defined Oracle audit rules.
12
+ *
13
+ * Focus indicator rules:
14
+ * - `oracle/focus-not-visible` — WCAG 2.4.7 Focus Visible (Level AA)
15
+ * - `oracle/focus-low-contrast` — WCAG 2.4.12 Focus Appearance (Level AA, WCAG 2.2)
16
+ *
17
+ * Keyboard navigation rules:
18
+ * - `oracle/keyboard-trap` — WCAG 2.1.2 No Keyboard Trap (Level A)
19
+ * - `oracle/focus-missing-name` — WCAG 4.1.2 Name, Role, Value (Level A)
20
+ * - `oracle/focus-generic-role` — WCAG 4.1.2 Name, Role, Value (Level A)
21
+ * - `oracle/positive-tabindex` — WCAG 2.4.3 Focus Order (Level A)
22
+ */
23
+ export const RULES = {
24
+ 'oracle/focus-not-visible': {
25
+ ruleId: 'oracle/focus-not-visible',
26
+ help: 'Focused element must have a visible focus indicator',
27
+ description: 'Ensures that every interactive element has a visible focus indicator ' +
28
+ 'when focused via the keyboard. A missing focus indicator makes it ' +
29
+ 'impossible for keyboard users to know where they are on the page.',
30
+ impact: 'serious',
31
+ tags: ['wcag2aa', 'wcag247', 'cat.keyboard', 'oracle'],
32
+ helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html',
33
+ failureSummary: 'Fix any of the following:\n' +
34
+ ' Element has no visible focus indicator when focused via keyboard.',
35
+ },
36
+ 'oracle/focus-low-contrast': {
37
+ ruleId: 'oracle/focus-low-contrast',
38
+ help: 'Focus indicator must have sufficient contrast (>= 3:1)',
39
+ description: 'Ensures the visual focus indicator has a contrast ratio of at least ' +
40
+ '3:1 against the background, meeting WCAG 2.4.12 (Focus Appearance) ' +
41
+ 'AA requirements.',
42
+ impact: 'moderate',
43
+ tags: ['wcag22aa', 'wcag2412', 'cat.keyboard', 'oracle'],
44
+ helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html',
45
+ failureSummary: 'Fix any of the following:\n' +
46
+ ' Focus indicator contrast ratio is below the 3:1 minimum.',
47
+ },
48
+ 'oracle/keyboard-trap': {
49
+ ruleId: 'oracle/keyboard-trap',
50
+ help: 'Interactive content must not trap keyboard focus',
51
+ description: 'Ensures that keyboard focus can be moved away from any interactive ' +
52
+ 'component using the keyboard alone. A keyboard trap prevents users ' +
53
+ 'from navigating the rest of the page.',
54
+ impact: 'critical',
55
+ tags: ['wcag2a', 'wcag212', 'cat.keyboard', 'oracle'],
56
+ helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/no-keyboard-trap.html',
57
+ failureSummary: 'Fix any of the following:\n' +
58
+ ' Keyboard focus is trapped within the container and cannot escape.',
59
+ },
60
+ 'oracle/focus-missing-name': {
61
+ ruleId: 'oracle/focus-missing-name',
62
+ help: 'Focused element must have an accessible name',
63
+ description: 'Ensures that interactive elements have a non-empty computed accessible ' +
64
+ 'name when focused via the keyboard. Without an accessible name, screen ' +
65
+ 'readers cannot identify the element to users.',
66
+ impact: 'serious',
67
+ tags: ['wcag2a', 'wcag412', 'cat.keyboard', 'oracle'],
68
+ helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
69
+ failureSummary: 'Fix any of the following:\n' +
70
+ ' Focused element has no accessible name (aria-label, aria-labelledby, or text content).',
71
+ },
72
+ 'oracle/focus-generic-role': {
73
+ ruleId: 'oracle/focus-generic-role',
74
+ help: 'Focused element must have a meaningful role',
75
+ description: 'Ensures that interactive elements exposed to the keyboard have a ' +
76
+ 'meaningful ARIA role. Elements with generic, presentation, or none ' +
77
+ 'roles provide no semantic information to assistive technologies.',
78
+ impact: 'serious',
79
+ tags: ['wcag2a', 'wcag412', 'cat.keyboard', 'oracle'],
80
+ helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html',
81
+ failureSummary: 'Fix any of the following:\n' +
82
+ ' Focused element has a generic or presentational role instead of a semantic role.',
83
+ },
84
+ 'oracle/positive-tabindex': {
85
+ ruleId: 'oracle/positive-tabindex',
86
+ help: 'Elements should not use positive tabindex values',
87
+ description: 'Ensures that elements in the tab order do not use tabindex values ' +
88
+ 'greater than 0. Positive tabindex creates an unpredictable focus ' +
89
+ 'order that diverges from the visual reading order, making keyboard ' +
90
+ 'navigation confusing for users.',
91
+ impact: 'serious',
92
+ tags: ['wcag2a', 'wcag243', 'cat.keyboard', 'oracle'],
93
+ helpUrl: 'https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html',
94
+ failureSummary: 'Fix any of the following:\n' +
95
+ ' Element uses a positive tabindex value, disrupting the natural focus order.',
96
+ },
97
+ };
98
+ /** Get a rule by ID, or throw if unknown. */
99
+ export function getRule(ruleId) {
100
+ const rule = RULES[ruleId];
101
+ if (!rule) {
102
+ throw new Error(`Unknown oracle rule: ${ruleId}`);
103
+ }
104
+ return rule;
105
+ }
106
+ /**
107
+ * Map each WCAG standard to the set of rule-level tags it includes.
108
+ *
109
+ * Each standard includes all rules from earlier versions at the same
110
+ * or lower conformance level. For example, `'wcag21aa'` includes
111
+ * rules tagged `wcag2a`, `wcag2aa`, `wcag21a`, and `wcag21aa`.
112
+ */
113
+ const STANDARD_TAG_MAP = {
114
+ 'wcag2a': new Set(['wcag2a']),
115
+ 'wcag2aa': new Set(['wcag2a', 'wcag2aa']),
116
+ 'wcag21a': new Set(['wcag2a', 'wcag21a']),
117
+ 'wcag21aa': new Set(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']),
118
+ 'wcag22a': new Set(['wcag2a', 'wcag21a', 'wcag22a']),
119
+ 'wcag22aa': new Set(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa']),
120
+ };
121
+ /**
122
+ * Check if a rule applies at the given WCAG standard.
123
+ *
124
+ * A rule matches if any of its tags appear in the set of tags
125
+ * covered by the given standard. For example, a rule tagged
126
+ * `wcag22aa` matches `'wcag22aa'` but not `'wcag21aa'`.
127
+ */
128
+ export function matchesWcagLevel(rule, level) {
129
+ const allowedTags = STANDARD_TAG_MAP[level];
130
+ return rule.tags.some((tag) => allowedTags.has(tag));
131
+ }
132
+ /** All defined rule IDs. */
133
+ export const RULE_IDS = Object.keys(RULES);