fix: align org chart hierarchy and typography weights
This commit is contained in:
1 parent
72d427fcd2
commit
d42466f5eb
33 files changed
+358
-85
No files matched your search
@@ -0,0 +1,146 @@
|
||||
// Actual Expanded Storybook UI: seven companies x three sizes x three layouts.
|
||||
// Build Storybook first; uses the existing optional Playwright/Chrome runtime.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import { launchChromium } from '../../scripts/orgchart-browser-runtime.mjs';
|
||||
|
||||
const output = path.resolve('test-results/org-chart-hierarchy');
|
||||
const directory = path.join(output, 'storybook');
|
||||
const template = JSON.parse(await fs.readFile('examples/org-chart/data-expanded-large.json', 'utf8'));
|
||||
const companies = template.affiliations;
|
||||
const mime = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.ttf': 'font/ttf', '.woff2': 'font/woff2' };
|
||||
const server = http.createServer(async (request, response) => {
|
||||
try {
|
||||
const pathname = decodeURIComponent(new URL(request.url, 'http://localhost').pathname);
|
||||
if (pathname === '/favicon.ico') return response.writeHead(204).end();
|
||||
const file = path.resolve(directory, '.' + (pathname === '/' ? '/index.html' : pathname));
|
||||
const relative = path.relative(directory, file);
|
||||
if (relative.startsWith('..') || path.isAbsolute(relative)) return response.writeHead(403).end();
|
||||
response.writeHead(200, { 'Content-Type': mime[path.extname(file)] ?? 'application/octet-stream' }).end(await fs.readFile(file));
|
||||
} catch { response.writeHead(404).end(); }
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
let browser, failure;
|
||||
// Resume only complete company groups after a test harness interruption.
|
||||
const savedChecks = process.argv.includes('--resume')
|
||||
? JSON.parse(await fs.readFile(path.join(output, 'browser-progress.json'), 'utf8')).checks : [];
|
||||
const checks = savedChecks.filter(check => savedChecks.filter(row => row.company === check.company).length === 9);
|
||||
const errors = [];
|
||||
try {
|
||||
browser = await launchChromium({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
|
||||
page.on('pageerror', error => errors.push(error.message));
|
||||
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
|
||||
const settle = () => page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
for (let i = 0; i < 5; i++) await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
});
|
||||
const ready = async count => {
|
||||
await page.waitForFunction(count => {
|
||||
const shadow = document.querySelector('.hm-orgchart-host')?.shadowRoot;
|
||||
return shadow?.querySelector('.hm-orgchart-shell[aria-busy="false"]')
|
||||
&& (count === undefined || shadow.querySelectorAll('.hm-orgchart-employee').length === count)
|
||||
&& [...shadow.querySelectorAll('link')].every(link => link.sheet);
|
||||
}, count);
|
||||
await settle();
|
||||
};
|
||||
const select = async (label, option) => {
|
||||
await page.getByRole('combobox', { name: label, exact: true }).click();
|
||||
await page.getByRole('option', { name: option, exact: true }).click();
|
||||
};
|
||||
const snapshot = () => page.locator('.hm-orgchart-host').evaluate(host => {
|
||||
const shadow = host.shadowRoot;
|
||||
const box = element => { const r = element.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; };
|
||||
return {
|
||||
nodes: [...shadow.querySelectorAll('.node[data-organization-id]')].map(node => ({ id: node.dataset.organizationId, parentId: node.dataset.parentId || null, type: node.dataset.organizationType })),
|
||||
employees: [...shadow.querySelectorAll('.hm-orgchart-employee')].map(card => ({ id: card.dataset.employeeId, organizationId: card.closest('.node').dataset.organizationId })),
|
||||
viewport: box(shadow.querySelector('.hm-orgchart-viewport')),
|
||||
forest: box(shadow.querySelector('.hm-orgchart-forest')),
|
||||
contentBounds: (() => {
|
||||
const boxes = [...shadow.querySelectorAll('.hm-orgchart-organization, .hm-orgchart-organization-connectors path')].filter(element => element.getClientRects().length).map(box);
|
||||
const x = Math.min(...boxes.map(b => b.x)), y = Math.min(...boxes.map(b => b.y));
|
||||
return { x, y, width: Math.max(...boxes.map(b => b.x + b.width)) - x, height: Math.max(...boxes.map(b => b.y + b.height)) - y };
|
||||
})(),
|
||||
layout: shadow.querySelector('.hm-orgchart-expanded').dataset.autoLayout,
|
||||
paths: [...shadow.querySelectorAll('.hm-orgchart-organization-connectors path')].map(p => ({ id: p.dataset.organizationId, parentId: p.dataset.parentId, d: p.getAttribute('d') })),
|
||||
upstreamConnectors: [...shadow.querySelectorAll('.hierarchy, .node[data-depth]')].flatMap(element => ['::before', '::after'].map(pseudo => {
|
||||
const style = getComputedStyle(element, pseudo);
|
||||
return style.content !== 'none' && style.display !== 'none' && [style.borderTopWidth, style.borderLeftWidth, style.borderRightWidth, style.borderBottomWidth].some(width => parseFloat(width) > 0);
|
||||
})).filter(Boolean).length,
|
||||
};
|
||||
});
|
||||
const fit = async () => {
|
||||
await page.getByRole('button', { name: 'Fit to View', exact: true }).click();
|
||||
await settle();
|
||||
const { viewport: v, contentBounds: f } = await snapshot();
|
||||
assert.ok(f.x >= v.x - 1 && f.y >= v.y - 1 && f.x + f.width <= v.x + v.width + 1 && f.y + f.height <= v.y + v.height + 1, `complete forest fits viewport: ${JSON.stringify({ viewport: v, forest: f })}`);
|
||||
};
|
||||
await page.goto(`http://127.0.0.1:${server.address().port}/iframe.html?id=orgchart--expanded-org-chart&viewMode=story`);
|
||||
await ready(140);
|
||||
let previousCompany;
|
||||
for (const company of companies) {
|
||||
if (checks.filter(check => check.company === company.id).length === 9) continue;
|
||||
await page.getByRole('button', { name: /회사 선택/ }).click();
|
||||
await page.getByRole('checkbox', { name: previousCompany?.name ?? '전체 선택', exact: true }).uncheck();
|
||||
await page.getByRole('checkbox', { name: company.name, exact: true }).check();
|
||||
await page.keyboard.press('Escape');
|
||||
previousCompany = company;
|
||||
for (const count of [20, 100, 300]) {
|
||||
await select('회사당 직원 수', `${count}명`);
|
||||
await page.getByRole('button', { name: '데이터 적용', exact: true }).click();
|
||||
await ready(count);
|
||||
const expected = template.organizations.filter(org => org.companyId === company.id).map(org => ({ id: org.id, parentId: org.parentId, type: org.type })).sort((a, b) => a.id.localeCompare(b.id));
|
||||
let employees;
|
||||
for (const [type, label] of [['tb', '세로형 TB'], ['lr', '가로형 LR'], ['hybrid', '혼합형 Hybrid']]) {
|
||||
await select('조직도 표시 유형', label);
|
||||
await ready(count);
|
||||
const rendered = await snapshot();
|
||||
assert.equal(rendered.layout, type);
|
||||
assert.deepEqual(rendered.nodes.sort((a, b) => a.id.localeCompare(b.id)), expected);
|
||||
const byId = new Map(rendered.nodes.map(org => [org.id, org]));
|
||||
for (const team of rendered.nodes.filter(org => org.type === 'team')) {
|
||||
let ancestor = byId.get(team.parentId);
|
||||
while (ancestor) { assert.notEqual(ancestor.type, 'cell'); ancestor = byId.get(ancestor.parentId); }
|
||||
}
|
||||
const roster = rendered.employees.sort((a, b) => a.id.localeCompare(b.id));
|
||||
assert.equal(new Set(roster.map(employee => employee.id)).size, count);
|
||||
assert.ok(roster.every(employee => byId.has(employee.organizationId)));
|
||||
if (employees) assert.deepEqual(roster, employees); else employees = roster;
|
||||
if (type === 'tb') assert.ok(rendered.upstreamConnectors > 0, 'TB engine CSS connectors are present');
|
||||
else {
|
||||
assert.equal(rendered.paths.length, expected.filter(org => org.parentId).length);
|
||||
assert.ok(rendered.paths.every(p => p.d && !/NaN|Infinity/.test(p.d) && byId.get(p.id)?.parentId === p.parentId), 'measured connectors follow parent IDs and have finite geometry');
|
||||
}
|
||||
await fit();
|
||||
const teamId = `${company.id}-team-0`, cellId = `${company.id}-cell-0`;
|
||||
const toggle = page.locator(`.hm-orgchart-expanded-toggle[data-organization-id="${teamId}"]`);
|
||||
await toggle.focus(); await toggle.press('Enter'); await ready();
|
||||
assert.equal(await page.locator(`.node[data-organization-id="${cellId}"]`).count(), 0);
|
||||
assert.ok((await snapshot()).employees.length < count);
|
||||
await fit();
|
||||
await toggle.focus(); await toggle.press('Enter'); await ready(count);
|
||||
assert.equal(await page.locator(`.node[data-organization-id="${cellId}"]`).count(), 1);
|
||||
assert.deepEqual((await snapshot()).employees.sort((a, b) => a.id.localeCompare(b.id)), employees);
|
||||
await fit();
|
||||
checks.push({ company: company.id, employeeCount: count, layout: type, hierarchy: 'PASS', employeeIdentity: 'PASS', connectors: 'PASS', collapseExpand: 'PASS', fit: 'PASS' });
|
||||
await fs.writeFile(path.join(output, 'browser-progress.json'), JSON.stringify({ checks, errors }, null, 2));
|
||||
if (company.id === 'hanmac' && count === 300) await page.screenshot({ path: path.join(output, `storybook-${type}-300.png`), fullPage: true });
|
||||
}
|
||||
}
|
||||
console.log(`${company.name}: 9 combinations PASS`);
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
assert.equal(await page.getByRole('alert').count(), 0);
|
||||
assert.equal(checks.length, companies.length * 9);
|
||||
assert.equal(new Set(checks.map(check => `${check.company}/${check.employeeCount}/${check.layout}`)).size, companies.length * 9);
|
||||
} catch (error) {
|
||||
failure = error.stack;
|
||||
throw error;
|
||||
} finally {
|
||||
await fs.writeFile(path.join(output, 'browser-result.json'), JSON.stringify({ status: failure ? 'FAIL' : 'PASS', checks, errors, ...(failure ? { failure } : {}) }, null, 2));
|
||||
await browser?.close();
|
||||
await new Promise(resolve => server.close(resolve));
|
||||
}
|
||||
console.log(`Expanded Storybook: ${checks.length}/63 combinations PASS; ${errors.length} browser errors`);
|
||||
@@ -23,10 +23,12 @@ for (const [size, count, companyCount, depth] of [['small', 20, 3, 2], ['medium'
|
||||
if (count > 20) division.leaderId = addEmployee(division, '본부장');
|
||||
const office = org(`office-${branch}`, division.id, `가상 ${branch + 1}실`, 'office', 0);
|
||||
if (depth >= 3) {
|
||||
const cell = org(`cell-${branch}`, office.id, `가상 ${branch + 1}셀`, 'cell', 0);
|
||||
// Keep record order/IDs and weighted employee placement stable. Large
|
||||
// examples use office -> team -> cell; no team may descend from a cell.
|
||||
org(`cell-${branch}`, depth >= 4 ? `${companyId}-team-${branch}` : office.id, `가상 ${branch + 1}셀`, 'cell', 0);
|
||||
if (depth >= 4) {
|
||||
org(`team-${branch}`, cell.id, `가상 ${branch + 1}팀`, 'team', 0);
|
||||
if (branch === 0) org('team-sibling', cell.id, '가상 협력팀', 'team', 1);
|
||||
org(`team-${branch}`, office.id, `가상 ${branch + 1}팀`, 'team', 0);
|
||||
if (branch === 0) org('team-sibling', office.id, '가상 협력팀', 'team', 1);
|
||||
}
|
||||
}
|
||||
if (count > 20 && branch === 0) org('office-sibling', division.id, '가상 계획실', 'office', 1);
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
{
|
||||
"id": "hanmac-cell-0",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-office-0",
|
||||
"parentId": "hanmac-team-0",
|
||||
"name": "가상 1셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -40,7 +40,7 @@
|
||||
{
|
||||
"id": "hanmac-team-0",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-cell-0",
|
||||
"parentId": "hanmac-office-0",
|
||||
"name": "가상 1팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -49,7 +49,7 @@
|
||||
{
|
||||
"id": "hanmac-team-sibling",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-cell-0",
|
||||
"parentId": "hanmac-office-0",
|
||||
"name": "가상 협력팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -85,7 +85,7 @@
|
||||
{
|
||||
"id": "hanmac-cell-1",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-office-1",
|
||||
"parentId": "hanmac-team-1",
|
||||
"name": "가상 2셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -94,7 +94,7 @@
|
||||
{
|
||||
"id": "hanmac-team-1",
|
||||
"companyId": "hanmac",
|
||||
"parentId": "hanmac-cell-1",
|
||||
"parentId": "hanmac-office-1",
|
||||
"name": "가상 2팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -130,7 +130,7 @@
|
||||
{
|
||||
"id": "saman-cell-0",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-office-0",
|
||||
"parentId": "saman-team-0",
|
||||
"name": "가상 1셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -139,7 +139,7 @@
|
||||
{
|
||||
"id": "saman-team-0",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-cell-0",
|
||||
"parentId": "saman-office-0",
|
||||
"name": "가상 1팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -148,7 +148,7 @@
|
||||
{
|
||||
"id": "saman-team-sibling",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-cell-0",
|
||||
"parentId": "saman-office-0",
|
||||
"name": "가상 협력팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -184,7 +184,7 @@
|
||||
{
|
||||
"id": "saman-cell-1",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-office-1",
|
||||
"parentId": "saman-team-1",
|
||||
"name": "가상 2셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -193,7 +193,7 @@
|
||||
{
|
||||
"id": "saman-team-1",
|
||||
"companyId": "saman",
|
||||
"parentId": "saman-cell-1",
|
||||
"parentId": "saman-office-1",
|
||||
"name": "가상 2팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -229,7 +229,7 @@
|
||||
{
|
||||
"id": "jangheon-industry-cell-0",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-office-0",
|
||||
"parentId": "jangheon-industry-team-0",
|
||||
"name": "가상 1셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -238,7 +238,7 @@
|
||||
{
|
||||
"id": "jangheon-industry-team-0",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-cell-0",
|
||||
"parentId": "jangheon-industry-office-0",
|
||||
"name": "가상 1팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -247,7 +247,7 @@
|
||||
{
|
||||
"id": "jangheon-industry-team-sibling",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-cell-0",
|
||||
"parentId": "jangheon-industry-office-0",
|
||||
"name": "가상 협력팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -283,7 +283,7 @@
|
||||
{
|
||||
"id": "jangheon-industry-cell-1",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-office-1",
|
||||
"parentId": "jangheon-industry-team-1",
|
||||
"name": "가상 2셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -292,7 +292,7 @@
|
||||
{
|
||||
"id": "jangheon-industry-team-1",
|
||||
"companyId": "jangheon-industry",
|
||||
"parentId": "jangheon-industry-cell-1",
|
||||
"parentId": "jangheon-industry-office-1",
|
||||
"name": "가상 2팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -328,7 +328,7 @@
|
||||
{
|
||||
"id": "jangheon-cell-0",
|
||||
"companyId": "jangheon",
|
||||
"parentId": "jangheon-office-0",
|
||||
"parentId": "jangheon-team-0",
|
||||
"name": "가상 1셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -337,7 +337,7 @@
|
||||
{
|
||||
"id": "jangheon-team-0",
|
||||
"companyId": "jangheon",
|
||||
"parentId": "jangheon-cell-0",
|
||||
"parentId": "jangheon-office-0",
|
||||
"name": "가상 1팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -346,7 +346,7 @@
|
||||
{
|
||||
"id": "jangheon-team-sibling",
|
||||
"companyId": "jangheon",
|
||||
"parentId": "jangheon-cell-0",
|
||||
"parentId": "jangheon-office-0",
|
||||
"name": "가상 협력팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -382,7 +382,7 @@
|
||||
{
|
||||
"id": "jangheon-cell-1",
|
||||
"companyId": "jangheon",
|
||||
"parentId": "jangheon-office-1",
|
||||
"parentId": "jangheon-team-1",
|
||||
"name": "가상 2셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -391,7 +391,7 @@
|
||||
{
|
||||
"id": "jangheon-team-1",
|
||||
"companyId": "jangheon",
|
||||
"parentId": "jangheon-cell-1",
|
||||
"parentId": "jangheon-office-1",
|
||||
"name": "가상 2팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -427,7 +427,7 @@
|
||||
{
|
||||
"id": "ptc-cell-0",
|
||||
"companyId": "ptc",
|
||||
"parentId": "ptc-office-0",
|
||||
"parentId": "ptc-team-0",
|
||||
"name": "가상 1셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -436,7 +436,7 @@
|
||||
{
|
||||
"id": "ptc-team-0",
|
||||
"companyId": "ptc",
|
||||
"parentId": "ptc-cell-0",
|
||||
"parentId": "ptc-office-0",
|
||||
"name": "가상 1팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -445,7 +445,7 @@
|
||||
{
|
||||
"id": "ptc-team-sibling",
|
||||
"companyId": "ptc",
|
||||
"parentId": "ptc-cell-0",
|
||||
"parentId": "ptc-office-0",
|
||||
"name": "가상 협력팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -481,7 +481,7 @@
|
||||
{
|
||||
"id": "ptc-cell-1",
|
||||
"companyId": "ptc",
|
||||
"parentId": "ptc-office-1",
|
||||
"parentId": "ptc-team-1",
|
||||
"name": "가상 2셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -490,7 +490,7 @@
|
||||
{
|
||||
"id": "ptc-team-1",
|
||||
"companyId": "ptc",
|
||||
"parentId": "ptc-cell-1",
|
||||
"parentId": "ptc-office-1",
|
||||
"name": "가상 2팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -526,7 +526,7 @@
|
||||
{
|
||||
"id": "halla-cell-0",
|
||||
"companyId": "halla",
|
||||
"parentId": "halla-office-0",
|
||||
"parentId": "halla-team-0",
|
||||
"name": "가상 1셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -535,7 +535,7 @@
|
||||
{
|
||||
"id": "halla-team-0",
|
||||
"companyId": "halla",
|
||||
"parentId": "halla-cell-0",
|
||||
"parentId": "halla-office-0",
|
||||
"name": "가상 1팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -544,7 +544,7 @@
|
||||
{
|
||||
"id": "halla-team-sibling",
|
||||
"companyId": "halla",
|
||||
"parentId": "halla-cell-0",
|
||||
"parentId": "halla-office-0",
|
||||
"name": "가상 협력팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -580,7 +580,7 @@
|
||||
{
|
||||
"id": "halla-cell-1",
|
||||
"companyId": "halla",
|
||||
"parentId": "halla-office-1",
|
||||
"parentId": "halla-team-1",
|
||||
"name": "가상 2셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -589,7 +589,7 @@
|
||||
{
|
||||
"id": "halla-team-1",
|
||||
"companyId": "halla",
|
||||
"parentId": "halla-cell-1",
|
||||
"parentId": "halla-office-1",
|
||||
"name": "가상 2팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -625,7 +625,7 @@
|
||||
{
|
||||
"id": "baron-cell-0",
|
||||
"companyId": "baron",
|
||||
"parentId": "baron-office-0",
|
||||
"parentId": "baron-team-0",
|
||||
"name": "가상 1셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -634,7 +634,7 @@
|
||||
{
|
||||
"id": "baron-team-0",
|
||||
"companyId": "baron",
|
||||
"parentId": "baron-cell-0",
|
||||
"parentId": "baron-office-0",
|
||||
"name": "가상 1팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -643,7 +643,7 @@
|
||||
{
|
||||
"id": "baron-team-sibling",
|
||||
"companyId": "baron",
|
||||
"parentId": "baron-cell-0",
|
||||
"parentId": "baron-office-0",
|
||||
"name": "가상 협력팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
@@ -679,7 +679,7 @@
|
||||
{
|
||||
"id": "baron-cell-1",
|
||||
"companyId": "baron",
|
||||
"parentId": "baron-office-1",
|
||||
"parentId": "baron-team-1",
|
||||
"name": "가상 2셀",
|
||||
"type": "cell",
|
||||
"leaderId": null,
|
||||
@@ -688,7 +688,7 @@
|
||||
{
|
||||
"id": "baron-team-1",
|
||||
"companyId": "baron",
|
||||
"parentId": "baron-cell-1",
|
||||
"parentId": "baron-office-1",
|
||||
"name": "가상 2팀",
|
||||
"type": "team",
|
||||
"leaderId": null,
|
||||
|
||||
@@ -96,7 +96,7 @@ function verifyTypography(value, label) {
|
||||
check(cards.length > 0 && cards.every(card => card.role.color === (size === 'representative' ? 'rgb(34, 67, 135)' : 'rgb(121, 121, 121)')
|
||||
&& card.role.fontSize === (small ? '15px' : '20px') && card.role.fontWeight === '400' && card.role.lineHeight === (small ? '18px' : '22px')
|
||||
&& card.role.letterSpacing === (small ? '-0.6px' : '-0.8px')), `${label}/${size}: position exact computed typography`);
|
||||
check(cards.every(card => card.person.color === 'rgb(7, 29, 58)' && card.person.fontSize === (small ? '20px' : '24px') && card.person.fontWeight === '600'
|
||||
check(cards.every(card => card.person.color === 'rgb(7, 29, 58)' && card.person.fontSize === (small ? '20px' : '24px') && card.person.fontWeight === '700'
|
||||
&& card.person.lineHeight === (small ? '24px' : '30px') && card.person.letterSpacing === (size === 'leader' ? '-0.96px' : '-0.8px')), `${label}/${size}: name exact computed typography`);
|
||||
check(cards.every(card => card.cssGap === '0px' && card.textGap === 0 && card.role.margin === '0px' && card.role.padding === '0px'
|
||||
&& card.person.margin === '0px' && card.person.padding === '0px'), `${label}/${size}: zero gap/margins/padding between two text rows`);
|
||||
|
||||
@@ -95,7 +95,7 @@ async function capture(story, scenario, data) {
|
||||
const person = (data ?? fixture).employees.find(item => item.id === card.id);
|
||||
check(card.badge?.text === person.jobTitle || (!person.jobTitle && !card.badge), `${prefix}: jobTitle-driven badge`, { jobTitle: person.jobTitle, badge: card.badge?.text ?? null });
|
||||
if (!person.jobTitle) check(card.attributesHeight === 0 && card.attributesPosition === 'absolute', `${prefix}: no empty badge space`, card.attributesHeight);
|
||||
if (card.badge && card.size !== 'representative') check(card.badge.fontSize === (card.size === 'leader' ? '9px' : '8.5px') && card.badge.fontWeight === (card.size === 'leader' ? '700' : '600'), `${prefix}: badge typography`, card.badge);
|
||||
if (card.badge && card.size !== 'representative') check(card.badge.fontSize === (card.size === 'leader' ? '9px' : '8.5px') && card.badge.fontWeight === (card.size === 'leader' ? '700' : '500'), `${prefix}: badge typography`, card.badge);
|
||||
if (card.size !== 'representative') {
|
||||
check(card.row?.display === 'flex' && card.row.alignItems === 'center' && card.row.gap === '4px' && Math.abs(card.row.nameGap) < 0.1, `${prefix}: position/badge row and zero name gap`, card.row);
|
||||
if (card.badge) check(Math.abs(card.row.badgeGap - 4) < 0.1 && card.row.badgeMargin === '0px', `${prefix}: actual position/badge gap and margin`, card.row);
|
||||
|
||||
@@ -109,9 +109,9 @@ function verify(m, route, v) {
|
||||
check(m.cards.every(c => { const [w,h] = c.size === 'representative' ? [146,74] : c.size === 'leader' ? [146,68] : c.size === 'employee' ? [108,52] : [114,54]; return near(c.box.width,w) && near(c.box.height,h); }), `${name}: approved card sizes and existing P3 status sizes preserved`);
|
||||
check(m.cards.every(c => { const [w,h] = c.size === 'representative' ? [36,62] : c.size === 'leader' ? [32,56] : c.size === 'employee' ? [24,44] : [28,28]; return c.photo === 'placeholder' && c.image?.placeholder === 'true' && c.image.naturalWidth > 0 && near(c.avatar.box.width,w) && near(c.avatar.box.height,h) && c.avatar.borderRadius !== '50%'; }) && new Set(m.cards.map(c => c.image?.src)).size === 1, `${name}: common decoded square icon in every noncircular slot`);
|
||||
check(m.headers.filter(h => h.depth === 1).every(h => h.fontSize === '20px' && h.fontWeight === '700' && h.lineHeight === '36px' && near(h.box.height,36)), `${name}: 1Depth 20/700/36 and header36`, m.headers.filter(h => h.depth === 1));
|
||||
check(m.headers.filter(h => h.depth >= 2).every(h => { const small = h.id.startsWith('talent-growth-') || h.id.startsWith('business-planning-'); return h.fontSize === (small ? '18px':'20px') && h.fontWeight === (small?'600':'700') && h.lineHeight === (small?'16.9px':'19.7px') && near(h.box.width,114) && near(h.box.height,30.4); }), `${name}: 2Depth source typography/surface`, m.headers.filter(h => h.depth >= 2));
|
||||
check(m.headers.filter(h => h.depth >= 2).every(h => { const small = h.id.startsWith('talent-growth-') || h.id.startsWith('business-planning-'); return h.fontSize === (small ? '18px':'20px') && h.fontWeight === '700' && h.lineHeight === (small?'16.9px':'19.7px') && near(h.box.width,114) && near(h.box.height,30.4); }), `${name}: 2Depth source typography/surface`, m.headers.filter(h => h.depth >= 2));
|
||||
check(m.headers.every(h => h.gap === null || near(h.gap,8)), `${name}: all title-to-first-card gaps 8px`, m.headers.map(h => [h.id,h.gap]));
|
||||
check(m.hub.font.fontSize === '21px' && m.hub.font.fontWeight === '750' && m.hub.font.lineHeight === '24px' && m.hub.paddingLeft === '22px' && m.hub.paddingRight === '24px' && m.hub.gap === '8px' && near(m.hub.box.height,42) && near(m.hub.icon.width,20) && near(m.hub.icon.height,20) && near(m.hub.icon.y+10,m.hub.label.y+m.hub.label.height/2) && near(m.hub.box.x+m.hub.box.width/2,m.hub.axis.x+m.hub.axis.width/2) && m.hub.paths === 3, `${name}: hub font/padding/icon/axis alignment`, m.hub);
|
||||
check(m.hub.font.fontSize === '21px' && m.hub.font.fontWeight === '700' && m.hub.font.lineHeight === '24px' && m.hub.paddingLeft === '22px' && m.hub.paddingRight === '24px' && m.hub.gap === '8px' && near(m.hub.box.height,42) && near(m.hub.icon.width,20) && near(m.hub.icon.height,20) && near(m.hub.icon.y+10,m.hub.label.y+m.hub.label.height/2) && near(m.hub.box.x+m.hub.box.width/2,m.hub.axis.x+m.hub.axis.width/2) && m.hub.paths === 3, `${name}: hub font/padding/icon/axis alignment`, m.hub);
|
||||
check(m.cells.every(c => c.whiteSpace === 'nowrap' && c.textOverflow !== 'ellipsis' && c.ranges.length === 1 && c.ranges[0].x >= c.box.x + parseFloat(c.paddingLeft) - .5 && c.ranges[0].x+c.ranges[0].width <= c.box.x+c.box.width-parseFloat(c.paddingRight)+.5), `${name}: every table label fits on one line without clipping`, m.cells.filter(c => c.ranges.length !== 1 || c.ranges[0].width > c.box.width-parseFloat(c.paddingLeft)-parseFloat(c.paddingRight)+.5));
|
||||
check(near(m.table.box.width,330,1) && m.table.total === '41' && m.footer.includes('41'), `${name}: table width/count and footer retained`, { table:m.table.box,total:m.table.total });
|
||||
check(m.document.scrollWidth === m.document.clientWidth && m.document.scrollHeight === m.document.clientHeight && !m.outside.length, `${name}: no page scroll; every card inside stage`, m.document);
|
||||
|
||||
@@ -191,7 +191,7 @@ function verify(value, label, viewport) {
|
||||
check(assetCards.every(card => near(card.rowGap, 0) && card.badgeInRow && (card.badgeGap === null || near(card.badgeGap, 4))), `${label}: role and badge share one row, gap4; position/name gap0`, assetCards.map(({ id, rowGap, badgeGap, badgeInRow }) => ({ id, rowGap, badgeGap, badgeInRow })));
|
||||
check(value.zeroSpacingSerialization.authored === '0px' && assetCards.filter(card => card.badge).every(card => {
|
||||
const small = card.size === 'employee', style = card.badgeStyle;
|
||||
return style.borderTopWidth === '1px' && style.borderRadius === '4px' && style.letterSpacing === value.zeroSpacingSerialization.computed && style.fontSize === (small ? '8.5px' : '9px') && style.fontWeight === (small ? '600' : '700') && style.lineHeight === (small ? '8.5px' : '9px') && style.paddingLeft === (small ? '2px' : '3px') && style.paddingRight === (small ? '2px' : '3px');
|
||||
return style.borderTopWidth === '1px' && style.borderRadius === '4px' && style.letterSpacing === value.zeroSpacingSerialization.computed && style.fontSize === (small ? '8.5px' : '9px') && style.fontWeight === (small ? '500' : '700') && style.lineHeight === (small ? '8.5px' : '9px') && style.paddingLeft === (small ? '2px' : '3px') && style.paddingRight === (small ? '2px' : '3px');
|
||||
}), `${label}: badge source border/radius/font restored`, { zeroSpacingSerialization: value.zeroSpacingSerialization, badges: assetCards.filter(card => card.badge).map(({ id, badgeStyle }) => ({ id, badgeStyle })) });
|
||||
const tagColors = { 'business-planning': 'rgb(37, 78, 126)', 'talent-growth': 'rgb(14, 103, 200)', 'integrated-system': 'rgb(110, 71, 160)', 'design-planning': 'rgb(6, 154, 149)', 'solution-sales': 'rgb(81, 139, 53)' };
|
||||
check(assetCards.filter(card => card.badge).every(card => card.badgeStyle.color === (tagColors[card.asset] ?? 'rgb(14, 84, 146)')), `${label}: organization-specific badge source colors`, assetCards.filter(card => card.badge).map(({ id, asset, badgeStyle }) => ({ id, asset, color: badgeStyle.color })));
|
||||
@@ -208,9 +208,9 @@ function verify(value, label, viewport) {
|
||||
check(value.headers.filter(header => header.depth >= 2).every(header => {
|
||||
let id = header.id; while (depthOf(id) > 1) id = organizationById.get(id).parentId;
|
||||
const expected = headerGradients[id], hr = id === 'talent-growth';
|
||||
return expected?.every(color => header.style.backgroundImage.includes(color)) && header.style.fontSize === (hr ? '18px' : '20px') && header.style.fontWeight === (hr ? '600' : '700') && header.style.lineHeight === (hr ? '16.9px' : '19.7px');
|
||||
return expected?.every(color => header.style.backgroundImage.includes(color)) && header.style.fontSize === (hr ? '18px' : '20px') && header.style.fontWeight === '700' && header.style.lineHeight === (hr ? '16.9px' : '19.7px');
|
||||
}), `${label}: deeper headers inherit original parent color and source typography`, value.headers.filter(header => header.depth >= 2).map(({ id, style }) => ({ id, style })));
|
||||
check(value.hub.paths === 3 && near(value.hub.iconBox?.width, 20) && near(value.hub.iconBox?.height, 20) && near(value.hub.gap, 8) && value.hub.style.fontSize === '21px' && value.hub.style.fontWeight === '750' && value.hub.style.lineHeight === '24px', `${label}: original hub icon and source text metrics`, value.hub);
|
||||
check(value.hub.paths === 3 && near(value.hub.iconBox?.width, 20) && near(value.hub.iconBox?.height, 20) && near(value.hub.gap, 8) && value.hub.style.fontSize === '21px' && value.hub.style.fontWeight === '700' && value.hub.style.lineHeight === '24px', `${label}: original hub icon and source text metrics`, value.hub);
|
||||
check(value.hub.iconBox && value.hub.labelBox && [value.hub.iconBox, value.hub.labelBox].every(box => box.x >= value.hub.box.x - 0.5 && box.y >= value.hub.box.y - 0.5 && box.x + box.width <= value.hub.box.x + value.hub.box.width + 0.5 && box.y + box.height <= value.hub.box.y + value.hub.box.height + 0.5), `${label}: hub icon and label remain inside source pill`, value.hub);
|
||||
check(value.status.every(group => group.heading === (group.status === 'dispatched' ? '파견' : '휴직')) && value.cards.filter(card => card.statusGroup).every(card => card.statusLabels === 0), `${label}: compact status headers retained and below-name labels removed`, value.status);
|
||||
check(value.table?.title === '직위별 인원 현황 (팀별)' && JSON.stringify(value.table?.header) === JSON.stringify(['팀원', ...positions, '소계']) && JSON.stringify(value.table?.rows) === JSON.stringify(expectedRows) && JSON.stringify(value.table?.total) === JSON.stringify(expectedTotal) && fixture.employees.length === 41, `${label}: position matrix cross-counts original 41 employees`, { actual: value.table, expectedRows, expectedTotal });
|
||||
|
||||
Reference in new issue
Block a user