chore(sw): improve interactive header story preview
This commit is contained in:
1 parent
50bf5bda8e
commit
bf16578226
58 files changed
+6028
-15
No files matched your search
@@ -0,0 +1,68 @@
|
||||
// Installed Firefox via WebDriver BiDi, matching the existing SW cross-browser workflow.
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
||||
import { spawn } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import http from 'node:http'
|
||||
|
||||
const out = path.resolve(process.argv[2] ?? 'docs/sw/storybook-overview-qa')
|
||||
await mkdir(out,{recursive:true})
|
||||
const report = { checks:[], errors:[], infrastructureWarnings:[] }
|
||||
const check = (name,ok,data) => {report.checks.push({name,status:ok?'PASS':'FAIL',data});console.log(ok?'PASS':'FAIL',name)}
|
||||
const sleep = ms => new Promise(resolve=>setTimeout(resolve,ms))
|
||||
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 url=`http://127.0.0.1:${server.address().port}/iframe.html?id=sw-header--overview&viewMode=story`
|
||||
let processFirefox,ws
|
||||
try {
|
||||
for(const [width,height]of[[1400,1000],[500,800]]){
|
||||
const profile=path.resolve('_local',`overview-firefox-${process.pid}-${width}`)
|
||||
await mkdir(profile,{recursive:true})
|
||||
const port=9500+process.pid%400
|
||||
processFirefox=spawn(process.env.SW_FIREFOX_PATH??'C:/Program Files/Mozilla Firefox/firefox.exe',['--headless','--no-remote','--profile',profile,`--remote-debugging-port=${port}`],{stdio:'ignore',env:{...process.env,MOZ_HEADLESS_WIDTH:String(width),MOZ_HEADLESS_HEIGHT:String(height)}})
|
||||
ws=undefined
|
||||
for(let i=0;i<40&&!ws;i++){await sleep(250);try{ws=await new Promise((resolve,reject)=>{const s=new WebSocket(`ws://127.0.0.1:${port}/session`);s.onopen=()=>resolve(s);s.onerror=reject})}catch{ws=undefined}}
|
||||
if(!ws)throw Error('Firefox BiDi unavailable')
|
||||
let id=0;const pending=new Map()
|
||||
ws.onmessage=event=>{const m=JSON.parse(event.data);if(m.id&&pending.has(m.id)){const {resolve,reject}=pending.get(m.id);pending.delete(m.id);m.type==='error'?reject(Error(m.message)):resolve(m.result)}else if(m.method==='log.entryAdded'&&m.params.level==='error'){
|
||||
const text=m.params.text
|
||||
if(text.startsWith('downloadable font: download failed')&&text.includes('/sb-common-assets/nunito-sans-regular.woff2'))report.infrastructureWarnings.push(text)
|
||||
else report.errors.push(text)
|
||||
}}
|
||||
const send=(method,params={})=>new Promise((resolve,reject)=>{const next=++id;pending.set(next,{resolve,reject});ws.send(JSON.stringify({id:next,method,params}))})
|
||||
const session=await send('session.new',{capabilities:{}});report.version=session.capabilities.browserVersion
|
||||
await send('session.subscribe',{events:['log.entryAdded']})
|
||||
const context=(await send('browsingContext.getTree')).contexts[0].context
|
||||
const evaluate=async expression=>{const r=await send('script.evaluate',{expression,target:{context},awaitPromise:true});if(r.type==='exception')throw Error(r.exceptionDetails.text);return r.result.value}
|
||||
const data=async expression=>JSON.parse(await evaluate(`JSON.stringify(${expression})`))
|
||||
const settle=()=>evaluate('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(()=>resolve(true))))')
|
||||
const key=async value=>send('input.performActions',{context,actions:[{type:'key',id:'keyboard',actions:[{type:'keyDown',value},{type:'keyUp',value}]}]})
|
||||
await send('browsingContext.navigate',{context,url,wait:'complete'})
|
||||
let ready=false
|
||||
for(let i=0;i<40&&!ready;i++){ready=await evaluate('!!document.querySelector("[data-preview-stage]")');if(!ready)await sleep(100)}
|
||||
if(!ready)throw Error('Overview did not render')
|
||||
await settle()
|
||||
const fit=await data('(()=>{const s=document.querySelector("[data-preview-stage]").getBoundingClientRect(),b=document.querySelector("[data-preview-slot]").getBoundingClientRect();return {width:s.width,height:s.height,slotWidth:b.width,slotHeight:b.height,headerHeight:document.querySelector("header.hm-sw-header").getBoundingClientRect().height,viewportWidth:innerWidth,viewportHeight:innerHeight}})()')
|
||||
check(`Firefox ${width}: 16:9 in Inner and Header 49`,Math.abs(fit.width/fit.height-16/9)<.005&&fit.width<=fit.slotWidth+.1&&fit.height<=fit.slotHeight+.1&&fit.headerHeight===49,fit)
|
||||
await evaluate('(()=>{const s=document.querySelector("select");s.value="expanded";s.dispatchEvent(new Event("change",{bubbles:true}));return true})()');await settle()
|
||||
await evaluate('(()=>{const b=document.querySelector("[data-property-panel-trigger]");if(b.getClientRects().length)b.click();else{document.querySelector(".hm-sw-header__more-trigger").click()}return true})()');await settle()
|
||||
await evaluate('(()=>{const m=[...document.querySelectorAll("[role=menuitemcheckbox]")].find(e=>e.textContent.trim()==="속성창");if(m)m.click();return true})()');await settle()
|
||||
const popup=await data('(()=>{const e=document.querySelector(".hm-sw-property"),r=e.getBoundingClientRect();return {width:r.width,height:r.height,focus:document.activeElement===e}})()')
|
||||
check(`Firefox ${width}: full original popup 314x754`,popup.width===314&&popup.height===754,popup)
|
||||
const bottom=await data('(()=>{const z=document.querySelector(".hm-sw-property [data-property-id=z] input"),s=document.querySelector("[data-preview-stage]");z.scrollIntoView({block:"nearest",inline:"nearest"});z.focus();const r=z.getBoundingClientRect();return {top:s.scrollTop,visible:document.elementFromPoint(r.x+r.width/2,r.y+r.height/2)===z}})()')
|
||||
check(`Firefox ${width}: popup bottom reachable and focusable`,bottom.top>0&&bottom.visible,bottom)
|
||||
await key('\uE00C');await settle()
|
||||
check(`Firefox ${width}: Escape closes popup and restores focus`,await evaluate('!document.querySelector(".hm-sw-property")&&document.activeElement.matches("[data-property-panel-trigger],.hm-sw-header__more-trigger")'))
|
||||
await writeFile(path.join(out,`overview-firefox-${width}.png`),Buffer.from((await send('browsingContext.captureScreenshot',{context})).data,'base64'))
|
||||
await send('session.end');ws.close();ws=undefined;processFirefox.kill();processFirefox=undefined
|
||||
await sleep(250)
|
||||
}
|
||||
check('Firefox SW runtime errors absent; known Storybook font warnings recorded separately',report.errors.length===0,{errors:report.errors,warnings:report.infrastructureWarnings})
|
||||
}catch(error){check('Firefox suite completes',false,error.stack)}
|
||||
finally{
|
||||
ws?.close();processFirefox?.kill();await new Promise(resolve=>server.close(resolve))
|
||||
report.status=report.checks.every(c=>c.status==='PASS')?'PASS':'FAIL'
|
||||
await writeFile(path.join(out,'firefox-result.json'),JSON.stringify(report,null,2)+'\n')
|
||||
console.log(JSON.stringify({status:report.status,checks:report.checks.length,version:report.version}))
|
||||
process.exitCode=report.status==='PASS'?0:1
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
// Focused Overview integration checks. Build Storybook first; historical evidence stays untouched.
|
||||
// Usage: node packages/sw/tests/overview.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-overview-qa')
|
||||
await mkdir(out, { recursive: true })
|
||||
const report = { baseCommit: '50bf5bda8e956a4973b923d64131f2f2285bd38c', 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 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')
|
||||
check('all 28 Story IDs, names, order and tags retained', after.length === 28 && JSON.stringify(before.map(e => [e.id,e.name,e.tags])) === JSON.stringify(after.map(e => [e.id,e.name,e.tags])))
|
||||
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').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)
|
||||
check('other 27 Story declarations byte-identical', Object.keys(oldStories).filter(k => k !== 'Overview').every(k => oldStories[k] === newStories[k]))
|
||||
check('package production source untouched', !execFileSync('git', ['diff', report.baseCommit, '--', 'packages/sw/src'], { encoding:'utf8' }))
|
||||
|
||||
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 } = 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, 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`)
|
||||
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()}
|
||||
}
|
||||
const barInput=id=>page.locator(`.hm-sw-property-bar [data-property-id="${id}"] input`)
|
||||
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)
|
||||
|
||||
await barInput('radius').fill('6.25');await barInput('radius').press('Enter')
|
||||
check('Bar Enter commits state',await barInput('radius').inputValue()==='6.25'&&(await callback()).includes('onPropertyChange(radius, 6.25)'))
|
||||
await barInput('radius').fill('-1');await barInput('radius').press('Enter');check('Bar validates min=0',await barInput('radius').inputValue()==='6.25')
|
||||
await barInput('radius').fill('9');await barInput('radius').press('Escape');check('Bar Escape cancels edit',await barInput('radius').inputValue()==='6.25')
|
||||
await panelButton();await page.locator('.hm-sw-property').waitFor()
|
||||
check('Header opens panel with same Bar value',await panelInput('radius').inputValue()==='6.25')
|
||||
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 waitValue(barInput('radius'),'7.5')
|
||||
check('panel edit updates the Bar',await barInput('radius').inputValue()==='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 waitValue(barInput('radius'),'12.5');check('external Props update Bar',await barInput('radius').inputValue()==='12.5')
|
||||
await page.getByRole('checkbox',{name:'속성 ReadOnly',exact:true}).check();check('Bar ReadOnly',await barInput('radius').evaluate(e=>e.readOnly))
|
||||
await page.getByRole('checkbox',{name:'속성 ReadOnly',exact:true}).uncheck()
|
||||
await page.getByRole('checkbox',{name:'속성 Disabled',exact:true}).check();check('Bar Disabled',await barInput('radius').isDisabled())
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user