===== REPORTING TEST SOURCE =====
'use strict';
const fs = require('fs');
const path = require('path');
const puppeteer = require(
'/var/www/vhosts/aiwebpageseo.com/site-audit/node_modules/puppeteer'
);
const audit = fs.readFileSync(
path.join(process.env.STAGE, 'public/accessibility-audit.js'),
'utf8'
);
const chrome =
'/var/www/vhosts/aiwebpageseo.com/.cache/puppeteer/chrome/linux-148.0.7778.97/chrome-linux64/chrome';
const scenarios = [
{
name: 'overlapping-axe-and-act',
axe: true,
act: [
{ id: '9e45ec', outcome: 'failed', count: 1 },
{ id: '24afc2', outcome: 'failed', count: 1 }
],
expectedActIssues: 0,
expectedNotes: 2
},
{
name: 'act-only',
axe: false,
act: [
{ id: '9e45ec', outcome: 'failed', count: 1 },
{ id: '24afc2', outcome: 'failed', count: 1 }
],
expectedActIssues: 2,
expectedNotes: 0
},
{
name: 'act-execution-error',
axe: false,
act: [
{ id: '9e45ec', outcome: 'error', error: 'test failure' },
{ id: '24afc2', outcome: 'inapplicable', count: 0 }
],
expectedActIssues: 0,
expectedNotes: 1
}
];
(async () => {
const browser = await puppeteer.launch({
executablePath: chrome,
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage']
});
try {
const results = [];
for (const scenario of scenarios) {
const page = await browser.newPage();
try {
await page.setContent(`
ACT reporting test
`);
await page.evaluate(source => {
window.a11yQueueRelease = function () {};
window.a11yBuiltByScripts = false;
window.a11yKeyboard = null;
window.a11yKeyboardP = null;
window.a11yInner = null;
window.a11yInnerFixed = true;
window.SEOGate = { check() {}, close() {} };
(0, eval)(source);
}, audit);
const result = await page.evaluate(scenario => {
const violations = [];
if (scenario.axe) {
violations.push({
id: 'avoid-inline-spacing',
impact: 'serious',
help: 'Inline text spacing must be adjustable',
description: 'Restrictive inline spacing detected',
tags: ['wcag1412'],
n: 1,
bp: false,
ex: []
});
}
for (const item of scenario.act) {
if (item.outcome !== 'failed') continue;
violations.push({
id: 'act-' + item.id,
impact: 'serious',
help: 'Restrictive text spacing',
description: 'ACT spacing failure',
tags: ['wcag1412'],
n: item.count,
bp: false,
ex: []
});
}
checkA11yReport({
status: 'skipped',
reason: 'controlled reporting test',
axe: {
ok: true,
version: '4.10.2',
violations,
actResults: scenario.act
}
});
return window.a11yLastReport;
}, scenario);
if (!result || !Array.isArray(result.issues)) {
throw Error(scenario.name + ': no final report');
}
const actIssues = result.issues.filter(i =>
/^act-(9e45ec|24afc2)$/.test(i.axe || '')
);
const actNotes = result.notes.filter(n =>
/Additional ACT evidence|ACT .*check could not complete/.test(n)
);
if (actIssues.length !== scenario.expectedActIssues) {
throw Error(
scenario.name + ': expected ' +
scenario.expectedActIssues +
' ACT issues, got ' + actIssues.length
);
}
if (actNotes.length !== scenario.expectedNotes) {
throw Error(
scenario.name + ': expected ' +
scenario.expectedNotes +
' ACT notes, got ' + actNotes.length
);
}
results.push({
name: scenario.name,
passed: true,
actIssueCount: actIssues.length,
actNoteCount: actNotes.length,
score: result.score
});
console.log('PASS: ' + scenario.name);
} finally {
await page.close();
}
}
const output = path.join(
process.env.BASE,
'regression/act-reporting-results.json'
);
fs.writeFileSync(output, JSON.stringify(results, null, 2));
console.log('\n=== REPORTING TEST RESULTS ===');
console.log(JSON.stringify(results, null, 2));
console.log('Saved: ' + output);
console.log('Production changed: NO');
console.log('PM2 restarted: NO');
} finally {
await browser.close();
}
})().catch(error => {
console.error('TEST FAILED:', error.stack || error);
process.exitCode = 1;
});
===== PRODUCTION REPORT CREATION =====
const autoplayMedia = [...html.matchAll(/<(video|audio)[^>]*autoplay[^>]*>/gi)];
const autoplayNoMute = autoplayMedia.filter(m => !/muted/i.test(m[0]) && !/data-a11y-sound="no"/i.test(m[0]));
for (const m of html.matchAll(/<(video|audio)[^>]*data-a11y-playing-sound="yes"[^>]*>/gi)) if (!/autoplay/i.test(m[0]) && !/\scontrols[\s>=]/i.test(m[0])) autoplayNoMute.push(m);
if (autoplayNoMute.length) issues.push({type:'error',label:`${autoplayNoMute.length} auto-playing media element${autoplayNoMute.length>1?'s':''} without mute`,desc:'Audio or video that plays sound by itself as the page opens talks over screen readers and is disorienting; sound that plays for more than 3 seconds needs a way to pause or stop it (WCAG 1.4.2 Audio Control, Level A).',fix:'Add the muted attribute to auto-playing video, or remove autoplay entirely.'});
// Extra WCAG checks by axe-core (see a11yAxe): each failed rule is one issue. Serious and critical failures
// count as errors, moderate and minor ones as warnings. The examples quote the page's own markup.
const a11yAx = contrast && contrast.axe;
if (a11yAx && a11yAx.ok) {
const wcagOf = tags => (tags || []).map(t => { const m = /^wcag(\d)(\d)(\d+)$/.exec(t); return m ? m[1] + '.' + m[2] + '.' + m[3] : ''; }).filter(Boolean);
// ACT spacing outcomes are preserved separately from axe-core.
// When axe already reports spacing, retain the ACT evidence
// without adding duplicate scored issues.
const actSpacing = Array.isArray(a11yAx.actResults)
? a11yAx.actResults : [];
for (const result of actSpacing) {
const name = result.id === '9e45ec'
? 'word spacing'
: result.id === '24afc2'
? 'letter spacing'
: 'text spacing';
if (result.outcome === 'error' || result.outcome === 'unavailable') {
notes.push('ACT ' + name +
' check could not complete: ' +
String(result.error || 'unknown error'));
}
}
const axeHasSpacing = a11yAx.violations.some(v =>
v.id === 'avoid-inline-spacing'
);
const a11yAxWcag = a11yAx.violations.filter(v =>
!v.bp &&
!(axeHasSpacing && /^act-(9e45ec|24afc2)$/.test(v.id))
);
if (axeHasSpacing) {
for (const result of actSpacing) {
if (result.outcome === 'failed') {
notes.push(
'Additional ACT evidence: ' + result.id +
' detected restrictive ' +
(result.id === '9e45ec'
? 'word spacing'
: 'letter spacing') +
'. Included under the existing axe spacing issue.'
);
}
}
}
// Best-practice failures (not WCAG requirements) are listed as notes and never lower the score.
for (const v of a11yAx.violations.filter(v => v.bp)) {
const ex = (v.ex || []).map(x => a11yCut(x.html, 120)).join('; ');
notes.push(`Best practice, not a WCAG requirement (not scored): ${v.n} element${v.n > 1 ? 's' : ''}: ${a11yCut(v.help, 110)}.${ex ? ' Examples: ' + ex : ''}`);
}
for (const v of a11yAxWcag) {
const sc = wcagOf(v.tags);
const ex = (v.ex || []).map(x => a11yCut(x.html, 120) + (x.fix ? ' (' + a11yCut(String(x.fix).replace(/^Fix (any|all) of the following:\s*/i, '').replace(/\s+/g, ' '), 140) + ')' : '')).join('; ');
issues.push({ type: /^(critical|serious)$/.test(v.impact || '') ? 'error' : 'warning',
label: `${v.n} element${v.n > 1 ? 's' : ''}: ${a11yCut(v.help, 110)}${sc.length ? ' (WCAG ' + sc.join(', ') + ')' : ''}`,
desc: String(v.description || '').replace(/\.?\s*$/, '.') + ' Checked with axe-core.' + (ex ? ' Examples: ' + ex : ''),
fix: 'Fix each element listed: ' + a11yCut(String(v.help || ''), 200) + '.', axe: v.id });
}
if (!a11yAxWcag.length) passes.push('No axe-core WCAG violations reported for the rules it tested');
} else if (a11yAx) notes.push('The extra WCAG checks (ARIA, list structure, zoom, page title and others) could not run on this page: ' + (a11yAx.error || 'unknown reason') + '.');
// Inner pages (window.a11yInner, from the server: two pages from the site's menu, each given this same report). A
// problem already reported for this page is not repeated; a problem found only on an inner page is added, marked
// with that page. The menu and the help ways are compared across the pages (WCAG 3.2.3, 3.2.6).
const inn = window.a11yInner;
let a11yNavChecked = 0, a11yHelpChecked = 0;
if (inn && inn.pages) {
const stem = l => String(l || '').replace(/\d+/g, '#').toLowerCase();
const have = new Set(issues.map(i => stem(i.label)));
const pathOf = u => { try { const x = new URL(u); return (x.pathname + x.search) || '/'; } catch (e) { return String(u); } };
for (const pg of inn.pages) for (const i of (pg.issues || [])) { const k = stem(i.label); if (have.has(k)) continue; have.add(k); issues.push(Object.assign({}, i, { label: i.label + ' (on ' + pathOf(pg.url) + ')', page: pg.url })); }
const hs = inn.home || {};
// Consistent navigation (3.2.3): menu links found on both pages must come in the same order
const navBad = [];
let navChecked = 0;
// each menu is compared with the same menu on the other page: the menu there sharing most of its links (at least
// half of the smaller one), not a section's own sub-menu
for (const pg of inn.pages) { const ps = pg.site; if (!ps) continue;
const hb = hs.blocks || (hs.nav ? [hs.nav] : []), pb = ps.blocks || (ps.nav ? [ps.nav] : []);
let bad = '';
for (const A0 of hb) { const aSet = new Set(A0.map(n => n.h));
let best = null, bestN = 0; for (const B0 of pb) { const n = B0.filter(x => aSet.has(x.h)).length; if (n > bestN) { bestN = n; best = B0; } }
if (!best || bestN < 3 || bestN < 0.5 * Math.min(A0.length, best.length)) continue; navChecked++;
const bSet = new Set(best.map(n => n.h)), A = A0.filter(n => bSet.has(n.h)).map(n => n.h), inOrder = best.filter(n => aSet.has(n.h)).map(n => n.h);
const at = A.findIndex((h, j) => h !== inOrder[j]);
if (at >= 0 && !bad) { const nm = h => { const n = A0.find(x => x.h === h) || best.find(x => x.h === h); return n && n.t ? n.t : pathOf(h); }; bad = pathOf(pg.url) + ' ("' + a11yCut(nm(inOrder[at]), 30) + '" where the home page has "' + a11yCut(nm(A[at]), 30) + '")'; } }
if (bad) navBad.push(bad); }
if (navBad.length) issues.push({type:'error',label:`Menu links are in a different order on ${navBad.length} other page${navBad.length > 1 ? 's' : ''}`,desc:`The menu repeated on every page must keep its links in the same order, so visitors who rely on finding things where they were before (including screen reader and screen magnifier users) do not get lost (WCAG 3.2.3 Consistent Navigation, Level AA). Different order on: ${navBad.join('; ')}`,fix:'Use one shared menu for every page, with its links always in the same order. Extra links on some pages are fine; changing the order of the shared ones is not.'});
else if (navChecked) passes.push('The menu keeps its links in the same order on the other pages checked');
a11yNavChecked = navChecked;
// Consistent help (3.2.6): a help way shown on two pages must be in the same place (header, footer, main, fixed)
const helpBad = [];
let helpChecked = 0;
const NAMES = { phone: 'phone number', email: 'email link', contact: 'contact link', chat: 'chat', help: 'help link' };
const WHERE = { header: 'the header', footer: 'the footer', main: 'the page content', menu: 'the menu', fixed: 'a box fixed on screen' };
for (const pg of inn.pages) { const ps = pg.site; if (!ps || !hs.help || !ps.help) continue;
for (const kind of Object.keys(NAMES)) { const a = hs.help.filter(h => h.kind === kind).map(h => h.where), b = ps.help.filter(h => h.kind === kind).map(h => h.where); if (!a.length || !b.length) continue; helpChecked++;
if (!a.some(w => b.includes(w))) helpBad.push(`${NAMES[kind]} in ${WHERE[a[0]] || a[0]} on the home page but in ${WHERE[b[0]] || b[0]} on ${pathOf(pg.url)}`); } }
if (helpBad.length) issues.push({type:'error',label:`Help is in a different place on ${helpBad.length > 1 ? 'other pages' : 'another page'}`,desc:`Ways to get help (contact details, a help page, chat) that appear on several pages must stay in the same place relative to the rest of the page, so people who need help can find it again (WCAG 3.2.6 Consistent Help, Level A). Moved: ${helpBad.slice(0, 4).join('; ')}`,fix:'Keep contact details, help links and chat in the same place on every page, for example always in the header or always in the footer.'});
else if (helpChecked) passes.push('Help links stay in the same place on the other pages checked');
a11yHelpChecked = helpChecked;
if (inn.pages.length) notes.push(`Also checked ${inn.pages.length} other page${inn.pages.length > 1 ? 's' : ''} of the site: ${inn.pages.map(pg => pathOf(pg.url) + ' (' + (pg.errorCount || 0) + ' error' + ((pg.errorCount || 0) === 1 ? '' : 's') + ', ' + (pg.warnCount || 0) + ' warning' + ((pg.warnCount || 0) === 1 ? '' : 's') + ')').join(', ')}. Problems found only there are listed above with the page they are on.`);
else if (inn.tried) notes.push('Other pages of the site could not be checked: they could not be loaded as visitors see them.');
}
// WCAG 2.2 coverage (Level A and AA): every criterion, with what this report found for it. A criterion fails when a
// result above names it; one the automatic checks cover passes when none does (most can also need a person to check
// part of it); the rest need a person. One whose check could not run on this page is shown as not checked.
const a11yCoverage = (() => {
const kbc = window.a11yKeyboard, kbOk = !!(kbc && kbc.checked), lay2 = kbc && kbc.layout || {};
const cOk = !!(contrast && contrast.status === 'ok'), axOk = !!(contrast && contrast.axe && contrast.axe.ok);
// [criterion, level, name, how the checks cover it ('auto' all of it, 'part' some of it, '' none), whether it ran, what a person should still check]
const L = [
===== PRODUCTION FILE HASHES =====
88422ce140ee30ffa1c972ff306e2595a7dff6408be0b64cf9567148b4230b60 /var/www/vhosts/aiwebpageseo.com/site-audit/public/accessibility-audit.js
88422ce140ee30ffa1c972ff306e2595a7dff6408be0b64cf9567148b4230b60 /var/www/vhosts/aiwebpageseo.com/site-audit/public/accessibility-audit.js
bb62d2c9e747507641b676c5faf26b9794905acb39129d736356c4b5501d9958 /var/www/vhosts/aiwebpageseo.com/site-audit/public/a11y-axe/run.js
a63abfcdc28fab7516d895933e0f2457411d0d922d9eb942a06d2e71fad039f9 /var/www/vhosts/aiwebpageseo.com/site-audit/public/a11y-axe/text-spacing.js