import { loadPlaywright, launchChromium } from '../../scripts/orgchart-browser-runtime.mjs'; import http from 'node:http'; import path from 'node:path'; import { readFile, writeFile } from 'node:fs/promises'; await loadPlaywright(); const output = 'test-results/org-chart-final-check'; const baselineMode = process.argv.includes('--baseline'); const colorsRepair = process.argv.includes('--colors-repair'); const browser = await launchChromium({ 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)); }