Files
HMDesign/examples/org-chart/verify-p9-1.mjs

281 lines
27 KiB
JavaScript

// Focused P9.1 before/after evidence. Does not repeat P7-P9 or interact with Expanded.
import fs from 'node:fs/promises';
import { writeFileSync } from 'node:fs';
import path from 'node:path';
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-1');
const origin = (process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6061').replace(/\/$/, '');
const baselineMode = process.argv.includes('--baseline');
const resizeOnly = process.argv.includes('--resize-only');
const sizes = [{ width: 1920, height: 1080 }, { width: 1440, height: 900 }, { width: 390, height: 844 }];
const stories = { department: 'orgchart--department-colors', fixed: 'orgchart--full-composition' };
const publicStories = ['orgchart--logo-personnel-summary', ...Object.values(stories), 'orgchart--expanded-org-chart'];
const startedAt = new Date().toISOString(), results = [], snapshots = {}, errors = [];
await fs.mkdir(output, { recursive: true });
const progressFile = path.join(output, resizeOnly ? 'browser-resize-progress.json' : baselineMode ? 'browser-baseline-progress.json' : 'browser-progress.json');
const persist = () => writeFileSync(progressFile, JSON.stringify({ startedAt, origin, baselineMode, resizeOnly, results, snapshots, errors }, null, 2));
const check = (pass, name, detail) => {
results.push({ name, status: pass ? 'PASS' : 'FAIL', ...(detail === undefined ? {} : { detail }) });
persist();
if (!pass) console.log('FAIL', name, JSON.stringify(detail));
};
const equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const near = (a, b, tolerance = 0.15) => a && b && Object.keys(a).every(key => Math.abs(a[key] - b[key]) <= tolerance);
const contains = (outer, inner, tolerance = 1) => outer && inner && inner.x >= outer.x - tolerance && inner.y >= outer.y - tolerance && inner.right <= outer.right + tolerance && inner.bottom <= outer.bottom + tolerance;
const browser = await launchChromium({ headless: true });
const context = await browser.newContext({ viewport: sizes[0] });
context.on('page', target => {
target.on('pageerror', error => { errors.push({ type: 'pageerror', url: target.url(), message: error.message }); persist(); });
target.on('console', message => { if (message.type() === 'error') { errors.push({ type: 'console', url: target.url(), message: message.text(), location: message.location() }); persist(); } });
});
const page = await context.newPage();
page.setDefaultTimeout(20000);
async function settle() {
await page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 8; i++) await new Promise(resolve => requestAnimationFrame(resolve)); });
}
async function goto(name, size) {
await page.setViewportSize(size);
await page.goto(`${origin}/iframe.html?id=${stories[name]}&viewMode=story`);
await page.locator('.sb-page-title').waitFor();
await page.waitForFunction(() => {
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
return hosts.length === 1 && hosts.every(host => host.shadowRoot && !host.shadowRoot.querySelector('[aria-busy="true"]') && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet));
});
// Force offscreen lazy placeholders to load in verification only. The same
// browser-only attribute is applied to both baseline and final DOM hashes.
await page.evaluate(async () => {
const shadow = document.querySelector('.hm-orgchart-host,.hm-orgchart-component-host').shadowRoot;
await Promise.all([...shadow.querySelectorAll('img')].map(async image => { image.loading = 'eager'; await image.decode().catch(() => {}); }));
});
await page.waitForFunction(() => [...document.querySelector('.hm-orgchart-host,.hm-orgchart-component-host').shadowRoot.querySelectorAll('img')].every(image => image.complete && image.naturalWidth > 0));
await settle();
}
async function snapshot() {
return page.evaluate(async () => {
const host = document.querySelector('.hm-orgchart-host,.hm-orgchart-component-host');
const shadow = host?.shadowRoot;
const all = selector => [...(shadow?.querySelectorAll(selector) ?? [])];
const first = selector => shadow?.querySelector(selector);
const rect = element => {
if (!element) return null;
const r = element.getBoundingClientRect();
return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom };
};
const union = elements => {
const boxes = elements.map(rect).filter(box => box && box.width > 0 && box.height > 0);
if (!boxes.length) return null;
const x = Math.min(...boxes.map(box => box.x)), y = Math.min(...boxes.map(box => box.y));
const right = Math.max(...boxes.map(box => box.right)), bottom = Math.max(...boxes.map(box => box.bottom));
return { x, y, width: right - x, height: bottom - y, right, bottom };
};
const styleKeys = ['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'letterSpacing', 'color', 'backgroundColor', 'backgroundImage', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor', 'borderRadius', 'boxShadow', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'width', 'height', 'minWidth', 'minHeight', 'textAlign', 'whiteSpace', 'overflowWrap', 'verticalAlign', 'objectFit', 'objectPosition', 'gap', 'rowGap', 'columnGap'];
const style = element => element ? Object.fromEntries(styleKeys.map(key => [key, getComputedStyle(element)[key]])) : null;
const hash = async input => [...new Uint8Array(await crypto.subtle.digest('SHA-256', typeof input === 'string' ? new TextEncoder().encode(input) : input))].map(byte => byte.toString(16).padStart(2, '0')).join('');
const assetHashes = new Map();
const assets = async element => Promise.all([...element.querySelectorAll('img')].map(async image => {
const url = image.currentSrc || image.src;
if (!assetHashes.has(url)) assetHashes.set(url, fetch(url).then(response => { if (!response.ok) throw new Error(`Asset HTTP ${response.status}: ${url.slice(0, 120)}`); return response.arrayBuffer(); }).then(hash));
return { hash: await assetHashes.get(url), naturalWidth: image.naturalWidth, naturalHeight: image.naturalHeight, loaded: image.complete && image.naturalWidth > 0 };
}));
const visible = element => { const r = rect(element); return r?.width > 0 && r.height > 0 && getComputedStyle(element).display !== 'none'; };
const scroll = element => {
if (!element) return null;
const css = getComputedStyle(element);
return { scrollWidth: element.scrollWidth, clientWidth: element.clientWidth, scrollHeight: element.scrollHeight, clientHeight: element.clientHeight, left: element.scrollLeft, top: element.scrollTop, overflowX: css.overflowX, overflowY: css.overflowY };
};
const common = Object.fromEntries(['.sb-page', '.sb-page-inner', '.sb-page-title', '.sb-page-section-title', '.sb-page-section-header', '.sb-page-section-body'].map(selector => [selector, style(document.querySelector(selector))]));
// Page/section height is content-dependent; typography, borders and gutters are invariant.
for (const value of Object.values(common)) if (value) for (const key of ['height', 'minHeight']) delete value[key];
const documentScroll = { viewportWidth: innerWidth, viewportHeight: innerHeight, root: scroll(document.documentElement), body: scroll(document.body), scrollingElement: scroll(document.scrollingElement) };
const departmentRoot = first('.hm-orgchart-department-colors');
const departments = departmentRoot ? await Promise.all([...departmentRoot.querySelectorAll('.hm-orgchart-color-preview')].map(async row => {
const heading = row.querySelector('.hm-orgchart-color-department-name');
const preview = row.querySelector('.hm-orgchart-preview-grid');
const cards = [...row.querySelectorAll('.hm-orgchart-employee')];
const headingBox = rect(heading), cardBox = union(cards);
return {
name: heading?.textContent, box: rect(row), heading: headingBox, headingStyle: style(heading), preview: rect(preview), previewStyle: style(preview),
gap: headingBox && cardBox ? cardBox.y - headingBox.bottom : null,
visibleChildren: [...row.children].filter(visible).map(child => ({ tag: child.tagName, className: child.className })),
cards: await Promise.all(cards.map(async card => ({
kind: card.dataset.cardKind, width: card.offsetWidth, height: card.offsetHeight, box: rect(card), hash: await hash(card.outerHTML), assets: await assets(card),
subtree: [card, ...card.querySelectorAll('*')].map(child => ({ tag: child.tagName, className: typeof child.className === 'string' ? child.className : child.className?.baseVal, style: style(child) })),
}))),
};
})) : null;
const unwantedSelectors = '.hm-orgchart-color-legend,.hm-orgchart-contrast-note,.hm-orgchart-contrast-comparison,.hm-orgchart-card-caption,.hm-orgchart-representative-preview,.hm-orgchart-department-colors > p,.hm-orgchart-color-preview > p';
const unwanted = departmentRoot ? [...departmentRoot.querySelectorAll(unwantedSelectors)].filter(visible).map(element => ({ className: element.className, text: element.textContent.slice(0, 150) })) : [];
const wrapper = first('.hm-orgchart-composition'), stage = first('.hm-orgchart-stage');
let fixed = null;
if (stage) {
const stageBox = rect(stage), scale = stageBox.width / stage.offsetWidth;
const available = rect(document.querySelector('.hm-orgchart-story-full'));
const trees = all('.hm-orgchart-forest[data-layout="workdesk"] > .hm-orgchart-tree');
// Measure rendered visible six-column content, never the full-width tree viewport.
const bodyElements = trees.flatMap(tree => [...tree.querySelectorAll('.hm-orgchart-organization-name,.hm-orgchart-employee')]);
const body = union(bodyElements);
const normalized = box => box ? { x: (box.x - stageBox.x) / scale, y: (box.y - stageBox.y) / scale, width: box.width / scale, height: box.height / scale } : null;
const selectors = '.hm-orgchart-employee,.hm-orgchart-organization-name,.hm-orgchart-organization-totals,.hm-orgchart-organization-totals-title,.hm-orgchart-workdesk-summary-frame,.hm-orgchart-workdesk-position-table,.hm-orgchart-workdesk-position-table th,.hm-orgchart-workdesk-position-table td,.hm-orgchart-slot-side,.hm-orgchart-slot-belowTree,.hm-orgchart-status-group,.hm-orgchart-summary-title,.hm-orgchart-slot-footer,.hm-orgchart-affiliation-bar,.hm-orgchart-affiliation-bar-item,.hm-orgchart-axis,.hm-orgchart-top-leaders,.hm-orgchart-hub,.hm-orgchart-workdesk-overlay,.hm-orgchart-workdesk-decoration';
const elements = [...stage.querySelectorAll(selectors)];
const invariants = await Promise.all(elements.map(async (element, index) => ({
key: `${element.tagName}:${typeof element.className === 'string' ? element.className : element.className.baseVal}:${element.dataset.employeeId ?? element.dataset.organizationId ?? element.dataset.contentSlot ?? element.dataset.workdeskDecoration ?? ''}:${index}`,
box: normalized(rect(element)), style: style(element), htmlHash: await hash(element.outerHTML),
})));
const collisionElements = [...stage.querySelectorAll('.hm-orgchart-employee,.hm-orgchart-organization-name,.hm-orgchart-organization-totals,.hm-orgchart-affiliation-bar,.hm-orgchart-status-group > .hm-orgchart-summary-title')].filter(visible);
const collisions = [];
for (let i = 0; i < collisionElements.length; i++) for (let j = i + 1; j < collisionElements.length; j++) {
const a = collisionElements[i], b = collisionElements[j];
if (a.contains(b) || b.contains(a)) continue;
const ra = normalized(rect(a)), rb = normalized(rect(b));
const width = Math.min(ra.x + ra.width, rb.x + rb.width) - Math.max(ra.x, rb.x);
const height = Math.min(ra.y + ra.height, rb.y + rb.height) - Math.max(ra.y, rb.y);
if (width > 0.5 && height > 0.5) collisions.push({ pair: `${i}:${j}`, a: a.className, b: b.className, width, height });
}
const matrix = getComputedStyle(stage).transform.match(/matrix\(([^)]+)\)/)?.[1].split(',').map(Number);
fixed = {
scale, verticalScale: stageBox.height / parseFloat(getComputedStyle(stage).height), matrix, stage: stageBox, available, wrapper: rect(wrapper), host: rect(host), baseWidth: stage.offsetWidth, baseHeight: stage.offsetHeight, computedBaseWidth: parseFloat(getComputedStyle(stage).width), computedBaseHeight: parseFloat(getComputedStyle(stage).height),
treeCount: trees.length, body, bodyStage: normalized(body), bodyElementCount: bodyElements.length, bodyCenter: body?.x + body?.width / 2, availableCenter: available?.x + available?.width / 2,
centerDelta: body && available ? body.x + body.width / 2 - available.x - available.width / 2 : null,
htmlHash: await hash(stage.innerHTML), invariants, assets: await assets(stage), collisions,
scroll: ['.hm-orgchart-shell', '.hm-orgchart-content', '.hm-orgchart-composition', '.hm-orgchart-viewport'].map(selector => ({ selector, ...scroll(first(selector)) })),
allVisibleContained: bodyElements.every(element => { const b = rect(element); return b.x >= stageBox.x - 1 && b.y >= stageBox.y - 1 && b.right <= stageBox.right + 1 && b.bottom <= stageBox.bottom + 1; }),
};
}
return {
title: document.querySelector('.sb-page-title')?.textContent, common, documentScroll, departments, unwanted,
optionsPresent: [...document.querySelectorAll('button')].filter(visible).some(button => /긴 이름 검증/.test(button.textContent)),
sectionTitles: [...document.querySelectorAll('.sb-page-section-title')].map(element => element.textContent), fixed,
};
});
}
function documentFits(state, bothAxes) {
const d = state.documentScroll;
return [d.root, d.body, d.scrollingElement].filter(Boolean).every(value => value.scrollWidth <= d.viewportWidth + 1 && (!bothAxes || value.scrollHeight <= d.viewportHeight + 1));
}
function preserveDepartment(before, after) {
return before.departments?.length === after.departments?.length && after.departments.every((row, index) => {
const old = before.departments[index];
return row.name === old.name && equal(row.headingStyle, old.headingStyle) && row.cards.length === old.cards.length && row.cards.every((card, i) => equal({ kind: card.kind, width: card.width, height: card.height, hash: card.hash, assets: card.assets, subtree: card.subtree }, { kind: old.cards[i].kind, width: old.cards[i].width, height: old.cards[i].height, hash: old.cards[i].hash, assets: old.cards[i].assets, subtree: old.cards[i].subtree }));
});
}
function preserveFixed(before, after) {
return before.htmlHash === after.htmlHash && equal(before.assets, after.assets) && before.invariants.length === after.invariants.length && after.invariants.every((item, index) => {
const old = before.invariants[index];
return old.key === item.key && old.htmlHash === item.htmlHash && equal(old.style, item.style) && near(old.box, item.box);
});
}
let fatal;
try {
const baseline = baselineMode ? null : JSON.parse(await fs.readFile(path.join(output, 'visual-baseline.json'), 'utf8'));
if (resizeOnly) {
// Mount once; every following measurement reuses this exact document and
// proves ResizeObserver handles both viewport axes without a page reload.
await goto('fixed', sizes[1]);
snapshots.fixed = {};
for (const size of [sizes[0], sizes[2], { width: 1440, height: 640 }, sizes[1]]) {
await page.setViewportSize(size);
await page.waitForFunction(() => {
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
const available = document.querySelector('.hm-orgchart-story-full')?.getBoundingClientRect();
const elements = [...(shadow?.querySelectorAll('.hm-orgchart-forest[data-layout="workdesk"] > .hm-orgchart-tree .hm-orgchart-organization-name,.hm-orgchart-forest[data-layout="workdesk"] > .hm-orgchart-tree .hm-orgchart-employee') ?? [])];
if (!available || !elements.length) return false;
const boxes = elements.map(element => element.getBoundingClientRect());
const left = Math.min(...boxes.map(box => box.left)), right = Math.max(...boxes.map(box => box.right));
return Math.abs((left + right) / 2 - available.x - available.width / 2) <= 1;
});
await settle();
const state = await snapshot(), fixed = state.fixed, key = `${size.width}x${size.height}`;
const before = baseline.snapshots.fixed[size.width].fixed;
const stability = await page.evaluate(async () => {
const stage = document.querySelector('.hm-orgchart-host').shadowRoot.querySelector('.hm-orgchart-stage');
let styleMutations = 0;
const observer = new MutationObserver(records => { styleMutations += records.length; });
observer.observe(stage, { attributes: true, attributeFilter: ['style'] });
const signatures = [stage.getAttribute('style')];
for (let frame = 0; frame < 10; frame++) {
await new Promise(resolve => requestAnimationFrame(resolve));
signatures.push(stage.getAttribute('style'));
}
styleMutations += observer.takeRecords().length;
observer.disconnect();
return { frames: 10, styleMutations, stable: signatures.every(signature => signature === signatures[0]), signatures: [...new Set(signatures)] };
});
snapshots.fixed[key] = { ...state, stability };
check(fixed.treeCount === 6 && Math.abs(fixed.centerDelta) <= 1 && contains(fixed.available, fixed.stage) && contains(fixed.host, fixed.stage) && fixed.allVisibleContained && Math.abs(fixed.scale - fixed.verticalScale) <= 0.0001,
`Resize ${key}: six-group body stays centered and full stage fits with uniform scale`, { bodyCenter: fixed.bodyCenter, availableCenter: fixed.availableCenter, delta: fixed.centerDelta, scale: fixed.scale, verticalScale: fixed.verticalScale, stage: fixed.stage, available: fixed.available });
const offsets = await page.evaluate(() => ({ x: scrollX, y: scrollY }));
check(documentFits(state, true) && offsets.x === 0 && offsets.y === 0 && fixed.scroll.every(item => item.left === undefined || (item.left === 0 && item.top === 0)),
`Resize ${key}: no document overflow or document/internal scroll offsets`, { document: state.documentScroll, offsets, internal: fixed.scroll });
check(preserveFixed(before, fixed), `Resize ${key}: cards, connectors and statistics keep baseline stage-relative geometry and styles`, { beforeHTMLHash: before.htmlHash, afterHTMLHash: fixed.htmlHash, elementCount: fixed.invariants.length });
check(stability.stable && stability.styleMutations === 0, `Resize ${key}: stage style stops mutating over 10 animation frames`, stability);
await page.screenshot({ path: path.join(output, `after-resize-fixed-${key}.png`), fullPage: true });
}
} else {
for (const name of Object.keys(stories)) {
snapshots[name] = {};
for (const size of sizes) {
await goto(name, size);
const state = await snapshot();
snapshots[name][size.width] = state;
await page.screenshot({ path: path.join(output, `${baselineMode ? 'before' : 'after'}-${name}-${size.width}.png`), fullPage: true });
if (baselineMode) {
check(name === 'department' ? state.departments?.length === 6 : state.fixed?.treeCount === 6, `Baseline ${name} ${size.width}: six groups and actual browser geometry captured`);
continue;
}
const before = baseline.snapshots[name][size.width];
check(state.title === before.title && equal(state.common, before.common), `${name} ${size.width}: common StoryPage title, typography and gutters preserved`);
if (name === 'department') {
check(state.departments?.length === 6 && state.departments.every(row => row.name && row.cards.length === 2 && row.cards.some(card => card.kind === 'leader') && row.cards.some(card => card.kind === 'employee')), `Department ${size.width}: six titles, 1Depth large leaders and 2Depth small employee cards`, state.departments?.map(row => ({ name: row.name, cards: row.cards.map(card => ({ kind: card.kind, width: card.width, height: card.height })) })));
check(state.unwanted.length === 0 && !state.optionsPresent && !state.sectionTitles.includes('표시 옵션') && state.departments.every(row => row.visibleChildren.length === 2), `Department ${size.width}: descriptions, provenance, legends, contrast and captions removed`, { unwanted: state.unwanted, optionsPresent: state.optionsPresent, sections: state.sectionTitles, children: state.departments.map(row => row.visibleChildren) });
check(preserveDepartment(before, state), `Department ${size.width}: exact existing header colors/gradient, card DOM, assets, typography and computed styles preserved`);
check(state.departments.every(row => row.gap >= 1 && row.cards.every(card => contains(row.box, card.box)) && contains(row.box, row.heading)), `Department ${size.width}: header/card separation and complete card containment`, state.departments.map(row => ({ name: row.name, beforeGap: before.departments.find(old => old.name === row.name)?.gap, afterGap: row.gap })));
check(state.departments.every((row, i, rows) => rows.every((other, j) => i === j || Math.abs(row.box.y - other.box.y) > 1 || Math.abs(row.preview.y - other.preview.y) <= 1)), `Department ${size.width}: card starts align across each shared desktop row`);
check(documentFits(state, false), `Department ${size.width}: no document horizontal overflow`, state.documentScroll);
} else {
const fixed = state.fixed, old = before.fixed;
check(fixed.treeCount === 6 && Math.abs(fixed.centerDelta) <= 1, `Full Composition ${size.width}: actual six-group body centered in available Storybook region`, { before: { bodyCenter: old.bodyCenter, availableCenter: old.availableCenter, delta: old.centerDelta, bounds: old.body }, after: { bodyCenter: fixed.bodyCenter, availableCenter: fixed.availableCenter, delta: fixed.centerDelta, bounds: fixed.body } });
check(fixed.baseWidth === 1920 && fixed.baseHeight === 915 && Math.abs(fixed.computedBaseHeight - 914.5) <= 0.02 && fixed.scale > 0 && fixed.scale <= 1 && Math.abs(fixed.scale - fixed.verticalScale) <= 0.0001 && fixed.matrix && Math.abs(fixed.matrix[0] - fixed.matrix[3]) <= 0.0001 && contains(fixed.available, fixed.stage) && contains(fixed.host, fixed.stage) && fixed.allVisibleContained, `Full Composition ${size.width}: original 1920 x 914.5 canvas fully fits with uniform scale`, { scale: fixed.scale, verticalScale: fixed.verticalScale, matrix: fixed.matrix, stage: fixed.stage, available: fixed.available, host: fixed.host, naturalSize: [fixed.baseWidth, fixed.baseHeight], computedBaseHeight: fixed.computedBaseHeight });
check(preserveFixed(old, fixed), `Full Composition ${size.width}: all stage-relative cards, headers, axis, connectors, decorations, side/status/footer geometry and styles preserved`, { beforeHTMLHash: old.htmlHash, afterHTMLHash: fixed.htmlHash, elementCount: fixed.invariants.length, assetCount: fixed.assets.length });
const newCollisions = fixed.collisions.filter(collision => !old.collisions.some(previous => previous.pair === collision.pair));
check(newCollisions.length === 0, `Full Composition ${size.width}: no new card/header/statistics overlaps`, { baselineOverlapCount: old.collisions.length, afterOverlapCount: fixed.collisions.length, newCollisions });
// Transformed fixed-size child layout can retain an unscaled scrollWidth
// in the existing hidden fit shell. Full painted-stage containment is
// separately mandatory; only unchanged hidden-shell overflow is exempt.
const internalFits = fixed.scroll.every((item, index) => item.scrollWidth === undefined || (item.left === 0 && item.top === 0
&& (item.scrollWidth <= item.clientWidth + 1 || (item.overflowX === 'hidden' && old.scroll[index].overflowX === 'hidden'))
&& (item.scrollHeight <= item.clientHeight + 1 || (item.overflowY === 'hidden' && old.scroll[index].overflowY === 'hidden'))));
check(documentFits(state, true) && internalFits, `Full Composition ${size.width}: no document or internal horizontal/vertical scroll`, { document: state.documentScroll, internal: fixed.scroll });
const wheelBefore = await page.evaluate(() => ({ x: scrollX, y: scrollY }));
await page.mouse.move(fixed.available.x + fixed.available.width / 2, fixed.available.y + Math.min(40, fixed.available.height / 2));
await page.mouse.wheel(120, 120);
await settle();
const wheelAfter = await page.evaluate(() => ({ x: scrollX, y: scrollY }));
check(equal(wheelBefore, wheelAfter), `Full Composition ${size.width}: wheel does not scroll document`, { before: wheelBefore, after: wheelAfter });
}
}
}
if (!baselineMode) {
const response = await page.request.get(`${origin}/index.json`), index = await response.json();
const entries = Object.values(index.entries).filter(entry => entry.type === 'story' && entry.title.startsWith('OrgChart/') && !(entry.tags ?? []).includes('internal'));
check(entries.length === 4 && equal(entries.map(entry => entry.id).sort(), publicStories.sort()), 'Storybook: all four public OrgChart menu entries retained', entries.map(entry => ({ id: entry.id, title: entry.title, name: entry.name })));
}
}
} catch (error) {
fatal = { message: error.message, stack: error.stack };
check(false, 'Focused P9.1 runner completed', fatal);
} finally {
check(errors.length === 0, 'No browser console or page errors', errors);
const result = { startedAt, completedAt: new Date().toISOString(), origin, browser: browser.version(), baselineMode, resizeOnly, sizes, results, snapshots, errors, fatal, pass: results.filter(result => result.status === 'PASS').length, fail: results.filter(result => result.status === 'FAIL').length,
limitations: ['Only the two P9.1 pages and public Storybook index checked; Expanded interactions and historical P7-P9 suites were not repeated.', 'Browser rendering and computed style preservation do not replace user design approval.', 'Other browsers, real devices, screen readers and production deployment remain NOT VERIFIED.'] };
const resultName = resizeOnly ? 'browser-resize.json' : baselineMode ? 'visual-baseline.json' : 'browser.json';
await fs.writeFile(path.join(output, resultName), JSON.stringify(result, null, 2));
console.log(JSON.stringify({ pass: result.pass, fail: result.fail, errors: errors.length, output: `test-results/org-chart-p9-1/${resultName}`, centers: Object.entries(snapshots.fixed ?? {}).map(([viewport, state]) => ({ viewport, bodyCenter: state.fixed?.bodyCenter, availableCenter: state.fixed?.availableCenter, delta: state.fixed?.centerDelta })) }));
await context.close();
await browser.close();
if (result.fail) process.exitCode = 1;
}