fix(sw): stabilize theme regression test and update integration docs
This commit is contained in:
1 parent
73e30242f2
commit
47972c55ff
11 files changed
+186
-23
No files matched your search
@@ -5,7 +5,8 @@
|
||||
// 2) Regression: every existing SW Story renders identically with the new cascade and with the legacy cascade
|
||||
// (token blocks re-declared on every `.hm-sw-header, .hm-sw-theme`, i.e. the code before this change): computed
|
||||
// font / color / geometry of every SW element and an element screenshot comparison. No Story sets an override,
|
||||
// so any difference would be a regression.
|
||||
// so any difference would be a regression. Pixels are compared with a control shot taken after the CURRENT token
|
||||
// blocks are injected once more (2026-10-20): re-declaring them alone re-rasterizes a few gradient pixels.
|
||||
// Usage: node packages/sw/tests/theme-inheritance.browser.mjs <storybook-url> <out-dir>
|
||||
import { createRequire } from 'node:module'
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||
@@ -137,13 +138,19 @@ const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${fa
|
||||
|
||||
// ---------- 2. Regression: new cascade vs legacy cascade on every existing SW Story ----------
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const legacy = (await Promise.all(['sw.css', 'sw-viewer.css', 'sw-footer.css'].map(file => readFile(path.join(here, '../src/theme', file), 'utf8'))))
|
||||
const outermost = ':is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *)'
|
||||
const tokenBlocks = (await Promise.all(['sw.css', 'sw-viewer.css', 'sw-footer.css'].map(file => readFile(path.join(here, '../src/theme', file), 'utf8'))))
|
||||
.map(text => {
|
||||
const start = text.indexOf(':is(.hm-sw-header, .hm-sw-theme):not(:where(.hm-sw-header, .hm-sw-theme) *) {')
|
||||
const start = text.indexOf(`${outermost} {`)
|
||||
const end = text.indexOf('\n}', start)
|
||||
if (start < 0 || end < 0) throw new Error('token block not found')
|
||||
return `.hm-sw-header,\n.hm-sw-theme ${text.slice(text.indexOf('{', start), end + 2)}`
|
||||
}).join('\n')
|
||||
return text.slice(text.indexOf('{', start), end + 2)
|
||||
})
|
||||
const legacy = tokenBlocks.map(block => `.hm-sw-header,\n.hm-sw-theme ${block}`).join('\n')
|
||||
// Control (2026-10-20): the SAME token blocks on the SAME selector, i.e. the current cascade declared once more.
|
||||
// Re-declaring the token blocks re-rasterizes a few gradient pixels (±1–2 per channel at the end of the Footer
|
||||
// command row) even though no value changes, so the legacy shot is also compared with this control shot.
|
||||
const control = tokenBlocks.map(block => `${outermost} ${block}`).join('\n')
|
||||
const index = await (await fetch(`${storybook}/index.json`)).json()
|
||||
const ids = Object.values(index.entries).filter(entry => entry.type === 'story' && /^sw-(header|popup|viewer|footer|home-menu)/.test(entry.id)).map(entry => entry.id)
|
||||
const snapshot = page => page.evaluate(() => [...document.querySelectorAll('.hm-sw-header, .hm-sw-theme')].flatMap(root => [root, ...root.querySelectorAll('*')]).map(element => {
|
||||
@@ -161,21 +168,37 @@ for (const width of [1920, 1440, 1180, 900, 560]) for (const id of ids) {
|
||||
await page.waitForTimeout(150)
|
||||
// Pixels that change between two shots of the SAME CSS (live Story content) are masked out as noise.
|
||||
const shotA2 = await shot()
|
||||
// Control: the current cascade declared again. Its computed values must equal the first state.
|
||||
await page.addStyleTag({ content: control })
|
||||
await page.waitForTimeout(150)
|
||||
const c = await snapshot(page)
|
||||
const shotC = await shot()
|
||||
await page.waitForTimeout(150)
|
||||
const shotC2 = await shot()
|
||||
await page.addStyleTag({ content: legacy })
|
||||
await page.waitForTimeout(150)
|
||||
const b = await snapshot(page)
|
||||
const shotB = await shot()
|
||||
const changed = a.length === b.length ? a.filter((value, i) => value !== b[i]).length : -1
|
||||
let pixels = -1, noise = 0
|
||||
if (shotA.width === shotB.width && shotA.height === shotB.height && shotA2.height === shotA.height) {
|
||||
const changedValues = (x, y) => x.length === y.length ? x.filter((value, i) => value !== y[i]).length : -1
|
||||
const changed = changedValues(a, b), controlChanged = changedValues(a, c)
|
||||
let pixels = -1, noise = 0, controlPixels = -1
|
||||
if ([shotA2, shotC, shotC2, shotB].every(item => item.width === shotA.width && item.height === shotA.height)) {
|
||||
pixels = 0
|
||||
controlPixels = 0
|
||||
for (let i = 0; i < shotA.data.length; i += 4) {
|
||||
const same = (x, y) => x[i] === y[i] && x[i + 1] === y[i + 1] && x[i + 2] === y[i + 2] && x[i + 3] === y[i + 3]
|
||||
if (!same(shotA.data, shotA2.data)) noise++
|
||||
else if (!same(shotA.data, shotB.data)) pixels++
|
||||
if (!same(shotA.data, shotC.data)) controlPixels++
|
||||
// Live Story content changing at rest (before or after the control) is masked out as noise.
|
||||
if (!same(shotA.data, shotA2.data) || !same(shotC.data, shotC2.data)) noise++
|
||||
// First shot and control shot both render the unchanged cascade (re-raster artifacts appear only in the
|
||||
// control; a transient relayout may appear in either one). The legacy cascade fails when its pixel matches
|
||||
// neither of them exactly.
|
||||
else if (!same(shotC.data, shotB.data) && !same(shotA.data, shotB.data)) pixels++
|
||||
}
|
||||
}
|
||||
regression.push({ id, width, elements: a.length, changedElements: changed, diffPixels: pixels, maskedNoisePixels: noise, result: changed === 0 && pixels === 0 ? 'PASS' : 'FAIL' })
|
||||
// Computed values: legacy vs first state (and control vs first state). Pixels: legacy vs first state and control.
|
||||
regression.push({ id, width, elements: a.length, changedElements: changed, controlChangedElements: controlChanged, diffPixels: pixels,
|
||||
controlRasterPixels: controlPixels, maskedNoisePixels: noise, result: changed === 0 && controlChanged === 0 && pixels === 0 ? 'PASS' : 'FAIL' })
|
||||
await page.close()
|
||||
}
|
||||
// Negative control: the legacy cascade must reproduce the reported defect on the themed Story (the comparison above is
|
||||
|
||||
Reference in new issue
Block a user