Files
HMDesign/scripts/verify-quality-reference.mjs
T
shkim4 4c29894e56 refactor(storybook): simplify component and preview examples
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.
2026-10-23 00:28:05 +09:00

85 lines
6.8 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(process.env.HM_PLAYWRIGHT_DIR ? path.join(process.env.HM_PLAYWRIGHT_DIR, 'package.json') : import.meta.url);
const { chromium } = require('playwright');
const browser = await chromium.launch({ headless: true, ...(process.env.HM_CHROME_PATH ? { executablePath: process.env.HM_CHROME_PATH } : {}) });
const out = '_local/quality-20261010';
const results = [];
const selected = process.argv.slice(2);
const base = 'http://127.0.0.1:6322';
const task = async (name, fn) => { if (selected.length && !selected.includes(name)) return; try { const evidence = await fn(); results.push({ name, status: 'PASS', evidence }); } catch (e) { results.push({ name, status: 'FAIL', error: e.message }); } console.log(JSON.stringify(results.at(-1))); };
const assert = (ok, message) => { if (!ok) throw Error(message); };
try {
for (const [name, route] of [['APP', '/'], ['PUBLISHER', '/publisher/'], ['DASHBOARD', '/dashboard/'], ['RECRUIT', '/recruit/']]) {
await task(`${name}_RESPONSIVE_RUNTIME`, async () => {
const evidence = [];
for (const width of [390, 768, 1280, 1440]) {
const page = await browser.newPage({ viewport: { width, height: 900 } });
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto(base + route, { waitUntil: 'networkidle' });
await page.locator('#root [data-slot]:visible').first().waitFor();
const result = await page.evaluate(() => {
const frame = document.querySelector('[data-slot="dashboard-layout"]');
const scroll = document.querySelector('[data-slot="dashboard-scroll"]');
return { overflow: document.documentElement.scrollWidth > innerWidth + 1, hmElements: document.querySelectorAll('[data-slot]').length, frameWidth: frame?.getBoundingClientRect().width, headerHeight: document.querySelector('[data-slot="dashboard-header"]')?.getBoundingClientRect().height, internalOverflow: scroll ? scroll.scrollWidth > scroll.clientWidth : null };
});
await page.screenshot({ path: `${out}/existing-${name}-${width}.png`, fullPage: true });
await page.close();
assert(!result.overflow && !errors.length, `${width}: overflow / browser errors ${JSON.stringify(errors)}`);
if (result.frameWidth) assert(result.frameWidth >= 960 && result.headerHeight === 48, 'Dashboard geometry');
evidence.push({ width, ...result, errors });
}
return evidence;
});
}
await task('FILTERBAR_REFERENCE_USAGE', async () => {
const page = await browser.newPage();
await page.goto(`${base}/storybook/iframe.html?id=patterns-data-filter-bar--draft-applied&viewMode=story`, { waitUntil: 'networkidle' });
const status = page.getByRole('status');
await page.getByRole('searchbox', { name: '검색어' }).fill('김');
assert((await status.innerText()).includes('입력한 조건 1개 · 조회 적용 조건 0개'), 'draft / applied distinction');
await page.getByRole('button', { name: '조회', exact: true }).click(); assert((await status.innerText()).includes('조회 적용 조건 1개'), 'reference query');
await page.getByRole('button', { name: '초기화', exact: true }).click(); assert((await status.innerText()).includes('입력한 조건 0개 · 조회 적용 조건 0개'), 'reference reset');
const evidence = [];
for (const width of [390, 768, 1280, 1440]) {
await page.setViewportSize({ width, height: 900 });
const metrics = await page.locator('[data-slot="filter-bar"]').evaluate(e => ({ layout: e.dataset.layout, gap: getComputedStyle(e).gap, justify: getComputedStyle(e).justifyContent, wrap: getComputedStyle(e).flexWrap }));
assert(metrics.layout === 'list-extended' && metrics.gap === '8px' && metrics.wrap === 'wrap' && metrics.justify === 'flex-start', 'FilterBar composition');
await page.screenshot({ path: `${out}/filter-reference-${width}.png`, fullPage: true }); evidence.push({ width, ...metrics });
}
await page.close(); return evidence;
});
await task('THEME_DESIGNER', async () => {
const page = await browser.newPage();
await page.goto(`${base}/storybook/iframe.html?id=theme-designer-playground--live-verification&viewMode=story`, { waitUntil: 'networkidle' });
await page.waitForFunction(() => /^(PASS|FAIL)/.test(document.querySelector('[data-testid="designer-result"]')?.textContent ?? ''), { timeout: 20000 });
const text = await page.locator('[data-testid="designer-result"]').innerText(); assert(text.startsWith('PASS'), text);
await page.close(); return { text };
});
await task('THEME_JSON_LOADER', async () => {
const page = await browser.newPage();
await page.goto(`${base}/storybook/iframe.html?id=theme-designer-playground--editor&viewMode=story`, { waitUntil: 'networkidle' });
await page.getByRole('button', { name: 'Load Service Example JSON', exact: true }).click();
await page.waitForFunction(() => document.querySelector('[aria-label="Primary / Brand HEX"]')?.value !== '#123328');
const changed = await page.getByRole('textbox', { name: 'Primary / Brand HEX', exact: true }).inputValue();
await page.getByRole('button', { name: 'Load Default JSON', exact: true }).click();
await page.waitForFunction(() => document.querySelector('[aria-label="Primary / Brand HEX"]')?.value === '#123328');
await page.close(); return { servicePrimary: changed, defaultPrimary: '#123328' };
});
await task('RUNTIME_THEME_KEYBOARD', async () => {
const page = await browser.newPage();
await page.goto(`${base}/storybook/iframe.html?id=theme-runtime-contract--verification&viewMode=story`, { waitUntil: 'networkidle' });
await page.waitForFunction(() => /^(PASS|FAIL)/.test(document.querySelector('[data-testid="result"]')?.textContent ?? ''), { timeout: 20000 });
const auto = await page.locator('[data-testid="result"]').innerText(); assert(auto.startsWith('PASS'), auto);
const run = page.getByRole('button', { name: 'Run verification', exact: true });
await run.focus(); await page.keyboard.press('Tab'); await page.keyboard.press('Shift+Tab'); await page.keyboard.press('Enter');
await page.waitForFunction(() => /^(PASS|FAIL)/.test(document.querySelector('[data-testid="result"]')?.textContent ?? '') && document.querySelector('[data-testid="result"]')?.dataset.running !== 'true', { timeout: 20000 });
const keyboard = await page.locator('[data-testid="result"]').innerText(); assert(keyboard.startsWith('PASS') && !keyboard.includes('KEYBOARD_FOCUS: NOT VERIFIED'), keyboard);
await page.close(); return { auto, keyboard };
});
} finally { await browser.close(); }
fs.writeFileSync(`${out}/${selected.length ? 'reference-' + selected.join('-') : 'reference-runtime'}.json`, JSON.stringify(results, null, 2));
if (results.some(r => r.status === 'FAIL')) process.exitCode = 1;