Files
HMDesign/examples/org-chart-react/verify-boundaries.mjs

74 lines
6.2 KiB
JavaScript

import { loadPlaywright, launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
import assert from 'node:assert/strict';
import http from 'node:http';
import path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath, pathToFileURL } from 'node:url';
const root = fileURLToPath(new URL('../../', import.meta.url));
await loadPlaywright();
const server = http.createServer(async (request, response) => {
try {
const url = new URL(request.url, 'http://localhost');
const file = path.resolve(root, 'examples/org-chart-react/dist', '.' + decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname));
const base = path.join(root, 'examples/org-chart-react/dist');
const relative = path.relative(base, file);
if (relative.startsWith('..') || path.isAbsolute(relative)) { response.writeHead(403).end(); return; }
response.setHeader('Content-Type', { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.ttf': 'font/ttf' }[path.extname(file)] || 'application/octet-stream');
response.end(await readFile(file));
} catch { response.writeHead(404).end(); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const browser = await launchChromium({ headless: true });
const results = [], errors = [];
const check = (condition, name, detail) => { results.push({ name, status: condition ? 'PASS' : 'FAIL', detail }); assert.ok(condition, name); };
try {
const page = await browser.newPage({ viewport: { width: 390, height: 900 } });
page.on('pageerror', error => errors.push(error.message));
page.on('console', message => { if(message.type() === 'error') errors.push(message.text()); });
await page.goto(`http://127.0.0.1:${server.address().port}`);
await page.waitForFunction(() => window.OrgChartReactDemo?.events.at(-1)?.state.status === 'ready');
check(await page.evaluate(() => window.OrgChartReactDemo.requests.v1.loads === 1 && window.OrgChartReactDemo.events.filter(event => event.state.status === 'ready').length === 1), 'production mount has one initial load and one ready callback');
const layout = await page.evaluate(() => {
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot, viewport = shadow.querySelector('.hm-orgchart-viewport');
viewport.scrollLeft = viewport.scrollWidth - viewport.clientWidth;
return { width: document.documentElement.clientWidth, documentScrollWidth: document.documentElement.scrollWidth, internalScrollLeft: viewport.scrollLeft };
});
check(layout.documentScrollWidth <= 390 && layout.internalScrollLeft > 0, 'production 390px page containment and actual internal horizontal scrolling', layout);
const pollution = await page.evaluate(() => {
const container = document.createElement('div'), node = document.createElement('div');
node.textContent = 'Light DOM CSS isolation probe'; container.append(node); document.body.append(container);
const read = () => { const style = getComputedStyle(node); return { width: style.width, margin: style.margin, background: style.backgroundColor, textAlign: style.textAlign, border: style.border, font: style.fontFamily }; };
const before = read(); container.className = 'orgchart'; node.className = 'node'; const after = read(); container.remove();
return { before, after };
});
check(JSON.stringify(pollution.before) === JSON.stringify(pollution.after), 'Engine selectors do not pollute light DOM', pollution);
const hostId = await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance');
await page.evaluate(async () => {
const demo = window.OrgChartReactDemo;
const next = await demo.sources.v2.load();
demo.sources.v1.load = async () => structuredClone(next);
demo.render({ callback: 9, className: 'consumer-chart boundary' });
});
await page.waitForFunction(() => document.querySelector('.consumer-chart.boundary'));
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId && await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.summary.raw.total === 4), 'same source internal mutation and unrelated render do not imply reload');
await page.evaluate(() => { const demo = window.OrgChartReactDemo; demo.render({ sourceA: { load: options => demo.sources.v1.load(options) } }); });
await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1)?.state.dataVersion === 'mock-2');
check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId && await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.summary.raw.total === 5 && window.OrgChartReactDemo.events.at(-1).callback === 9), 'production source identity update uses same Core with new 5-person summary and latest callback');
await page.evaluate(() => window.OrgChartReactDemo.render({ mountedA: false }));
await page.waitForFunction(() => !document.querySelector('.hm-orgchart-host'));
check(await page.locator('.hm-orgchart-host').count() === 0, 'production unmount removes Core');
await page.goto(pathToFileURL(path.join(root, 'examples/org-chart/dist/classic.html')).href);
await page.waitForFunction(() => window.orgChartReady);
check(await page.evaluate(() => window.OrgChartDemo.chart.getState().summary.raw.total === 4), 'existing direct-open Vanilla classic remains ready with common v1 total 4');
await page.evaluate(() => window.OrgChartDemo.update());
check(await page.evaluate(() => window.OrgChartDemo.chart.getState().summary.raw.total === 5), 'existing direct-open Vanilla classic v2 update remains total 5');
check(errors.length === 0, 'boundary browser checks have zero console/runtime errors', errors);
} catch(error) {
results.push({ name: 'verification completion', status: 'FAIL', detail: error.stack }); process.exitCode = 1;
} finally {
await writeFile(path.join(root, 'test-results/org-chart-react/boundaries.json'), JSON.stringify({ browser: await browser.version(), results, errors }, null, 2));
await browser.close(); await new Promise(resolve => server.close(resolve));
console.log(JSON.stringify({ pass: results.filter(result => result.status === 'PASS').length, fail: results.filter(result => result.status === 'FAIL').length, errors }));
}