// P9.5 Full Composition only: same-mount resize, artwork coordinates and native interactions. import fs from 'node:fs/promises'; import path from 'node:path'; import { createHash } from 'node:crypto'; import { fileURLToPath } from 'node:url'; import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs'; const root = fileURLToPath(new URL('../../', import.meta.url)); const output = path.join(root, 'test-results/org-chart-p9-5-full'); const origin = (process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6064').replace(/\/$/, ''); const baseline = JSON.parse(await fs.readFile(path.join(output, 'before.json'), 'utf8')); const fixture = JSON.parse(await fs.readFile(path.join(root, 'examples/org-chart/data-p1.json'), 'utf8')); const names = ['team-arrow-blue-short.png', 'team-arrow-blue-purple.png', 'team-arrow-purple-teal.png', 'team-arrow-teal-green.png']; const assetHashes = Object.fromEntries(await Promise.all(names.map(async name => [name, createHash('sha256').update(await fs.readFile(path.join(root, 'packages/org-chart/src/assets/workdesk', name))).digest('hex')]))); const arrowAssets = { 'blue-short-1': names[0], 'blue-short-2': names[0], 'blue-purple': names[1], 'purple-teal': names[2], 'teal-green': names[3] }; const checks = [], measurements = [], errors = [], arrowPaint = []; const startedAt = new Date().toISOString(); const browser = await launchChromium({ headless: true }); const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); page.setDefaultTimeout(30000); page.on('pageerror', error => errors.push({ type: 'pageerror', message: error.message })); page.on('console', message => { if (message.type() === 'error') errors.push({ type: 'console', message: message.text() }); }); const check = (pass, name, detail) => { checks.push({ name, status: pass ? 'PASS' : 'FAIL', detail }); if (!pass) console.log('FAIL', name, JSON.stringify(detail)); }; const notVerified = (name, detail) => checks.push({ name, status: 'NOT VERIFIED', detail }); const near = (a, b, tolerance = .15) => Object.keys(a).every(key => Math.abs(a[key] - b[key]) <= tolerance); const settle = () => page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 20; i++) await new Promise(requestAnimationFrame); }); async function ready() { await page.waitForFunction(() => { const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot; return shadow?.querySelector('.hm-orgchart-stage[data-workdesk-artwork]') && !shadow.querySelector('[aria-busy="true"]') && [...shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet); }); await page.evaluate(async () => { await Promise.all([...document.querySelector('.hm-orgchart-host').shadowRoot.querySelectorAll('img')].map(image => image.decode())); }); await settle(); } async function snapshot(name) { const value = await page.evaluate(async () => { const host = document.querySelector('.hm-orgchart-story-full'), shadow = host.querySelector('.hm-orgchart-host').shadowRoot; const stage = shadow.querySelector('.hm-orgchart-stage'), wrapper = shadow.querySelector('.hm-orgchart-composition'); const rect = element => { const r = element.getBoundingClientRect(); return { left: r.left, right: r.right, top: r.top, bottom: r.bottom, width: r.width, height: r.height }; }; const origin = rect(stage), scale = origin.width / stage.offsetWidth, offset = Number(wrapper.dataset.centerOffset) || 0; const normal = element => { const r = rect(element); return { x: (r.left - origin.left) / scale, y: (r.top - origin.top) / scale - offset, width: r.width / scale, height: r.height / scale }; }; const hash = async bytes => [...new Uint8Array(await crypto.subtle.digest('SHA-256', bytes))].map(byte => byte.toString(16).padStart(2, '0')).join(''); const central = [...stage.querySelectorAll('.hm-orgchart-axis .hm-orgchart-employee,.hm-orgchart-hub,.hm-orgchart-viewport .hm-orgchart-employee,.hm-orgchart-viewport .hm-orgchart-organization-name')].map((element, index) => ({ key: `${element.className}:${element.dataset.employeeId ?? element.closest('.node')?.dataset.organizationId ?? ''}:${index}`, box: normal(element) })); const arrows = await Promise.all([...stage.querySelectorAll('image[data-arrow]')].map(async element => { const href = element.getAttribute('href'), image = new Image(); image.src = href; await image.decode(); return { id: element.dataset.arrow, box: normal(element), rect: rect(element), hash: await hash(await (await fetch(href)).arrayBuffer()), loaded: image.naturalWidth > 0, attributes: Object.fromEntries(['x', 'y', 'width', 'height', 'preserveAspectRatio'].map(key => [key, element.getAttribute(key)])) }; })); const paths = [...stage.querySelectorAll('.hm-orgchart-workdesk-overlay path')].map(element => { const point = element.getPointAtLength(element.getTotalLength()), screen = new DOMPoint(point.x, point.y).matrixTransform(element.getScreenCTM()); return { id: element.dataset.connector, d: element.getAttribute('d'), box: normal(element), endpoint: { x: screen.x, y: screen.y } }; }); const management = rect(stage.querySelector('.node[data-organization-id="business-management"] .hm-orgchart-organization-name')); const hostRect = rect(host), css = getComputedStyle(host); const left = hostRect.left + (parseFloat(css.borderLeftWidth) || 0) + (parseFloat(css.paddingLeft) || 0); const right = hostRect.right - (parseFloat(css.borderRightWidth) || 0) - (parseFloat(css.paddingRight) || 0); const line = paths.find(item => item.id === 'hub-to-business-management'); return { size: { width: innerWidth, height: innerHeight }, stage: origin, scale, offset, canvasHeight: Number(wrapper.dataset.canvasHeight), sameMount: stage === window.__p95MountedStage, horizontalError: (origin.left + origin.right - left - right) / 2, central, arrows, paths, managementEndpointError: { x: line.endpoint.x - (management.left + management.right) / 2, y: line.endpoint.y - management.top - 2 * scale }, links: [...stage.querySelectorAll('.hm-orgchart-organization-link')].map(element => ({ href: element.href, text: element.textContent })) }; }); measurements.push({ name, ...value }); return value; } async function stability() { return page.evaluate(async () => { const stage = document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.hm-orgchart-stage'); const records = []; const observer = new MutationObserver(items => { for (const item of items) records.push({ oldStyle: item.oldValue, style: stage.getAttribute('style'), at: performance.now() }); }); observer.observe(stage, { attributes: true, attributeFilter: ['style'], attributeOldValue: true }); for (let i = 0; i < 20; i++) await new Promise(requestAnimationFrame); observer.disconnect(); return { count: records.length, records }; }); } // Directly affected by the adaptive-anchor notification: a bounded parent // grows while viewport and scale remain unchanged. Do not replay other checks. if (process.argv.includes('--anchor-notify-only')) { const probes = []; try { await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`); await ready(); const value = await page.evaluate(async data => { const api = await import('/core/index.js'), host = document.createElement('section'); Object.assign(host.style, { position: 'fixed', left: '0', top: '0', width: '1920px', height: '914.5px', zIndex: '1000', overflow: 'hidden' }); document.body.append(host); const handle = api.createOrgChart({ container: host, dataSource: new api.JsonOrgChartDataSource(data), displayMode: 'readOnly', stylesheetUrls: { engine: '/core/engine.css', core: '/core/style.css' } }); try { await handle.mount(); const shadow = host.querySelector('.hm-orgchart-host').shadowRoot; await Promise.all([...shadow.querySelectorAll('link[rel="stylesheet"]')].map(link => link.sheet ? Promise.resolve() : new Promise((resolve, reject) => { link.addEventListener('load', resolve, { once: true }); link.addEventListener('error', reject, { once: true }); }))); const waitFrames = async () => { await document.fonts.ready; for (let i = 0; i < 20; i++) await new Promise(requestAnimationFrame); }; await waitFrames(); const wrapper = shadow.querySelector('.hm-orgchart-composition'), stage = shadow.querySelector('.hm-orgchart-stage'); let notifications = 0; wrapper.addEventListener('hm-orgchart-scale', () => { notifications++; }); const rect = element => { const r = element.getBoundingClientRect(); return { left: r.left, right: r.right, top: r.top, bottom: r.bottom, width: r.width, height: r.height }; }; const scroll = element => ({ width: element.scrollWidth, clientWidth: element.clientWidth, height: element.scrollHeight, clientHeight: element.clientHeight, overflowX: getComputedStyle(element).overflowX, overflowY: getComputedStyle(element).overflowY }); const measure = () => ({ size: { width: innerWidth, height: innerHeight }, host: rect(host), stage: rect(stage), scale: rect(stage).width / 1920, centerOffset: Number(wrapper.dataset.centerOffset), canvasHeight: Number(wrapper.dataset.canvasHeight), notifications, representative: rect(stage.querySelector('.hm-orgchart-top-leaders .hm-orgchart-employee')), hub: rect(stage.querySelector('.hm-orgchart-hub')), tree: rect(stage.querySelector('.hm-orgchart-viewport')), scroll: { root: scroll(document.documentElement), body: scroll(document.body), host: scroll(host), wrapper: scroll(wrapper), viewport: scroll(stage.querySelector('.hm-orgchart-viewport')) }, painted: [...stage.querySelectorAll('.hm-orgchart-employee,.hm-orgchart-organization-name,image[data-arrow],.hm-orgchart-heading,.hm-orgchart-slot-side,.hm-orgchart-slot-footer')].map(rect) }); const before = measure(); host.style.height = '1014.5px'; await waitFrames(); const after = measure(); return { before, after }; } finally { handle.destroy(); host.remove(); } }, fixture); probes.push(value); check(value.before.scale === 1 && value.after.scale === 1 && value.before.size.width === value.after.size.width && value.before.size.height === value.after.size.height, 'Bounded parent height-only resize retains scale 1 without viewport resize', { before: value.before.scale, after: value.after.scale, viewport: value.after.size }); check(value.before.centerOffset === 0 && value.after.centerOffset > value.before.centerOffset && value.after.canvasHeight > value.before.canvasHeight && value.after.representative.top > value.before.representative.top && value.after.hub.top > value.before.hub.top && value.after.tree.top > value.before.tree.top, 'Bounded parent height-only resize recalculates central anchors', { beforeOffset: value.before.centerOffset, afterOffset: value.after.centerOffset, representativeDelta: value.after.representative.top - value.before.representative.top, hubDelta: value.after.hub.top - value.before.hub.top, treeDelta: value.after.tree.top - value.before.tree.top }); check(value.before.notifications === 0 && value.after.notifications > 0, 'Adaptive anchor change emits existing hm-orgchart-scale notification at unchanged scale', { before: value.before.notifications, after: value.after.notifications }); const scrollable = Object.entries(value.after.scroll).filter(([, item]) => ['auto', 'scroll'].includes(item.overflowX) && item.width > item.clientWidth + 1 || ['auto', 'scroll'].includes(item.overflowY) && item.height > item.clientHeight + 1); const documentFits = ['root', 'body'].every(key => value.after.scroll[key].width <= 1921 && value.after.scroll[key].height <= 1081); const inside = value.after.painted.every(item => item.left >= value.after.host.left - 1 && item.right <= value.after.host.right + 1 && item.top >= value.after.host.top - 1 && item.bottom <= value.after.host.bottom + 1); check(!scrollable.length && documentFits && inside && value.after.stage.bottom <= value.after.host.bottom + 1, 'Bounded parent resize keeps actual content contained without document or internal scrollbars', { scrollable, scroll: value.after.scroll, inside }); } catch (error) { check(false, 'Anchor notification probe completed', { message: error.message, stack: error.stack }); } check(errors.length === 0, 'Anchor notification probe console/page errors zero', errors); const result = { checks, probes, errors, pass: checks.filter(item => item.status === 'PASS').length, fail: checks.filter(item => item.status === 'FAIL').length }; await fs.writeFile(path.join(output, 'anchor-notification.json'), JSON.stringify(result, null, 2)); await browser.close(); console.log(JSON.stringify({ pass: result.pass, fail: result.fail, checks, output: path.join(output, 'anchor-notification.json') })); process.exit(result.fail ? 1 : 0); } // Repeat only a failed stability probe, preserving its first-run evidence. if (process.argv.includes('--stability-only')) { const probes = []; try { await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`); await ready(); await page.evaluate(() => { window.__p95MountedStage = document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.hm-orgchart-stage'); }); for (const size of [{ width: 1920, height: 1080 }, { width: 1920, height: 1440 }, { width: 1440, height: 900 }, { width: 1920, height: 1080 }]) { await page.setViewportSize(size); await settle(); const state = await page.evaluate(() => { const host = document.querySelector('.hm-orgchart-story-full'), shadow = host.querySelector('.hm-orgchart-host').shadowRoot; const stage = shadow.querySelector('.hm-orgchart-stage'), wrapper = shadow.querySelector('.hm-orgchart-composition'); const r = stage.getBoundingClientRect(), h = host.getBoundingClientRect(), css = getComputedStyle(host); const left = h.left + (parseFloat(css.borderLeftWidth) || 0) + (parseFloat(css.paddingLeft) || 0), right = h.right - (parseFloat(css.borderRightWidth) || 0) - (parseFloat(css.paddingRight) || 0); return { horizontalError: (r.left + r.right - left - right) / 2, sameMount: stage === window.__p95MountedStage, offset: Number(wrapper.dataset.centerOffset), scale: r.width / 1920 }; }); probes.push({ size, ...state }); check(state.sameMount && Math.abs(state.horizontalError) <= 1, `${size.width}x${size.height}-${probes.length}: modified centering helper retains same mount within 1px`, probes.at(-1)); if (size.width === 1440) { const probe = await stability(); probes.at(-1).stability = probe; check(probe.count === 0, '1440x900: twenty settled frames have zero Stage style mutations (failed item recheck)', probe); } } const benchmark = await page.evaluate(async data => { const api = await import('/core/index.js'), host = document.createElement('section'); Object.assign(host.style, { position: 'fixed', left: '0', top: '0', width: '1920px', height: '914.5px', zIndex: '1000', overflow: 'hidden' }); document.body.append(host); const handle = api.createOrgChart({ container: host, dataSource: new api.JsonOrgChartDataSource(data), displayMode: 'readOnly', stylesheetUrls: { engine: '/core/engine.css', core: '/core/style.css' } }); try { await handle.mount(); const shadow = host.querySelector('.hm-orgchart-host').shadowRoot; await Promise.all([...shadow.querySelectorAll('link[rel="stylesheet"]')].map(link => link.sheet ? Promise.resolve() : new Promise((resolve, reject) => { link.addEventListener('load', resolve, { once: true }); link.addEventListener('error', reject, { once: true }); }))); await document.fonts.ready; for (let i = 0; i < 20; i++) await new Promise(requestAnimationFrame); const stage = shadow.querySelector('.hm-orgchart-stage'), wrapper = shadow.querySelector('.hm-orgchart-composition'); const rect = element => { const r = element.getBoundingClientRect(); return { left: r.left, right: r.right, top: r.top, bottom: r.bottom, width: r.width, height: r.height }; }; const s = rect(stage), h = rect(host), status = rect(stage.querySelector('.hm-orgchart-slot-belowTree')), footer = rect(stage.querySelector('.hm-orgchart-slot-footer')); return { scale: s.width / 1920, centerOffset: Number(wrapper.dataset.centerOffset), canvasHeight: Number(wrapper.dataset.canvasHeight), stage: s, host: h, status, footer, gap: footer.top - status.bottom, cards: stage.querySelectorAll('.hm-orgchart-employee').length, horizontalError: (s.left + s.right - h.left - h.right) / 2 }; } finally { handle.destroy(); host.remove(); } }, fixture); probes.push({ name: 'public-1920x914.5-base-benchmark', ...benchmark }); check(benchmark.scale === 1 && benchmark.centerOffset === 0 && benchmark.canvasHeight === 914.5 && Math.abs(benchmark.stage.height - 914.5) < .02, 'Original 1920x914.5 design benchmark retains scale 1 and center offset 0', benchmark); check(Math.abs(benchmark.gap - 14.609375) < .02, 'Original design benchmark exposes 14.609375px status/footer gap', benchmark.gap); check(benchmark.cards === 43 && Math.abs(benchmark.horizontalError) <= 1, 'Original design benchmark retains cards and content horizontal center', benchmark); } catch (error) { check(false, 'Stability reproduction completed', { message: error.message, stack: error.stack }); } check(errors.length === 0, 'Stability reproduction console/page errors zero', errors); const result = { checks, probes, errors, pass: checks.filter(item => item.status === 'PASS').length, fail: checks.filter(item => item.status === 'FAIL').length }; await fs.writeFile(path.join(output, 'stability-recheck.json'), JSON.stringify(result, null, 2)); await browser.close(); console.log(JSON.stringify(result)); process.exit(result.fail ? 1 : 0); } try { await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`); await ready(); await page.evaluate(() => { window.__p95MountedStage = document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.hm-orgchart-stage'); }); let first; for (const size of [{ width: 1920, height: 1080 }, { width: 1920, height: 1440 }, { width: 1440, height: 900 }, { width: 1920, height: 1080 }]) { await page.setViewportSize(size); await settle(); const name = `${size.width}x${size.height}-${measurements.length + 1}`, current = await snapshot(name); first ??= current; check(current.sameMount, `${name}: same Full Composition mount retained`); check(Math.abs(current.horizontalError) < .75, `${name}: actual wrapper horizontal center retained`, current.horizontalError); check(current.central.length === first.central.length && current.central.every((item, index) => item.key === first.central[index].key && near(item.box, first.central[index].box)), `${name}: representative, hub and all organization/card relative coordinates retained after center offset`, { count: current.central.length, offset: current.offset }); check(current.paths.length === 5 && current.paths.every((item, index) => item.id === first.paths[index].id && item.d === first.paths[index].d && near(item.box, first.paths[index].box)), `${name}: all five SVG paths share central movement without coordinate drift`, current.paths); check(Math.abs(current.managementEndpointError.x) < .05 && Math.abs(current.managementEndpointError.y) < .05, `${name}: management connector endpoint meets live header`, current.managementEndpointError); check(current.arrows.length === 5 && current.arrows.every((item, index) => item.loaded && item.id === first.arrows[index].id && near(item.box, first.arrows[index].box) && item.hash === assetHashes[arrowAssets[item.id]]), `${name}: five original arrow assets and relative geometry retained`, current.arrows.map(({ id, hash, box }) => ({ id, hash, box }))); const old = baseline.measurements.find(item => item.name === `${size.width}x${size.height}`); check(current.arrows.every(item => { const previous = old.arrows.find(arrow => arrow.id === item.id); return near(item.box, { x: previous.box.x, y: previous.box.y, width: previous.box.width, height: previous.box.height }); }), `${name}: collaboration arrow base placement unchanged from pre-P9.5 measurement`); const settled = await stability(); check(settled.count === 0, `${name}: twenty settled frames have zero Stage style mutations`, settled); } check(measurements[1].offset > measurements[0].offset && Math.abs(measurements[3].offset - measurements[0].offset) < .01, 'Taller viewport increases center offset and reverse resize restores it', measurements.map(({ name, offset, canvasHeight }) => ({ name, offset, canvasHeight }))); check(first.links.length === 0, 'Canonical Full Story retains its existing unconfigured group titles', first.links); // Compare each actually rendered arrow crop against the same crop with only // that arrow hidden. A changed image proves painted pixels, not merely DOM presence. for (const arrow of measurements.at(-1).arrows) { const r = arrow.rect, left = Math.max(0, Math.floor(r.left)), top = Math.max(0, Math.floor(r.top)); const clip = { x: left, y: top, width: Math.ceil(r.right) - left, height: Math.ceil(r.bottom) - top }; const shown = await page.screenshot({ clip, animations: 'disabled' }); await page.evaluate(id => { const element = document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(`image[data-arrow="${id}"]`); element.dataset.p95OldStyle = element.getAttribute('style') ?? ''; element.style.visibility = 'hidden'; }, arrow.id); const hidden = await page.screenshot({ clip, animations: 'disabled' }); await page.evaluate(id => { const element = document.querySelector('.hm-orgchart-host').shadowRoot.querySelector(`image[data-arrow="${id}"]`); const style = element.dataset.p95OldStyle; if (style) element.setAttribute('style', style); else element.removeAttribute('style'); delete element.dataset.p95OldStyle; }, arrow.id); const painted = !shown.equals(hidden); arrowPaint.push({ id: arrow.id, painted, clip, pngBytes: shown.length }); check(painted, `${arrow.id}: collaboration arrow contributes visible painted pixels`, arrowPaint.at(-1)); } await page.screenshot({ path: path.join(output, 'interaction-artwork-1920x1080.png'), fullPage: true, animations: 'disabled' }); const employee = page.locator('.hm-orgchart-viewport .hm-orgchart-employee').first(); const employeeId = await employee.getAttribute('data-employee-id'); await employee.focus(); await page.keyboard.press('Enter'); const dialog = page.locator('.hm-orgchart-employee-dialog[open]'); await dialog.waitFor(); check(await dialog.getAttribute('data-employee-id') === employeeId && (await dialog.textContent()).includes('홍 길 동'), 'Full employee keyboard activation opens existing popup with employee identity', { employeeId }); await page.screenshot({ path: path.join(output, 'interaction-employee-popup.png'), fullPage: true }); await page.keyboard.press('Escape'); await dialog.waitFor({ state: 'hidden' }); await settle(); check(await employee.evaluate(element => element === element.getRootNode().activeElement), 'Full employee popup ESC restores card focus'); // The Story intentionally has no configured links. Exercise the public link // API in an isolated temporary Workdesk mount; suppress native navigation. const coreAvailable = await fetch(`${origin}/core/index.js`).then(response => response.ok).catch(() => false); if (!coreAvailable) notVerified('Public organizationLinks native keyboard interaction', '/core/index.js route unavailable; canonical Story does not configure group links.'); else { const linkState = await page.evaluate(async data => { const api = await import('/core/index.js'); const host = document.createElement('section'); host.id = 'p95-link-probe'; Object.assign(host.style, { position: 'fixed', left: '0', top: '0', width: '1200px', height: '600px', zIndex: '1000', overflow: 'hidden' }); document.body.append(host); const href = 'https://example.invalid/group/business'; const handle = api.createOrgChart({ container: host, dataSource: new api.JsonOrgChartDataSource(data), displayMode: 'readOnly', stylesheetUrls: { engine: '/core/engine.css', core: '/core/style.css' }, organizationLinks: { 'business-management': href } }); await handle.mount(); const shadow = host.querySelector('.hm-orgchart-host').shadowRoot; await Promise.all([...shadow.querySelectorAll('link[rel="stylesheet"]')].map(link => link.sheet ? Promise.resolve() : new Promise((resolve, reject) => { link.addEventListener('load', resolve, { once: true }); link.addEventListener('error', reject, { once: true }); }))); for (let i = 0; i < 20; i++) await new Promise(requestAnimationFrame); const link = shadow.querySelector('.hm-orgchart-organization-link'); window.__p95LinkProbe = { handle, host, shadow, clicks: 0 }; link.addEventListener('click', event => { event.preventDefault(); window.__p95LinkProbe.clicks++; }); return { links: shadow.querySelectorAll('.hm-orgchart-organization-link').length, href: link.href, nativeTag: link.tagName, tabIndex: link.tabIndex, beforeCards: shadow.querySelectorAll('.hm-orgchart-employee').length }; }, fixture); const link = page.locator('#p95-link-probe .hm-orgchart-organization-link'); await link.focus(); check(await link.evaluate(element => element === element.getRootNode().activeElement) && linkState.links === 1 && linkState.nativeTag === 'A' && linkState.tabIndex === 0 && linkState.href === 'https://example.invalid/group/business', 'Public Full group link retains native anchor href and keyboard focus', linkState); await page.keyboard.press('Enter'); await settle(); const afterLink = await page.evaluate(() => ({ clicks: window.__p95LinkProbe.clicks, cards: window.__p95LinkProbe.shadow.querySelectorAll('.hm-orgchart-employee').length, dialogs: window.__p95LinkProbe.shadow.querySelectorAll('dialog[open]').length, url: location.href })); check(afterLink.clicks === 1 && afterLink.cards === linkState.beforeCards && afterLink.dialogs === 0 && afterLink.url.includes('orgchart--full-composition'), 'Group link keyboard activation preserves chart and suppresses network navigation', afterLink); await page.evaluate(() => { window.__p95LinkProbe.handle.destroy(); window.__p95LinkProbe.host.remove(); delete window.__p95LinkProbe; }); } } catch (error) { check(false, 'Focused Full interaction run completed', { message: error.message, stack: error.stack }); } finally { await page.evaluate(() => { if (window.__p95LinkProbe) { window.__p95LinkProbe.handle.destroy(); window.__p95LinkProbe.host.remove(); delete window.__p95LinkProbe; } }).catch(() => {}); check(errors.length === 0, 'No Full interaction console/page errors', errors); const result = { startedAt, completedAt: new Date().toISOString(), origin, browser: browser.version(), checks, measurements, arrowPaint, assetHashes, errors, pass: checks.filter(item => item.status === 'PASS').length, fail: checks.filter(item => item.status === 'FAIL').length, notVerified: checks.filter(item => item.status === 'NOT VERIFIED').length }; await fs.writeFile(path.join(output, 'interactions.json'), JSON.stringify(result, null, 2)); await browser.close(); console.log(JSON.stringify({ pass: result.pass, fail: result.fail, notVerified: result.notVerified, output: path.join(output, 'interactions.json') })); if (result.fail) process.exitCode = 1; }