247 lines
27 KiB
JavaScript
247 lines
27 KiB
JavaScript
// 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;
|
|
}
|