Remove nine SW/Header demo pages (property-fields, property-shape-slot, composition, colorize, dynamic-controls, property-bar, additional-hover, property-panel, layer-detail-input) and the demo code and Storybook-only CSS used only by them. SW Stories 19 -> 10, total 338 -> 329. The package runtime, public API, AI template and the retained Stories (Overview, Dropdown, Property Panel 4-state comparison, Window Move Motion and others) are unchanged. Add a current regression script and mark the older scripts that open the retired IDs as historical evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
230 lines
21 KiB
JavaScript
230 lines
21 KiB
JavaScript
// Historical since 2026-10-19: opens SW Story IDs retired by the unused demo removal (property-fields). Kept as evidence
|
|
// for its recorded run; current regression: storybook-unused-removal.browser.mjs.
|
|
// 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.
|
|
// Usage: node packages/sw/tests/property-fields.browser.mjs <evidence-dir> <label> [storybook-url]
|
|
// Storybook serves packages/sw/dist: build @hm-design/sw first.
|
|
import { createRequire } from 'node:module'
|
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
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 } = createRequire(import.meta.url)(process.env.SW_PLAYWRIGHT_PATH ?? 'C:/Users/user/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright')
|
|
await mkdir(out, { recursive: true })
|
|
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(`${base}/iframe.html?id=sw-header--property-fields&viewMode=story`)
|
|
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)
|
|
}
|
|
for (const id of ['weight', 'opacity']) {
|
|
const s = await style(row(id).locator('.hm-sw-property__compact'), 'borderTopColor', 'backgroundColor', 'paddingLeft', 'paddingRight')
|
|
check(`Default ${id}: compact 28px hug field (pl 6 / pr 4 incl. border), no border`, s.h === 28 && s.w < 60 && s.borderTopColor === CLEAR && s.paddingLeft === '5px' && s.paddingRight === '3px', 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 = row(id).locator('.hm-sw-property__compact')
|
|
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} compact`, 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')
|
|
await weight.click(); const w = await style(weight, 'backgroundColor')
|
|
check('On(편집) 선가중치 compact: #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. No invented On: 하이퍼링크 (text) focus keeps the Hover look only; 재료 (computed) is read-only.
|
|
await row('link').locator('input').click()
|
|
const link = await style(field('link'), 'backgroundColor', 'borderTopColor')
|
|
check('하이퍼링크 focus: no On surface (Figma has no On), editable', link.backgroundColor === CLEAR && 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): 466 = 182 + 142 + 142, right-aligned, preview follows the value.
|
|
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')
|
|
check('Line style On: #211D13, (ByL) #D4CBBD, 3 columns, 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 === 3 && (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 })
|
|
check('Line style drop 466 x 190 (type column grows only for wider font names)', d.w >= 466 && d.h === 190, { w: d.w, h: d.h, cols })
|
|
await shot('on-line', page)
|
|
await drop.getByRole('button', { name: 'ByBlock', exact: true }).nth(1).click()
|
|
check('Weight column: callback only, stays open', (await log()).includes('lineWeight(w-byblock)') && await drop.count() === 1, 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(`${base}/iframe.html?id=sw-header--property-panel&viewMode=story`)
|
|
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()
|
|
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)
|