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.
221 lines
22 KiB
JavaScript
221 lines
22 KiB
JavaScript
// 2026-10-21 Firefox counterpart of viewer-logo-document-menu.browser.mjs: the installed Firefox over WebDriver BiDi (no
|
||
// Playwright build), real pointer clicks and key presses. Logo menu (SWHeaderMore) and file-name list
|
||
// (SWHeaderDocumentTitle) of SWViewerHeader: open / select / Escape / outside click / focus return / never together,
|
||
// title + divider rule, same face as the SWHeader `...` menu, responsive icon toggles and panels.
|
||
// Usage: node packages/sw/tests/viewer-logo-document-menu-firefox.browser.mjs <evidence-dir> [storybook-url]
|
||
import { mkdir, writeFile, rm } from 'node:fs/promises'
|
||
import { spawn } from 'node:child_process'
|
||
import os from 'node:os'
|
||
import path from 'node:path'
|
||
import { runtimeRequire } from './playwright-runtime.mjs'
|
||
|
||
const out = path.resolve(process.argv[2] ?? 'docs/sw/viewer-logo-document-menu-qa/firefox')
|
||
const base = process.argv[3] ?? 'http://127.0.0.1:6024'
|
||
const FIREFOX = process.env.SW_FIREFOX_PATH ?? 'C:/Program Files/Mozilla Firefox/firefox.exe'
|
||
const VIEW = { width: 1920, height: 1100 }
|
||
const sleep = ms => new Promise(r => setTimeout(r, ms))
|
||
await mkdir(out, { recursive: true })
|
||
|
||
const profile = path.join(os.tmpdir(), `sw-ff-viewer-menu-${process.pid}`)
|
||
await mkdir(profile, { recursive: true })
|
||
const port = 9300 + (process.pid % 500)
|
||
const proc = spawn(FIREFOX, ['--headless', '--no-remote', '--profile', profile, `--remote-debugging-port=${port}`], { stdio: 'ignore', env: { ...process.env, MOZ_HEADLESS_WIDTH: String(VIEW.width), MOZ_HEADLESS_HEIGHT: String(VIEW.height) } })
|
||
let ws
|
||
for (let i = 0; i < 60 && !ws; i++) {
|
||
await sleep(500)
|
||
try { ws = await new Promise((res, rej) => { const s = new WebSocket(`ws://127.0.0.1:${port}/session`); s.onopen = () => res(s); s.onerror = rej }) } catch { ws = undefined }
|
||
}
|
||
if (!ws) throw new Error('Firefox BiDi not reachable')
|
||
let id = 0; const pending = new Map(), errors = []
|
||
ws.onmessage = event => {
|
||
const m = JSON.parse(event.data)
|
||
if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id); m.type === 'error' ? rej(new Error(`${m.error}: ${m.message}`)) : res(m.result) }
|
||
// Storybook's own UI font (sb-common-assets) is served by the dev server only, not by a static build; the Chromium
|
||
// suites ignore such 404s too.
|
||
else if (m.method === 'log.entryAdded' && m.params.level === 'error' && !/sb-common-assets/.test(m.params.text)) errors.push(m.params.text)
|
||
}
|
||
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej: e => rej(new Error(method + ' -> ' + e.message)) }); ws.send(JSON.stringify({ id: i, method, params })) })
|
||
const session = await send('session.new', { capabilities: {} })
|
||
await send('session.subscribe', { events: ['log.entryAdded'] })
|
||
// A tab of our own: in current Firefox the first tree entry can be a privileged (chrome) context that BiDi scripts
|
||
// and viewport calls may not touch without -remote-allow-system-access.
|
||
const context = (await send('browsingContext.create', { type: 'tab' })).context
|
||
const viewport = async size => { try { await send('browsingContext.setViewport', { context, viewport: size }) } catch (e) { console.log('INFO setViewport:', e.message) } }
|
||
await viewport(VIEW)
|
||
const ev = async expr => { const r = await send('script.evaluate', { expression: expr, target: { context }, awaitPromise: true }); if (r.type === 'exception') throw new Error(r.exceptionDetails.text + ' :: ' + expr.slice(0, 160)); return r.result.value }
|
||
const goto = async story => {
|
||
await send('browsingContext.navigate', { context, url: `${base}/iframe.html?id=${story}&viewMode=story`, wait: 'complete' })
|
||
for (let i = 0; i < 40; i++) { if (await ev(`!!document.querySelector('.hm-sw-viewer-header')`)) break; await sleep(250) }
|
||
// Lift the Story page envelope like the Chromium suites; scroll instantly (the preview sets smooth scrolling).
|
||
await ev(`(() => { const s = document.createElement('style'); s.textContent = '.sb-page { --sb-page-max-width: 100vw; } html { scroll-behavior: auto !important; }'; document.head.append(s); return true })()`)
|
||
await ev('document.fonts.ready.then(() => true)'); await sleep(400)
|
||
}
|
||
const frames = () => ev('new Promise(r => requestAnimationFrame(() => requestAnimationFrame(() => r(true))))')
|
||
const pointer = actions => send('input.performActions', { context, actions: [{ type: 'pointer', id: 'mouse', parameters: { pointerType: 'mouse' }, actions }] })
|
||
// Click the center of the first visible match (scrolled into view first), with a real pointer.
|
||
const click = async selector => {
|
||
const point = JSON.parse(await ev(`(() => { const el = [...document.querySelectorAll(${JSON.stringify(selector)})].find(e => e.getClientRects().length)
|
||
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' }); const r = el.getBoundingClientRect(); return JSON.stringify([r.x + r.width / 2, r.y + r.height / 2]) })()`))
|
||
await pointer([{ type: 'pointerMove', x: Math.round(point[0]), y: Math.round(point[1]), duration: 0 }, { type: 'pointerDown', button: 0 }, { type: 'pointerUp', button: 0 }])
|
||
await frames()
|
||
}
|
||
const clickAt = async (x, y) => { await pointer([{ type: 'pointerMove', x, y, duration: 0 }, { type: 'pointerDown', button: 0 }, { type: 'pointerUp', button: 0 }]); await frames() }
|
||
const KEYS = { ArrowDown: '\uE015', Escape: '\uE00C', Enter: '\uE007' }
|
||
const press = async key => { await send('input.performActions', { context, actions: [{ type: 'key', id: 'kbd', actions: [{ type: 'keyDown', value: KEYS[key] }, { type: 'keyUp', value: KEYS[key] }] }] }); await frames() }
|
||
const focus = selector => ev(`(document.querySelector(${JSON.stringify(selector)}).focus(), true)`)
|
||
const shot = async (file, selector, width, height) => {
|
||
const clip = JSON.parse(await ev(`(() => { const r = document.querySelector(${JSON.stringify(selector)}).getBoundingClientRect(); return JSON.stringify({ x: Math.max(0, r.x - 8), y: Math.max(0, r.y - 8) }) })()`))
|
||
const result = await send('browsingContext.captureScreenshot', { context, clip: { type: 'box', ...clip, width, height } })
|
||
await writeFile(path.join(out, file), Buffer.from(result.data, 'base64'))
|
||
}
|
||
|
||
const checks = []; const check = (n, ok, d) => { checks.push({ n, ok, d }); console.log(ok ? 'PASS' : 'FAIL', n, ok ? '' : String(JSON.stringify(d) ?? '').slice(0, 600)) }
|
||
const state = () => ev(`(() => {
|
||
const header = document.querySelector('.hm-sw-viewer-header'), logo = header.querySelector('button.hm-sw-viewer-header__brand')
|
||
const menu = header.querySelector('.hm-sw-header__more-menu'), list = header.querySelector('.hm-sw-header__document-menu')
|
||
const name = header.querySelector('.hm-sw-viewer-header__document .hm-sw-header__document-button'), active = document.activeElement
|
||
return JSON.stringify({ logoExpanded: logo?.getAttribute('aria-expanded'), menu: !!menu, list: !!list, nameExpanded: name?.getAttribute('aria-expanded'),
|
||
name: name?.textContent.trim(), focus: active === logo ? 'logo' : active === name ? 'name' : (active?.textContent ?? '').trim(),
|
||
log: document.querySelector('[data-callback-log]')?.textContent ?? '' })
|
||
})()`).then(JSON.parse)
|
||
// Face of an open menu and its first shown row, split into colors and the other metrics (2026-10-21 style unification:
|
||
// the logo menu and the document list take the metrics of the viewer dropdown, SWViewerMenu, and keep their colors).
|
||
const FACE = `(menu, itemSelector) => { const s = getComputedStyle(menu), item = [...menu.querySelectorAll(itemSelector)].find(e => e.offsetParent), i = getComputedStyle(item)
|
||
const m = menu.getBoundingClientRect(), b = item.getBoundingClientRect()
|
||
return { colors: { background: s.backgroundColor, border: s.borderTopColor, shadow: s.boxShadow, item: i.color },
|
||
metrics: { border: s.borderTopWidth + ' ' + s.borderTopStyle, radius: s.borderTopLeftRadius, padding: s.paddingTop + ' ' + s.paddingLeft, minWidth: s.minWidth, font: s.fontSize + '/' + s.lineHeight + ' ' + s.fontWeight,
|
||
itemHeight: Math.round(b.height), itemInset: Math.round(b.left - m.left), itemPadding: i.paddingLeft + ' ' + i.paddingRight, itemGap: i.columnGap, itemRadius: i.borderTopLeftRadius,
|
||
itemFont: i.fontFamily.split(',')[0] + ' ' + i.fontSize + '/' + i.lineHeight + ' ' + i.fontWeight } } }`
|
||
const faceOf = (menuSelector, itemSelector) => ev(`JSON.stringify((${FACE})(document.querySelector(${JSON.stringify(menuSelector)}), ${JSON.stringify(itemSelector)}))`).then(JSON.parse)
|
||
// Scrolled into view first (the 520 row sits at the bottom of the Story; its list with Open reaches below the fold).
|
||
const onTop = selector => ev(`(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) return false; el.scrollIntoView({ block: 'nearest', behavior: 'instant' }); const r = el.getBoundingClientRect(); return el.contains(document.elementFromPoint(r.x + r.width / 2, r.y + r.height - 6)) })()`)
|
||
|
||
try {
|
||
console.log('firefox', (await ev('navigator.userAgent')))
|
||
// ---------- SWHeader `...` menu reference ----------
|
||
await goto('sw-viewer--compare-with-sw-header')
|
||
// 2026-10-21: SWHeader shows More only below 760px: the first header's 1440px Story wrapper is narrowed to 700px for
|
||
// this reference measurement (face colors and metrics do not depend on the width).
|
||
await ev(`(() => { document.querySelector('.hm-sw-header').parentElement.style.width = '700px'; return true })()`); await sleep(300)
|
||
await click('.hm-sw-header .hm-sw-header__more-trigger')
|
||
const reference = await faceOf('.hm-sw-header .hm-sw-header__more-menu', '.hm-sw-header__more-item')
|
||
const BOX = `element => { const s = getComputedStyle(element); return [s.borderTopWidth, s.borderTopStyle, s.borderTopColor, s.borderTopLeftRadius, s.backgroundImage !== 'none' ? s.backgroundImage : s.backgroundColor, s.boxShadow].join(' | ') }`
|
||
const refTrigger = await ev(`(${BOX})(document.querySelector('.hm-sw-header .hm-sw-header__more-trigger'))`)
|
||
check('SWHeader ... menu opens (reference colors)', !!reference.colors.background, reference)
|
||
await press('Escape')
|
||
check('SWHeader ... menu Escape closes, focus back on ...', await ev(`!document.querySelector('.hm-sw-header .hm-sw-header__more-menu') && document.activeElement?.classList.contains('hm-sw-header__more-trigger')`))
|
||
|
||
// ---------- SWViewerMenu reference metrics ("상단 메뉴 + Dropdown") ----------
|
||
await goto('sw-viewer--dropdowns')
|
||
await click('.hm-sw-viewer-header [aria-haspopup="menu"]')
|
||
const viewerMenu = await faceOf('.hm-sw-viewer-menu', '.hm-sw-viewer-menu__item')
|
||
check('SWViewerMenu reference opens (36px rows, 4px padding)', viewerMenu.metrics.itemHeight === 36 && viewerMenu.metrics.padding === '4px 4px', viewerMenu.metrics)
|
||
await press('Escape')
|
||
|
||
// ---------- File open button (the logo menu is gone) ----------
|
||
await goto('sw-viewer--viewer-header')
|
||
await click('.hm-sw-viewer-header .hm-sw-viewer-header__brand')
|
||
let s = await state()
|
||
check('logo opens no dropdown', !s.menu && !(await ev(`!!document.querySelector('[role=menu]')`)), s)
|
||
const open = JSON.parse(await ev(`(() => { const b = document.querySelector('.hm-sw-viewer-header [data-document-open]'), w = b.parentElement
|
||
return JSON.stringify({ surface: b.classList.contains('hm-sw-header__document-button') && w.matches('.hm-sw-header__document[data-tool]'), afterLogo: w.previousElementSibling?.classList.contains('hm-sw-viewer-header__brand'),
|
||
label: b.querySelector('.hm-sw-viewer-header__open-label')?.textContent, icon: b.querySelectorAll('.hm-sw-viewer-header__open-icon svg').length }) })()`))
|
||
check('file open button after the logo: SWHeader document surface, icon + 파일 열기', open.surface && open.afterLogo && open.label === '파일 열기' && open.icon === 1, open)
|
||
const iconBox = await ev(`(${BOX})(document.querySelector('.hm-sw-viewer-header .hm-sw-viewer-header__open-icon'))`)
|
||
// 2026-10-21 user request: no border, transparent, no shadow.
|
||
check('file open icon: no border, transparent, no shadow', iconBox.startsWith('0px | none') && iconBox.includes('rgba(0, 0, 0, 0)') && iconBox.endsWith('none'), iconBox)
|
||
await click('.hm-sw-viewer-header [data-document-open]')
|
||
s = await state()
|
||
check('file open button runs the existing file open', s.log.endsWith('파일 선택창 (미연결) → document/open'), s)
|
||
await shot('firefox-header-open.png', '.hm-sw-viewer-header .hm-sw-viewer-header__brand', 640, 60)
|
||
|
||
// ---------- Document list ----------
|
||
// The logo cap (and its slanted tail) has the same pixels with the list open, as in SWHeader. Left out: the bottom
|
||
// 2px, which the list's own drop shadow (0 2px 4px: blur 4 − offset 2) reaches above the list (Firefox 2px, Chromium 1px).
|
||
const capClip = JSON.parse(await ev(`(() => { const r = document.querySelector('.hm-sw-viewer-header .hm-sw-viewer-header__brand').getBoundingClientRect(); return JSON.stringify({ x: r.x, y: r.y, width: r.width + 18, height: r.height - 2 }) })()`))
|
||
const capShot = async () => (await send('browsingContext.captureScreenshot', { context, clip: { type: 'box', ...capClip } })).data
|
||
const n = JSON.parse(await ev(`(() => { const r = document.querySelector('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button').getBoundingClientRect(); return JSON.stringify([r.x + r.width / 2, r.y + r.height / 2]) })()`))
|
||
await pointer([{ type: 'pointerMove', x: Math.round(n[0]), y: Math.round(n[1]), duration: 0 }]); await frames()
|
||
const capClosed = await capShot()
|
||
await click('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button')
|
||
check('open list never covers the logo cap (pixels identical)', capClosed === await capShot())
|
||
s = await state()
|
||
const list = JSON.parse(await ev(`(() => { const m = document.querySelector('.hm-sw-viewer-header .hm-sw-header__document-menu'), n = document.querySelector('.hm-sw-viewer-header .hm-sw-viewer-header__document .hm-sw-header__document-button')
|
||
const r = m.getBoundingClientRect(), b = n.getBoundingClientRect(), row = m.querySelector('.hm-sw-header__document-row').getBoundingClientRect()
|
||
return JSON.stringify({ rows: [...m.querySelectorAll('.hm-sw-header__document-item-name')].map(e => e.textContent.trim()), actions: m.querySelectorAll('.hm-sw-header__document-footer, .hm-sw-header__document-action').length,
|
||
active: m.querySelector('[data-active=true] .hm-sw-header__document-item-name')?.textContent, width: r.width, button: b.width, left: r.left - b.left, right: r.right - b.right, row: Math.round(row.height), gap: Math.round(r.y - b.bottom), background: getComputedStyle(m).backgroundColor }) })()`))
|
||
check('file name click opens the 3 samples only (no Open / + New), SWHeader face', s.list && s.nameExpanded === 'true' && list.rows.length === 3 && list.actions === 0 && list.row === 32 && Math.abs(list.gap) <= 1
|
||
&& list.background === 'rgb(16, 49, 39)' && list.active === 'C0060202-001-평면및종단면도.dwg', list)
|
||
check('document list >= file-name button (outer) and >= 312px, covers it', list.width + 0.5 >= list.button && list.width >= 311.5 && list.left <= 0.5 && list.right >= -0.5, list)
|
||
check('list painted on top (not cut)', await onTop('.hm-sw-viewer-header .hm-sw-header__document-menu'))
|
||
await shot('firefox-document-list.png', '.hm-sw-viewer-header .hm-sw-viewer-header__brand', 640, 200)
|
||
await click('.hm-sw-viewer-header .hm-sw-header__document-row:last-child .hm-sw-header__document-item')
|
||
s = await state()
|
||
check('sample select loads it, renames, closes, focus on the file name', s.name === 'simple.dxf' && s.log.includes('samples/sample-dxf') && !s.list && s.focus === 'name', s)
|
||
await press('ArrowDown')
|
||
s = await state()
|
||
check('ArrowDown opens the list on its first row', s.list && s.focus === 'BasicSample.dwg', s)
|
||
await press('Escape')
|
||
s = await state()
|
||
check('Escape closes the list and refocuses the file name', !s.list && s.focus === 'name', s)
|
||
|
||
// ---------- Above open panels ----------
|
||
await goto('sw-theme--theme-inheritance')
|
||
const themedArea = '[data-theme-case="viewer-themed"] .hm-sw-viewer-header'
|
||
await click(`${themedArea} .hm-sw-viewer-header__document .hm-sw-header__document-button`)
|
||
check('document list above an open SWViewerPanel', await onTop(`${themedArea} .hm-sw-header__document-menu`))
|
||
await press('Escape')
|
||
|
||
// ---------- Responsive ----------
|
||
await goto('sw-viewer--responsive')
|
||
const report = JSON.parse(await ev(`JSON.stringify([...document.querySelectorAll('[data-width] .hm-sw-viewer-header')].map(h => { const rect = s => h.querySelector(s).getBoundingClientRect()
|
||
const toggles = [...h.querySelectorAll('.hm-sw-viewer-header__button[data-kind=toggle]')], object = h.querySelector('.hm-sw-viewer-header__object'), open = h.querySelector('[data-document-open]')
|
||
return { width: Math.round(h.getBoundingClientRect().width), overflow: h.scrollWidth - h.clientWidth, objectOverflow: object.scrollWidth - object.clientWidth,
|
||
toggles: toggles.map(t => [t.getAttribute('aria-label') ?? t.textContent.trim(), !!t.querySelector('.hm-sw-viewer-header__label'), !!t.title]),
|
||
fit: [...object.querySelectorAll('button')].some(b => (b.getAttribute('aria-label') ?? b.textContent.trim()) === '전체 보기'), more: !!h.querySelector('.hm-sw-viewer-header__more'),
|
||
open: [!!open.querySelector('.hm-sw-viewer-header__open-label'), open.title, open.querySelector('.hm-sw-viewer-header__open-icon').getBoundingClientRect().width],
|
||
order: [rect('.hm-sw-viewer-header__brand').right, open.getBoundingClientRect().left, open.getBoundingClientRect().right, rect('.hm-sw-viewer-header__document').left, rect('.hm-sw-viewer-header__document').right, rect('.hm-sw-viewer-header__object').left] } }))`))
|
||
check('no horizontal overflow at 1920 / 1440 / 1280 / 950 / 860 / 760 / 520', report.length === 7 && report.every(r => r.overflow <= 0 && r.objectOverflow <= 1), report.map(r => [r.width, r.overflow, r.objectOverflow]))
|
||
check('layer / settings / properties always in the header with tooltips', report.every(r => r.toggles.map(t => t[0]).join() === '레이어,설정,속성' && r.toggles.every(t => t[2])), report.map(r => [r.width, r.toggles]))
|
||
// 2026-10-21 measured steps (widths of SW Footer / SW Header Responsive More + 520): full at 1920 / 1440, icons at 950,
|
||
// More at 860 / 760 / 520. 1280 is at the measured full / icons edge (Firefox fits the labels, Chromium does not): either
|
||
// whole state, never More.
|
||
const full = r => r.fit && !r.more && r.open[0] && r.toggles.every(t => t[1])
|
||
const icons = r => !r.more && r.toggles.every(t => !t[1]) && !r.open[0] && r.open[1] === '파일 열기' && Math.round(r.open[2]) === 34
|
||
check('1920 / 1440 full (파일 열기 + 전체 보기 labels, no More); 1280 full or icons, no More; 950 icons only, no More; 860 / 760 / 520 icons + More', report.slice(0, 2).every(full)
|
||
&& (full(report[2]) || icons(report[2])) && icons(report[3]) && report.slice(4).every(r => r.more && r.toggles.every(t => !t[1]) && !r.open[0] && r.open[1] === '파일 열기' && Math.round(r.open[2]) === 34),
|
||
report.map(r => [r.width, r.fit, r.more, r.open, r.toggles.map(t => t[1])]))
|
||
check('logo, file open, file name and toolbar never overlap', report.every(r => r.order[0] <= r.order[1] + 2.5 && r.order[2] <= r.order[3] + 0.5 && r.order[4] <= r.order[5] + 0.5), report.map(r => [r.width, r.order]))
|
||
await click('[data-width="520"] .hm-sw-viewer-header__document .hm-sw-header__document-button')
|
||
check('520: document list shown inside the header width', await onTop('[data-width="520"] .hm-sw-header__document-menu') && await ev(`(() => { const m = document.querySelector('[data-width="520"] .hm-sw-header__document-menu').getBoundingClientRect(), h = document.querySelector('[data-width="520"] .hm-sw-viewer-header').getBoundingClientRect(); return m.x >= h.x - 2 && m.right <= h.right })()`))
|
||
await press('Escape')
|
||
await click('[data-width="520"] [data-document-open]')
|
||
check('520: icon-only file open runs the file open', (await state()).log !== undefined && await ev(`document.querySelector('[data-callback-log]').textContent.endsWith('document/open')`))
|
||
|
||
// ---------- Narrow screen: icon toggles open the panels ----------
|
||
await viewport({ width: 820, height: 900 })
|
||
await goto('sw-viewer--viewer-header')
|
||
const panels = []
|
||
for (const [name, dialog] of [['레이어', '레이어 ('], ['설정', '옵션'], ['속성', '속성']]) {
|
||
const selector = `.hm-sw-viewer-header .hm-sw-viewer-header__button[aria-label="${name}"]`
|
||
await click(selector)
|
||
const after = JSON.parse(await ev(`(() => { const t = document.querySelector(${JSON.stringify(selector)}); return JSON.stringify({ pressed: t.getAttribute('aria-pressed'), color: getComputedStyle(t).color, title: t.title,
|
||
open: [...document.querySelectorAll('[role=dialog]')].some(d => (d.getAttribute('aria-label') ?? d.textContent).includes(${JSON.stringify(dialog)}) || document.getElementById(d.getAttribute('aria-labelledby'))?.textContent.includes(${JSON.stringify(dialog)})) }) })()`))
|
||
await click(selector)
|
||
after.closed = await ev(`document.querySelector(${JSON.stringify(selector)}).getAttribute('aria-pressed') === 'false'`)
|
||
panels.push({ name, ...after })
|
||
}
|
||
const width = await ev(`Math.round(document.querySelector('.hm-sw-viewer-header').getBoundingClientRect().width)`)
|
||
check(`narrow header (${width}px): icon toggles open / close panels, pressed #FFC600, tooltip`, width < 900 && panels.every(p => p.pressed === 'true' && p.color === 'rgb(255, 198, 0)' && p.title && p.open && p.closed), panels)
|
||
await shot('firefox-viewer-narrow.png', '.hm-sw-viewer-header', 820, 60)
|
||
} finally {
|
||
check('no console errors', errors.length === 0, errors.slice(0, 5))
|
||
const failed = checks.filter(c => !c.ok).length
|
||
await writeFile(path.join(out, 'viewer-logo-document-menu-firefox.json'), JSON.stringify({ base, checks, errors }, null, 2))
|
||
console.log(`${checks.length - failed}/${checks.length} PASS (firefox)`)
|
||
try { await send('session.end', {}) } catch {}
|
||
ws.close(); proc.kill()
|
||
await sleep(500); await rm(profile, { recursive: true, force: true }).catch(() => {})
|
||
process.exitCode = failed ? 1 : 0
|
||
}
|