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;