docs(storybook): make Expanded OrgChart a preview-only page
Deploy Storybook / deploy (push) Successful in 59s
Deploy Storybook / deploy (push) Successful in 59s
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).
This commit is contained in:
1 parent
4a54cfbeb2
commit
f4ddd5ac4b
14 files changed
+163
-1134
No files matched your search
@@ -1,4 +1,4 @@
|
||||
// Focused P9.5 transform geometry and pointer-coordinate probes; no P9.4 replay.
|
||||
// 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';
|
||||
@@ -8,20 +8,7 @@ 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 = [];
|
||||
const pointerOnly = process.argv.includes('--pointer-only');
|
||||
await fs.mkdir(output, { recursive: true });
|
||||
if (pointerOnly) {
|
||||
const initial = JSON.parse(await fs.readFile(path.join(output, 'geometry-initial.json'), 'utf8'));
|
||||
measurements.push(...initial.measurements);
|
||||
for (const c of initial.checks) {
|
||||
if (c.name === 'Focused geometry browser console/page errors zero') continue;
|
||||
if (c.name.includes(': pointer move starts at fitted scale')) continue;
|
||||
if (c.status === 'PASS') checks.push(c);
|
||||
else if (c.name.endsWith(': connector endpoints reach intended targets')) {
|
||||
checks.push({ ...c, status: c.detail.maxLocalError < 0.75 ? 'PASS' : 'FAIL', detail: { ...c.detail, toleranceLocalPx: 0.75, reason: 'Integer offset geometry and fractional CSS widths differ by up to 0.5 local px; evaluated from preserved initial measurements without browser replay.' } });
|
||||
}
|
||||
}
|
||||
}
|
||||
const browser = await launchChromium({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1700 } });
|
||||
page.setDefaultTimeout(30000);
|
||||
@@ -37,19 +24,11 @@ async function ready(count) {
|
||||
}, count, { timeout: 60000 });
|
||||
await settle();
|
||||
}
|
||||
async function setup(type, label) {
|
||||
// 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 page.getByRole('button', { name: /^회사 선택/ }).waitFor(); await ready(140);
|
||||
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); await panel.getByRole('checkbox', { name: '한맥기술', exact: true }).click(); await ready(20);
|
||||
await page.keyboard.press('Escape'); await panel.waitFor({ state: 'hidden' });
|
||||
if (type !== 'tb') {
|
||||
await page.getByRole('combobox', { name: '조직도 표시 유형', exact: true }).click();
|
||||
await page.getByRole('option', { name: label, exact: true }).click(); await ready(20);
|
||||
}
|
||||
await ready(140);
|
||||
await page.locator('.hm-orgchart-viewport').scrollIntoViewIfNeeded(); await settle();
|
||||
}
|
||||
async function snapshot(name) {
|
||||
@@ -84,7 +63,7 @@ async function snapshot(name) {
|
||||
}
|
||||
function validateEndpoints(s, label) {
|
||||
const paths = [...s.employeePaths, ...s.organizationPaths];
|
||||
check(s.cards.length === 20 && s.employeePaths.length === 20, `${label}: all 20 cards retain employee connectors`, { cards: s.cards.length, employeePaths: s.employeePaths.length });
|
||||
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);
|
||||
}
|
||||
@@ -107,80 +86,23 @@ async function pan() {
|
||||
await page.locator('.hm-orgchart-viewport').focus();
|
||||
await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowUp'); await settle();
|
||||
}
|
||||
async function pointerMove(type) {
|
||||
const toggle = page.getByRole('switch', { name: /Drag & Drop|조직 이동/ });
|
||||
if (await toggle.getAttribute('aria-checked') !== 'true') await toggle.click();
|
||||
await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle();
|
||||
const fitted = await snapshot(`${type}-drag-fit`); await pan();
|
||||
const panned = await snapshot(`${type}-drag-pan`);
|
||||
check(panned.x !== fitted.x && panned.y !== fitted.y && panned.scale === fitted.scale, `${type}: pointer move starts at fitted scale with nonzero manual Pan`, { fitted: { x: fitted.x, y: fitted.y, scale: fitted.scale }, panned: { x: panned.x, y: panned.y, scale: panned.scale } });
|
||||
const pair = await page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot, viewport = shadow.querySelector('.hm-orgchart-viewport').getBoundingClientRect();
|
||||
const box = element => { const r = element.getBoundingClientRect(); return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, x: r.left + r.width / 2, y: r.top + r.height / 2 }; };
|
||||
const inside = r => r.left > viewport.left + 4 && r.right < viewport.right - 4 && r.top > viewport.top + 4 && r.bottom < viewport.bottom - 4
|
||||
&& r.left > 4 && r.right < innerWidth - 4 && r.top > 4 && r.bottom < innerHeight - 4;
|
||||
const sources = [...shadow.querySelectorAll('.hm-orgchart-employees .hm-orgchart-employee')].map(element => ({ element, rect: box(element) })).filter(item => inside(item.rect));
|
||||
sources.sort((a, b) => Math.hypot(a.rect.x - (viewport.left + viewport.right) / 2, a.rect.y - (viewport.top + viewport.bottom) / 2) - Math.hypot(b.rect.x - (viewport.left + viewport.right) / 2, b.rect.y - (viewport.top + viewport.bottom) / 2));
|
||||
for (const source of sources) {
|
||||
const sourceOrg = source.element.closest('.node').dataset.organizationId;
|
||||
const targets = [...shadow.querySelectorAll('.hm-orgchart-expanded-toggle')].filter(element => element.dataset.organizationId !== sourceOrg && element.closest('.node').dataset.parentId !== '').map(element => ({ element, rect: box(element) })).filter(item => inside(item.rect));
|
||||
targets.sort((a, b) => Math.hypot(a.rect.x - source.rect.x, a.rect.y - source.rect.y) - Math.hypot(b.rect.x - source.rect.x, b.rect.y - source.rect.y));
|
||||
if (targets.length) return { employeeId: source.element.dataset.employeeId, beforeOrganizationId: sourceOrg, targetOrganizationId: targets[0].element.dataset.organizationId, source: source.rect, target: targets[0].rect };
|
||||
}
|
||||
throw new Error('No visible employee/other-organization pointer pair after Fit/Pan');
|
||||
});
|
||||
// Native pointer focus may reveal/scroll a source. Settle that before taking
|
||||
// client coordinates, and verify both endpoints through live shadow hit tests.
|
||||
await page.locator(`.hm-orgchart-employee[data-employee-id="${pair.employeeId}"]`).focus(); await settle();
|
||||
const actualPair = await page.evaluate(pair => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const employee = [...shadow.querySelectorAll('.hm-orgchart-employee')].find(item => item.dataset.employeeId === pair.employeeId);
|
||||
const target = [...shadow.querySelectorAll('.hm-orgchart-expanded-toggle')].find(item => item.dataset.organizationId === pair.targetOrganizationId);
|
||||
const center = element => { const r = element.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; };
|
||||
const sourcePoint = center(employee), targetPoint = center(target);
|
||||
return { ...pair, source: sourcePoint, target: targetPoint, sourceHit: shadow.elementFromPoint(sourcePoint.x, sourcePoint.y)?.closest('.hm-orgchart-employee')?.dataset.employeeId, targetHit: shadow.elementFromPoint(targetPoint.x, targetPoint.y)?.closest('.node[data-organization-id]')?.dataset.organizationId };
|
||||
}, pair);
|
||||
check(actualPair.sourceHit === pair.employeeId && actualPair.targetHit === pair.targetOrganizationId, `${type}: focused pointer endpoints are visible and hit intended DOM targets`, actualPair);
|
||||
await page.mouse.move(actualPair.source.x, actualPair.source.y); await page.mouse.down();
|
||||
await page.mouse.move(actualPair.source.x + 8, actualPair.source.y, { steps: 2 });
|
||||
await page.mouse.move(actualPair.target.x, actualPair.target.y, { steps: 14 });
|
||||
const hover = await page.evaluate(() => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
return { dragging: shadow.querySelector('.hm-orgchart-viewport').dataset.editDragging, target: shadow.querySelector('[data-drop-hover="allowed"]')?.dataset.organizationId, feedback: shadow.querySelector('.hm-orgchart-move-feedback').textContent };
|
||||
});
|
||||
check(hover.dragging === 'true' && hover.target === pair.targetOrganizationId, `${type}: actual pointer hit testing highlights intended transformed target`, { pair, hover });
|
||||
await page.mouse.up();
|
||||
await page.waitForFunction(({ employeeId, targetOrganizationId }) => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
||||
const card = [...shadow.querySelectorAll('.hm-orgchart-employee')].find(card => card.dataset.employeeId === employeeId);
|
||||
return card?.closest('.node').dataset.organizationId === targetOrganizationId && shadow.querySelector('.hm-orgchart-move-feedback').dataset.moveStatus === 'success';
|
||||
}, pair); await ready(20);
|
||||
const moved = await snapshot(`${type}-after-pointer-move`);
|
||||
check(moved.cards.find(card => card.id === pair.employeeId)?.organizationId === pair.targetOrganizationId, `${type}: pointer release moves employee to intended organization`, pair);
|
||||
validateEndpoints(moved, `${type} after pointer move`);
|
||||
}
|
||||
try {
|
||||
for (const [type, label] of [['tb', '세로형 TB'], ['lr', '가로형 LR'], ['hybrid', '혼합형 Hybrid']]) {
|
||||
try {
|
||||
await setup(type, label);
|
||||
if (!pointerOnly) {
|
||||
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 pointerMove(type);
|
||||
if (type === 'hybrid') await page.screenshot({ path: path.join(output, 'geometry-pointer-hybrid.png'), fullPage: true });
|
||||
} catch (error) {
|
||||
check(false, `${type}: focused geometry section completed`, { message: error.message, stack: error.stack });
|
||||
await page.mouse.up().catch(() => {});
|
||||
}
|
||||
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(), repairedPointerOnly: pointerOnly, initialEvidence: pointerOnly ? 'geometry-initial.json' : undefined, scope: 'P9.5 only: 1 company × 20 staff; TB/LR/Hybrid transforms and one actual employee pointer move per mode; 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 };
|
||||
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;
|
||||
|
||||
Reference in new issue
Block a user