Files
HMDesign/examples/org-chart/verify-p9-5-full.mjs
T

95 lines
12 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
const root = fileURLToPath(new URL('../../', import.meta.url));
const output = path.join(root,'test-results/org-chart-p9-5-full');
const phase = process.argv[2] ?? 'after';
const origin = process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6064';
const sizes = [{width:1920,height:1080},{width:1920,height:1440},{width:1440,height:900},{width:1280,height:1024},{width:390,height:844}];
const browser = await launchChromium({headless:true});
const page = await browser.newPage({viewport:sizes[0]});
const measurements = [], checks = [], errors = [];
page.setDefaultTimeout(30000);
page.on('pageerror',e=>errors.push(e.message));
page.on('console',m=>{if(m.type()==='error')errors.push(m.text());});
await fs.mkdir(output,{recursive:true});
const check=(pass,name,detail)=>{checks.push({name,status:pass?'PASS':'FAIL',detail});if(!pass)console.log('FAIL',name,JSON.stringify(detail));};
const settle=()=>page.evaluate(async()=>{await document.fonts.ready;for(let i=0;i<16;i++)await new Promise(requestAnimationFrame);});
async function ready(){
await page.waitForFunction(()=>{const s=document.querySelector('.hm-orgchart-host')?.shadowRoot;return s?.querySelector('.hm-orgchart-stage[data-workdesk-artwork]')&&!s.querySelector('[aria-busy="true"]')&&[...s.querySelectorAll('link[rel="stylesheet"]')].every(l=>l.sheet);});
await page.evaluate(async()=>{await Promise.all([...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('img')].map(async i=>{i.loading='eager';await i.decode();}));}); await settle();
}
async function snapshot(name){
const s=await page.evaluate(async()=>{
const host=document.querySelector('.hm-orgchart-story-full'),shadow=host.querySelector('.hm-orgchart-host').shadowRoot;
const wrapper=shadow.querySelector('.hm-orgchart-composition'),stage=shadow.querySelector('.hm-orgchart-stage');
const rect=e=>{const r=e.getBoundingClientRect();return{left:r.left,right:r.right,top:r.top,bottom:r.bottom,width:r.width,height:r.height};};
const hb=rect(host),st=rect(stage),css=getComputedStyle(host),scale=st.width/1920;
const content={left:hb.left+parseFloat(css.borderLeftWidth)+parseFloat(css.paddingLeft),right:hb.right-parseFloat(css.borderRightWidth)-parseFloat(css.paddingRight),top:hb.top+parseFloat(css.borderTopWidth)+parseFloat(css.paddingTop),bottom:hb.bottom-parseFloat(css.borderBottomWidth)-parseFloat(css.paddingBottom)};
content.width=content.right-content.left;content.height=content.bottom-content.top;
const union=items=>{const b=[...items].map(rect).filter(r=>r.width&&r.height);const left=Math.min(...b.map(r=>r.left)),right=Math.max(...b.map(r=>r.right)),top=Math.min(...b.map(r=>r.top)),bottom=Math.max(...b.map(r=>r.bottom));return{left,right,top,bottom,width:right-left,height:bottom-top};};
const norm=b=>({x:(b.left-st.left)/scale,y:(b.top-st.top)/scale,width:b.width/scale,height:b.height/scale});
const heading=rect(stage.querySelector('.hm-orgchart-heading')),side=rect(stage.querySelector('.hm-orgchart-slot-side'));
const axis=rect(stage.querySelector('.hm-orgchart-axis')),tree=rect(stage.querySelector('.hm-orgchart-viewport'));
const status=rect(stage.querySelector('.hm-orgchart-slot-belowTree')),footer=rect(stage.querySelector('.hm-orgchart-slot-footer'));
const center=union(stage.querySelectorAll('.hm-orgchart-axis .hm-orgchart-employee,.hm-orgchart-hub,.hm-orgchart-viewport .hm-orgchart-employee,.hm-orgchart-viewport .hm-orgchart-organization-name'));
const cards=[...stage.querySelectorAll('.hm-orgchart-employee')].map(e=>({id:e.dataset.employeeId,rect:rect(e),box:norm(rect(e)),role:e.querySelector('.hm-orgchart-person-role')?.textContent,name:e.querySelector('.hm-orgchart-person-name')?.textContent,asset:e.dataset.cardAssetSize,style:Object.fromEntries(['fontFamily','fontSize','fontWeight','lineHeight','backgroundImage','backgroundColor','width','height'].map(k=>[k,getComputedStyle(e)[k]]))}));
const titles=[...stage.querySelectorAll('.hm-orgchart-viewport .hm-orgchart-organization-name')].map(e=>({id:e.closest('.node').dataset.organizationId,rect:rect(e),box:norm(rect(e)),style:Object.fromEntries(['fontFamily','fontSize','fontWeight','lineHeight','color','backgroundImage','width','height'].map(k=>[k,getComputedStyle(e)[k]]))}));
const arrows=await Promise.all([...stage.querySelectorAll('image[data-arrow]')].map(async e=>{
const img=new Image();img.src=e.getAttribute('href');await img.decode();
const box=rect(e);const blockers=[...stage.querySelectorAll('.hm-orgchart-employee,.hm-orgchart-organization-name,.hm-orgchart-hub')].filter(el=>{const r=rect(el);return Math.min(box.right,r.right)-Math.max(box.left,r.left)>.5&&Math.min(box.bottom,r.bottom)-Math.max(box.top,r.top)>.5;}).map(e=>e.className);
return{id:e.dataset.arrow,rect:box,box:norm(box),loaded:img.naturalWidth>0,natural:[img.naturalWidth,img.naturalHeight],blockers,display:getComputedStyle(e).display,visibility:getComputedStyle(e).visibility};
}));
const paths=[...stage.querySelectorAll('.hm-orgchart-workdesk-overlay path')].map(e=>({id:e.dataset.connector,d:e.getAttribute('d'),rect:rect(e)}));
const decorations=[...stage.querySelectorAll('.hm-orgchart-workdesk-decoration')].map(e=>({id:e.dataset.workdeskAsset,box:norm(rect(e)),rect:rect(e)}));
const centralCards=cards.filter(c=>!stage.querySelector('.hm-orgchart-slot-belowTree [data-employee-id="'+c.id+'"]'));
const collisions=centralCards.filter(c=>Math.min(c.rect.right,status.right)-Math.max(c.rect.left,status.left)>.5&&Math.min(c.rect.bottom,status.bottom)-Math.max(c.rect.top,status.top)>.5).map(c=>c.id);
const scroll=e=>({width:e.scrollWidth,clientWidth:e.clientWidth,height:e.scrollHeight,clientHeight:e.clientHeight,overflowX:getComputedStyle(e).overflowX,overflowY:getComputedStyle(e).overflowY});
return{size:{width:innerWidth,height:innerHeight},host:hb,content,wrapper:rect(wrapper),stage:st,scale,baseWidth:stage.offsetWidth,stageLayoutHeight:parseFloat(getComputedStyle(stage).height),top:{heading,side,bounds:union([stage.querySelector('.hm-orgchart-heading'),stage.querySelector('.hm-orgchart-slot-side')])},axis,tree,center,status,footer,
logical:{heading:norm(heading),side:norm(side),axis:norm(axis),tree:norm(tree),status:norm(status),footer:norm(footer),center:norm(center)},gap:footer.top-status.bottom,gapDesign:(footer.top-status.bottom)/scale,
horizontalError:(st.left+st.right-content.left-content.right)/2,centerOffset:Number(wrapper.dataset.centerOffset)||0,
centerAvailable:{top:Math.max(heading.bottom,side.bottom),bottom:status.top},centerErrorY:(center.top+center.bottom-Math.max(heading.bottom,side.bottom)-status.top)/2,
cards,titles,arrows,paths,decorations,collisions,totals:stage.querySelector('.hm-orgchart-slot-side').textContent,affiliations:stage.querySelector('.hm-orgchart-slot-footer').textContent,
layer:{overlay:getComputedStyle(stage.querySelector('.hm-orgchart-workdesk-overlay')).zIndex,tree:getComputedStyle(stage.querySelector('.hm-orgchart-viewport')).zIndex},
scroll:{root:scroll(document.documentElement),body:scroll(document.body),host:scroll(host),wrapper:scroll(wrapper)}};
});measurements.push({name,...s});await fs.writeFile(path.join(output,`${phase}-progress.json`),JSON.stringify({measurements,checks,errors},null,2));return s;
}
try{
const baseline=phase==='after'?JSON.parse(await fs.readFile(path.join(output,'before.json'),'utf8')):null;
for(const size of sizes){
await page.setViewportSize(size);await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`);await ready();
const name=`${size.width}x${size.height}`,s=await snapshot(name);
await page.screenshot({path:path.join(output,`${phase}-${name}.png`),fullPage:true});
if(phase==='after'){
const old=baseline.measurements.find(m=>m.name===name);
check(Math.abs(s.horizontalError)<=1,`${name}: wrapper horizontal center`,s.horizontalError);
check(s.arrows.length===5&&s.arrows.every(a=>a.loaded&&a.visibility==='visible'&&a.display!=='none'&&!a.blockers.length),`${name}: 5 loaded arrows without card/title intersections`,s.arrows);
check(s.gapDesign>=12&&s.gapDesign<=16,`${name}: status/footer design gap`,{before:old.gapDesign,after:s.gapDesign,visible:s.gap});
check(s.collisions.length===0,`${name}: center/status no collisions`,s.collisions);
check(s.cards.length===old.cards.length&&s.cards.every((c,i)=>c.id===old.cards[i].id&&JSON.stringify(c.style)===JSON.stringify(old.cards[i].style))&&s.titles.every((t,i)=>t.id===old.titles[i].id&&JSON.stringify(t.style)===JSON.stringify(old.titles[i].style)),`${name}: card/title typography and geometry preserved`);
check(s.totals===old.totals&&s.affiliations===old.affiliations,`${name}: headcounts/logos/text preserved`);
const movedBox=(current,previous)=>Math.abs(current.x-previous.x)<.1&&Math.abs(current.width-previous.width)<.1&&Math.abs(current.height-previous.height)<.1&&Math.abs(current.y-previous.y-s.centerOffset)<.1;
const inOldStatus=c=>c.box.x>=old.logical.status.x-.1&&c.box.x+c.box.width<=old.logical.status.x+old.logical.status.width+.1&&c.box.y>=old.logical.status.y-.1&&c.box.y+c.box.height<=old.logical.status.y+old.logical.status.height+.1;
check(s.cards.every((c,i)=>inOldStatus(old.cards[i])||movedBox(c.box,old.cards[i].box))&&s.titles.every((t,i)=>movedBox(t.box,old.titles[i].box))&&movedBox(s.logical.axis,old.logical.axis)&&movedBox(s.logical.tree,old.logical.tree),`${name}: entire central chart keeps relative coordinates`,s.centerOffset);
check(s.arrows.every((a,i)=>a.id===old.arrows[i].id&&movedBox(a.box,old.arrows[i].box))&&s.paths.every((p,i)=>p.id===old.paths[i].id&&p.d===old.paths[i].d)&&s.decorations.every((d,i)=>d.id===old.decorations[i].id&&movedBox(d.box,old.decorations[i].box)),`${name}: 5 arrows/connectors/decorations share center offset without resizing`);
check(s.scroll.root.width<=size.width+1&&s.scroll.root.height<=size.height+1&&s.scroll.body.width<=size.width+1&&s.scroll.body.height<=size.height+1,`${name}: document no horizontal/vertical scroll`,s.scroll);
const outside=[...s.cards.map(c=>c.rect),...s.titles.map(t=>t.rect),...s.arrows.map(a=>a.rect),s.top.heading,s.top.side,s.footer].filter(r=>r.left<s.content.left-1||r.right>s.content.right+1||r.top<s.content.top-1||r.bottom>s.content.bottom+1);
check(!outside.length,`${name}: all actual content contained, no overflow clipping`,outside);
check(Math.abs(s.logical.heading.y-old.logical.heading.y)<.2&&Math.abs(s.logical.side.y-old.logical.side.y)<.2,`${name}: top anchors unchanged`,{old:old.top,after:s.top});
const footerInset=(s.stage.bottom-s.footer.bottom)/s.scale;
check(Math.abs(footerInset-(old.stage.bottom-old.footer.bottom)/old.scale)<.2,`${name}: footer bottom anchor`,footerInset);
const centerBase=old.logical.center;
const deltas=s.cards.filter(c=>!c.id.startsWith('leave-')&&!c.id.startsWith('dispatch-')).map(c=>{const previous=old.cards.find(o=>o.id===c.id);return{x:c.box.x-previous.box.x,y:c.box.y-previous.box.y};});
const central=s.cards.filter(c=>s.tree.top<=c.rect.top&&c.rect.bottom<=s.tree.bottom+.5);
check(Math.abs(s.scale-Math.min(1,s.content.width/1920,s.content.height/914.5))<.002,`${name}: one proportional scale without enlargement`,s.scale);
if(s.centerAvailable.bottom-s.centerAvailable.top>=s.center.height)check(Math.abs(s.centerErrorY)<1,`${name}: center occupies available vertical middle`,s.centerErrorY);
}
}
}catch(e){check(false,'Full layout measurement completed',{message:e.message,stack:e.stack});}
check(!errors.length,'Console/page errors zero',errors);
const result={phase,origin,browser:browser.version(),measurements,checks,errors,pass:checks.filter(c=>c.status==='PASS').length,fail:checks.filter(c=>c.status==='FAIL').length};
await fs.writeFile(path.join(output,`${phase}.json`),JSON.stringify(result,null,2));await browser.close();
console.log(JSON.stringify({phase,measurements:measurements.map(m=>({name:m.name,scale:m.scale,h:m.stageLayoutHeight,center:m.logical.center,status:m.logical.status,footer:m.logical.footer,gap:m.gapDesign,arrows:m.arrows.map(a=>({id:a.id,blockers:a.blockers})),centerError:m.centerErrorY})),pass:result.pass,fail:result.fail}));if(result.fail)process.exitCode=1;