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).
315 lines
32 KiB
JavaScript
315 lines
32 KiB
JavaScript
// Focused P9.4 Storybook deltas. Run named sections to repair only failed probes.
|
|
// Existing P7-P9.3 evidence is preserved and is not replayed by this harness.
|
|
// Expanded is preview-only (2026-10-22): company/data/layout settings and the Drag & Drop switch were
|
|
// removed, so data/dropdown/on/cancel/modes sections were retired (Core ON/cancel stays in verify-p9-4-core.mjs).
|
|
import fs from 'node:fs/promises';
|
|
import { writeFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { createHash } from 'node:crypto';
|
|
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-4');
|
|
const origin = (process.env.ORGCHART_STORY_ORIGIN ?? 'http://127.0.0.1:6064').replace(/\/$/, '');
|
|
const only = process.argv.slice(2), suffix = only.length ? `-${only.join('-')}` : '';
|
|
const results = [], measurements = [], errors = [], startedAt = new Date().toISOString();
|
|
await fs.mkdir(output, { recursive: true });
|
|
const persist = () => writeFileSync(path.join(output, `browser-progress${suffix}.json`), JSON.stringify({ startedAt, results, measurements, 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 browser = await launchChromium({ headless: true });
|
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1700 } });
|
|
page.setDefaultTimeout(20000);
|
|
page.on('pageerror', error => { errors.push({ type: 'pageerror', message: error.message }); persist(); });
|
|
page.on('console', message => { if (message.type() === 'error') { errors.push({ type: 'console', message: message.text(), location: message.location() }); persist(); } });
|
|
const viewport = () => page.locator('.hm-orgchart-viewport');
|
|
const employee = id => page.locator(`.hm-orgchart-employee[data-employee-id="${id}"]`);
|
|
const title = id => page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${id}"]`);
|
|
const dialog = () => page.locator('[data-component="HMOrgChartEmployeeDialog"]');
|
|
const settle = () => page.evaluate(async () => { await document.fonts.ready; for (let i = 0; i < 8; i++) await new Promise(requestAnimationFrame); });
|
|
async function ready(count) {
|
|
await page.waitForFunction(count => {
|
|
const shadow = document.querySelector('.hm-orgchart-host,.hm-orgchart-component-host')?.shadowRoot;
|
|
return shadow && !shadow.querySelector('[aria-busy="true"]') && [...shadow.querySelectorAll('link[rel="stylesheet"]')].every(link => link.sheet)
|
|
&& (count === undefined || shadow.querySelectorAll('.hm-orgchart-employee').length === count);
|
|
}, count, { timeout: 45000 }); await settle();
|
|
}
|
|
async function setup() {
|
|
await page.setViewportSize({ width: 1920, height: 1700 });
|
|
await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`);
|
|
await ready(140); await viewport().scrollIntoViewIfNeeded();
|
|
await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle();
|
|
}
|
|
async function state() {
|
|
return page.evaluate(() => {
|
|
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot;
|
|
const viewport = shadow.querySelector('.hm-orgchart-viewport'), expanded = shadow.querySelector('.hm-orgchart-expanded');
|
|
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 }; };
|
|
const nodes = [...shadow.querySelectorAll('.node[data-organization-id]')].map(element => ({
|
|
id: element.dataset.organizationId, parentId: element.dataset.parentId, companyId: element.dataset.companyId,
|
|
direct: Number(element.dataset.directEmployeeCount), inclusive: Number(element.dataset.inclusiveEmployeeCount), collapsed: element.dataset.collapsed,
|
|
}));
|
|
const cards = [...shadow.querySelectorAll('.hm-orgchart-employee')].map(element => ({
|
|
id: element.dataset.employeeId, organizationId: element.closest('.node').dataset.organizationId, companyId: element.dataset.companyId,
|
|
direction: element.dataset.connectionDirection, kind: element.dataset.cardKind,
|
|
border: { top: getComputedStyle(element).borderTopColor, left: getComputedStyle(element).borderLeftColor, right: getComputedStyle(element).borderRightColor },
|
|
}));
|
|
const feedback = shadow.querySelector('.hm-orgchart-move-feedback');
|
|
return { layout: expanded?.dataset.autoLayout, scale: Number(viewport.dataset.scale), panX: Number(viewport.dataset.panX), panY: Number(viewport.dataset.panY),
|
|
viewport: rect(viewport), forest: rect(shadow.querySelector('.hm-orgchart-forest')), nodes, cards,
|
|
feedback: { status: feedback?.dataset.moveStatus, text: feedback?.textContent },
|
|
drop: [...shadow.querySelectorAll('[data-drop-state]')].map(element => ({ id: element.dataset.organizationId, state: element.dataset.dropState, hover: element.dataset.dropHover })),
|
|
dragging: viewport.dataset.editDragging, panDragging: viewport.dataset.dragging, activeEmployee: shadow.activeElement?.dataset.employeeId,
|
|
activeOrganization: shadow.activeElement?.dataset.organizationId, dragDropEnabled: viewport.dataset.dragDropEnabled,
|
|
dialogs: document.querySelectorAll('[data-component="HMOrgChartEmployeeDialog"]').length,
|
|
companyRoots: nodes.filter(org => org.parentId === '').map(org => org.companyId), documentWidth: document.documentElement.scrollWidth,
|
|
};
|
|
});
|
|
}
|
|
const relations = s => JSON.stringify({ nodes: s.nodes.map(({ id, parentId, companyId }) => ({ id, parentId, companyId })), cards: s.cards.map(({ id, organizationId, companyId }) => ({ id, organizationId, companyId })) });
|
|
async function blankPoint() {
|
|
return page.evaluate(() => {
|
|
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot, viewport = shadow.querySelector('.hm-orgchart-viewport'), box = viewport.getBoundingClientRect();
|
|
for (const [dx, dy] of [[8, 8], [8, box.height - 8], [box.width - 8, 8], [box.width - 8, box.height - 8], [box.width / 2, box.height - 8]]) {
|
|
const point = { x: box.left + dx, y: box.top + dy }, hit = shadow.elementFromPoint(point.x, point.y);
|
|
if (hit && !hit.closest('button,a,.hm-orgchart-employee,.node')) return point;
|
|
} throw new Error('No visible blank Pan point found');
|
|
});
|
|
}
|
|
async function pan(dx, dy) {
|
|
const p = await blankPoint(); await page.mouse.move(p.x, p.y); await page.mouse.down(); const v = await viewport().boundingBox();
|
|
await page.mouse.move(p.x + Math.sign(v.x + v.width / 2 - p.x) * 8, p.y, { steps: 2 });
|
|
await page.mouse.move(p.x + dx, p.y + dy, { steps: 8 }); await page.mouse.up(); await settle();
|
|
}
|
|
async function centerPair(source, target) {
|
|
await viewport().scrollIntoViewIfNeeded(); await source.focus(); await settle();
|
|
for (let pass = 0; pass < 3; pass++) {
|
|
const a = await source.boundingBox(), b = await target.boundingBox(), v = await viewport().boundingBox();
|
|
if (!a || !b || !v) throw new Error('Drag source/target has no geometry');
|
|
const left = Math.min(a.x, b.x), right = Math.max(a.x + a.width, b.x + b.width), top = Math.min(a.y, b.y), bottom = Math.max(a.y + a.height, b.y + b.height);
|
|
if (right - left > v.width - 24 || bottom - top > v.height - 24) throw new Error('Drag pair exceeds viewport at current scale');
|
|
const dx = v.x + v.width / 2 - (left + right) / 2, dy = v.y + v.height / 2 - (top + bottom) / 2;
|
|
if (Math.abs(dx) < 1 && Math.abs(dy) < 1) break; await pan(dx, dy);
|
|
}
|
|
}
|
|
async function drag(source, target, beforeRelease) {
|
|
await centerPair(source, target); const a = await source.boundingBox(), b = await target.boundingBox(), before = await state();
|
|
await page.mouse.move(a.x + a.width / 2, a.y + a.height / 2); await page.mouse.down();
|
|
await page.mouse.move(a.x + a.width / 2 + 8, a.y + a.height / 2, { steps: 2 });
|
|
await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2, { steps: 14 }); const hover = await state();
|
|
if (beforeRelease) await beforeRelease();
|
|
await page.mouse.up(); await ready(); return { before, hover, after: await state() };
|
|
}
|
|
async function snap(name, fullPage = false) { await page.screenshot({ path: path.join(output, `${name}.png`), fullPage }); }
|
|
async function section(name, action) {
|
|
if (only.length && !only.includes(name)) return;
|
|
try { await action(); } catch (error) { check(false, `${name}: execution completed`, { message: error.message, stack: error.stack }); await page.mouse.up().catch(() => {}); await snap(`failure-${name}`, true).catch(() => {}); }
|
|
}
|
|
await section('department', async () => {
|
|
await page.setViewportSize({ width: 1440, height: 1000 });
|
|
await page.goto(`${origin}/iframe.html?id=orgchart--department-colors&viewMode=story`); await ready();
|
|
await page.evaluate(async () => { await Promise.all([...document.querySelector('.hm-orgchart-component-host').shadowRoot.querySelectorAll('img')].map(async image => { image.loading = 'eager'; await image.decode(); })); });
|
|
const department = await page.evaluate(() => {
|
|
const shadow = document.querySelector('.hm-orgchart-component-host').shadowRoot, root = shadow.querySelector('.hm-orgchart-department-colors');
|
|
const cards = [...root.querySelectorAll('[data-card-asset-size="representative"]')], card = cards[0];
|
|
const styles = element => Object.fromEntries(['fontFamily','fontSize','fontWeight','lineHeight','letterSpacing','color'].map(key => [key, getComputedStyle(element)[key]]));
|
|
const renderedCard = element => ({ id: element?.dataset.employeeId, width: element?.offsetWidth, height: element?.offsetHeight,
|
|
role: element?.querySelector('.hm-orgchart-person-role')?.textContent, name: element?.querySelector('.hm-orgchart-person-name')?.textContent,
|
|
badgeCount: element?.querySelectorAll('.hm-orgchart-job-title,.hm-orgchart-status-label').length,
|
|
background: element?.querySelector('.hm-orgchart-card-background')?.src, photo: element?.querySelector('.hm-orgchart-avatar img')?.src,
|
|
roleStyle: element ? styles(element.querySelector('.hm-orgchart-person-role')) : {}, nameStyle: element ? styles(element.querySelector('.hm-orgchart-person-name')) : {} });
|
|
const rows = [...root.querySelectorAll('.hm-orgchart-color-preview')].map(row => ({ title: row.querySelector('.hm-orgchart-color-department-name')?.textContent,
|
|
gradient: getComputedStyle(row.querySelector('.hm-orgchart-color-department-name')).backgroundImage,
|
|
color: getComputedStyle(row.querySelector('.hm-orgchart-color-department-name')).color,
|
|
cards: [...row.querySelectorAll('.hm-orgchart-employee')].map(card => ({ kind: card.dataset.cardKind, width: card.offsetWidth, height: card.offsetHeight, asset: card.dataset.cardAsset })) }));
|
|
const unwanted = root.querySelectorAll('.hm-orgchart-color-legend,.hm-orgchart-contrast-note,.hm-orgchart-contrast-comparison,.hm-orgchart-card-caption,.hm-orgchart-color-preview > p').length;
|
|
const frame = root.querySelector('.tunnel-reference__sample'), stage = frame?.querySelector('.tunnel-reference__stage');
|
|
return { representativeCount: cards.length, card: renderedCard(card), rows, unwanted,
|
|
frameStyle: frame ? Object.fromEntries(['backgroundColor','border','borderRadius','boxShadow','padding','overflow'].map(key => [key, getComputedStyle(frame)[key]])) : {},
|
|
stageStyle: stage ? Object.fromEntries(['padding','overflowX','maxWidth'].map(key => [key, getComputedStyle(stage)[key]])) : {},
|
|
representativeFirst: Boolean(card && root.firstElementChild?.contains(card)), documentWidth: document.documentElement.scrollWidth };
|
|
});
|
|
check(department.representativeCount === 1 && department.card.width === 146 && department.card.height === 74, 'Department representative: one shared 146x74 card', department.card);
|
|
check(department.card.role === '대표이사' && department.card.name === '홍 길 동' && department.card.badgeCount === 0 && /Noto Sans KR/.test(department.card.nameStyle.fontFamily), 'Department representative: approved text, font and zero badges');
|
|
check(department.representativeFirst && department.rows.length === 6 && department.rows.every(row => row.cards.length === 2 && row.cards[0].kind === 'leader' && row.cards[0].width === 146 && row.cards[0].height === 68 && row.cards[1].width === 108 && row.cards[1].height === 52), 'Department order: representative, six group titles, large leaders and small employees');
|
|
check(department.unwanted === 0, 'Department compact preview retains no legends, contrast/debug prose or duplicate card captions');
|
|
await snap('department-representative'); await snap('department-full', true);
|
|
await page.goto(`${origin}/iframe.html?id=orgchart--full-composition&viewMode=story`); await ready();
|
|
const fixedCard = await page.locator('[data-card-asset-size="representative"]').evaluate(element => {
|
|
const styles = target => Object.fromEntries(['fontFamily','fontSize','fontWeight','lineHeight','letterSpacing','color'].map(key => [key, getComputedStyle(target)[key]]));
|
|
return { id: element.dataset.employeeId, background: element.querySelector('.hm-orgchart-card-background')?.src, photo: element.querySelector('.hm-orgchart-avatar img')?.src,
|
|
roleStyle: styles(element.querySelector('.hm-orgchart-person-role')), nameStyle: styles(element.querySelector('.hm-orgchart-person-name')) };
|
|
});
|
|
check(['id','background','photo','roleStyle','nameStyle'].every(key => JSON.stringify(fixedCard[key]) === JSON.stringify(department.card[key])), 'Department and Fixed representative reuse identical SVG, avatar and typography', { department: department.card, fixed: fixedCard });
|
|
const prior = JSON.parse(await fs.readFile(path.join(root, 'test-results/org-chart-p9-1/browser.json'), 'utf8')).snapshots?.department?.['1440'];
|
|
if (prior?.departments) check(department.rows.every((row, i) => row.gradient === prior.departments[i].headingStyle.backgroundImage && row.color === prior.departments[i].headingStyle.color), 'Department approved group gradients and white titles preserve P9.1 baseline');
|
|
else check(false, 'Department historical approved gradient evidence exists', 'Check P9.1 browser.json snapshot structure');
|
|
await page.goto(`${origin}/iframe.html?id=center-graphic-facility--tunnel&viewMode=story`); await page.locator('.tunnel-reference__sample').first().waitFor(); await settle();
|
|
const reference = await page.locator('.tunnel-reference__sample').first().evaluate(frame => {
|
|
const stage = frame.querySelector('.tunnel-reference__stage');
|
|
return { frameStyle: Object.fromEntries(['backgroundColor','border','borderRadius','boxShadow','padding','overflow'].map(key => [key, getComputedStyle(frame)[key]])),
|
|
stageStyle: Object.fromEntries(['padding','overflowX','maxWidth'].map(key => [key, getComputedStyle(stage)[key]])) };
|
|
});
|
|
check(JSON.stringify(reference.frameStyle) === JSON.stringify(department.frameStyle) && JSON.stringify(reference.stageStyle) === JSON.stringify(department.stageStyle), 'Department shares actual Tunnel preview frame, background, border, radius, shadow, padding and overflow', { department: { frameStyle: department.frameStyle, stageStyle: department.stageStyle }, reference });
|
|
measurements.push({ name: 'Department', department, fixedCard, reference });
|
|
});
|
|
|
|
await section('department-names', async () => {
|
|
// User's later naming correction only; keep every prior P9.4 PASS as evidence.
|
|
const previous = JSON.parse(await fs.readFile(path.join(output, 'browser.json'), 'utf8')).measurements.find(item => item.name === 'Department').department;
|
|
const oldStyles = JSON.parse(await fs.readFile(path.join(root, 'test-results/org-chart-p9-1/browser.json'), 'utf8')).snapshots.department['1440'].departments;
|
|
const fixture = JSON.parse(await fs.readFile(path.join(root, 'examples/org-chart/data-p1.json'), 'utf8'));
|
|
await page.setViewportSize({ width:1440, height:1000 });
|
|
await page.goto(`${origin}/iframe.html?id=orgchart--department-colors&viewMode=story`); await ready();
|
|
await page.evaluate(async () => { await Promise.all([...document.querySelector('.hm-orgchart-component-host').shadowRoot.querySelectorAll('img')].map(async image => { image.loading = 'eager'; await image.decode(); })); });
|
|
const current = await page.evaluate(async () => {
|
|
const shadow = document.querySelector('.hm-orgchart-component-host').shadowRoot, root = shadow.querySelector('.hm-orgchart-department-colors');
|
|
const hash = async data => [...new Uint8Array(await crypto.subtle.digest('SHA-256', data))].map(byte => byte.toString(16).padStart(2,'0')).join('');
|
|
const keys = ['fontFamily','fontSize','fontWeight','lineHeight','letterSpacing','color'];
|
|
const styles = element => Object.fromEntries(keys.map(key => [key,getComputedStyle(element)[key]]));
|
|
const card = async element => ({ id:element.dataset.employeeId,kind:element.dataset.cardKind,asset:element.dataset.cardAsset,width:element.offsetWidth,height:element.offsetHeight,
|
|
name:element.querySelector('.hm-orgchart-person-name')?.textContent,role:element.querySelector('.hm-orgchart-person-role')?.textContent,
|
|
badges:[...element.querySelectorAll('.hm-orgchart-job-title')].map(badge => badge.textContent),
|
|
roleStyle:styles(element.querySelector('.hm-orgchart-person-role')),nameStyle:styles(element.querySelector('.hm-orgchart-person-name')),
|
|
assets:await Promise.all([...element.querySelectorAll('img')].map(async image => ({ hash:await hash(await (await fetch(image.currentSrc || image.src)).arrayBuffer()),naturalWidth:image.naturalWidth,naturalHeight:image.naturalHeight,loaded:image.complete && image.naturalWidth > 0 }))) });
|
|
return { cards:await Promise.all([...root.querySelectorAll('.hm-orgchart-employee')].map(card)),
|
|
rows:await Promise.all([...root.querySelectorAll('.hm-orgchart-color-preview')].map(async row => ({ title:row.querySelector('.hm-orgchart-color-department-name').textContent,
|
|
gradient:getComputedStyle(row.querySelector('.hm-orgchart-color-department-name')).backgroundImage,color:getComputedStyle(row.querySelector('.hm-orgchart-color-department-name')).color,
|
|
cards:await Promise.all([...row.querySelectorAll('.hm-orgchart-employee')].map(card)) }))) };
|
|
});
|
|
check(current.cards.length === 13 && current.cards.every(card => card.name === '홍 길 동'), 'Department naming correction: all thirteen displayed names are 홍 길 동', current.cards.map(({ id,name }) => ({ id,name })));
|
|
const rolePreserved = current.cards.every(card => {
|
|
const person = fixture.employees.find(employee => employee.id === card.id);
|
|
const expectedRole = card.kind === 'topLeader' ? '대표이사' : person?.position.replace(/\s+/gu,'');
|
|
const expectedBadges = card.kind === 'topLeader' ? [] : person?.jobTitle ? [person.jobTitle] : [];
|
|
return Boolean(person) && card.role === expectedRole && JSON.stringify(card.badges) === JSON.stringify(expectedBadges);
|
|
});
|
|
check(rolePreserved && new Set(current.cards.map(card => card.id)).size === 13, 'Department naming correction preserves fixture Employee IDs, positions and organizational badges');
|
|
check(current.rows.length === previous.rows.length && current.rows.every((row,index) => {
|
|
const old = previous.rows[index]; return row.title === old.title && row.gradient === old.gradient && row.color === old.color
|
|
&& row.cards.length === old.cards.length && row.cards.every((card,i) => ['kind','width','height','asset'].every(key => card[key] === old.cards[i][key]));
|
|
}), 'Department naming correction preserves P9.4 group titles, colors, gradients, card sizes and artwork mappings');
|
|
const styleKeys = ['fontFamily','fontSize','fontWeight','lineHeight','letterSpacing','color'];
|
|
check(current.rows.every((row,index) => row.cards.every((card,i) => {
|
|
const old = oldStyles[index].cards[i];
|
|
const role = old.subtree.find(item => item.className === 'hm-orgchart-person-role')?.style, name = old.subtree.find(item => item.className === 'hm-orgchart-person-name')?.style;
|
|
return styleKeys.every(key => card.roleStyle[key] === role?.[key] && card.nameStyle[key] === name?.[key]) && JSON.stringify(card.assets) === JSON.stringify(old.assets);
|
|
})), 'Department naming correction preserves approved card SVG/photo hashes and typography from P9.1');
|
|
const baseline = JSON.parse(await fs.readFile(path.join(output, 'baseline.json'), 'utf8'));
|
|
const safe = baseline.files.filter(file => /^examples\/org-chart\/data.*\.json$/.test(file.path) || /^packages\/org-chart\/src\/assets\//.test(file.path)
|
|
|| ['packages/org-chart/src/core/employee-card.ts','packages/org-chart/src/styles/employee-card.css','packages/org-chart/src/core/org-chart-content.ts','packages/org-chart/src/core/render.ts','src/org-chart/org-chart-full-composition.stories.tsx'].includes(file.path));
|
|
const changed = [];
|
|
for (const file of safe) { const hash = createHash('sha256').update(await fs.readFile(path.join(root,file.path))).digest('hex'); if (hash.toUpperCase() !== file.sha256.toUpperCase()) changed.push(file.path); }
|
|
check(changed.length === 0 && safe.length > 20, 'Department naming correction leaves original fixtures, shared card renderer/styles, Fixed presenter and approved assets unchanged', { checked:safe.length,changed });
|
|
const representative = current.cards.find(card => card.kind === 'topLeader');
|
|
check(representative.width === previous.card.width && representative.height === previous.card.height && representative.badges.length === 0
|
|
&& JSON.stringify(representative.nameStyle) === JSON.stringify(previous.card.nameStyle) && JSON.stringify(representative.roleStyle) === JSON.stringify(previous.card.roleStyle), 'Department representative retains 146x74, zero badges and P9.4 typography');
|
|
await snap('department-names',true); measurements.push({ name:'Department naming correction',current });
|
|
});
|
|
|
|
await section('off', async () => {
|
|
await setup();
|
|
check(await viewport().getAttribute('data-drag-drop-enabled') === 'false', 'Drag & Drop fixed OFF'); await snap('drag-drop-off', true);
|
|
for (const [name, source, target] of [
|
|
['employee within company', employee('virtual-022'), title('hanmac-office-sibling')],
|
|
['group within company', title('hanmac-office-0'), title('hanmac-office-1')],
|
|
['employee across companies', employee('virtual-022'), title('saman-office-0')],
|
|
['group across companies', title('hanmac-office-0'), title('saman-office-1')],
|
|
]) {
|
|
const result = await drag(source, target);
|
|
check(relations(result.before) === relations(result.after) && !result.hover.dragging && result.hover.drop.length === 0 && result.after.drop.length === 0, `OFF blocks ${name} and all Drop highlights`);
|
|
}
|
|
for (const source of [employee('virtual-022'), title('hanmac-office-0')]) {
|
|
await source.focus(); const before = await state(); await page.keyboard.press('Alt+m'); await settle(); const after = await state();
|
|
check(!after.dragging && after.drop.length === 0 && relations(after) === relations(before), 'OFF blocks Alt+M on employee/group without highlighting');
|
|
}
|
|
await viewport().scrollIntoViewIfNeeded(); const before = await state(); await pan(70, -35); const after = await state();
|
|
check(Math.abs(after.panX - before.panX - 70) < 1 && Math.abs(after.panY - before.panY + 35) < 1 && relations(before) === relations(after), 'OFF preserves blank-space Pan');
|
|
await title('hanmac-office-sibling').click(); await ready(); check(await title('hanmac-office-sibling').getAttribute('aria-expanded') === 'false', 'OFF group click collapses');
|
|
await title('hanmac-office-sibling').press('Space'); await ready(140); check(await title('hanmac-office-sibling').getAttribute('aria-expanded') === 'true', 'OFF group Space expands');
|
|
await employee('virtual-022').click(); await dialog().waitFor(); check(await dialog().getAttribute('data-employee-id') === 'virtual-022', 'OFF employee click opens HM Dialog');
|
|
await page.keyboard.press('Escape'); await dialog().waitFor({ state: 'hidden' }); await settle();
|
|
check((await state()).activeEmployee === 'virtual-022', 'OFF Dialog closes and restores employee focus');
|
|
const zoomBefore = await state(); await page.getByRole('button', { name: '확대', exact: true }).click(); await settle(); check((await state()).scale > zoomBefore.scale, 'OFF Zoom remains available');
|
|
await page.getByRole('button', { name: 'Fit to View', exact: true }).click(); await settle(); const fit = await state();
|
|
check(fit.forest.x >= fit.viewport.x - 1 && fit.forest.right <= fit.viewport.right + 1 && fit.forest.y >= fit.viewport.y - 1 && fit.forest.bottom <= fit.viewport.bottom + 1, 'OFF Fit contains selected forest');
|
|
measurements.push({ name: 'OFF browsing', state: fit });
|
|
});
|
|
|
|
await section('responsive', async () => {
|
|
for (const width of [390,768,1280,1440]) {
|
|
await page.setViewportSize({ width, height: 1100 }); await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`); await ready(140);
|
|
const metric = await page.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth }));
|
|
check(metric.scrollWidth <= width + 1, `${width}: preview-only Expanded has no horizontal overflow`, metric);
|
|
await snap(`expanded-${width}`, true);
|
|
await page.goto(`${origin}/iframe.html?id=orgchart--department-colors&viewMode=story`); await ready();
|
|
const departmentOverflow = await page.evaluate(() => ({ width: innerWidth, scrollWidth: document.documentElement.scrollWidth }));
|
|
check(departmentOverflow.scrollWidth <= width + 1, `${width}: Department preview containers fit viewport`, departmentOverflow);
|
|
await snap(`department-${width}`, true);
|
|
}
|
|
});
|
|
|
|
await section('expanded-frame', async () => {
|
|
// Later outer-frame style correction only. Existing layout/move suites remain
|
|
// the evidence for those features.
|
|
await page.setViewportSize({ width:1440,height:1100 });
|
|
await page.goto(`${origin}/iframe.html?id=center-graphic-facility--tunnel&viewMode=story`); await page.locator('.tunnel-reference__sample').first().waitFor(); await settle();
|
|
const reference = await page.locator('.tunnel-reference__sample').first().evaluate(element => {
|
|
const css = getComputedStyle(element); return { backgroundColor:css.backgroundColor,border:css.border,borderRadius:css.borderRadius };
|
|
});
|
|
await page.goto(`${origin}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`); await ready(140);
|
|
const frameSnapshot = () => page.evaluate(() => {
|
|
const shadow = document.querySelector('.hm-orgchart-host').shadowRoot, root = shadow.querySelector('.hm-orgchart-expanded'), css = getComputedStyle(root);
|
|
return { frame:{ backgroundColor:css.backgroundColor,border:css.border,borderRadius:css.borderRadius },
|
|
inputs:Object.fromEntries(['--hm-orgchart-expanded-preview-background','--hm-orgchart-expanded-preview-border','--hm-orgchart-expanded-preview-radius'].map(name => [name,css.getPropertyValue(name).trim()])),
|
|
cards:[...shadow.querySelectorAll('.hm-orgchart-employee')].map(card => ({ id:card.dataset.employeeId,background:getComputedStyle(card).backgroundColor,width:card.offsetWidth,height:card.offsetHeight,kind:card.dataset.cardKind })),
|
|
titles:[...shadow.querySelectorAll('.node[data-company-id] .hm-orgchart-organization-name')].map(title => ({ company:title.closest('.node').dataset.companyId,color:getComputedStyle(title).color,background:getComputedStyle(title).backgroundColor })),
|
|
layout:root.dataset.autoLayout,width:innerWidth,documentWidth:document.documentElement.scrollWidth,
|
|
pan:shadow.querySelector('.hm-orgchart-viewport').dataset.panEnabled,zoom:shadow.querySelector('.hm-orgchart-viewport').dataset.zoomEnabled,
|
|
editing:shadow.querySelector('.hm-orgchart-viewport').dataset.dragDropEnabled };
|
|
});
|
|
const before = await frameSnapshot();
|
|
check(JSON.stringify(before.frame) === JSON.stringify(reference) && Object.values(before.inputs).every(value => value.length > 0), 'Expanded outer frame matches actual Tunnel shared background, border and radius', { expanded:before.frame,reference,inputs:before.inputs });
|
|
const expectedColors = { hanmac:'rgb(203, 46, 40)',saman:'rgb(240, 131, 0)','jangheon-industry':'rgb(69, 128, 166)',jangheon:'rgb(29, 65, 141)',ptc:'rgb(132, 51, 142)',halla:'rgb(0, 104, 183)',baron:'rgb(12, 54, 127)' };
|
|
check(before.cards.length === 140 && before.cards.every(card => card.background === 'rgb(255, 255, 255)') && before.titles.every(title => title.color === 'rgb(255, 255, 255)' && title.background === expectedColors[title.company]), 'Expanded outer-frame style preserves white employee backgrounds and approved company/white group titles');
|
|
check(before.layout === 'tb' && before.editing === 'false' && before.pan === 'true' && before.zoom === 'true', 'Expanded frame correction preserves TB, fixed OFF and enabled browsing', before);
|
|
await snap('expanded-frame-off',true); await snap('expanded-frame-scope-removed',true);
|
|
await page.setViewportSize({ width:390,height:1100 }); await settle(); const narrow = await frameSnapshot();
|
|
check(narrow.documentWidth <= 391 && JSON.stringify(narrow.frame) === JSON.stringify(reference), '390px corrected Expanded frame fits page and retains shared style', { width:narrow.width,scrollWidth:narrow.documentWidth,frame:narrow.frame });
|
|
await snap('expanded-frame-390',true);
|
|
measurements.push({ name:'Expanded outer-frame correction',reference,before,narrow });
|
|
});
|
|
|
|
await section('names-mobile', async () => {
|
|
// Only the narrow viewport and current captures are new; all thirteen names,
|
|
// roles/artwork/typography already passed the department-names section.
|
|
await page.setViewportSize({ width:1440,height:1000 }); await page.goto(`${origin}/iframe.html?id=orgchart--department-colors&viewMode=story`); await ready();
|
|
await page.evaluate(async () => { await Promise.all([...document.querySelector('.hm-orgchart-component-host').shadowRoot.querySelectorAll('img')].map(async image => { image.loading = 'eager'; await image.decode(); })); });
|
|
await snap('department-full-current',true); await page.locator('.hm-orgchart-representative-preview').screenshot({ path:path.join(output,'department-representative-current.png') });
|
|
await page.setViewportSize({ width:390,height:1100 }); await settle();
|
|
const narrow = await page.evaluate(() => {
|
|
const shadow = document.querySelector('.hm-orgchart-component-host').shadowRoot;
|
|
const cards = [...shadow.querySelectorAll('.hm-orgchart-employee')].map(card => { const box = card.getBoundingClientRect(); return { id:card.dataset.employeeId,left:box.left,right:box.right,name:card.querySelector('.hm-orgchart-person-name').textContent }; });
|
|
return { width:innerWidth,documentWidth:document.documentElement.scrollWidth,cards };
|
|
});
|
|
check(narrow.documentWidth <= 391 && narrow.cards.every(card => card.left >= -1 && card.right <= 391), '390px corrected Department names/cards stay within page without horizontal overflow', narrow);
|
|
await snap('department-names-390',true); measurements.push({ name:'Department narrow capture',narrow });
|
|
});
|
|
|
|
await section('menu', async () => {
|
|
const index = await (await page.request.get(`${origin}/index.json`)).json();
|
|
const entries = Object.values(index.entries).filter(entry => entry.type === 'story' && entry.title.startsWith('OrgChart/') && !(entry.tags ?? []).includes('internal'));
|
|
const expected = ['orgchart--logo-personnel-summary','orgchart--department-colors','orgchart--full-composition','orgchart--expanded-org-chart'];
|
|
check(entries.length === 4 && entries.every(entry => expected.includes(entry.id)), 'Four existing public OrgChart Story IDs retained', entries.map(entry => ({ id: entry.id, title: entry.title })));
|
|
});
|
|
|
|
check(errors.length === 0, 'New console/page errors: zero', errors);
|
|
const result = { startedAt, completedAt: new Date().toISOString(), browser: browser.version(), origin, sections: only, results, measurements, errors,
|
|
notVerified: ['Physical touch device','Actual screen reader','Design intent approval'], pass: results.filter(item => item.status === 'PASS').length, fail: results.filter(item => item.status === 'FAIL').length };
|
|
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-p9-4/browser${suffix}.json` }));
|
|
await browser.close(); if (result.fail) process.exitCode = 1;
|