- SWViewerHeader: data-driven viewer menu (button / toggle / menu / tabs / switch / custom), More by collapseBelow, trailing slot, onCommand; SWHeader logo props with no default logo - SWViewerMenu: dropdown, check / radio, multi-level submenu, context menu at a point - SWViewerPanel: viewer popup frame with free children / footer, drag, corner resize, collapse, modal - Panel controls: SWViewerSection / Field / Hint / Switch / Slider / Input / Button / Badge / Status - SWHeader black text outline tokens on viewer text, matching icon outline (drop-shadow) - Storybook SW/Viewer stories; remove SW/Tool story (SW/Popup kept) - AI guide ai/VIEWER.md, example H, verification scripts and evidence
48 lines
2.8 KiB
JavaScript
48 lines
2.8 KiB
JavaScript
// Regression for the SW viewer work (2026-10-19): the existing SW/Header Stories must render pixel-identical
|
|
// before (base Storybook) and after (this branch) at several widths.
|
|
// 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] = process.argv.slice(2)
|
|
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']
|
|
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) => {
|
|
const page = await browser.newPage({ viewport: { width, height: 1000 }, reducedMotion: 'reduce' })
|
|
await page.goto(url)
|
|
await page.waitForSelector('#storybook-root > *')
|
|
await page.evaluate(() => document.fonts.ready)
|
|
await page.waitForTimeout(400)
|
|
const buffer = await page.screenshot({ fullPage: true, animations: 'disabled' })
|
|
await page.close()
|
|
return PNG.sync.read(buffer)
|
|
}
|
|
const results = []
|
|
for (const id of ids) for (const width of widths) {
|
|
const url = root => `${root}/iframe.html?id=sw-header--${id}&viewMode=story`
|
|
const [a, b] = [await shot(url(base), width), await shot(url(next), width)]
|
|
const same = a.width === b.width && a.height === b.height
|
|
let diff = same ? 0 : -1
|
|
if (same) {
|
|
const image = new PNG({ width: a.width, height: a.height })
|
|
diff = pixelmatch(a.data, b.data, image.data, a.width, a.height, { threshold: 0 })
|
|
if (diff) await writeFile(path.join(out, `${id}-${width}-diff.png`), PNG.sync.write(image))
|
|
}
|
|
results.push({ id, width, size: `${a.width}x${a.height} / ${b.width}x${b.height}`, diffPixels: diff, result: diff === 0 ? 'PASS' : 'FAIL' })
|
|
console.log(`${diff === 0 ? 'PASS' : 'FAIL'} sw-header--${id} @${width} diff ${diff}`)
|
|
}
|
|
await browser.close()
|
|
await writeFile(path.join(out, '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)
|