test(sw): align legacy storybook suites and add header composition fixture

- dropdown-consolidation, storybook-cleanup-stage1, detail-buttons, property-panel-comparison: assert their recorded
  state and accept today only the approved later Story changes (storybook-inventory.mjs helpers).
- header-composition.browser.mjs + fixtures/header-composition.tsx: the removed Composition Story presets, hidden
  controls, invalid menuOrder / hiddenControls and live changes, on a test-only page using the public API only.
- storybook-unused-removal: header comment lists which older scripts run on current Stories again.
This commit is contained in:
shkim4 committed 2026-10-21 06:08:23 +09:00
1 parent 1e05eaf1de
commit b50e95c325
8 files changed
+375 -66

No files matched your search

+59 -28
View File
@@ -1,12 +1,17 @@
// Historical since 2026-10-19: opens SW Story IDs retired by the unused demo removal (composition, colorize, layer-detail-input; preservation/verify modes). Kept as evidence
// for its recorded run; current regression: storybook-unused-removal.browser.mjs.
// 2026-10-20 (test alignment): `capture`, `preservation`, `baseline` and `verify` are current regressions again. The checks of
// the Stories retired by the unused demo removal (5faa1f1) run on current public Stories: K Guide (the same fluid Header
// as Composition), Overview (btn_colorize, layer detail), Property Panel · 4개 비교 (editable Input) and Document Open
// (`controls` allowlist composition). No current Story sets hiddenControls / menuOrder presets, so the Composition preset
// loop is the historical `composition-history` mode, for a Storybook built at or before c8e5fbe (pass its base URL).
// The same presets run today on a test-only fixture in header-composition.browser.mjs.
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import http from 'node:http'
import { swStoryEntries, retainedInventory, retiredPropertyPanelIds } from './storybook-inventory.mjs'
import { swStoryEntries, retainedInventory, retiredPropertyPanelIds, retiredUnusedDemoIds, laterRetiredHeaderIds, approvedAddedHeaderIds, movedToPopupIds } from './storybook-inventory.mjs'
import { loadPlaywright } from './playwright-runtime.mjs'
// Usage: node packages/sw/tests/detail-buttons.browser.mjs baseline|verify <evidence-dir> [base-url]
// Run `baseline` into a new evidence directory at the reference state, then `verify` into the same directory.
const mode = process.argv[2] ?? 'verify'
const output = path.resolve(process.argv[3] ?? 'docs/sw/detail-button-qa')
let base = process.argv[4] ?? 'static'
@@ -37,6 +42,8 @@ const style = e => {
iconOpacity:i && getComputedStyle(i).opacity,iconContent:i && getComputedStyle(i).content,
height:e.getBoundingClientRect().height,width:e.getBoundingClientRect().width,outline:s.outline,disabled:e.disabled }
}
// Header height, overflow and the menus inside its fields in DOM order (configured controls or the default menus).
const composedMenus = h => ({height:h.getBoundingClientRect().height,overflow:h.scrollWidth-h.clientWidth,menus:[...h.querySelectorAll('.hm-sw-header__fields > *')].filter(e=>e.querySelector('.hm-sw-header__field')).map(e=>e.dataset.control??({layer:'layer',color:'color','line-style':'lineStyle','text-style':'textStyle','dimension-style':'dimensionStyle'}[e.dataset.menu]??(e.classList.contains('hm-sw-header__layer')?'layer':null)))})
const snapshots = {}
try {
if (mode === 'capture') {
@@ -45,7 +52,9 @@ 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('composition')
// 2026-10-20: Composition, Colorize and Layer Detail · Input were removed (5faa1f1). K Guide renders the same fluid
// figmaArgs Header (window motion, K guide), Overview's Header the btn_colorize menu, the comparison the editable Input.
await goto('k-guide')
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}))
@@ -57,27 +66,48 @@ try {
const box=await page.locator(`[data-compare="${id}"] .hm-sw-property`).boundingBox()
check(`preserve property panel ${id}`,box.width===314&&box.height===height,box)
}
await goto('composition')
await goto('k-guide')
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))
const guide=page.locator('.hm-sw-k-guide').first();await guide.locator('button').hover()
check('preserve K guide animation',await guide.locator('.hm-sw-k-guide__panel').isVisible()&&await guide.locator('.hm-sw-k-guide__panel').evaluate(e=>getComputedStyle(e).animationDuration==='0.511s'))
await page.mouse.move(1,900);check('K guide leave closes',await guide.locator('.hm-sw-k-guide__panel').count()===0)
await goto('colorize');const colorHeader=page.locator('header.hm-sw-header').first();await colorHeader.getByRole('button',{name:'색상',exact:true}).click()
check('preserve Colorize size',(await colorHeader.getByRole('button',{name:'색상 추출',exact:true}).boundingBox()).width===24)
await goto('layer-detail-input');const editing=page.getByRole('textbox',{name:'Editing',exact:true})
check('preserve editable Input',await editing.isEditable()&&await editing.evaluate(e=>getComputedStyle(e).caretColor==='rgb(230, 4, 3)'))
await goto('overview');const colorHeader=page.locator('header.hm-sw-header').first();await colorHeader.getByRole('button',{name:'색상',exact:true}).click()
check('preserve Colorize size (Overview Header)',(await colorHeader.getByRole('button',{name:'색상 추출',exact:true}).boundingBox()).width===24)
await goto('property-panel-compare');const editing=page.locator('[data-compare="circle"] [data-property-id="radius"] input.hm-sw-header__input').first()
check('preserve editable Input (Property Panel · 4개 비교)',await editing.isEditable()&&await editing.evaluate(e=>getComputedStyle(e).caretColor==='rgb(230, 4, 3)'))
} else if (mode === 'composition-history') {
// Historical: the verify-mode Composition preset loop of 2026-10-19 on sw-header--composition (removed in 5faa1f1).
// Run it only against a Storybook built at or before c8e5fbe (base-url); the current Storybook has no such Story.
await goto('composition')
const presets=['기본 전체','구성 A','구성 B (레이어·선스타일)','구성 C (로고만)','파일 목록 제거','레이어 제거','색상 제거','스타일 그룹 제거','여러 메뉴 제거','순서 변경','숨김 + 순서 변경']
const all=['layer','color','lineStyle','textStyle','dimensionStyle']
const removed=[[],['textStyle','dimensionStyle'],['color','textStyle','dimensionStyle'],all,[],['layer'],['color'],['lineStyle','textStyle','dimensionStyle'],['color','textStyle'],[],['layer','textStyle']]
const orders=[...Array(9).fill(all),['color','layer','dimensionStyle','lineStyle','textStyle'],['dimensionStyle','textStyle','color','layer','lineStyle']]
for(let p=0;p<presets.length;p++)for(const width of [1920,1440,1080,900,768,640,390]){
await page.setViewportSize({width,height:1000});await page.getByRole('button',{name:presets[p],exact:true}).click()
const h=page.locator('[data-composition-demo] header.hm-sw-header')
const actual=await h.evaluate(composedMenus)
const expected=orders[p].filter(id=>!removed[p].includes(id))
check(`composition ${presets[p]} @${width}`,actual.height===49&&actual.overflow===0&&JSON.stringify(actual.menus)===JSON.stringify(expected),{actual,expected})
const more=h.locator('.hm-sw-header__more-trigger')
if(await more.isVisible()) {await more.click();const items=await h.locator('.hm-sw-header__more-item').allTextContents();check(`nonempty More ${presets[p]} @${width}`,items.length>0);await page.keyboard.press('Escape')}
}
} else {
const index = await (await fetch(`${base}/index.json`)).json()
const ids = swStoryEntries(index).map(e => e.id)
report.storyIds = ids
if (mode === 'baseline') await writeFile(path.join(output, 'baseline-story-ids.json'), JSON.stringify(ids))
// These retained examples cover fixed-width and fluid headers after the two Args demos were retired.
for (const story of ['overview', 'composition']) {
// 2026-10-20: Composition was removed (5faa1f1); K Guide renders the same fluid figmaArgs Header.
for (const story of ['overview', 'k-guide']) {
await goto(story)
for (const width of [1920,1800,1440,1180,1080,900,768,640,520,390]) {
await page.setViewportSize({ width, height:1000 })
// 2026-10-20: Overview sizes its 16:9 Stage through a ResizeObserver; wait until the Header box holds for three frames
// (without it a snapshot could catch the previous width, so baseline and verify differed on the same build).
await page.locator('header.hm-sw-header').first().evaluate(h => new Promise(resolve => { let last = '', stable = 0; const tick = () => { const r = h.getBoundingClientRect(), now = [r.x, r.y, r.width].join(); stable = now === last ? stable + 1 : 0; last = now; stable >= 3 ? resolve() : requestAnimationFrame(tick) }; requestAnimationFrame(tick) }))
snapshots[`${story}-${width}`] = await page.locator('header.hm-sw-header').first().evaluate(h => [h,...h.querySelectorAll('*')].map(e => {
const b=e.getBoundingClientRect(),s=getComputedStyle(e)
return {tag:e.tagName,class:e.getAttribute('class'),x:b.x,y:b.y,width:b.width,height:b.height,display:s.display,visibility:s.visibility,color:s.color,background:s.backgroundColor}
@@ -104,24 +134,24 @@ try {
const retiredIds=['default','controls','responsive','text-stroke','dynamic-data','layer-dropdown','color-dropdown','line-style-dropdown','text-dimension-style-dropdown'].map(id=>`sw-header--${id}`)
// 2026-10-19: the five duplicate property panel demos are retired as well (24 -> 19).
retiredIds.push(...retiredPropertyPanelIds)
const retainedIds=retainedInventory(oldIds.map(id=>({id}))).map(e=>e.id).filter(id=>!retiredPropertyPanelIds.includes(id))
check('retained stories remain; only approved retirements are absent',ids.length===19&&retainedIds.every(id=>ids.includes(id))&&retiredIds.every(id=>!ids.includes(id)),{before:oldIds.length,after:ids.length})
// 2026-10-20: then the nine unused demos (5faa1f1) and the Popup move; the approved Header additions are listed in
// storybook-inventory.mjs. Expected from this run's baseline and from the list recorded on 2026-10-16 (which predates
// this suite's own sw-header--detail-buttons), so a fresh baseline cannot accept an unapproved Story.
retiredIds.push(...retiredUnusedDemoIds)
const expectedFrom=list=>{const retained=retainedInventory(list.map(id=>({id}))).map(e=>e.id).filter(id=>!laterRetiredHeaderIds.includes(id)&&!movedToPopupIds.includes(id));return {retained,ids:[...new Set([...retained,...approvedAddedHeaderIds,'sw-header--detail-buttons'])].sort()}}
const fromBaseline=expectedFrom(oldIds),fromRecorded=expectedFrom(JSON.parse(await readFile('docs/sw/detail-button-qa/baseline-story-ids.json','utf8')))
check('retained stories remain; only approved retirements are absent; only approved additions',JSON.stringify([...ids].sort())===JSON.stringify(fromBaseline.ids)&&JSON.stringify(fromBaseline.ids)===JSON.stringify(fromRecorded.ids)&&fromBaseline.retained.every(id=>ids.includes(id))&&retiredIds.every(id=>!ids.includes(id))&&movedToPopupIds.every(id=>index.entries[id]?.title==='SW/Popup'),{before:oldIds.length,after:ids.length,unexpected:ids.filter(id=>!fromRecorded.ids.includes(id)),missing:fromRecorded.ids.filter(id=>!ids.includes(id))})
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 (로고만)','파일 목록 제거','레이어 제거','색상 제거','스타일 그룹 제거','여러 메뉴 제거','순서 변경','숨김 + 순서 변경']
const all=['layer','color','lineStyle','textStyle','dimensionStyle']
const removed=[[],['textStyle','dimensionStyle'],['color','textStyle','dimensionStyle'],all,[],['layer'],['color'],['lineStyle','textStyle','dimensionStyle'],['color','textStyle'],[],['layer','textStyle']]
const orders=[...Array(9).fill(all),['color','layer','dimensionStyle','lineStyle','textStyle'],['dimensionStyle','textStyle','color','layer','lineStyle']]
for(let p=0;p<presets.length;p++)for(const width of [1920,1440,1080,900,768,640,390]){
await page.setViewportSize({width,height:1000});await page.getByRole('button',{name:presets[p],exact:true}).click()
const h=page.locator('[data-composition-demo] header.hm-sw-header')
const actual=await h.evaluate(h=>({height:h.getBoundingClientRect().height,overflow:h.scrollWidth-h.clientWidth,menus:[...h.querySelectorAll('.hm-sw-header__fields > *')].filter(e=>e.querySelector('.hm-sw-header__field')).map(e=>e.dataset.control??({layer:'layer',color:'color','line-style':'lineStyle','text-style':'textStyle','dimension-style':'dimensionStyle'}[e.dataset.menu]??(e.classList.contains('hm-sw-header__layer')?'layer':null)))}))
const expected=orders[p].filter(id=>!removed[p].includes(id))
check(`composition ${presets[p]} @${width}`,actual.height===49&&actual.overflow===0&&JSON.stringify(actual.menus)===JSON.stringify(expected),{actual,expected})
// 2026-10-20: the Composition presets (hiddenControls / menuOrder) are the `composition-history` mode. The configured
// composition that stays public is Document Open's `controls` allowlist, rendered at ten frame widths.
await goto('document-open')
for(const width of [1920,1800,1440,1180,1080,900,768,640,520,390]){
const h=page.locator(`[data-composed-responsive] [data-width="${width}"] header.hm-sw-header`)
const actual=await h.evaluate(composedMenus)
check(`controls allowlist composition @${width}`,actual.height===49&&actual.overflow===0&&JSON.stringify(actual.menus)===JSON.stringify(['layer','color']),actual)
const more=h.locator('.hm-sw-header__more-trigger')
if(await more.isVisible()) {await more.click();const items=await h.locator('.hm-sw-header__more-item').allTextContents();check(`nonempty More ${presets[p]} @${width}`,items.length>0);await page.keyboard.press('Escape')}
if(await more.isVisible()) {await more.click();const items=await h.locator('.hm-sw-header__more-item').allTextContents();check(`nonempty More controls allowlist @${width}`,items.length>0,items);await page.keyboard.press('Escape')}
}
await page.setViewportSize({width:1920,height:1000})
// Real dropdown integration, separate from the shared-button specimen.
await goto('composed-controls')
const host=page.locator('.sw-composed-controls')
@@ -135,11 +165,12 @@ try {
}
await detail.click();check(`actual ${name} callback / close / focus`,(await host.locator('[data-callback-log]').textContent()).includes(callback)&&await trigger.getAttribute('aria-expanded')==='false'&&await trigger.evaluate(e=>e===document.activeElement))
}
await goto('layer-detail-input')
const layerHost=page.locator('section').filter({has:page.getByRole('heading',{name:'레이어 상세 Click Callback',exact:true})})
// Layer Detail · Input was removed (5faa1f1); Overview's Header connects onLayerDetail to its callback output.
await goto('overview')
const layerHost=page.locator('header.hm-sw-header').first()
const layerTrigger=layerHost.getByRole('button',{name:'레이어',exact:true});await layerTrigger.click()
await layerHost.getByRole('button',{name:'레이어 상세',exact:true}).click()
check('actual layer callback / close / focus',(await layerHost.locator('[data-callback-log]').textContent()).includes('onLayerDetail')&&await layerTrigger.getAttribute('aria-expanded')==='false'&&await layerTrigger.evaluate(e=>e===document.activeElement))
check('actual layer callback / close / focus (Overview)',(await page.locator('[data-preview-callback]').textContent()).includes('onLayerDetail')&&await layerTrigger.getAttribute('aria-expanded')==='false'&&await layerTrigger.evaluate(e=>e===document.activeElement))
await goto('detail-buttons')
const rows=page.locator('[data-detail-comparison] [data-detail-row]')
const names=['레이어 상세','색상 상세','선스타일 상세','문자스타일 상세','치수스타일 상세'], widths=[113,99,128,142,142]