import '../brand.js'; import {browser} from './browser.mjs'; import {mkdir,writeFile} from 'node:fs/promises'; import assert from 'node:assert/strict'; const out=new URL('../evidence/',import.meta.url); await mkdir(out,{recursive:true}); const report={observedAt:new Date().toISOString(),browser:'Installed headless Chromium through CDP',contrast:[],layouts:[],interactions:[],limits:['No Firefox, Safari, real mobile device, screen reader, or trademark clearance.']}; for(const p of Brand.palettes)for(const mode of ['light','dim','dark']){ const t=Brand.tokens(p,mode),checks=[]; for(const fg of ['text','muted','action','accent','success','warning','danger'])for(const bg of ['canvas','surface','raised'])checks.push({fg,bg,minimum:4.5,ratio:Brand.contrast(t[fg],t[bg])}); for(const fg of ['border','focus'])for(const bg of ['canvas','surface','raised'])checks.push({fg,bg,minimum:3,ratio:Brand.contrast(t[fg],t[bg])}); checks.push({fg:'onAction',bg:'action',minimum:4.5,ratio:Brand.contrast(t.onAction,t.action)}); assert(checks.every(c=>c.ratio>=c.minimum),`${p.id}/${mode} contrast`); report.contrast.push({palette:p.id,mode,checks}); } const b=await browser(); const url=new URL('../index.html',import.meta.url).href; const change=async(id,value)=>b.evaluate(`(()=>{const e=document.getElementById(${JSON.stringify(id)});e.value=${JSON.stringify(value)};e.dispatchEvent(new Event('change',{bubbles:true}));return e.value})()`); const mode=async value=>b.evaluate(`document.querySelector('[name="mode"][value="${value}"]').click()`); const measure=()=>b.evaluate(`({width:innerWidth,scroll:document.documentElement.scrollWidth,body:document.body.scrollWidth,overflows:[...document.querySelectorAll('main *,header *,aside *')].filter(e=>{const r=e.getBoundingClientRect();return r.width>0&&(r.right>innerWidth+1||r.left < -1)&&!e.classList.contains('sr-only')}).slice(0,10).map(e=>({tag:e.tagName,class:e.className,id:e.id}))})`); try{ await b.call('Page.addScriptToEvaluateOnNewDocument',{source:'window.__errors=[];addEventListener("error",e=>window.__errors.push(e.message));addEventListener("unhandledrejection",e=>window.__errors.push(String(e.reason)));'}); await b.viewport(1440,1100); await b.navigate(url); assert.equal(await b.evaluate('document.querySelectorAll("[data-palette-card]").length'),10); assert.equal(await b.evaluate('document.querySelectorAll("[data-mark-card]").length'),3); assert.equal(await b.evaluate('document.querySelectorAll("[data-font-card]").length'),3); // All theme choices are applied through the actual page controls, then laid out. for(const p of Brand.palettes)for(const m of ['light','dim','dark']){ await change('palette',p.id);await mode(m); assert.equal(await b.evaluate('document.documentElement.dataset.palette'),p.id); assert.equal(await b.evaluate('document.documentElement.dataset.mode'),m); for(const width of [320,390,768,960,1440,1920,2560,3440]){ await b.viewport(width,1000); const result=await measure(); report.layouts.push({palette:p.id,mode:m,...result}); assert(result.scroll<=width && result.body<=width && !result.overflows.length,JSON.stringify(report.layouts.at(-1))); } } await change('palette','harbor');await mode('light'); for(const width of [320,390,768,1440,3440]){ await b.viewport(width,width===390?844:1100);await b.evaluate('scrollTo(0,0)'); await b.screenshot(new URL(`board-light-${width}.png`,out)); } for(const m of ['dim','dark']){ await mode(m);await b.viewport(1440,1100);await b.evaluate('scrollTo(0,0)'); await b.screenshot(new URL(`board-${m}-1440.png`,out)); } // Real keyboard: native select, radio navigation, buttons, forward/reverse tab. await mode('light');await b.viewport(1440,1100); await b.evaluate('document.getElementById("palette").focus()'); await b.key('ArrowDown');await b.key('Enter'); assert.equal(await b.evaluate('document.getElementById("palette").value'),'carmine'); await b.evaluate('document.querySelector("[name=mode][value=light]").focus()'); await b.key('ArrowRight'); assert.equal(await b.evaluate('document.documentElement.dataset.mode'),'dim'); const focus=await b.evaluate('({outline:getComputedStyle(document.activeElement.nextElementSibling).outlineStyle,width:getComputedStyle(document.activeElement.nextElementSibling).outlineWidth})'); assert.equal(focus.outline,'solid');assert.equal(focus.width,'3px'); await b.evaluate('document.querySelector("[data-choose-mark=weave]").focus()');await b.key('Enter'); assert.equal(await b.evaluate('document.getElementById("mark").value'),'weave'); await b.evaluate('document.querySelector("[data-choose-font=plex]").focus()');await b.key(' ' ,'Space'); assert.equal(await b.evaluate('document.getElementById("font").value'),'plex'); await b.evaluate('document.getElementById("sample-title").focus()'); await b.key('Tab');assert.equal(await b.evaluate('document.activeElement.id'),'sample-state'); await b.key('Tab','Tab',8);assert.equal(await b.evaluate('document.activeElement.id'),'sample-title'); report.interactions.push({name:'Keyboard selection, native radio arrow, Enter/Space buttons, Tab/Shift+Tab',result:'pass',focus}); await change('sample-state','error'); assert.equal(await b.evaluate('document.getElementById("sample-title").value'),'Summer reading notes'); await b.evaluate('document.getElementById("sample-action").focus()');await b.key('Enter'); assert.equal(await b.evaluate('document.getElementById("sample-feedback").dataset.state'),'success'); await change('sample-state','loading');assert(await b.evaluate('document.getElementById("sample-action").disabled')); for(const s of ['empty','ready']) {await change('sample-state',s);assert.equal(await b.evaluate('document.getElementById("sample-feedback").dataset.state'),s);} report.interactions.push({name:'All five component states and error recovery preserving input',result:'pass'}); // All bundled families loaded, and long labels remain usable at mobile width. for(const f of Brand.fonts){ await change('font',f.id);await b.evaluate('document.fonts.ready.then(()=>true)'); const loaded=await b.evaluate(`document.fonts.check('16px "${f.name}"')`); assert(loaded,f.name+' font load'); await b.viewport(320,1000);assert((await measure()).scroll<=320); } report.interactions.push({name:'Three local fonts loaded and reflowed at 320 px',result:'pass'}); await b.evaluate('document.getElementById("review-notes").value="Keep this review note.";document.getElementById("review-notes").dispatchEvent(new Event("input"));document.getElementById("reset").click()'); assert.equal(await b.evaluate('document.getElementById("review-notes").value'),'Keep this review note.'); await b.navigate(url); assert.equal(await b.evaluate('document.getElementById("review-notes").value'),'Keep this review note.'); report.interactions.push({name:'Local preference and note storage; reset preserves notes',result:'pass'}); await b.call('Emulation.setEmulatedMedia',{features:[{name:'prefers-reduced-motion',value:'reduce'}]}); await mode('dark');await b.key('Tab'); assert(await b.evaluate('matchMedia("(prefers-reduced-motion: reduce)").matches')); report.interactions.push({name:'Reduced motion still permits theme and keyboard interaction',result:'pass'}); await b.viewport(320,1000); await b.evaluate(`(()=>{const style=document.createElement('style');style.id='spacing-test';style.textContent='*{line-height:1.5!important;letter-spacing:.12em!important;word-spacing:.16em!important}p{margin-bottom:2em!important}';document.head.append(style)})()`); let result=await measure();assert(result.scroll<=320,JSON.stringify(result)); await b.screenshot(new URL('board-text-spacing-320.png',out)); await b.evaluate('document.getElementById("spacing-test").remove();document.documentElement.style.fontSize="200%"'); result=await measure();assert(result.scroll<=320,JSON.stringify(result)); await b.screenshot(new URL('board-text-200-320.png',out)); report.interactions.push({name:'320 px text-spacing overrides and root text size 200%',result:'pass',qualification:'Not browser-native zoom; pixel-sized component text also needs separate scaling checks.'}); await b.evaluate('document.documentElement.style.fontSize=""'); const errors=await b.evaluate('window.__errors');assert.deepEqual(errors,[]); report.interactions.push({name:'Uncaught page errors',result:'pass',errors}); } catch(error){report.failure=error.stack;} finally{await b.close();} await writeFile(new URL('verification.json',out),JSON.stringify(report,null,2)+'\n'); console.log(JSON.stringify({contrastSets:report.contrast.length,layouts:report.layouts.length,interactions:report.interactions,failure:report.failure},null,2)); if(report.failure)process.exitCode=1;