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).
120 lines
7.9 KiB
JavaScript
120 lines
7.9 KiB
JavaScript
// Actual Expanded Storybook UI: the preview-only fixed state (7 companies x 20, TB).
|
|
// Build Storybook first; uses the existing optional Playwright/Chrome runtime.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs/promises';
|
|
import http from 'node:http';
|
|
import path from 'node:path';
|
|
import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
|
|
|
const output = path.resolve('test-results/org-chart-hierarchy');
|
|
const directory = path.join(output, 'storybook');
|
|
const template = JSON.parse(await fs.readFile('examples/org-chart/data-expanded-large.json', 'utf8'));
|
|
const companies = template.affiliations;
|
|
const mime = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.ttf': 'font/ttf', '.woff2': 'font/woff2' };
|
|
const server = http.createServer(async (request, response) => {
|
|
try {
|
|
const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
|
|
if (pathname === '/favicon.ico') return response.writeHead(204).end();
|
|
const file = path.resolve(directory, '.' + (pathname === '/' ? '/index.html' : pathname));
|
|
const relative = path.relative(directory, file);
|
|
if (relative.startsWith('..') || path.isAbsolute(relative)) return response.writeHead(403).end();
|
|
response.writeHead(200, { 'Content-Type': mime[path.extname(file)] ?? 'application/octet-stream' }).end(await fs.readFile(file));
|
|
} catch { response.writeHead(404).end(); }
|
|
});
|
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
let browser, failure;
|
|
const checks = [];
|
|
const errors = [];
|
|
try {
|
|
browser = await launchChromium({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
|
|
page.on('pageerror', error => errors.push(error.message));
|
|
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
|
|
const settle = () => page.evaluate(async () => {
|
|
await document.fonts.ready;
|
|
for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve));
|
|
});
|
|
const ready = async count => {
|
|
await page.waitForFunction(count => {
|
|
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
|
return shadow?.querySelector('.hm-orgchart-shell[aria-busy="false"]')
|
|
&& (count === undefined || shadow.querySelectorAll('.hm-orgchart-employee').length === count)
|
|
&& [...shadow.querySelectorAll('link')].every(link => link.sheet);
|
|
}, count);
|
|
await settle();
|
|
};
|
|
const snapshot = () => page.locator('.hm-orgchart-host').evaluate(host => {
|
|
const shadow = host.shadowRoot;
|
|
const box = element => { const r = element.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; };
|
|
return {
|
|
nodes: [...shadow.querySelectorAll('.node[data-organization-id]')].map(node => ({ id: node.dataset.organizationId, parentId: node.dataset.parentId || null, type: node.dataset.organizationType })),
|
|
employees: [...shadow.querySelectorAll('.hm-orgchart-employee')].map(card => ({ id: card.dataset.employeeId, organizationId: card.closest('.node').dataset.organizationId })),
|
|
viewport: box(shadow.querySelector('.hm-orgchart-viewport')),
|
|
forest: box(shadow.querySelector('.hm-orgchart-forest')),
|
|
contentBounds: (() => {
|
|
const boxes = [...shadow.querySelectorAll('.hm-orgchart-organization, .hm-orgchart-organization-connectors path')].filter(element => element.getClientRects().length).map(box);
|
|
const x = Math.min(...boxes.map(b => b.x)), y = Math.min(...boxes.map(b => b.y));
|
|
return { x, y, width: Math.max(...boxes.map(b => b.x + b.width)) - x, height: Math.max(...boxes.map(b => b.y + b.height)) - y };
|
|
})(),
|
|
layout: shadow.querySelector('.hm-orgchart-expanded').dataset.autoLayout,
|
|
paths: [...shadow.querySelectorAll('.hm-orgchart-organization-connectors path')].map(p => ({ id: p.dataset.organizationId, parentId: p.dataset.parentId, d: p.getAttribute('d') })),
|
|
upstreamConnectors: [...shadow.querySelectorAll('.hierarchy, .node[data-depth]')].flatMap(element => ['::before', '::after'].map(pseudo => {
|
|
const style = getComputedStyle(element, pseudo);
|
|
return style.content !== 'none' && style.display !== 'none' && [style.borderTopWidth, style.borderLeftWidth, style.borderRightWidth, style.borderBottomWidth].some(width => parseFloat(width) > 0);
|
|
})).filter(Boolean).length,
|
|
};
|
|
});
|
|
const fit = async () => {
|
|
await page.getByRole('button', { name: 'Fit to View', exact: true }).click();
|
|
await settle();
|
|
const { viewport: v, contentBounds: f } = await snapshot();
|
|
assert.ok(f.x >= v.x - 1 && f.y >= v.y - 1 && f.x + f.width <= v.x + v.width + 1 && f.y + f.height <= v.y + v.height + 1, `complete forest fits viewport: ${JSON.stringify({ viewport: v, forest: f })}`);
|
|
};
|
|
// Preview-only Expanded Story (2026-10-22): one fixed state, 7 companies x 20 employees in TB.
|
|
const count = companies.length * 20;
|
|
await page.goto(`http://127.0.0.1:${server.address().port}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`);
|
|
await ready(count);
|
|
const companyOf = new Map(template.organizations.map(org => [org.id, org.companyId]));
|
|
const rendered = await snapshot();
|
|
assert.equal(rendered.layout, 'tb');
|
|
assert.deepEqual(rendered.nodes.sort((a, b) => a.id.localeCompare(b.id)),
|
|
template.organizations.map(org => ({ id: org.id, parentId: org.parentId, type: org.type })).sort((a, b) => a.id.localeCompare(b.id)));
|
|
const byId = new Map(rendered.nodes.map(org => [org.id, org]));
|
|
for (const team of rendered.nodes.filter(org => org.type === 'team')) {
|
|
let ancestor = byId.get(team.parentId);
|
|
while (ancestor) { assert.notEqual(ancestor.type, 'cell'); ancestor = byId.get(ancestor.parentId); }
|
|
}
|
|
const employees = rendered.employees.sort((a, b) => a.id.localeCompare(b.id));
|
|
assert.equal(new Set(employees.map(employee => employee.id)).size, count);
|
|
assert.ok(employees.every(employee => byId.has(employee.organizationId)));
|
|
assert.ok(rendered.upstreamConnectors > 0, 'TB engine CSS connectors are present');
|
|
await fit();
|
|
await page.screenshot({ path: path.join(output, 'storybook-tb-140.png'), fullPage: true });
|
|
for (const company of companies) {
|
|
assert.equal(employees.filter(employee => companyOf.get(employee.organizationId) === company.id).length, 20, `${company.id}: 20 employees`);
|
|
const teamId = `${company.id}-team-0`, cellId = `${company.id}-cell-0`;
|
|
const toggle = page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${teamId}"]`);
|
|
await toggle.focus(); await toggle.press('Enter'); await ready();
|
|
assert.equal(await page.locator(`.node[data-organization-id="${cellId}"]`).count(), 0);
|
|
assert.ok((await snapshot()).employees.length < count);
|
|
await fit();
|
|
await toggle.focus(); await toggle.press('Enter'); await ready(count);
|
|
assert.equal(await page.locator(`.node[data-organization-id="${cellId}"]`).count(), 1);
|
|
assert.deepEqual((await snapshot()).employees.sort((a, b) => a.id.localeCompare(b.id)), employees);
|
|
await fit();
|
|
checks.push({ company: company.id, employeeCount: 20, layout: 'tb', hierarchy: 'PASS', employeeIdentity: 'PASS', connectors: 'PASS', collapseExpand: 'PASS', fit: 'PASS' });
|
|
await fs.writeFile(path.join(output, 'browser-progress.json'), JSON.stringify({ checks, errors }, null, 2));
|
|
}
|
|
assert.deepEqual(errors, []);
|
|
assert.equal(await page.getByRole('alert').count(), 0);
|
|
assert.equal(checks.length, companies.length);
|
|
} catch (error) {
|
|
failure = error.stack;
|
|
throw error;
|
|
} finally {
|
|
await fs.writeFile(path.join(output, 'browser-result.json'), JSON.stringify({ status: failure ? 'FAIL' : 'PASS', checks, errors, ...(failure ? { failure } : {}) }, null, 2));
|
|
await browser?.close();
|
|
await new Promise(resolve => server.close(resolve));
|
|
}
|
|
console.log(`Expanded Storybook: ${checks.length}/${companies.length} companies PASS; ${errors.length} browser errors`);
|