257 lines
24 KiB
JavaScript
257 lines
24 KiB
JavaScript
// Property fields (Figma 26:12388 ~ 26:12649): Default / real pointer Hover / Mouse Leave / On (editing, dropdown),
|
||
// selection callbacks, keyboard, Escape, external data, Disabled / ReadOnly, and the four panel states.
|
||
// 2026-11-10 (test alignment): the SW/Header › Property Fields and Property Panel demos this suite opened were removed in
|
||
// 5faa1f1 (approved). Sections 1–9 and the Header-hosted panel run on the test-only fixture fixtures/property-fields.tsx,
|
||
// which restores those demos' host data and host controls with the public API (as header-composition does); the four
|
||
// panel states run on the current Story sw-header--property-panel-compare (SW/Popup › 속성창). Every check is unchanged.
|
||
// Usage: node packages/sw/tests/property-fields.browser.mjs <evidence-dir> <label> [storybook-url]
|
||
// The fixture and Storybook use packages/sw/dist: build @hm-design/sw first.
|
||
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||
import { createRequire } from 'node:module'
|
||
import http from 'node:http'
|
||
import path from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
import { loadPlaywright } from './playwright-runtime.mjs'
|
||
const out = path.resolve(process.argv[2] ?? 'docs/sw/property-fields-qa')
|
||
const label = process.argv[3] ?? 'run'
|
||
const base = process.argv[4] ?? 'http://127.0.0.1:6024'
|
||
const { chromium } = loadPlaywright()
|
||
await mkdir(out, { recursive: true })
|
||
// Test-only fixture page (built into the evidence directory, served only for this run).
|
||
const site = path.join(out, 'fixture')
|
||
await mkdir(site, { recursive: true })
|
||
await createRequire(import.meta.url)('esbuild').build({
|
||
entryPoints: [fileURLToPath(new URL('./fixtures/property-fields.tsx', import.meta.url))], bundle: true, outdir: site, format: 'esm', jsx: 'automatic', minify: true,
|
||
loader: { '.ttf': 'file', '.woff': 'file', '.woff2': 'file', '.svg': 'file', '.png': 'file' }, define: { 'process.env.NODE_ENV': '"production"' }, logLevel: 'warning',
|
||
})
|
||
await writeFile(path.join(site, 'index.html'), '<!doctype html><html lang="ko"><head><meta charset="utf-8"><link rel="icon" href="data:,"><link rel="stylesheet" href="property-fields.css"></head><body style="margin:0"><div id="root"></div><script type="module" src="property-fields.js"></script></body></html>')
|
||
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.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(site, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); if (path.relative(site, 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 fixture = `http://127.0.0.1:${server.address().port}/`
|
||
const browser = await chromium.launch({ executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
|
||
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 }, deviceScaleFactor: 2 })
|
||
const report = { label, checks: [], errors: [] }
|
||
page.on('pageerror', e => report.errors.push(e.message)); page.on('console', m => { if (m.type() === 'error') report.errors.push(m.text()) })
|
||
const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name, JSON.stringify(data)) }
|
||
const shot = async (name, locator) => writeFile(path.join(out, `${label}-${name}.png`), await (locator ?? page).screenshot())
|
||
|
||
await page.goto(fixture)
|
||
await page.locator('[data-fields="live"] .hm-sw-property').waitFor(); await page.evaluate(() => document.fonts.ready)
|
||
const live = page.locator('[data-fields="live"]')
|
||
const row = id => live.locator(`.hm-sw-property__row[data-property-id="${id}"]`)
|
||
const field = id => row(id).locator('.hm-sw-property__value').first()
|
||
const log = () => page.locator('[data-property-log]').textContent()
|
||
const HOVER = 'rgba(33, 29, 19, 0.4)', CLEAR = 'rgba(0, 0, 0, 0)'
|
||
const style = (locator, ...props) => locator.evaluate((e, props) => { const s = getComputedStyle(e), r = e.getBoundingClientRect(); return Object.assign({ x: r.x, y: r.y, w: +r.width.toFixed(2), h: +r.height.toFixed(2) }, ...props.map(p => ({ [p]: s[p] }))) }, props)
|
||
const textBox = async id => row(id).locator('.hm-sw-header__input, .hm-sw-property__text').first().evaluate(e => { const r = e.getBoundingClientRect(); return [r.x, r.y] })
|
||
await page.mouse.move(1, 1)
|
||
|
||
// 1. Geometry (Default): 200 x 28 boxes, 28px compact (선가중치 / 투명도 value), radius 2, no border, no surface.
|
||
for (const id of ['layer-mixed', 'layer', 'color', 'line', 'scale', 'link', 'thickness', 'material']) {
|
||
const s = await style(field(id), 'borderTopColor', 'backgroundColor', 'borderTopLeftRadius')
|
||
check(`Default ${id}: 200 x 28, radius 2, no border / surface`, s.w === 200 && s.h === 28 && s.borderTopColor === CLEAR && s.backgroundColor === CLEAR && s.borderTopLeftRadius === '2px', s)
|
||
}
|
||
// 2026-11-10: since the user-approved 2026-10-21 design review (2fc1b3e) 선가중치 is no longer the 41px hug field but the
|
||
// 200 x 28 box like 선축적, and the 투명도 value is its own 28px box before the gauge (SWHeaderPropertyPanel.tsx). The
|
||
// same properties are checked on the current fields: size, padding (7px incl. no border), radius, no border / surface.
|
||
const compactField = id => id === 'weight' ? field('weight') : row(id).locator('.hm-sw-property__slider-value')
|
||
for (const id of ['weight', 'opacity']) {
|
||
const s = await style(compactField(id), 'borderTopColor', 'backgroundColor', 'paddingLeft', 'paddingRight', 'borderTopLeftRadius')
|
||
check(`Default ${id}: ${id === 'weight' ? '200 x 28 box' : '28px slider value box'} (pl / pr 7), radius 2, no border / surface`, s.h === 28 && (id === 'weight' ? s.w === 200 : s.w < 200) && s.borderTopColor === CLEAR && s.backgroundColor === CLEAR && s.paddingLeft === '7px' && s.paddingRight === '7px' && s.borderTopLeftRadius === '2px', s)
|
||
}
|
||
const typo = await row('scale').evaluate(r => { const v = getComputedStyle(r.querySelector('.hm-sw-header__input')), u = getComputedStyle(r.querySelector('.hm-sw-property__unit')); return { value: [v.color, v.fontSize], unit: [u.color, u.fontSize] } })
|
||
check('Typography: value #E60403 14px, unit #000 12px', typo.value.join() === 'rgb(230, 4, 3),14px' && typo.unit.join() === 'rgb(0, 0, 0),12px', typo)
|
||
await shot('default', live.locator('.hm-sw-property'))
|
||
|
||
// 2. Real pointer Hover → border only, value does not move; Mouse Leave → Default.
|
||
for (const id of ['layer-mixed', 'layer', 'color', 'line', 'scale', 'link', 'thickness', 'material']) {
|
||
const before = await textBox(id)
|
||
await field(id).hover()
|
||
const s = await style(field(id), 'borderTopColor', 'backgroundColor')
|
||
const after = await textBox(id)
|
||
const extra = id === 'color' ? await style(row(id).locator('.hm-sw-property__color-value'), 'borderRightColor') : {}
|
||
check(`Hover ${id}: rgba(33,29,19,.4) border, 28px kept, value not moved${id === 'color' ? ', divider shown' : ''}`, s.borderTopColor === HOVER && s.h === 28 && s.backgroundColor === CLEAR && before.join() === after.join() && (id !== 'color' || extra.borderRightColor === HOVER), { ...s, before, after, ...extra })
|
||
await page.mouse.move(1, 1)
|
||
const left = await style(field(id), 'borderTopColor')
|
||
check(`Mouse Leave ${id}: back to Default`, left.borderTopColor === CLEAR, left)
|
||
}
|
||
for (const id of ['weight', 'opacity']) {
|
||
const c = compactField(id)
|
||
await c.hover(); const s = await style(c, 'borderTopColor'); await page.mouse.move(1, 1); const l = await style(c, 'borderTopColor')
|
||
check(`Hover / Leave ${id} ${id === 'weight' ? 'box' : 'slider value box'}`, s.borderTopColor === HOVER && l.borderTopColor === CLEAR, { hover: s.borderTopColor, leave: l.borderTopColor })
|
||
}
|
||
await field('scale').hover(); await shot('hover-scale', field('scale')); await page.mouse.move(1, 1)
|
||
|
||
// 3. On (editing): numbers only. #0F0D0A, unit white, outlined value, real input focus; Enter commits (4 decimals).
|
||
for (const id of ['scale', 'thickness']) {
|
||
await row(id).locator('.hm-sw-property__unit').click()
|
||
const s = await style(field(id), 'backgroundColor', 'borderTopColor')
|
||
const u = await style(row(id).locator('.hm-sw-property__unit'), 'color')
|
||
const input = await row(id).locator('input').evaluate(e => ({ focused: document.activeElement === e, caret: getComputedStyle(e).caretColor, shadow: getComputedStyle(e).textShadow !== 'none' }))
|
||
check(`On(편집) ${id}: #0F0D0A, border, unit #FFF, focused input with red caret + outline`, s.backgroundColor === 'rgb(15, 13, 10)' && s.borderTopColor === HOVER && u.color === 'rgb(255, 255, 255)' && input.focused && input.caret === 'rgb(230, 4, 3)' && input.shadow && s.h === 28, { ...s, unit: u.color, ...input })
|
||
await shot(`on-${id}`, field(id))
|
||
await page.keyboard.press('Escape')
|
||
}
|
||
const scale = row('scale').locator('input')
|
||
await scale.click(); await scale.fill('2'); await page.keyboard.press('Enter')
|
||
check('Enter: 선축적 2 → validated, 4 decimals committed', (await log()).includes('property(scale, 2.0000)') && await scale.inputValue() === '2.0000', await log())
|
||
await scale.fill('7'); await page.keyboard.press('Escape')
|
||
check('Escape: draft cancelled (2.0000 restored, no callback)', await scale.inputValue() === '2.0000' && !(await log()).includes('7'), await scale.inputValue())
|
||
const weight = row('weight').locator('input')
|
||
// 2fc1b3e (2026-10-21): the editing face is on the 200px box (as 선축적), no longer on the hug field's input.
|
||
await weight.click(); const w = await style(field('weight'), 'backgroundColor')
|
||
check('On(편집) 선가중치 box: #0F0D0A', w.backgroundColor === 'rgb(15, 13, 10)', w)
|
||
await shot('on-weight', row('weight'))
|
||
await weight.fill('-1'); await page.keyboard.press('Enter')
|
||
check('Invalid 선가중치 -1 (min 0): restored, no callback', await weight.inputValue() === '0.25' && !(await log()).includes('weight, -1'), await weight.inputValue())
|
||
await weight.fill('0.5'); await page.keyboard.press('Enter')
|
||
check('선가중치 0.5 committed', (await log()).includes('property(weight, 0.5)'), await log())
|
||
await page.mouse.click(1, 1)
|
||
|
||
// 4. 하이퍼링크 (editable text): since 6d46289 (2026-10-22, approved) its editing face is the numbers' On (#0F0D0A,
|
||
// Hover border); 재료 (computed) stays read-only without On.
|
||
await row('link').locator('input').click()
|
||
const link = await style(field('link'), 'backgroundColor', 'borderTopColor')
|
||
check('하이퍼링크 focus: editing face #0F0D0A like numbers (6d46289), Hover border, editable', link.backgroundColor === 'rgb(15, 13, 10)' && link.borderTopColor === HOVER, link)
|
||
await row('link').locator('input').fill('http://a'); await page.keyboard.press('Enter')
|
||
check('하이퍼링크 commit via host callback', (await log()).includes('property(link, http://a)'), await log())
|
||
await row('material').locator('input').click()
|
||
const mat = await style(field('material'), 'backgroundColor')
|
||
check('재료: read-only, no On', mat.backgroundColor === CLEAR && await row('material').locator('input').evaluate(e => e.readOnly), mat)
|
||
await page.mouse.click(1, 1)
|
||
|
||
// 5. Layer dropdown (26:12265 / 26:12291): #211D13, 234px list right-aligned under the field, rows 30, footer 40.
|
||
const layerTrigger = field('layer').locator('.hm-sw-property__trigger')
|
||
await layerTrigger.click()
|
||
const drop = page.locator('.hm-sw-property__drop')
|
||
let f = await style(field('layer'), 'backgroundColor', 'borderTopColor'), d = await style(drop.first())
|
||
const rows = await drop.locator('.hm-sw-header__layer-row').count()
|
||
const rowH = await style(drop.locator('.hm-sw-header__layer-row').first()), foot = await style(drop.locator('.hm-sw-header__layer-footer'))
|
||
check('Layer On: #211D13 field, 234 wide list, right edges meet, starts at field bottom', f.backgroundColor === 'rgb(33, 29, 19)' && d.w === 234 && Math.abs(d.x + d.w - (f.x + f.w)) < 0.5 && Math.abs(d.y - (f.y + f.h)) < 0.5, { f, d })
|
||
check('Layer list rows 30px, footer 40px, height = rows*30+40', rowH.h === 30 && foot.h === 40 && Math.abs(d.h - (rows * 30 + 40)) < 0.5, { rows, row: rowH.h, footer: foot.h, h: d.h })
|
||
check('Layer current row marked (0_Layer_Cludmark)', await drop.locator('[aria-current="true"]').textContent() === '0_Layer_Cludmark', null)
|
||
await shot('on-layer', page)
|
||
await drop.locator('.hm-sw-header__layer-row').nth(3).locator('.hm-sw-header__layer-toggle').first().click()
|
||
check('Bulb toggle: callback only, list stays open, row not selected', (await log()).includes('layer.visible(Sample-Wall, true)') && await drop.count() === 1 && !(await log()).includes('property(layer, Sample-Wall)'), await log())
|
||
await drop.getByRole('button', { name: 'CC-D GNL', exact: true }).click()
|
||
check('Row select: property(layer, CC-D GNL), closes, field shows value', (await log()).includes('property(layer, CC-D GNL)') && await drop.count() === 0 && (await field('layer').textContent()).includes('CC-D GNL'), await log())
|
||
check('Close keeps the value and focus returns to the field', await layerTrigger.evaluate(e => document.activeElement === e), null)
|
||
// Mixed (다양함): no current row; host switch to single value.
|
||
await field('layer-mixed').locator('.hm-sw-property__trigger').click()
|
||
check('다양함: text 다양함, no current row in list', (await field('layer-mixed').locator('.hm-sw-property__trigger').textContent()) === '다양함' && await drop.locator('[aria-current="true"]').count() === 0, null)
|
||
await shot('on-layer-mixed', page)
|
||
await page.keyboard.press('Escape')
|
||
check('Escape closes the layer list; panel stays', await drop.count() === 0 && await live.locator('.hm-sw-property').count() === 1, null)
|
||
await page.getByRole('button', { name: '레이어 다양함 ↔ 단일' }).click()
|
||
check('Host data: 다양함 → 단일 값 (0_Layer_Cludmark)', (await field('layer-mixed').textContent()) === '0_Layer_Cludmark', await field('layer-mixed').textContent())
|
||
await page.getByRole('button', { name: '레이어 외부 값 변경' }).click()
|
||
check('External value change shown (CC-D GNL)', (await field('layer').textContent()) === 'CC-D GNL', null)
|
||
// Keyboard: Enter opens, ArrowDown moves into the list, Escape closes back to the trigger.
|
||
await layerTrigger.focus(); await page.keyboard.press('ArrowDown')
|
||
const focusedInList = await page.evaluate(() => !!document.activeElement?.closest('.hm-sw-property__drop'))
|
||
await page.keyboard.press('Escape')
|
||
check('Keyboard: ArrowDown opens + focuses list, Escape closes to trigger', focusedInList && await drop.count() === 0 && await layerTrigger.evaluate(e => document.activeElement === e), null)
|
||
await layerTrigger.click(); await page.mouse.click(1300, 900)
|
||
check('Outside click closes', await drop.count() === 0, null)
|
||
|
||
// 6. Line style (26:12388 / 26:12404): right-aligned, preview follows the value. Since 2fc1b3e (2026-10-21 design
|
||
// review, DESIGN_RULES 속성창 입력) the property window list is the line type column only (preview + name; type column
|
||
// 182px, growing to 240px only for wider font names); weights / scale stay in the SWHeader line style field.
|
||
await field('line').locator('.hm-sw-property__trigger').click()
|
||
f = await style(field('line'), 'backgroundColor'); d = await style(drop.locator('.hm-sw-header__line-drop'))
|
||
const cols = await drop.locator('.hm-sw-header__line-column').evaluateAll(es => es.map(e => +e.getBoundingClientRect().width.toFixed(2)))
|
||
const unitOn = await style(field('line').locator('.hm-sw-property__unit'), 'color')
|
||
const typeOnly = await drop.locator('.hm-sw-header__line-column').evaluateAll(es => es.length === 1 && es[0].classList.contains('hm-sw-header__line-column--type'))
|
||
check('Line style On: #211D13, (ByL) #D4CBBD, 1 column (line types), right edges meet (or shifted to the 4px screen edge when no room)', f.backgroundColor === 'rgb(33, 29, 19)' && unitOn.color === 'rgb(212, 203, 189)' && cols.length === 1 && typeOnly && (Math.abs(d.x + d.w - (f.x + f.w)) < 0.5 || d.x === 4) && Math.abs(d.y - (f.y + f.h)) < 0.5, { f, d, cols, typeOnly })
|
||
check('Line style drop 182–240 x 190 (type column grows only for wider font names)', d.w >= 182 && d.w <= 240 && Math.abs(d.w - cols[0]) < 0.5 && d.h === 190, { w: d.w, h: d.h, cols })
|
||
await shot('on-line', page)
|
||
// No weight column: the only ByBlock entry is the line type, and no weight / scale callback was called.
|
||
const byBlock = await drop.getByRole('button', { name: 'ByBlock', exact: true }).count()
|
||
check('Weight column: absent (ByBlock only as a line type), no lineWeight / lineScale callback, list stays open', byBlock === 1 && !/lineWeight|lineScale/.test(await log()) && await drop.count() === 1, { byBlock, log: await log() })
|
||
await drop.getByRole('button', { name: 'CENTER' }).click()
|
||
const segs = await field('line').locator('.hm-sw-header__line-preview > i').count()
|
||
check('Line type select: property(line, CENTER), closes, preview = CENTER pattern (4 segments)', (await log()).includes('property(line, CENTER)') && await drop.count() === 0 && segs === 4, { segs })
|
||
await page.getByRole('button', { name: '선스타일 외부 CENTER' }).click()
|
||
|
||
// 7. Color (26:12170 / 26:12184): RGB value | swatch, 192px dropdown left-aligned, ByLayer / ByBlock, chips, pick, validation.
|
||
const colorInput = field('color').locator('.hm-sw-property__color-value input')
|
||
check('Figma sample 255,259,0 shown as host text, swatch from host', await colorInput.inputValue() === '255,259,0', null)
|
||
await colorInput.click()
|
||
f = await style(field('color')); d = await style(drop.first())
|
||
const left = await style(field('color').locator('.hm-sw-property__color-value'), 'backgroundColor', 'borderRightColor')
|
||
check('Color On: dropdown 192 wide, left edges meet, RGB area #211D13 + divider', d.w === 192 && Math.abs(d.x - f.x) < 0.5 && Math.abs(d.y - (f.y + f.h)) < 0.5 && left.backgroundColor === 'rgb(33, 29, 19)' && left.borderRightColor === HOVER, { f, d, left })
|
||
check('Color dropdown height 158 (Figma)', d.h === 158, d.h)
|
||
await shot('on-color', page)
|
||
await drop.getByRole('button', { name: 'ByBlock' }).click()
|
||
check('ByBlock: callback, required single choice (ByBlock pressed, ByLayer not)', (await log()).includes('colorMode(byBlock)') && await drop.getByRole('button', { name: 'ByBlock' }).getAttribute('aria-pressed') === 'true' && await drop.getByRole('button', { name: 'ByLayer' }).getAttribute('aria-pressed') === 'false', null)
|
||
await drop.getByRole('button', { name: 'ByBlock' }).click()
|
||
check('Clicking the pressed mode is a no-op', (await log()).split('colorMode').length === 2, null)
|
||
await drop.getByRole('button', { name: '빨강' }).click()
|
||
check('Chip: property(color, 241,4,4)', (await log()).includes('property(color, 241,4,4)'), await log())
|
||
const inner = drop.getByRole('textbox', { name: '색상값 (R,G,B)' })
|
||
await inner.fill('255,259,0'); await page.keyboard.press('Enter')
|
||
check('Out-of-range 255,259,0 rejected (no callback, restored)', !(await log()).includes('255,259,0') && await inner.inputValue() === '241,4,4', await inner.inputValue())
|
||
await inner.fill('1, 2, 3'); await page.keyboard.press('Enter')
|
||
check('Valid RGB committed normalized (1,2,3)', (await log()).includes('property(color, 1,2,3)'), await log())
|
||
await drop.getByRole('button', { name: '색상 추출' }).click()
|
||
check('btn_colorize: host callback, dropdown closes', (await log()).includes('colorPick(color)') && await drop.count() === 0, null)
|
||
await colorInput.click(); await colorInput.fill('300,0,0'); await page.keyboard.press('Enter')
|
||
check('Field RGB 300,0,0 rejected', !(await log()).includes('300,0,0') && await colorInput.inputValue() === '1,2,3', await colorInput.inputValue())
|
||
await colorInput.fill('9,9,9'); await page.keyboard.press('Escape')
|
||
check('Escape #1 cancels draft, dropdown still open', await colorInput.inputValue() === '1,2,3' && await drop.count() === 1, null)
|
||
await page.keyboard.press('Escape')
|
||
check('Escape #2 closes the dropdown, panel stays', await drop.count() === 0 && await live.locator('.hm-sw-property').count() === 1, null)
|
||
|
||
// 8. Disabled / ReadOnly and external updates.
|
||
await page.getByRole('button', { name: '두께·레이어 ReadOnly' }).click()
|
||
await page.getByRole('button', { name: '선축적·색상 Disabled' }).click()
|
||
await row('thickness').locator('input').click()
|
||
const ro = await style(field('thickness'), 'backgroundColor')
|
||
await field('layer').locator('.hm-sw-property__trigger').click({ force: true })
|
||
check('ReadOnly 두께: no On; ReadOnly 레이어: no dropdown', ro.backgroundColor === CLEAR && await drop.count() === 0, ro)
|
||
await field('thickness').hover()
|
||
check('ReadOnly keeps Hover border', (await style(field('thickness'), 'borderTopColor')).borderTopColor === HOVER, null)
|
||
await colorInput.click({ force: true })
|
||
await field('scale').hover()
|
||
check('Disabled 색상: no dropdown; Disabled 선축적: no hover border', await drop.count() === 0 && (await style(field('scale'), 'borderTopColor')).borderTopColor === CLEAR, null)
|
||
await page.getByRole('button', { name: '두께·레이어 ReadOnly' }).click(); await page.getByRole('button', { name: '선축적·색상 Disabled' }).click()
|
||
await page.getByRole('button', { name: '선축적 외부 값 변경' }).click(); await page.getByRole('button', { name: '두께 단위 변경' }).click()
|
||
check('External value / unit props: 선축적 2.5000, 두께 cm', await scale.inputValue() === '2.5000' && (await row('thickness').locator('.hm-sw-property__unit').textContent()) === 'cm', null)
|
||
await page.getByRole('button', { name: '속성 추가' }).click(); await page.getByRole('button', { name: '하이퍼링크 삭제' }).click()
|
||
const ids = await live.locator('.hm-sw-property__row').evaluateAll(es => es.map(e => e.dataset.propertyId))
|
||
check('Dynamic rows: added at the end, removed row gone, order kept', ids.at(-1).startsWith('extra-') && !ids.includes('link') && ids[0] === 'layer-mixed', ids)
|
||
await page.getByRole('button', { name: /menus 없음/ }).click()
|
||
await field('line').locator('.hm-sw-property__trigger').click()
|
||
const fallback = await drop.locator('.hm-sw-header__line-column').count()
|
||
await page.keyboard.press('Escape')
|
||
check('Without menus: line types from options only (1 column), no crash', fallback === 1, fallback)
|
||
|
||
// 9. Pinned Hover specimen matches the real hover.
|
||
const pinned = await page.locator('[data-fields="hover"] .hm-sw-property__value[data-field]').first().evaluate(e => getComputedStyle(e).borderTopColor)
|
||
check('Pinned Hover specimen uses the same border', pinned === HOVER, pinned)
|
||
|
||
// 10. Four property panel states keep their Figma sizes.
|
||
await page.goto(`${base}/iframe.html?id=sw-header--property-panel-compare&viewMode=story`)
|
||
await page.locator('.hm-sw-property').first().waitFor(); await page.evaluate(() => document.fonts.ready)
|
||
const sizes = await page.locator('[data-compare]').evaluateAll(cells => cells.map(c => { const r = c.querySelector('.hm-sw-property').getBoundingClientRect(); return [c.dataset.compare, Math.round(r.width), Math.round(r.height)] }))
|
||
const want = { none: [314, 256], all: [314, 361], circle: [314, 658], expanded: [314, 754] }
|
||
check('4 panel states: 314 x 256 / 361 / 658 / 754', sizes.every(([k, w, h]) => w === want[k][0] && h === want[k][1]), sizes)
|
||
await shot('compare', page.locator('.sw-story-compare'))
|
||
// The header-hosted panel: layer field opens its list over the header page as well.
|
||
await page.goto(`${fixture}?view=header`)
|
||
await page.locator('.hm-sw-header').waitFor()
|
||
await page.getByRole('button', { name: '원 1개', exact: true }).click()
|
||
await page.locator('.hm-sw-header [data-property-panel-trigger]').click()
|
||
await page.locator('.hm-sw-property .hm-sw-property__row[data-property-id="layer"] .hm-sw-property__trigger').click()
|
||
const hosted = await page.evaluate(() => { const d = document.querySelector('.hm-sw-property__drop'), f = document.querySelector('.hm-sw-property__row[data-property-id="layer"] .hm-sw-property__value'); if (!d) return null; const a = d.getBoundingClientRect(), b = f.getBoundingClientRect(); return { right: a.right - b.right, top: a.top - b.bottom, panelOpen: !!document.querySelector('.hm-sw-property') } })
|
||
await page.keyboard.press('Escape')
|
||
const stillOpen = await page.locator('.hm-sw-property').count()
|
||
check('Header-hosted panel: layer list attached; Escape closes list only', hosted && Math.abs(hosted.right) < 0.5 && Math.abs(hosted.top) < 0.5 && stillOpen === 1, { hosted, stillOpen })
|
||
check('no console / page errors', report.errors.length === 0, report.errors)
|
||
await writeFile(path.join(out, `${label}-result.json`), JSON.stringify(report, null, 2))
|
||
await browser.close(); server.close()
|
||
const failed = report.checks.filter(c => c.status === 'FAIL').length
|
||
console.log(`${report.checks.length - failed}/${report.checks.length} PASS`)
|
||
process.exit(failed ? 1 : 0)
|