@burdenoff/fe-libs 2026.823.2 → 2026.825.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/dist/shared/hooks/index.d.ts +4 -0
- package/dist/shared/hooks/index.d.ts.map +1 -1
- package/dist/shared/hooks/usePushDeviceRegistration.d.ts +10 -0
- package/dist/shared/hooks/usePushDeviceRegistration.d.ts.map +1 -0
- package/dist/shared/hooks/usePushDeviceRegistration.js +54 -0
- package/dist/shared/native/push-registration.d.ts +124 -0
- package/dist/shared/native/push-registration.d.ts.map +1 -0
- package/dist/shared/native/push-registration.js +120 -0
- package/dist/shared/native/types.d.ts +11 -1
- package/dist/shared/native/types.d.ts.map +1 -1
- package/dist/shared-hooks.js +10 -8
- package/dist/shared.js +23 -21
- package/dist/shell-native/index.d.ts +3 -2
- package/dist/shell-native/index.d.ts.map +1 -1
- package/dist/shell-native/push.d.ts +10 -0
- package/dist/shell-native/push.d.ts.map +1 -1
- package/dist/shell-native/push.js +27 -22
- package/dist/shell-native/service-worker.d.ts +9 -1
- package/dist/shell-native/service-worker.d.ts.map +1 -1
- package/dist/shell-native/service-worker.js +102 -1
- package/dist/shell-native/types.d.ts +21 -9
- package/dist/shell-native/types.d.ts.map +1 -1
- package/dist/shell-native/web-manifest.d.ts +21 -0
- package/dist/shell-native/web-manifest.d.ts.map +1 -1
- package/dist/shell-native/web-manifest.js +4 -1
- package/dist/shell-native/web-push.d.ts +74 -0
- package/dist/shell-native/web-push.d.ts.map +1 -0
- package/dist/shell-native/web-push.js +106 -0
- package/dist/shell-native.js +10 -9
- package/package.json +6 -3
- package/scripts/a11y-audit/README.md +172 -0
- package/scripts/a11y-audit/allowlist.ts +178 -0
- package/scripts/a11y-audit/audit.ts +306 -0
- package/scripts/a11y-audit/cli.ts +161 -0
- package/scripts/a11y-audit/config.ts +319 -0
- package/scripts/a11y-audit/index.ts +19 -0
- package/scripts/a11y-audit/report.ts +144 -0
- package/scripts/a11y-audit/server.ts +237 -0
- package/scripts/a11y-audit/types.ts +234 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Getting a running app in front of the browser.
|
|
3
|
+
*
|
|
4
|
+
* Three modes, mirroring how the fleet ships frontends:
|
|
5
|
+
* staticDir — app shells and websites, which build to a servable `dist/`
|
|
6
|
+
* command — microfrontends, which build to a library and need their Vite
|
|
7
|
+
* harness (`bun run dev`) to render anything
|
|
8
|
+
* baseUrl — an already-running target (a preview deploy, alpha, prod)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
12
|
+
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
|
|
13
|
+
import { createServer, type Server } from 'node:http';
|
|
14
|
+
import { extname, join, normalize, resolve, sep } from 'node:path';
|
|
15
|
+
import type { ResolvedConfig } from './types';
|
|
16
|
+
|
|
17
|
+
const MIME_TYPES: Record<string, string> = {
|
|
18
|
+
'.html': 'text/html; charset=utf-8',
|
|
19
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
20
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
21
|
+
'.css': 'text/css; charset=utf-8',
|
|
22
|
+
'.json': 'application/json; charset=utf-8',
|
|
23
|
+
'.svg': 'image/svg+xml',
|
|
24
|
+
'.png': 'image/png',
|
|
25
|
+
'.jpg': 'image/jpeg',
|
|
26
|
+
'.jpeg': 'image/jpeg',
|
|
27
|
+
'.gif': 'image/gif',
|
|
28
|
+
'.webp': 'image/webp',
|
|
29
|
+
'.avif': 'image/avif',
|
|
30
|
+
'.ico': 'image/x-icon',
|
|
31
|
+
'.woff': 'font/woff',
|
|
32
|
+
'.woff2': 'font/woff2',
|
|
33
|
+
'.ttf': 'font/ttf',
|
|
34
|
+
'.otf': 'font/otf',
|
|
35
|
+
'.map': 'application/json; charset=utf-8',
|
|
36
|
+
'.wasm': 'application/wasm',
|
|
37
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
38
|
+
'.webmanifest': 'application/manifest+json',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** A file resolved out of the built `dist/` directory. */
|
|
42
|
+
export interface ResolvedAsset {
|
|
43
|
+
body: Buffer;
|
|
44
|
+
contentType: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RunningApp {
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
close: () => Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* Present in `staticDir` mode: resolves a URL path straight out of the build
|
|
52
|
+
* directory. The audit fulfils the page's own requests through this instead
|
|
53
|
+
* of letting Chromium open a socket per chunk, which keeps a code-split shell
|
|
54
|
+
* from losing a hundred parallel chunk loads to one spurious
|
|
55
|
+
* ERR_NETWORK_CHANGED and rendering blank.
|
|
56
|
+
*/
|
|
57
|
+
resolveAsset?: (urlPath: string) => ResolvedAsset | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolveWithinRoot(root: string, urlPath: string): string | null {
|
|
61
|
+
let decoded: string;
|
|
62
|
+
try {
|
|
63
|
+
decoded = decodeURIComponent(urlPath);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const candidate = resolve(root, `.${normalize(decoded)}`);
|
|
68
|
+
if (candidate !== root && !candidate.startsWith(root + sep)) return null;
|
|
69
|
+
return candidate;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Map a URL path onto a file inside the build directory.
|
|
74
|
+
*
|
|
75
|
+
* `null` means "outside the root" (403) and `undefined` means "no such file"
|
|
76
|
+
* (404). Both the HTTP server and the browser-side interception use this, so
|
|
77
|
+
* the two can never disagree about what the build contains.
|
|
78
|
+
*/
|
|
79
|
+
function resolveBuildFile(
|
|
80
|
+
rootDir: string,
|
|
81
|
+
indexPath: string,
|
|
82
|
+
spa: boolean,
|
|
83
|
+
urlPath: string
|
|
84
|
+
): string | null | undefined {
|
|
85
|
+
let filePath = resolveWithinRoot(rootDir, urlPath);
|
|
86
|
+
if (filePath === null) return null;
|
|
87
|
+
|
|
88
|
+
if (existsSync(filePath) && statSync(filePath).isDirectory()) {
|
|
89
|
+
filePath = join(filePath, 'index.html');
|
|
90
|
+
}
|
|
91
|
+
if (existsSync(filePath) && statSync(filePath).isFile()) return filePath;
|
|
92
|
+
// Client-routed URLs get the shell. A missing *asset* stays a 404 — a broken
|
|
93
|
+
// bundle must not be laundered into a successful page load.
|
|
94
|
+
if (spa && extname(urlPath) === '') return indexPath;
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Serve a built `dist/` over loopback, with optional SPA fallback. */
|
|
99
|
+
export function startStaticServer(root: string, spa: boolean): Promise<RunningApp> {
|
|
100
|
+
const rootDir = resolve(root);
|
|
101
|
+
if (!existsSync(rootDir)) {
|
|
102
|
+
return Promise.reject(
|
|
103
|
+
new Error(
|
|
104
|
+
`staticDir "${root}" does not exist — build the app before running the audit ` +
|
|
105
|
+
'(e.g. `bun run build`)'
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const indexPath = join(rootDir, 'index.html');
|
|
110
|
+
if (!existsSync(indexPath)) {
|
|
111
|
+
return Promise.reject(new Error(`staticDir "${root}" has no index.html`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const contentTypeFor = (filePath: string): string =>
|
|
115
|
+
MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
116
|
+
|
|
117
|
+
const resolveAsset = (urlPath: string): ResolvedAsset | null => {
|
|
118
|
+
const filePath = resolveBuildFile(rootDir, indexPath, spa, urlPath);
|
|
119
|
+
if (filePath === null || filePath === undefined) return null;
|
|
120
|
+
return { body: readFileSync(filePath), contentType: contentTypeFor(filePath) };
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const server: Server = createServer((req, res) => {
|
|
124
|
+
const urlPath = (req.url ?? '/').split('?')[0].split('#')[0];
|
|
125
|
+
const filePath = resolveBuildFile(rootDir, indexPath, spa, urlPath);
|
|
126
|
+
|
|
127
|
+
if (filePath === null) {
|
|
128
|
+
res.writeHead(403).end('Forbidden');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (filePath === undefined) {
|
|
132
|
+
res.writeHead(404, { 'content-type': 'text/plain' }).end('Not found');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
res.writeHead(200, {
|
|
137
|
+
'content-type': contentTypeFor(filePath),
|
|
138
|
+
'cache-control': 'no-store',
|
|
139
|
+
});
|
|
140
|
+
createReadStream(filePath).pipe(res);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return new Promise<RunningApp>((resolvePromise, rejectPromise) => {
|
|
144
|
+
server.on('error', rejectPromise);
|
|
145
|
+
server.listen(0, '127.0.0.1', () => {
|
|
146
|
+
const address = server.address();
|
|
147
|
+
if (address === null || typeof address === 'string') {
|
|
148
|
+
rejectPromise(new Error('static server did not bind to a TCP port'));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
resolvePromise({
|
|
152
|
+
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
153
|
+
resolveAsset,
|
|
154
|
+
close: () =>
|
|
155
|
+
new Promise<void>((done) => {
|
|
156
|
+
server.close(() => done());
|
|
157
|
+
server.closeAllConnections?.();
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function waitForUrl(url: string, timeoutMs: number, child?: ChildProcess): Promise<void> {
|
|
165
|
+
const deadline = Date.now() + timeoutMs;
|
|
166
|
+
let lastError = 'no response';
|
|
167
|
+
while (Date.now() < deadline) {
|
|
168
|
+
if (child && child.exitCode !== null) {
|
|
169
|
+
throw new Error(`server command exited with code ${child.exitCode} before serving ${url}`);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const response = await fetch(url, { redirect: 'manual' });
|
|
173
|
+
if (response.status < 500) return;
|
|
174
|
+
lastError = `HTTP ${response.status}`;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
lastError = (error as Error).message;
|
|
177
|
+
}
|
|
178
|
+
await new Promise((done) => setTimeout(done, 500));
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`timed out after ${timeoutMs}ms waiting for ${url} (last error: ${lastError})`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Start a server via a shell command and wait until its port answers. */
|
|
184
|
+
export async function startCommandServer(
|
|
185
|
+
command: string,
|
|
186
|
+
port: number,
|
|
187
|
+
readyTimeoutMs: number
|
|
188
|
+
): Promise<RunningApp> {
|
|
189
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
190
|
+
const child = spawn(command, {
|
|
191
|
+
shell: true,
|
|
192
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
193
|
+
detached: true,
|
|
194
|
+
env: { ...process.env, PORT: String(port) },
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const close = async (): Promise<void> => {
|
|
198
|
+
if (child.pid === undefined || child.exitCode !== null) return;
|
|
199
|
+
try {
|
|
200
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
201
|
+
} catch {
|
|
202
|
+
child.kill('SIGTERM');
|
|
203
|
+
}
|
|
204
|
+
await new Promise((done) => setTimeout(done, 500));
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await waitForUrl(baseUrl, readyTimeoutMs, child);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
await close();
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
return { baseUrl, close };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Resolve the configured serve mode into a running app. */
|
|
217
|
+
export async function startApp(
|
|
218
|
+
config: ResolvedConfig,
|
|
219
|
+
baseUrlOverride?: string
|
|
220
|
+
): Promise<RunningApp> {
|
|
221
|
+
if (baseUrlOverride) {
|
|
222
|
+
await waitForUrl(baseUrlOverride, config.serve.readyTimeoutMs);
|
|
223
|
+
return { baseUrl: baseUrlOverride.replace(/\/$/, ''), close: async () => {} };
|
|
224
|
+
}
|
|
225
|
+
if (config.serve.baseUrl) {
|
|
226
|
+
await waitForUrl(config.serve.baseUrl, config.serve.readyTimeoutMs);
|
|
227
|
+
return { baseUrl: config.serve.baseUrl.replace(/\/$/, ''), close: async () => {} };
|
|
228
|
+
}
|
|
229
|
+
if (config.serve.staticDir) {
|
|
230
|
+
return startStaticServer(config.serve.staticDir, config.serve.spa);
|
|
231
|
+
}
|
|
232
|
+
return startCommandServer(
|
|
233
|
+
config.serve.command as string,
|
|
234
|
+
config.serve.port as number,
|
|
235
|
+
config.serve.readyTimeoutMs
|
|
236
|
+
);
|
|
237
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type definitions for the fleet accessibility gate.
|
|
3
|
+
*
|
|
4
|
+
* See README.md in this directory for the config shape and worked examples.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** axe-core impact levels, ordered least to most severe. */
|
|
8
|
+
export const IMPACT_LEVELS = ['minor', 'moderate', 'serious', 'critical'] as const;
|
|
9
|
+
|
|
10
|
+
export type ImpactLevel = (typeof IMPACT_LEVELS)[number];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* One explicitly-documented exception. Every entry MUST carry a `reason`;
|
|
14
|
+
* there is deliberately no way to disable a rule globally, no `enabled: false`
|
|
15
|
+
* escape hatch and no wildcard rule id. Narrowing an entry to specific routes
|
|
16
|
+
* and/or specific DOM subtrees is optional but strongly encouraged — an entry
|
|
17
|
+
* with neither is a fleet-wide exemption for that one rule and reads that way
|
|
18
|
+
* in the report.
|
|
19
|
+
*/
|
|
20
|
+
export interface AllowlistEntry {
|
|
21
|
+
/** axe rule id, e.g. `color-contrast`. Wildcards are rejected. */
|
|
22
|
+
rule: string;
|
|
23
|
+
/** Why this violation is tolerated. Free prose, minimum 20 characters. */
|
|
24
|
+
reason: string;
|
|
25
|
+
/**
|
|
26
|
+
* Routes this entry applies to. Each is either an exact configured route
|
|
27
|
+
* path or a `/prefix/*` subtree. Omit to apply to every audited route.
|
|
28
|
+
*/
|
|
29
|
+
routes?: string[];
|
|
30
|
+
/**
|
|
31
|
+
* CSS selectors this entry applies to. A node matches when the selector is
|
|
32
|
+
* the node's own axe target or an ancestor prefix of it. Omit to apply to
|
|
33
|
+
* every node the rule flags.
|
|
34
|
+
*/
|
|
35
|
+
selectors?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* ISO `YYYY-MM-DD` date after which the entry stops suppressing anything and
|
|
38
|
+
* the violations it covered start failing the build again. Optional, but the
|
|
39
|
+
* only thing that keeps an allowlist from becoming permanent.
|
|
40
|
+
*/
|
|
41
|
+
expires?: string;
|
|
42
|
+
/** Linear ticket tracking the fix, e.g. `BOFF-1234`. */
|
|
43
|
+
ticket?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One page to audit. */
|
|
47
|
+
export interface RouteSpec {
|
|
48
|
+
/** Path appended to the base URL, e.g. `/settings/profile`. */
|
|
49
|
+
path: string;
|
|
50
|
+
/** Human label used in reports. Defaults to `path`. */
|
|
51
|
+
name?: string;
|
|
52
|
+
/** Selector that must be present before axe runs. */
|
|
53
|
+
waitForSelector?: string;
|
|
54
|
+
/** Settle time in ms for this route, replacing the global `settleMs`. */
|
|
55
|
+
settleMs?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Minimum rendered text length for THIS route, overriding the global
|
|
58
|
+
* `minTextLength`. Raise it on a heavy page whose load has a visible plateau
|
|
59
|
+
* — a splash or skeleton can hold steady long enough to look like a settled
|
|
60
|
+
* DOM, and auditing that frame measures the skeleton, not the page.
|
|
61
|
+
*/
|
|
62
|
+
minTextLength?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Treat a redirect away from `path` as expected. When false (the default) a
|
|
65
|
+
* redirect is still audited, but the report records the final URL so a
|
|
66
|
+
* "clean" result on an unexpected error page cannot be mistaken for a pass.
|
|
67
|
+
*/
|
|
68
|
+
allowRedirect?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ViewportSpec {
|
|
72
|
+
name: string;
|
|
73
|
+
width: number;
|
|
74
|
+
height: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* How the audit gets a running app. Exactly one of `staticDir`, `command` or
|
|
79
|
+
* `baseUrl` must be set.
|
|
80
|
+
*/
|
|
81
|
+
export interface ServeConfig {
|
|
82
|
+
/** Directory of built static assets to serve, relative to the repo root. */
|
|
83
|
+
staticDir?: string;
|
|
84
|
+
/** Serve `index.html` for unknown extension-less paths. Client-routed apps need this. */
|
|
85
|
+
spa?: boolean;
|
|
86
|
+
/** Command that starts a server, e.g. `bun run preview --port 4180`. */
|
|
87
|
+
command?: string;
|
|
88
|
+
/** Port the command listens on. Required with `command`. */
|
|
89
|
+
port?: number;
|
|
90
|
+
/** Audit an already-running server instead of starting one. */
|
|
91
|
+
baseUrl?: string;
|
|
92
|
+
/** How long to wait for `command` to answer, ms. Default 60000. */
|
|
93
|
+
readyTimeoutMs?: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface A11yAuditConfig {
|
|
97
|
+
/** Repo/app name, used in report headings. */
|
|
98
|
+
name: string;
|
|
99
|
+
serve: ServeConfig;
|
|
100
|
+
routes: RouteSpec[];
|
|
101
|
+
/** Defaults to a single 1280x800 `desktop` viewport. */
|
|
102
|
+
viewports?: ViewportSpec[];
|
|
103
|
+
/** axe tag set. Defaults to the WCAG 2.1 AA tags. */
|
|
104
|
+
standard?: string[];
|
|
105
|
+
/** Impacts that fail the build. Defaults to `['critical', 'serious']`. */
|
|
106
|
+
failOn?: ImpactLevel[];
|
|
107
|
+
allowlist?: AllowlistEntry[];
|
|
108
|
+
/** Where the JSON report is written. Defaults to `a11y-report.json`. */
|
|
109
|
+
reportPath?: string;
|
|
110
|
+
/** Fail when an allowlist entry matched nothing. Defaults to false (warn only). */
|
|
111
|
+
failOnStaleAllowlist?: boolean;
|
|
112
|
+
/** Per-navigation timeout in ms. Defaults to 45000. */
|
|
113
|
+
navigationTimeoutMs?: number;
|
|
114
|
+
/** Settle time in ms applied to every route after load. Defaults to 1500. */
|
|
115
|
+
settleMs?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Abort every request that leaves the app's own origin. Defaults to true.
|
|
118
|
+
*
|
|
119
|
+
* A build-time gate must produce the same DOM on a laptop, a CI runner and a
|
|
120
|
+
* re-run three weeks later. Letting the built bundle talk to a live gateway
|
|
121
|
+
* makes axe race backend latency: the same route audits a splash screen on
|
|
122
|
+
* one run and a rendered page on the next. Blocking egress pins the app to
|
|
123
|
+
* one deterministic state. Set false only when auditing a live deployment
|
|
124
|
+
* via `--base-url`, where the backend is part of what you are auditing.
|
|
125
|
+
*/
|
|
126
|
+
blockExternalRequests?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* How long the DOM must stop changing before axe runs, in ms. Defaults to
|
|
129
|
+
* 1500.
|
|
130
|
+
*
|
|
131
|
+
* Single-page shells paint a splash, then hydrate, then swap in the real
|
|
132
|
+
* page. A fixed sleep audits whichever frame it happens to land on, which is
|
|
133
|
+
* how a gate ends up reporting a clean splash screen as a pass.
|
|
134
|
+
*/
|
|
135
|
+
domQuietMs?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Minimum rendered `body.innerText` length for a route to count as audited.
|
|
138
|
+
* Defaults to 100 characters.
|
|
139
|
+
*
|
|
140
|
+
* Below this the page never rendered, and axe finding nothing means nothing.
|
|
141
|
+
* The run fails as an incomplete audit rather than reporting a false pass.
|
|
142
|
+
*/
|
|
143
|
+
minTextLength?: number;
|
|
144
|
+
/**
|
|
145
|
+
* How many times a route may be re-navigated on a fresh page before the run
|
|
146
|
+
* is declared an incomplete audit. Defaults to 3.
|
|
147
|
+
*
|
|
148
|
+
* Chromium in a VM/container periodically fires a spurious network-change
|
|
149
|
+
* notification that cancels every in-flight request; on a code-split shell
|
|
150
|
+
* that lands as a blank page. The content floor catches it, and a retry on a
|
|
151
|
+
* clean page recovers it. A route that never renders across all attempts is
|
|
152
|
+
* still a hard failure — this retries a flake, it does not tolerate one.
|
|
153
|
+
*/
|
|
154
|
+
routeAttempts?: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Config with every default resolved. */
|
|
158
|
+
export interface ResolvedConfig {
|
|
159
|
+
name: string;
|
|
160
|
+
serve: Required<Pick<ServeConfig, 'spa' | 'readyTimeoutMs'>> & ServeConfig;
|
|
161
|
+
routes: Array<Required<Pick<RouteSpec, 'path' | 'name' | 'allowRedirect'>> & RouteSpec>;
|
|
162
|
+
viewports: ViewportSpec[];
|
|
163
|
+
standard: string[];
|
|
164
|
+
failOn: ImpactLevel[];
|
|
165
|
+
allowlist: AllowlistEntry[];
|
|
166
|
+
reportPath: string;
|
|
167
|
+
failOnStaleAllowlist: boolean;
|
|
168
|
+
navigationTimeoutMs: number;
|
|
169
|
+
settleMs: number;
|
|
170
|
+
blockExternalRequests: boolean;
|
|
171
|
+
domQuietMs: number;
|
|
172
|
+
minTextLength: number;
|
|
173
|
+
routeAttempts: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** A single failing DOM node, flattened out of an axe violation. */
|
|
177
|
+
export interface FlatNode {
|
|
178
|
+
rule: string;
|
|
179
|
+
impact: ImpactLevel;
|
|
180
|
+
help: string;
|
|
181
|
+
helpUrl: string;
|
|
182
|
+
tags: string[];
|
|
183
|
+
route: string;
|
|
184
|
+
routeName: string;
|
|
185
|
+
viewport: string;
|
|
186
|
+
target: string[];
|
|
187
|
+
html: string;
|
|
188
|
+
failureSummary: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface AllowedNode extends FlatNode {
|
|
192
|
+
allowlistIndex: number;
|
|
193
|
+
allowlistReason: string;
|
|
194
|
+
allowlistTicket?: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface RouteResult {
|
|
198
|
+
route: string;
|
|
199
|
+
routeName: string;
|
|
200
|
+
viewport: string;
|
|
201
|
+
/** URL the browser actually ended on — records redirects. */
|
|
202
|
+
finalUrl: string;
|
|
203
|
+
blocking: FlatNode[];
|
|
204
|
+
allowed: AllowedNode[];
|
|
205
|
+
advisory: FlatNode[];
|
|
206
|
+
/** Rules that ran clean, for the "we actually looked" evidence trail. */
|
|
207
|
+
passCount: number;
|
|
208
|
+
incompleteCount: number;
|
|
209
|
+
/** Rendered text length at the moment axe ran — evidence the page was real. */
|
|
210
|
+
renderedTextLength: number;
|
|
211
|
+
/** Whether the DOM went quiet before the navigation timeout. */
|
|
212
|
+
domSettled: boolean;
|
|
213
|
+
/** Navigation attempts this route needed. >1 means the runner hit a flake. */
|
|
214
|
+
attempts: number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface AuditReport {
|
|
218
|
+
name: string;
|
|
219
|
+
generatedAt: string;
|
|
220
|
+
baseUrl: string;
|
|
221
|
+
standard: string[];
|
|
222
|
+
failOn: ImpactLevel[];
|
|
223
|
+
axeVersion: string;
|
|
224
|
+
blockExternalRequests: boolean;
|
|
225
|
+
routes: RouteResult[];
|
|
226
|
+
staleAllowlistEntries: Array<{ index: number; rule: string; reason: string }>;
|
|
227
|
+
expiredAllowlistEntries: Array<{ index: number; rule: string; expires: string }>;
|
|
228
|
+
totals: {
|
|
229
|
+
blocking: number;
|
|
230
|
+
allowed: number;
|
|
231
|
+
advisory: number;
|
|
232
|
+
routesAudited: number;
|
|
233
|
+
};
|
|
234
|
+
}
|