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'); const phase = process.argv[2] ?? 'after'; const origin = process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6065'; await fs.mkdir(output, { recursive: true }); const browser = await launchChromium({ headless: true }); const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } }); page.setDefaultTimeout(30000); const errors = [], measurements = [], checks = []; page.on('pageerror', error => errors.push(error.message)); page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); }); const settle = () => page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 12; i++) await new Promise(requestAnimationFrame); }); async function ready(count) { await page.waitForFunction(count => { const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot; return shadow && !shadow.querySelector('[aria-busy="true"]') && [...shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet) && (count === undefined || shadow.querySelectorAll('.hm-orgchart-employee').length === count); }, count, { timeout: 60000 }); await settle(); } async function measure(name) { const value = await page.evaluate(() => { const shadow = document.querySelector('.hm-orgchart-host').shadowRoot; const viewport = shadow.querySelector('.hm-orgchart-viewport'), forest = shadow.querySelector('.hm-orgchart-forest'); const rect = element => { const r = element.getBoundingClientRect(); return { left:r.left,top:r.top,width:r.width,height:r.height,right:r.right,bottom:r.bottom }; }; const union = elements => { const boxes = [...elements].filter(element => element.getClientRects().length && getComputedStyle(element).visibility !== 'hidden').map(rect).filter(r => r.width > 0 && r.height > 0); if (!boxes.length) return null; const left = Math.min(...boxes.map(r => r.left)), right = Math.max(...boxes.map(r => r.right)); const top = Math.min(...boxes.map(r => r.top)), bottom = Math.max(...boxes.map(r => r.bottom)); return { left,right,top,bottom,width:right-left,height:bottom-top }; }; const v = rect(viewport), f = rect(forest); const content = { left:v.left+viewport.clientLeft,top:v.top+viewport.clientTop,width:viewport.clientWidth,height:viewport.clientHeight }; const cx = content.left + content.width/2, cy = content.top + content.height/2; const trees = union(shadow.querySelectorAll('.hm-orgchart-tree')); const visible = union(shadow.querySelectorAll('.hm-orgchart-organization')); const error = bounds => bounds ? { x:(bounds.left+bounds.right)/2-cx,y:(bounds.top+bounds.bottom)/2-cy } : null; return { wrapper:rect(shadow.querySelector('.hm-orgchart-expanded')),viewport:v,content,contentCenter:{x:cx,y:cy},forest:f,trees,visible, error:{forest:error(f),trees:error(trees),visible:error(visible)},scale:Number(viewport.dataset.scale),x:Number(viewport.dataset.panX),y:Number(viewport.dataset.panY), unscaledForest:{width:forest.offsetWidth,height:forest.offsetHeight},employees:shadow.querySelectorAll('.hm-orgchart-employee').length, companies:shadow.querySelectorAll('.node[data-parent-id=""]').length,layout:shadow.querySelector('.hm-orgchart-expanded').dataset.autoLayout }; }); measurements.push({ name, ...value }); await fs.writeFile(path.join(output, `${phase}-progress.json`), JSON.stringify({ measurements,checks,errors }, null, 2)); return value; } const check = (pass, name, detail) => { checks.push({name,status:pass?'PASS':'FAIL',detail}); if (!pass) console.log('FAIL',name,JSON.stringify(detail)); }; const centered = (s, name, vertical = false) => check(Math.abs(s.error.visible.x) < 0.75 && (!vertical || Math.abs(s.error.visible.y) < 0.75), name, s.error); async function select(label, option) { await page.getByRole('combobox', { name:label, exact:true }).click(); await page.getByRole('option', { name:option, exact:true }).click(); await ready(); } async function companies(names) { await page.getByRole('button',{name:/^회사 선택/}).click(); const panel = page.getByRole('dialog',{name:'회사 선택',exact:true}); await panel.waitFor(); const all = panel.getByRole('checkbox',{name:'전체 선택',exact:true}); for (let i=0;i<2 && await all.getAttribute('aria-checked') !== 'false';i++) { await all.click(); await ready(); } await ready(0); if (names.length === 7) { await all.click(); await ready(140); await page.keyboard.press('Escape'); await panel.waitFor({state:'hidden'}); return; } for (const name of names) { await panel.getByRole('checkbox',{name,exact:true}).click(); await ready(); } await page.keyboard.press('Escape'); await panel.waitFor({state:'hidden'}); await ready(); } async function count(count, total) { await select('회사당 직원 수',`${count}명`); const apply = page.getByRole('button',{name:'데이터 적용',exact:true}); if (await apply.isEnabled()) await apply.click(); await ready(total); } async function open() { await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`); await page.getByRole('button',{name:/^회사 선택/}).waitFor(); await ready(140); } async function fit() { await page.getByRole('button',{name:'Fit to View',exact:true}).click(); await settle(); } try { await open(); const initial = await measure('initial-7x20-tb'); await page.screenshot({path:path.join(output,`${phase}-initial.png`),fullPage:true}); if (phase === 'after') { centered(initial,'Initial 7-company forest horizontally centered'); check(initial.scale === 1,'Initial oversized forest keeps 100% scale',initial.scale); } for (const [companyNames, label] of [[['한맥기술'],'one'],[['한맥기술','삼안'],'two'],[['한맥기술','삼안','장헌산업','장헌','피티씨','한라','바론'],'seven']]) { if (process.argv[3] && process.argv[3] !== label) continue; for (const size of [20,100,300]) { for (const [type,labelText] of [['tb','세로형 TB'],['lr','가로형 LR'],['hybrid','혼합형 Hybrid']]) { await open(); await companies(companyNames); await count(size,companyNames.length*size); await select('조직도 표시 유형',labelText); const s = await measure(`${label}-${size}-${type}-auto`); if (phase === 'after') { centered(s,`${label}/${size}/${type}: automatic horizontal center`); check(s.scale === 1,`${label}/${size}/${type}: no automatic shrink`,s.scale); } await fit(); const fitted = await measure(`${label}-${size}-${type}-fit`); if (phase === 'after') { centered(fitted,`${label}/${size}/${type}: Fit center both axes`,true); check(fitted.visible.left >= fitted.content.left+15 && fitted.visible.right <= fitted.content.left+fitted.content.width-15 && fitted.visible.top >= fitted.content.top+15 && fitted.visible.bottom <= fitted.content.top+fitted.content.height-15,`${label}/${size}/${type}: Fit contains visible tree`,fitted.visible); } } } } if (phase === 'after') { // User Pan preservation across resize and deliberate content/layout changes. await open(); await companies(['한맥기술']); await fit(); const viewport = page.locator('.hm-orgchart-viewport'); await viewport.scrollIntoViewIfNeeded(); await viewport.focus(); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowUp'); await settle(); const panned = await measure('user-pan'); await page.setViewportSize({width:1280,height:1100}); await ready(); const resized = await measure('pan-resize'); await companies(['한맥기술','삼안']); const filtered = await measure('pan-company'); await count(100,200); const changed = await measure('pan-data'); await select('조직도 표시 유형','가로형 LR'); const switched = await measure('pan-layout'); for (const [name,s] of [['resize',resized],['company selection',filtered],['data',changed],['layout',switched]]) { check(s.x===panned.x && s.y===panned.y && s.scale===panned.scale,`Manual Pan retained through ${name}`,{before:panned,after:s}); } await fit(); centered(await measure('fit-after-pan'),'Explicit Fit restores center after user Pan',true); for (const width of [390,768,1280,1440]) { await page.setViewportSize({width,height:1100}); await open(); await companies(['한맥기술']); centered(await measure(`width-${width}-initial`),`${width}px automatic horizontal center`); await fit(); centered(await measure(`width-${width}-fit`),`${width}px Fit center`,true); } await page.setViewportSize({width:1440,height:1100}); await open(); await companies(['한맥기술']); await fit(); await page.screenshot({path:path.join(output,'after-single-fit.png'),fullPage:true}); } } catch(error) { check(false,'Measurement completed',{message:error.message,stack:error.stack}); } check(errors.length===0,'Browser 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)); console.log(JSON.stringify({phase,measurements:measurements.length,pass:result.pass,fail:result.fail,initial:measurements[0],output:path.join(output,`${phase}.json`)})); await browser.close(); if(result.fail) process.exitCode=1;