@a11y-oracle/axe-bridge 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.
- package/README.md +254 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/lib/axe-bridge.d.ts +33 -0
- package/dist/lib/axe-bridge.d.ts.map +1 -0
- package/dist/lib/axe-bridge.js +106 -0
- package/dist/lib/resolve-all.d.ts +45 -0
- package/dist/lib/resolve-all.d.ts.map +1 -0
- package/dist/lib/resolve-all.js +117 -0
- package/dist/lib/resolver-pipeline.d.ts +51 -0
- package/dist/lib/resolver-pipeline.d.ts.map +1 -0
- package/dist/lib/resolver-pipeline.js +94 -0
- package/dist/lib/resolvers/aria-hidden-focus.d.ts +31 -0
- package/dist/lib/resolvers/aria-hidden-focus.d.ts.map +1 -0
- package/dist/lib/resolvers/aria-hidden-focus.js +148 -0
- package/dist/lib/resolvers/content-on-hover.d.ts +27 -0
- package/dist/lib/resolvers/content-on-hover.d.ts.map +1 -0
- package/dist/lib/resolvers/content-on-hover.js +230 -0
- package/dist/lib/resolvers/focus-indicator.d.ts +32 -0
- package/dist/lib/resolvers/focus-indicator.d.ts.map +1 -0
- package/dist/lib/resolvers/focus-indicator.js +188 -0
- package/dist/lib/resolvers/frame-tested.d.ts +31 -0
- package/dist/lib/resolvers/frame-tested.d.ts.map +1 -0
- package/dist/lib/resolvers/frame-tested.js +177 -0
- package/dist/lib/resolvers/identical-links-same-purpose.d.ts +35 -0
- package/dist/lib/resolvers/identical-links-same-purpose.d.ts.map +1 -0
- package/dist/lib/resolvers/identical-links-same-purpose.js +117 -0
- package/dist/lib/resolvers/link-in-text-block.d.ts +29 -0
- package/dist/lib/resolvers/link-in-text-block.d.ts.map +1 -0
- package/dist/lib/resolvers/link-in-text-block.js +141 -0
- package/dist/lib/resolvers/scrollable-region-focusable.d.ts +26 -0
- package/dist/lib/resolvers/scrollable-region-focusable.d.ts.map +1 -0
- package/dist/lib/resolvers/scrollable-region-focusable.js +139 -0
- package/dist/lib/resolvers/skip-link.d.ts +26 -0
- package/dist/lib/resolvers/skip-link.d.ts.map +1 -0
- package/dist/lib/resolvers/skip-link.js +140 -0
- package/dist/lib/resolvers/target-size.d.ts +25 -0
- package/dist/lib/resolvers/target-size.d.ts.map +1 -0
- package/dist/lib/resolvers/target-size.js +125 -0
- package/dist/lib/types.d.ts +227 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +8 -0
- package/dist/lib/wcag-thresholds.d.ts +34 -0
- package/dist/lib/wcag-thresholds.d.ts.map +1 -0
- package/dist/lib/wcag-thresholds.js +55 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# @a11y-oracle/axe-bridge
|
|
2
|
+
|
|
3
|
+
Axe-core result post-processor that resolves "incomplete" (Needs Review) findings using visual analysis, keyboard interaction, and CDP inspection. Drop-in middleware between axe-core's `analyze()` and your assertion layer.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
axe-core marks rules as "incomplete" when they require state changes, interaction, or spatial awareness that static DOM analysis cannot reliably perform. This creates noise in CI dashboards and requires manual review for rules like color contrast, focus indicators, target sizes, and more.
|
|
8
|
+
|
|
9
|
+
## The Solution
|
|
10
|
+
|
|
11
|
+
`resolveAllIncomplete()` takes axe-core results and a live CDP session, then pipes them through 10 specialized resolvers that promote findings from `incomplete` to `passes` or `violations`:
|
|
12
|
+
|
|
13
|
+
| # | Rule ID | WCAG SC | Technique |
|
|
14
|
+
|---|---------|---------|-----------|
|
|
15
|
+
| 1 | `color-contrast` | 1.4.3 | CSS halo heuristic + pixel-level screenshot analysis |
|
|
16
|
+
| 2 | `identical-links-same-purpose` | 2.4.4 | URL normalization and comparison |
|
|
17
|
+
| 3 | `link-in-text-block` | 1.4.1 | Default-state computed style checks |
|
|
18
|
+
| 4 | `target-size` | 2.5.8 | Bounding box measurements + spacing |
|
|
19
|
+
| 5 | `scrollable-region-focusable` | 2.1.1 | Scroll height + focusable child traversal |
|
|
20
|
+
| 6 | `skip-link` | 2.4.1 | Tab-focus visibility verification |
|
|
21
|
+
| 7 | `aria-hidden-focus` | 4.1.2 | Full keyboard Tab traversal |
|
|
22
|
+
| 8 | `focus-indicator` | 2.4.7 | Before/after screenshot pixel diff |
|
|
23
|
+
| 9 | `content-on-hover` | 1.4.13 | Hover + dismiss interaction tests |
|
|
24
|
+
| 10 | `frame-tested` | N/A | Cross-origin iframe axe-core injection |
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @a11y-oracle/axe-bridge
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### Resolve All Incompletes (Recommended)
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
|
|
38
|
+
|
|
39
|
+
// After running axe-core while the page is still live:
|
|
40
|
+
const axeResults = await axe.run(document);
|
|
41
|
+
const resolved = await resolveAllIncomplete(cdpSession, axeResults);
|
|
42
|
+
|
|
43
|
+
// resolved.incomplete will have fewer (or zero) entries
|
|
44
|
+
// resolved.violations and resolved.passes will have the promoted entries
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### With Options
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const resolved = await resolveAllIncomplete(cdpSession, axeResults, {
|
|
51
|
+
wcagLevel: 'wcag22aa',
|
|
52
|
+
contrast: { threshold: 4.5, largeTextThreshold: 3.0 },
|
|
53
|
+
focusIndicator: { focusSettleDelay: 150, diffThreshold: 0.2 },
|
|
54
|
+
skipRules: ['frame-tested'], // skip specific resolvers
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Individual Resolvers
|
|
59
|
+
|
|
60
|
+
Each resolver can be used standalone:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { resolveIncompleteContrast } from '@a11y-oracle/axe-bridge';
|
|
64
|
+
|
|
65
|
+
const resolved = await resolveIncompleteContrast(cdpSession, axeResults, {
|
|
66
|
+
wcagLevel: 'wcag22aa',
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### With Playwright
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { test, expect } from '@playwright/test';
|
|
74
|
+
import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
|
|
75
|
+
|
|
76
|
+
test('no unresolved accessibility issues', async ({ page }) => {
|
|
77
|
+
await page.goto('/my-page.html');
|
|
78
|
+
const cdp = await page.context().newCDPSession(page);
|
|
79
|
+
|
|
80
|
+
const axeResults = await axe.run(document);
|
|
81
|
+
const resolved = await resolveAllIncomplete(cdp, axeResults);
|
|
82
|
+
|
|
83
|
+
expect(resolved.violations).toHaveLength(0);
|
|
84
|
+
expect(resolved.incomplete).toHaveLength(0);
|
|
85
|
+
|
|
86
|
+
await cdp.detach();
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### With Cypress
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Using Cypress.automation for CDP access:
|
|
94
|
+
cy.window().then(async (win) => {
|
|
95
|
+
const axeResults = await axeCore.run(win.document);
|
|
96
|
+
|
|
97
|
+
// cdpSession obtained via Cypress.automation('remote:debugger:protocol', ...)
|
|
98
|
+
const resolved = await resolveAllIncomplete(cdpSession, axeResults);
|
|
99
|
+
|
|
100
|
+
expect(resolved.violations).to.have.length(0);
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API Reference
|
|
105
|
+
|
|
106
|
+
### `resolveAllIncomplete(cdp, axeResults, options?)`
|
|
107
|
+
|
|
108
|
+
Orchestrator that pipes results through all 10 resolvers in sequence. Each resolver receives the output of the previous one.
|
|
109
|
+
|
|
110
|
+
- **Parameters:**
|
|
111
|
+
- `cdp: CDPSessionLike` — CDP session connected to the page
|
|
112
|
+
- `axeResults: AxeResults` — Raw results from axe-core's `analyze()`
|
|
113
|
+
- `options?: IncompleteResolutionOptions` — Per-resolver options and `skipRules` filter
|
|
114
|
+
- **Returns:** `Promise<AxeResults>` — Modified copy with resolved findings
|
|
115
|
+
- **Pipeline order:** color-contrast → identical-links-same-purpose → link-in-text-block → target-size → scrollable-region-focusable → skip-link → aria-hidden-focus → focus-indicator → content-on-hover → frame-tested
|
|
116
|
+
|
|
117
|
+
### Individual Resolvers
|
|
118
|
+
|
|
119
|
+
#### `resolveIncompleteContrast(cdp, axeResults, options?)`
|
|
120
|
+
|
|
121
|
+
Resolves `color-contrast` incomplete entries using CSS halo heuristics and pixel-level screenshot analysis.
|
|
122
|
+
|
|
123
|
+
- **Options:** `ContrastResolutionOptions` — `wcagLevel`, `threshold`, `largeTextThreshold`
|
|
124
|
+
- Automatically detects large text from axe-core's check data (>= 24px or bold >= 18.66px)
|
|
125
|
+
|
|
126
|
+
#### `resolveIdenticalLinksSamePurpose(cdp, axeResults)`
|
|
127
|
+
|
|
128
|
+
Resolves `identical-links-same-purpose` by normalizing URLs (stripping query params, hashes, resolving relative paths) and comparing destinations.
|
|
129
|
+
|
|
130
|
+
- Same destination → **Pass**, different → **Violation**
|
|
131
|
+
|
|
132
|
+
#### `resolveLinkInTextBlock(cdp, axeResults, options?)`
|
|
133
|
+
|
|
134
|
+
Resolves `link-in-text-block` by checking the **default/resting state** for non-color visual indicators.
|
|
135
|
+
|
|
136
|
+
- **Options:** `LinkInTextBlockOptions` — `linkTextContrastThreshold` (default: 3.0)
|
|
137
|
+
- Checks: `text-decoration: underline`, `border-bottom > 0`, `font-weight` difference from parent
|
|
138
|
+
- If no non-color indicator: compares link vs surrounding text color contrast
|
|
139
|
+
|
|
140
|
+
#### `resolveTargetSize(cdp, axeResults, options?)`
|
|
141
|
+
|
|
142
|
+
Resolves `target-size` by measuring bounding boxes and center-to-center spacing.
|
|
143
|
+
|
|
144
|
+
- **Options:** `TargetSizeOptions` — `minSize` (default: 24)
|
|
145
|
+
- Width/height >= 24px → **Pass**; undersized with 24px+ spacing → **Pass**; otherwise → **Violation**
|
|
146
|
+
|
|
147
|
+
#### `resolveScrollableRegionFocusable(cdp, axeResults, options?)`
|
|
148
|
+
|
|
149
|
+
Resolves `scrollable-region-focusable` by checking scroll height, tabindex, and focusable children.
|
|
150
|
+
|
|
151
|
+
- **Options:** `ScrollableRegionOptions` — `maxChildren` (default: 50)
|
|
152
|
+
- Not actually scrollable → **Pass**; has `tabindex >= 0` → **Pass**; focusable children scroll to content → **Pass**
|
|
153
|
+
|
|
154
|
+
#### `resolveSkipLink(cdp, axeResults, options?)`
|
|
155
|
+
|
|
156
|
+
Resolves `skip-link` by focusing the skip link and verifying it becomes visible.
|
|
157
|
+
|
|
158
|
+
- **Options:** `SkipLinkOptions` — `focusSettleDelay` (default: 100)
|
|
159
|
+
- Checks bounding box, viewport containment, opacity, clip, position
|
|
160
|
+
|
|
161
|
+
#### `resolveAriaHiddenFocus(cdp, axeResults, options?)`
|
|
162
|
+
|
|
163
|
+
Resolves `aria-hidden-focus` via a single keyboard Tab traversal across all flagged nodes.
|
|
164
|
+
|
|
165
|
+
- **Options:** `AriaHiddenFocusOptions` — `maxTabs` (default: 100)
|
|
166
|
+
- Tab lands on flagged element → **Violation**; traversal completes without landing → **Pass**
|
|
167
|
+
|
|
168
|
+
#### `resolveFocusIndicator(cdp, axeResults, options?)`
|
|
169
|
+
|
|
170
|
+
Resolves `focus-indicator` by pixel-diffing before/after focus screenshots.
|
|
171
|
+
|
|
172
|
+
- **Options:** `FocusIndicatorOptions` — `focusSettleDelay` (default: 100), `diffThreshold` (default: 0.1%)
|
|
173
|
+
- Screenshots are identical → **Violation**; pixels changed → **Pass**
|
|
174
|
+
|
|
175
|
+
#### `resolveContentOnHover(cdp, axeResults, options?)`
|
|
176
|
+
|
|
177
|
+
Resolves `content-on-hover` with hover and dismiss interaction tests.
|
|
178
|
+
|
|
179
|
+
- **Options:** `ContentOnHoverOptions` — `hoverDelay` (default: 300), `dismissDelay` (default: 200)
|
|
180
|
+
- Tests: content appears on hover, remains when mouse moves to content (hoverable), disappears on Escape (dismissible)
|
|
181
|
+
|
|
182
|
+
#### `resolveFrameTested(cdp, axeResults, options?)`
|
|
183
|
+
|
|
184
|
+
Resolves `frame-tested` by injecting axe-core into cross-origin iframes via CDP.
|
|
185
|
+
|
|
186
|
+
- **Options:** `FrameTestedOptions` — `axeSource` (complete axe-core script), `iframeTimeout` (default: 30000)
|
|
187
|
+
- Uses `Page.createIsolatedWorld` to bypass same-origin restrictions
|
|
188
|
+
|
|
189
|
+
### Pipeline Utilities
|
|
190
|
+
|
|
191
|
+
Shared helpers used by all resolvers:
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
import {
|
|
195
|
+
getSelector, // Extract innermost CSS selector from axe node target
|
|
196
|
+
cloneResults, // Deep-clone AxeResults without mutation
|
|
197
|
+
ruleShell, // Create a rule shell (metadata only, no nodes)
|
|
198
|
+
findIncompleteRule, // Find a rule by ID in the incomplete array
|
|
199
|
+
applyPromotions, // Move nodes between incomplete/passes/violations
|
|
200
|
+
} from '@a11y-oracle/axe-bridge';
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Types
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import type {
|
|
207
|
+
// Axe-core compatible types
|
|
208
|
+
AxeResults,
|
|
209
|
+
AxeRule,
|
|
210
|
+
AxeNode,
|
|
211
|
+
AxeCheck,
|
|
212
|
+
|
|
213
|
+
// WCAG level
|
|
214
|
+
WcagLevel,
|
|
215
|
+
ContrastThresholds,
|
|
216
|
+
|
|
217
|
+
// Per-resolver options
|
|
218
|
+
ContrastResolutionOptions,
|
|
219
|
+
LinkInTextBlockOptions,
|
|
220
|
+
TargetSizeOptions,
|
|
221
|
+
ScrollableRegionOptions,
|
|
222
|
+
SkipLinkOptions,
|
|
223
|
+
AriaHiddenFocusOptions,
|
|
224
|
+
FocusIndicatorOptions,
|
|
225
|
+
ContentOnHoverOptions,
|
|
226
|
+
FrameTestedOptions,
|
|
227
|
+
|
|
228
|
+
// Orchestrator options
|
|
229
|
+
IncompleteResolutionOptions,
|
|
230
|
+
|
|
231
|
+
// Pipeline utility types
|
|
232
|
+
PromotionResult,
|
|
233
|
+
} from '@a11y-oracle/axe-bridge';
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
> **Note:** Axe-core types are locally defined for structural compatibility without requiring axe-core as a runtime dependency.
|
|
237
|
+
|
|
238
|
+
## How It Works
|
|
239
|
+
|
|
240
|
+
Each resolver follows a common pipeline pattern:
|
|
241
|
+
|
|
242
|
+
1. **Clone** — Deep-clone the input results (original is never mutated)
|
|
243
|
+
2. **Find** — Locate the target rule in the `incomplete` array
|
|
244
|
+
3. **Analyze** — For each flagged node, run the resolver's specific technique (CDP queries, screenshots, keyboard traversal, etc.)
|
|
245
|
+
4. **Promote** — Move resolved nodes to `passes` or `violations`; unresolved nodes stay in `incomplete`
|
|
246
|
+
|
|
247
|
+
The `resolveAllIncomplete` orchestrator chains all resolvers in sequence, so findings accumulate through the pipeline.
|
|
248
|
+
|
|
249
|
+
## Dependencies
|
|
250
|
+
|
|
251
|
+
- **`@a11y-oracle/visual-engine`** — Visual analysis engine (halo detection, pixel analysis, PNG decoding, CDP capture)
|
|
252
|
+
- **`@a11y-oracle/focus-analyzer`** — Color parsing and contrast ratio computation
|
|
253
|
+
- **`@a11y-oracle/keyboard-engine`** — Native CDP keyboard dispatch for skip-link, aria-hidden-focus, and content-on-hover
|
|
254
|
+
- **`@a11y-oracle/cdp-types`** — `CDPSessionLike` interface for framework-agnostic CDP access
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @a11y-oracle/axe-bridge
|
|
3
|
+
*
|
|
4
|
+
* Axe-core result post-processor that resolves "incomplete" rule
|
|
5
|
+
* findings using visual analysis, keyboard interaction, and CDP
|
|
6
|
+
* inspection. Drop-in middleware between axe-core's `analyze()`
|
|
7
|
+
* and your assertion layer.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
export { resolveAllIncomplete } from './lib/resolve-all.js';
|
|
12
|
+
export { resolveIncompleteContrast } from './lib/axe-bridge.js';
|
|
13
|
+
export { resolveIdenticalLinksSamePurpose, normalizeUrl } from './lib/resolvers/identical-links-same-purpose.js';
|
|
14
|
+
export { resolveLinkInTextBlock } from './lib/resolvers/link-in-text-block.js';
|
|
15
|
+
export { resolveTargetSize } from './lib/resolvers/target-size.js';
|
|
16
|
+
export { resolveScrollableRegionFocusable } from './lib/resolvers/scrollable-region-focusable.js';
|
|
17
|
+
export { resolveSkipLink } from './lib/resolvers/skip-link.js';
|
|
18
|
+
export { resolveAriaHiddenFocus } from './lib/resolvers/aria-hidden-focus.js';
|
|
19
|
+
export { resolveFocusIndicator } from './lib/resolvers/focus-indicator.js';
|
|
20
|
+
export { resolveContentOnHover } from './lib/resolvers/content-on-hover.js';
|
|
21
|
+
export { resolveFrameTested } from './lib/resolvers/frame-tested.js';
|
|
22
|
+
export { getContrastThresholds } from './lib/wcag-thresholds.js';
|
|
23
|
+
export { getSelector, cloneResults, ruleShell, findIncompleteRule, applyPromotions, } from './lib/resolver-pipeline.js';
|
|
24
|
+
export type { PromotionResult } from './lib/resolver-pipeline.js';
|
|
25
|
+
export type { ContrastResolutionOptions, ContrastThresholds, WcagLevel, AxeResults, AxeRule, AxeNode, AxeCheck, LinkInTextBlockOptions, TargetSizeOptions, ScrollableRegionOptions, SkipLinkOptions, AriaHiddenFocusOptions, FocusIndicatorOptions, ContentOnHoverOptions, FrameTestedOptions, IncompleteResolutionOptions, } from './lib/types.js';
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG5D,OAAO,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,gCAAgC,EAAE,YAAY,EAAE,MAAM,iDAAiD,CAAC;AACjH,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,gCAAgC,EAAE,MAAM,gDAAgD,CAAC;AAClG,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAGrE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,EACL,WAAW,EACX,YAAY,EACZ,SAAS,EACT,kBAAkB,EAClB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC,YAAY,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAGlE,YAAY,EACV,yBAAyB,EACzB,kBAAkB,EAClB,SAAS,EACT,UAAU,EACV,OAAO,EACP,OAAO,EACP,QAAQ,EACR,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,kBAAkB,EAClB,2BAA2B,GAC5B,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @a11y-oracle/axe-bridge
|
|
3
|
+
*
|
|
4
|
+
* Axe-core result post-processor that resolves "incomplete" rule
|
|
5
|
+
* findings using visual analysis, keyboard interaction, and CDP
|
|
6
|
+
* inspection. Drop-in middleware between axe-core's `analyze()`
|
|
7
|
+
* and your assertion layer.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
// ─── Orchestrator ───────────────────────────────────────────────
|
|
12
|
+
export { resolveAllIncomplete } from './lib/resolve-all.js';
|
|
13
|
+
// ─── Individual resolvers ───────────────────────────────────────
|
|
14
|
+
export { resolveIncompleteContrast } from './lib/axe-bridge.js';
|
|
15
|
+
export { resolveIdenticalLinksSamePurpose, normalizeUrl } from './lib/resolvers/identical-links-same-purpose.js';
|
|
16
|
+
export { resolveLinkInTextBlock } from './lib/resolvers/link-in-text-block.js';
|
|
17
|
+
export { resolveTargetSize } from './lib/resolvers/target-size.js';
|
|
18
|
+
export { resolveScrollableRegionFocusable } from './lib/resolvers/scrollable-region-focusable.js';
|
|
19
|
+
export { resolveSkipLink } from './lib/resolvers/skip-link.js';
|
|
20
|
+
export { resolveAriaHiddenFocus } from './lib/resolvers/aria-hidden-focus.js';
|
|
21
|
+
export { resolveFocusIndicator } from './lib/resolvers/focus-indicator.js';
|
|
22
|
+
export { resolveContentOnHover } from './lib/resolvers/content-on-hover.js';
|
|
23
|
+
export { resolveFrameTested } from './lib/resolvers/frame-tested.js';
|
|
24
|
+
// ─── Threshold helpers ──────────────────────────────────────────
|
|
25
|
+
export { getContrastThresholds } from './lib/wcag-thresholds.js';
|
|
26
|
+
// ─── Pipeline utilities ─────────────────────────────────────────
|
|
27
|
+
export { getSelector, cloneResults, ruleShell, findIncompleteRule, applyPromotions, } from './lib/resolver-pipeline.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module axe-bridge
|
|
3
|
+
*
|
|
4
|
+
* Post-processor for axe-core results that resolves "incomplete"
|
|
5
|
+
* color-contrast warnings using visual pixel analysis and CSS
|
|
6
|
+
* halo heuristics.
|
|
7
|
+
*/
|
|
8
|
+
import type { CDPSessionLike } from '@a11y-oracle/cdp-types';
|
|
9
|
+
import type { AxeResults, ContrastResolutionOptions } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Resolve incomplete color-contrast warnings from axe-core results
|
|
12
|
+
* using visual pixel analysis and CSS halo heuristics.
|
|
13
|
+
*
|
|
14
|
+
* The function deep-clones the input results and returns a modified
|
|
15
|
+
* copy where:
|
|
16
|
+
* - Elements that pass worst-case contrast are promoted to `passes`
|
|
17
|
+
* - Elements that fail best-case contrast are promoted to `violations`
|
|
18
|
+
* - Ambiguous elements (split decision, dynamic content) remain in `incomplete`
|
|
19
|
+
*
|
|
20
|
+
* @param cdp - CDP session connected to the page being tested.
|
|
21
|
+
* @param axeResults - Raw results from axe-core's `analyze()`.
|
|
22
|
+
* @param options - Optional threshold overrides.
|
|
23
|
+
* @returns Modified axe results with resolved contrast findings.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const axeResults = await axe.run(document);
|
|
28
|
+
* const cleaned = await resolveIncompleteContrast(cdp, axeResults);
|
|
29
|
+
* expect(cleaned.violations).toHaveLength(0);
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolveIncompleteContrast(cdp: CDPSessionLike, axeResults: AxeResults, options?: ContrastResolutionOptions): Promise<AxeResults>;
|
|
33
|
+
//# sourceMappingURL=axe-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"axe-bridge.d.ts","sourceRoot":"","sources":["../../src/lib/axe-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAE7D,OAAO,KAAK,EACV,UAAU,EAEV,yBAAyB,EAC1B,MAAM,YAAY,CAAC;AA2CpB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,UAAU,CAAC,CA8DrB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module axe-bridge
|
|
3
|
+
*
|
|
4
|
+
* Post-processor for axe-core results that resolves "incomplete"
|
|
5
|
+
* color-contrast warnings using visual pixel analysis and CSS
|
|
6
|
+
* halo heuristics.
|
|
7
|
+
*/
|
|
8
|
+
import { VisualContrastAnalyzer } from '@a11y-oracle/visual-engine';
|
|
9
|
+
import { getContrastThresholds } from './wcag-thresholds.js';
|
|
10
|
+
import { getSelector, cloneResults, findIncompleteRule, applyPromotions, } from './resolver-pipeline.js';
|
|
11
|
+
/**
|
|
12
|
+
* Determine the effective contrast threshold for an axe node.
|
|
13
|
+
*
|
|
14
|
+
* axe-core stores font metadata in `node.any[0].data` for the
|
|
15
|
+
* color-contrast check. Large text (>= 18pt, or >= 14pt bold)
|
|
16
|
+
* uses the lower 3.0 threshold.
|
|
17
|
+
*/
|
|
18
|
+
function getEffectiveThreshold(node, threshold, largeTextThreshold) {
|
|
19
|
+
try {
|
|
20
|
+
const checkData = node.any?.[0]?.data;
|
|
21
|
+
if (!checkData)
|
|
22
|
+
return threshold;
|
|
23
|
+
const fontSize = parseFloat(String(checkData['fontSize'] ?? '0'));
|
|
24
|
+
const fontWeight = String(checkData['fontWeight'] ?? 'normal');
|
|
25
|
+
const isBold = fontWeight === 'bold' ||
|
|
26
|
+
fontWeight === 'bolder' ||
|
|
27
|
+
parseInt(fontWeight, 10) >= 700;
|
|
28
|
+
// WCAG large text: >= 18pt (24px) or >= 14pt (18.66px) if bold
|
|
29
|
+
if (fontSize >= 24 || (fontSize >= 18.66 && isBold)) {
|
|
30
|
+
return largeTextThreshold;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// If data parsing fails, use the stricter threshold
|
|
35
|
+
}
|
|
36
|
+
return threshold;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve incomplete color-contrast warnings from axe-core results
|
|
40
|
+
* using visual pixel analysis and CSS halo heuristics.
|
|
41
|
+
*
|
|
42
|
+
* The function deep-clones the input results and returns a modified
|
|
43
|
+
* copy where:
|
|
44
|
+
* - Elements that pass worst-case contrast are promoted to `passes`
|
|
45
|
+
* - Elements that fail best-case contrast are promoted to `violations`
|
|
46
|
+
* - Ambiguous elements (split decision, dynamic content) remain in `incomplete`
|
|
47
|
+
*
|
|
48
|
+
* @param cdp - CDP session connected to the page being tested.
|
|
49
|
+
* @param axeResults - Raw results from axe-core's `analyze()`.
|
|
50
|
+
* @param options - Optional threshold overrides.
|
|
51
|
+
* @returns Modified axe results with resolved contrast findings.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const axeResults = await axe.run(document);
|
|
56
|
+
* const cleaned = await resolveIncompleteContrast(cdp, axeResults);
|
|
57
|
+
* expect(cleaned.violations).toHaveLength(0);
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveIncompleteContrast(cdp, axeResults, options) {
|
|
61
|
+
const clone = cloneResults(axeResults);
|
|
62
|
+
// Derive thresholds: explicit values > wcagLevel > default (wcag22aa)
|
|
63
|
+
const levelThresholds = getContrastThresholds(options?.wcagLevel);
|
|
64
|
+
// Level A has no contrast requirement — skip resolution entirely
|
|
65
|
+
if (!levelThresholds && !options?.threshold && !options?.largeTextThreshold) {
|
|
66
|
+
return clone;
|
|
67
|
+
}
|
|
68
|
+
const threshold = options?.threshold ?? levelThresholds?.normalText ?? 4.5;
|
|
69
|
+
const largeTextThreshold = options?.largeTextThreshold ?? levelThresholds?.largeText ?? 3.0;
|
|
70
|
+
// Find the color-contrast rule in incomplete results
|
|
71
|
+
const found = findIncompleteRule(clone, 'color-contrast');
|
|
72
|
+
if (!found)
|
|
73
|
+
return clone;
|
|
74
|
+
const { index: ccIndex, rule: ccRule } = found;
|
|
75
|
+
const analyzer = new VisualContrastAnalyzer(cdp);
|
|
76
|
+
const passNodes = [];
|
|
77
|
+
const violationNodes = [];
|
|
78
|
+
const incompleteNodes = [];
|
|
79
|
+
// Analyze each incomplete node
|
|
80
|
+
for (const node of ccRule.nodes) {
|
|
81
|
+
const selector = getSelector(node);
|
|
82
|
+
if (!selector) {
|
|
83
|
+
incompleteNodes.push(node);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const effectiveThreshold = getEffectiveThreshold(node, threshold, largeTextThreshold);
|
|
87
|
+
const result = await analyzer.analyzeElement(selector, effectiveThreshold);
|
|
88
|
+
switch (result.category) {
|
|
89
|
+
case 'pass':
|
|
90
|
+
passNodes.push(node);
|
|
91
|
+
break;
|
|
92
|
+
case 'violation':
|
|
93
|
+
violationNodes.push(node);
|
|
94
|
+
break;
|
|
95
|
+
case 'incomplete':
|
|
96
|
+
incompleteNodes.push(node);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
applyPromotions(clone, ccIndex, ccRule, {
|
|
101
|
+
passNodes,
|
|
102
|
+
violationNodes,
|
|
103
|
+
incompleteNodes,
|
|
104
|
+
});
|
|
105
|
+
return clone;
|
|
106
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module resolve-all
|
|
3
|
+
*
|
|
4
|
+
* Orchestrator that pipes axe-core results through all available
|
|
5
|
+
* incomplete rule resolvers in sequence.
|
|
6
|
+
*
|
|
7
|
+
* Each resolver receives the output of the previous one, so resolved
|
|
8
|
+
* findings accumulate through the pipeline.
|
|
9
|
+
*/
|
|
10
|
+
import type { CDPSessionLike } from '@a11y-oracle/cdp-types';
|
|
11
|
+
import type { AxeResults, IncompleteResolutionOptions } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Resolve all incomplete axe-core results by running each resolver
|
|
14
|
+
* in sequence.
|
|
15
|
+
*
|
|
16
|
+
* The pipeline processes rules in order:
|
|
17
|
+
* 1. `color-contrast` — visual pixel analysis
|
|
18
|
+
* 2. `identical-links-same-purpose` — URL normalization
|
|
19
|
+
* 3. `link-in-text-block` — default-state style checks
|
|
20
|
+
* 4. `target-size` — bounding box measurements
|
|
21
|
+
* 5. `scrollable-region-focusable` — scroll + focus tests
|
|
22
|
+
* 6. `skip-link` — focus visibility check
|
|
23
|
+
* 7. `aria-hidden-focus` — Tab traversal
|
|
24
|
+
* 8. `focus-indicator` — screenshot diff
|
|
25
|
+
* 9. `content-on-hover` — hover + dismiss tests
|
|
26
|
+
* 10. `frame-tested` — cross-origin iframe injection
|
|
27
|
+
*
|
|
28
|
+
* @param cdp - CDP session connected to the page being tested.
|
|
29
|
+
* @param axeResults - Raw results from axe-core's `analyze()`.
|
|
30
|
+
* @param options - Per-resolver options and `skipRules` filter.
|
|
31
|
+
* @returns Modified axe results with all resolvable findings classified.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
|
|
36
|
+
*
|
|
37
|
+
* const raw = await axe.run(document);
|
|
38
|
+
* const resolved = await resolveAllIncomplete(cdp, raw, {
|
|
39
|
+
* wcagLevel: 'wcag22aa',
|
|
40
|
+
* skipRules: ['frame-tested'], // skip iframe injection
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveAllIncomplete(cdp: CDPSessionLike, axeResults: AxeResults, options?: IncompleteResolutionOptions): Promise<AxeResults>;
|
|
45
|
+
//# sourceMappingURL=resolve-all.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-all.d.ts","sourceRoot":"","sources":["../../src/lib/resolve-all.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,KAAK,EAAE,UAAU,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAyF1E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,cAAc,EACnB,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module resolve-all
|
|
3
|
+
*
|
|
4
|
+
* Orchestrator that pipes axe-core results through all available
|
|
5
|
+
* incomplete rule resolvers in sequence.
|
|
6
|
+
*
|
|
7
|
+
* Each resolver receives the output of the previous one, so resolved
|
|
8
|
+
* findings accumulate through the pipeline.
|
|
9
|
+
*/
|
|
10
|
+
import { resolveIncompleteContrast } from './axe-bridge.js';
|
|
11
|
+
import { resolveIdenticalLinksSamePurpose } from './resolvers/identical-links-same-purpose.js';
|
|
12
|
+
import { resolveLinkInTextBlock } from './resolvers/link-in-text-block.js';
|
|
13
|
+
import { resolveTargetSize } from './resolvers/target-size.js';
|
|
14
|
+
import { resolveScrollableRegionFocusable } from './resolvers/scrollable-region-focusable.js';
|
|
15
|
+
import { resolveSkipLink } from './resolvers/skip-link.js';
|
|
16
|
+
import { resolveAriaHiddenFocus } from './resolvers/aria-hidden-focus.js';
|
|
17
|
+
import { resolveFocusIndicator } from './resolvers/focus-indicator.js';
|
|
18
|
+
import { resolveContentOnHover } from './resolvers/content-on-hover.js';
|
|
19
|
+
import { resolveFrameTested } from './resolvers/frame-tested.js';
|
|
20
|
+
/**
|
|
21
|
+
* Ordered pipeline of resolvers. Each entry maps a rule ID to
|
|
22
|
+
* its resolver function. The order is chosen to run simpler
|
|
23
|
+
* resolvers first (pure DOM queries) and more complex ones
|
|
24
|
+
* (screenshots, keyboard traversal) later.
|
|
25
|
+
*/
|
|
26
|
+
const RESOLVER_PIPELINE = [
|
|
27
|
+
{
|
|
28
|
+
ruleId: 'color-contrast',
|
|
29
|
+
invoke: (cdp, results, opts) => resolveIncompleteContrast(cdp, results, {
|
|
30
|
+
wcagLevel: opts.wcagLevel,
|
|
31
|
+
...opts.contrast,
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
ruleId: 'identical-links-same-purpose',
|
|
36
|
+
invoke: (cdp, results) => resolveIdenticalLinksSamePurpose(cdp, results),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
ruleId: 'link-in-text-block',
|
|
40
|
+
invoke: (cdp, results, opts) => resolveLinkInTextBlock(cdp, results, opts.linkInTextBlock),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
ruleId: 'target-size',
|
|
44
|
+
invoke: (cdp, results, opts) => resolveTargetSize(cdp, results, opts.targetSize),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
ruleId: 'scrollable-region-focusable',
|
|
48
|
+
invoke: (cdp, results, opts) => resolveScrollableRegionFocusable(cdp, results, opts.scrollableRegion),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
ruleId: 'skip-link',
|
|
52
|
+
invoke: (cdp, results, opts) => resolveSkipLink(cdp, results, opts.skipLink),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
ruleId: 'aria-hidden-focus',
|
|
56
|
+
invoke: (cdp, results, opts) => resolveAriaHiddenFocus(cdp, results, opts.ariaHiddenFocus),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
ruleId: 'focus-indicator',
|
|
60
|
+
invoke: (cdp, results, opts) => resolveFocusIndicator(cdp, results, opts.focusIndicator),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
ruleId: 'content-on-hover',
|
|
64
|
+
invoke: (cdp, results, opts) => resolveContentOnHover(cdp, results, opts.contentOnHover),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
ruleId: 'frame-tested',
|
|
68
|
+
invoke: (cdp, results, opts) => resolveFrameTested(cdp, results, opts.frameTested),
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* Resolve all incomplete axe-core results by running each resolver
|
|
73
|
+
* in sequence.
|
|
74
|
+
*
|
|
75
|
+
* The pipeline processes rules in order:
|
|
76
|
+
* 1. `color-contrast` — visual pixel analysis
|
|
77
|
+
* 2. `identical-links-same-purpose` — URL normalization
|
|
78
|
+
* 3. `link-in-text-block` — default-state style checks
|
|
79
|
+
* 4. `target-size` — bounding box measurements
|
|
80
|
+
* 5. `scrollable-region-focusable` — scroll + focus tests
|
|
81
|
+
* 6. `skip-link` — focus visibility check
|
|
82
|
+
* 7. `aria-hidden-focus` — Tab traversal
|
|
83
|
+
* 8. `focus-indicator` — screenshot diff
|
|
84
|
+
* 9. `content-on-hover` — hover + dismiss tests
|
|
85
|
+
* 10. `frame-tested` — cross-origin iframe injection
|
|
86
|
+
*
|
|
87
|
+
* @param cdp - CDP session connected to the page being tested.
|
|
88
|
+
* @param axeResults - Raw results from axe-core's `analyze()`.
|
|
89
|
+
* @param options - Per-resolver options and `skipRules` filter.
|
|
90
|
+
* @returns Modified axe results with all resolvable findings classified.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* import { resolveAllIncomplete } from '@a11y-oracle/axe-bridge';
|
|
95
|
+
*
|
|
96
|
+
* const raw = await axe.run(document);
|
|
97
|
+
* const resolved = await resolveAllIncomplete(cdp, raw, {
|
|
98
|
+
* wcagLevel: 'wcag22aa',
|
|
99
|
+
* skipRules: ['frame-tested'], // skip iframe injection
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export async function resolveAllIncomplete(cdp, axeResults, options = {}) {
|
|
104
|
+
const skipSet = new Set(options.skipRules ?? []);
|
|
105
|
+
let current = axeResults;
|
|
106
|
+
for (const entry of RESOLVER_PIPELINE) {
|
|
107
|
+
// Skip if rule is in skipRules
|
|
108
|
+
if (skipSet.has(entry.ruleId))
|
|
109
|
+
continue;
|
|
110
|
+
// Skip if no incomplete entries exist for this rule
|
|
111
|
+
const hasRule = current.incomplete.some((r) => r.id === entry.ruleId);
|
|
112
|
+
if (!hasRule)
|
|
113
|
+
continue;
|
|
114
|
+
current = await entry.invoke(cdp, current, options);
|
|
115
|
+
}
|
|
116
|
+
return current;
|
|
117
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module resolver-pipeline
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for axe-core incomplete rule resolvers.
|
|
5
|
+
* Provides the common clone → find-rule → analyze → promote → return
|
|
6
|
+
* pipeline that every resolver follows.
|
|
7
|
+
*/
|
|
8
|
+
import type { AxeResults, AxeRule, AxeNode } from './types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Extract the innermost CSS selector from an axe node's target.
|
|
11
|
+
*
|
|
12
|
+
* axe-core represents selectors as arrays where shadow DOM targets
|
|
13
|
+
* have multiple entries. We use the last (innermost) selector.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getSelector(node: AxeNode): string;
|
|
16
|
+
/**
|
|
17
|
+
* Deep-clone an AxeResults object to avoid mutating the original.
|
|
18
|
+
*/
|
|
19
|
+
export declare function cloneResults(results: AxeResults): AxeResults;
|
|
20
|
+
/**
|
|
21
|
+
* Create a rule shell (all metadata, no nodes) from an existing rule.
|
|
22
|
+
*/
|
|
23
|
+
export declare function ruleShell(rule: AxeRule): AxeRule;
|
|
24
|
+
/** Categorized node results from a resolver's analysis phase. */
|
|
25
|
+
export interface PromotionResult {
|
|
26
|
+
passNodes: AxeNode[];
|
|
27
|
+
violationNodes: AxeNode[];
|
|
28
|
+
incompleteNodes: AxeNode[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Apply node promotions to a cloned AxeResults object.
|
|
32
|
+
*
|
|
33
|
+
* Moves nodes from the incomplete rule entry into the passes and
|
|
34
|
+
* violations arrays, then updates or removes the incomplete entry.
|
|
35
|
+
*
|
|
36
|
+
* @param clone - The cloned AxeResults to mutate.
|
|
37
|
+
* @param ruleIndex - Index of the rule in `clone.incomplete`.
|
|
38
|
+
* @param rule - The incomplete rule being resolved.
|
|
39
|
+
* @param result - Categorized nodes from the resolver's analysis.
|
|
40
|
+
*/
|
|
41
|
+
export declare function applyPromotions(clone: AxeResults, ruleIndex: number, rule: AxeRule, result: PromotionResult): void;
|
|
42
|
+
/**
|
|
43
|
+
* Find a rule by ID in the incomplete array.
|
|
44
|
+
*
|
|
45
|
+
* @returns The rule index and rule object, or null if not found.
|
|
46
|
+
*/
|
|
47
|
+
export declare function findIncompleteRule(clone: AxeResults, ruleId: string): {
|
|
48
|
+
index: number;
|
|
49
|
+
rule: AxeRule;
|
|
50
|
+
} | null;
|
|
51
|
+
//# sourceMappingURL=resolver-pipeline.d.ts.map
|