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).
136 lines
11 KiB
JavaScript
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;
|