Files
HMDesign/packages/sw/tests/overview.browser.mjs
T
shkim4 2fc1b3e108 fix(sw): 2026-10-21 SW design review and responsive header without More from 760px
User-approved design review (2026-10-21):
- SW Header: from 760px up nothing moves into More; the five menus stay with their
  titles (titles and values ellipsize), file name shrinks to 96px. Open next to the
  logo no longer blocks field shrinking above 760px (760/768 overflow fixed, also for
  configured `controls` headers). Dropdown yellow ring inside the button, black seam.
- Property Bar values as read-only red text; property popup "=" labels, read-only
  black values, per-entry hover with pointer.
- Home Menu recent files (3 rows, path + name caption, empty preview), Footer + slot
  guide off by default, Viewer file list without icons, Storybook widths 1920-760.
- Tests: old More expectations (integrated-improvement, keyboard-dismiss,
  footer-popup-viewer-refinement, storybook-cleanup-stage1) moved to the new rule;
  functional checks kept.
2026-10-21 17:29:25 +09:00

247 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Focused Overview integration checks. Build Storybook first; historical evidence stays untouched.
// Usage: node packages/sw/tests/overview.browser.mjs [evidence-dir]
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'
import { swStoryEntries, retainedInventory, inventorySignature, retiredDropdownIds, integratedChangedFiles, integratedStoryIds, retiredPropertyPanelIds, retiredUnusedDemoIds, movedHomeMenuIds, retiredHomeMenuCompareIds, movedToPopupIds, approvedMainCommit, homeMenuSidebarIds, withApprovedEntryChanges, approvedHeaderDeclarations, movedHeaderDeclarations } from './storybook-inventory.mjs'
import { loadPlaywright } from './playwright-runtime.mjs'
import { sourceChanges, unapprovedChanges } from './source-changes.mjs'
const out = path.resolve(process.argv[2] ?? 'docs/sw/storybook-overview-qa')
await mkdir(out, { recursive: true })
// 2026-10-20 (test alignment): 50bf5bd and its before-index.json stay the baseline. The checks below accept only the
// approved changes after it and the approved main merge, both recorded in storybook-inventory.mjs.
const report = { baseCommit: '50bf5bda8e956a4973b923d64131f2f2285bd38c', approvedMainCommit, checks: [], errors: [], sizes: [] }
const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name) }
const before = JSON.parse(await readFile(path.join(out, 'before-index.json'), 'utf8'))
const index = JSON.parse(await readFile('storybook-static/index.json', 'utf8'))
const after = swStoryEntries(index)
await writeFile(path.join(out, 'after-index.json'), JSON.stringify(after, null, 2) + '\n')
const removedHeaderIds = [...retiredPropertyPanelIds, ...retiredUnusedDemoIds, ...movedToPopupIds]
const addedHeaderIds = [...integratedStoryIds.filter(id => id.startsWith('sw-header--')), ...homeMenuSidebarIds, ...Object.values(movedHomeMenuIds).filter(id => !retiredHomeMenuCompareIds.includes(id))]
const expectedRetained = retainedInventory(before).filter(entry => !removedHeaderIds.includes(entry.id)).map(withApprovedEntryChanges)
const inventory = { expected: expectedRetained.length + addedHeaderIds.length, actual: after.length, missingAdded: addedHeaderIds.filter(id => !after.some(e => e.id === id)), movedToPopup: movedToPopupIds.map(id => index.entries[id]?.title ?? null) }
check(`${inventory.expected} SW/Header Story IDs: the 24 retained at 50bf5bd (names, tags) minus approved retirements and the Popup move, plus approved additions`,
after.length === inventory.expected && inventory.missingAdded.length === 0 && inventory.movedToPopup.every(title => title === 'SW/Popup')
&& JSON.stringify(inventorySignature(expectedRetained)) === JSON.stringify(inventorySignature(after.filter(e => !addedHeaderIds.includes(e.id)))), inventory)
const source = await readFile('src/sw/sw-header.stories.tsx', 'utf8')
const baseline = execFileSync('git', ['show', `${report.baseCommit}:src/sw/sw-header.stories.tsx`], { encoding: 'utf8' })
const stories = text => Object.fromEntries(parse(text, { sourceType:'module', plugins:['typescript','jsx'] }).program.body
.filter(n => n.type === 'ExportNamedDeclaration' && n.declaration.type === 'VariableDeclaration').map(n => [n.declaration.declarations[0].id.name, text.slice(n.start,n.end).replaceAll('\r\n','\n')]))
const oldStories = stories(baseline), newStories = stories(source)
const retiredExports = ['LayerDropdown','ColorDropdown','LineStyleDropdown','TextDimensionStyleDropdown','ComposedControls']
// A declaration changed after 50bf5bd must equal its version at the approving commit; a removed one must be an approved
// retirement, or the Popup move compared in its new file. All others stay byte-identical to 50bf5bd.
const gitStories = (commit, file) => stories(execFileSync('git', ['show', `${commit}:${file}`], { encoding: 'utf8' }))
const storyId = name => `sw-header--${name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()}`
const approvedDeclarations = approvedHeaderDeclarations, movedDeclarations = movedHeaderDeclarations
const movedNow = Object.fromEntries(await Promise.all(Object.entries(movedDeclarations).map(async ([name, [file]]) => [name, stories(await readFile(file, 'utf8'))[name]])))
const declarationIssues = Object.keys(oldStories).filter(k => k !== 'Overview' && !retiredExports.includes(k)).filter(k => {
if (movedDeclarations[k]) return !!newStories[k] || movedNow[k] !== gitStories(movedDeclarations[k][1], movedDeclarations[k][0])[k]
if (!newStories[k]) return ![...retiredPropertyPanelIds, ...retiredUnusedDemoIds].includes(storyId(k))
return newStories[k] !== (approvedDeclarations[k] ? gitStories(approvedDeclarations[k], 'src/sw/sw-header.stories.tsx')[k] : oldStories[k])
})
check('retained non-Overview declarations byte-identical to 50bf5bd or their approving commit; only approved removals; relocated composition separately verified', declarationIssues.length === 0, declarationIssues)
check('retired Dropdown IDs absent', retiredDropdownIds.every(id => !after.some(e => e.id === id)))
// The package source may differ from the approved main merge only in the integrated-improvement files (their own suites
// verify them). Approved package changes after 50bf5bd are listed as sinceBaseline; update approvedMainCommit on the next approved merge.
// Untracked new source files count as changes (source-changes.mjs).
const sourceDiff = { unapproved: unapprovedChanges(report.approvedMainCommit, ['packages/sw/src'], integratedChangedFiles), sinceBaseline: sourceChanges(report.baseCommit, ['packages/sw/src']).length }
check('package production source unchanged since the approved main merge except the integrated-improvement files', sourceDiff.unapproved.length === 0, sourceDiff)
const root = path.resolve('storybook-static'), 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 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 } = loadPlaywright()
let browser, page
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()
page = await browser.newPage({ viewport:{width:1440,height:1000} }); page.setDefaultTimeout(6000)
page.on('pageerror', e => report.errors.push(e.message)); page.on('console', m => { if(m.type() === 'error') report.errors.push(m.text()) })
const go = async(id='overview') => {
await page.goto(`${base}/iframe.html?id=sw-header--${id}&viewMode=story${id === 'overview' ? '&verification' : ''}`)
await page.locator('.hm-sw-header,.hm-sw-theme').first().waitFor(); await page.evaluate(() => document.fonts.ready)
await page.evaluate(()=>new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve))))
}
const geometry = () => page.evaluate(() => {
const box = e => { const r=e.getBoundingClientRect(); return {x:r.x,y:r.y,w:r.width,h:r.height,right:r.right,bottom:r.bottom} }
const stage=document.querySelector('[data-preview-stage]'),slot=document.querySelector('[data-preview-slot]')
return {stage:box(stage),slot:box(slot),header:box(document.querySelector('header.hm-sw-header')),viewport:{w:innerWidth,h:innerHeight},overflow:document.documentElement.scrollWidth-document.documentElement.clientWidth}
})
if(process.argv[3]==='overlays'){
await go()
const stage=page.locator('[data-preview-stage]'),header=page.locator('header.hm-sw-header')
const covered=locator=>locator.evaluate(e=>{const r=e.getBoundingClientRect();return e.contains(document.elementFromPoint(r.x+r.width/2,r.y+r.height/2))})
await header.locator('.hm-sw-header__document-button').click()
const firstDocument=header.locator('.hm-sw-header__document-item').first()
check('document first row is above the property Bar and clickable',await covered(firstDocument));await firstDocument.click()
for(const [id,label,detail]of[['layer','레이어','레이어 상세'],['color','색상','색상 상세'],['line-style','선스타일','선스타일 상세'],['text-style','문자스타일','문자스타일 상세'],['dimension-style','치수스타일','치수스타일 상세']]){
await stage.evaluate(e=>e.scrollTop=0)
const button=header.getByRole('button',{name:label,exact:true})
if(await button.isVisible())await button.click();else{await header.getByRole('button',{name:'더보기',exact:true}).click();await header.locator(`[role=menuitem][data-menu="${id}"]`).click()}
await header.getByRole('button',{name:detail,exact:true}).click()
check(`existing ${detail} reports Callback`,(await page.locator('[data-preview-callback]').textContent()).includes(`on${{'layer':'Layer','color':'Color','line-style':'LineStyle','text-style':'TextStyle','dimension-style':'DimensionStyle'}[id]}Detail`))
}
await header.getByRole('button',{name:'색상',exact:true}).click()
await header.getByRole('button',{name:'ByBlock',exact:true}).click()
check('ByBlock mode immediately appears in Header without changing numeric RGB property',(await header.locator('.hm-sw-header__field--color').textContent()).includes('ByBlock'))
await page.keyboard.press('Escape')
await header.getByRole('button',{name:'색상',exact:true}).click()
await header.locator('.hm-sw-header__color-option').first().click()
check('direct color selection clears Header mode label',!(await header.locator('.hm-sw-header__field--color').textContent()).includes('ByBlock'))
await page.setViewportSize({width:390,height:700});await go();await page.getByLabel('선택 상태',{exact:true}).selectOption('expanded')
await header.getByRole('button',{name:'더보기',exact:true}).click();await header.getByRole('menuitemcheckbox',{name:'속성창',exact:true}).click()
for(const id of ['layer','line','color']){
const field=page.locator(`.hm-sw-property [data-property-id="${id}"]`),trigger=field.locator(id==='color'?'input':'button')
await trigger.scrollIntoViewIfNeeded();await trigger.click()
const drop=page.locator('.hm-sw-property__drop');await drop.waitFor()
const r=await drop.boundingBox()
check(`small Inner ${id} dropdown stays in browser width`,r.x>=0&&r.x+r.width<=390.5,r)
if(id==='layer'){const last=drop.getByRole('button',{name:'Sample-Text',exact:true});check('small Inner lower layer row hit-test is unobstructed',await covered(last));await last.click()}
else await page.keyboard.press('Escape')
check(`small Inner ${id} selection or Escape closes only nested dropdown`,await drop.count()===0&&await page.locator('.hm-sw-property').count()===1)
}
await page.screenshot({path:path.join(out,'overview-narrow-overlay.png')})
check('no overlay browser errors',report.errors.length===0,report.errors)
}else{
for(const [width,height] of [[1920,1080],[1440,900],[1280,720],[768,600],[390,844],[1000,500]]) {
await page.setViewportSize({width,height}); await go()
const g=await geometry(); report.sizes.push(g)
check(`16:9 fitted to both Inner dimensions at ${width}x${height}`,g.stage.w>0&&Math.abs(g.stage.w/g.stage.h-16/9)<.005&&g.stage.w<=g.slot.w+.1&&g.stage.h<=g.slot.h+.1&&g.header.h===49&&g.overflow===0,g)
}
await page.setViewportSize({width:1440,height:1000}); await go()
const header=page.locator('header.hm-sw-header'),stage=page.locator('[data-preview-stage]')
const callback=()=>page.locator('[data-preview-callback]').textContent()
const open=async(id,label) => {
await stage.evaluate(e=>{e.scrollTop=0;e.scrollLeft=0})
const button=header.getByRole('button',{name:label,exact:true})
if(await button.isVisible()) {if(await button.getAttribute('aria-expanded')!=='true')await button.click()}
else {await header.getByRole('button',{name:'더보기',exact:true}).click();await header.locator(`[role="menuitem"][data-menu="${id}"]`).click()}
}
const panelButton=async()=>{
const b=header.locator('[data-property-panel-trigger]')
if(await b.isVisible())await b.click()
else{await header.getByRole('button',{name:'더보기',exact:true}).click();await header.getByRole('menuitemcheckbox',{name:'속성창',exact:true}).click()}
}
// 2026-10-21 (user decision): the Bar values (반지름 / 지름 / 원주 / 면적) are read-only red text (<output>, edited in the panel), never focused.
const barValue=id=>page.locator(`.hm-sw-property-bar [data-property-id="${id}"] output`)
const waitText=(id,value)=>page.waitForFunction(([id,value])=>document.querySelector(`.hm-sw-property-bar [data-property-id="${id}"] output`)?.textContent===value,[id,value])
const panelInput=id=>page.locator(`.hm-sw-property [data-property-id="${id}"] input`).first()
const waitValue=async(locator,value)=>page.waitForFunction(([element,value])=>element.value===value,[await locator.elementHandle(),value])
check('one Header and no duplicate fixed Figma specimen',await header.count()===1&&await page.getByText('Figma 기준 1920 × 49',{exact:true}).count()===0)
await header.locator('.hm-sw-header__document-button').click()
const document=header.locator('.hm-sw-header__document-list .hm-sw-header__document-item').nth(1)
const documentName=await document.textContent();await document.click()
check('document selection updates Header and Callback',!!documentName&&(await header.locator('.hm-sw-header__document-button').textContent()).includes(documentName.trim())&&(await callback()).includes('onDocumentSelect'),await callback())
await open('layer','레이어');await header.getByRole('button',{name:'CC-D GNL',exact:true}).click()
check('layer selection updates shared data', (await callback()).includes('onLayerSelect(layer, CC-D GNL)'),await callback())
await open('layer','레이어');await header.getByRole('button',{name:'레이어 상세',exact:true}).click()
check('existing layer Detail callback closes list',await header.locator('.hm-sw-header__layer-panel').count()===0&&(await callback()).includes('onLayerDetail'))
await open('color','색상')
const chip=header.locator('.hm-sw-header__color-menu .hm-sw-header__color-option').first()
await chip.click();check('color selection invokes host callback',(await callback()).includes('onColorSelect'),await callback())
await open('line-style','선스타일');await header.getByRole('button',{name:'CENTER',exact:true}).click()
check('line selection updates Header', (await callback()).includes('onLineTypeSelect(line, CENTER)'),await callback())
await open('line-style','선스타일');await header.locator('ul[aria-label="가중치"]').getByRole('button',{name:'ByLayer',exact:true}).click()
check('ByLayer weight mode does not turn numeric property into a mode string',(await header.locator('.hm-sw-header__field--line-style').textContent()).includes('ByLayer'))
await page.keyboard.press('Escape')
await open('text-style','문자스타일');await header.locator('.hm-sw-header__list-menu--text-style').getByRole('button',{name:/Dotum/}).click()
check('text style selection updates Header',(await callback()).includes('onTextStyleSelect(dotum)'),await callback())
await open('dimension-style','치수스타일');await header.locator('.hm-sw-header__list-menu--dimension-style').getByRole('button',{name:/ISO - 25/}).click()
check('dimension style selection updates Header',(await callback()).includes('onDimensionStyleSelect(iso-25)'),await callback())
await open('color','색상');await open('layer','레이어')
check('opening another dropdown closes the previous one',await header.locator('.hm-sw-header__color-menu').count()===0&&await header.locator('.hm-sw-header__layer-panel').count()===1)
await page.keyboard.press('Escape');check('Escape closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0)
await open('layer','레이어');await page.locator('.sb-page-title').click();check('outside click closes Header dropdown',await header.locator('.hm-sw-header__layer-panel').count()===0)
const barState=await page.locator('.hm-sw-property-bar').evaluate(bar=>({object:bar.querySelector('.hm-sw-property-bar__object').textContent,order:[...bar.querySelectorAll('[data-property-id]')].map(e=>e.dataset.propertyId),inputs:bar.querySelectorAll('input').length,focusable:[...bar.querySelectorAll('output')].filter(e=>e.tabIndex>=0).length,colors:[...new Set([...bar.querySelectorAll('output')].map(e=>getComputedStyle(e).color))]}))
check('Bar: object text 원, order 반지름 / 지름 / 원주 / 면적, read-only red text (edited in the panel) without input or focus stop',barState.object==='원'&&barState.order.join()==='radius,diameter,circumference,area'&&barState.inputs===0&&barState.focusable===0&&barState.colors.join()==='rgb(230, 4, 3)',barState)
await barValue('radius').click();check('Bar value click never focuses the value',await page.evaluate(()=>document.activeElement?.tagName!=='OUTPUT'&&document.activeElement?.tagName!=='INPUT'))
await panelButton();await page.locator('.hm-sw-property').waitFor()
check('Header opens panel with same Bar value',await panelInput('radius').inputValue()===await barValue('radius').textContent())
check('numeric weight property remains numeric after ByLayer mode',await panelInput('weight').inputValue()==='0.25')
check('Header layer value shared with panel',(await page.locator('.hm-sw-property [data-property-id="layer"]').textContent()).includes('CC-D GNL'))
await panelInput('radius').fill('7.5');await panelInput('radius').press('Enter')
await waitText('radius','7.5')
check('panel edit updates the Bar',await barValue('radius').textContent()==='7.5'&&(await callback()).includes('onPropertyChange(radius, 7.5)'))
await panelInput('radius').fill('-2');await panelInput('radius').press('Enter');check('panel number validation rejects negative radius',await panelInput('radius').inputValue()==='7.5')
await page.locator('.hm-sw-property [data-property-id="layer"] button').click()
const drop=page.locator('.hm-sw-property__drop')
await drop.getByRole('button',{name:'Sample-Wall',exact:true}).click()
check('panel Layer dropdown updates Header shared value',(await header.locator('.hm-sw-header__field--layer').textContent()).includes('Sample-Wall'))
await page.locator('.hm-sw-property [data-property-id="line"] button').click();await drop.getByRole('button',{name:'HIDDEN',exact:true}).click()
check('panel line dropdown updates Header shared value',(await header.locator('.hm-sw-header__field--line-style').textContent()).includes('HIDDEN'))
await panelInput('color').click()
const colorOption=drop.locator('.hm-sw-header__color-option').nth(1)
const expectedColor=await colorOption.evaluate(e=>getComputedStyle(e.querySelector('.hm-sw-header__color-chip > span')).backgroundColor.match(/\d+/g).slice(0,3).join(','))
await colorOption.click();await waitValue(panelInput('color'),expectedColor)
const selectedColor=await panelInput('color').inputValue()
check('panel color dropdown updates Header shared value',(await header.locator('.hm-sw-header__field--color').textContent()).includes(selectedColor))
await panelInput('color').fill('300,0,0');await panelInput('color').press('Enter');check('panel RGB validation rejects out-of-range value',await panelInput('color').inputValue()===selectedColor)
await page.keyboard.press('Escape')
const gauge=page.locator('.hm-sw-property input[type="range"]')
await gauge.press('End');await gauge.press('Enter');await waitValue(panelInput('opacity'),'1')
check('gauge keyboard editing commits host state',await panelInput('opacity').inputValue()==='1')
await panelInput('radius').fill('8.5');await panelInput('radius').press('Escape')
check('first Escape cancels draft and retains panel',await panelInput('radius').inputValue()==='7.5'&&await page.locator('.hm-sw-property').count()===1)
await panelInput('radius').press('Escape');check('next Escape closes panel and restores Header focus',await page.locator('.hm-sw-property').count()===0&&await header.locator('[data-property-panel-trigger]').evaluate(e=>document.activeElement===e))
await panelButton();await page.getByRole('button',{name:'속성창 닫기',exact:true}).click();check('panel Close button restores focus',await header.locator('[data-property-panel-trigger]').evaluate(e=>document.activeElement===e))
await page.getByText('시연 데이터',{exact:true}).click()
await page.getByRole('button',{name:'외부 값 12.5',exact:true}).click();await waitText('radius','12.5');check('external Props update Bar',await barValue('radius').textContent()==='12.5')
await page.getByRole('checkbox',{name:'속성 ReadOnly',exact:true}).check();check('Bar ReadOnly: text, no input',await barValue('radius').count()===1&&await page.locator('.hm-sw-property-bar input').count()===0)
await page.getByRole('checkbox',{name:'속성 ReadOnly',exact:true}).uncheck()
await page.getByRole('checkbox',{name:'속성 Disabled',exact:true}).check();check('Bar Disabled: dimmed text',await barValue('radius').getAttribute('data-disabled')==='true')
await page.getByRole('checkbox',{name:'속성 Disabled',exact:true}).uncheck()
await page.getByRole('button',{name:'필드 추가',exact:true}).click();check('dynamic Bar field added',await page.locator('.hm-sw-property-bar [data-property-id^="preview-extra-"]').count()===1)
await panelButton();check('dynamic field shared with panel',await page.locator('.hm-sw-property [data-property-id^="preview-extra-"]').count()===1)
await page.getByRole('button',{name:'추가 필드 제거',exact:true}).click();check('dynamic field removed from both',await page.locator('[data-property-id^="preview-extra-"]').count()===0)
await page.getByRole('checkbox',{name:'형상 Slot 비우기',exact:true}).check();check('shape Slot can be empty without changing panel structure',await page.locator('.hm-sw-property [data-shape-slot="empty"]').count()===1)
await page.getByRole('checkbox',{name:'형상 Slot 비우기',exact:true}).uncheck()
await page.getByText('시연 데이터',{exact:true}).click()
for(const [selection,height]of[['none',256],['all',361],['circle',658],['expanded',754]]){
await page.getByLabel('선택 상태',{exact:true}).selectOption(selection)
if(await page.locator('.hm-sw-property').count()===0)await panelButton()
const box=await page.locator('.hm-sw-property').boundingBox()
check(`original panel size ${selection}:314x${height}`,box.width===314&&box.height===height,box)
}
await page.screenshot({path:path.join(out,'overview-after-expanded.png')})
await stage.evaluate(e=>{e.scrollTop=e.scrollHeight})
await panelInput('z').scrollIntoViewIfNeeded();await panelInput('z').fill('5');await panelInput('z').press('Enter')
check('expanded popup bottom accessible through internal scroll',await panelInput('z').inputValue()==='5')
await page.screenshot({path:path.join(out,'overview-after-scrolled.png')})
await go();await page.screenshot({path:path.join(out,'overview-after.png')})
// A small Inner keeps native component dimensions; the Stage, rather than the browser document, scrolls.
await page.setViewportSize({width:390,height:700});await go();await page.getByLabel('선택 상태',{exact:true}).selectOption('expanded');await panelButton()
await panelInput('z').scrollIntoViewIfNeeded();await panelInput('z').fill('3');await panelInput('z').press('Enter')
const narrow=await stage.evaluate(e=>({top:e.scrollTop,left:e.scrollLeft,height:e.clientHeight,scrollHeight:e.scrollHeight,width:e.clientWidth,scrollWidth:e.scrollWidth}))
check('small Inner reaches full popup bottom with native Header and popup dimensions',await panelInput('z').inputValue()==='3'&&narrow.scrollHeight>narrow.height&&narrow.scrollWidth>=390,narrow)
await page.screenshot({path:path.join(out,'overview-narrow.png')})
await panelInput('z').press('Escape')
check('narrow popup Escape restores More focus',await header.getByRole('button',{name:'더보기',exact:true}).evaluate(e=>document.activeElement===e))
await page.setViewportSize({width:1440,height:1000});await page.goto(`${base}/?path=/story/sw-header--overview`)
const preview=page.frameLocator('#storybook-preview-iframe');await preview.locator('[data-preview-stage]').waitFor()
report.managerOrder=await page.locator('[data-item-id^="sw-header--"]').evaluateAll(elements=>[...new Set(elements.map(e=>e.dataset.itemId))])
await writeFile(path.join(out,'manager-story-order.json'),JSON.stringify(report.managerOrder,null,2)+'\n')
await page.screenshot({path:path.join(out,'overview-manager.png')})
const managerFit=await preview.locator('[data-preview-stage]').evaluate(e=>{const b=e.getBoundingClientRect(),slot=e.parentElement.getBoundingClientRect();return {width:b.width,height:b.height,slotWidth:slot.width,slotHeight:slot.height,viewportWidth:innerWidth,viewportHeight:innerHeight}})
check('actual Storybook manager iframe fits its own Inner',managerFit.width<=managerFit.slotWidth+.1&&managerFit.height<=managerFit.slotHeight+.1&&Math.abs(managerFit.width/managerFit.height-16/9)<.005,managerFit)
check('no browser errors',report.errors.length===0,report.errors)
}
}catch(error){check('suite completes',false,error.stack);await page?.screenshot({path:path.join(out,'failure.png')}).catch(()=>{})}
finally{
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,process.argv[3]==='overlays'?'chrome-overlays-result.json':'chrome-result.json'),JSON.stringify(report,null,2)+'\n')
console.log(JSON.stringify({status:report.status,checks:report.checks.length,failures:report.checks.filter(c=>c.status==='FAIL')}))
process.exitCode=report.status==='PASS'?0:1
}