Files
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

136 lines
11 KiB
JavaScript

// P9.3 browser-state checks on the preview-only Expanded Story (2026-10-22). Drag & Drop is fixed
// OFF and company/data/layout settings were removed, so Story editing sections were retired;
// move, pointer drag, invalid-move and cross-company coverage stays in verify-p9-3-core.mjs.
import fs from 'node:fs/promises';
import { writeFileSync } from 'node:fs';
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-3');
const origin = process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6064';
const only = process.argv.slice(2), suffix = only.length ? `-${only.join('-')}` : '';
const results = [], measurements = [], errors = [], startedAt = new Date().toISOString();
await fs.mkdir(output, { recursive: true });
const persist = () => writeFileSync(path.join(output, `browser-progress${suffix}.json`), JSON.stringify({ startedAt, results, measurements, errors }, null, 2));
const check = (pass, name, detail) => {
results.push({ name, status: pass ? 'PASS' : 'FAIL', ...(detail === undefined ? {} : { detail }) }); persist();
if (!pass) console.log('FAIL', name, JSON.stringify(detail));
};
const browser = await launchChromium({ headless: true });
const page = await browser.newPage({ viewport: { width: 1920, height: 1700 } });
page.setDefaultTimeout(15000);
page.on('pageerror', error => { errors.push({ type: 'pageerror', message: error.message }); persist(); });
page.on('console', message => { if (message.type() === 'error') { errors.push({ type: 'console', message: message.text(), location: message.location() }); persist(); } });
const viewport = () => page.locator('.hm-orgchart-viewport');
const employee = id => page.locator(`.hm-orgchart-employee[data-employee-id="${id}"]`);
const title = id => page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${id}"]`);
const dialog = () => page.locator('[data-component="HMOrgChartEmployeeDialog"]');
const settle = () => page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 8; 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);
await settle();
}
async function setup() {
await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`);
await ready(140);
await viewport().scrollIntoViewIfNeeded(); await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle();
}
async function state() {
return page.evaluate(() => {
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
const viewport = shadow.querySelector('.hm-orgchart-viewport'), expanded = shadow.querySelector('.hm-orgchart-expanded');
const rect = element => { const r = element.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; };
const nodes = [...shadow.querySelectorAll('.node[data-organization-id]')].map(element => ({
id: element.dataset.organizationId, parentId: element.dataset.parentId, companyId: element.dataset.companyId,
domParentId: element.closest('.hierarchy')?.parentElement?.closest('.hierarchy')?.querySelector(':scope > .node')?.dataset.organizationId ?? '',
direct: Number(element.dataset.directEmployeeCount), inclusive: Number(element.dataset.inclusiveEmployeeCount), collapsed: element.dataset.collapsed,
}));
const cards = [...shadow.querySelectorAll('.hm-orgchart-employee')].map(element => ({
id: element.dataset.employeeId, organizationId: element.closest('.node').dataset.organizationId, companyId: element.dataset.companyId,
direction: element.dataset.connectionDirection, kind: element.dataset.cardKind,
border: { top: getComputedStyle(element).borderTopColor, left: getComputedStyle(element).borderLeftColor, right: getComputedStyle(element).borderRightColor },
}));
const paths = [...shadow.querySelectorAll('.hm-orgchart-organization-connectors path')].map(path => ({ id: path.dataset.organizationId, parentId: path.dataset.parentId, d: path.getAttribute('d') }));
const employeePaths = [...shadow.querySelectorAll('.hm-orgchart-employee-connectors path')].map(path => ({ id: path.dataset.employeeId, organizationId: path.closest('.node').dataset.organizationId, direction: path.dataset.direction, d: path.getAttribute('d') }));
const feedback = shadow.querySelector('.hm-orgchart-move-feedback');
return { layout: expanded?.dataset.autoLayout, scale: Number(viewport.dataset.scale), panX: Number(viewport.dataset.panX), panY: Number(viewport.dataset.panY),
viewport: rect(viewport), forest: rect(shadow.querySelector('.hm-orgchart-forest')), nodes, cards, paths, employeePaths, feedback: { status: feedback?.dataset.moveStatus, text: feedback?.textContent },
drop: [...shadow.querySelectorAll('[data-drop-state]')].map(element => ({ id: element.dataset.organizationId, state: element.dataset.dropState, hover: element.dataset.dropHover })),
dragging: viewport.dataset.editDragging, panDragging: viewport.dataset.dragging, activeEmployee: shadow.activeElement?.dataset.employeeId,
activeOrganization: shadow.activeElement?.dataset.organizationId,
dialogs: document.querySelectorAll('[data-component="HMOrgChartEmployeeDialog"]').length,
employeesVisible: cards.length, employeeCount: cards.length, organizationCount: nodes.length,
companyRoots: nodes.filter(org => org.parentId === '').map(org => org.companyId),
};
});
}
const relationState = s => JSON.stringify({ nodes: s.nodes, cards: s.cards.map(({ id, organizationId, direction }) => ({ id, organizationId, direction })) });
async function blankPoint() {
return page.evaluate(() => {
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot, viewport = shadow.querySelector('.hm-orgchart-viewport'), box = viewport.getBoundingClientRect();
for (const [dx, dy] of [[8, 8], [8, box.height - 8], [box.width - 8, 8], [box.width - 8, box.height - 8], [box.width / 2, box.height - 8]]) {
const point = { x: box.left + dx, y: box.top + dy }, hit = shadow.elementFromPoint(point.x, point.y);
if (hit && !hit.closest('button,a,.hm-orgchart-employee,.node')) return point;
}
throw new Error('No visible blank Pan point found');
});
}
async function pan(dx, dy) {
const p = await blankPoint(); await page.mouse.move(p.x, p.y); await page.mouse.down();
const v = await viewport().boundingBox();
// Cross the six-pixel Pan threshold inside the viewport before the requested
// motion. A large first upward step from its top edge never reaches the
// viewport's pointermove listener and therefore cannot establish capture.
await page.mouse.move(p.x + Math.sign(v.x + v.width / 2 - p.x) * 8, p.y, { steps: 2 });
await page.mouse.move(p.x + dx, p.y + dy, { steps: 8 }); await page.mouse.up(); await settle();
}
async function centerPair(source, target) {
await viewport().scrollIntoViewIfNeeded(); await source.focus(); await settle();
for (let pass = 0; pass < 3; pass++) {
const a = await source.boundingBox(), b = await target.boundingBox(), v = await viewport().boundingBox();
if (!a || !b || !v) throw new Error('Drag source or target has no geometry');
const left = Math.min(a.x, b.x), right = Math.max(a.x + a.width, b.x + b.width);
const top = Math.min(a.y, b.y), bottom = Math.max(a.y + a.height, b.y + b.height);
if (right - left > v.width - 24 || bottom - top > v.height - 24) throw new Error(`Drag pair exceeds viewport at current scale: ${JSON.stringify({ a, b, v })}`);
const dx = v.x + v.width / 2 - (left + right) / 2, dy = v.y + v.height / 2 - (top + bottom) / 2;
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) break;
await pan(dx, dy);
}
}
async function snap(name) { await page.screenshot({ path: path.join(output, `${name}.png`), fullPage: false }); }
async function section(name, action) {
if (only.length && !only.includes(name)) return;
try { await action(); } catch (error) {
check(false, `${name}: execution completed`, { message: error.message, stack: error.stack });
await page.mouse.up().catch(() => {}); await snap(`failure-${name}`).catch(() => {});
}
}
await section('pan-click', async () => {
await setup(); await centerPair(employee('virtual-022'), title('hanmac-office-sibling'));
const before = await state(); await pan(85, -45); const after = await state();
check(Math.abs(after.panX - before.panX - 85) < 1 && Math.abs(after.panY - before.panY + 45) < 1 && after.scale === before.scale && relationState(after) === relationState(before), 'Blank-space pointer Pan preserves Zoom and every relationship');
await centerPair(employee('virtual-022'), title('hanmac-office-sibling'));
const box = await employee('virtual-022').boundingBox();
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 2, box.y + box.height / 2 + 1); const small = await state(); await page.mouse.up();
await dialog().waitFor(); check(!small.dragging && await dialog().getAttribute('data-employee-id') === 'virtual-022', 'Movement below six-pixel threshold remains employee click');
await page.keyboard.press('Escape'); await dialog().waitFor({ state: 'hidden' });
await title('hanmac-office-sibling').click(); await ready(); check(await title('hanmac-office-sibling').getAttribute('aria-expanded') === 'false', 'Simple group click collapses');
await title('hanmac-office-sibling').click(); await ready(140); check(await title('hanmac-office-sibling').getAttribute('aria-expanded') === 'true', 'Simple group click expands');
});
check(errors.length === 0, 'New console/page errors: zero', errors);
const result = { startedAt, completedAt: new Date().toISOString(), browser: browser.version(), origin, sections: only, results, measurements, errors,
notVerified: ['Physical touch device', 'Actual screen reader'], pass: results.filter(item => item.status === 'PASS').length, fail: results.filter(item => item.status === 'FAIL').length };
await fs.writeFile(path.join(output, `browser-editing${suffix}.json`), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ pass: result.pass, fail: result.fail, errors: errors.length, output: `test-results/org-chart-p9-3/browser-editing${suffix}.json` }));
await browser.close(); if (result.fail) process.exitCode = 1;