Deploy Storybook / deploy (push) Successful in 59s
Remove the data, layout, interaction and employee-popup setting panels from the Expanded OrgChart Story. The chart now renders a fixed sample (7 companies x 20, TB, all expanded, collapse/zoom/pan on, Drag & Drop off, HM Dialog) right under the page title. Story ID, OrgChart packages and public API are unchanged. Align the Story browser checks (P7, P8, P9, P9.2-P9.5, geometry, popup parity, hierarchy, HM Story) with the preview: setting-only scenarios are removed, chart rendering, collapse, Zoom/Pan/Fit and HM Dialog checks run on the fixed state. Move/Drag & Drop coverage stays in the Vanilla Core checks (verify-p9-3-core, verify-p9-4-core). Verified: tsc, test:guard 94/94, node --check on 11 scripts, changed browser sections PASS. Pre-existing P7 story-menu, P8 Department long-name, P9 Expanded tb/runner failures remain separate issues. verify-p9-5-popup-parity was not run (needs the Vanilla example server).
354 lines
31 KiB
JavaScript
354 lines
31 KiB
JavaScript
// P8 Storybook presentation-only browser checks. P7 units/full suites stay unrun.
|
|
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-p8');
|
|
const origin = (process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6057').replace(/\/$/, '');
|
|
const sections = process.argv.slice(2);
|
|
const run = section => (!sections.length && !['full-repair', 'route-final'].includes(section)) || sections.includes(section);
|
|
const results = [], measurements = [], errors = [];
|
|
const startedAt = new Date().toISOString();
|
|
await fs.mkdir(output, { recursive: true });
|
|
const persist = () => writeFileSync(path.join(output, 'browser-progress.json'), JSON.stringify({ startedAt, origin, sections, results, measurements, errors }, null, 2));
|
|
function 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 stories = [
|
|
{ id: 'orgchart--logo-personnel-summary', name: 'Overview', file: 'overview' },
|
|
{ id: 'orgchart--department-colors', name: 'Department Colors', file: 'department-colors' },
|
|
{ id: 'orgchart--full-composition', name: 'Full Composition', file: 'full-composition' },
|
|
{ id: 'orgchart--expanded-org-chart', name: 'Expanded OrgChart', file: 'expanded-org-chart' },
|
|
];
|
|
const patterns = ['patterns-data-filter-bar--all', 'patterns-layout-page-header--all', 'patterns-layout-loading-screen--all'];
|
|
const internalIds = [
|
|
'orgchart--logo', 'orgchart--position-summary', 'orgchart--affiliation-summary',
|
|
...['business-management', 'business-planning', 'talent-growth', 'integrated-system', 'design-planning', 'solution-sales', 'top-leader', 'organization-leader', 'employee'].map(name => 'orgchart-components-cards--' + name),
|
|
...['position-summary', 'affiliation-summary', 'status-summary'].map(name => 'orgchart-components-summaries--' + name),
|
|
...['basic-organization', 'organization-hierarchy', 'employee-cards', 'organization-summary', 'json-data-update', 'loading-empty-error', 'responsive-container', 'multiple-instances'].map(name => 'orgchart-content--' + name),
|
|
];
|
|
const browser = await launchChromium({ headless: true });
|
|
const context = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
|
|
const page = await context.newPage();
|
|
context.on('page', p => {
|
|
p.on('pageerror', error => errors.push({ url: p.url(), type: 'pageerror', message: error.message }));
|
|
p.on('console', message => { if (message.type() === 'error') errors.push({ url: p.url(), type: 'console', message: message.text(), location: message.location() }); });
|
|
});
|
|
// The initial page predates the context event listener.
|
|
page.on('pageerror', error => errors.push({ url: page.url(), type: 'pageerror', message: error.message }));
|
|
page.on('console', message => { if (message.type() === 'error') errors.push({ url: page.url(), type: 'console', message: message.text(), location: message.location() }); });
|
|
page.setDefaultTimeout(20000);
|
|
async function settle(target = page) {
|
|
await target.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve)); });
|
|
}
|
|
async function ready(expected) {
|
|
await page.locator('.sb-page-title').waitFor();
|
|
await page.waitForFunction(expected => {
|
|
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
|
|
if (!hosts.length) return false;
|
|
return hosts.every(host => {
|
|
const shadow = host.shadowRoot;
|
|
return shadow && [...shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet)
|
|
&& !shadow.querySelector('[aria-busy="true"]')
|
|
&& (expected === undefined || shadow.querySelectorAll('.hm-orgchart-employee').length === expected);
|
|
});
|
|
}, expected);
|
|
await settle();
|
|
}
|
|
async function gotoStory(story, width = 1440) {
|
|
await page.setViewportSize({ width, height: 1100 });
|
|
await page.goto(`${origin}/iframe.html?id=${story.id}&viewMode=story`);
|
|
await ready(story.file === 'expanded-org-chart' ? 140 : undefined);
|
|
}
|
|
async function capture(file, fullPage = true) { await page.screenshot({ path: path.join(output, file + '.png'), fullPage }); }
|
|
async function section(name, work) {
|
|
if (!run(name)) return;
|
|
try { await work(); }
|
|
catch (error) { check(false, `${name}: runner completed`, { message: error.message, stack: error.stack }); }
|
|
}
|
|
async function commonStyle() {
|
|
return page.evaluate(() => {
|
|
const properties = {
|
|
'.sb-page': ['fontFamily', 'color', 'paddingTop', 'paddingBottom', 'boxSizing', 'width'],
|
|
'.sb-page-inner': ['width', 'boxSizing'],
|
|
'.sb-page-header': ['paddingBottom', 'borderBottomWidth', 'borderBottomColor', 'fontSize', 'lineHeight'],
|
|
'.sb-page-kicker': ['fontSize', 'fontWeight', 'color', 'marginBottom'],
|
|
'.sb-page-title': ['fontSize', 'fontWeight', 'lineHeight', 'marginTop', 'marginBottom'],
|
|
'.sb-page-description': ['fontSize', 'fontWeight', 'lineHeight', 'marginBottom'],
|
|
'.sb-page-section': ['marginTop', 'scrollMarginTop'],
|
|
'.sb-page-section-header': ['paddingBottom', 'marginBottom', 'borderBottomWidth', 'borderBottomColor'],
|
|
'.sb-page-section-title': ['fontSize', 'lineHeight', 'fontWeight', 'marginTop', 'marginBottom'],
|
|
'.sb-page-section-description': ['fontSize', 'lineHeight', 'fontWeight', 'marginTop', 'color'],
|
|
};
|
|
return Object.fromEntries(Object.entries(properties).map(([selector, keys]) => {
|
|
const element = document.querySelector(selector);
|
|
if (!element) return [selector, null];
|
|
const css = getComputedStyle(element);
|
|
const style = Object.fromEntries(keys.map(key => [key, css[key]]));
|
|
if (selector === '.sb-page-inner') {
|
|
// Chrome can expose auto margins as 0px after the Core's Typed OM
|
|
// ancestor inspection. Actual symmetric gutters are the user contract.
|
|
const outer = element.parentElement.getBoundingClientRect(), inner = element.getBoundingClientRect();
|
|
style.leftGutter = inner.left - outer.left;
|
|
style.rightGutter = outer.right - inner.right;
|
|
}
|
|
return [selector, style];
|
|
}));
|
|
});
|
|
}
|
|
async function pageGeometry() {
|
|
return page.evaluate(() => {
|
|
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 documentGeometry = { scrollWidth: document.documentElement.scrollWidth, clientWidth: document.documentElement.clientWidth, innerWidth, scrollHeight: document.documentElement.scrollHeight, clientHeight: document.documentElement.clientHeight };
|
|
const frame = { page: rect(document.querySelector('.sb-page')), inner: rect(document.querySelector('.sb-page-inner')), title: document.querySelector('.sb-page-title')?.textContent };
|
|
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
|
const wrapper = shadow?.querySelector('.hm-orgchart-composition');
|
|
const stage = shadow?.querySelector('.hm-orgchart-stage');
|
|
const fixed = wrapper && stage ? {
|
|
wrapper: rect(wrapper), stage: rect(stage), baseWidth: stage.offsetWidth, baseHeight: stage.offsetHeight,
|
|
scale: Number(wrapper.dataset.scale), matrix: getComputedStyle(stage).transform,
|
|
scroll: ['.hm-orgchart-shell', '.hm-orgchart-content', '.hm-orgchart-composition', '.hm-orgchart-viewport'].map(selector => {
|
|
const element = shadow.querySelector(selector);
|
|
if (!element) return { selector, missing: true };
|
|
const css = getComputedStyle(element);
|
|
return { selector, clientWidth: element.clientWidth, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, scrollHeight: element.scrollHeight, scrollLeft: element.scrollLeft, scrollTop: element.scrollTop, overflowX: css.overflowX, overflowY: css.overflowY };
|
|
}),
|
|
visibleBounds: [...stage.querySelectorAll('.hm-orgchart-employee,.hm-orgchart-organization,.hm-orgchart-summary,.hm-orgchart-logo,.hm-orgchart-workdesk-overlay')].map(element => ({ className: element.className, box: rect(element) })),
|
|
} : null;
|
|
return { document: documentGeometry, frame, fixed };
|
|
});
|
|
}
|
|
async function autoState() {
|
|
return page.evaluate(() => {
|
|
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
|
const viewport = shadow.querySelector('.hm-orgchart-viewport'), forest = shadow.querySelector('.hm-orgchart-forest');
|
|
const rect = element => { const r = element.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height, right: r.right, bottom: r.bottom }; };
|
|
return { type: shadow.querySelector('.hm-orgchart-expanded')?.dataset.autoLayout, employees: shadow.querySelectorAll('.hm-orgchart-employee').length,
|
|
organizations: shadow.querySelectorAll('.node[data-depth]').length, scale: Number(viewport.dataset.scale), x: Number(viewport.dataset.panX), y: Number(viewport.dataset.panY),
|
|
viewport: rect(viewport), forest: rect(forest), zoomEnabled: viewport.dataset.zoomEnabled, panEnabled: viewport.dataset.panEnabled,
|
|
scroll: { left: viewport.scrollLeft, top: viewport.scrollTop, width: viewport.scrollWidth, clientWidth: viewport.clientWidth, height: viewport.scrollHeight, clientHeight: viewport.clientHeight } };
|
|
});
|
|
}
|
|
const autoFits = state => state.forest.x >= state.viewport.x - 1 && state.forest.y >= state.viewport.y - 1 && state.forest.right <= state.viewport.right + 1 && state.forest.bottom <= state.viewport.bottom + 1;
|
|
const fixedHasNoInternalScroll = fixed => fixed.scroll.filter(item => !item.missing).every(item => item.scrollLeft === 0 && item.scrollTop === 0 && (
|
|
item.selector === '.hm-orgchart-composition'
|
|
? item.overflowX === 'hidden' && item.overflowY === 'hidden'
|
|
: item.scrollWidth <= item.clientWidth + 1 && item.scrollHeight <= item.clientHeight + 1
|
|
));
|
|
try {
|
|
await section('manager', async () => {
|
|
const response = await page.request.get(origin + '/index.json');
|
|
check(response.ok(), 'Storybook index responds', { status: response.status() });
|
|
const index = await response.json();
|
|
await fs.writeFile(path.join(output, 'story-index.json'), JSON.stringify(index, null, 2));
|
|
const entries = Object.values(index.entries);
|
|
const publicEntries = entries.filter(entry => (entry.title === 'OrgChart' || entry.title.startsWith('OrgChart/')) && entry.type === 'story' && !entry.tags?.some(tag => ['internal', 'verification'].includes(tag)));
|
|
check(publicEntries.length === 4 && stories.every(story => publicEntries.some(entry => entry.id === story.id && entry.name === story.name)), 'Exactly four public names and original Story IDs', publicEntries.map(({ id, name, title, tags }) => ({ id, name, title, tags })));
|
|
check(internalIds.every(id => index.entries[id]?.tags?.includes('internal')), 'Existing internal Story IDs retained', { expected: internalIds, missingOrPublic: internalIds.filter(id => !index.entries[id]?.tags?.includes('internal')) });
|
|
check(new Set(entries.map(entry => entry.id)).size === entries.length, 'Story index contains no duplicate IDs');
|
|
const manager = await context.newPage();
|
|
await manager.goto(`${origin}/?path=/story/${stories[0].id}`);
|
|
await manager.locator('#storybook-explorer-tree').waitFor();
|
|
await manager.locator(`#storybook-explorer-tree a[id="${stories[0].id}"]`).waitFor();
|
|
await manager.waitForFunction(() => [...document.querySelectorAll('#storybook-explorer-tree [data-item-id]')].some(row => row.dataset.itemId === 'orgchart' && row.dataset.hmDepth === '0'));
|
|
await manager.waitForFunction(() => {
|
|
const preview = document.querySelector('#storybook-preview-iframe')?.contentDocument;
|
|
const hosts = [...preview?.querySelectorAll('.hm-orgchart-component-host') ?? []];
|
|
return preview?.querySelector('.sb-page-title')?.textContent === 'Overview' && hosts.length > 0
|
|
&& hosts.every(host => host.shadowRoot && [...host.shadowRoot.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet));
|
|
});
|
|
await manager.frameLocator('#storybook-preview-iframe').locator('.sb-page-title').evaluate(async element => {
|
|
const document = element.ownerDocument, view = document.defaultView;
|
|
await document.fonts.ready;
|
|
for (let i = 0; i < 5; i++) await new Promise(resolve => view.requestAnimationFrame(resolve));
|
|
});
|
|
await settle(manager);
|
|
const sidebar = await manager.evaluate(() => [...document.querySelectorAll('#storybook-explorer-tree [data-item-id]')].map(row => {
|
|
const primary = row.querySelector(':scope > a[id],:scope > button[id]');
|
|
const label = primary?.querySelector('.hm-manager-label')?.textContent ?? primary?.textContent ?? row.querySelector(':scope > button')?.textContent ?? row.textContent;
|
|
return { id: row.dataset.itemId, text: label?.trim(), depth: Number(row.dataset.hmDepth), section: row.dataset.hmSection, type: row.dataset.nodetype, href: primary?.getAttribute('href'), visible: row.getBoundingClientRect().height > 0 };
|
|
}));
|
|
await fs.writeFile(path.join(output, 'manager-sidebar.json'), JSON.stringify(sidebar, null, 2));
|
|
const roots = sidebar.filter(row => row.depth === 0 && row.visible);
|
|
const patternsRoot = roots.findIndex(row => row.id === 'patterns'), orgRoot = roots.findIndex(row => row.id === 'orgchart'), centerRoot = roots.findIndex(row => row.id === 'center-graphic');
|
|
check(patternsRoot >= 0 && orgRoot === patternsRoot + 1 && centerRoot === orgRoot + 1, 'OrgChart is between Patterns and Center Graphic at manager depth 0', roots);
|
|
const publicRows = sidebar.filter(row => stories.some(story => story.id === row.id));
|
|
check(JSON.stringify(publicRows.map(row => row.text)) === JSON.stringify(stories.map(story => story.name)) && publicRows.every(row => row.depth === 1), 'Four manager pages follow requested order at one child depth', publicRows);
|
|
check(sidebar.filter(row => row.id.startsWith('orgchart--')).length === 4 && internalIds.every(id => !sidebar.some(row => row.id === id)), 'Internal pages remain default hidden with no duplicate public row');
|
|
const sidebarHeight = await manager.locator('#storybook-sidebar-region').evaluate(element => Math.max(element.scrollHeight, ...[...element.querySelectorAll('[data-radix-scroll-area-content]')].map(item => item.scrollHeight)));
|
|
await manager.setViewportSize({ width: 1440, height: Math.max(1100, Math.min(2400, sidebarHeight + 100)) });
|
|
await settle(manager);
|
|
await manager.screenshot({ path: path.join(output, 'manager-full-sidebar.png'), fullPage: true });
|
|
await manager.locator('#storybook-sidebar-region').screenshot({ path: path.join(output, 'manager-sidebar.png') });
|
|
await manager.close();
|
|
});
|
|
await section('route-final', async () => {
|
|
const expected = {
|
|
overview: { hosts: 3, selectors: ['[data-component="Logo"]', '[data-component="PositionSummary"]', '[data-component="AffiliationSummary"]'] },
|
|
'department-colors': { hosts: 1, selectors: ['[data-component="DepartmentColors"]', '.hm-orgchart-employee'] },
|
|
'full-composition': { hosts: 1, selectors: ['.hm-orgchart-composition', '.hm-orgchart-stage', '.hm-orgchart-employee'] },
|
|
'expanded-org-chart': { hosts: 1, selectors: ['.hm-orgchart-expanded', '.hm-orgchart-viewport', '.hm-orgchart-employee'] },
|
|
};
|
|
for (const story of stories) {
|
|
await gotoStory(story);
|
|
const rendered = await page.evaluate(({ selectors }) => {
|
|
const hosts = [...document.querySelectorAll('.hm-orgchart-host,.hm-orgchart-component-host')];
|
|
return { title: document.querySelector('.sb-page-title')?.textContent, hosts: hosts.length,
|
|
specimens: Object.fromEntries(selectors.map(selector => [selector, hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll(selector).length, 0)])),
|
|
employees: hosts.reduce((total, host) => total + host.shadowRoot.querySelectorAll('.hm-orgchart-employee').length, 0),
|
|
autoLayout: hosts.find(host => host.classList.contains('hm-orgchart-host'))?.shadowRoot.querySelector('.hm-orgchart-expanded')?.dataset.autoLayout };
|
|
}, expected[story.file]);
|
|
check(rendered.title === story.name && rendered.hosts === expected[story.file].hosts && Object.values(rendered.specimens).every(count => count > 0)
|
|
&& (story.file !== 'expanded-org-chart' || (rendered.employees === 20 && rendered.autoLayout === 'tb')), `${story.name}: final CSF old URL loads expected Core specimens`, { url: page.url(), ...rendered });
|
|
measurements.push({ name: story.name + ' final retained route', rendered });
|
|
await capture(story.file + '-1440');
|
|
}
|
|
});
|
|
await section('styles', async () => {
|
|
const references = [];
|
|
for (const id of patterns) {
|
|
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
await page.goto(`${origin}/iframe.html?id=${id}&viewMode=story`);
|
|
await page.locator('.sb-page-title').waitFor(); await settle();
|
|
references.push({ id, style: await commonStyle() });
|
|
}
|
|
const reference = references[0].style;
|
|
check(references.every(item => JSON.stringify(item.style) === JSON.stringify(reference)), 'Three existing Pattern pages share the measured StoryPage frame', references);
|
|
for (const story of stories) {
|
|
await gotoStory(story);
|
|
const style = await commonStyle();
|
|
measurements.push({ name: story.name + ' common StoryPage computed styles', style });
|
|
check(JSON.stringify(style) === JSON.stringify(reference), story.name + ': StoryPage title, spacing, section and inner styles match Pattern pages', { reference: patterns[0], difference: Object.keys(reference).filter(key => JSON.stringify(reference[key]) !== JSON.stringify(style[key])), style });
|
|
await capture(story.file + '-1440');
|
|
if (['full-composition', 'expanded-org-chart'].includes(story.file)) {
|
|
const shielding = await page.evaluate(() => {
|
|
const pageFrame = document.querySelector('.sb-page');
|
|
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
|
const keys = ['fontFamily', 'fontSize', 'lineHeight', 'color', 'backgroundColor', 'backgroundImage', 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor', 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderRadius', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', 'width', 'height'];
|
|
const snapshot = () => ['.hm-orgchart-employee', '.hm-orgchart-organization', '.hm-orgchart-organization-name', '.hm-orgchart-person-name', '.hm-orgchart-summary'].flatMap(selector => [...shadow.querySelectorAll(selector)].map(element => ({ selector, id: element.dataset.employeeId ?? element.dataset.organizationId, style: Object.fromEntries(keys.map(key => [key, getComputedStyle(element)[key]])) })));
|
|
const withFrame = snapshot(), previous = pageFrame.className;
|
|
try { pageFrame.className = ''; return { withFrame, withoutFrame: snapshot() }; }
|
|
finally { pageFrame.className = previous; }
|
|
});
|
|
check(JSON.stringify(shielding.withFrame) === JSON.stringify(shielding.withoutFrame), story.name + ': StoryPage frame does not override Core shadow typography, colors, borders or dimensions', { elements: shielding.withFrame.length, changed: shielding.withFrame.filter((value, index) => JSON.stringify(value) !== JSON.stringify(shielding.withoutFrame[index])) });
|
|
await settle();
|
|
}
|
|
}
|
|
});
|
|
await section('widths', async () => {
|
|
for (const story of stories) for (const width of [390, 768, 1280, 1440]) {
|
|
await gotoStory(story, width);
|
|
const geometry = await pageGeometry();
|
|
measurements.push({ name: `${story.name} ${width}px`, geometry });
|
|
check(geometry.document.scrollWidth <= geometry.document.clientWidth + 1 && geometry.document.scrollWidth <= width + 1, `${story.name} ${width}px: no document horizontal overflow`, geometry.document);
|
|
check(geometry.frame.title === story.name, `${story.name} ${width}px: direct URL renders requested page`, { url: page.url(), title: geometry.frame.title });
|
|
if (geometry.fixed) {
|
|
const fixed = geometry.fixed;
|
|
const matrix = fixed.matrix === 'none' ? [1, 0, 0, 1] : fixed.matrix.slice(7, -1).split(',').map(Number);
|
|
const uniform = Math.abs(matrix[0] - matrix[3]) < 0.0001 && Math.abs(matrix[1]) < 0.0001 && Math.abs(matrix[2]) < 0.0001;
|
|
const bounds = fixed.stage.x >= fixed.wrapper.x - 1 && fixed.stage.y >= fixed.wrapper.y - 1 && fixed.stage.right <= fixed.wrapper.right + 1 && fixed.stage.bottom <= fixed.wrapper.bottom + 1;
|
|
const noScroll = fixedHasNoInternalScroll(fixed);
|
|
check(fixed.scale > 0 && fixed.scale <= 1 && uniform && Math.abs(fixed.stage.width / fixed.baseWidth - matrix[0]) < 0.001 && Math.abs(fixed.stage.height / fixed.baseHeight - matrix[3]) < 0.001 && bounds, `Full Composition ${width}px: uniform scale and entire stage fit content wrapper`, fixed);
|
|
check(noScroll, `Full Composition ${width}px: no internal horizontal or vertical scroll`, { note: 'The overflow-hidden composition retains unscaled transformed extents in CSSOM; complete visible stage bounds are checked separately.', scroll: fixed.scroll });
|
|
check(fixed.visibleBounds.every(({ box }) => box.x >= fixed.stage.x - 1 && box.y >= fixed.stage.y - 1 && box.right <= fixed.stage.right + 1 && box.bottom <= fixed.stage.bottom + 1), `Full Composition ${width}px: cards and summaries remain inside complete stage`, fixed.visibleBounds);
|
|
}
|
|
if (width === 390) await capture(story.file + '-390');
|
|
}
|
|
});
|
|
await section('full-repair', async () => {
|
|
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
await page.goto(`${origin}/iframe.html?id=${patterns[0]}&viewMode=story`);
|
|
await page.locator('.sb-page-title').waitFor(); await settle();
|
|
const reference = await commonStyle();
|
|
await gotoStory(stories[2]);
|
|
const style = await commonStyle();
|
|
const inspection = await page.evaluate(() => ['.sb-page', '.sb-page-inner'].map(selector => {
|
|
const element = document.querySelector(selector), css = getComputedStyle(element), rect = element.getBoundingClientRect();
|
|
return { selector, display: css.display, marginLeft: css.marginLeft, marginRight: css.marginRight, marginInline: css.marginInline, rect: { x: rect.x, y: rect.y, width: rect.width, right: rect.right }, inline: element.getAttribute('style') };
|
|
}));
|
|
check(JSON.stringify(style) === JSON.stringify(reference), 'Full Composition repair: common styles and actual centered gutters match existing Pattern', { style, inspection, reference });
|
|
await capture('full-composition-1440');
|
|
for (const width of [390, 768, 1280, 1440]) {
|
|
await gotoStory(stories[2], width);
|
|
const before = await pageGeometry();
|
|
const wrapper = await page.locator('.hm-orgchart-composition').boundingBox();
|
|
await page.mouse.move(wrapper.x + wrapper.width / 2, wrapper.y + Math.min(30, wrapper.height / 2));
|
|
await page.mouse.wheel(80, 80); await settle();
|
|
const after = await pageGeometry();
|
|
const fixed = after.fixed;
|
|
const bounds = fixed.stage.x >= fixed.wrapper.x - 1 && fixed.stage.y >= fixed.wrapper.y - 1 && fixed.stage.right <= fixed.wrapper.right + 1 && fixed.stage.bottom <= fixed.wrapper.bottom + 1;
|
|
check(fixedHasNoInternalScroll(fixed) && bounds && fixed.visibleBounds.every(({ box }) => box.x >= fixed.stage.x - 1 && box.y >= fixed.stage.y - 1 && box.right <= fixed.stage.right + 1 && box.bottom <= fixed.stage.bottom + 1), `Full Composition repair ${width}px: no wheel-scroll area or clipped transformed contents`, { before: before.fixed.scroll, after: fixed.scroll, wrapper: fixed.wrapper, stage: fixed.stage, document: after.document });
|
|
measurements.push({ name: `Full Composition repair ${width}px`, geometry: after });
|
|
}
|
|
});
|
|
await section('smoke', async () => {
|
|
await gotoStory(stories[3]);
|
|
// Expanded is preview-only (2026-10-22): settings, data apply, layout/Depth switches and the
|
|
// independent Zoom/Pan checkboxes were removed; the fixed 7x20 TB preview keeps chart browsing.
|
|
check(await page.getByRole('combobox').count() === 0 && await page.getByRole('checkbox').count() === 0 && await page.getByRole('switch').count() === 0, 'Expanded: preview-only page has no setting controls');
|
|
const preview = await autoState();
|
|
check(preview.type === 'tb' && preview.employees === 140 && preview.zoomEnabled === 'true' && preview.panEnabled === 'true', 'Expanded: fixed TB 140 employee preview with Zoom/Pan enabled', preview);
|
|
const beforeCollapse = await autoState();
|
|
const toggle = page.locator('.hm-orgchart-expanded-toggle').first();
|
|
const organizationId = await toggle.getAttribute('data-organization-id');
|
|
await toggle.focus(); await toggle.press('Enter'); await ready();
|
|
const collapsed = await autoState();
|
|
check(await toggle.getAttribute('aria-expanded') === 'false' && collapsed.organizations < beforeCollapse.organizations && collapsed.employees < beforeCollapse.employees, 'Expanded: Enter collapses a group and its descendants', { organizationId, before: beforeCollapse, after: collapsed });
|
|
await page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${organizationId}"]`).press('Space'); await ready(140);
|
|
check(await page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${organizationId}"]`).getAttribute('aria-expanded') === 'true', 'Expanded: Space expands the same group');
|
|
await page.getByRole('button', { name: '100% 복원', exact: true }).click();
|
|
const beforeZoom = await autoState();
|
|
await page.getByRole('button', { name: '확대', exact: true }).click(); await settle();
|
|
check((await autoState()).scale > beforeZoom.scale, 'Expanded: Zoom changes actual forest scale');
|
|
await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle();
|
|
const beforePan = await autoState();
|
|
await page.locator('.hm-orgchart-viewport').scrollIntoViewIfNeeded();
|
|
const viewport = await page.locator('.hm-orgchart-viewport').boundingBox();
|
|
await page.mouse.move(viewport.x + 10, viewport.y + 10); await page.mouse.down(); await page.mouse.move(viewport.x + 50, viewport.y + 35); await page.mouse.up(); await settle();
|
|
const afterPan = await autoState();
|
|
check(afterPan.x !== beforePan.x && afterPan.y !== beforePan.y, 'Expanded: pointer Pan changes both forest coordinates', { before: beforePan, after: afterPan });
|
|
await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle();
|
|
check(autoFits(await autoState()), 'Expanded: toolbar Fit contains complete forest');
|
|
const employee = page.locator('.hm-orgchart-employee').first(), employeeId = await employee.getAttribute('data-employee-id');
|
|
await employee.focus(); await employee.press('Enter');
|
|
const dialog = page.locator('[data-component="HMOrgChartEmployeeDialog"]');
|
|
await dialog.waitFor({ state: 'visible' }); await settle();
|
|
const popup = await dialog.evaluate(element => ({ role: element.getAttribute('role'), employeeId: element.dataset.employeeId, portal: element.parentElement === document.body, focusInside: element.contains(document.activeElement), text: element.textContent, box: (() => { const r = element.getBoundingClientRect(); return { x: r.x, right: r.right, width: r.width }; })() }));
|
|
check(popup.role === 'dialog' && popup.employeeId === employeeId && popup.portal && popup.focusInside && await page.getByRole('dialog').count() === 1 && await page.locator('dialog[open]').count() === 0, 'Expanded: actual single HM Dialog with exact employee and Portal focus', popup);
|
|
await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab');
|
|
check(await dialog.evaluate(element => element.contains(document.activeElement)), 'Expanded: HM Dialog traps Tab focus');
|
|
await capture('expanded-hm-dialog', false);
|
|
await page.keyboard.press('Escape'); await dialog.waitFor({ state: 'hidden' }); await settle();
|
|
const focusedEmployee = () => page.evaluate(() => document.querySelector('.hm-orgchart-host').shadowRoot.activeElement?.dataset.employeeId);
|
|
check(await focusedEmployee() === employeeId, 'Expanded: ESC closes HM Dialog and returns employee focus');
|
|
await employee.press('Space'); await dialog.waitFor({ state: 'visible' });
|
|
await dialog.getByRole('button', { name: '닫기', exact: true }).click(); await dialog.waitFor({ state: 'hidden' }); await settle();
|
|
check(await focusedEmployee() === employeeId, 'Expanded: HM close button returns employee focus');
|
|
await page.setViewportSize({ width: 390, height: 844 }); await settle();
|
|
await employee.focus(); await employee.press('Enter'); await dialog.waitFor({ state: 'visible' }); await settle();
|
|
const narrow = await dialog.evaluate(element => { const r = element.getBoundingClientRect(); return { x: r.x, right: r.right, width: r.width, scrollWidth: element.scrollWidth, clientWidth: element.clientWidth, documentScrollWidth: document.documentElement.scrollWidth, innerWidth }; });
|
|
check(narrow.x >= -1 && narrow.right <= 391 && narrow.scrollWidth <= narrow.clientWidth + 1 && narrow.documentScrollWidth <= 390, 'Expanded: actual HM Dialog fits 390px', narrow);
|
|
await capture('expanded-hm-dialog-390', false);
|
|
await page.keyboard.press('Escape'); await dialog.waitFor({ state: 'hidden' });
|
|
// Department Colors remains a live Core specimen with its original option.
|
|
await gotoStory(stories[1]);
|
|
const longNameButton = page.getByRole('button', { name: '긴 이름 검증 시작', exact: true });
|
|
await longNameButton.click(); await ready();
|
|
check(await page.getByRole('button', { name: '긴 이름 검증 종료', exact: true }).getAttribute('aria-pressed') === 'true' && await page.locator('.hm-orgchart-person-name').filter({ hasText: '아주긴가상이름표시검증사례' }).count() > 0, 'Department Colors: original long-name specimen option remains functional');
|
|
});
|
|
} finally {
|
|
check(errors.length === 0, 'No new browser page or console errors', errors);
|
|
const result = { startedAt, completedAt: new Date().toISOString(), origin, sections, browser: browser.version(), results, measurements, errors, pass: results.filter(item => item.status === 'PASS').length, fail: results.filter(item => item.status === 'FAIL').length,
|
|
limitations: ['Historical P7 units/full suites were not repeated.', 'Common computed styles prove runtime parity; design intent approval remains separate.', 'Document vertical scrolling caused by StoryPage text is reported separately from internal chart scrolling.', 'The smoke checks are a narrow regression sample, not full option-combination or accessibility certification.'] };
|
|
const suffix = sections.length ? '-' + sections.join('-') : '';
|
|
await fs.writeFile(path.join(output, `browser${suffix}.json`), JSON.stringify(result, null, 2));
|
|
console.log(JSON.stringify({ pass: result.pass, fail: result.fail, errors: errors.length, output: `test-results/org-chart-p8/browser${suffix}.json` }));
|
|
await context.close(); await browser.close();
|
|
if (result.fail) process.exitCode = 1;
|
|
}
|