107 lines
8.6 KiB
JavaScript
107 lines
8.6 KiB
JavaScript
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;
|