Files
HMDesign/examples/org-chart/verify-p9-5-geometry.mjs
shkim4 f4ddd5ac4b
Deploy Storybook / deploy (push) Successful in 59s
docs(storybook): make Expanded OrgChart a preview-only page
Remove the data, layout, interaction and employee-popup setting panels
from the Expanded OrgChart Story. The chart now renders a fixed sample
(7 companies x 20, TB, all expanded, collapse/zoom/pan on, Drag & Drop
off, HM Dialog) right under the page title. Story ID, OrgChart packages
and public API are unchanged.

Align the Story browser checks (P7, P8, P9, P9.2-P9.5, geometry, popup
parity, hierarchy, HM Story) with the preview: setting-only scenarios
are removed, chart rendering, collapse, Zoom/Pan/Fit and HM Dialog checks
run on the fixed state. Move/Drag & Drop coverage stays in the Vanilla
Core checks (verify-p9-3-core, verify-p9-4-core).

Verified: tsc, test:guard 94/94, node --check on 11 scripts, changed
browser sections PASS. Pre-existing P7 story-menu, P8 Department
long-name, P9 Expanded tb/runner failures remain separate issues.
verify-p9-5-popup-parity was not run (needs the Vanilla example server).
2026-10-22 17:51:21 +09:00

110 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Focused P9.5 transform geometry probes on the preview-only Expanded Story; no P9.4 replay.
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 origin = (process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6064').replace(/\/$/, '');
const startedAt = new Date().toISOString(), checks = [], measurements = [], errors = [];
await fs.mkdir(output, { recursive: true });
const browser = await launchChromium({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 1700 } });
page.setDefaultTimeout(30000);
page.on('pageerror', error => errors.push({ type: 'pageerror', message: error.message }));
page.on('console', message => { if (message.type() === 'error') errors.push({ type: 'console', message: message.text() }); });
const check = (pass, name, detail) => { checks.push({ name, status: pass ? 'PASS' : 'FAIL', detail }); fs.writeFile(path.join(output, 'geometry-progress.json'), JSON.stringify({ startedAt, checks, measurements, errors }, null, 2)).catch(() => {}); if (!pass) console.log('FAIL', name, JSON.stringify(detail)); };
const settle = () => page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 10; 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();
}
// Expanded is preview-only (2026-10-22): the fixed 7 companies x 20 TB state is measured;
// company filtering, layout switching and Drag & Drop are no longer reachable from the Story.
async function setup() {
await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`);
await ready(140);
await page.locator('.hm-orgchart-viewport').scrollIntoViewIfNeeded(); await settle();
}
async function snapshot(name) {
const measurement = 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 scale = Number(viewport.dataset.scale), origin = forest.getBoundingClientRect();
const localPoint = p => ({ x: (p.x - origin.left) / scale, y: (p.y - origin.top) / scale });
const localRect = element => { const r = element.getBoundingClientRect(); return { x: (r.left - origin.left) / scale, y: (r.top - origin.top) / scale, width: r.width / scale, height: r.height / scale }; };
const endPoint = p => localPoint(p.getPointAtLength(p.getTotalLength()).matrixTransform(p.getScreenCTM()));
const employeePaths = [...shadow.querySelectorAll('.hm-orgchart-employee-connectors path')].map(p => {
const article = p.closest('.hm-orgchart-organization');
const card = [...article.querySelectorAll('.hm-orgchart-employee')].find(card => card.dataset.employeeId === p.dataset.employeeId);
const box = localRect(card), direction = p.dataset.direction;
const expected = direction === 'left' ? { x: box.x, y: box.y + box.height / 2 }
: direction === 'right' ? { x: box.x + box.width, y: box.y + box.height / 2 } : { x: box.x + box.width / 2, y: box.y };
const endpoint = endPoint(p);
return { id: p.dataset.employeeId, direction, d: p.getAttribute('d'), endpoint, expected, error: Math.hypot(endpoint.x - expected.x, endpoint.y - expected.y) };
});
const organizationPaths = [...shadow.querySelectorAll('.hm-orgchart-organization-connectors path')].map(p => {
const tree = p.closest('.hm-orgchart-tree');
const node = [...tree.querySelectorAll('.node')].find(node => node.dataset.organizationId === p.dataset.organizationId);
const heading = localRect(node.querySelector('.hm-orgchart-organization-name'));
const expected = p.dataset.direction === 'left' ? { x: heading.x, y: heading.y + heading.height / 2 } : { x: heading.x + heading.width / 2, y: heading.y };
const endpoint = endPoint(p);
return { id: p.dataset.organizationId, parentId: p.dataset.parentId, direction: p.dataset.direction, d: p.getAttribute('d'), endpoint, expected, error: Math.hypot(endpoint.x - expected.x, endpoint.y - expected.y) };
});
const cards = [...shadow.querySelectorAll('.hm-orgchart-employee')].map(card => ({ id: card.dataset.employeeId, organizationId: card.closest('.node').dataset.organizationId, ...localRect(card) }));
return { layout: shadow.querySelector('.hm-orgchart-expanded').dataset.autoLayout, scale, x: Number(viewport.dataset.panX), y: Number(viewport.dataset.panY), cards, employeePaths, organizationPaths, organizationConnectorPresentation: organizationPaths.length ? 'measured-svg' : 'existing-TB-pseudo-elements' };
});
measurements.push({ name, ...measurement }); return measurement;
}
function validateEndpoints(s, label) {
const paths = [...s.employeePaths, ...s.organizationPaths];
check(s.cards.length === 140 && s.employeePaths.length === 140, `${label}: all 140 cards retain employee connectors`, { cards: s.cards.length, employeePaths: s.employeePaths.length });
check(paths.every(p => p.error < 0.75), `${label}: connector endpoints reach intended targets`, { paths: paths.length, organizationPaths: s.organizationPaths.length, maxLocalError: Math.max(0, ...paths.map(p => p.error)), toleranceLocalPx: 0.75 });
if (s.layout !== 'tb') check(s.organizationPaths.length > 0, `${label}: organization routes are measured`, s.organizationPaths.length);
}
function compareGeometry(before, after, label) {
let maxDelta = 0;
const cardKeys = ['x', 'y', 'width', 'height'];
for (const a of before.cards) {
const b = after.cards.find(b => b.id === a.id);
if (!b) { maxDelta = Infinity; break; }
for (const key of cardKeys) maxDelta = Math.max(maxDelta, Math.abs(a[key] - b[key]));
}
for (const key of ['employeePaths', 'organizationPaths']) for (const a of before[key]) {
const b = after[key].find(b => b.id === a.id);
if (!b || a.d !== b.d) { maxDelta = Infinity; break; }
maxDelta = Math.max(maxDelta, Math.abs(a.endpoint.x - b.endpoint.x), Math.abs(a.endpoint.y - b.endpoint.y));
}
check(maxDelta < 0.1, `${label}: forest-local card/connector geometry is unchanged`, { maxLocalDelta: maxDelta, beforeScale: before.scale, afterScale: after.scale });
}
async function pan() {
await page.locator('.hm-orgchart-viewport').focus();
await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowUp'); await settle();
}
try {
const type = 'tb';
try {
await setup();
const centered = await snapshot(`${type}-centered`); validateEndpoints(centered, `${type} centered`);
await pan(); const panned = await snapshot(`${type}-panned`); compareGeometry(centered, panned, `${type} Pan`); validateEndpoints(panned, `${type} panned`);
await page.getByRole('button', { name: '확대', exact: true }).click(); await settle();
const zoomed = await snapshot(`${type}-zoomed`); compareGeometry(centered, zoomed, `${type} Zoom`); validateEndpoints(zoomed, `${type} zoomed`);
await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle();
const fitted = await snapshot(`${type}-fitted`); compareGeometry(centered, fitted, `${type} Fit`); validateEndpoints(fitted, `${type} fitted`);
await page.screenshot({ path: path.join(output, 'geometry-tb.png'), fullPage: true });
} catch (error) {
check(false, `${type}: focused geometry section completed`, { message: error.message, stack: error.stack });
}
} finally {
check(errors.length === 0, 'Focused geometry browser console/page errors zero', errors);
const result = { startedAt, completedAt: new Date().toISOString(), origin, browser: browser.version(), scope: 'P9.5 preview-only Expanded: 7 companies × 20 staff; TB Pan/Zoom/Fit transforms; existing TB organization pseudo-elements unchanged', 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, 'geometry.json'), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ pass: result.pass, fail: result.fail, measurements: measurements.length, output: path.join(output, 'geometry.json') }));
await browser.close(); if (result.fail) process.exitCode = 1;
}