@wcag-checkr/ci 1.0.0-rc.140 → 1.0.0-rc.141
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/manifest.json +2 -2
- package/package.json +1 -1
- package/wcagcheckr-ci.mjs +104 -13
package/dist/manifest.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "wcagcheckr",
|
|
4
4
|
"description": "Audit components across hover, focus, dark mode, forced colors, RTL — every state your users actually encounter. Per-component baselines surface only NEW violations.",
|
|
5
|
-
"version": "1.0.0.
|
|
6
|
-
"version_name": "1.0.0-rc.
|
|
5
|
+
"version": "1.0.0.141",
|
|
6
|
+
"version_name": "1.0.0-rc.141",
|
|
7
7
|
"author": "Locustware",
|
|
8
8
|
"homepage_url": "https://wcagcheckr.com",
|
|
9
9
|
"icons": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wcag-checkr/ci",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.141",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Headless wcagcheckr accessibility audit runner for CI/CD pipelines. Drives the wcagcheckr Chrome extension via Playwright, runs full-page audits across the state matrix (108 combinations: hover, focus, dark mode, RTL, breakpoints), outputs JSON / SARIF / JUnit, exits with severity-aware codes.",
|
|
6
6
|
"license": "UNLICENSED",
|
package/wcagcheckr-ci.mjs
CHANGED
|
@@ -76,6 +76,9 @@ function parseArgs(argv) {
|
|
|
76
76
|
else if (a === '--quiet') args.quiet = true;
|
|
77
77
|
else if (a === '--license') args.license = argv[++i];
|
|
78
78
|
else if (a === '--public-key-url') args.publicKeyUrl = argv[++i];
|
|
79
|
+
else if (a === '--watch') args.watch = true;
|
|
80
|
+
else if (a === '--watch-dir') args.watchDir = argv[++i];
|
|
81
|
+
else if (a === '--watch-debounce') args.watchDebounce = parseInt(argv[++i], 10);
|
|
79
82
|
else if (a === '--help' || a === '-h') args.help = true;
|
|
80
83
|
}
|
|
81
84
|
return args;
|
|
@@ -97,6 +100,12 @@ audit options:
|
|
|
97
100
|
--timeout <ms> Audit timeout (default: 120000)
|
|
98
101
|
--license <token> Activate this license token before auditing
|
|
99
102
|
(gates paid features like forensic anchoring)
|
|
103
|
+
--watch Stay open after the initial audit and re-run
|
|
104
|
+
whenever a file under --watch-dir changes
|
|
105
|
+
--watch-dir <path> Directory to watch when --watch is on
|
|
106
|
+
(default: current working directory)
|
|
107
|
+
--watch-debounce <ms> Debounce window after a change before
|
|
108
|
+
re-auditing (default: 1500)
|
|
100
109
|
--quiet Suppress progress output
|
|
101
110
|
|
|
102
111
|
verify options:
|
|
@@ -162,6 +171,9 @@ if (!['none', 'critical', 'serious', 'moderate', 'minor'].includes(args.threshol
|
|
|
162
171
|
|
|
163
172
|
log(`Loading extension from ${EXT_DIR}`);
|
|
164
173
|
log(`Auditing ${args.url} (threshold: ${args.threshold}, format: ${args.format})`);
|
|
174
|
+
if (args.watch) {
|
|
175
|
+
log(`watch mode ON — re-audits on file changes under ${args.watchDir ?? process.cwd()}`);
|
|
176
|
+
}
|
|
165
177
|
|
|
166
178
|
// Chrome extensions cannot load in legacy `headless: true` mode, so we
|
|
167
179
|
// pass `--headless=new` as a Chromium arg (the modern headless mode that
|
|
@@ -179,8 +191,10 @@ const context = await chromium.launchPersistentContext('', {
|
|
|
179
191
|
});
|
|
180
192
|
|
|
181
193
|
let exitCode = 0;
|
|
194
|
+
let targetPage;
|
|
195
|
+
let sidePanel;
|
|
182
196
|
try {
|
|
183
|
-
|
|
197
|
+
[targetPage] = context.pages();
|
|
184
198
|
await targetPage.goto(args.url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
185
199
|
log('target page loaded');
|
|
186
200
|
|
|
@@ -193,7 +207,7 @@ try {
|
|
|
193
207
|
if (!extId) throw new Error('extension SW did not register within 10s');
|
|
194
208
|
log(`extension loaded (id: ${extId.slice(0, 8)}…)`);
|
|
195
209
|
|
|
196
|
-
|
|
210
|
+
sidePanel = await context.newPage();
|
|
197
211
|
await sidePanel.goto(`chrome-extension://${extId}/side-panel/side-panel.html`);
|
|
198
212
|
|
|
199
213
|
// Headless persona — pick dev mode if wizard appears.
|
|
@@ -255,21 +269,43 @@ try {
|
|
|
255
269
|
break;
|
|
256
270
|
}
|
|
257
271
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
272
|
+
// The per-audit work: navigate target (re-load on subsequent runs to
|
|
273
|
+
// pick up code changes), kick off the scan, wait for completion, export,
|
|
274
|
+
// print, threshold-check. Returns the exit code that THIS audit pass
|
|
275
|
+
// should produce — the watch loop overall stays at 0 until Ctrl+C.
|
|
276
|
+
async function runOneAudit() {
|
|
277
|
+
// On re-runs, navigate the target page again so it picks up source
|
|
278
|
+
// changes (assumes the dev server hot-reloads or the URL serves fresh
|
|
279
|
+
// content).
|
|
280
|
+
await targetPage.bringToFront();
|
|
281
|
+
await targetPage.reload({ waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
282
|
+
await targetPage.waitForTimeout(500);
|
|
283
|
+
await sidePanel.bringToFront();
|
|
284
|
+
await sidePanel.waitForTimeout(300);
|
|
285
|
+
|
|
286
|
+
await sidePanel.getByLabel('Audit mode').selectOption('full-page');
|
|
287
|
+
await sidePanel.getByRole('button', { name: /Scan page|Auditing/ }).click();
|
|
288
|
+
log('audit started');
|
|
289
|
+
|
|
290
|
+
await sidePanel
|
|
291
|
+
.getByText(/\d+\s+states?\s+·\s+\d+\s+unique\s+violations?/, { exact: false })
|
|
292
|
+
.waitFor({ timeout: args.timeout });
|
|
293
|
+
log('audit completed');
|
|
294
|
+
|
|
295
|
+
return await collectResults();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Result-export + threshold-check, extracted so runOneAudit returns the
|
|
299
|
+
// exit code suggestion. Closure over `args`, `sidePanel`, etc.
|
|
300
|
+
async function collectResults() {
|
|
301
|
+
return await doExportAndThresholdCheck();
|
|
302
|
+
}
|
|
265
303
|
|
|
266
|
-
await
|
|
267
|
-
.getByText(/\d+\s+states?\s+·\s+\d+\s+unique\s+violations?/, { exact: false })
|
|
268
|
-
.waitFor({ timeout: args.timeout });
|
|
269
|
-
log('audit completed');
|
|
304
|
+
await runOneAudit();
|
|
270
305
|
|
|
271
306
|
// Trigger the existing export pipeline. The SW handles EXPORT_REQUEST and
|
|
272
307
|
// returns the formatted content for the requested format.
|
|
308
|
+
async function doExportAndThresholdCheck() {
|
|
273
309
|
const formatMap = { json: 'json', sarif: 'sarif', junit: 'junit' };
|
|
274
310
|
const exportFormat = formatMap[args.format];
|
|
275
311
|
|
|
@@ -353,6 +389,61 @@ try {
|
|
|
353
389
|
log(`✓ threshold not exceeded (--threshold=${args.threshold})`);
|
|
354
390
|
}
|
|
355
391
|
}
|
|
392
|
+
} // end doExportAndThresholdCheck
|
|
393
|
+
|
|
394
|
+
// Watch mode: keep the browser open, watch the directory tree, re-audit
|
|
395
|
+
// on change (debounced). Ctrl+C exits.
|
|
396
|
+
if (args.watch) {
|
|
397
|
+
const watchDir = args.watchDir ?? process.cwd();
|
|
398
|
+
const debounceMs = args.watchDebounce ?? 1500;
|
|
399
|
+
const { watch } = await import('node:fs');
|
|
400
|
+
log(`watching ${watchDir} (debounce ${debounceMs}ms) — Ctrl+C to exit`);
|
|
401
|
+
let pending = false;
|
|
402
|
+
let inFlight = false;
|
|
403
|
+
let runCount = 1;
|
|
404
|
+
function trigger() {
|
|
405
|
+
if (inFlight) {
|
|
406
|
+
pending = true;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
inFlight = true;
|
|
410
|
+
setTimeout(async () => {
|
|
411
|
+
try {
|
|
412
|
+
runCount++;
|
|
413
|
+
log(`\n── Re-audit #${runCount} ──`);
|
|
414
|
+
await runOneAudit();
|
|
415
|
+
if (pending) {
|
|
416
|
+
pending = false;
|
|
417
|
+
inFlight = false;
|
|
418
|
+
trigger();
|
|
419
|
+
} else {
|
|
420
|
+
inFlight = false;
|
|
421
|
+
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
log(`re-audit failed: ${err.message}`);
|
|
424
|
+
inFlight = false;
|
|
425
|
+
}
|
|
426
|
+
}, debounceMs);
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
watch(watchDir, { recursive: true }, (eventType, filename) => {
|
|
430
|
+
// Skip noise: node_modules, dist artifacts, .git, dotfiles other
|
|
431
|
+
// than the typical source files.
|
|
432
|
+
if (!filename) return;
|
|
433
|
+
const f = String(filename);
|
|
434
|
+
if (f.includes('node_modules')) return;
|
|
435
|
+
if (f.includes('.git')) return;
|
|
436
|
+
if (f.includes('/dist/') || f.startsWith('dist/') || f.includes('\\dist\\') || f.startsWith('dist\\')) return;
|
|
437
|
+
log(`change: ${f}`);
|
|
438
|
+
trigger();
|
|
439
|
+
});
|
|
440
|
+
} catch (err) {
|
|
441
|
+
log(`fs.watch failed (recursive may not be supported on this platform): ${err.message}`);
|
|
442
|
+
log('watch mode disabled; exiting after this audit.');
|
|
443
|
+
}
|
|
444
|
+
// Block forever until the user kills the process.
|
|
445
|
+
await new Promise(() => {});
|
|
446
|
+
}
|
|
356
447
|
} catch (err) {
|
|
357
448
|
console.error(`✗ ${err.message}`);
|
|
358
449
|
if (err.stack && !args.quiet) console.error(err.stack);
|