=== 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