Files
HMDesign/packages/sw/tests/viewer-regression.browser.mjs
shkim4 af7f98d66b test(sw): stabilize Firefox hover, pixel and timing checks
- hover-stability-firefox: instant scroll and state waits instead of a fixed 60ms delay (smooth scroll in preview).
- integrated-visual, viewer-regression, theme-inheritance: deterministic Chromium raster flags, animations held at
  time 0, layout settle wait, integer instant scroll of inner panels; tolerance stays 0.
- text-weight: wait for the Viewer More panel instead of a fixed 200ms and fix the failure report.
2026-10-21 06:08:31 +09:00

73 lines
5.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 { mkdir, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { runtimeRequire } from './playwright-runtime.mjs'
const require = runtimeRequire()
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]
// 2026-10-20: software raster without font hinting / subpixel positioning, as integrated-visual.browser.mjs: two
// captures of the same build otherwise differed in text anti-aliasing (Δ1 areas, 2 × 2 glyph spots). Threshold stays 0.
const browser = await chromium.launch({ headless: true, args: ['--disable-gpu', '--font-render-hinting=none', '--disable-font-subpixel-positioning', '--disable-lcd-text', '--force-color-profile=srgb'], 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
// 2026-10-20: panels inside an inner scroller (Property Panel · 4개 비교) were scrolled into view at varying offsets, so
// two captures of the same build differed by 3–7px. Scroll instantly, keep every scroll offset whole, wait for stable frames.
await element.evaluate(e => new Promise(resolve => {
e.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' })
for (let n = e.parentElement; n; n = n.parentElement) { n.scrollTop = Math.round(n.scrollTop); n.scrollLeft = Math.round(n.scrollLeft) }
let last = '', stable = 0
const tick = () => { const r = e.getBoundingClientRect(), now = [r.x, r.y, r.width, r.height].join(); stable = now === last ? stable + 1 : 0; last = now; stable >= 3 ? resolve() : requestAnimationFrame(tick) }
requestAnimationFrame(tick)
}))
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)