108 lines
9.3 KiB
JavaScript
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)); }
|