===== INTEGRATION TEST =====
'use strict';
const fs = require('fs');
const path = require('path');
const puppeteer = require(
'/var/www/vhosts/aiwebpageseo.com/site-audit/node_modules/puppeteer'
);
const SITE = process.env.SITE;
const BASE = process.env.BASE;
const STAGE = process.env.STAGE;
const axe = fs.readFileSync(
path.join(SITE, 'public/a11y-axe/axe.min.js'), 'utf8'
);
const spacing = fs.readFileSync(
path.join(STAGE, 'public/a11y-axe/text-spacing.js'), 'utf8'
);
const runner = fs.readFileSync(
path.join(STAGE, 'public/a11y-axe/run.js'), 'utf8'
);
const chrome =
'/var/www/vhosts/aiwebpageseo.com/.cache/puppeteer/chrome/linux-148.0.7778.97/chrome-linux64/chrome';
function fixture(restrictive) {
const css = restrictive
? ''
: 'p{word-spacing:0.2em;letter-spacing:0.15em}';
const paragraphStyle = restrictive
? ' style="word-spacing:1px!important;letter-spacing:1px!important"'
: '';
return `
Accessibility integration test
Accessibility integration test
Several words are present here to test text spacing.
`;
}
async function runCase(browser, name, restrictive) {
const page = await browser.newPage();
try {
await page.setViewport({
width: 1280,
height: 900
});
await page.setContent('');
const result = await page.evaluate(
async ({ html, axe, spacing, runner }) => {
return await new Promise((resolve, reject) => {
const frame = document.createElement('iframe');
frame.style.cssText =
'width:1280px;height:900px;border:0';
const timeout = setTimeout(() => {
window.removeEventListener('message', onMessage);
frame.remove();
reject(new Error('ACT/axe runner timed out'));
}, 25000);
function onMessage(event) {
if (
event.source !== frame.contentWindow ||
!event.data ||
event.data.a11yAxe !== 1
) {
return;
}
clearTimeout(timeout);
window.removeEventListener('message', onMessage);
frame.remove();
resolve(event.data);
}
window.addEventListener('message', onMessage);
const doc = new DOMParser().parseFromString(
html, 'text/html'
);
const scripts = [
axe,
'var module={exports:{}};' +
'var exports=module.exports;\n' +
spacing +
'\nwindow.a11yTextSpacing=module.exports;',
runner
];
for (const code of scripts) {
const script = doc.createElement('script');
script.textContent = String(code).replace(
/<\/script/gi,
'<\\/script'
);
doc.body.appendChild(script);
}
frame.srcdoc =
'' + doc.documentElement.outerHTML;
document.body.appendChild(frame);
});
},
{
html: fixture(restrictive),
axe,
spacing,
runner
}
);
if (!result.ok) {
throw new Error(
name + ': axe runner failed: ' + result.error
);
}
const act = result.actResults || [];
const violations = result.violations || [];
if (act.length !== 2) {
throw new Error(
name + ': expected two ACT results, got ' + act.length
);
}
if (act.some(x => x.outcome === 'error')) {
throw new Error(
name + ': ACT execution error: ' + JSON.stringify(act)
);
}
for (const id of ['9e45ec', '24afc2']) {
const entry = act.find(x => x.id === id);
const acceptable = restrictive
? ['failed']
: ['passed', 'inapplicable'];
if (!entry || !acceptable.includes(entry.outcome)) {
throw new Error(
name + ': ACT ' + id +
' expected ' + acceptable.join(' or ') +
', got ' + JSON.stringify(entry)
);
}
}
const actFailures = violations.filter(v =>
/^act-(9e45ec|24afc2)$/.test(v.id)
);
if (actFailures.length !== (restrictive ? 2 : 0)) {
throw new Error(
name + ': unexpected ACT violation count: ' +
actFailures.length
);
}
const axeViolations = violations.filter(v =>
!v.id.startsWith('act-')
);
if (!axeViolations.some(v => v.id === 'image-alt')) {
// image-alt is not necessarily enabled in this site's
// selected axe rule set. Report its absence, but do
// not treat it as an integration failure.
console.log(
'INFO: image-alt not included in selected axe rules'
);
}
const summary = {
name,
passed: true,
act,
actFailures: actFailures.map(v => v.id),
axeViolations: axeViolations.map(v => v.id),
axeVersion: result.version
};
console.log('\nPASS: ' + name);
console.log(JSON.stringify(summary, null, 2));
return summary;
} finally {
await page.close();
}
}
(async () => {
const browser = await puppeteer.launch({
executablePath: chrome,
headless: true,
args: [
'--no-sandbox',
'--disable-dev-shm-usage'
]
});
try {
const results = [];
results.push(
await runCase(browser, 'restrictive', true)
);
results.push(
await runCase(browser, 'normal', false)
);
const output = path.join(
BASE,
'regression/staged-integration-results.json'
);
fs.writeFileSync(
output,
JSON.stringify(results, null, 2)
);
console.log('\n=== STAGED INTEGRATION RESULTS ===');
console.log('Restrictive fixture: PASS');
console.log('Normal fixture: PASS');
console.log('ACT validators executed: YES');
console.log('Axe runner executed: YES');
console.log('Saved: ' + output);
console.log('Production changed: NO');
console.log('PM2 restarted: NO');
} finally {
await browser.close();
}
})().catch(error => {
console.error('\nTEST FAILED:');
console.error(error.stack || error);
process.exitCode = 1;
});
===== PRODUCTION RENDERER =====
async function toolReport(html, url, builtByScripts, keyboard, fetchCss, inner) {
await acquire();
let page = null, timer = null;
try {
const browser = await getBrowser();
page = await browser.newPage();
await page.setViewport({ width: 1280, height: 900 });
await page.setRequestInterception(true);
page.on('request', req => {
const u = req.url();
if (u.startsWith(AXE_URL)) {
const f = AXE_FILES[u.slice(AXE_URL.length).split(/[?#]/)[0]];
if (!f) return req.abort().catch(() => {});
return fs.promises.readFile(f).then(body => req.respond({ status: 200, contentType: 'application/javascript', body })).catch(() => req.abort().catch(() => {}));
}
if (/^(file|data|about|blob):/i.test(u)) {
// the only file Chrome may open is the tool page itself
if (/^file:/i.test(u) && u.split('#')[0].split('?')[0] !== TOOL) return req.abort().catch(() => {});
return req.continue().catch(() => {});
}
let host;
try { const p = new URL(u); if (!/^https?:$/.test(p.protocol)) return req.abort().catch(() => {}); host = p.hostname; } catch (e) { return req.abort().catch(() => {}); }
if (BLOCK_HOSTS.test(host)) return req.abort().catch(() => {});
publicHost(host).then(ok => (ok ? req.continue() : req.abort())).catch(() => req.abort().catch(() => {}));
});
await page.exposeFunction('__a11yFetch', async (u, ref) => {
try {
const p = new URL(u);
if (!/^https?:$/.test(p.protocol) || !(await publicHost(p.hostname))) return { ok: false, html: '', error: 'blocked' };
return await fetchCss(u, ref || null);
} catch (e) { return { ok: false, html: '', error: String(e && e.message || e) }; }
});
await page.goto(TOOL, { waitUntil: 'domcontentloaded', timeout: 20000 });
await page.addStyleTag({ path: TOOL_CSS }).catch(() => {});
await page.addScriptTag({ path: TOOL_JS });
// axe-core and its runner, from this server's own copy, for the tool page's extra WCAG checks (see a11yAxe).
let axeSrc = null;
try { axeSrc = { axe: await fs.promises.readFile(AXE_FILES['axe.min.js'], 'utf8'), run: await fs.promises.readFile(AXE_FILES['run.js'], 'utf8'), spacing: await fs.promises.readFile(path.join(__dirname, '../public/a11y-axe/text-spacing.js'), 'utf8') }; } catch (e) { axeSrc = null; }
const run = page.evaluate(async (h, base, byScripts, axeSrc, kb, inner) => {
if (axeSrc) window.__a11yAxeSrc = axeSrc;
window.a11yKeyboard = kb || null; window.a11yKeyboardP = null; window.a11yInner = inner || null; window.a11yInnerFixed = true;
window.fetchRawHtml = (u, r) => window.__a11yFetch(u, r || null);
window.a11yBuiltByScripts = byScripts;
window.a11yLastReport = null;
document.getElementById('a11y-input').value = h;
checkA11y(base);
const until = Date.now() + 58000;
while (!window.a11yLastReport && Date.now() < until) await new Promise(r => setTimeout(r, 100));
return window.a11yLastReport;
}, html, url || '', builtByScripts, axeSrc, keyboard, inner || null);
const report = await Promise.race([run, new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('rendered accessibility check timed out')), TIMEOUT_MS); })]);
if (!report || !Array.isArray(report.issues)) throw new Error('rendered accessibility check returned no report');
report.rendered = true;
return report;
} finally {
if (timer) clearTimeout(timer);
if (page) await page.close().catch(() => {});
release();
}
}
// The two inner pages, each loaded as visitors see it and given the full report on its own. A page that cannot be
// loaded that way (error status, bot-check, almost empty, a different site after redirects) is left out.
// onProgress (optional) is called while they run, so a caller streaming results can show the audit is still working.
async function innerReports(homeUrl, urls, fetchCss, onProgress) {
const pages = [];
let homeHost = ''; try { homeHost = new URL(homeUrl).hostname.replace(/^www\./, ''); } catch (e) { return pages; }
const tick = onProgress ? setInterval(() => { try { onProgress(); } catch (e) {} }, 20000) : null;
try {
// up to 4 menu links are tried, until 2 pages are checked; a link that ends up on a login page after its redirects
// (HSBC's "log on" link lands on /auth/?returnUrl=) is not a page to audit
const SKIPRE = /\/(cart|basket|checkout|login|log-in|logon|signin|sign-in|logout|auth|oauth|sso|account|my-account|register|signup|sign-up|search|wp-admin|wp-login\.php|admin)(\/|\?|$)|[?&](returnurl|redirect|redirect_uri)=/i;
for (const u of (urls || []).slice(0, 4)) {
if (pages.length >= 2) break;
try {
if (new URL(u).hostname.replace(/^www\./, '') !== homeHost) continue;
const r = await renderWithScripts(u, { keyboard: true });
if (!r.ok || (r.status && (r.status < 200 || r.status >= 300)) || nearlyEmpty(r.html) || botCheck(r.html)) continue;
if (new URL(r.finalUrl || u).hostname.replace(/^www\./, '') !== homeHost || SKIPRE.test(r.finalUrl || '')) continue;
const rep = await toolReport(r.html, r.finalUrl || u, true, r.keyboard || null, fetchCss, null);
pages.push({ url: r.finalUrl || u, issues: rep.issues, errorCount: rep.errorCount, warnCount: rep.warnCount, site: r.site || null });
} catch (e) {}
}
} finally { if (tick) clearInterval(tick); }
return pages;
}
async function runRenderedAccessibilityCheck(html, url, fetchCss, onProgress) {
if (!html || typeof html !== 'string') throw new Error('html required');
// Standalone SVG is not an HTML page. Do not run HTML-only checks.
if (/^\s*(?:<\?xml[^>]*>\s*)?(?:]*>\s*)?