Files
HMDesign/examples/org-chart/verify-final-status-check.mjs
T

108 lines
9.3 KiB
JavaScript

import http from 'node:http';
import path from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
const { chromium } = await import(pathToFileURL(process.env.ORGCHART_PLAYWRIGHT_MODULE).href);
const output = 'test-results/org-chart-final-check';
const baselineMode = process.argv.includes('--baseline');
const colorsRepair = process.argv.includes('--colors-repair');
const browser = await chromium.launch({ executablePath: 'C:/Program Files/Google/Chrome/Application/chrome.exe', headless: true });
const results = [], measurements = [], errors = [];
const check = (ok, name, actual) => results.push({ name, status: ok ? 'PASS' : 'FAIL', actual });
async function measure(url, label) {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
page.on('pageerror', error => errors.push({ label, message: error.message }));
page.on('console', message => { if (message.type() === 'error') errors.push({ label, message: message.text() }); });
await page.goto(url);
await page.waitForFunction(() => {
const root = document.querySelector('.hm-orgchart-host')?.shadowRoot;
return root?.querySelector('.hm-orgchart-status-group') && [...root.querySelectorAll('link')].every(link => link.sheet);
});
await page.evaluate(async () => {
await document.fonts.ready;
const root = document.querySelector('.hm-orgchart-host').shadowRoot;
await Promise.all([...root.querySelectorAll('img')].map(image => image.decode().catch(() => {})));
});
const value = await page.evaluate(() => {
const root = document.querySelector('.hm-orgchart-host').shadowRoot;
const properties = (element, keys) => { const s = getComputedStyle(element); return Object.fromEntries(keys.map(key => [key, s[key]])); };
const typography = element => properties(element, ['fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'margin', 'padding']);
const box = element => { const b = element.getBoundingClientRect(); return { x: b.x, y: b.y, right: b.right, bottom: b.bottom }; };
const overlap = (a, b) => Math.min(a.right, b.right) - Math.max(a.x, b.x) > 1 && Math.min(a.bottom, b.bottom) - Math.max(a.y, b.y) > 1;
const organization = [...root.querySelectorAll('[data-card-asset]')].map(card => ({ id: card.dataset.employeeId,
card: properties(card, ['backgroundImage', 'border', 'width', 'height', 'columnGap']),
name: properties(card.querySelector('.hm-orgchart-person-name'), ['color', 'fontSize', 'fontWeight']),
logo: properties(card.querySelector('.hm-orgchart-affiliation-mark img'), ['filter', 'opacity', 'width', 'height']) }));
const groups = [...root.querySelectorAll('.hm-orgchart-status-group')].map(group => ({
status: group.dataset.workStatus, titleText: group.querySelector('h3').textContent,
title: properties(group.querySelector('h3'), ['backgroundImage', 'border', 'boxShadow']),
cards: [...group.querySelectorAll('.hm-orgchart-employee')].map(card => {
const role = card.querySelector('.hm-orgchart-person-role'), name = card.querySelector('.hm-orgchart-person-name');
const avatar = card.querySelector('.hm-orgchart-avatar'), mark = card.querySelector('.hm-orgchart-affiliation-mark'), image = mark.querySelector('img');
const collisions = [];
const roleRange = document.createRange(); roleRange.selectNodeContents(role);
const nameRange = document.createRange(); nameRange.selectNodeContents(name);
for (const [kind, range] of [['role', roleRange], ['name', nameRange]]) for (const r of range.getClientRects()) {
const textBox = { x: r.x, y: r.y, right: r.right, bottom: r.bottom };
if (overlap(textBox, box(avatar))) collisions.push(`${kind}/avatar`);
if (overlap(textBox, box(image))) collisions.push(`${kind}/logo`);
}
return { id: card.dataset.employeeId, card: properties(card, ['margin', 'backgroundImage', 'border', 'borderRadius', 'cursor', 'columnGap', 'filter', 'opacity']),
roleColor: getComputedStyle(role).color, nameColor: getComputedStyle(name).color,
roleTypography: typography(role), nameTypography: typography(name),
logo: properties(image, ['filter', 'opacity']), mark: properties(mark, ['backgroundColor', 'border', 'boxShadow', 'filter', 'opacity']), collisions };
})
}));
return { organization, groups, radiusVariable: getComputedStyle(root.querySelector('.hm-orgchart-shell')).getPropertyValue('--br-3_5').trim(),
resolvedRadius: getComputedStyle(root.querySelector('.hm-orgchart-shell')).getPropertyValue('--hm-orgchart-resolved-radius-container').trim() };
});
if (!baselineMode) await page.locator('.hm-orgchart-status-summary').screenshot({ path: `${output}/${label}-status.png` });
await page.close();
return value;
}
let server;
try {
const storyUrl = `${process.env.ORGCHART_STORY_ORIGIN}/iframe.html?id=orgchart--full-composition&viewMode=story`;
const react = await measure(storyUrl, 'react');
if (baselineMode) await writeFile(`${output}/status-before.json`, JSON.stringify(react, null, 2));
else {
const baseline = JSON.parse(await readFile(`${output}/status-before.json`, 'utf8'));
if (!colorsRepair) check(JSON.stringify(react.organization) === JSON.stringify(baseline.organization), 'normal organization cards and colored logos unchanged', react.organization.length);
const directory = path.resolve('examples/org-chart/dist');
server = http.createServer(async (req, res) => {
try {
const file = path.resolve(directory, '.' + decodeURIComponent(new URL(req.url, 'http://localhost').pathname));
if (path.relative(directory, file).startsWith('..')) return res.writeHead(403).end();
res.setHeader('Content-Type', ({ '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.ttf': 'font/ttf' })[path.extname(file)] ?? 'application/octet-stream');
res.end(await readFile(file));
} catch { res.writeHead(404).end(); }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const html = await measure(`http://127.0.0.1:${server.address().port}/p1.html`, 'html');
measurements.push({ label: 'react', ...react }, { label: 'html', ...html });
check(JSON.stringify(react.groups) === JSON.stringify(html.groups), 'HTML and React status groups computed styles match', react.groups.map(group => group.status));
for (const group of react.groups) {
const before = baseline.groups.find(item => item.status === group.status);
if (colorsRepair) {
for (const card of group.cards) check(card.roleColor === 'rgb(85, 95, 106)' && card.nameColor === 'rgb(62, 70, 80)', `${group.status}/${card.id}: repaired text colors`, [card.roleColor, card.nameColor]);
continue;
}
check(group.title.backgroundImage === 'linear-gradient(rgb(135, 144, 156) 0%, rgb(104, 114, 126) 100%)' && group.title.border === '1px solid rgba(92, 101, 112, 0.52)' && group.title.boxShadow === 'rgba(75, 84, 96, 0.18) 0px 7px 16px 0px', `${group.status}: title gradient/border/shadow`, group.title);
for (const card of group.cards) {
const old = before.cards.find(item => item.id === card.id);
check(card.card.backgroundImage === 'linear-gradient(rgb(244, 245, 246) 0%, rgb(223, 227, 231) 100%)' && card.card.border === '1px solid rgba(120, 130, 142, 0.35)' && card.card.borderRadius === '4px' && card.card.margin === '0px' && card.card.cursor === 'pointer', `${group.status}/${card.id}: card surface`, card.card);
check(card.roleColor === 'rgb(85, 95, 106)' && card.nameColor === 'rgb(62, 70, 80)', `${group.status}/${card.id}: text colors`, [card.roleColor, card.nameColor]);
check(JSON.stringify(card.roleTypography) === JSON.stringify(old.roleTypography) && JSON.stringify(card.nameTypography) === JSON.stringify(old.nameTypography) && card.card.columnGap === old.card.columnGap, `${group.status}/${card.id}: existing typography and gap preserved`, { role: card.roleTypography, name: card.nameTypography, gap: card.card.columnGap });
check(card.logo.filter === 'grayscale(1)' && card.logo.opacity === '0.72' && card.mark.filter === 'none' && card.mark.opacity === '1' && card.card.filter === 'none' && card.card.opacity === '1', `${group.status}/${card.id}: image-only filter/opacity`, card.logo);
check(card.mark.backgroundColor === 'rgba(0, 0, 0, 0)' && card.mark.border === '0px none rgb(89, 101, 117)' && card.mark.boxShadow === 'none', `${group.status}/${card.id}: transparent logo frame`, card.mark);
check(card.collisions.length === 0, `${group.status}/${card.id}: profile/text/logo collisions`, card.collisions);
}
check(group.titleText === before.titleText && group.cards.length === before.cards.length, `${group.status}: same employee IDs/counts`, group.cards.map(card => card.id));
}
check(errors.length === 0, 'browser errors', errors);
await writeFile(`${output}/${colorsRepair ? 'status-colors-repair' : 'status-browser'}.json`, JSON.stringify({ results, measurements, errors }, null, 2));
console.log(JSON.stringify({ checks: results.length, pass: results.filter(item => item.status === 'PASS').length, failures: results.filter(item => item.status === 'FAIL'), errors }, null, 2));
if (results.some(item => item.status === 'FAIL')) process.exitCode = 1;
}
} finally { await browser.close(); if (server) await new Promise(resolve => server.close(resolve)); }