=== FAILING SOURCE LOCATION ===
2110 ov.classList.add('open');
2111 }
2112
2113 function close() { document.getElementById('seo-gate-overlay').classList.remove('open'); }
2114
2115 function updateNavBar() {
2116 const nav = document.getElementById('seo-user-nav'); if (!nav) return;
2117 if (_user) {
2118 const bal = String.fromCharCode(163) + (_credits / 100).toFixed(2);
2119 nav.innerHTML = '' + _user.name + ' - ' + bal + ' creditsDashboardSign out';
2120 } else {
2121 nav.innerHTML = 'Sign in / Register';
2122 }
2123 }
2124
2125 async function logout() {
2126 await fetch(BACKEND_URL + '/api/auth/logout', { method: 'POST', credentials: 'include' });
2127 window.location.reload();
2128 }
2129
2130 document.getElementById('seo-gate-overlay').addEventListener('click', function(e) { if (e.target === this) close(); });
2131 init();
2132 function setCredits(pence) { if (pence !== null && pence !== undefined) { _credits = pence; updateNavBar(); refreshPriceDisplay(); } }
2133 async function chargeOnSuccess(toolKey) {
2134 try {
2135 const dr = await fetch(BACKEND_URL + '/api/tools/deduct', {
2136 method: 'POST', headers: { 'Content-Type': 'application/json' },
2137 credentials: 'include', body: JSON.stringify({ tool: toolKey }),
2138 });
2139 const dd = await dr.json();
2140 if (dd.balance_pence != null) { _credits = dd.balance_pence; updateNavBar(); refreshPriceDisplay(); }
2141 return dd;
2142 } catch (e) { return null; }
2143 }
2144
2145 return { check, chargeOnSuccess, close, logout, getUser:()=>_user, getCredits:()=>_credits, setCredits, getPrice:(key)=>_prices[key]??null, showPopup };
2146 })();
2147
2148 // Master list of tools currently free-to-try without an account.
2149 var FREE_TOOLS = [
2150 { key: 'llms_audit', name: 'LLMs.txt Auditor', href: 'content-tools.html#llms' },
=== ALL EVENT LISTENER ELEMENTS ===
13: const target = document.getElementById(prefix + '-' + idx);
79: const box = document.getElementById('a11y-results'), min = Math.max(1, Math.round((Number(m.waitS) || 0) / 60));
101: var btn = document.getElementById('a11y-run-btn');
111: var el = document.getElementById('a11y-url');
112: var status = document.getElementById('a11y-url-status');
167: return !doc.querySelector('parsererror') &&
174: const url = document.getElementById('a11y-url').value.trim();
177: document.getElementById('a11y-results').innerHTML = `
`;
180: const box = document.getElementById('a11y-results');
182: box.querySelector('.card > div').textContent = title;
183: box.querySelector('.card > div + div').textContent = body;
224: probe.querySelectorAll('script, style, noscript, template').forEach(n => n.remove());
225: return { words: ((probe.body && probe.body.textContent) || '').trim().split(/\s+/).filter(Boolean).length, links: probe.querySelectorAll('a[href]').length };
240: document.getElementById('a11y-results').innerHTML = `Loading the page with its scripts running, as your visitors see it…
`;
256: document.getElementById('a11y-input').value = d.html;
361: return ids.split(/\s+/).map(id => { const t = d.getElementById(id); return t ? ((t.getAttribute('aria-label') || '').trim() || (t.textContent || '').replace(/\s+/g, ' ').trim()) : ''; }).filter(Boolean).join(' ').trim();
397: const modals = [...doc.querySelectorAll('[aria-modal="true" i]')].filter(d => !d.closest('[aria-hidden="true" i]') && (!win || (d.getClientRects().length && win.getComputedStyle(d).visibility === 'visible')));
401: for (const el of doc.querySelectorAll(A11Y_FOCUSABLE)) {
432: for (const el of doc.querySelectorAll('iframe')) {
470: while ((t = doc.querySelector('template[shadowrootmode]')) && guard++ < 5000) t.replaceWith(t.content);
501: const sheets = [...doc.querySelectorAll('link[rel~="stylesheet"][href]')].filter(l => (l.getAttribute('href') || '').trim());
530: if (!mediaCache.has(a)) mediaCache.set(a, [...a.querySelectorAll('img, video, canvas, picture, svg, iframe, object, embed')]);
639: if (!paintedCache.has(a)) paintedCache.set(a, [...a.querySelectorAll('*')].filter(paintsFill));
818: const a11yRoots = [doc.body]; { const w = n => { for (const e of n.querySelectorAll('*')) if (e.shadowRoot) { a11yRoots.push(e.shadowRoot); w(e.shadowRoot); } }; w(doc); }
868: for (const f of doc.body.querySelectorAll('input, textarea, select')) {
928: const ctls = doc.body.querySelectorAll('button, input:not([type="hidden"]), select, textarea, summary, [role="button"], [role="tab"], [role="checkbox"], [role="radio"], [role="switch"], a[href]');
951: if ([...c.querySelectorAll('svg, img, canvas')].some(g => { const r = g.getBoundingClientRect(); return r.width * r.height >= ca * 0.5 && win.getComputedStyle(g).display !== 'none'; })) continue;
952: if ([...c.querySelectorAll('*')].some(g => { const gs = win.getComputedStyle(g); if (!gs.backgroundImage || gs.backgroundImage === 'none' || gs.display === 'none' || gs.visibility !== 'visible') return false; const r = g.getBoundingClientRect(); return r.width * r.height >= ca * 0.5; })) continue; }
973: const hid = new Set(c.querySelectorAll('[aria-hidden="true"], .sr-only, .visually-hidden, .screen-reader-text'));
975: const imgAlt = [...c.querySelectorAll('img[alt]')].filter(shown).map(i => (i.getAttribute('alt') || '').trim()).filter(Boolean).join(' ');
976: const svgTitle = (([...c.querySelectorAll('svg title')].filter(shown)[0] || {}).textContent || '').trim();
1066: d.querySelectorAll('script, noscript, meta[http-equiv], object, embed, applet, frame, frameset, portal, animate, set, animateMotion, animateTransform, base').forEach(n => n.remove());
1067: d.querySelectorAll('iframe').forEach(f => { f.removeAttribute('src'); f.removeAttribute('srcdoc'); });
1068: for (const el of d.querySelectorAll('*')) {
1099: window.addEventListener('message', onMsg);
1157: const b = d.querySelector('base[href]');
1232: d.querySelectorAll('link[rel~="stylesheet"][href]').forEach(l => {
1244: d.querySelectorAll('style:not([data-a11y-from])').forEach(st => {
1254: const html = document.getElementById('a11y-input').value.trim();
1257: const box = document.getElementById('a11y-results');
1265: const el = document.getElementById('a11y-results');
1272: const box = el.querySelector('.card div + div'); if (box) box.textContent = 'Checking keyboard focus…';
1278: const nIn = Math.min(2, inner.length), box = el.querySelector('.card div + div'); if (box) box.textContent = 'Checking ' + nIn + ' more page' + (nIn > 1 ? 's' : '') + ' of the site…';
1296: const raw = document.getElementById('a11y-input').value.trim();
1299: const el = document.getElementById('a11y-results');
1344: const a11yImgAll = [...doc.querySelectorAll('img')];
1377: if (!a11yLabelRead) a11yLabelRead = a11yReadName('label', [...d.querySelectorAll('label')]);
1382: if (id && [...d.querySelectorAll('label[for]')].some(l => l.getAttribute('for') === id && says(l))) return true;
1387: const a11yFields = d => [...d.querySelectorAll('input, textarea, select')].filter(el =>
1391: const a11yClickOnlyLinks = d => [...d.querySelectorAll('a:not([href])')].filter(a =>
1396: const links = [...doc.querySelectorAll('a')];
1399: const a11yInnerName = el => [...el.querySelectorAll('[aria-label], [aria-labelledby], img[alt]')].some(n => !n.closest('[aria-hidden="true" i]') &&
1424: const a11yFieldOn = a11yOnPage('field', [...doc.querySelectorAll('input, textarea, select')]);
1440: const scriptMarkup = [...doc.querySelectorAll('script:not([src])')]
1457: const buttons = [...doc.querySelectorAll('button')];
1472: const a11yHAll = [...doc.querySelectorAll('h1, h2, h3, h4, h5, h6')];
1477: const a11yModalList = [...doc.querySelectorAll('[aria-modal="true" i]')], a11yModalOn = a11yOnPage('modal', a11yModalList);
1482: const a11yMainEl = doc.querySelector('main, [role="main" i]');
1579: const a11yTabAll = [...doc.querySelectorAll('[tabindex]')];
1591: const a11yLabelled = [...doc.querySelectorAll(A11Y_LABELLED)];
1598: c.querySelectorAll('[aria-hidden="true"], svg, [role="tooltip"], .sr-only, .visually-hidden, .screen-reader-text').forEach(n => n.remove());
1623: } else if (doc.querySelector('[aria-label]')) passes.push('aria-labels match the visible text of their controls');
1626: const videos = [...doc.querySelectorAll('video')];
1630: const noCaptions = videos.filter(v => v.getAttribute('data-a11y-sound') !== 'no' && canHear(v) && !v.querySelector('track[kind="captions" i], track[kind="subtitles" i]'));
1634: if (!noCaptions.length && videos.length) passes.push(videos.some(v => v.querySelector('track[kind="captions" i], track[kind="subtitles" i]')) ? 'Videos that can be heard have a captions track' : 'No video with sound that can be heard (no captions needed)');
1642: const words = f => { const id = f.getAttribute('id'); const lab = (id ? [...doc.querySelectorAll('label[for]')].filter(l => l.getAttribute('for') === id).map(l => l.textContent).join(' ') : '') + ' ' + (f.closest('label') ? f.closest('label').textContent : '');
1645: for (const f of doc.querySelectorAll('input, textarea')) {
1659: const a11ySvgAll = [...doc.querySelectorAll('svg')];
1663: !(s.getAttribute('aria-label') || '').trim() && !a11yRefText(s) && !s.querySelector(':scope > title'));
1668: const newTab = [...doc.querySelectorAll('a[target]')].filter(a => /^_blank$/i.test(a.getAttribute('target') || ''));
1689: const c = [window.a11yBaseUrl, (doc.querySelector('base[href]') || {}).href ? doc.querySelector('base[href]').getAttribute('href') : '', (doc.querySelector('link[rel~="canonical"]') || { getAttribute: () => '' }).getAttribute('href'), (doc.querySelector('meta[property="og:url"]') || { getAttribute: () => '' }).getAttribute('content')];
1729: const presTags = [...doc.querySelectorAll('b, i')].filter(el => a11yText(el) && !el.closest('[aria-hidden="true"]') && !(el.tagName === 'I' && iconClass.test(el.getAttribute('class') || '')));
1758: const mwHost = (() => { try { const c = doc.querySelector('link[rel="canonical" i]'); return new URL(window.a11yBaseUrl || (c && c.getAttribute('href')) || '').hostname.replace(/^www\./i, '').toLowerCase(); } catch (e) { return ''; } })();
1761: const hasSearch = !![...doc.querySelectorAll('input[type="search" i], [role="search" i], search')].filter(notConsent).length || [...doc.querySelectorAll('form')].filter(notConsent).some(f => SRCH.test(f.getAttribute('action') || '') || !!f.querySelector('input[name="q"], input[name="s"], input[name="search" i], input[name="query" i], input[name="keyword" i], input[name="keywords" i], input[name="term" i]'))
1762: || [...doc.querySelectorAll('input:not([type]), input[type="text" i]')].filter(notConsent).some(i => SRCH.test((i.getAttribute('placeholder') || '') + ' ' + (i.getAttribute('aria-label') || '') + ' ' + (i.getAttribute('name') || '') + ' ' + (i.getAttribute('id') || '')))
1763: || [...doc.querySelectorAll('button, [role="button"], a[href], input[type="submit"], input[type="button"]')].filter(notConsent).filter(c => !c.matches('a[href]') || mwSame(c.getAttribute('href'))).some(c => SRCH.test((c.getAttribute('aria-label') || '') + ' ' + (c.getAttribute('title') || '') + ' ' + (c.textContent || '').slice(0, 40) + ' ' + (c.getAttribute('value') || '')) || /[?&](search|s|q|query)=|\/search(\/|\?|$)/i.test(c.getAttribute('href') || ''));
1764: const hasMap = [...doc.querySelectorAll('a[href]')].filter(l => mwSame(l.getAttribute('href'))).some(l => (/site-?map|site-?index/i.test(l.getAttribute('href') || '') && !/\.xml(\b|$)/i.test(l.getAttribute('href') || '')) || /\b(site ?map|site index|a ?(to|-) ?z)\b/i.test(l.textContent || ''));
1983: document.getElementById('a11y-input').value = `
2018: document.querySelectorAll('.tool-cost-indicator[data-tool]').forEach(el => {
2070: const ov = document.getElementById('seo-gate-overlay');
2071: const icon = document.getElementById('seo-gate-icon');
2072: const title = document.getElementById('seo-gate-title');
2073: const desc = document.getElementById('seo-gate-desc');
2074: const cta1 = document.getElementById('seo-gate-cta1');
2075: const cta2 = document.getElementById('seo-gate-cta2');
2076: const limitBar = document.getElementById('seo-gate-limit-bar'); limitBar.style.display = 'none';
2113: function close() { document.getElementById('seo-gate-overlay').classList.remove('open'); }
2116: const nav = document.getElementById('seo-user-nav'); if (!nav) return;
2130: document.getElementById('seo-gate-overlay').addEventListener('click', function(e) { if (e.target === this) close(); });
2199: var b = document.getElementById('theme-toggle');
2203: var btn = document.getElementById('theme-toggle');
2204: if (btn) btn.addEventListener('click', function(){
2216: function on(sel, ev, fn) { document.querySelectorAll(sel).forEach(function (el) { el.addEventListener(ev, fn); }); }
2225: if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire); else wire();
2240: var el = document.getElementById(id);
2249: el.addEventListener('input', function () {
2253: el.addEventListener('change', run); // fires when a phone keyboard is dismissed
2254: el.addEventListener('keydown', function (ev) {
2259: if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
2275: var el = id && document.getElementById(id);
=== REPORT FUNCTION DEFINITION ===
1292:function checkA11yReport(contrast) {
1929: window.a11yLastReport = { issues: issues.map(i => Object.assign({}, i)), passes: passes.slice(), score: score, errorCount: errorCount, warnCount: warnCount, passCount: passes.length, notes: (typeof notes !== 'undefined' ? notes.slice() : []), coverage: a11yCoverage, contrast: contrast ? contrast.status : null };
=== REPORT HANDLING SECTION ===
1760 const notConsent = e => !e.closest('[id*="onetrust" i], [class*="onetrust" i], [id*="cookie" i], [class*="cookie" i], [id*="consent" i], [class*="consent" i], [id*="cmp" i], [class*="ot-sdk" i], [class*="ot-pc" i]') && !/vendor|cookie/i.test((e.getAttribute('placeholder') || '') + ' ' + (e.getAttribute('aria-label') || '') + ' ' + (e.getAttribute('name') || '') + ' ' + (e.getAttribute('id') || ''));
1761 const hasSearch = !![...doc.querySelectorAll('input[type="search" i], [role="search" i], search')].filter(notConsent).length || [...doc.querySelectorAll('form')].filter(notConsent).some(f => SRCH.test(f.getAttribute('action') || '') || !!f.querySelector('input[name="q"], input[name="s"], input[name="search" i], input[name="query" i], input[name="keyword" i], input[name="keywords" i], input[name="term" i]'))
1762 || [...doc.querySelectorAll('input:not([type]), input[type="text" i]')].filter(notConsent).some(i => SRCH.test((i.getAttribute('placeholder') || '') + ' ' + (i.getAttribute('aria-label') || '') + ' ' + (i.getAttribute('name') || '') + ' ' + (i.getAttribute('id') || '')))
1763 || [...doc.querySelectorAll('button, [role="button"], a[href], input[type="submit"], input[type="button"]')].filter(notConsent).filter(c => !c.matches('a[href]') || mwSame(c.getAttribute('href'))).some(c => SRCH.test((c.getAttribute('aria-label') || '') + ' ' + (c.getAttribute('title') || '') + ' ' + (c.textContent || '').slice(0, 40) + ' ' + (c.getAttribute('value') || '')) || /[?&](search|s|q|query)=|\/search(\/|\?|$)/i.test(c.getAttribute('href') || ''));
1764 const hasMap = [...doc.querySelectorAll('a[href]')].filter(l => mwSame(l.getAttribute('href'))).some(l => (/site-?map|site-?index/i.test(l.getAttribute('href') || '') && !/\.xml(\b|$)/i.test(l.getAttribute('href') || '')) || /\b(site ?map|site index|a ?(to|-) ?z)\b/i.test(l.textContent || ''));
1765 if (!hasSearch && !hasMap) issues.push({type:'warning',label:'No site search or site map found',desc:'Besides the menu, visitors need another way to find pages, such as a search box or a site map page. Neither was found on this page. A small site whose menu links to every page, or another way such as an A to Z list, can also meet this (WCAG 2.4.5 Multiple Ways, Level AA).',fix:'Add a site search, or a site map page that lists every page, and link to it from every page (usually in the footer).'});
1766 else passes.push(hasSearch ? 'Site search found (more than one way to find pages)' : 'Site map link found (more than one way to find pages)');
1767 }
1768
1769 // Auto-play media
1770 // (a video the browser found has no sound track plays no sound, so it is not counted; media playing sound by
1771 // itself right now without controls is counted even when a script started it)
1772 const autoplayMedia = [...html.matchAll(/<(video|audio)[^>]*autoplay[^>]*>/gi)];
1773 const autoplayNoMute = autoplayMedia.filter(m => !/muted/i.test(m[0]) && !/data-a11y-sound="no"/i.test(m[0]));
1774 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);
1775 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.'});
1776
1777 // Extra WCAG checks by axe-core (see a11yAxe): each failed rule is one issue. Serious and critical failures
1778 // count as errors, moderate and minor ones as warnings. The examples quote the page's own markup.
1779 const a11yAx = contrast && contrast.axe;
1780 if (a11yAx && a11yAx.ok) {
1781 const wcagOf = tags => (tags || []).map(t => { const m = /^wcag(\d)(\d)(\d+)$/.exec(t); return m ? m[1] + '.' + m[2] + '.' + m[3] : ''; }).filter(Boolean);
1782 // ACT spacing outcomes are preserved separately from axe-core.
1783 // When axe already reports spacing, retain the ACT evidence
1784 // without adding duplicate scored issues.
1785 const actSpacing = Array.isArray(a11yAx.actResults)
1786 ? a11yAx.actResults : [];
1787
1788 for (const result of actSpacing) {
1789 const name = result.id === '9e45ec'
1790 ? 'word spacing'
1791 : result.id === '24afc2'
1792 ? 'letter spacing'
1793 : 'text spacing';
1794
1795 if (result.outcome === 'error') {
1796 notes.push('ACT ' + name +
1797 ' check could not complete: ' +
1798 String(result.error || 'unknown error'));
1799 }
1800 }
1801
1802 const axeHasSpacing = a11yAx.violations.some(v =>
1803 v.id === 'avoid-inline-spacing'
1804 );
1805
1806 const a11yAxWcag = a11yAx.violations.filter(v =>
1807 !v.bp &&
1808 !(axeHasSpacing && /^act-(9e45ec|24afc2)$/.test(v.id))
1809 );
1810
1811 if (axeHasSpacing) {
1812 for (const result of actSpacing) {
1813 if (result.outcome === 'failed') {
1814 notes.push(
1815 'Additional ACT evidence: ' + result.id +
1816 ' detected restrictive ' +
1817 (result.id === '9e45ec'
1818 ? 'word spacing'
1819 : 'letter spacing') +
1820 '. Included under the existing axe spacing issue.'
1821 );
1822 }
1823 }
1824 }
1825 // Best-practice failures (not WCAG requirements) are listed as notes and never lower the score.
1826 for (const v of a11yAx.violations.filter(v => v.bp)) {
1827 const ex = (v.ex || []).map(x => a11yCut(x.html, 120)).join('; ');
1828 notes.push(`Best practice, not a WCAG requirement (not scored): ${v.n} element${v.n > 1 ? 's' : ''}: ${a11yCut(v.help, 110)}.${ex ? ' Examples: ' + ex : ''}`);
1829 }
1830 for (const v of a11yAxWcag) {
1831 const sc = wcagOf(v.tags);
1832 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('; ');
1833 issues.push({ type: /^(critical|serious)$/.test(v.impact || '') ? 'error' : 'warning',
1834 label: `${v.n} element${v.n > 1 ? 's' : ''}: ${a11yCut(v.help, 110)}${sc.length ? ' (WCAG ' + sc.join(', ') + ')' : ''}`,
1835 desc: String(v.description || '').replace(/\.?\s*$/, '.') + ' Checked with axe-core.' + (ex ? ' Examples: ' + ex : ''),
1836 fix: 'Fix each element listed: ' + a11yCut(String(v.help || ''), 200) + '.', axe: v.id });
1837 }
1838 if (!a11yAxWcag.length) passes.push('ARIA, list structure, zoom, page title and other WCAG checks passed (axe-core)');
1839 } 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') + '.');
1840
1841 // Inner pages (window.a11yInner, from the server: two pages from the site's menu, each given this same report). A
1842 // problem already reported for this page is not repeated; a problem found only on an inner page is added, marked
1843 // with that page. The menu and the help ways are compared across the pages (WCAG 3.2.3, 3.2.6).
1844 const inn = window.a11yInner;
1845 let a11yNavChecked = 0, a11yHelpChecked = 0;
1846 if (inn && inn.pages) {
1847 const stem = l => String(l || '').replace(/\d+/g, '#').toLowerCase();
1848 const have = new Set(issues.map(i => stem(i.label)));
1849 const pathOf = u => { try { const x = new URL(u); return (x.pathname + x.search) || '/'; } catch (e) { return String(u); } };
1850 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 })); }
=== SERVER-SIDE AXE FRAME INJECTION ===
775 } catch (e) { return { ok: false, html: '', error: String(e && e.message || e) }; }
776 });
777 await page.goto(TOOL, { waitUntil: 'domcontentloaded', timeout: 20000 });
778 await page.addStyleTag({ path: TOOL_CSS }).catch(() => {});
779 await page.addScriptTag({ path: TOOL_JS });
780 // axe-core and its runner, from this server's own copy, for the tool page's extra WCAG checks (see a11yAxe).
781 let axeSrc = null;
782 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; }
783 const run = page.evaluate(async (h, base, byScripts, axeSrc, kb, inner) => {
784 if (axeSrc) window.__a11yAxeSrc = axeSrc;
785 window.a11yKeyboard = kb || null; window.a11yKeyboardP = null; window.a11yInner = inner || null; window.a11yInnerFixed = true;
786 window.fetchRawHtml = (u, r) => window.__a11yFetch(u, r || null);
787 window.a11yBuiltByScripts = byScripts;
788 window.a11yLastReport = null;
789 document.getElementById('a11y-input').value = h;
790 checkA11y(base);
791 const until = Date.now() + 58000;
792 while (!window.a11yLastReport && Date.now() < until) await new Promise(r => setTimeout(r, 100));
793 return window.a11yLastReport;
794 }, html, url || '', builtByScripts, axeSrc, keyboard, inner || null);
795 const report = await Promise.race([run, new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('rendered accessibility check timed out')), TIMEOUT_MS); })]);
796 if (!report || !Array.isArray(report.issues)) throw new Error('rendered accessibility check returned no report');
797 report.rendered = true;
798 return report;
799 } finally {
800 if (timer) clearTimeout(timer);
801 if (page) await page.close().catch(() => {});
802 release();
803 }
804 }
805
806 // The two inner pages, each loaded as visitors see it and given the full report on its own. A page that cannot be
807 // loaded that way (error status, bot-check, almost empty, a different site after redirects) is left out.
808 // onProgress (optional) is called while they run, so a caller streaming results can show the audit is still working.
809 async function innerReports(homeUrl, urls, fetchCss, onProgress) {
810 const pages = [];
811 let homeHost = ''; try { homeHost = new URL(homeUrl).hostname.replace(/^www\./, ''); } catch (e) { return pages; }
812 const tick = onProgress ? setInterval(() => { try { onProgress(); } catch (e) {} }, 20000) : null;
813 try {
814 // up to 4 menu links are tried, until 2 pages are checked; a link that ends up on a login page after its redirects
815 // (HSBC's "log on" link lands on /auth/?returnUrl=) is not a page to audit
816 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;
817 for (const u of (urls || []).slice(0, 4)) {
818 if (pages.length >= 2) break;
819 try {
820 if (new URL(u).hostname.replace(/^www\./, '') !== homeHost) continue;
821 const r = await renderWithScripts(u, { keyboard: true });
822 if (!r.ok || (r.status && (r.status < 200 || r.status >= 300)) || nearlyEmpty(r.html) || botCheck(r.html)) continue;
823 if (new URL(r.finalUrl || u).hostname.replace(/^www\./, '') !== homeHost || SKIPRE.test(r.finalUrl || '')) continue;
824 const rep = await toolReport(r.html, r.finalUrl || u, true, r.keyboard || null, fetchCss, null);
825 pages.push({ url: r.finalUrl || u, issues: rep.issues, errorCount: rep.errorCount, warnCount: rep.warnCount, site: r.site || null });
=== TEST HARNESS LOCATION ===
60 for (const scenario of scenarios) {
61 const page = await browser.newPage();
62
63 try {
64 await page.setContent(`
65
66 ACT reporting test
67
68
69
70
71 `);
72
73 await page.evaluate(source => {
74 window.a11yQueueRelease = function () {};
75 window.a11yBuiltByScripts = false;
76 window.a11yKeyboard = null;
77 window.a11yKeyboardP = null;
78 window.a11yInner = null;
79 window.a11yInnerFixed = true;
80 window.SEOGate = { check() {}, close() {} };
81 (0, eval)(source);
82 }, audit);
83
84 const result = await page.evaluate(scenario => {
85 const violations = [];
86
87 if (scenario.axe) {
88 violations.push({
89 id: 'avoid-inline-spacing',
90 impact: 'serious',
91 help: 'Inline text spacing must be adjustable',
92 description: 'Restrictive inline spacing detected',
93 tags: ['wcag1412'],
94 n: 1,
95 bp: false,
96 ex: []
97 });
98 }
99
100 for (const item of scenario.act) {
101 if (item.outcome !== 'failed') continue;
102
103 violations.push({
104 id: 'act-' + item.id,
105 impact: 'serious',
106 help: 'Restrictive text spacing',
107 description: 'ACT spacing failure',
108 tags: ['wcag1412'],
109 n: item.count,
110 bp: false,
111 ex: []
112 });
113 }
114
115 checkA11yReport({
116 status: 'skipped',
117 reason: 'controlled reporting test',
118 axe: {
119 ok: true,
120 version: '4.10.2',
121 violations,
122 actResults: scenario.act
123 }
124 });
125
Production changed: NO
PM2 restarted: NO