- A: SWViewerIcon draws the SWHeader black outline as SVG stroke on a copy behind the icon (no CSS filter) - B: SWViewerPanel renders the common SWPopup frame (same DOM / CSS) and adds drag, corner resize, collapse, bounds and modal; SWPopup gains optional titleActions / footer; title art spans any width - C: shared Story canvas grid (--sw-story-canvas) behind the SW Header work area and dropdown room - D: SWViewerButton tone="green" for 전체 켜기 / 전체 끄기: default kept, hover = primary (확인) hover token, pressed = primary face - AI guide, verification scripts (76 checks, element regression) and evidence updated
63 lines
4.2 KiB
JavaScript
63 lines
4.2 KiB
JavaScript
// Regression for the SW viewer work (2026-10-19): the existing SW/Header and SW/Popup Stories must render their
|
|
// headers and popups pixel-identical before (base Storybook) and after (this branch) at several widths.
|
|
// Element mode compares every .hm-sw-header / .hm-sw-property element; the full page is reported for information
|
|
// (the 2026-10-19 follow-up changed the Story work-area background to the shared grid canvas on purpose).
|
|
// Usage: node packages/sw/tests/viewer-regression.browser.mjs <base-storybook-url> <new-storybook-url> <out-dir>
|
|
import { createRequire } from 'node:module'
|
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
|
|
const runtime = process.env.SW_RUNTIME_MODULES ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules'
|
|
const require = createRequire(path.join(runtime, 'index.js'))
|
|
const { chromium } = require('playwright')
|
|
const { PNG } = require('pngjs')
|
|
const pixelmatch = require('pixelmatch').default ?? require('pixelmatch')
|
|
const [base, next, outArg, mode] = process.argv.slice(2)
|
|
// --neutral-canvas: on the new side only, put back the pre-grid Story backgrounds (white work area, page color under
|
|
// dropdowns), so semi-transparent header / dropdown edge pixels compare against the same backdrop.
|
|
const neutral = mode === "--neutral-canvas"
|
|
const out = path.resolve(outArg ?? 'docs/sw/viewer-qa/regression')
|
|
await mkdir(out, { recursive: true })
|
|
const ids = ['overview', 'interactive-states', 'logo-slot', 'document-dropdown', 'responsive-more', 'k-guide', 'property-panel-compare', 'composed-controls', 'document-open', 'hidden-sub-controls', 'detail-buttons'].map(id => `sw-header--${id}`)
|
|
.concat(['sw-popup--custom-content', 'sw-popup--multiple-popups'])
|
|
const widths = [1920, 1180, 768]
|
|
const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
|
|
const shot = async (url, width, restore = false) => {
|
|
const page = await browser.newPage({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' })
|
|
await page.goto(url)
|
|
await page.waitForSelector('#storybook-root > *')
|
|
if (restore) await page.addStyleTag({ content: '.sw-overview-stage { background: var(--hm-sem-bg-surface) !important; } .sw-story-scroller--menu { background: none !important; }' })
|
|
await page.evaluate(() => document.fonts.ready)
|
|
await page.waitForTimeout(400)
|
|
const full = PNG.sync.read(await page.screenshot({ fullPage: true, animations: 'disabled' }))
|
|
const elements = []
|
|
for (const element of await page.locator('.hm-sw-header, .hm-sw-property').all()) {
|
|
if (!(await element.isVisible())) continue
|
|
elements.push(PNG.sync.read(await element.screenshot({ animations: 'disabled' })))
|
|
}
|
|
await page.close()
|
|
return { full, elements }
|
|
}
|
|
const compare = (a, b, out) => {
|
|
if (a.width !== b.width || a.height !== b.height) return -1
|
|
const image = new PNG({ width: a.width, height: a.height })
|
|
const diff = pixelmatch(a.data, b.data, image.data, a.width, a.height, { threshold: 0 })
|
|
if (diff && out) return { diff, image }
|
|
return diff
|
|
}
|
|
const results = []
|
|
for (const id of ids) for (const width of widths) {
|
|
const url = root => `${root}/iframe.html?id=${id}&viewMode=story`
|
|
const [a, b] = [await shot(url(base), width), await shot(url(next), width, neutral)]
|
|
const elementDiffs = a.elements.length === b.elements.length ? a.elements.map((item, index) => compare(item, b.elements[index])) : [-1]
|
|
const fullDiff = compare(a.full, b.full)
|
|
const pass = elementDiffs.every(diff => diff === 0)
|
|
results.push({ id, width, elements: a.elements.length + ' / ' + b.elements.length, elementDiffPixels: elementDiffs, fullPageDiffPixels: fullDiff, result: pass ? 'PASS' : 'FAIL' })
|
|
console.log(`${pass ? 'PASS' : 'FAIL'} ${id} @${width} elements ${a.elements.length}/${b.elements.length} diff [${elementDiffs.join(',')}] full-page diff ${fullDiff}`)
|
|
}
|
|
await browser.close()
|
|
await writeFile(path.join(out, neutral ? 'regression-neutral-canvas.json' : 'regression.json'), JSON.stringify({ base, next, results }, null, 2) + '\n')
|
|
const failed = results.filter(item => item.result !== 'PASS').length
|
|
console.log(`${results.length - failed}/${results.length} PASS`)
|
|
process.exit(failed ? 1 : 0)
|