196 lines
24 KiB
JavaScript
196 lines
24 KiB
JavaScript
import http from 'node:http';
|
||
import path from 'node:path';
|
||
import fs from 'node:fs/promises';
|
||
import { createHash } from 'node:crypto';
|
||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||
|
||
// P3.1 change-specific checks. The prior 732 checks and their evidence are retained.
|
||
const root = fileURLToPath(new URL('../../', import.meta.url));
|
||
const output = path.join(root, 'test-results/org-chart-p3.1');
|
||
const fixtureBytes = await fs.readFile(path.join(root, 'examples/org-chart/data-p1.json'));
|
||
const fixture = JSON.parse(fixtureBytes);
|
||
const { chromium } = await import(pathToFileURL(process.env.ORGCHART_PLAYWRIGHT_MODULE ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright/index.mjs').href);
|
||
const viewports = [{ width: 1920, height: 1080 }, { width: 1440, height: 900 }, { width: 1280, height: 800 }, { width: 768, height: 1024 }, { width: 390, height: 844 }];
|
||
const results = [], measurements = [], fontEvidence = [], errors = [], servers = [];
|
||
const check = (pass, name, detail) => results.push({ name, status: pass ? 'PASS' : 'FAIL', ...(detail === undefined ? {} : { detail }) });
|
||
const near = (a, b, tolerance = .4) => Number.isFinite(a) && Math.abs(a - b) <= tolerance;
|
||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.ttf': 'font/ttf', '.svg': 'image/svg+xml' };
|
||
await fs.mkdir(output, { recursive: true });
|
||
async function serve(directory) {
|
||
const server = http.createServer(async (request, response) => {
|
||
try {
|
||
const url = new URL(request.url, 'http://127.0.0.1');
|
||
if (url.pathname === '/__p31_link') return response.end('<!doctype html><link rel="icon" href="data:,"><p id="proof">Group link reached</p>');
|
||
if (url.pathname === '/__p31_photo.svg') { response.setHeader('Content-Type', 'image/svg+xml'); return response.end('<svg xmlns="http://www.w3.org/2000/svg" width="80" height="100"><rect width="80" height="100" fill="#334455"/></svg>'); }
|
||
if (url.pathname === '/__p31_bad_photo.png') { response.setHeader('Content-Type', 'image/png'); return response.end('intentional invalid image bytes'); }
|
||
const file = path.resolve(directory, '.' + decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname));
|
||
const relative = path.relative(directory, file);
|
||
if (relative.startsWith('..') || path.isAbsolute(relative)) return response.writeHead(403).end();
|
||
response.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream');
|
||
response.end(await fs.readFile(file));
|
||
} catch { response.writeHead(404).end(); }
|
||
});
|
||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); servers.push(server);
|
||
return `http://127.0.0.1:${server.address().port}`;
|
||
}
|
||
async function ready(page, mode = 'readOnly') {
|
||
await page.waitForFunction(mode => {
|
||
const s = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
||
return s?.querySelector(`.hm-orgchart-composition[data-display-mode="${mode}"] [data-card-asset]`) && [...s.querySelectorAll('link[rel=stylesheet]')].every(l => l.sheet);
|
||
}, mode, { timeout: 25000 });
|
||
await page.evaluate(async () => {
|
||
await document.fonts.load('700 20px "Noto Sans KR"', '홍 길 동 협업 소통 허브 경영진 통합시스템');
|
||
await document.fonts.ready;
|
||
await Promise.all([...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('img')].map(i => i.decode().catch(() => {})));
|
||
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||
});
|
||
}
|
||
async function measure(page) {
|
||
return page.evaluate(() => {
|
||
const s = document.querySelector('.hm-orgchart-host').shadowRoot, stage = s.querySelector('.hm-orgchart-stage');
|
||
const origin = stage.getBoundingClientRect(), scale = origin.width / 1920;
|
||
const rect = e => { const r = e.getBoundingClientRect(); return { x: (r.x - origin.x) / scale, y: (r.y - origin.y) / scale, width: r.width / scale, height: r.height / scale }; };
|
||
const style = (e, props) => Object.fromEntries(props.map(p => [p, getComputedStyle(e)[p]]));
|
||
const props = ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing'];
|
||
const cards = [...s.querySelectorAll('.hm-orgchart-employee')].map(c => ({
|
||
id: c.dataset.employeeId, size: c.dataset.cardAssetSize, kind: c.dataset.cardKind, box: rect(c),
|
||
name: c.querySelector('.hm-orgchart-person-name').textContent, role: c.querySelector('.hm-orgchart-person-role').textContent,
|
||
badge: c.querySelector('.hm-orgchart-job-title')?.textContent ?? '', photo: c.dataset.photoState,
|
||
image: c.querySelector('.hm-orgchart-avatar img') && { src: c.querySelector('.hm-orgchart-avatar img').src, naturalWidth: c.querySelector('.hm-orgchart-avatar img').naturalWidth, placeholder: c.querySelector('.hm-orgchart-avatar img').dataset.photoPlaceholder },
|
||
avatar: { box: rect(c.querySelector('.hm-orgchart-avatar')), ...style(c.querySelector('.hm-orgchart-avatar'), ['borderRadius']) },
|
||
emptyAttributes: !!c.querySelector('.hm-orgchart-person-attributes:empty'),
|
||
text: [...c.querySelectorAll('.hm-orgchart-person-name,.hm-orgchart-person-role,.hm-orgchart-job-title')].map(e => ({ text: e.textContent, box: rect(e), clientWidth: e.clientWidth, scrollWidth: e.scrollWidth, ...style(e, props) })),
|
||
}));
|
||
const headers = [...s.querySelectorAll('.node[data-organization-id]')].map(n => {
|
||
const h = n.querySelector('.hm-orgchart-organization-name'), first = n.querySelector('.hm-orgchart-employee');
|
||
return { id: n.dataset.organizationId, depth: Number(n.dataset.organizationDepth), box: rect(h), gap: first ? rect(first).y - rect(h).y - rect(h).height : null, ...style(h, [...props, 'backgroundImage', 'textAlign', 'borderRadius', 'boxShadow']) };
|
||
});
|
||
const hub = s.querySelector('.hm-orgchart-hub'), label = hub.querySelector('.hm-orgchart-hub-label'), icon = hub.querySelector('.hm-orgchart-hub-icon');
|
||
const cells = [...s.querySelectorAll('.hm-orgchart-workdesk-position-table th,.hm-orgchart-workdesk-position-table td')].map(e => {
|
||
const r = document.createRange(); r.selectNodeContents(e);
|
||
const ranges = [...r.getClientRects()].filter(r => r.width > 0).map(r => ({ x: (r.x - origin.x) / scale, y: (r.y - origin.y) / scale, width: r.width / scale, height: r.height / scale }));
|
||
return { text: e.textContent, box: rect(e), ranges, ...style(e, ['whiteSpace', 'textOverflow', 'overflowX', 'paddingLeft', 'paddingRight', ...props]) };
|
||
});
|
||
const fontViolations = [...s.querySelectorAll('.hm-orgchart-shell *')].filter(e => [...e.childNodes].some(n => n.nodeType === 3 && n.textContent.trim()) && !getComputedStyle(e).fontFamily.includes('Noto Sans KR')).map(e => ({ class: e.className, text: e.textContent, fontFamily: getComputedStyle(e).fontFamily }));
|
||
const table = s.querySelector('.hm-orgchart-workdesk-position-table');
|
||
return { scale, cards, headers, hub: { box: rect(hub), icon: rect(icon), label: rect(label), axis: rect(s.querySelector('.hm-orgchart-axis')), ...style(hub, ['paddingLeft', 'paddingRight', 'gap']), font: style(label, props), paths: icon.querySelectorAll('path').length }, cells, fontViolations,
|
||
table: { box: rect(table), rows: [...table.rows].map(r => [...r.cells].map(c => c.textContent)), total: table.querySelector('[data-total-count]').textContent },
|
||
document: { clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, clientHeight: document.documentElement.clientHeight, scrollHeight: document.documentElement.scrollHeight },
|
||
outside: cards.filter(c => c.box.x < -.5 || c.box.y < -.5 || c.box.x + c.box.width > 1920.5 || c.box.y + c.box.height > 915),
|
||
connectorPaths: s.querySelectorAll('.hm-orgchart-workdesk-overlay path').length,
|
||
footer: s.querySelector('.hm-orgchart-affiliation-bar').textContent,
|
||
};
|
||
});
|
||
}
|
||
async function glyphFonts(page, route, selectors) {
|
||
const session = await page.context().newCDPSession(page);
|
||
await session.send('DOM.enable'); await session.send('CSS.enable');
|
||
for (const [name, selector] of selectors) {
|
||
const target = page.locator(selector).first();
|
||
await target.evaluate(e => e.setAttribute('data-p31-font-proof', 'yes'));
|
||
const { root: dom } = await session.send('DOM.getDocument', { depth: -1, pierce: true });
|
||
const find = node => node.attributes?.some((a, i) => i % 2 === 0 && a === 'data-p31-font-proof') ? node : [...(node.children ?? []), ...(node.shadowRoots ?? [])].map(find).find(Boolean);
|
||
const node = find(dom);
|
||
const value = await session.send('CSS.getPlatformFontsForNode', { nodeId: node.nodeId });
|
||
fontEvidence.push({ route, name, ...value });
|
||
// This variable font's internal family is "Noto Sans KR Thin" even at bold weights.
|
||
check(value.fonts.length > 0 && value.fonts.every(f => f.familyName.startsWith('Noto Sans KR') && f.postScriptName.startsWith('NotoSansKR') && f.isCustomFont && f.glyphCount > 0), `${route}: actual glyph font ${name}`, value.fonts);
|
||
await target.evaluate(e => e.removeAttribute('data-p31-font-proof'));
|
||
}
|
||
await session.detach();
|
||
}
|
||
function verify(m, route, v) {
|
||
const name = `${route}/${v.width}×${v.height}`;
|
||
check(!m.fontViolations.length, `${name}: every text family is scoped Noto Sans KR`, m.fontViolations);
|
||
check(m.cards.length === 43 && m.cards.every(c => c.name === '홍 길 동' && c.role === fixture.employees.find(e => e.id === c.id).position.replace(/\s+/gu, '')), `${name}: display-only name/position; 43 cards`, m.cards.map(c => [c.id,c.name,c.role]));
|
||
check(m.cards.every(c => c.badge !== '경영진' && c.badge === ((fixture.employees.find(e => e.id === c.id).jobTitle?.trim() === '경영진') ? '' : fixture.employees.find(e => e.id === c.id).jobTitle ?? '')), `${name}: executive badge absent; other raw badges retained`);
|
||
check(m.cards.every(c => !c.emptyAttributes), `${name}: no empty badge/attribute container`);
|
||
check(m.cards.every(c => { const [w,h] = c.size === 'representative' ? [146,74] : c.size === 'leader' ? [146,68] : c.size === 'employee' ? [108,52] : [114,54]; return near(c.box.width,w) && near(c.box.height,h); }), `${name}: approved card sizes and existing P3 status sizes preserved`);
|
||
check(m.cards.every(c => { const [w,h] = c.size === 'representative' ? [36,62] : c.size === 'leader' ? [32,56] : c.size === 'employee' ? [24,44] : [28,28]; return c.photo === 'placeholder' && c.image?.placeholder === 'true' && c.image.naturalWidth > 0 && near(c.avatar.box.width,w) && near(c.avatar.box.height,h) && c.avatar.borderRadius !== '50%'; }) && new Set(m.cards.map(c => c.image?.src)).size === 1, `${name}: common decoded square icon in every noncircular slot`);
|
||
check(m.headers.filter(h => h.depth === 1).every(h => h.fontSize === '20px' && h.fontWeight === '700' && h.lineHeight === '36px' && near(h.box.height,36)), `${name}: 1Depth 20/700/36 and header36`, m.headers.filter(h => h.depth === 1));
|
||
check(m.headers.filter(h => h.depth >= 2).every(h => { const small = h.id.startsWith('talent-growth-') || h.id.startsWith('business-planning-'); return h.fontSize === (small ? '18px':'20px') && h.fontWeight === (small?'600':'700') && h.lineHeight === (small?'16.9px':'19.7px') && near(h.box.width,114) && near(h.box.height,30.4); }), `${name}: 2Depth source typography/surface`, m.headers.filter(h => h.depth >= 2));
|
||
check(m.headers.every(h => h.gap === null || near(h.gap,8)), `${name}: all title-to-first-card gaps 8px`, m.headers.map(h => [h.id,h.gap]));
|
||
check(m.hub.font.fontSize === '21px' && m.hub.font.fontWeight === '750' && m.hub.font.lineHeight === '24px' && m.hub.paddingLeft === '22px' && m.hub.paddingRight === '24px' && m.hub.gap === '8px' && near(m.hub.box.height,42) && near(m.hub.icon.width,20) && near(m.hub.icon.height,20) && near(m.hub.icon.y+10,m.hub.label.y+m.hub.label.height/2) && near(m.hub.box.x+m.hub.box.width/2,m.hub.axis.x+m.hub.axis.width/2) && m.hub.paths === 3, `${name}: hub font/padding/icon/axis alignment`, m.hub);
|
||
check(m.cells.every(c => c.whiteSpace === 'nowrap' && c.textOverflow !== 'ellipsis' && c.ranges.length === 1 && c.ranges[0].x >= c.box.x + parseFloat(c.paddingLeft) - .5 && c.ranges[0].x+c.ranges[0].width <= c.box.x+c.box.width-parseFloat(c.paddingRight)+.5), `${name}: every table label fits on one line without clipping`, m.cells.filter(c => c.ranges.length !== 1 || c.ranges[0].width > c.box.width-parseFloat(c.paddingLeft)-parseFloat(c.paddingRight)+.5));
|
||
check(near(m.table.box.width,330,1) && m.table.total === '41' && m.footer.includes('41'), `${name}: table width/count and footer retained`, { table:m.table.box,total:m.table.total });
|
||
check(m.document.scrollWidth === m.document.clientWidth && m.document.scrollHeight === m.document.clientHeight && !m.outside.length, `${name}: no page scroll; every card inside stage`, m.document);
|
||
check(m.cards.every(c => c.text.every(t => t.scrollWidth <= t.clientWidth+1 && t.box.x >= c.box.x-.5 && t.box.x+t.box.width <= c.box.x+c.box.width+.5)), `${name}: card typography has no new horizontal overflow`);
|
||
}
|
||
const dialogTargets = [['representative','.hm-orgchart-top-leaders .hm-orgchart-employee'],['depth1','.node[data-organization-depth="1"] .hm-orgchart-employee[data-card-kind="leader"]'],['depth2','.node[data-organization-depth="2"] .hm-orgchart-employee[data-card-kind="leader"]'],['employee','.hm-orgchart-engine .hm-orgchart-employee[data-card-kind="employee"]'],['dispatched','.hm-orgchart-status-group[data-work-status="dispatched"] .hm-orgchart-employee'],['onLeave','.hm-orgchart-status-group[data-work-status="onLeave"] .hm-orgchart-employee']];
|
||
async function dialogs(page, route, viewport) {
|
||
await page.setViewportSize(viewport); await ready(page);
|
||
for (const [kind, selector] of dialogTargets) {
|
||
const card = page.locator(selector).first(), dialog = page.locator('dialog[open]');
|
||
const id = await card.getAttribute('data-employee-id');
|
||
await card.click(); await dialog.waitFor({ state:'visible' });
|
||
const value = await dialog.evaluate(d => ({ fields:Object.fromEntries([...d.querySelectorAll('dt')].map(t=>[t.textContent,t.nextElementSibling.textContent])), font:getComputedStyle(d).fontFamily, width:d.getBoundingClientRect().width, scaled:!!d.closest('.hm-orgchart-stage'), transform:getComputedStyle(d).transform }));
|
||
const employee = fixture.employees.find(e=>e.id===id);
|
||
check(value.fields['이름']==='홍 길 동' && value.fields['직위']===employee.position && (!employee.jobTitle || value.fields['직책']===employee.jobTitle) && value.font.includes('Noto Sans KR') && !value.scaled && value.transform==='none' && near(value.width,Math.min(420,viewport.width-32)), `${route}/${viewport.width}: ${kind} popup retains data/font/unscaled size`, value);
|
||
if (kind==='representative' && viewport.width===1920) await glyphFonts(page,route,[['popup name','dialog[open] dd']]);
|
||
await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab');
|
||
check(await dialog.evaluate(d=>d.contains(d.getRootNode().activeElement)),`${route}/${viewport.width}: ${kind} modal focus trap`);
|
||
await page.keyboard.press('Escape'); await dialog.waitFor({state:'hidden'});
|
||
check(await card.evaluate(e=>e.getRootNode().activeElement===e),`${route}/${viewport.width}: ${kind} ESC focus return`);
|
||
if(kind==='representative') { await card.press('Enter'); await dialog.waitFor({state:'visible'}); await dialog.locator('button').click(); await dialog.waitFor({state:'hidden'}); await card.press('Space'); await dialog.waitFor({state:'visible'}); await page.keyboard.press('Escape'); check(await card.evaluate(e=>e.getRootNode().activeElement===e),`${route}/${viewport.width}: Enter/Space/close preserve activation`); }
|
||
}
|
||
}
|
||
async function setData(page,data,transport) { await page.evaluate(async({data,transport})=>{ if(transport==='react')window.OrgChartP1React.setData(data);else await window.OrgChartP1.setData(data); },{data,transport}); await ready(page); }
|
||
let browser;
|
||
try {
|
||
const htmlOrigin = await serve(path.join(root,'examples/org-chart/dist'));
|
||
const reactOrigin = await serve(path.join(root,'examples/org-chart-react/dist'));
|
||
browser = await chromium.launch({executablePath:process.env.ORGCHART_CHROME ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe',headless:true});
|
||
const context = await browser.newContext();
|
||
const routes = [{name:'html',url:`${htmlOrigin}/p1.html?mode=readOnly`},{name:'file-classic',url:pathToFileURL(path.join(root,'examples/org-chart/dist/p1-classic.html')).href+'?mode=readOnly'},{name:'react',url:`${reactOrigin}/p1.html?mode=readOnly`},{name:'storybook',url:'http://127.0.0.1:6041/iframe.html?id=orgchart--full-composition&viewMode=story'}];
|
||
for(const route of routes) {
|
||
const page = await context.newPage(); page.on('pageerror',e=>errors.push({route:route.name,type:'runtime',message:e.message})); page.on('console',e=>{if(e.type()==='error')errors.push({route:route.name,type:'console',message:e.text()});});
|
||
try {
|
||
await page.goto(route.url); await ready(page);
|
||
for(const viewport of viewports) { await page.setViewportSize(viewport); await ready(page); const m = await measure(page); measurements.push({route:route.name,...viewport,...m}); verify(m,route.name,viewport); if([1920,390].includes(viewport.width))await page.screenshot({path:path.join(output,`${route.name}-${viewport.width}x${viewport.height}.png`)}); }
|
||
await page.setViewportSize(viewports[0]); await ready(page);
|
||
await glyphFonts(page,route.name,[['card name','.hm-orgchart-person-name'],['card position','.hm-orgchart-person-role'],['1Depth','.node[data-organization-depth="1"] .hm-orgchart-organization-name'],['2Depth','.node[data-organization-depth="2"] .hm-orgchart-organization-name'],['hub','.hm-orgchart-hub-label'],['table','.hm-orgchart-workdesk-position-table th'],['footer','.hm-orgchart-affiliation-bar-item dt'],['status','.hm-orgchart-status-group .hm-orgchart-summary-title']]);
|
||
await dialogs(page,route.name,viewports[0]); await dialogs(page,route.name,viewports.at(-1));
|
||
if(['html','react'].includes(route.name)) {
|
||
const data = structuredClone(fixture); data.employees[0].profileImage=`${htmlOrigin}/__p31_photo.svg`; data.employees[1].profileImage=`${htmlOrigin}/__p31_bad_photo.png`; data.employees[2].position='책 임';
|
||
await setData(page,data,route.name);
|
||
const imageState = await page.evaluate(ids=>ids.map(id=>{const c=document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(`[data-employee-id="${id}"]`),i=c.querySelector('.hm-orgchart-avatar img');return {id,photo:c.dataset.photoState,src:i.src,width:i.naturalWidth,role:c.querySelector('.hm-orgchart-person-role').textContent};}),data.employees.slice(0,3).map(e=>e.id));
|
||
check(imageState[0].photo==='image' && imageState[0].width===80 && imageState[1].photo==='placeholder' && imageState[1].width===64 && imageState[2].role==='책임' && data.employees[2].position==='책 임',`${route.name}: real image precedence/load-failure fallback/display whitespace`,imageState);
|
||
await setData(page,fixture,route.name);
|
||
// The preserved fixture has position=경영진 but no jobTitle=경영진.
|
||
// Exercise the requested badge policy with in-memory variants, never edit JSON.
|
||
const executiveIds = await page.evaluate(selectors=>selectors.map(selector=>document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(selector).dataset.employeeId),dialogTargets.map(t=>t[1]));
|
||
const executiveData = structuredClone(fixture);
|
||
for(const employee of executiveData.employees)if(executiveIds.includes(employee.id))employee.jobTitle='경영진';
|
||
await setData(page,executiveData,route.name);
|
||
const executiveState = await page.evaluate(ids=>[...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('.hm-orgchart-employee')].filter(c=>ids.includes(c.dataset.employeeId)).map(c=>({id:c.dataset.employeeId,badge:c.querySelector('.hm-orgchart-job-title')?.textContent,role:c.querySelector('.hm-orgchart-person-role').textContent,empty:!!c.querySelector('.hm-orgchart-person-attributes:empty')})),executiveIds);
|
||
check(executiveState.length>=6 && executiveState.every(c=>!c.badge&&!c.empty&&c.role===fixture.employees.find(e=>e.id===c.id).position),`${route.name}: executive badge suppressed in all six card roles; positions preserved`,executiveState);
|
||
await page.locator(dialogTargets[0][1]).click();await page.locator('dialog[open]').waitFor({state:'visible'});
|
||
const rawRole=await page.locator('dialog[open]').evaluate(d=>[...d.querySelectorAll('dt')].find(t=>t.textContent==='직책')?.nextElementSibling.textContent);
|
||
check(rawRole==='경영진' && executiveData.employees.find(e=>e.id===executiveIds[0]).jobTitle==='경영진',`${route.name}: hidden badge remains in data and popup contract`);await page.keyboard.press('Escape');
|
||
await setData(page,fixture,route.name);
|
||
const id=fixture.organizations.find(o=>o.parentId===fixture.organizations.find(o=>o.parentId===null).id).id;
|
||
const links={[id]:`${route.name==='html'?htmlOrigin:reactOrigin}/__p31_link`};
|
||
await page.evaluate(async({links,transport})=>{if(transport==='react')window.OrgChartP1React.setOptions({organizationLinks:links});else {const s=window.OrgChartP1;s.chart.destroy();s.chart=s.api.createOrgChart({container:document.getElementById('chart-host'),dataSource:new s.api.JsonOrgChartDataSource(s.data),stylesheetUrls:s.stylesheetUrls,displayMode:'readOnly',organizationLinks:links});await s.chart.mount();}},{links,transport:route.name}); await ready(page);
|
||
const link=page.locator('.hm-orgchart-organization-link');check(await link.count()===1,`${route.name}: only configured 1Depth title links`);await link.focus();await Promise.all([page.waitForURL(links[id]),page.keyboard.press('Enter')]);check(await page.locator('#proof').textContent()==='Group link reached',`${route.name}: keyboard group link navigation`);
|
||
}
|
||
} catch(error) { check(false,`${route.name}: focused verification completed`,{message:error.message,stack:error.stack}); }
|
||
await page.close();
|
||
}
|
||
for(const v of viewports) {
|
||
const variants=measurements.filter(m=>m.width===v.width);
|
||
const base=variants.find(m=>m.route==='html');
|
||
const equal=(a,b)=>['x','y','width','height'].every(k=>near(a[k],b[k]));
|
||
check(variants.length===4 && variants.every(m=>m.cards.length===base.cards.length && m.cards.every((c,i)=>equal(c.box,base.cards[i].box)&&c.name===base.cards[i].name&&c.role===base.cards[i].role&&c.badge===base.cards[i].badge&&c.image.src===base.cards[i].image.src)&&equal(m.hub.box,base.hub.box)&&JSON.stringify(m.table.rows)===JSON.stringify(base.table.rows)&&equal(m.table.box,base.table.box)),`HTML/classic/React/Storybook identical content/geometry/${v.width}`);
|
||
}
|
||
const standalone=await context.newPage();await standalone.goto(`${htmlOrigin}/p1.html`);await ready(standalone,'interactive');
|
||
const standaloneData=structuredClone(fixture);standaloneData.employees[0].jobTitle='경영진';await standalone.evaluate(async data=>window.OrgChartP1.setData(data),standaloneData);await ready(standalone,'interactive');
|
||
check((await standalone.locator('.hm-orgchart-job-title').allTextContents()).includes('경영진'),'shared renderer preserves executive badge outside readOnly Full Composition');await standalone.close();
|
||
const index=await fetch('http://127.0.0.1:6041/index.json').then(r=>r.json());const userStories=Object.values(index.entries).filter(e=>e.title==='OrgChart'&&e.type==='story'&&!e.tags?.includes('internal'));check(userStories.length===3,'existing Storybook three user menus retained',userStories.map(e=>e.name));
|
||
check(!errors.length,'no new browser console/runtime errors',errors);
|
||
} finally {
|
||
await browser?.close(); for(const server of servers)await new Promise(r=>server.close(r));
|
||
const summary={total:results.length,pass:results.filter(r=>r.status==='PASS').length,fail:results.filter(r=>r.status==='FAIL').length};
|
||
await fs.writeFile(path.join(output,'browser.json'),JSON.stringify({scope:'P3.1 only; P3 732 checks retained without rerun',fixtureSha256:createHash('sha256').update(fixtureBytes).digest('hex'),summary,results,measurements,fontEvidence,errors},null,2));
|
||
console.log(JSON.stringify({summary,errors,failures:results.filter(r=>r.status==='FAIL').map(r=>r.name)},null,2));if(summary.fail)process.exitCode=1;
|
||
}
|