Drop page / section descriptions, state notes and repeated captions from the representative Stories of 44 components, the Patterns, Center Graphic and OrgChart pages. Value readouts, spec lists and captions hide only on the representative page; hidden verification Stories keep them. Tabs Default API and the Filter Bar draft / applied counts move to the hidden DefaultApi and DraftApplied Stories; Center Overview is hidden with its direct URL kept. Story IDs are unchanged. Align the component, pattern and OrgChart docs with the pages, point the quality reference check at the DraftApplied Story, and make the P8 styles check expect no section header on the preview-only Expanded page.
359 lines
31 KiB
JavaScript
359 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);
|
|
// Expanded OrgChart is preview-only (no StorySection since f4ddd5a): its section elements must be absent (null),
|
|
// every other selector still has to match the Pattern pages.
|
|
const sectionless = ['.sb-page-section', '.sb-page-section-header', '.sb-page-section-title'];
|
|
for (const story of stories) {
|
|
await gotoStory(story);
|
|
const style = await commonStyle();
|
|
const previewOnly = story.file === 'expanded-org-chart';
|
|
const expected = previewOnly ? { ...reference, ...Object.fromEntries(sectionless.map(selector => [selector, null])) } : reference;
|
|
measurements.push({ name: story.name + ' common StoryPage computed styles', style });
|
|
check(JSON.stringify(style) === JSON.stringify(expected), story.name + (previewOnly ? ': StoryPage title and inner styles match Pattern pages; no section header' : ': StoryPage title, spacing, section and inner styles match Pattern pages'), { reference: patterns[0], difference: Object.keys(expected).filter(key => JSON.stringify(expected[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;
|
|
}
|