import assert from 'node:assert/strict'; import http from 'node:http'; import path from 'node:path'; import { readFile, mkdir, writeFile } from 'node:fs/promises'; import { fileURLToPath, pathToFileURL } from 'node:url'; const root = fileURLToPath(new URL('../../', import.meta.url)); const output = path.join(root, 'test-results/org-chart-react'); await mkdir(output, { recursive: true }); const modulePath = process.env.ORGCHART_PLAYWRIGHT_MODULE; const { chromium } = await import(modulePath ? pathToFileURL(modulePath).href : 'playwright'); const mime = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css', '.ttf': 'font/ttf', '.woff2': 'font/woff2', '.svg': 'image/svg+xml' }; async function serve(directory) { const server = http.createServer(async (request, response) => { try { const url = new URL(request.url, 'http://localhost'); const file = path.resolve(directory, '.' + decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname)); const relative = path.relative(directory, file); if (relative.startsWith('..') || path.isAbsolute(relative)) { response.writeHead(403).end(); return; } response.setHeader('Content-Type', mime[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)); return { server, origin: `http://127.0.0.1:${server.address().port}` }; } const consumer = await serve(path.join(root, 'examples/org-chart-react/dist')); const vanilla = await serve(path.join(root, 'examples/org-chart/dist')); const storybook = await serve(path.join(root, 'storybook-static')); const browser = await chromium.launch({ executablePath: process.env.ORGCHART_CHROME || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', headless: true }); const results = [], errors = [], metrics = [], snapshots = {}; function check(condition, name, detail) { results.push({ name, status: condition ? 'PASS' : 'FAIL', detail }); assert.ok(condition, `${name}: ${JSON.stringify(detail)}`); } function captureErrors(page, transport) { page.on('pageerror', error => errors.push({ transport, type: 'pageerror', message: error.message })); page.on('console', message => { if (message.type() === 'error') errors.push({ transport, type: 'console', message: message.text() }); }); } async function snapshot(page, selector = '.hm-orgchart-host') { return page.locator(selector).first().evaluate(host => { const shadow = host.shadowRoot; const nodes = [...shadow.querySelectorAll('.node')]; return { organizations: nodes.map(node => ({ id: node.dataset.organizationId, parentId: node.parentElement.parentElement.closest('.hierarchy')?.querySelector(':scope > .node')?.dataset.organizationId || null, employees: [...node.querySelectorAll('.hm-orgchart-employee')].map(employee => employee.dataset.employeeId), text: node.querySelector('.hm-orgchart-organization').textContent, })), summary: shadow.querySelector('.hm-orgchart-summary')?.textContent, heading: shadow.querySelector('.hm-orgchart-heading')?.textContent, css: [...shadow.querySelectorAll('link')].map(link => ({ href: link.href, loaded: !!link.sheet })), style: (() => { const s = getComputedStyle(shadow.querySelector('.hm-orgchart-organization')); return { color: s.color, background: s.backgroundColor, font: s.fontFamily, radius: s.borderRadius }; })(), }; }); } async function ready(page, instance = 'A', version) { await page.waitForFunction(({ instance, version }) => { const event = window.OrgChartReactDemo.events.filter(event => event.instance === instance).at(-1); const host = document.querySelector(`[data-instance="${instance}"] .hm-orgchart-host`); return !!host?.shadowRoot.querySelector('.hm-orgchart-shell[aria-busy="false"] .node') && event?.state.status === 'ready' && (!version || event.state.dataVersion === version); }, { instance, version }); } async function patch(page, value) { await page.evaluate(value => window.OrgChartReactDemo.render(value), value); } async function source(page, name, instance = 'A') { await page.evaluate(({ name, instance }) => window.OrgChartReactDemo.render({ [`source${instance}`]: window.OrgChartReactDemo.sources[name] }), { name, instance }); } try { const context = await browser.newContext({ viewport: { width: 1440, height: 1100 } }); await context.addInitScript(() => { const NativeObserver = window.MutationObserver; const observers = new Map(); window.MutationObserver = class extends NativeObserver { observe(...args) { observers.set(this, args[0]); return super.observe(...args); } disconnect() { observers.delete(this); return super.disconnect(); } }; window.chartObservers = () => [...observers.values()].filter(target => target.getRootNode()?.host?.classList.contains('hm-orgchart-host')).length; }); const html = await context.newPage(); captureErrors(html, 'vanilla-http'); await html.goto(`${vanilla.origin}/index.html`); await html.waitForFunction(() => window.orgChartReady); snapshots.htmlV1 = await snapshot(html); const page = await context.newPage(); captureErrors(page, 'react-production'); if (process.env.ORGCHART_CONSUMER_URL) { const production = await context.newPage(); captureErrors(production, 'react-production'); await production.goto(consumer.origin); await ready(production, 'A', 'mock-1'); snapshots.reactProduction = await snapshot(production); check(JSON.stringify(snapshots.reactProduction.organizations) === JSON.stringify(snapshots.htmlV1.organizations) && snapshots.reactProduction.css.every(link => link.loaded), 'independent production React Consumer: public imports, assets and identical HTML nodes'); await production.close(); } await page.goto(process.env.ORGCHART_CONSUMER_URL || consumer.origin); await ready(page, 'A', 'mock-1'); snapshots.reactV1 = await snapshot(page); check(JSON.stringify(snapshots.reactV1.organizations) === JSON.stringify(snapshots.htmlV1.organizations), 'HTML/React v1 hierarchy, employee placement and identical node UI', snapshots.reactV1.organizations); check(snapshots.reactV1.summary === snapshots.htmlV1.summary && snapshots.reactV1.heading === snapshots.htmlV1.heading, 'HTML/React v1 identical summary and presentation'); check(JSON.stringify(snapshots.reactV1.style) === JSON.stringify(snapshots.htmlV1.style), 'HTML/React identical computed theme', snapshots.reactV1.style); check(snapshots.reactV1.css.length === 2 && snapshots.reactV1.css.every(link => link.loaded), 'production Shadow DOM CSS assets loaded', snapshots.reactV1.css); const initial = await page.evaluate(() => ({ events: window.OrgChartReactDemo.events, requests: window.OrgChartReactDemo.requests, observers: window.chartObservers() })); const expectedLoads = process.env.ORGCHART_CONSUMER_URL ? 2 : 1; check(initial.requests.v1.loads === expectedLoads && initial.events.filter(event => event.state.status === 'ready').length === 1, 'Strict Mode: expected initial load count and single ready (development replay; production single setup)', initial); check(await page.locator('.hm-orgchart-host').count() === 1, 'Strict Mode: exactly one active Core host'); const hostId = await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance'); await patch(page, { callback: 2, className: 'consumer-chart changed' }); await page.waitForFunction(() => document.querySelector('.consumer-chart.changed')); check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId && (await page.evaluate(() => window.OrgChartReactDemo.requests.v1.loads)) === expectedLoads, 'callback/className-only render retains Core and does not reload'); await source(page, 'v2'); await ready(page, 'A', 'mock-2'); check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === hostId, 'source identity uses reload on the same Core host'); const stateV2 = await page.evaluate(() => window.OrgChartReactDemo.events.at(-1)); check(stateV2.callback === 2 && stateV2.state.summary.raw.total === 5 && stateV2.state.summary.raw.byOrganization['team-a'].direct === 1 && stateV2.state.summary.raw.byOrganization['team-b'].direct === 3, 'latest callback and v2 expected 1/1/3 placement, total 5', stateV2); await html.evaluate(() => window.OrgChartDemo.update()); snapshots.htmlV2 = await snapshot(html); snapshots.reactV2 = await snapshot(page); check(JSON.stringify(snapshots.reactV2.organizations) === JSON.stringify(snapshots.htmlV2.organizations) && snapshots.reactV2.summary === snapshots.htmlV2.summary, 'HTML/React v2 data, hierarchy, employee nodes and aggregation parity'); for (const width of [390, 768, 1280, 1440]) { await page.setViewportSize({ width, height: 1100 }); await page.evaluate(() => document.fonts.ready); const metric = await page.locator('.hm-orgchart-host').evaluate(host => { const shadow = host.shadowRoot, viewport = shadow.querySelector('.hm-orgchart-viewport'); const rectangles = [...shadow.querySelectorAll('.node')].map(node => node.getBoundingClientRect()); const overlap = rectangles.some((a, index) => rectangles.slice(index + 1).some(b => Math.min(a.right, b.right) - Math.max(a.left, b.left) > 1 && Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 1)); const organization = shadow.querySelector('.hm-orgchart-organization'), style = getComputedStyle(organization); const probe = document.createElement('div'); probe.style.color = 'var(--hm-sem-fg-primary)'; probe.style.background = 'var(--hm-sem-bg-surface)'; probe.style.borderRadius = 'var(--hm-radius-container)'; shadow.append(probe); const expectedStyle = getComputedStyle(probe), expected = { color: expectedStyle.color, background: expectedStyle.backgroundColor, radius: expectedStyle.borderRadius }; const match = expected.color === style.color && expected.background === style.backgroundColor && expected.radius === style.borderRadius; probe.remove(); const nodes = [...shadow.querySelectorAll('.node')], rect = nodes[0].getBoundingClientRect(); const centers = nodes.slice(1).map(node => { const r = node.getBoundingClientRect(); return r.x + r.width / 2; }); return { document: { clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, clientHeight: document.documentElement.clientHeight, scrollHeight: document.documentElement.scrollHeight }, host: { clientWidth: host.clientWidth, scrollWidth: host.scrollWidth, clientHeight: host.clientHeight, scrollHeight: host.scrollHeight }, viewport: { clientWidth: viewport.clientWidth, scrollWidth: viewport.scrollWidth, clientHeight: viewport.clientHeight, scrollHeight: viewport.scrollHeight, overflowX: getComputedStyle(viewport).overflowX }, overlap, themeMatch: match, expected, color: style.color, background: style.backgroundColor, radius: style.borderRadius, font: style.fontFamily, fontLoaded: document.fonts.check('13px "Noto Sans KR"'), idWhiteSpace: getComputedStyle(shadow.querySelector('.hm-orgchart-domain-id')).whiteSpace, connectorsAligned: Math.abs(rect.x + rect.width / 2 - (centers[0] + centers[1]) / 2) < 2, connectorTheme: getComputedStyle(nodes[1], '::before').backgroundColor === getComputedStyle(viewport).borderTopColor, }; }); metrics.push({ width, ...metric }); check(metric.document.scrollWidth <= width && metric.host.scrollWidth <= metric.host.clientWidth && metric.viewport.overflowX === 'auto' && !metric.overlap, `${width}px container overflow/scroll/no overlap`, metric); check(metric.themeMatch && metric.font.includes('Noto Sans KR') && metric.fontLoaded && metric.idWhiteSpace === 'nowrap' && metric.connectorsAligned && metric.connectorTheme, `${width}px theme/fonts/IDs/connectors`, metric); await page.screenshot({ path: path.join(output, `react-consumer-${width}.png`), fullPage: true }); } const toggle = page.locator('.hm-orgchart-toggle').first(); await toggle.focus(); await toggle.press('Enter'); await page.waitForFunction(() => !document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.sliding')); check(await toggle.getAttribute('aria-expanded') === 'false', 'React keyboard Enter collapses'); await toggle.press('Space'); await page.waitForFunction(() => !document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.sliding')); check(await toggle.getAttribute('aria-expanded') === 'true', 'React keyboard Space expands'); await source(page, 'v1'); await ready(page, 'A', 'mock-1'); check(await page.locator('.hm-orgchart-viewport').evaluate(element => element.getRootNode().activeElement === element), 'source reload restores removed keyboard focus to stable viewport'); await page.evaluate(() => window.OrgChartReactDemo.render({ sourceA: window.OrgChartReactDemo.slowSource })); await page.waitForFunction(() => window.OrgChartReactDemo.requests.slow.loads === 1); await source(page, 'v2'); await ready(page, 'A', 'mock-2'); const beforeLate = await page.evaluate(() => window.OrgChartReactDemo.events.length); await page.evaluate(() => window.OrgChartReactDemo.releaseSlow()); await page.waitForTimeout(100); check(await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.dataVersion === 'mock-2' && window.OrgChartReactDemo.requests.slow.aborts === 1) && (await page.evaluate(() => window.OrgChartReactDemo.events.length)) === beforeLate, 'superseded abort-ignoring source cannot update DOM/callback'); await source(page, 'error'); await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1).state.status === 'error'); check(await page.locator('.hm-orgchart-issues').count() === 1, 'source failure controlled error state'); await source(page, 'v1'); await ready(page, 'A', 'mock-1'); check(await page.locator('.hm-orgchart-issues').count() === 0, 'Error to Ready recovery via source replacement'); await source(page, 'zeroEmployees'); await ready(page); check((await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.summary.raw.total)) === 0, 'zero employees remains Ready with total 0'); await source(page, 'zeroOrganizations'); await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1).state.status === 'error'); check(await page.locator('.hm-orgchart-issues').count() === 1, 'zero organizations is Error; no fabricated empty state'); await source(page, 'v1'); await ready(page, 'A', 'mock-1'); await page.evaluate(() => window.OrgChartReactDemo.render({ aggregationPolicy: { policyVersion: 'consumer-test', includedStatuses: ['active'], activeStatuses: ['active'], includeLeaders: true, membershipMode: 'primaryOnly' } })); await page.waitForFunction(() => window.OrgChartReactDemo.events.at(-1).state.aggregation?.status === 'ready'); check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') !== hostId && (await page.evaluate(() => window.OrgChartReactDemo.events.at(-1).state.aggregation.counts.total)) === 2, 'policy change safely recreates Core and uses explicit test policy'); let currentHost = await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance'); await page.evaluate(() => window.OrgChartReactDemo.render({ stylesheetUrls: { ...window.OrgChartReactDemo.configuration.stylesheetUrls } })); await page.waitForTimeout(70); check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === currentHost, 'equal stylesheet URL values retain Core'); await page.evaluate(() => { const urls = window.OrgChartReactDemo.configuration.stylesheetUrls; window.OrgChartReactDemo.render({ stylesheetUrls: { engine: urls.engine + '?consumer=2', core: urls.core }, aggregationPolicy: undefined }); }); await page.waitForFunction(oldId => document.querySelector('.hm-orgchart-host')?.dataset.orgchartInstance !== oldId, currentHost); await ready(page); check(await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') !== currentHost && (await snapshot(page)).css.every(link => link.loaded), 'changed stylesheet URL safely recreates and loads assets'); await patch(page, { mountedB: true }); await ready(page, 'B', 'mock-2'); const bHost = await page.locator('[data-instance="B"] .hm-orgchart-host').getAttribute('data-orgchart-instance'); const bSnapshot = await snapshot(page, '[data-instance="B"] .hm-orgchart-host'); await source(page, 'v2'); await ready(page, 'A', 'mock-2'); check(await page.locator('.hm-orgchart-host').count() === 2 && JSON.stringify(await snapshot(page, '[data-instance="B"] .hm-orgchart-host')) === JSON.stringify(bSnapshot), 'two simultaneous instances; A update preserves B'); await page.evaluate(() => { document.querySelector('[data-instance="A"]').style.setProperty('--hm-sem-bg-surface', getComputedStyle(document.documentElement).getPropertyValue('--hm-sem-bg-content')); }); check((await snapshot(page, '[data-instance="A"] .hm-orgchart-host')).style.background !== bSnapshot.style.background && (await snapshot(page, '[data-instance="B"] .hm-orgchart-host')).style.background === bSnapshot.style.background, 'scoped existing Theme variable change inherits through Shadow DOM and isolates B'); await patch(page, { mountedA: false }); await page.waitForFunction(() => !document.querySelector('[data-instance="A"]')); check(await page.locator('.hm-orgchart-host').count() === 1 && await page.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === bHost, 'A unmount preserves B and its Core identity'); const bObservers = await page.evaluate(() => window.chartObservers()); for (let iteration = 0; iteration < 5; iteration++) { await patch(page, { mountedA: true }); await ready(page, 'A', 'mock-2'); await page.waitForFunction(() => document.querySelectorAll('.hm-orgchart-host').length === 2); await patch(page, { mountedA: false }); await page.waitForFunction(() => document.querySelectorAll('.hm-orgchart-host').length === 1); } check(await page.evaluate(() => window.chartObservers()) === bObservers, 'five Strict Mode mount/unmount cycles preserve B observer baseline'); await patch(page, { mountedA: true }); await ready(page, 'A', 'mock-2'); await page.evaluate(() => window.OrgChartReactDemo.render({ sourceA: window.OrgChartReactDemo.slowSource })); await page.waitForFunction(() => window.OrgChartReactDemo.requests.slow.loads === 2); await patch(page, { mountedA: false }); await page.waitForFunction(() => !document.querySelector('[data-instance="A"]')); const unmountEvents = await page.evaluate(() => window.OrgChartReactDemo.events.length); await page.evaluate(() => window.OrgChartReactDemo.releaseSlow()); await page.waitForTimeout(100); check((await page.evaluate(() => window.OrgChartReactDemo.events.length)) === unmountEvents, 'late response after unmount cannot emit callback'); check((await page.evaluate(() => window.OrgChartReactDemo.lifecycleErrors.length)) === 0, 'normal lifecycle has no rejected-use errors'); await patch(page, { mountedA: true, stylesheetUrls: { engine: '', core: '' } }); await page.waitForFunction(() => window.OrgChartReactDemo.lifecycleErrors.length > 0); check(await page.locator('.hm-orgchart-host').count() === 0, 'invalid Core configuration is reported via lifecycle error without leaked hosts'); await page.evaluate(() => window.OrgChartReactDemo.destroy()); check(await page.locator('.hm-orgchart-host').count() === 0 && await page.evaluate(() => window.chartObservers()) === 0, 'terminal React root unmount removes all Core DOM/observers'); const index = JSON.parse(await readFile(path.join(root, 'storybook-static/index.json'), 'utf8')); const stories = Object.values(index.entries).filter(entry => entry.title === 'OrgChart/Content' && entry.type === 'story'); check(stories.length === 8, 'production Storybook registers exactly eight OrgChart stories', stories.map(story => ({ id: story.id, name: story.name }))); const sb = await context.newPage(); captureErrors(sb, 'storybook-production'); for (const story of stories) { await sb.goto(`${storybook.origin}/iframe.html?id=${story.id}&viewMode=story`); await sb.waitForFunction(() => [...document.querySelectorAll('.hm-orgchart-host')].some(host => host.shadowRoot.querySelector('.hm-orgchart-organization, .hm-orgchart-issues, .hm-orgchart-state:not(:empty)'))); if (story.id.includes('loading')) { check((await sb.locator('.hm-orgchart-state').allTextContents()).some(text => text.includes('불러오는')), `${story.name}: actual loading source`); } else { await sb.waitForFunction(() => [...document.querySelectorAll('.hm-orgchart-host')].every(host => host.shadowRoot.querySelector('.hm-orgchart-organization'))); check(await sb.locator('.node').count() >= 3, `${story.name}: real Core nodes rendered`); } if (story.id.includes('json-data-update')) { await sb.getByRole('button', { name: 'JSON v2', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2')); check((await sb.locator('.hm-orgchart-raw-total').textContent()).includes('5명'), 'Storybook JSON v1 to v2 recalculates 5 employees'); await sb.getByRole('button', { name: 'Unmount', exact: true }).click(); check(await sb.locator('.hm-orgchart-host').count() === 0, 'Storybook Unmount removes Core'); await sb.getByRole('button', { name: 'Mount', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('.hm-orgchart-host')?.shadowRoot.querySelector('.node')); check(await sb.locator('.hm-orgchart-host').count() === 1, 'Storybook remount creates one Core'); } if (story.id.includes('loading')) { await sb.getByRole('button', { name: 'Source Error', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('error')); check(await sb.locator('.hm-orgchart-issues').count() === 1, 'Storybook source error controlled by Core'); await sb.getByRole('button', { name: 'Ready v2 / 복구', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2')); check(await sb.locator('.node').count() === 3, 'Storybook Error to Ready recovery'); await sb.getByRole('button', { name: '직원 0명 (Ready)', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('story-zero-employees')); check((await sb.locator('.hm-orgchart-raw-total').textContent()).includes('0명'), 'Storybook zero employees Ready'); await sb.getByRole('button', { name: '조직 0개 (Error)', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('error')); check(await sb.locator('.hm-orgchart-issues').count() === 1, 'Storybook zero organizations Error'); await sb.getByRole('button', { name: '느린 v1 (취소 무시)', exact: true }).click(); await sb.getByRole('button', { name: 'Ready v2 / 복구', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2')); const outputBefore = await sb.locator('output').textContent(); await sb.waitForTimeout(4100); check(await sb.locator('output').textContent() === outputBefore, 'Storybook stale abort-ignoring response produces no callback'); } if (story.id.includes('multiple-instances')) { const b = sb.getByRole('region', { name: 'OrgChart B', exact: true }); const bIdentity = await b.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance'); const bState = await b.locator('output').textContent(); await sb.getByRole('button', { name: 'A → JSON v2', exact: true }).click(); await sb.waitForFunction(() => document.querySelector('output')?.textContent.includes('mock-2')); await sb.getByRole('button', { name: 'A Unmount', exact: true }).click(); check(await sb.locator('.hm-orgchart-host').count() === 1 && await b.locator('.hm-orgchart-host').getAttribute('data-orgchart-instance') === bIdentity && await b.locator('output').textContent() === bState, 'Storybook A update/unmount leaves B identity, data and callback count unchanged'); } if (story.id.includes('basic-organization') || story.id.includes('employee-cards') || story.id.includes('responsive-container')) { for (const width of [390, 768, 1280, 1440]) { await sb.setViewportSize({ width, height: 1100 }); if (story.id.includes('responsive-container')) await sb.getByRole('button', { name: `${width}px`, exact: true }).click(); const dimensions = await sb.evaluate(() => { const shadow = document.querySelector('.hm-orgchart-host').shadowRoot; const nodes = [...shadow.querySelectorAll('.node')], viewport = shadow.querySelector('.hm-orgchart-viewport'); const rects = nodes.map(node => node.getBoundingClientRect()); return { clientWidth: document.documentElement.clientWidth, scrollWidth: document.documentElement.scrollWidth, viewport: { clientWidth: viewport.clientWidth, scrollWidth: viewport.scrollWidth, clientHeight: viewport.clientHeight, scrollHeight: viewport.scrollHeight }, nodeWidths: rects.map(rect => rect.width), overlap: rects.some((a, index) => rects.slice(index + 1).some(b => Math.min(a.right,b.right)-Math.max(a.left,b.left)>1 && Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top)>1)), avatars: [...shadow.querySelectorAll('.hm-orgchart-avatar')].every(avatar => avatar.textContent && !avatar.querySelector('img')), textFits: [...shadow.querySelectorAll('.hm-orgchart-person-name,.hm-orgchart-person-role,.hm-orgchart-organization-name')].every(element => element.scrollWidth <= element.clientWidth), }; }); metrics.push({ story: story.name, width, ...dimensions }); check(dimensions.scrollWidth <= width && !dimensions.overlap && dimensions.textFits && dimensions.avatars, `${story.name}: ${width}px document scroll/overlap/long text/profile fallback`, dimensions); await sb.screenshot({ path: path.join(output, `${story.id}-${width}.png`), fullPage: true }); } } await sb.screenshot({ path: path.join(output, `${story.id}.png`), fullPage: true }); } if (process.env.ORGCHART_STORYBOOK_URL) { await sb.goto(`${process.env.ORGCHART_STORYBOOK_URL}/iframe.html?id=orgchart-content--basic-organization&viewMode=story`); await sb.waitForFunction(() => document.querySelector('.hm-orgchart-host')?.shadowRoot.querySelector('.hm-orgchart-organization')); check(await sb.locator('.node').count() === 3, 'development Storybook on reserved separate port executes real Wrapper'); } check(errors.length === 0, 'browser console/runtime errors are zero across consumer, Vanilla and Storybook', errors); } catch (error) { results.push({ name: 'verification completion', status: 'FAIL', detail: error.stack }); process.exitCode = 1; } finally { await writeFile(path.join(output, 'report.json'), JSON.stringify({ browser: await browser.version(), results, errors, metrics, snapshots }, null, 2)); await browser.close(); for (const { server } of [consumer, vanilla, storybook]) 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, report: path.join(output, 'report.json') })); }