Files
HMDesign/docs/sw/sw-dotum-font-qa/consumer-viewer/viewer-firefox.mjs
T
shkim4 6504668e85 docs(sw): Dotum default font, Footer design check and real viewer consumer QA
Report docs/sw/sw-dotum-font-2026-10-20.md and evidence docs/sw/sw-dotum-font-qa/: Noto -> Dotum layout diff of 40 SW
Stories x 4 widths, Header hysteresis trace, Footer design items (4 PASS), SW regression runs with baseline failure
classification, and the isolated HMDesignSwTest-viewer run (Chrome 34/34, Firefox 15/15; original project untouched).
Open items: synthesized-bold overflow in fixed Header cells and Footer POLYGON / WIPEOUT labels (design review),
viewer-regression not run, macOS / Linux fallback not verified.
2026-10-20 16:59:55 +09:00

59 lines
7.9 KiB
JavaScript

// Installed Firefox (WebDriver BiDi) — real SW consumer: Dotum resolution, viewer chrome fonts, DWG load, menu/panel,
// Escape, responsive overflow. Usage: node viewer-firefox.mjs <base> <out-dir>
import { spawn } from 'node:child_process'
import { mkdir, writeFile, rm } from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
const [base = 'http://127.0.0.1:5391', outArg = 'viewer-verify-out'] = process.argv.slice(2)
const out = path.resolve(outArg); await mkdir(out, { recursive: true })
const sleep = ms => new Promise(r => setTimeout(r, ms))
const results = [], errors = []
const check = (name, ok, data) => { results.push({ name, result: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name, data === undefined ? '' : JSON.stringify(data)) }
let version
for (const [width, height] of [[1400, 1000], [500, 800]]) {
const port = 9300 + Math.floor(Math.random() * 500)
const profile = path.join(os.tmpdir(), `sw-viewer-ff-${process.pid}-${width}`)
await mkdir(profile, { recursive: true })
const ff = spawn('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) } })
let ws
for (let i = 0; i < 60 && !ws; i++) { await sleep(250); 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) { check(`Firefox ${width}: BiDi session`, false); ff.kill(); continue }
let id = 0; const pending = new Map()
ws.onmessage = e => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id); m.type === 'error' ? rej(Error(m.message)) : res(m.result) } else if (m.method === 'log.entryAdded' && m.params.level === 'error') errors.push(`${width}: ${m.params.text}`) }
const send = (method, params = {}) => new Promise((res, rej) => { const n = ++id; pending.set(n, { res, rej }); ws.send(JSON.stringify({ id: n, method, params })) })
try {
const session = await send('session.new', { capabilities: {} }); version = session.capabilities.browserVersion
await send('session.subscribe', { events: ['log.entryAdded'] })
const context = (await send('browsingContext.getTree')).contexts[0].context
const ev = async expr => { const r = await send('script.evaluate', { expression: `JSON.stringify(${expr})`, target: { context }, awaitPromise: true }); if (r.type === 'exception') throw Error(r.exceptionDetails.text); return JSON.parse(r.result.value) }
const key = v => send('input.performActions', { context, actions: [{ type: 'key', id: 'k', actions: [{ type: 'keyDown', value: v }, { type: 'keyUp', value: v }] }] })
await send('browsingContext.navigate', { context, url: `${base}/?model=${encodeURIComponent('/samples/civil.dwg')}`, wait: 'complete' })
let st; for (let i = 0; i < 120; i++) { st = await ev('document.querySelector(".hm-sw-viewer-status")?.getAttribute("data-state")'); if (st && st !== 'loading' && st !== 'idle') break; await sleep(250) }
await sleep(800)
check(`Firefox ${width}: civil.dwg loads (status)`, st === 'ready', st)
const dotum = await ev(`(() => { const c = document.createElement('canvas').getContext('2d'); const t = '그리기모드 Standard 1.00'; c.font = '14px Dotum'; const a = c.measureText(t).width; c.font = '14px "Malgun Gothic"'; const b = c.measureText(t).width; c.font = '14px monospace'; const m = c.measureText(t).width; return { dotum: +a.toFixed(1), malgun: +b.toFixed(1), mono: +m.toFixed(1) } })()`)
check(`Firefox ${width}: Dotum resolves to an installed face (width differs from fallbacks)`, dotum.dotum !== dotum.malgun && dotum.dotum !== dotum.mono, dotum)
const fonts = await ev(`(() => { const m = {}; for (const e of document.querySelectorAll('.hm-sw-viewer-header *')) { if (![...e.childNodes].some(n => n.nodeType === 3 && n.textContent.trim())) continue; const f = getComputedStyle(e).fontFamily.split(',')[0].replace(/"/g, ''); m[f] = (m[f] ?? 0) + 1 } return m })()`)
check(`Firefox ${width}: SWViewerHeader computed font = Dotum`, Object.keys(fonts).length > 0 && Object.keys(fonts).every(f => f === 'Dotum'), fonts)
const layout = await ev(`(() => { const h = document.querySelector('.hm-sw-viewer-header'); return { header: h.scrollWidth - h.clientWidth, page: document.documentElement.scrollWidth - innerWidth, more: !!document.querySelector('.hm-sw-viewer-header__more'), h: h.getBoundingClientRect().height } })()`)
check(`Firefox ${width}: no horizontal overflow`, layout.header <= 1 && layout.page <= 1, layout)
// Open a header menu (샘플, or More at narrow widths) and close with Escape.
await ev(`(() => { const b = [...document.querySelectorAll('.hm-sw-viewer-header button')].find(e => e.textContent.trim() === '샘플') ?? document.querySelector('.hm-sw-viewer-header__more'); b.click(); return !!b })()`); await sleep(200)
const menu = await ev(`(() => { const m = document.querySelector('.hm-sw-viewer-menu'); return m ? { items: m.querySelectorAll('button').length, font: getComputedStyle(m.querySelector('button')).fontFamily.split(',')[0] } : null })()`)
check(`Firefox ${width}: header menu opens (Dotum)`, !!menu && menu.items > 0 && menu.font === 'Dotum', menu)
await key('\uE00C'); await sleep(200)
check(`Firefox ${width}: Escape closes the menu`, await ev('!document.querySelector(".hm-sw-viewer-menu")'))
// Options panel (via the header toggle, or More when collapsed).
const opened = await ev(`(() => { const t = [...document.querySelectorAll('.hm-sw-viewer-header button')].find(e => e.textContent.trim() === '설정'); if (t && t.getClientRects().length) { t.click(); return 'toggle' } document.querySelector('.hm-sw-viewer-header__more')?.click(); return 'more' })()`); await sleep(250)
if (opened === 'more') { await ev(`(() => { const i = [...document.querySelectorAll('.hm-sw-viewer-menu [role^=menuitem]')].find(e => e.textContent.includes('설정')); i?.click(); return !!i })()`); await sleep(250) }
const panel = await ev(`(() => { const p = [...document.querySelectorAll('.hm-sw-viewer-panel')].find(e => e.textContent.includes('옵션')); if (!p) return null; const b = p.querySelector('.hm-sw-property__body'); const r = p.getBoundingClientRect(); const clipped = [...p.querySelectorAll('*')].filter(e => { if (![...e.childNodes].some(n => n.nodeType === 3 && n.textContent.trim())) return false; const cs = getComputedStyle(e); if (cs.overflowX === 'visible' && cs.textOverflow !== 'ellipsis') return false; const g = document.createRange(); g.selectNodeContents(e); return g.getBoundingClientRect().width > e.clientWidth - parseFloat(cs.paddingLeft) - parseFloat(cs.paddingRight) + 0.5 }).length; return { w: Math.round(r.width), h: Math.round(r.height), font: getComputedStyle(p).fontFamily.split(',')[0], scroll: b.scrollHeight > b.clientHeight, clipped, inView: r.right <= innerWidth + 1 } })()`)
check(`Firefox ${width}: options SWViewerPanel opens, Dotum, scrollable body, no clipped text`, !!panel && panel.font === 'Dotum' && panel.clipped === 0, { opened, panel })
await writeFile(path.join(out, `firefox-${width}.png`), Buffer.from((await send('browsingContext.captureScreenshot', { context })).data, 'base64'))
await send('session.end')
} catch (error) { check(`Firefox ${width}: suite completes`, false, error.message) }
ws.close(); ff.kill(); await sleep(400); await rm(profile, { recursive: true, force: true }).catch(() => {})
}
check('Firefox: no console errors (favicon 404 excluded)', errors.filter(e => !/favicon/i.test(e)).length === 0, errors.slice(0, 5))
await writeFile(path.join(out, 'firefox-result.json'), JSON.stringify({ base, version, results, errors }, null, 2))
console.log(`Firefox ${version}: ${results.filter(r => r.result === 'PASS').length}/${results.length} PASS`)