@peripheries/engine 0.23.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -6,4 +6,5 @@ import{readFileSync,writeFileSync}from"node:fs";import{resolve}from"node:path";i
|
|
|
6
6
|
<button style="width:16px;height:16px;padding:0"></button>
|
|
7
7
|
<input style="font:16px sans-serif" type="text">
|
|
8
8
|
<p style="font:16px sans-serif;color:#222">Read the <a href="#" style="color:#333;text-decoration:none">policy</a> before you go.<style>a::after{content:'';position:absolute}</style></p>
|
|
9
|
-
</main></body></html>`;const CONTROL_EXPECTS=["axe:color-contrast","axe:button-name","axe:label","small_target","small_text","link_in_text"];const PAGE_RULES=Object.fromEntries(["document-title","landmark-one-main","page-has-heading-one","region"].map(id=>[id,{enabled:false}]));const PROFILES=[{width:390,ctx:{viewport:{width:390,height:844},hasTouch:true,isMobile:true,deviceScaleFactor:2}},{width:1280,ctx:{viewport:{width:1280,height:900}}}];const TARGET_MIN=a11y.targetTouch;const TARGET_MIN_FINE=a11y.targetMouse;const TEXT_MIN=a11y.textMin;const CONTROL_MIN=a11y.controlMin;const TAB_MAX=a11y.tabMax;const served=control?null:await buildAndServeHarness();const url=(q="")=>`${served.origin}${served.path}${q}`;const browser=await chromium.launch();let targets;if(control){targets=[{fixture:"(control)",scenario:"a deliberately bad page",control:true}]}else{const listPage=await browser.newPage();await listPage.goto(url());await listPage.waitForSelector('a[href*="fixture="]');const scenarios=await listPage.$$eval('a[href*="fixture="]',as=>as.map(a=>a.getAttribute("href")).filter(h=>h&&!h.includes("container=")));await listPage.close();targets=scenarios.map(href=>{const q=new URL(href,"file:///").searchParams;return{fixture:q.get("fixture"),scenario:q.get("scenario"),href}}).filter(t=>!only||t.fixture===only)}const measure=({coarse,targetMin,textMin,controlMin,links})=>{const visible=el=>{const r=el.getBoundingClientRect();const s=getComputedStyle(el);return r.width>0&&r.height>0&&s.visibility!=="hidden"&&s.display!=="none"&&Number(s.opacity)!==0};const name=el=>`${el.tagName.toLowerCase()}${el.id?`#${el.id}`:""}.${String(el.getAttribute("class")??"").split(/\s+/).filter(Boolean).slice(0,2).join(".")}`;const effective=el=>{const s=getComputedStyle(el);const box=el.getBoundingClientRect();let[x0,y0,x1,y1]=[box.left,box.top,box.right,box.bottom];const cb=s.position!=="static"?el:el.offsetParent;if(cb){const c=cb.getBoundingClientRect();const cs=getComputedStyle(cb);const[ox,oy]=[c.left+parseFloat(cs.borderLeftWidth),c.top+parseFloat(cs.borderTopWidth)];for(const which of["::before","::after"]){const p=getComputedStyle(el,which);if(p.content==="none"||p.position!=="absolute")continue;const[l,t,pw,ph]=[p.left,p.top,p.width,p.height].map(parseFloat);if([l,t,pw,ph].some(Number.isNaN))continue;[x0,y0,x1,y1]=[Math.min(x0,ox+l),Math.min(y0,oy+t),Math.max(x1,ox+l+pw),Math.max(y1,oy+t+ph)]}}for(const child of el.children){if(getComputedStyle(child).position!=="absolute"||child.getAttribute("aria-hidden")!=="true")continue;const c=child.getBoundingClientRect();if(c.width>0&&c.height>0)[x0,y0,x1,y1]=[Math.min(x0,c.left),Math.min(y0,c.top),Math.max(x1,c.right),Math.max(y1,c.bottom)]}return[x1-x0,y1-y0]};const found={small_target:[],small_text:[],small_control:[],link_in_text:[],no_focus_ring:[]};const interactive=[...document.querySelectorAll('a[href], button, input:not([type=hidden]), select, textarea, summary, [role=button], [role=link], [role=tab], [role=menuitem], [role=option], [role=checkbox], [role=radio], [role=switch], [role=treeitem], [tabindex]:not([tabindex="-1"])')].filter(visible);for(const el of interactive){const r=el.getBoundingClientRect();const s=getComputedStyle(el);const alone=(el.parentElement?.textContent??"").trim()===(el.textContent??"").trim()&&!el.matches(links);const inline=s.display==="inline"&&!alone;const wrapped=Boolean(el.closest("label"))&&effective(el.closest("label")).every(n=>n>=targetMin-.5);const words=(el.textContent??"").trim().length>0&&!(el instanceof HTMLInputElement);const short=r.height<targetMin-.5||(words?r.width<24:r.width<targetMin-.5);const scroller=el.matches("[tabindex]")&&!el.matches("a, button, input, select, textarea, summary, [role=button], [role=link], [role=tab], [role=menuitem], [role=option], [role=checkbox], [role=radio], [role=switch], [role=treeitem]");const hidden=s.clipPath!=="none"&&s.overflow==="hidden"||el.getAttribute("tabindex")==="-1"||Boolean(el.closest('[aria-hidden="true"]'));const disabled=el.matches(":disabled")||el.getAttribute("aria-disabled")==="true";if(inline||wrapped||scroller||hidden||disabled||!short)continue;const[w,h]=effective(el);const stillShort=h<targetMin-.5||(words?w<24:w<targetMin-.5);if(stillShort)found.small_target.push(`${name(el)} ${Math.round(w)}x${Math.round(h)} :: ${el.outerHTML.replace(/\s+/g," ").slice(0,130)}`)}const rgb=c=>{const m=/rgba?\(([^)]+)\)/.exec(c);return m?m[1].split(/[ ,/]+/).slice(0,3).map(Number):null};const lum=c=>{const[r,g,b]=c.map(v=>{const x=v/255;return x<=.03928?x/12.92:((x+.055)/1.055)**2.4});return .2126*r+.7152*g+.0722*b};for(const el of document.querySelectorAll("a[href]")){if(!visible(el)||el.closest('[aria-hidden="true"]'))continue;const parent=el.parentElement;if(!parent)continue;const others=(parent.textContent??"").replace(el.textContent??"","").trim();if(!others||getComputedStyle(el).display!=="inline"||getComputedStyle(parent).display==="flex"||getComputedStyle(parent).display==="grid")continue;const s=getComputedStyle(el);if(s.textDecorationLine.includes("underline")||s.borderBottomStyle!=="none"&&parseFloat(s.borderBottomWidth)>0)continue;const a=rgb(s.color);const b=rgb(getComputedStyle(parent).color);if(!a||!b)continue;const[hi,lo]=[lum(a),lum(b)].sort((x,y)=>y-x);const ratio=(hi+.05)/(lo+.05);if(ratio<3)found.link_in_text.push(`${name(el)} ${Math.round(ratio*100)/100}:1 against its words and not underlined :: ${el.outerHTML.replace(/\s+/g," ").slice(0,100)}`)}if(coarse){for(const el of document.querySelectorAll('input, select, textarea, [contenteditable=""], [contenteditable="true"]')){if(el instanceof HTMLInputElement&&["checkbox","radio","range","hidden","file","submit","button","reset","image","color"].includes(el.type))continue;if(!visible(el)||el.getAttribute("tabindex")==="-1"||el.closest('[aria-hidden="true"]'))continue;const px=parseFloat(getComputedStyle(el).fontSize);if(px<controlMin)found.small_control.push(`${name(el)} ${px}px`)}}const walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);const seen=new Set;for(let n=walker.nextNode();n;n=walker.nextNode()){if(!n.nodeValue?.trim())continue;const el=n.parentElement;if(!el||seen.has(el)||el.closest('script, style, noscript, svg, [aria-hidden="true"], [role="img"]')||!visible(el))continue;seen.add(el);const px=parseFloat(getComputedStyle(el).fontSize);if(px>0&&px<textMin-.01)found.small_text.push(`${name(el)} ${Math.round(px*100)/100}px "${n.nodeValue.trim().slice(0,24)}"`)}return found};const focusRing=async()=>{const el=document.activeElement;if(!el||el===document.body)return null;const drawn=n=>{const s=getComputedStyle(n);return s.outlineStyle!=="none"&&parseFloat(s.outlineWidth)>0&&s.outlineColor!=="rgba(0, 0, 0, 0)"||Boolean(s.boxShadow&&s.boxShadow!=="none")||n instanceof SVGElement&&s.stroke!=="none"&&parseFloat(s.strokeWidth)>0};let ring=[el,...el.children].some(drawn);if(!ring){const sign=n=>{const c=getComputedStyle(n);return[c.outlineStyle,c.outlineWidth,c.outlineColor,c.boxShadow,c.borderTopColor,c.borderBottomColor,c.backgroundColor].join("|")};const up=[];for(let n=el.parentElement,i=0;n&&n!==document.body&&i<3;n=n.parentElement,i++)up.push(n);const on=up.map(sign);const settle=()=>new Promise(r=>setTimeout(r,60));el.blur();await settle();const off=up.map(sign);el.focus();await settle();ring=on.some((v,i)=>v!==off[i])}const name=`${el.tagName.toLowerCase()}${el.id?`#${el.id}`:""}.${String(el.getAttribute("class")??"").split(/\s+/).filter(Boolean).slice(0,2).join(".")}`;return{name,ring:Boolean(ring),matches:el.matches(":focus-visible")}};const findings=[];const add=(t,width,rule,detail)=>findings.push({component:t.fixture,scenario:t.scenario,width,rule,detail});let index=0;const run=async(t,p)=>{const context=await browser.newContext(p.ctx);const page=await context.newPage();try{if(t.control)await page.setContent(CONTROL_HTML);else await page.goto(url(`?${t.href.split("?")[1]}`));await page.waitForTimeout(600);if(rectsOut){rects[`${t.fixture}/${t.scenario}${p.width===1280?"":`@${p.width}`}`]=await page.evaluate(()=>[document.documentElement.scrollHeight,[...document.querySelectorAll("body *")].map(e=>{const r=e.getBoundingClientRect();return`${e.tagName}.${String(e.getAttribute("class")??"").split(" ")[0]}:${Math.round(r.width)}x${Math.round(r.height)}@${Math.round(r.left)},${Math.round(r.top)}`})])}await page.addScriptTag({content:axeSource});const axe=await page.evaluate(async RULES=>{const r=await window.axe.run(document,{runOnly:{type:"tag",values:["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa","best-practice"]},resultTypes:["violations"],rules:RULES});return r.violations.map(v=>{const d=v.nodes[0]?.any?.[0]?.data;const colours=d&&d.contrastRatio?` ${d.fgColor} on ${d.bgColor} = ${d.contrastRatio} (needs ${d.expectedContrastRatio}, ${d.fontSize})`:"";return{id:v.id,count:v.nodes.length,sample:`${String(v.nodes[0]?.target?.[0]??"")} :: ${(v.nodes[0]?.html??"").slice(0,100)}${colours}`}})},PAGE_RULES);for(const v of axe)add(t,p.width,`axe:${v.id}`,`x${v.count} ${v.sample}`);const m=await page.evaluate(measure,{coarse:p.width<600,targetMin:p.width<600?TARGET_MIN:TARGET_MIN_FINE,textMin:TEXT_MIN,controlMin:CONTROL_MIN,links:LINKS});for(const[rule,list]of Object.entries(m))if(rule!=="no_focus_ring")for(const d of list)add(t,p.width,rule,d);const stray=await page.evaluate(()=>[...document.querySelectorAll("[role=dialog][aria-modal=true]")].filter(d=>d.getBoundingClientRect().width>0&&!d.contains(document.activeElement)).map(d=>`${d.getAttribute("aria-label")??"dialog"}: focus is on ${document.activeElement?.tagName.toLowerCase()}`));for(const d of stray)add(t,p.width,"dialog_focus",d);const seen=new Set;for(let i=0;i<TAB_MAX;i++){await page.keyboard.press("Tab");const f=await page.evaluate(focusRing);if(!f||seen.has(f.name+i%1)){if(!f)break}if(seen.has(f.name))continue;seen.add(f.name);if(!f.ring&&!f.name.startsWith("iframe"))add(t,p.width,"no_focus_ring",f.name)}}catch(e){add(t,p.width,"error",String(e).slice(0,160))}finally{await context.close();if(++index%100===0)console.log(` ${index}/${targets.length*PROFILES.length}`)}};const jobs=targets.flatMap(t=>PROFILES.map(p=>({t,p})));const workers=Array.from({length:4},async()=>{for(let j=jobs.shift();j;j=jobs.shift())await run(j.t,j.p)});await Promise.all(workers);await browser.close();await served?.close();if(!control)writeFileSync(out,JSON.stringify({scenarios:targets.length,findings},null,1));if(rectsOut)writeFileSync(rectsOut,JSON.stringify(rects));const by=new Map;for(const f of findings)by.set(f.rule,(by.get(f.rule)??0)+1);const comps=rule=>new Set(findings.filter(f=>f.rule===rule).map(f=>f.component)).size;console.log(`${targets.length} scenarios at ${PROFILES.map(p=>p.width).join(" and ")}px: ${findings.length} findings`);for(const[rule,n]of[...by].sort((a,b)=>b[1]-a[1]))console.log(` ${rule.padEnd(28)} ${String(n).padStart(5)} in ${comps(rule)} components`);if(findings.length&&argv.includes("--fail")){for(const f of findings.slice(0,60))console.log(` ${f.component} \xB7 ${f.scenario} @${f.width}: ${f.rule} ${f.detail}`);process.exitCode=1}if(control){const seen=new Set(findings.map(f=>f.rule));const missing=CONTROL_EXPECTS.filter(r=>!seen.has(r));if(missing.length){console.log(`NEGATIVE CONTROL FAILED: the sweep did not report ${missing.join(", ")} on a page that has them; a green sweep means nothing.`);process.exitCode=1}else{console.log(`negative control ok: the sweep reports every known fault (${CONTROL_EXPECTS.join(", ")}).`);process.exitCode=0}}
|
|
9
|
+
<p style="font:16px sans-serif;color:#000">A claim<sup><a href="#n1" style="color:#000;text-decoration:underline">[1]</a></sup> is made.</p>
|
|
10
|
+
</main></body></html>`;const CONTROL_EXPECTS=["axe:color-contrast","axe:button-name","axe:label","small_target","small_text","link_in_text"];const PAGE_RULES=Object.fromEntries(["document-title","landmark-one-main","page-has-heading-one","region"].map(id=>[id,{enabled:false}]));const PROFILES=[{width:390,ctx:{viewport:{width:390,height:844},hasTouch:true,isMobile:true,deviceScaleFactor:2}},{width:1280,ctx:{viewport:{width:1280,height:900}}}];const TARGET_MIN=a11y.targetTouch;const TARGET_MIN_FINE=a11y.targetMouse;const TEXT_MIN=a11y.textMin;const CONTROL_MIN=a11y.controlMin;const TAB_MAX=a11y.tabMax;const served=control?null:await buildAndServeHarness();const url=(q="")=>`${served.origin}${served.path}${q}`;const browser=await chromium.launch();let targets;if(control){targets=[{fixture:"(control)",scenario:"a deliberately bad page",control:true}]}else{const listPage=await browser.newPage();await listPage.goto(url());await listPage.waitForSelector('a[href*="fixture="]');const scenarios=await listPage.$$eval('a[href*="fixture="]',as=>as.map(a=>a.getAttribute("href")).filter(h=>h&&!h.includes("container=")));await listPage.close();targets=scenarios.map(href=>{const q=new URL(href,"file:///").searchParams;return{fixture:q.get("fixture"),scenario:q.get("scenario"),href}}).filter(t=>!only||t.fixture===only)}const measure=({coarse,targetMin,textMin,controlMin,links})=>{const visible=el=>{const r=el.getBoundingClientRect();const s=getComputedStyle(el);return r.width>0&&r.height>0&&s.visibility!=="hidden"&&s.display!=="none"&&Number(s.opacity)!==0};const name=el=>`${el.tagName.toLowerCase()}${el.id?`#${el.id}`:""}.${String(el.getAttribute("class")??"").split(/\s+/).filter(Boolean).slice(0,2).join(".")}`;const effective=el=>{const s=getComputedStyle(el);const box=el.getBoundingClientRect();let[x0,y0,x1,y1]=[box.left,box.top,box.right,box.bottom];const cb=s.position!=="static"?el:el.offsetParent;if(cb){const c=cb.getBoundingClientRect();const cs=getComputedStyle(cb);const[ox,oy]=[c.left+parseFloat(cs.borderLeftWidth),c.top+parseFloat(cs.borderTopWidth)];for(const which of["::before","::after"]){const p=getComputedStyle(el,which);if(p.content==="none"||p.position!=="absolute")continue;const[l,t,pw,ph]=[p.left,p.top,p.width,p.height].map(parseFloat);if([l,t,pw,ph].some(Number.isNaN))continue;[x0,y0,x1,y1]=[Math.min(x0,ox+l),Math.min(y0,oy+t),Math.max(x1,ox+l+pw),Math.max(y1,oy+t+ph)]}}for(const child of el.children){if(getComputedStyle(child).position!=="absolute"||child.getAttribute("aria-hidden")!=="true")continue;const c=child.getBoundingClientRect();if(c.width>0&&c.height>0)[x0,y0,x1,y1]=[Math.min(x0,c.left),Math.min(y0,c.top),Math.max(x1,c.right),Math.max(y1,c.bottom)]}return[x1-x0,y1-y0]};const found={small_target:[],small_text:[],small_control:[],link_in_text:[],no_focus_ring:[]};const interactive=[...document.querySelectorAll('a[href], button, input:not([type=hidden]), select, textarea, summary, [role=button], [role=link], [role=tab], [role=menuitem], [role=option], [role=checkbox], [role=radio], [role=switch], [role=treeitem], [tabindex]:not([tabindex="-1"])')].filter(visible);for(const el of interactive){const r=el.getBoundingClientRect();const s=getComputedStyle(el);let block=el.parentElement;while(block&&block.parentElement&&getComputedStyle(block).display.startsWith("inline"))block=block.parentElement;const alone=(block?.textContent??"").trim()===(el.textContent??"").trim()&&!el.matches(links);const inline=s.display==="inline"&&!alone;const wrapped=Boolean(el.closest("label"))&&effective(el.closest("label")).every(n=>n>=targetMin-.5);const words=(el.textContent??"").trim().length>0&&!(el instanceof HTMLInputElement);const short=r.height<targetMin-.5||(words?r.width<24:r.width<targetMin-.5);const scroller=el.matches("[tabindex]")&&!el.matches("a, button, input, select, textarea, summary, [role=button], [role=link], [role=tab], [role=menuitem], [role=option], [role=checkbox], [role=radio], [role=switch], [role=treeitem]");const hidden=s.clipPath!=="none"&&s.overflow==="hidden"||el.getAttribute("tabindex")==="-1"||Boolean(el.closest('[aria-hidden="true"]'));const disabled=el.matches(":disabled")||el.getAttribute("aria-disabled")==="true";if(inline||wrapped||scroller||hidden||disabled||!short)continue;const[w,h]=effective(el);const stillShort=h<targetMin-.5||(words?w<24:w<targetMin-.5);if(stillShort)found.small_target.push(`${name(el)} ${Math.round(w)}x${Math.round(h)} :: ${el.outerHTML.replace(/\s+/g," ").slice(0,130)}`)}const rgb=c=>{const m=/rgba?\(([^)]+)\)/.exec(c);return m?m[1].split(/[ ,/]+/).slice(0,3).map(Number):null};const lum=c=>{const[r,g,b]=c.map(v=>{const x=v/255;return x<=.03928?x/12.92:((x+.055)/1.055)**2.4});return .2126*r+.7152*g+.0722*b};for(const el of document.querySelectorAll("a[href]")){if(!visible(el)||el.closest('[aria-hidden="true"]'))continue;const parent=el.parentElement;if(!parent)continue;const others=(parent.textContent??"").replace(el.textContent??"","").trim();if(!others||getComputedStyle(el).display!=="inline"||getComputedStyle(parent).display==="flex"||getComputedStyle(parent).display==="grid")continue;const s=getComputedStyle(el);if(s.textDecorationLine.includes("underline")||s.borderBottomStyle!=="none"&&parseFloat(s.borderBottomWidth)>0)continue;const a=rgb(s.color);const b=rgb(getComputedStyle(parent).color);if(!a||!b)continue;const[hi,lo]=[lum(a),lum(b)].sort((x,y)=>y-x);const ratio=(hi+.05)/(lo+.05);if(ratio<3)found.link_in_text.push(`${name(el)} ${Math.round(ratio*100)/100}:1 against its words and not underlined :: ${el.outerHTML.replace(/\s+/g," ").slice(0,100)}`)}if(coarse){for(const el of document.querySelectorAll('input, select, textarea, [contenteditable=""], [contenteditable="true"]')){if(el instanceof HTMLInputElement&&["checkbox","radio","range","hidden","file","submit","button","reset","image","color"].includes(el.type))continue;if(!visible(el)||el.getAttribute("tabindex")==="-1"||el.closest('[aria-hidden="true"]'))continue;const px=parseFloat(getComputedStyle(el).fontSize);if(px<controlMin)found.small_control.push(`${name(el)} ${px}px`)}}const walker=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT);const seen=new Set;for(let n=walker.nextNode();n;n=walker.nextNode()){if(!n.nodeValue?.trim())continue;const el=n.parentElement;if(!el||seen.has(el)||el.closest('script, style, noscript, svg, [aria-hidden="true"], [role="img"]')||!visible(el))continue;seen.add(el);const px=parseFloat(getComputedStyle(el).fontSize);if(px>0&&px<textMin-.01)found.small_text.push(`${name(el)} ${Math.round(px*100)/100}px "${n.nodeValue.trim().slice(0,24)}"`)}return found};const focusRing=async()=>{const el=document.activeElement;if(!el||el===document.body)return null;const drawn=n=>{const s=getComputedStyle(n);return s.outlineStyle!=="none"&&parseFloat(s.outlineWidth)>0&&s.outlineColor!=="rgba(0, 0, 0, 0)"||Boolean(s.boxShadow&&s.boxShadow!=="none")||n instanceof SVGElement&&s.stroke!=="none"&&parseFloat(s.strokeWidth)>0};let ring=[el,...el.children].some(drawn);if(!ring){const sign=n=>{const c=getComputedStyle(n);return[c.outlineStyle,c.outlineWidth,c.outlineColor,c.boxShadow,c.borderTopColor,c.borderBottomColor,c.backgroundColor].join("|")};const up=[];for(let n=el.parentElement,i=0;n&&n!==document.body&&i<3;n=n.parentElement,i++)up.push(n);const on=up.map(sign);const settle=()=>new Promise(r=>setTimeout(r,60));el.blur();await settle();const off=up.map(sign);el.focus();await settle();ring=on.some((v,i)=>v!==off[i])}const name=`${el.tagName.toLowerCase()}${el.id?`#${el.id}`:""}.${String(el.getAttribute("class")??"").split(/\s+/).filter(Boolean).slice(0,2).join(".")}`;return{name,ring:Boolean(ring),matches:el.matches(":focus-visible")}};const findings=[];const add=(t,width,rule,detail)=>findings.push({component:t.fixture,scenario:t.scenario,width,rule,detail});let index=0;const run=async(t,p)=>{const context=await browser.newContext(p.ctx);const page=await context.newPage();try{if(t.control)await page.setContent(CONTROL_HTML);else await page.goto(url(`?${t.href.split("?")[1]}`));await page.waitForTimeout(600);if(rectsOut){rects[`${t.fixture}/${t.scenario}${p.width===1280?"":`@${p.width}`}`]=await page.evaluate(()=>[document.documentElement.scrollHeight,[...document.querySelectorAll("body *")].map(e=>{const r=e.getBoundingClientRect();return`${e.tagName}.${String(e.getAttribute("class")??"").split(" ")[0]}:${Math.round(r.width)}x${Math.round(r.height)}@${Math.round(r.left)},${Math.round(r.top)}`})])}await page.addScriptTag({content:axeSource});const axe=await page.evaluate(async RULES=>{const r=await window.axe.run(document,{runOnly:{type:"tag",values:["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa","best-practice"]},resultTypes:["violations"],rules:RULES});return r.violations.map(v=>{const d=v.nodes[0]?.any?.[0]?.data;const colours=d&&d.contrastRatio?` ${d.fgColor} on ${d.bgColor} = ${d.contrastRatio} (needs ${d.expectedContrastRatio}, ${d.fontSize})`:"";return{id:v.id,count:v.nodes.length,sample:`${String(v.nodes[0]?.target?.[0]??"")} :: ${(v.nodes[0]?.html??"").slice(0,100)}${colours}`}})},PAGE_RULES);for(const v of axe)add(t,p.width,`axe:${v.id}`,`x${v.count} ${v.sample}`);const m=await page.evaluate(measure,{coarse:p.width<600,targetMin:p.width<600?TARGET_MIN:TARGET_MIN_FINE,textMin:TEXT_MIN,controlMin:CONTROL_MIN,links:LINKS});for(const[rule,list]of Object.entries(m))if(rule!=="no_focus_ring")for(const d of list)add(t,p.width,rule,d);const stray=await page.evaluate(()=>[...document.querySelectorAll("[role=dialog][aria-modal=true]")].filter(d=>d.getBoundingClientRect().width>0&&!d.contains(document.activeElement)).map(d=>`${d.getAttribute("aria-label")??"dialog"}: focus is on ${document.activeElement?.tagName.toLowerCase()}`));for(const d of stray)add(t,p.width,"dialog_focus",d);const seen=new Set;for(let i=0;i<TAB_MAX;i++){await page.keyboard.press("Tab");const f=await page.evaluate(focusRing);if(!f||seen.has(f.name+i%1)){if(!f)break}if(seen.has(f.name))continue;seen.add(f.name);if(!f.ring&&!f.name.startsWith("iframe"))add(t,p.width,"no_focus_ring",f.name)}}catch(e){add(t,p.width,"error",String(e).slice(0,160))}finally{await context.close();if(++index%100===0)console.log(` ${index}/${targets.length*PROFILES.length}`)}};const jobs=targets.flatMap(t=>PROFILES.map(p=>({t,p})));const workers=Array.from({length:4},async()=>{for(let j=jobs.shift();j;j=jobs.shift())await run(j.t,j.p)});await Promise.all(workers);await browser.close();await served?.close();if(!control)writeFileSync(out,JSON.stringify({scenarios:targets.length,findings},null,1));if(rectsOut)writeFileSync(rectsOut,JSON.stringify(rects));const by=new Map;for(const f of findings)by.set(f.rule,(by.get(f.rule)??0)+1);const comps=rule=>new Set(findings.filter(f=>f.rule===rule).map(f=>f.component)).size;console.log(`${targets.length} scenarios at ${PROFILES.map(p=>p.width).join(" and ")}px: ${findings.length} findings`);for(const[rule,n]of[...by].sort((a,b)=>b[1]-a[1]))console.log(` ${rule.padEnd(28)} ${String(n).padStart(5)} in ${comps(rule)} components`);if(findings.length&&argv.includes("--fail")){for(const f of findings.slice(0,60))console.log(` ${f.component} \xB7 ${f.scenario} @${f.width}: ${f.rule} ${f.detail}`);process.exitCode=1}if(control){const seen=new Set(findings.map(f=>f.rule));const missing=CONTROL_EXPECTS.filter(r=>!seen.has(r));if(missing.length){console.log(`NEGATIVE CONTROL FAILED: the sweep did not report ${missing.join(", ")} on a page that has them; a green sweep means nothing.`);process.exitCode=1}else{console.log(`negative control ok: the sweep reports every known fault (${CONTROL_EXPECTS.join(", ")}).`);process.exitCode=0}}
|