vital-core / tests /unit /lib.test.js
vital-core sync
Deploy snapshot for Hugging Face Spaces
50841a0
Raw
History Blame Contribute Delete
65.2 kB
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { normalizeUrl, pageId, registrableDomain, isThirdParty } from '../../src/lib/urls.js';
import { isoWeekOf, previousWeekOf } from '../../src/lib/week.js';
import { parseRobots } from '../../src/lib/robots.js';
import { discoverFromSitemaps } from '../../src/lib/sitemap.js';
import { addPage, pickBatch } from '../../src/lib/state.js';
import { resolveWcag, severityFor, classifyFinding } from '../../src/lib/wcag.js';
import { buildUrlFilter } from '../../src/lib/urls.js';
import { buildBugReports, bugReportToMarkdown } from '../../src/lib/bug-report.js';
import { splitSentences, estimateSyllables } from '../../src/engines/plain-language.js';
import { checkLink } from '../../src/lib/links.js';
import { normalizeRate, shouldRun } from '../../src/lib/sampling.js';
import { updateFindings } from '../../src/lib/findings.js';
import { findMisspellings } from '../../src/lib/spell.js';
import { impactFor, estimateExcluded, pct } from '../../src/lib/fpc.js';
import { toCsv, ruleSlug } from '../../src/lib/csv.js';
import { writeLighthouseCsv, writeLighthouseJson } from '../../src/lib/csv.js';
import { updateResourceLedger } from '../../src/lib/resource-ledger.js';
import { buildAcrData, buildAcrYaml } from '../../src/lib/acr.js';
import { headersToWappalyzer } from '../../src/engines/tech.js';
import { buildCooccurrence, lift, rankAssociations, mergeFleet, rankFleetAssociations } from '../../src/lib/tech-findings.js';
import { rollupThirdParty } from '../../src/lib/third-party-rollup.js';
import { buildLineManifest } from '../../src/lib/paracharts.js';
import { extractAudits } from '../../src/engines/lighthouse.js';
import { renderLighthousePage } from '../../src/report-html.js';
import { assessAltText, isAltProblem, ALT_VERDICTS } from '../../src/lib/alt-text.js';
import { loadPriorityUrls } from '../../src/lib/top-tasks.js';
import { prioritizeAccessibilityBugs } from '../../src/lib/accessibility-priority.js';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
test('normalizeUrl: identity is stable and tracking-free', () => {
const base = 'https://example.gov/';
assert.equal(
normalizeUrl('/a/b/?utm_source=x&z=1&a=2#frag', base, 'example.gov'),
'https://example.gov/a/b?a=2&z=1'
);
assert.equal(normalizeUrl('https://EXAMPLE.gov:443/path/', base, 'example.gov'), 'https://example.gov/path');
assert.equal(normalizeUrl('https://other.gov/x', base, 'example.gov'), null, 'off-host rejected');
assert.equal(normalizeUrl('/file.pdf', base, 'example.gov'), null, 'binary rejected');
assert.equal(normalizeUrl('mailto:x@y.z', base, 'example.gov'), null);
assert.equal(normalizeUrl('https://example.gov/', base, 'example.gov'), 'https://example.gov/', 'root keeps slash');
});
test('normalizeUrl: apex and www are the same host, other subdomains are not', () => {
// Target the apex: www links of the same registrable domain are accepted...
assert.equal(
normalizeUrl('https://www.cdc.gov/about', 'https://cdc.gov/', 'cdc.gov'),
'https://www.cdc.gov/about',
'www variant accepted, actual host preserved'
);
// ...and vice versa: target www, apex link accepted.
assert.equal(
normalizeUrl('https://cdc.gov/about', 'https://www.cdc.gov/', 'www.cdc.gov'),
'https://cdc.gov/about',
'apex variant accepted from a www target'
);
// Any other subdomain is a different site and is rejected.
assert.equal(
normalizeUrl('https://data.cms.gov/x', 'https://www.cms.gov/', 'www.cms.gov'),
null,
'non-www subdomain rejected'
);
assert.equal(
normalizeUrl('https://www.cms.gov/x', 'https://data.cms.gov/', 'data.cms.gov'),
null,
'www of base domain rejected when target is a different subdomain'
);
});
test('pageId: deterministic', () => {
assert.equal(pageId('https://example.gov/a'), pageId('https://example.gov/a'));
assert.notEqual(pageId('https://example.gov/a'), pageId('https://example.gov/b'));
});
test('isoWeek: known dates', () => {
assert.equal(isoWeekOf(new Date(Date.UTC(2026, 0, 1))), '2026-W01');
assert.equal(isoWeekOf(new Date(Date.UTC(2026, 5, 12))), '2026-W24');
assert.equal(isoWeekOf(new Date(Date.UTC(2027, 0, 1))), '2026-W53'); // Jan 1 2027 is a Friday in ISO week 53 of 2026
assert.equal(previousWeekOf('2026-W24', ['2026-W22', '2026-W24', '2026-W20']), '2026-W22');
});
test('robots: disallow, allow, wildcards, crawl-delay', () => {
const r = parseRobots(
`User-agent: *\nDisallow: /private/\nDisallow: /*.cgi$\nAllow: /private/ok\nCrawl-delay: 2\n`,
'vital-scans/0.1'
);
assert.equal(r.isAllowed('/public/page'), true);
assert.equal(r.isAllowed('/private/secret'), false);
assert.equal(r.isAllowed('/private/ok/page'), true, 'longer Allow wins');
assert.equal(r.isAllowed('/script.cgi'), false, '$ anchor');
assert.equal(r.isAllowed('/script.cgi.html'), true);
assert.equal(r.crawlDelay, 2);
});
test('robots: empty file allows everything', () => {
const r = parseRobots('', 'vital-scans/0.1');
assert.equal(r.isAllowed('/anything'), true);
assert.equal(r.crawlDelay, null);
});
test('sitemap: traverses multiple sibling index branches one level deep', async () => {
const realFetch = globalThis.fetch;
try {
const xml = {
'https://example.gov/sitemap.xml':
'<?xml version="1.0"?><sitemapindex>' +
'<sitemap><loc>https://example.gov/sitemap-a.xml</loc></sitemap>' +
'<sitemap><loc>https://example.gov/sitemap-b.xml</loc></sitemap>' +
'</sitemapindex>',
'https://example.gov/sitemap-a.xml':
'<?xml version="1.0"?><urlset>' +
'<url><loc>https://example.gov/a</loc></url>' +
'</urlset>',
'https://example.gov/sitemap-b.xml':
'<?xml version="1.0"?><urlset>' +
'<url><loc>https://example.gov/b</loc></url>' +
'</urlset>',
};
globalThis.fetch = async (input) => {
const key = String(input);
const body = xml[key];
if (!body) return { ok: false, text: async () => '' };
return { ok: true, text: async () => body };
};
const found = await discoverFromSitemaps('https://example.gov', 'example.gov', 'vital-scans/0.1');
assert.deepEqual(found.sort(), ['https://example.gov/a', 'https://example.gov/b']);
} finally {
globalThis.fetch = realFetch;
}
});
test('pickBatch: never-scanned first, weekly cap respected, no rescan same week', () => {
const state = { domain: 'x', pages: {} };
addPage(state, 'a', 'https://x/a', 0);
addPage(state, 'b', 'https://x/b', 1);
addPage(state, 'c', 'https://x/c', 2);
state.pages.a.lastScannedWeek = '2026-W24';
state.pages.b.lastScannedWeek = '2026-W23';
const { batch } = pickBatch(state, '2026-W24', 10, 100);
// a excluded (already scanned this week); c (never scanned) before b (stale).
assert.equal(batch.find((x) => x.id === 'a'), undefined, 'a excluded — already scanned this week');
assert.deepEqual(batch.map((b) => b.id), ['c', 'b'], 'never-scanned (c) before previously-scanned (b)');
// Weekly cap: 1 already scanned this week, cap 2 -> only 1 more allowed.
const { batch: capped } = pickBatch(state, '2026-W24', 10, 2);
assert.equal(capped.length, 1);
// Failing pages excluded after 3 failures.
state.pages.c.failCount = 3;
const { batch: noFail } = pickBatch(state, '2026-W24', 10, 100);
assert.deepEqual(noFail.map((b) => b.id), ['b']);
});
test('pickBatch: priority URLs scanned first, no rescan within a week', () => {
const state = { domain: 'x', pages: {} };
// 5 normal pages, 1 priority page added later (so not first by insertion).
for (let i = 0; i < 5; i++) addPage(state, 'n' + i, `https://x/n${i}`, 1);
addPage(state, 'top', 'https://x/top', 0, { priority: true });
const { batch } = pickBatch(state, '2026-W24', 3, 100);
assert.equal(batch[0].id, 'top', 'priority page comes first regardless of insertion order');
assert.equal(batch[0].priority, true);
// Simulate scanning the batch this week; none reappear in the same week.
for (const b of batch) state.pages[b.id].lastScannedWeek = '2026-W24';
const { batch: next } = pickBatch(state, '2026-W24', 10, 100);
assert.ok(!next.some((b) => batch.some((p) => p.id === b.id)), 'already-scanned pages not repeated this week');
});
test('pickBatch: non-priority order is stable per week but varies across weeks', () => {
const state = { domain: 'x', pages: {} };
for (let i = 0; i < 50; i++) addPage(state, 'p' + i, `https://x/p${i}`, 1);
const w24a = pickBatch(state, '2026-W24', 50, 100).batch.map((b) => b.id);
const w24b = pickBatch(state, '2026-W24', 50, 100).batch.map((b) => b.id);
const w25 = pickBatch(state, '2026-W25', 50, 100).batch.map((b) => b.id);
assert.deepEqual(w24a, w24b, 'same week -> identical order (deterministic, replayable)');
assert.notDeepEqual(w24a, w25, 'different week -> different random spread');
// Same set, just reordered.
assert.deepEqual([...w24a].sort(), [...w25].sort(), 'same pages, different order');
});
test('pickBatch: failed pages can retry in-week until fail threshold', () => {
const state = { domain: 'x', pages: {} };
addPage(state, 'done', 'https://x/done', 1);
addPage(state, 'retry', 'https://x/retry', 1);
addPage(state, 'blocked', 'https://x/blocked', 1);
// done: completed this week -> excluded.
state.pages.done.lastScannedWeek = '2026-W24';
// retry: no completed outcome yet this week and below fail threshold -> eligible.
state.pages.retry.failCount = 2;
// blocked: reached fail threshold -> excluded.
state.pages.blocked.failCount = 3;
const { batch } = pickBatch(state, '2026-W24', 10, 100);
assert.deepEqual(batch.map((b) => b.id), ['retry']);
});
test('addPage: priority promotes an existing page without duplicating', () => {
const state = { domain: 'x', pages: {} };
assert.equal(addPage(state, 'a', 'https://x/a', 1), true, 'first add');
assert.equal(addPage(state, 'a', 'https://x/a', 1), false, 'duplicate add is a no-op');
assert.equal(state.pages.a.priority, false);
assert.equal(addPage(state, 'a', 'https://x/a', 0, { priority: true }), true, 'promotion counts as a change');
assert.equal(state.pages.a.priority, true, 'existing page promoted to priority');
});
test('resolveWcag: axe tags and alfa rule ids map to criteria', () => {
assert.deepEqual(resolveWcag('axe-core', { tags: ['cat.color', 'wcag2aa', 'wcag143'] }), {
sc: '1.4.3', name: 'Contrast (Minimum)', level: 'AA', wcag_version: '2.0',
});
assert.deepEqual(resolveWcag('axe-core', { tags: ['wcag412'] }), {
sc: '4.1.2', name: 'Name, Role, Value', level: 'A', wcag_version: '2.0',
});
assert.equal(resolveWcag('axe-core', { tags: ['best-practice', 'wcag2a'] }), null, 'level-only tags have no SC');
assert.deepEqual(resolveWcag('alfa', { ruleId: 'sia-r12' }), {
sc: '4.1.2', name: 'Name, Role, Value', level: 'A', wcag_version: '2.0',
});
assert.deepEqual(resolveWcag('alfa', { ruleId: '90' }), {
sc: '4.1.2', name: 'Name, Role, Value', level: 'A', wcag_version: '2.0',
}, 'numeric SI id is normalized to sia-rN and mapped');
assert.deepEqual(resolveWcag('alfa', { ruleId: 'sia-r67' }), {
sc: '1.1.1', name: 'Non-text Content', level: 'A', wcag_version: '2.0',
}, 'alfa map from data file resolves additional rules');
assert.equal(resolveWcag('alfa', { ruleId: 'sia-r9999' }), null, 'unknown alfa rule undetermined');
});
test('severityFor: axe impact maps, frequency amplifies', () => {
assert.equal(severityFor('critical', 1, 50), 'Critical');
assert.equal(severityFor('minor', 1, 50), 'Low', 'rare minor stays low');
assert.equal(severityFor('minor', 30, 50), 'Medium', 'site-wide minor escalates one level');
assert.equal(severityFor('serious', 40, 50), 'Critical', 'site-wide serious escalates to critical');
assert.equal(severityFor(null, 1, 50), 'Medium', 'no impact (alfa) defaults medium');
});
test('buildBugReports: shape, ids stable, sorted, placeholders present', () => {
const target = { domain: 'example.gov', key: 'example.gov' };
const summary = {
domain: 'example.gov',
week: '2026-W24',
generatedAt: '2026-06-13T00:00:00.000Z',
pagesScanned: 10,
axe: { rules: {
'color-contrast': { count: 8, pages: 6, impact: 'serious', help: 'Elements must have sufficient color contrast',
helpUrl: 'https://dequeuniversity.com/rules/axe/4.9/color-contrast', tags: ['wcag143', 'wcag2aa'],
examplePages: ['https://example.gov/a'],
instances: [{ url: 'https://example.gov/a', target: '.btn', html: '<a class="btn">Go</a>' }] },
} },
alfa: { rules: {
'sia-r12': { count: 2, pages: 1, ruleUrl: 'https://act-rules.github.io/rules/97a4e1',
examplePages: ['https://example.gov/b'],
instances: [{ url: 'https://example.gov/b', target: '<button>', html: null }] },
} },
};
const reports = buildBugReports(target, summary);
assert.equal(reports.length, 2);
// New sort order: WCAG 2.0 A (sia-r12 → 4.1.2) before WCAG 2.0 AA
// (color-contrast → 1.4.3). Level A requirements sort before Level AA
// so engineers tackle the baseline compliance obligations first.
const alfaReport = reports.find((r) => r.rule_id === 'sia-r12');
const axeReport = reports.find((r) => r.rule_id === 'color-contrast');
assert.ok(alfaReport, 'alfa sia-r12 present');
assert.ok(axeReport, 'axe color-contrast present');
assert.ok(reports.indexOf(alfaReport) < reports.indexOf(axeReport),
'WCAG 2.0 A (sia-r12) sorts before WCAG 2.0 AA (color-contrast)');
assert.equal(axeReport.severity, 'Critical', '6/10 pages escalates serious to critical');
assert.equal(axeReport.wcag_sc, '1.4.3');
assert.equal(axeReport.wcag_level, 'AA');
assert.equal(axeReport.wcag_version, '2.0');
assert.equal(axeReport.wcag_category, 'WCAG 2.0 AA');
assert.match(axeReport.summary, /\(WCAG 1\.4\.3\)$/);
assert.equal(axeReport.frequency.pages_affected, 6);
assert.equal(axeReport.xpath, '.btn');
assert.ok(axeReport.html_snippet.includes('btn'));
// Impact: WCAG 1.4.3 (contrast) maps to vision-related FPC groups.
assert.ok(axeReport.impact.groups.length > 0, 'mapped SC yields impact groups');
assert.ok(axeReport.impact.groups.some((g) => /vision/i.test(g.group)), 'contrast affects a vision group');
assert.match(axeReport.impact.summary, /Affects/);
// Stable ids: same input -> same ids.
const again = buildBugReports(target, summary);
assert.equal(again.find((r) => r.rule_id === 'color-contrast').instance_id, axeReport.instance_id);
assert.equal(again.find((r) => r.rule_id === 'color-contrast').pattern_id, axeReport.pattern_id);
// Alfa report: no impact -> Medium (default), mapped SC, WCAG category.
const alfa = reports.find((r) => r.rule_id === 'sia-r12');
assert.equal(alfa.severity, 'Medium');
assert.equal(alfa.wcag_sc, '4.1.2');
assert.equal(alfa.wcag_version, '2.0');
assert.equal(alfa.wcag_category, 'WCAG 2.0 A');
// Markdown renders the required headings.
const md = bugReportToMarkdown(axeReport);
assert.match(md, /\*\*Severity:\*\* Critical/);
assert.match(md, /### Steps to reproduce/);
assert.match(md, /\*\*WCAG SC:\*\* 1\.4\.3/);
});
test('lighthouse exports and page link the raw dataset', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lighthouse-'));
const summary = {
domain: 'example.gov',
week: '2026-W25',
generatedAt: '2026-06-18T00:00:00.000Z',
lighthouse: {
pageDetail: [
{
url: 'https://example.gov/high',
scores: { performance: 90, accessibility: 91, bestPractices: 92, seo: 93, agentic: 94 },
metrics: { firstContentfulPaintMs: 100, largestContentfulPaintMs: 200, speedIndexMs: 300, totalBlockingTimeMs: 400, cumulativeLayoutShift: 0.01 },
},
{
url: 'https://example.gov/low',
scores: { performance: 30, accessibility: 31, bestPractices: 32, seo: 33, agentic: 34 },
metrics: { firstContentfulPaintMs: 500, largestContentfulPaintMs: 600, speedIndexMs: 700, totalBlockingTimeMs: 800, cumulativeLayoutShift: 0.2 },
},
],
medianPerformance: 60,
medianAccessibility: 61,
medianBestPractices: 62,
medianSeo: 63,
medianAgentic: 64,
metrics: {},
recommendations: [],
},
};
const csvHref = writeLighthouseCsv(tmpDir, summary.lighthouse);
const jsonHref = writeLighthouseJson(tmpDir, summary.lighthouse, { domain: summary.domain, week: summary.week, generatedAt: summary.generatedAt });
assert.equal(csvHref, 'lighthouse.csv');
assert.equal(jsonHref, 'lighthouse.json');
const rawJson = JSON.parse(fs.readFileSync(path.join(tmpDir, 'lighthouse.json'), 'utf8'));
assert.equal(rawJson.pages[0].url, 'https://example.gov/low', 'raw JSON is sorted by lowest performance first');
assert.equal(rawJson.pages[1].url, 'https://example.gov/high');
const html = renderLighthousePage({ domain: 'example.gov', page_loads_per_week: null }, summary, csvHref, jsonHref);
assert.match(html, /Download raw dataset:/);
assert.match(html, /href="lighthouse\.csv"/);
assert.match(html, /href="lighthouse\.json"/);
});
test('plain-language: sentence splitting and syllable estimation', () => {
const s = splitSentences('The cat sat. It was happy! Was it? Yes.');
assert.equal(s.length, 4);
// Short words = 1 syllable; multi-vowel-group words counted.
assert.equal(estimateSyllables('cat'), 1);
assert.equal(estimateSyllables('happy'), 2);
assert.equal(estimateSyllables('accessibility') >= 4, true);
assert.equal(estimateSyllables(''), 0);
});
test('checkLink: classifies status codes, soft-ok, and network errors', async () => {
const realFetch = globalThis.fetch;
try {
globalThis.fetch = async () => ({ status: 200 });
assert.deepEqual(await pick(checkLink('https://x/ok')), { ok: true, broken: false, status: 200 });
globalThis.fetch = async () => ({ status: 404 });
assert.deepEqual(await pick(checkLink('https://x/missing')), { ok: false, broken: true, status: 404 });
// 403/429 are soft-ok: bots get challenged but the link is fine.
globalThis.fetch = async () => ({ status: 403 });
assert.deepEqual(await pick(checkLink('https://x/forbidden')), { ok: true, broken: false, status: 403 });
// HEAD unsupported (405) retries with GET.
let calls = 0;
globalThis.fetch = async (_u, opts) => {
calls++;
return { status: opts.method === 'HEAD' ? 405 : 200 };
};
const retried = await checkLink('https://x/headless');
assert.equal(retried.ok, true);
assert.equal(calls, 2, 'retried with GET after 405');
// Network failure -> broken with reason.
globalThis.fetch = async () => {
throw Object.assign(new Error('boom'), { cause: { code: 'ENOTFOUND' } });
};
const dead = await checkLink('https://nope/');
assert.equal(dead.broken, true);
assert.equal(dead.status, 0);
assert.match(dead.reason, /ENOTFOUND/);
} finally {
globalThis.fetch = realFetch;
}
});
async function pick(p) {
const r = await p;
return { ok: r.ok, broken: r.broken, status: r.status };
}
test('sampling: normalizeRate accepts fractions, percents, and junk', () => {
assert.equal(normalizeRate(0.3), 0.3);
assert.equal(normalizeRate(30), 0.3);
assert.equal(normalizeRate('30%'), 0.3);
assert.equal(normalizeRate(100), 1);
assert.equal(normalizeRate(150), 1, 'clamped to 1');
assert.equal(normalizeRate(null), 0, 'missing -> off');
assert.equal(normalizeRate('nonsense'), 0);
assert.equal(normalizeRate(-5), 0);
});
test('sampling: shouldRun is deterministic, ~rate, and per-engine independent', () => {
const week = '2026-W24';
// Deterministic: same inputs, same answer.
assert.equal(shouldRun('alfa', 'p1', week, 30), shouldRun('alfa', 'p1', week, 30));
// 0 and 100 are absolute.
assert.equal(shouldRun('x', 'p1', week, 0), false);
assert.equal(shouldRun('x', 'p1', week, 100), true);
// Distribution lands near the target over many pages.
const N = 3000;
let alfa = 0, lh = 0, both = 0;
for (let i = 0; i < N; i++) {
const a = shouldRun('alfa', 'pid-' + i, week, 30);
const l = shouldRun('lighthouse', 'pid-' + i, week, 10);
if (a) alfa++;
if (l) lh++;
if (a && l) both++;
}
assert.ok(Math.abs(alfa / N - 0.3) < 0.04, `alfa ~30% (got ${(100 * alfa / N).toFixed(1)}%)`);
assert.ok(Math.abs(lh / N - 0.1) < 0.04, `lighthouse ~10% (got ${(100 * lh / N).toFixed(1)}%)`);
// Independence: P(both) ~ P(alfa)*P(lighthouse) ~ 0.03, not correlated.
assert.ok(Math.abs(both / N - 0.03) < 0.03, 'engines sampled independently');
});
test('findings: ledger tracks first/last-seen and is idempotent per week', () => {
const ledger = { domain: 'x', findings: {} };
const reportA = { pattern_id: 'VS-aaa', tool: 'axe-core', rule_id: 'image-alt', summary: 'Images need alt (WCAG 1.1.1)', wcag_sc: '1.1.1', severity: 'critical', frequency: { pages_affected: 5 } };
const reportB = { pattern_id: 'VS-bbb', tool: 'alfa', rule_id: 'sia-r12', summary: 'Button name (WCAG 4.1.2)', wcag_sc: '4.1.2', severity: 'moderate', frequency: { pages_affected: 2 } };
updateFindings(ledger, '2026-W23', [reportA, reportB]);
assert.equal(ledger.findings['VS-aaa'].firstSeen, '2026-W23');
assert.equal(ledger.findings['VS-aaa'].weeksSeen, 1);
// Week 2: A persists (lastSeen advances), B gone (lastSeen stays W23).
updateFindings(ledger, '2026-W24', [reportA]);
assert.equal(ledger.findings['VS-aaa'].firstSeen, '2026-W23');
assert.equal(ledger.findings['VS-aaa'].lastSeen, '2026-W24');
assert.equal(ledger.findings['VS-aaa'].weeksSeen, 2);
assert.equal(ledger.findings['VS-bbb'].lastSeen, '2026-W23', 'resolved finding keeps last-seen');
// Idempotent: re-running W24 does not inflate weeksSeen.
updateFindings(ledger, '2026-W24', [reportA]);
assert.equal(ledger.findings['VS-aaa'].weeksSeen, 2, 're-run same week is idempotent');
});
test('spell: flags real misspellings, skips numbers/acronyms/allowlist/jargon', () => {
const r = findMisspellings([
'The', 'quick', 'accessibility', // correct dictionary words
'teh', 'recieve', 'xyzzy', // genuine misspellings
'Medicaid', 'telehealth', // allowlisted jargon
'COVID', 'API', // ALL-CAPS acronyms -> skipped
'2026', 'v4', // contain digits -> skipped
'a', 'to', // too short -> skipped
]);
assert.equal(r.misspelledCount, 3);
assert.deepEqual(r.misspelled.sort(), ['recieve', 'teh', 'xyzzy']);
});
test('spell: per-call extraAllowlist suppresses target-specific jargon', () => {
const words = ['MACRA', 'CHIPRA', 'xyzzy', 'beneficiares']; // MACRA/CHIPRA = ALL-CAPS -> always skipped; xyzzy/beneficiares = misspellings
const withoutExtra = findMisspellings(words);
assert.ok(withoutExtra.misspelled.includes('xyzzy'), 'xyzzy flagged without allowlist');
const withExtra = findMisspellings(words, 25, ['xyzzy', 'beneficiares']);
assert.equal(withExtra.misspelledCount, 0, 'extraAllowlist suppresses domain-specific terms');
assert.deepEqual(withExtra.misspelled, []);
});
test('spell: tolerates possessives and caps the distinct list', () => {
const r = findMisspellings(["government's", 'agency’s']);
assert.equal(r.misspelledCount, 0, "possessive of a real word is not a misspelling");
const many = Array.from({ length: 40 }, (_, i) => `xqzzx${String.fromCharCode(97 + (i % 26))}${i}`)
.map((w) => w.replace(/\d/g, '')); // strip digits so they're checkable
const capped = findMisspellings(many, 25);
assert.ok(capped.misspelled.length <= 25, 'distinct list is capped');
assert.ok(capped.misspelledCount >= capped.misspelled.length, 'count tracks all, list is capped');
});
test('fpc: maps WCAG SC to disability groups with prevalence', () => {
const contrast = impactFor('1.4.3');
assert.ok(contrast.groups.some((g) => g.code === 'LV'), 'contrast affects limited vision');
assert.ok(contrast.maxPrevalence > 0 && contrast.maxPrevalence < 1);
const altText = impactFor('1.1.1');
assert.ok(altText.groups.some((g) => g.code === 'WV'), 'non-text content affects without-vision');
assert.equal(impactFor('9.9.9'), null, 'unknown SC -> null');
assert.equal(pct(0.024), '2.4%');
});
test('fpc: estimateExcluded scales prevalence by page loads', () => {
assert.equal(estimateExcluded(0.01, 1_000_000), 10000);
assert.equal(estimateExcluded(0.024, 0), null, 'no loads -> null');
assert.equal(estimateExcluded(0.01, undefined), null);
});
test('csv: escapes fields and slugs rule ids', () => {
const csv = toCsv(['url', 'instances'], [['https://x/a,b', 3], ['https://x/"q"', 1]]);
assert.match(csv, /^url,instances\n/);
assert.match(csv, /"https:\/\/x\/a,b",3/, 'comma field quoted');
assert.match(csv, /"https:\/\/x\/""q""",1/, 'quotes doubled');
assert.equal(ruleSlug('axe-core', 'color-contrast'), 'axe-core__color-contrast');
assert.equal(ruleSlug('alfa', 'sia-r12'), 'alfa__sia-r12');
assert.equal(ruleSlug('x', 'weird/id:1'), 'x__weird-id-1', 'unsafe chars slugged');
});
test('buildBugReports: page-load estimate appears when target sets page_loads_per_week', () => {
const target = { domain: 'x', key: 'x', page_loads_per_week: 1_000_000 };
const summary = {
domain: 'x', week: '2026-W24', generatedAt: '2026-06-13T00:00:00Z', pagesScanned: 10,
axe: { rules: { 'color-contrast': { count: 5, pages: 10, impact: 'serious',
help: 'Contrast', helpUrl: 'https://x', tags: ['wcag143'], examplePages: ['https://x/a'],
instances: [{ url: 'https://x/a', target: '.b', html: '<a>' }] } } },
alfa: { rules: {} },
};
const [r] = buildBugReports(target, summary);
assert.ok(r.impact.groups.length > 0);
// 100% of pages affected, prevalence applied to full page loads.
assert.ok(r.impact.groups.every((g) => g.estimatedExcluded > 0), 'excluded estimates present with page loads');
// Without page_loads_per_week, no estimate (just percentages).
const [r2] = buildBugReports({ domain: 'x', key: 'x' }, summary);
assert.ok(r2.impact.groups.every((g) => g.estimatedExcluded == null), 'no estimate without page loads');
});
test('resource ledger: tracks first/last-seen and flags new-this-week', () => {
const ledger = { domain: 'x', resources: {} };
const w1 = updateResourceLedger(ledger, '2026-W23', [
{ url: 'https://x/a.pdf', type: 'pdf', pages: 3 },
{ url: 'https://x/embed', type: 'iframe', pages: 1 },
]);
assert.equal(w1.length, 2, 'all resources are new in the first week');
assert.equal(ledger.resources['https://x/a.pdf'].firstSeen, '2026-W23');
// Week 2: a.pdf persists, b.pdf is brand new.
const w2 = updateResourceLedger(ledger, '2026-W24', [
{ url: 'https://x/a.pdf', type: 'pdf', pages: 4 },
{ url: 'https://x/b.pdf', type: 'pdf', pages: 1 },
]);
assert.deepEqual(w2.map((r) => r.url), ['https://x/b.pdf'], 'only the genuinely new resource is flagged');
assert.equal(ledger.resources['https://x/a.pdf'].firstSeen, '2026-W23', 'persisting resource keeps first-seen');
assert.equal(ledger.resources['https://x/a.pdf'].lastSeen, '2026-W24', 'lastSeen advances');
assert.equal(ledger.resources['https://x/b.pdf'].firstSeen, '2026-W24');
// Re-running the same week is idempotent for weeksSeen (no inflation);
// a resource first seen this week is still correctly "new this week".
const again = updateResourceLedger(ledger, '2026-W24', [{ url: 'https://x/b.pdf', type: 'pdf', pages: 1 }]);
assert.deepEqual(again.map((r) => r.url), ['https://x/b.pdf'], 'a this-week resource is new on re-run too');
assert.equal(ledger.resources['https://x/b.pdf'].weeksSeen, 1, 're-run does not inflate weeksSeen');
});
test('act + consensus: axe and alfa for the same ACT rule are one issue', async () => {
const { canonicalRuleKey, actRuleIdsFor } = await import('../../src/lib/act.js');
const { buildConsensus } = await import('../../src/lib/consensus.js');
// image-alt (axe) and sia-r2 (alfa) are both ACT rule 23a2a8.
assert.deepEqual(actRuleIdsFor('axe-core', 'image-alt'), ['23a2a8']);
assert.deepEqual(actRuleIdsFor('alfa', 'sia-r2'), ['23a2a8']);
assert.equal(canonicalRuleKey('axe-core', 'image-alt'), canonicalRuleKey('alfa', 'sia-r2'));
// An unmapped rule keeps its own engine-scoped key (never merged).
assert.equal(canonicalRuleKey('axe-core', 'made-up-rule'), 'axe-core:made-up-rule');
const axe = { 'image-alt': { affectedPages: [{ url: 'p/a', instances: 1 }, { url: 'p/b', instances: 1 }] } };
const alfa = { 'sia-r2': { affectedPages: [{ url: 'p/a', instances: 1 }] } };
const c = buildConsensus(axe, alfa);
// /a: both engines -> consensus; /b: axe only. 2 unique from 3 raw.
assert.equal(c.uniqueIssues, 2, 'deduped to 2 unique issues');
assert.equal(c.consensus, 1, '/a caught by both');
assert.equal(c.axeOnly, 1, '/b axe only');
assert.equal(c.alfaOnly, 0);
assert.equal(c.rawAxe + c.rawAlfa, 3, 'naive count would be 3');
assert.equal(c.byKey['act:23a2a8'].engines, 'both');
});
test('consensus: unmapped rules from different engines stay separate', async () => {
const { buildConsensus } = await import('../../src/lib/consensus.js');
// Two unmapped rules on the same page must NOT merge.
const axe = { 'zzz-unmapped': { affectedPages: [{ url: 'p/a', instances: 1 }] } };
const alfa = { 'sia-rZZZ': { affectedPages: [{ url: 'p/a', instances: 1 }] } };
const c = buildConsensus(axe, alfa);
assert.equal(c.uniqueIssues, 2, 'distinct unmapped rules are distinct issues');
assert.equal(c.consensus, 0, 'no false consensus across unrelated rules');
});
test('remediation: tips resolve for known rules, null otherwise', async () => {
const { remediationTip } = await import('../../src/lib/remediation.js');
assert.match(remediationTip('axe-core', 'image-alt'), /alt/i);
assert.match(remediationTip('alfa', 'sia-r2'), /alt/i);
assert.equal(remediationTip('axe-core', 'no-such-rule'), null);
});
test('score: density-based, spreads across a curve so F is rare and meaningful', async () => {
const { scoreFor, grade, band, trajectory } = await import('../../src/lib/score.js');
const mk = (med) => ({ pagesAudited: 100, axe: { pagesScanned: 100, pagesWithViolations: 100, medianViolations: med }, alfa: { pagesScanned: 100, pagesWithFailures: 100, medianFailures: 0 } });
// The key fix: sites where every page has SOME issue no longer all get F.
const excellent = scoreFor(mk(1)); // ~1/page
const typical = scoreFor(mk(6)); // typical gov page
const poor = scoreFor(mk(16)); // heavy burden
assert.ok(excellent.score > typical.score && typical.score > poor.score, 'monotonic: fewer issues score higher');
assert.ok(excellent.grade === 'A' || excellent.grade === 'B', `low density is A/B (${excellent.grade} ${excellent.score})`);
assert.equal(typical.grade, 'C', `typical site is a C, not F (${typical.score})`);
assert.equal(poor.grade, 'F', `heavy burden is F (${poor.score})`);
// A typical site is NOT failing — the whole point.
assert.ok(typical.score >= 65, `typical site passes (${typical.score})`);
// Score is axe-only: Alfa's (sampled, element-level) count must not move it.
const lowAxeHighAlfa = scoreFor({ pagesAudited: 100, axe: { pagesScanned: 100, pagesWithViolations: 100, medianViolations: 1 }, alfa: { pagesScanned: 30, pagesWithFailures: 30, medianFailures: 99 } });
assert.equal(lowAxeHighAlfa.score, excellent.score, 'Alfa median does not affect the score (axe-only)');
assert.equal(grade(90), 'A'); assert.equal(grade(70), 'C'); assert.equal(grade(40), 'F');
assert.equal(band(90), 'Leading'); assert.equal(band(70), 'Typical'); assert.equal(band(40), 'At risk');
assert.equal(scoreFor({ pagesAudited: 0 }), null, 'no audited pages -> null');
// Trajectory: improving when the typical page's burden drops.
const series = [mk(16), mk(4)];
const t = trajectory(series, 4);
assert.equal(t.direction, 'improving');
assert.ok(t.delta > 0);
});
test('priority: ranks by pages x severity x reach; fleet flattens across domains', async () => {
const { priorityScore, rankBugs, fleetWorstOffenders } = await import('../../src/lib/priority.js');
const bug = (sev, pages, prev) => ({ severity: sev, frequency: { pages_affected: pages }, impact: { groups: prev != null ? [{ prevalence: prev }] : [] }, summary: `${sev}/${pages}` });
const widespreadCritical = bug('critical', 50, 0.1);
const rareLow = bug('minor', 1, 0.01);
assert.ok(priorityScore(widespreadCritical) > priorityScore(rareLow), 'widespread critical outranks rare minor');
const ranked = rankBugs([rareLow, widespreadCritical], 5);
assert.equal(ranked[0].summary, 'critical/50', 'highest priority first');
const fleet = fleetWorstOffenders([
{ target: { domain: 'a.gov', key: 'a' }, bugs: [rareLow] },
{ target: { domain: 'b.gov', key: 'b' }, bugs: [widespreadCritical] },
], 10);
assert.equal(fleet[0].domain, 'b.gov', 'worst issue across domains floats to the top, tagged with its domain');
});
test('inventory: accumulates last-known status, keeps newer over older', async () => {
const { updateInventory, inventorySummary } = await import('../../src/lib/inventory.js');
const inv = { domain: 'x', pages: {} };
updateInventory(inv, '2026-W20', [
{ url: 'x/a', pageId: 'a', status: 200, scannedAt: 't1', axe: { violationCount: 3 } },
{ url: 'x/b', pageId: 'b', status: 200, scannedAt: 't1', axe: { violationCount: 0 }, alfa: { failedCount: 0 } },
]);
// Later week re-checks /a (now clean); /b not re-scanned this week.
updateInventory(inv, '2026-W24', [{ url: 'x/a', pageId: 'a', status: 200, scannedAt: 't2', axe: { violationCount: 0 } }]);
assert.equal(inv.pages['x/a'].lastWeek, '2026-W24', '/a advanced to newer week');
assert.equal(inv.pages['x/a'].hasIssues, false, '/a now clean');
assert.equal(inv.pages['x/b'].lastWeek, '2026-W20', '/b retains its older last-known result');
// Re-applying an older week must NOT clobber the newer /a result.
updateInventory(inv, '2026-W20', [{ url: 'x/a', pageId: 'a', status: 200, scannedAt: 't0', axe: { violationCount: 9 } }]);
assert.equal(inv.pages['x/a'].lastWeek, '2026-W24', 'older re-run did not overwrite newer');
const s = inventorySummary(inv, '2026-W24');
assert.equal(s.totalKnownPages, 2);
assert.equal(s.scannedThisWeek, 1);
});
test('security: classifies TLD, HTTPS, and headers from a mocked origin', async () => {
const { runSecurity } = await import('../../src/engines/security.js');
const realFetch = globalThis.fetch;
try {
// Origin with good headers; security.txt + www both resolve.
globalThis.fetch = async (u) => ({
ok: true, status: 200,
headers: new Map([
['strict-transport-security', 'max-age=63072000'],
['content-security-policy', "default-src 'self'; frame-ancestors 'none'"],
['x-content-type-options', 'nosniff'],
]),
});
// Map needs a .has(); Map has it. .get() too. Good enough for the engine.
const r = await runSecurity('https://example.gov', 'ua', 1000);
const by = Object.fromEntries(r.checks.map((c) => [c.id, c.pass]));
assert.equal(by['https'], true, 'https detected');
assert.equal(by['hsts'], true);
assert.equal(by['csp'], true);
assert.equal(by['x-content-type-options'], true);
assert.equal(by['clickjacking'], true, 'CSP frame-ancestors counts as clickjacking protection');
assert.equal(by['gov-tld'], true, '.gov is a sponsored TLD');
// A non-gov http origin with no headers fails the right checks.
globalThis.fetch = async () => ({ ok: false, status: 404, headers: new Map() });
const r2 = await runSecurity('http://example.com', 'ua', 1000);
const by2 = Object.fromEntries(r2.checks.map((c) => [c.id, c.pass]));
assert.equal(by2['https'], false, 'http is not https');
assert.equal(by2['gov-tld'], false, '.com is not sponsored');
assert.equal(by2['security-txt'], false);
} finally {
globalThis.fetch = realFetch;
}
});
test('remediation: expanded tips cover landmark-unique and aria rules', async () => {
const { remediationTip } = await import('../../src/lib/remediation.js');
assert.match(remediationTip('axe-core', 'landmark-unique'), /aria-label|accessible name/i);
assert.match(remediationTip('axe-core', 'aria-required-parent'), /parent/i);
assert.match(remediationTip('axe-core', 'target-size'), /24|spacing/i);
assert.equal(remediationTip('axe-core', 'totally-made-up'), null);
});
test('affected pages: inline <=25, list 25 + CSV when more (Markdown)', async () => {
const { buildBugReports, bugReportToMarkdown } = await import('../../src/lib/bug-report.js');
const mkRule = (n) => ({
count: n, pages: n, impact: 'serious', help: 'X', helpUrl: 'https://x', tags: ['wcag412'],
examplePages: [], affectedPages: Array.from({ length: Math.min(n, 5000) }, (_, i) => ({ url: `https://x/p${i}`, instances: 1 })),
instances: [{ url: 'https://x/p0', target: '.a', html: '<a>' }],
});
const summary = (n) => ({ domain: 'x', week: '2026-W24', generatedAt: '2026-06-15T00:00:00Z', pagesScanned: 1200, axe: { rules: { 'select-name': mkRule(n) } }, alfa: { rules: {} } });
// 1 affected page: the URL is listed, no "download CSV".
let [bug] = buildBugReports({ domain: 'x', key: 'x' }, summary(1));
bug.affected_pages_csv = 'csv/x.csv';
let md = bugReportToMarkdown(bug);
assert.match(md, /- https:\/\/x\/p0/, 'single affected page listed inline');
assert.doesNotMatch(md.split('### Affected pages')[1].split('###')[0], /download CSV/, 'no CSV link for a single page');
// 25 affected pages: all listed inline, still no CSV.
[bug] = buildBugReports({ domain: 'x', key: 'x' }, summary(25));
bug.affected_pages_csv = 'csv/x.csv';
md = bugReportToMarkdown(bug);
const sect25 = md.split('### Affected pages')[1].split('###')[0];
assert.equal((sect25.match(/- https:\/\/x\/p\d+/g) || []).length, 25, '25 pages listed inline');
assert.doesNotMatch(sect25, /download CSV/, 'no CSV link at exactly 25');
// 26 affected pages: list first 25 + CSV link.
[bug] = buildBugReports({ domain: 'x', key: 'x' }, summary(26));
bug.affected_pages_csv = 'csv/x.csv';
md = bugReportToMarkdown(bug);
const sect26 = md.split('### Affected pages')[1].split('###')[0];
assert.equal((sect26.match(/- https:\/\/x\/p\d+/g) || []).length, 25, 'first 25 listed');
assert.match(sect26, /26 pages total/);
assert.match(sect26, /download CSV/, 'CSV link appears above 25');
});
test('perf-impact: averages always; totals only with page loads', async () => {
const { performanceImpact, humanDuration, humanBytes } = await import('../../src/lib/perf-impact.js');
// Two pages: one over both benchmarks, one under.
const lhPages = [
{ url: 'x/a', metrics: { largestContentfulPaintMs: 4500 } }, // 2s over 2.5s
{ url: 'x/b', metrics: { largestContentfulPaintMs: 2000 } }, // under -> 0
];
const weights = [2_600_000, 1_000_000]; // 1 MB over 1.6 MB, and under -> 0
const noTraffic = performanceImpact(lhPages, weights, null);
assert.equal(noTraffic.avgExtraLcpMs, 1000, 'avg extra LCP = (2000+0)/2 ms');
assert.equal(noTraffic.pagesOverLcp, 1);
assert.equal(noTraffic.avgExtraWeightBytes, 500000, 'avg extra weight = (1MB+0)/2');
assert.equal(noTraffic.totals, null, 'no totals without traffic');
// With traffic, totals appear (loads spread across sampled pages).
const withTraffic = performanceImpact(lhPages, weights, 1000);
assert.ok(withTraffic.totals, 'totals present with page loads');
// extra seconds = sum(maxOver/1000 * loads/nPages) = (2 + 0) * (1000/2) = 1000s
assert.equal(withTraffic.totals.extraSeconds, 1000);
assert.equal(withTraffic.totals.extraBytes, 500_000_000, '(1MB+0) * (1000/2)');
assert.equal(performanceImpact([], [], null), null, 'no data -> null');
assert.match(humanDuration(31557600 * 2 + 86400 * 3), /2 years, 3 days/);
assert.equal(humanBytes(2.5e12), '2.5 TB');
});
test('dashboard: blocked targets render in a collapsed accordion, not up top', async () => {
const { renderIndex } = await import('../../src/report-html.js');
const wk = (week, blocked) => ({ week, blocked, pagesScanned: 1, pagesAudited: blocked ? 0 : 1,
axe: { medianViolations: 0, pagesWithViolations: 0, pagesScanned: 1 },
alfa: { medianFailures: 0, pagesWithFailures: 0, pagesScanned: 1 } });
const dashboard = [
{ target: { domain: 'good.gov', key: 'good.gov' }, series: [wk('2026-W25', null)], diffs: {}, bugs: [], windowSummary: wk('2026-W25', null) },
{ target: { domain: 'blocked.gov', key: 'blocked.gov' }, series: [wk('2026-W25', { status: 403 })], diffs: {}, bugs: [] },
];
const html = renderIndex(dashboard);
assert.match(html, /<details class="blocked-accordion">/, 'blocked targets are in a <details> accordion');
assert.match(html, /Blocked targets \(1\)/, 'accordion summary shows the count');
// The blocked accordion comes AFTER the leaderboard table, not before it.
assert.ok(html.indexOf('<table') < html.indexOf('blocked-accordion'), 'accordion is below the main content');
});
test('buildUrlFilter: no config passes everything', () => {
const filter = buildUrlFilter({});
assert.equal(filter('https://example.gov/any/path?q=1'), true);
});
test('buildUrlFilter: url_include restricts to matching URLs', () => {
const filter = buildUrlFilter({ url_include: ['/children/'] });
assert.equal(filter('https://example.gov/children/page'), true, 'path match');
assert.equal(filter('https://example.gov/adults/page'), false, 'non-matching path');
});
test('buildUrlFilter: url_exclude blocks matching URLs after include', () => {
const filter = buildUrlFilter({ url_exclude: ['press_release', '?page='] });
assert.equal(filter('https://example.gov/news/press_release/2026'), false, 'exclude by keyword');
assert.equal(filter('https://example.gov/list?page=2'), false, 'exclude by query string');
assert.equal(filter('https://example.gov/about'), true, 'non-matching passes');
});
test('buildUrlFilter: url_include + url_exclude compose correctly', () => {
const filter = buildUrlFilter({ url_include: ['/news/'], url_exclude: ['press_release'] });
assert.equal(filter('https://example.gov/news/article'), true, 'include match, no exclude');
assert.equal(filter('https://example.gov/news/press_release'), false, 'include match but excluded');
assert.equal(filter('https://example.gov/about'), false, 'not in include list');
});
test('loadPriorityUrls: normalizes apex/www urls and reads files', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'priority-'));
const file = path.join(tmpDir, 'top-tasks.txt');
fs.writeFileSync(file, '# comment\nhttps://www.example.gov/a\nhttps://example.gov/b\nhttps://example.gov/a\n');
const target = { key: 'example.gov', domain: 'example.gov', priority_urls: ['https://example.gov/c'], priority_urls_file: file };
const urls = loadPriorityUrls(target);
assert.deepEqual(urls.sort(), [
'https://example.gov/a',
'https://example.gov/b',
'https://example.gov/c',
]);
});
test('classifyFinding: WCAG version and level produce correct category', () => {
const wcag20aa = { sc: '1.4.3', name: 'Contrast', level: 'AA', wcag_version: '2.0' };
const wcag21a = { sc: '1.3.4', name: 'Orientation', level: 'A', wcag_version: '2.1' };
const wcag22aa = { sc: '2.4.11', name: 'Focus Appearance', level: 'AA', wcag_version: '2.2' };
const wcagAAA = { sc: '1.4.6', name: 'Contrast (Enhanced)', level: 'AAA', wcag_version: '2.0' };
assert.equal(classifyFinding('axe-core', {}, wcag20aa), 'WCAG 2.0 AA');
assert.equal(classifyFinding('axe-core', {}, wcag21a), 'WCAG 2.1 A');
assert.equal(classifyFinding('axe-core', {}, wcag22aa), 'WCAG 2.2 AA');
assert.equal(classifyFinding('axe-core', {}, wcagAAA), 'WCAG 2.x AAA');
});
test('classifyFinding: axe best-practice tag without WCAG mapping', () => {
assert.equal(classifyFinding('axe-core', { tags: ['best-practice'] }, null), 'Best Practice');
});
test('classifyFinding: no mapping returns Undetermined', () => {
assert.equal(classifyFinding('alfa', {}, null), 'Undetermined');
});
test('buildBugReports: WCAG sort order across version groups', () => {
const makeRule = (tags, pages = 5) => ({ pages, tags, count: pages * 2, examplePages: ['https://x/a'] });
const target = { domain: 'x.gov', key: 'x.gov' };
const summary = {
week: '2026-W25',
pagesScanned: 10,
axe: {
pagesScanned: 10,
rules: {
'focus-appearance': makeRule(['wcag2aa', 'wcag2411']), // 2.4.11 → WCAG 2.2 AA
reflow: makeRule(['wcag21aa', 'wcag1410']), // 1.4.10 → WCAG 2.1 AA
'color-contrast': makeRule(['wcag2aa', 'wcag143']), // 1.4.3 → WCAG 2.0 AA
'avoid-inline-spacing': makeRule(['best-practice']), // Best Practice
},
},
alfa: { pagesScanned: 0, rules: {} },
};
// buildBugReports signature is (target, summary)
const reports = buildBugReports(target, summary);
const cats = reports.map((r) => r.wcag_category);
const i22 = cats.findIndex((c) => c?.startsWith('WCAG 2.2'));
const i21 = cats.findIndex((c) => c?.startsWith('WCAG 2.1'));
const i20 = cats.findIndex((c) => c?.startsWith('WCAG 2.0'));
const iBP = cats.findIndex((c) => c === 'Best Practice');
assert.ok(i22 !== -1, `WCAG 2.2 finding present — got: ${JSON.stringify(cats)}`);
assert.ok(i21 !== -1, 'WCAG 2.1 finding present');
assert.ok(i20 !== -1, 'WCAG 2.0 finding present');
assert.ok(iBP !== -1, 'Best Practice finding present');
assert.ok(i22 < i21, 'WCAG 2.2 before 2.1');
assert.ok(i21 < i20, 'WCAG 2.1 before 2.0');
assert.ok(i20 < iBP, 'WCAG 2.0 before Best Practice');
});
test('prioritizeAccessibilityBugs: keeps critical, key-page widespread, and top prevalence before the cap', () => {
const summary = {
pagesScanned: 100,
axe: { rules: { 'color-contrast': { affectedPages: [{ url: 'https://example.gov/top' }], pages: 6 } } },
alfa: { rules: {} },
deprecatedHtml: { rules: {} },
};
const bugs = [
{ instance_id: 'a', severity: 'Critical', wcag_category: 'WCAG 2.0 AA', frequency: { pages_affected: 1, total_pages_scanned: 100, instances: 1 }, engine_key: 'axe-core', rule_id: 'x', summary: 'a' },
{ instance_id: 'b', severity: 'Low', wcag_category: 'WCAG 2.0 AA', frequency: { pages_affected: 12, total_pages_scanned: 100, instances: 1 }, engine_key: 'axe-core', rule_id: 'color-contrast', summary: 'b' },
{ instance_id: 'c', severity: 'Low', wcag_category: 'WCAG 2.0 A', frequency: { pages_affected: 20, total_pages_scanned: 100, instances: 1 }, engine_key: 'axe-core', rule_id: 'y', summary: 'c' },
{ instance_id: 'd', severity: 'Low', wcag_category: 'Best Practice', frequency: { pages_affected: 30, total_pages_scanned: 100, instances: 1 }, engine_key: 'axe-core', rule_id: 'z', summary: 'd' },
];
const keyPages = ['https://example.gov/top'];
const view = prioritizeAccessibilityBugs(summary, bugs, {
keyPages,
reporting: { max_html_issues: 3, moderate_issue_threshold_percent: 5, include_key_page_issues: true },
});
assert.equal(view.visibleCount, 3, 'critical + key-page widespread + one expansion item shown before the cap');
assert.equal(view.bugs.find((b) => b.instance_id === 'a').default_visible, true);
assert.equal(view.bugs.find((b) => b.instance_id === 'b').default_visible, true);
assert.equal(view.bugs.find((b) => b.instance_id === 'c').default_visible, true, 'first expansion item is included when room remains');
assert.equal(view.bugs.find((b) => b.instance_id === 'd').default_visible, false);
});
test('buildAcrData: does-not-support when failures span ≥5% of pages', () => {
const summary = {
week: '2026-W25',
pagesScanned: 100,
axe: {
pagesScanned: 100,
rules: {
// color-contrast → 1.4.3, 10/100 = 10% → does-not-support
'color-contrast': { pages: 10, tags: ['wcag2aa', 'wcag143'], examplePages: ['https://x/a'] },
},
},
alfa: { pagesScanned: 0, rules: {} },
};
const { scMap } = buildAcrData(summary);
const sc143 = scMap.get('1.4.3');
assert.ok(sc143, '1.4.3 entry present');
assert.equal(sc143.adherence, 'does-not-support', '10% failure rate → does-not-support');
assert.equal(sc143.pagesAffected, 10);
assert.ok(sc143.engines.includes('axe-core'));
});
test('buildAcrData: partially-supports when failures span <5% of pages', () => {
const summary = {
week: '2026-W25',
pagesScanned: 100,
axe: {
pagesScanned: 100,
rules: {
// color-contrast → 1.4.3, 4/100 = 4% → partially-supports
'color-contrast': { pages: 4, tags: ['wcag2aa', 'wcag143'], examplePages: [] },
},
},
alfa: { pagesScanned: 0, rules: {} },
};
const { scMap } = buildAcrData(summary);
assert.equal(scMap.get('1.4.3').adherence, 'partially-supports');
});
test('buildAcrData: zero-page failure is partially-supports, not supports', () => {
// A rule with pages=0 still enters scFailures (pages: max(0,0)=0).
// fail.pages/axePages = 0/50 = 0% < 5% → partially-supports, not supports.
// "supports" requires the SC to be in testedSCs but absent from scFailures,
// which can only happen via Alfa's resolveWcag path when resolveWcag returns
// an SC that was not reported at all in axe rules. This is an edge case of
// the current conservative implementation.
const summary = {
week: '2026-W25',
pagesScanned: 50,
axe: {
pagesScanned: 50,
rules: {
'image-alt': { pages: 0, tags: ['wcag2a', 'wcag111'], examplePages: [] },
},
},
alfa: { pagesScanned: 0, rules: {} },
};
const { scMap } = buildAcrData(summary);
const sc111 = scMap.get('1.1.1');
assert.ok(sc111, '1.1.1 entry present');
// pages=0 → fail rate 0% < 5% → partially-supports (not "supports")
assert.equal(sc111.adherence, 'partially-supports', 'zero-page failure is partially-supports');
assert.equal(sc111.pagesAffected, 0);
});
test('buildAcrData: not-evaluated for SCs no engine covers', () => {
const summary = {
week: '2026-W25',
pagesScanned: 50,
axe: { pagesScanned: 50, rules: {} },
alfa: { pagesScanned: 0, rules: {} },
};
const { scMap } = buildAcrData(summary);
// 1.2.5 (Audio Description) is rarely covered by automated tools
const sc125 = scMap.get('1.2.5');
assert.ok(sc125, '1.2.5 entry present in catalog');
assert.equal(sc125.adherence, 'not-evaluated');
});
test('buildAcrYaml: valid YAML shape with required OpenACR fields', () => {
const summary = {
week: '2026-W25',
pagesScanned: 10,
axe: { pagesScanned: 10, version: '4.12.1', rules: {} },
alfa: { pagesScanned: 0, rules: {} },
};
const acrData = buildAcrData(summary);
const yaml = buildAcrYaml({ domain: 'example.gov' }, summary, '2026-W25', acrData);
assert.match(yaml, /^title:/, 'starts with title field');
assert.match(yaml, /catalog: 2\.5-edition-wcag-2\.2-en/, 'references WCAG 2.2 catalog');
assert.match(yaml, /chapters:/, 'has chapters section');
assert.match(yaml, /success_criteria_level_a:/, 'has Level A section');
assert.match(yaml, /success_criteria_level_aa:/, 'has Level AA section');
assert.match(yaml, /does-not-support|partially-supports|supports|not-evaluated/, 'contains adherence values');
});
test('headersToWappalyzer: produces array-valued, lowercased object shape', () => {
const out = headersToWappalyzer({ 'Content-Type': 'text/html', Server: 'AkamaiGHost' });
assert.deepEqual(out, { 'content-type': ['text/html'], server: ['AkamaiGHost'] });
// Already-array values are flattened, not nested.
assert.deepEqual(headersToWappalyzer({ 'Set-Cookie': ['a=1', 'b=2'] }), { 'set-cookie': ['a=1', 'b=2'] });
});
// Regression: Akamai-fronted sites (e.g. cms.gov) set ak_bmsc/bm_sv/bm_sz
// cookies that match a Wappalyzer fingerprint. If cookie/header values are
// bare strings instead of arrays, analyzeManyToMany throws "values.forEach
// is not a function" and crashes the whole page scan. This loads the real
// vendored Wappalyzer and asserts the shapes our tech engine produces are
// crash-free and actually detect Akamai.
test('wappalyzer: Akamai cookies/headers do not crash and are detected', () => {
const require = createRequire(import.meta.url);
const VENDOR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../vendor/wappalyzer');
const fn = new Function('module', 'exports', 'require', fs.readFileSync(path.join(VENDOR, 'wappalyzer.js'), 'utf8'));
const m = { exports: {} };
fn(m, m.exports, require);
const W = m.exports;
W.setCategories(JSON.parse(fs.readFileSync(path.join(VENDOR, 'categories.json'), 'utf8')));
let tech = {};
for (const f of fs.readdirSync(path.join(VENDOR, 'technologies'))) {
if (f.endsWith('.json')) Object.assign(tech, JSON.parse(fs.readFileSync(path.join(VENDOR, 'technologies', f), 'utf8')));
}
W.setTechnologies(tech);
// Cookies as the engine now builds them: { name -> [value] }.
const cookies = {};
for (const [k, v] of Object.entries({ ak_bmsc: 'F1', bm_sv: 'x', bm_sz: 'a' })) (cookies[k] ??= []).push(v);
const headers = headersToWappalyzer({ Server: 'AkamaiGHost' });
let detections;
assert.doesNotThrow(() => {
detections = W.resolve(W.analyze({ url: 'https://www.cms.gov', html: '<html></html>', scriptSrc: [], scripts: '', meta: {}, cookies, js: {}, headers }));
}, 'string-valued cookies/headers must not crash analyzeManyToMany');
assert.ok(detections.some((d) => /akamai/i.test(d.name)), 'Akamai detected from its cookies/headers');
});
test('tech-findings: co-occurrence dedupes findings per page', () => {
const pages = [
{ techs: ['WordPress', 'GTM'], findings: ['axe:color-contrast', 'axe:color-contrast', 'axe:image-alt'] },
{ techs: ['WordPress'], findings: ['axe:color-contrast'] },
{ techs: ['Drupal'], findings: ['axe:region'] },
];
const m = buildCooccurrence(pages);
assert.equal(m.pages, 3);
assert.equal(m.tech.WordPress, 2);
assert.equal(m.tech.GTM, 1);
// color-contrast counted once per page despite appearing twice on page 1.
assert.equal(m.finding['axe:color-contrast'], 2);
assert.equal(m.pair.WordPress['axe:color-contrast'], 2);
assert.equal(m.pair.GTM['axe:image-alt'], 1);
});
test('tech-findings: lift over- and under-representation, support guard', () => {
// 10 pages. GTM on 5 of them; every GTM page has the finding, only those do.
const pages = [];
for (let i = 0; i < 5; i++) pages.push({ techs: ['GTM'], findings: ['axe:contrast'] });
for (let i = 0; i < 5; i++) pages.push({ techs: ['Other'], findings: [] });
const m = buildCooccurrence(pages);
// P(finding|GTM)=1, P(finding)=0.5 -> lift 2.0
assert.equal(lift(m, 'GTM', 'axe:contrast', 5), 2);
// Below min support -> null.
assert.equal(lift(m, 'GTM', 'axe:contrast', 6), null, 'support guard rejects techPages < minPages');
// Unknown pair -> null.
assert.equal(lift(m, 'GTM', 'axe:missing', 1), null);
});
test('tech-findings: rankAssociations sorts by lift then support', () => {
const pages = [];
for (let i = 0; i < 6; i++) pages.push({ techs: ['A'], findings: ['axe:x'] });
for (let i = 0; i < 6; i++) pages.push({ techs: ['B'], findings: i < 3 ? ['axe:x'] : [] });
const m = buildCooccurrence(pages);
const ranked = rankAssociations(m, { minPages: 3 });
assert.ok(ranked.length >= 1);
// A (finding on all 6) has higher lift than B (finding on 3 of 6).
assert.equal(ranked[0].tech, 'A');
assert.ok(ranked[0].lift >= (ranked[1]?.lift ?? 0));
});
test('tech-findings: fleet merge counts distinct sites and scores breadth', () => {
// Each site: 6 GTM pages all with the finding + 6 other pages without it.
// Per site P(finding)=0.5, P(finding|GTM)=1 -> lift 2.0.
const mk = () => {
const pages = [];
for (let i = 0; i < 6; i++) pages.push({ techs: ['GTM'], findings: ['axe:x'] });
for (let i = 0; i < 6; i++) pages.push({ techs: ['Other'], findings: [] });
return buildCooccurrence(pages);
};
const fleet = mergeFleet([
{ domain: 'a.gov', model: mk() },
{ domain: 'b.gov', model: mk() },
{ domain: 'c.gov', model: mk() },
]);
assert.equal(fleet.techSites.GTM, 3, 'GTM seen on 3 sites');
assert.equal(fleet.pairSites.GTM['axe:x'], 3, 'pair seen on 3 sites');
const ranked = rankFleetAssociations(fleet, { minPages: 5, minSites: 2 });
assert.equal(ranked[0].tech, 'GTM');
assert.equal(ranked[0].sites, 3);
// score = lift(2.0) * sites(3) = 6.0
assert.equal(ranked[0].score, 6);
});
test('tech-findings: fleet minSites filters single-site coincidences', () => {
const pages = [];
for (let i = 0; i < 6; i++) pages.push({ techs: ['Solo'], findings: ['axe:y'] });
const fleet = mergeFleet([{ domain: 'only.gov', model: buildCooccurrence(pages) }]);
const ranked = rankFleetAssociations(fleet, { minPages: 5, minSites: 2 });
assert.equal(ranked.length, 0, 'single-site pair excluded by minSites=2');
});
test('registrableDomain: reduces to eTLD+1, handles multi-label suffixes', () => {
assert.equal(registrableDomain('www.cdc.gov'), 'cdc.gov');
assert.equal(registrableDomain('fonts.googleapis.com'), 'googleapis.com');
assert.equal(registrableDomain('a.b.c.example.com'), 'example.com');
assert.equal(registrableDomain('cms.gov'), 'cms.gov');
assert.equal(registrableDomain('service.gov.uk'), 'service.gov.uk');
assert.equal(registrableDomain('x.service.gov.uk'), 'service.gov.uk');
assert.equal(registrableDomain('localhost'), 'localhost');
assert.equal(registrableDomain(''), '');
});
test('isThirdParty: different registrable domain only', () => {
const page = 'https://www.cms.gov/page';
assert.equal(isThirdParty('https://www.googletagmanager.com/gtm.js', page), true);
assert.equal(isThirdParty('https://cdn.cms.gov/app.js', page), false, 'same-site subdomain is first party');
assert.equal(isThirdParty('https://cms.gov/a.js', page), false, 'apex of same site is first party');
assert.equal(isThirdParty('data:text/js,1', page), false, 'data URI is not third party');
assert.equal(isThirdParty('not a url', page), false, 'parse failure is not third party');
});
test('rollupThirdParty: per-vendor pages, medians, script flag, finding co-occurrence', () => {
const pages = [
{ pageUrl: 'p1', hasFindings: true, origins: [
{ origin: 'gtm.com', requests: 2, bytes: 100, scripts: 1, totalDurationMs: 50 },
{ origin: 'cdn.com', requests: 1, bytes: 400, scripts: 0, totalDurationMs: 20 },
] },
{ pageUrl: 'p2', hasFindings: false, origins: [
{ origin: 'gtm.com', requests: 4, bytes: 300, scripts: 1, totalDurationMs: 90 },
] },
{ pageUrl: 'p3', hasFindings: true, origins: [
{ origin: 'gtm.com', requests: 3, bytes: 200, scripts: 1, totalDurationMs: 70 },
] },
];
const r = rollupThirdParty(pages);
assert.equal(r.pagesScanned, 3);
const gtm = r.vendors.find((v) => v.origin === 'gtm.com');
assert.equal(gtm.pages, 3);
assert.equal(gtm.isScriptVendor, true);
assert.equal(gtm.medianBytes, 200, 'median of [100,300,200] -> 200');
assert.equal(gtm.medianRequests, 3);
assert.equal(gtm.pagesWithFindings, 2, 'gtm on p1+p3 (with findings) and p2 (without)');
// cdn.com is not a script vendor and appears on one page.
const cdn = r.vendors.find((v) => v.origin === 'cdn.com');
assert.equal(cdn.isScriptVendor, false);
assert.equal(cdn.pages, 1);
// Sorted by pages desc: gtm (3) before cdn (1).
assert.equal(r.vendors[0].origin, 'gtm.com');
});
test('buildLineManifest: JIM shape with facets, string records, data.source', () => {
const m = buildLineManifest('Median axe violations', 'Median axe violations',
[{ week: '2026-W23', value: 6 }, { week: '2026-W24', value: 3 }, { week: '2026-W25', value: null }],
{ unit: ' KB' });
assert.equal(m.datasets.length, 1);
const d = m.datasets[0];
assert.equal(d.type, 'line');
assert.equal(d.data.source, 'inline', 'data.source present (runtime throws without it)');
assert.ok(d.facets.x && d.facets.y, 'x and y facets present');
assert.equal(d.facets.y.units, 'KB', 'unit trimmed onto y facet');
// null-valued points dropped; remaining records are string-typed.
assert.equal(d.series[0].records.length, 2, 'null value filtered out');
assert.deepEqual(d.series[0].records[0], { x: '2026-W23', y: '6' });
assert.equal(typeof d.series[0].records[0].y, 'string', 'y is a string per runtime contract');
});
test('extractAudits: pulls failing non-a11y audits, tags category, skips a11y/passing', () => {
const categories = {
performance: { auditRefs: [{ id: 'unused-css-rules' }, { id: 'server-response-time' }, { id: 'speed-index' }] },
seo: { auditRefs: [{ id: 'meta-description' }] },
accessibility: { auditRefs: [{ id: 'color-contrast' }] }, // must be ignored
'agentic-browsing': { auditRefs: [{ id: 'llms-txt' }] },
};
const audits = {
'unused-css-rules': { title: 'Reduce unused CSS', score: 0.5, scoreDisplayMode: 'metricSavings',
metricSavings: { LCP: 350 }, details: { overallSavingsBytes: 177580, items: [1, 2, 3] } },
'server-response-time': { title: 'Server response', score: 1, scoreDisplayMode: 'binary' }, // passing -> skip
'speed-index': { title: 'Speed Index', score: 0.4, scoreDisplayMode: 'numeric', details: {} },
'meta-description': { title: 'No meta description', score: 0, scoreDisplayMode: 'binary', details: { items: [] } },
'color-contrast': { title: 'Contrast', score: 0, scoreDisplayMode: 'binary' }, // a11y -> skip
'llms-txt': { title: 'llms.txt follows recommendations', score: null, scoreDisplayMode: 'notApplicable' },
};
const out = extractAudits(categories, audits);
const ids = out.map((a) => a.id).sort();
// unused-css-rules: metricSavings mode with 177580 bytes -> kept as a savings opportunity.
// speed-index (numeric, 0.4) kept; meta-description (binary, 0) kept; llms-txt (agentic notApplicable) kept.
assert.ok(ids.includes('unused-css-rules'), 'metricSavings opportunity with real savings kept');
assert.ok(ids.includes('speed-index'), 'numeric failing audit kept');
assert.ok(ids.includes('meta-description'), 'binary failing audit kept');
assert.ok(ids.includes('llms-txt'), 'agentic notApplicable gap kept');
assert.ok(!ids.includes('color-contrast'), 'accessibility audit excluded');
assert.ok(!ids.includes('server-response-time'), 'passing audit excluded');
const css = out.find((a) => a.id === 'unused-css-rules');
assert.equal(css.savingsBytes, 177580, 'savings bytes captured');
assert.equal(css.savingsMs, 350, 'max metricSavings ms captured');
const seo = out.find((a) => a.id === 'meta-description');
assert.equal(seo.category, 'seo', 'category tagged from auditRefs');
});
test('assessAltText: classifies each alt-text problem', () => {
const v = (img) => assessAltText(img).verdict;
// Missing / decorative / hidden.
assert.equal(v({ isMissingAlt: true, hasAlt: false }), ALT_VERDICTS.MISSING);
assert.equal(v({ hasAlt: true, alt: '', isDecorative: true }), ALT_VERDICTS.DECORATIVE);
assert.equal(v({ hasAlt: true, alt: 'x', ariaHidden: true }), ALT_VERDICTS.DECORATIVE, 'aria-hidden wins');
assert.equal(v({ hasAlt: true, alt: 'x', rolePresentation: true }), ALT_VERDICTS.DECORATIVE);
// Filename-like.
assert.equal(v({ hasAlt: true, alt: 'hero_banner_2024.jpg' }), ALT_VERDICTS.FILENAME);
assert.equal(v({ hasAlt: true, alt: 'IMG_4821.PNG' }), ALT_VERDICTS.FILENAME);
// Redundant / meaningless phrasing.
assert.equal(v({ hasAlt: true, alt: 'Image of a senior couple smiling' }), ALT_VERDICTS.SUSPICIOUS);
assert.equal(v({ hasAlt: true, alt: 'photo' }), ALT_VERDICTS.SUSPICIOUS);
assert.equal(v({ hasAlt: true, alt: 'logo' }), ALT_VERDICTS.SUSPICIOUS);
// Too short.
assert.equal(v({ hasAlt: true, alt: '.' }), ALT_VERDICTS.SUSPICIOUS, 'bare dot is meaningless'); // '.' is in MEANINGLESS_EXACT
assert.equal(v({ hasAlt: true, alt: 'ab' }), ALT_VERDICTS.TOO_SHORT);
assert.equal(v({ hasAlt: true, alt: 'CMS' }), ALT_VERDICTS.TOO_SHORT, 'single 3-char word too short');
// Too long.
assert.equal(v({ hasAlt: true, alt: 'x '.repeat(200).trim() }), ALT_VERDICTS.TOO_LONG);
// Good.
assert.equal(v({ hasAlt: true, alt: 'Secretary signs the funding bill at a podium' }), ALT_VERDICTS.GOOD);
// A real filename inside a sentence is NOT flagged as a filename.
assert.equal(v({ hasAlt: true, alt: 'Screenshot of the report.pdf download page' }), ALT_VERDICTS.SUSPICIOUS, 'leads with "screenshot of"');
});
test('isAltProblem: GOOD and DECORATIVE are not problems', () => {
assert.equal(isAltProblem(ALT_VERDICTS.GOOD), false);
assert.equal(isAltProblem(ALT_VERDICTS.DECORATIVE), false);
assert.equal(isAltProblem(ALT_VERDICTS.FILENAME), true);
assert.equal(isAltProblem(ALT_VERDICTS.MISSING), true);
assert.equal(isAltProblem(ALT_VERDICTS.TOO_SHORT), true);
});