ความคืบหน้างาน ไฟล์ต้นฉบับ

tracker/e2e/mockup-contrast-check.mjs

// Read-only on server data: edits are local and are never saved.
import {pathToFileURL} from 'node:url';
const [chrome,base,out,modulePath]=process.argv.slice(2);
const {chromium}=await import(pathToFileURL(modulePath).href),browser=await chromium.launch({executablePath:chrome,headless:true,args:['--no-sandbox']});
let passed=0;const failures=[],errors=[];
async function audit(page,selectors,label){
 const results=await page.evaluate(selectors=>{
  const parse=s=>{const n=s.match(/[\d.]+/g)?.map(Number)||[0,0,0,0];return [n[0],n[1],n[2],n.length>3?n[3]:1]};
  const over=(f,b)=>[...f.slice(0,3).map((v,i)=>v*f[3]+b[i]*(1-f[3])),1];
  const lum=c=>c.slice(0,3).map(v=>{v/=255;return v<=.04045?v/12.92:((v+.055)/1.055)**2.4}).reduce((s,v,i)=>s+v*[.2126,.7152,.0722][i],0);
  return selectors.flatMap(selector=>[...document.querySelectorAll(selector)].filter(e=>e.getBoundingClientRect().width>0).map(e=>{
   let bg=[255,255,255,1],opacity=1;const ancestors=[];for(let p=e;p;p=p.parentElement)ancestors.unshift(p);
   for(const p of ancestors){const style=getComputedStyle(p);opacity*=Number(style.opacity);const color=parse(style.backgroundColor);color[3]*=opacity;bg=over(color,bg);}
   const style=getComputedStyle(e);let fg=parse(style.color);
   if(e instanceof SVGElement){fg=parse(style.fill);const shape=e.closest('.mock-node')?.querySelector('.mock-node-shape');if(shape)bg=over(parse(getComputedStyle(shape).fill),bg);else if(e.closest('.mock-edge'))bg=over(parse(style.stroke),bg);}
   fg[3]*=opacity;fg=over(fg,bg);const a=lum(fg),b=lum(bg);return {selector,text:e.textContent.trim().slice(0,35),ratio:(Math.max(a,b)+.05)/(Math.min(a,b)+.05),fg,bg};
  }));
 },selectors);
 if(!results.length)throw Error('no contrast targets for '+label);
 for(const row of results){if(row.ratio<4.5)failures.push({...row,label});else passed++;}
 console.log(label+': '+results.length+' contrast pairs, minimum '+Math.min(...results.map(r=>r.ratio)).toFixed(2)+':1');
}
try {
 for(const mode of [{name:'dark',os:'light',choice:'dark'},{name:'system-dark',os:'dark',choice:'system'},{name:'light',os:'dark',choice:'light'},{name:'system-light',os:'light',choice:'system'}]){
  const context=await browser.newContext({viewport:{width:1600,height:1100},colorScheme:mode.os});await context.addInitScript(choice=>{if(choice==='system')localStorage.removeItem('tracker-theme');else localStorage.setItem('tracker-theme',choice)},mode.choice);const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));await page.route('https://fonts.googleapis.com/**',r=>r.abort());await page.route('https://fonts.gstatic.com/**',r=>r.abort());
  await page.goto(base+'/mockup/screens/A01',{waitUntil:'domcontentloaded'});await page.locator('#mock-detail-image').evaluate(img=>img.decode());
  await audit(page,['.mock-tabs a[aria-current]','.mock-tabs small','.mock-tabs a[aria-current] span','.mock-variants button[aria-pressed=true]','.mock-detail-info dt','.mock-detail-info dd','.mock-pill','.mock-detail-heading p'],mode.name+' detail');
  for(const variant of ['default','loading','empty','error','pending','success']){await page.click('[data-variant='+variant+']');await audit(page,['.mock-variants button[aria-pressed=true]'],mode.name+' variant '+variant);}
  if(mode.name==='dark'){await page.locator('#mock-detail-image').evaluate(img=>img.decode());await page.screenshot({path:out+'/dark-detail.png'});}
  await page.goto(base+'/mockup/page-flow',{waitUntil:'domcontentloaded'});await audit(page,['.mock-flow-name','.mock-flow-group-count'],mode.name+' module groups');
  await page.goto(base+'/mockup/page-flow/W-41',{waitUntil:'domcontentloaded'});await page.waitForFunction(()=>document.querySelectorAll('.mock-node').length>0&&!document.querySelector('#mock-reload').disabled);await page.click('#mock-connect');await page.locator('[data-node=step-1]').focus();await page.keyboard.press('Enter');await page.fill('#mock-title','ตรวจสี ไม่บันทึกข้อมูลส่วนกลาง');
  await audit(page,['.mock-tabs a[aria-current]','.mock-tabs small','#mock-connect','#mock-save','.mock-notice','.mock-properties label','.mock-node-title','.mock-node-sub','.mock-node-body','.mock-edge text'],mode.name+' flow');
  await page.hover('#mock-save');await audit(page,['#mock-save'],mode.name+' save hover');await page.hover('#mock-connect');await audit(page,['#mock-connect'],mode.name+' connect hover');
  if(mode.name==='dark')await page.screenshot({path:out+'/dark-flow.png'});
  await context.close();
 }
 if(errors.length)throw Error(errors.join('\n'));
 if(failures.length){console.error(JSON.stringify(failures,null,2));throw Error(failures.length+' contrast pairs below 4.5:1');}
 console.log(passed+' readable contrast pairs passed across explicit/OS light and dark themes');
}finally{await browser.close()}