Files
HMDesign/packages/sw/tests/integrated-visual.browser.mjs
T
shkim4andClaude Opus 5.5 55179b4577 feat(sw): integrated improvement 0.2.0 (popup content, header open, tool buttons)
SW package 0.2.0 integrated improvement (Task A, steps 1-8). All new props are
optional; existing SW Stories are pixel-identical to the previous build (28/28).

- Build reproducibility: dist text normalized to LF, dist/build-info.json
  (commit, dirty, tool versions, no timestamp), packages/sw/.gitattributes
  (eol=lf). LF checkouts give identical dist and tarball.
- SWHeader: documentOpenPlacement ("menu" default | "header": Open right of
  the logo in the existing document-button look, reusing onDocumentOpen and
  documentOpenLabel, no duplicate Open in the dropdown), leading slot,
  hiddenControls sub-items documentNew / documentOpen / layerFreeze /
  layerLock. Open moves into More on measured overflow (with hysteresis).
- showNew / showOpen (SWHeaderDocumentTitle), showFreeze / showLock (layer
  menu, field, property window list).
- SWToolButton (text / pill / document) and SWToolGroup (radio selection),
  reusing the existing toggle, pill and file-name looks.
- SWPopup / SWPopupSection / SWPopupRow on the shared property window frame;
  SWHeaderPropertyPanel gains children, title, autoFocus and optional
  selection / properties / groups. Escape stack for several popups.
- Field dropdowns inside a popup use the shared fixed overlay
  (useSWFixedDropdownPosition), so the scrolling body no longer clips them.
- AI guide, examples E/F, README, Storybook (SW/Header/Options, SW/Tool,
  SW/Popup) and verification record docs/sw/sw-integrated-improvement-2026-10-19.md.
- Regression scripts rescoped for the intended changes (unused-removal 81/81,
  overview 52/54 with the two pre-existing historical assertions).

Known limitation (unresolved): tools / leading content (incl. SWToolGroup)
does not move into More. With a wide tool bar the configured fields shrink
instead; in Example F the document name and layer field reach 0px at 390px.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:49:19 +09:00

72 lines
5.2 KiB
JavaScript

// Visual preservation for the SW integrated improvement (2026-10-19).
// capture: screenshots every existing SW Story (1920) and the Header Stories at 10 widths from storybook-static.
// compare: pixel-diffs two capture directories and writes compare-result.json.
// Usage: node packages/sw/tests/integrated-visual.browser.mjs capture <dir>
// node packages/sw/tests/integrated-visual.browser.mjs compare <before-dir> <after-dir> <out-dir>
import { createRequire } from 'node:module'
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises'
import path from 'node:path'
import http from 'node:http'
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 [mode, a, b, c] = process.argv.slice(2)
// Stories that existed before the improvement; IDs are fixed so new Stories never enter the comparison.
export const baselineStories = ['overview', 'detail-buttons', 'interactive-states', 'window-move-motion', 'logo-slot', 'document-dropdown', 'responsive-more', 'k-guide', 'property-panel-compare', 'composed-controls']
const widthStories = ['overview', 'responsive-more']
const widths = [1920, 1800, 1440, 1180, 1080, 900, 768, 640, 520, 390]
if (mode === 'capture') {
const out = path.resolve(a)
await mkdir(out, { recursive: true })
const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ttf': 'font/ttf', '.woff2': 'font/woff2' }
const server = http.createServer(async (req, res) => { try { const p = new URL(req.url, 'http://localhost').pathname, file = path.resolve(root, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); if (path.relative(root, file).startsWith('..')) return res.writeHead(403).end(); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } })
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
const base = `http://127.0.0.1:${server.address().port}`
const { chromium } = require('playwright')
const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
const page = await browser.newPage({ viewport: { width: 1920, height: 1000 }, reducedMotion: 'reduce' })
const errors = []
page.on('pageerror', e => errors.push(e.message))
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()) })
const shots = []
const shoot = async (id, width) => {
await page.setViewportSize({ width, height: 1000 })
await page.goto(`${base}/iframe.html?id=sw-header--${id}&viewMode=story`)
await page.locator('.hm-sw-header, .hm-sw-theme').first().waitFor()
await page.evaluate(() => document.fonts.ready)
await page.mouse.move(1, 1)
await page.addStyleTag({ content: '*,*::before,*::after{animation-play-state:paused!important;animation-delay:0s!important;transition:none!important;caret-color:transparent!important}' })
await page.waitForTimeout(150)
const file = `${id}-${width}.png`
await page.screenshot({ path: path.join(out, file), fullPage: true })
shots.push(file)
}
for (const id of baselineStories) await shoot(id, 1920)
for (const id of widthStories) for (const width of widths.slice(1)) await shoot(id, width)
await writeFile(path.join(out, 'capture.json'), JSON.stringify({ browser: browser.version(), shots, errors }, null, 2))
await browser.close(); server.close()
console.log(`captured ${shots.length}, errors ${errors.length}`)
} else if (mode === 'compare') {
const { PNG } = require('pngjs')
const pixelmatch = require('pixelmatch').default ?? require('pixelmatch')
const before = path.resolve(a), after = path.resolve(b), out = path.resolve(c)
await mkdir(out, { recursive: true })
const results = []
for (const file of (await readdir(before)).filter(name => name.endsWith('.png'))) {
let next
try { next = PNG.sync.read(await readFile(path.join(after, file))) } catch { results.push({ file, status: 'MISSING' }); continue }
const prev = PNG.sync.read(await readFile(path.join(before, file)))
if (prev.width !== next.width || prev.height !== next.height) { results.push({ file, status: 'SIZE', before: [prev.width, prev.height], after: [next.width, next.height] }); continue }
const diff = new PNG({ width: prev.width, height: prev.height })
const changed = pixelmatch(prev.data, next.data, diff.data, prev.width, prev.height, { threshold: 0 })
if (changed) await writeFile(path.join(out, `diff-${file}`), PNG.sync.write(diff))
results.push({ file, status: changed ? 'DIFF' : 'SAME', changed })
}
const summary = { total: results.length, same: results.filter(r => r.status === 'SAME').length, results }
await writeFile(path.join(out, 'compare-result.json'), JSON.stringify(summary, null, 2))
console.log(`${summary.same}/${summary.total} identical`)
for (const r of results.filter(r => r.status !== 'SAME')) console.log(r.status, r.file, r.changed ?? '')
} else throw new Error('mode: capture | compare')