chore(sw): remove selected Storybook demos

This commit is contained in:
shkim4 committed 2026-10-19 10:42:32 +09:00
1 parent 92830fe11d
commit 50bf5bda8e
11 files changed
+2387 -265

No files matched your search

+8 -5
View File
@@ -42,7 +42,7 @@ try {
await page.locator('[data-detail-row="문자스타일 상세"] [data-detail-case="hover"] button').hover()
await page.screenshot({path:path.join(output,'comparison-pointer-hover.png'),fullPage:true})
} else if (mode === 'preservation') {
await goto('controls')
await goto('composition')
for(const width of [390,768,1280,1440]){
await page.setViewportSize({width,height:1000})
const layout=await page.locator('header.hm-sw-header').first().evaluate(h=>({height:h.getBoundingClientRect().height,overflow:h.scrollWidth-h.clientWidth,documentOverflow:document.documentElement.scrollWidth-document.documentElement.clientWidth}))
@@ -54,7 +54,7 @@ try {
const box=await page.locator('.hm-sw-property').first().boundingBox()
check(`preserve property panel ${id}`,box.width===314&&box.height===height,box)
}
await goto('controls')
await goto('composition')
const motion=page.locator('[data-sw-window-move]').first();await motion.hover()
check('preserve window motion 1400ms',await motion.evaluate(e=>e.getAnimations({subtree:true}).length===3&&e.getAnimations({subtree:true}).every(a=>a.effect.getTiming().duration===1400)))
await page.mouse.move(1,900);check('window motion leave clears animations',await motion.evaluate(e=>e.getAnimations({subtree:true}).length===0))
@@ -70,7 +70,8 @@ try {
const ids = Object.values(index.entries).filter(e => e.title === 'SW/Header' && e.type === 'story').map(e => e.id)
report.storyIds = ids
if (mode === 'baseline') await writeFile(path.join(output, 'baseline-story-ids.json'), JSON.stringify(ids))
for (const story of ['default', 'controls']) {
// These retained examples cover fixed-width and fluid headers after the two Args demos were retired.
for (const story of ['overview', 'composition']) {
await goto(story)
for (const width of [1920,1800,1440,1180,1080,900,768,640,520,390]) {
await page.setViewportSize({ width, height:1000 })
@@ -94,9 +95,11 @@ try {
await writeFile(path.join(output,'baseline.json'),JSON.stringify(snapshots,null,2))
} else {
const before=JSON.parse(await readFile(path.join(output,'baseline.json'),'utf8'))
for(const key of Object.keys(before).filter(k=>!k.startsWith('text'))) check(`unchanged header ${key}`,JSON.stringify(before[key])===JSON.stringify(snapshots[key]))
for(const key of Object.keys(snapshots).filter(k=>!k.startsWith('text'))) check(`unchanged header ${key} (capture a current baseline for replacement fixtures)`,JSON.stringify(before[key])===JSON.stringify(snapshots[key]))
const oldIds=JSON.parse(await readFile(path.join(output,'baseline-story-ids.json'),'utf8'))
check('all previous 31 stories retained',oldIds.length===31&&oldIds.every(id=>ids.includes(id)),{before:oldIds.length,after:ids.length})
const retiredIds=['default','controls','responsive','text-stroke','dynamic-data'].map(id=>`sw-header--${id}`)
const retainedIds=oldIds.filter(id=>!retiredIds.includes(id))
check('retained stories remain; only the five approved demos are absent',ids.length===28&&retainedIds.every(id=>ids.includes(id))&&retiredIds.every(id=>!ids.includes(id)),{before:oldIds.length,after:ids.length})
for(const id of ids){await goto(id.replace('sw-header--',''));check(`render ${id}`,!(await page.locator('.sb-errordisplay').isVisible()))}
await goto('composition')
const presets=['기본 전체','구성 A','구성 B (레이어·선스타일)','구성 C (로고만)','파일 목록 제거','레이어 제거','색상 제거','스타일 그룹 제거','여러 메뉴 제거','순서 변경','숨김 + 순서 변경']
@@ -0,0 +1,148 @@
// Scoped verification for the five approved Story removals; serves the production build locally.
// Usage: node packages/sw/tests/storybook-cleanup-stage1.browser.mjs [evidence-dir]
import { createRequire } from 'node:module'
import { readFile, writeFile, mkdir } from 'node:fs/promises'
import { execFileSync } from 'node:child_process'
import path from 'node:path'
import http from 'node:http'
import { parse } from '@babel/parser'
const out = path.resolve(process.argv[2] ?? 'docs/sw/storybook-cleanup-stage1-qa')
await mkdir(out, { recursive: true })
const report = { checks: [], errors: [], baseCommit: '92830fe11db7f37eb6a45e8bbc6a5038ec579649' }
const check = (name, ok, data) => {
report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data })
console.log(ok ? 'PASS' : 'FAIL', name)
}
const retired = ['responsive', 'text-stroke', 'dynamic-data', 'default', 'controls'].map(id => `sw-header--${id}`)
const before = JSON.parse(await readFile(path.join(out, 'before-index.json'), 'utf8'))
const after = Object.values(JSON.parse(await readFile('storybook-static/index.json', 'utf8')).entries)
.filter(e => e.type === 'story' && e.title === 'SW/Header')
await writeFile(path.join(out, 'after-index.json'), JSON.stringify(after, null, 2) + '\n')
report.deleted = before.filter(e => !after.some(a => a.id === e.id)).map(e => ({ name: e.name, id: e.id }))
check('33 -> 28; precisely the five requested IDs removed', before.length === 33 && after.length === 28 &&
report.deleted.length === 5 && retired.every(id => report.deleted.some(e => e.id === id)), report.deleted)
check('all retained names, IDs, ordering and tags unchanged', JSON.stringify(before.filter(e => !retired.includes(e.id)).map(e => [e.id, e.name, e.tags])) ===
JSON.stringify(after.map(e => [e.id, e.name, e.tags])))
// Compare declaration text with the starting commit, including every retained fixture/helper.
const file = 'src/sw/sw-header.stories.tsx'
const original = execFileSync('git', ['show', `${report.baseCommit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n')
const current = (await readFile(file, 'utf8')).replaceAll('\r\n', '\n')
const declarations = text => {
return parse(text, { sourceType: 'module', plugins: ['typescript', 'jsx'] }).program.body.map(statement => {
const n = statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement
return {
name: n.type === 'VariableDeclaration' ? n.declarations.map(d => d.id.name).join(',') :
n.type === 'ImportDeclaration' ? `import:${n.source.value}` : n.id?.name ?? n.type,
text: text.slice(statement.start, statement.end),
}
})
}
const oldDeclarations = new Map(declarations(original).map(d => [d.name, d.text]))
const changed = declarations(current).filter(d => d.name !== 'WindowMoveMotion' && oldDeclarations.get(d.name) !== d.text)
check('every retained declaration unchanged except WindowMoveMotion', changed.length === 0, changed.map(d => d.name))
const section = text => text.match(/<StorySection title="단계별 진행 \(3배 확대\)"[\s\S]*?<\/StorySection>/)?.[0]
check('retained step section is byte-identical after newline normalization', !!section(original) && section(original) === section(current))
const productionDiff = execFileSync('git', ['diff', report.baseCommit, '--', 'packages/sw/src', 'src/sw/sw-stories.css', 'src/sw/assets', '.storybook'], { encoding: 'utf8' })
check('production implementation, CSS, SVG/assets and Storybook configuration unchanged', productionDiff === '')
const root = path.resolve('storybook-static')
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.ttf': 'font/ttf', '.png': 'image/png' }
const server = http.createServer(async (req, res) => {
try {
const pathname = new URL(req.url, 'http://localhost').pathname
const file = path.resolve(root, '.' + decodeURIComponent(pathname === '/' ? '/index.html' : pathname))
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 } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
let browser
try {
browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
report.browser = browser.version()
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } })
page.on('pageerror', e => report.errors.push(e.message))
page.on('console', m => { if (m.type() === 'error') report.errors.push(m.text()) })
const goto = async id => {
await page.goto(`${base}/iframe.html?id=${id.startsWith('sw-header--') ? 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)
}
for (const entry of after) {
await goto(entry.id)
check(`render ${entry.id}`, !(await page.locator('.sb-errordisplay').isVisible()))
}
await goto('window-move-motion')
const titles = await page.locator('section > header h2').allTextContents()
check('only the requested section remains', JSON.stringify(titles) === JSON.stringify(['단계별 진행 (3배 확대)']), titles)
check('removed live demos, frame specimens and callback logs absent', await page.locator('[data-frame-width], [data-callback-log]').count() === 0)
const stages = await page.locator('[data-motion-time]').evaluateAll(elements => elements.map(e => {
const zoom = e.querySelector('.sw-story-zoom'), motion = e.querySelector('[data-sw-window-move]')
const box = zoom.getBoundingClientRect()
return { time: e.dataset.motionTime, zoom: getComputedStyle(zoom).zoom, width: box.width, height: box.height,
animations: motion.getAnimations({ subtree: true }).map(a => ({ state: a.playState, time: a.currentTime, duration: a.effect.getTiming().duration })) }
}))
report.stages = stages
const expected = ['rest', ...[0, 240, 480, 520, 800, 1040, 1600, 1640].map(t => String(t * 1400 / 1640))]
check('nine original steps and 3x magnification preserved', stages.length === 9 && stages.every((s, i) => s.time === expected[i] && Number(s.zoom) === 3 && s.width === 456 && s.height === 144), stages)
check('rest is static; timed specimens pause three 1400ms animations at their original positions', stages[0].animations.length === 0 && stages.slice(1).every(s => s.animations.length === 3 && s.animations.every(a => a.state === 'paused' && a.duration === 1400 && Math.abs(a.time - Number(s.time)) < 0.01)))
await page.screenshot({ path: path.join(out, 'window-move-motion.png'), fullPage: true })
const motion = page.locator('[data-motion-time="rest"] [data-sw-window-move]')
await motion.hover()
const timing = await motion.evaluate(async e => {
const animations = e.getAnimations({ subtree: true })
const before = animations.map(a => a.currentTime)
const started = performance.now()
await new Promise(resolve => { const tick = () => performance.now() - started >= 180 ? resolve() : requestAnimationFrame(tick); requestAnimationFrame(tick) })
return animations.map((a, i) => ({ before: before[i], after: a.currentTime, state: a.playState, duration: a.effect.getTiming().duration, iterations: a.effect.getTiming().iterations === Infinity ? 'infinite' : a.effect.getTiming().iterations }))
})
check('live Hover motion advances with original 1400ms infinite cycle', timing.length === 3 && timing.every(a => a.state === 'running' && a.after > a.before && a.duration === 1400 && a.iterations === 'infinite'), timing)
await page.mouse.down()
check('pointer held pauses motion and hides tooltip', await motion.evaluate(e => e.getAnimations({ subtree: true }).every(a => a.playState === 'paused') && getComputedStyle(e.querySelector('[role="tooltip"]')).visibility === 'hidden'))
await page.mouse.up()
await page.mouse.move(1, 1)
check('Hover leave resets live specimen', await motion.evaluate(e => e.getAnimations({ subtree: true }).length === 0))
await goto('responsive-more')
const header = page.locator('[data-frame-width="1440"] header.hm-sw-header').first()
await header.getByRole('button', { name: '더보기', exact: true }).click()
await header.locator('[role="menuitem"][data-menu="line-style"]').click()
check('Responsive More still switches the representative field and opens its dropdown', await header.locator('.hm-sw-header__line-menu').count() > 0 && await header.locator('.hm-sw-header__line-menu').isVisible())
await page.keyboard.press('Escape')
await goto('dynamic-controls')
await page.getByRole('checkbox', { name: '구성 color', exact: true }).uncheck()
check('Dynamic Controls removes a configured field', await page.locator('header.hm-sw-header [data-control="color"], header.hm-sw-header [data-menu="color"]').count() === 0)
await page.getByRole('checkbox', { name: '구성 color', exact: true }).check()
check('Dynamic Controls restores a configured field', await page.locator('header.hm-sw-header [data-menu="color"]').count() > 0)
await page.getByRole('button', { name: '구성 초기화', exact: true }).click()
const documents = page.locator('header.hm-sw-header .hm-sw-header__document-button')
await documents.click()
await page.getByRole('button', { name: '+ New', exact: true }).click()
await documents.click()
check('host-owned document list can still grow dynamically', await page.getByText('새 문서 1', { exact: true }).count() > 0)
await goto('composition')
for (const width of [390, 768, 1280, 1440]) {
await page.setViewportSize({ width, height: 1000 })
const geometry = await page.locator('header.hm-sw-header').evaluate(e => ({ height: e.getBoundingClientRect().height, overflow: e.scrollWidth - e.clientWidth }))
check(`retained fluid header layout at ${width}px`, geometry.height === 49 && geometry.overflow === 0, geometry)
}
check('no browser errors', report.errors.length === 0, report.errors)
} catch (error) {
check('suite completes', false, error.stack)
} finally {
if (browser) await browser.close()
await new Promise(resolve => server.close(resolve))
report.status = report.checks.every(c => c.status === 'PASS') ? 'PASS' : 'FAIL'
await writeFile(path.join(out, 'result.json'), JSON.stringify(report, null, 2) + '\n')
console.log(JSON.stringify({ status: report.status, checks: report.checks.length, errors: report.errors }))
process.exitCode = report.status === 'PASS' ? 0 : 1
}