// 2026-10-20 SW hover position stability + header field text + footer command ellipsis / tooltip.
// Hover (and Active / Selected) may change only color, background and the hover ring: the icon, the text, their gap
// and the item box stay where they are (Header Home Menu tiles, Footer command slots in every story and category,
// including the ▲ panel). Also: SWHeader field values are Noto Sans KR 400 and not cut; long footer commands end in an
// ellipsis inside the unchanged 68 × 47 slot and show their full name on hover.
// Usage: build Storybook first, then node packages/sw/tests/hover-stability.browser.mjs [evidence-dir] [chromium|firefox]
import { readFile, writeFile, mkdir } from 'node:fs/promises'
import path from 'node:path'
import http from 'node:http'
import { runtimeRequire } from './playwright-runtime.mjs'
const require = runtimeRequire()
const playwright = require('playwright')
const out = path.resolve(process.argv[2] ?? 'docs/sw/hover-stability-qa'); await mkdir(out, { recursive: true })
const engine = process.argv[3] ?? 'chromium'
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)); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } })
await new Promise(r => server.listen(0, '127.0.0.1', r)); const base = `http://127.0.0.1:${server.address().port}`
const browser = engine === 'firefox'
? await playwright.firefox.launch({ headless: true })
: await playwright.chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
const checks = []; const check = (n, ok, d) => { checks.push({ n, ok, d }); console.log(ok ? 'PASS' : 'FAIL', n, ok ? '' : JSON.stringify(d).slice(0, 600)) }
const report = { engine }
let errors = []
const page = await browser.newPage({ viewport: { width: 1920, height: 1100 } })
page.on('pageerror', e => errors.push(e.message)); page.on('console', m => { if (m.type() === 'error') errors.push(m.text()) })
const goto = async id => { errors = []; await page.goto(`${base}/iframe.html?id=${id}&viewMode=story`); await page.locator('#storybook-root > *').first().waitFor(); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(300) }
// Sub-pixel rounding (e.g. a centered odd-width box) is not a layout move: positions are compared to 0.01px.
const EPS = 0.01
// Geometry of one item: the box, the visible icon art (the
/