Deploy Storybook / deploy (push) Successful in 1m13s
- Model / Layout tabs follow Figma 7652:40344: gradient faces, gold / green gradient borders, blurred outline, inner shadow and top gloss, with every color in its own --hm-sw-viewer-tab(s)-* token (consumer-overridable) - Box height matches the adjacent field group (42px, from the control height token); padding 1px 1px 1.5px, button side padding 5px, 1px right margin for a tabs-only first group; 378px max width, equal buttons and the measured More steps are unchanged; existing text-shadow kept - Storybook: SW/Viewer > Model / Layout story (states and token override); Logo story listed before Responsive More - AI docs (VIEWER.md, DESIGN_RULES.md) and viewer / text-weight test expectations updated
220 lines
22 KiB
JavaScript
220 lines
22 KiB
JavaScript
// 2026-10-20 SW text weight request: Bold only on the named texts, with the black text outline where asked.
|
|
// Header file name (700) · Composed Controls field values (700) vs SWHeader field values (400) · Footer 보조기능 / 명령어
|
|
// titles and category tabs (700, 전체보기 / commands unchanged; Model · Layout tabs 700 since 2026-10-22) · Viewer header file name, labels and
|
|
// Model / Layout tabs (700, More kept; 2026-10-21 logo menu and sample list follow the SWHeader `...` menu / file-name
|
|
// list rules) · recent files name / path / preview title (700 + outline). Hover keeps the
|
|
// text in place. Usage: build Storybook first, then node packages/sw/tests/text-weight.browser.mjs [evidence-dir]
|
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
import path from 'node:path'
|
|
import http from 'node:http'
|
|
import { runtimeRequire } from './playwright-runtime.mjs'
|
|
const require = runtimeRequire()
|
|
const { chromium } = require('playwright')
|
|
const out = path.resolve(process.argv[2] ?? 'docs/sw/text-weight-qa'); await mkdir(out, { recursive: true })
|
|
const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.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(root, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } })
|
|
await new Promise(r => server.listen(0, '127.0.0.1', r)); const base = `http://127.0.0.1:${server.address().port}`
|
|
const browser = await chromium.launch({ headless: true, executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
|
|
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, 500)) }
|
|
const report = {}
|
|
let errors = []
|
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1100 } })
|
|
page.on('pageerror', e => errors.push(e.message)); page.on('console', m => { if (m.type() === 'error') errors.push(m.text()) })
|
|
const goto = async id => { errors = []; await page.goto(`${base}/iframe.html?id=${id}&viewMode=story`); await page.locator('#storybook-root > *').first().waitFor(); await page.evaluate(() => document.fonts.ready); await page.waitForTimeout(300) }
|
|
const away = async () => { await page.mouse.move(2, 2); await page.waitForTimeout(90) }
|
|
const OUTLINE_14 = 'rgb(0, 0, 0) 1.3px 0px 0px', OUTLINE_12 = 'rgb(0, 0, 0) 1px 0px 0px'
|
|
// Every shown element of a selector: weight, size, color, outline start, text rect, box rect, cut.
|
|
const styles = sel => page.locator(sel).evaluateAll(list => list.filter(e => e.getClientRects().length && e.textContent.trim()).map(e => {
|
|
const c = getComputedStyle(e), g = document.createRange(); g.selectNodeContents(e); const t = g.getBoundingClientRect(), b = e.getBoundingClientRect()
|
|
return { text: e.textContent.trim(), weight: c.fontWeight, size: c.fontSize, family: c.fontFamily.split(',')[0], color: c.color, outline: c.textShadow.split(', rgb')[0], textW: Math.round(t.width * 100) / 100, boxW: Math.round(b.width * 100) / 100, cut: t.width > b.width + 0.05 }
|
|
}))
|
|
const rect = loc => loc.evaluate(e => { const g = document.createRange(); g.selectNodeContents(e); const t = g.getBoundingClientRect(), b = e.getBoundingClientRect(); return [t.x, t.y, t.width, t.height, b.x, b.y, b.width, b.height].map(v => Math.round(v * 100) / 100).join(',') })
|
|
async function stableOnHover(name, target, text) {
|
|
await target.scrollIntoViewIfNeeded(); await away()
|
|
const d = await rect(text), w0 = await text.evaluate(e => getComputedStyle(e).fontWeight)
|
|
await target.hover(); await page.waitForTimeout(110)
|
|
const h = await rect(text), w1 = await text.evaluate(e => getComputedStyle(e).fontWeight)
|
|
await away(); const d2 = await rect(text)
|
|
check(`${name}: hover keeps the text place, size and weight (${w0})`, d === h && d === d2 && w0 === w1, { d, h, d2, w0, w1 })
|
|
}
|
|
|
|
// ---------- 1. Header ----------
|
|
for (const id of ['sw-header--overview', 'sw-header--document-open', 'sw-header--interactive-states']) {
|
|
await goto(id)
|
|
const names = await styles('.hm-sw-header .hm-sw-header__document-name')
|
|
check(`Header ${id}: file name Noto Sans KR 14px 700 + 1.3px outline`, names.length > 0 && names.every(n => n.weight === '700' && n.size === '14px' && n.family === '"Noto Sans KR"' && n.outline === OUTLINE_14), names.slice(0, 3))
|
|
const values = await styles('.hm-sw-header .hm-sw-header__field-value')
|
|
report[`headerValues:${id}`] = values
|
|
check(`Header ${id}: SWHeader field values stay Regular (400)`, values.length > 0 && values.every(v => v.weight === '400'), values.filter(v => v.weight !== '400'))
|
|
}
|
|
await goto('sw-header--overview')
|
|
const docButton = page.locator('.hm-sw-header__document-button').first()
|
|
await stableOnHover('Header file name', docButton, docButton.locator('.hm-sw-header__document-name'))
|
|
// SWHeader More (narrow): fields moved into the More menu are still SWHeader fields (400).
|
|
await goto('sw-header--responsive-more')
|
|
const narrow = page.locator('.hm-sw-header').filter({ visible: true }).nth(2)
|
|
const moreTrigger = narrow.locator('.hm-sw-header__more-trigger')
|
|
if (await moreTrigger.count()) { await moreTrigger.click(); await page.waitForTimeout(200) }
|
|
const moreValues = await styles('.hm-sw-header__field-value')
|
|
check('Header More: values shown with the More menu open stay 400 (none turn Bold)', moreValues.length > 0 && moreValues.every(v => v.weight === '400'), moreValues.filter(v => v.weight !== '400'))
|
|
await page.keyboard.press('Escape')
|
|
// Composed Controls: standalone menu fields (outside SWHeader) are Bold; labels and the font sample unchanged.
|
|
await goto('sw-header--composed-controls')
|
|
const composed = await styles('[data-composed-controls] .hm-sw-header__field-value')
|
|
report.composedValues = composed
|
|
check('Composed Controls: every field value Noto Sans KR 14px 700 + 1.3px outline', composed.length >= 7 && composed.every(v => v.weight === '700' && v.size === '14px' && v.outline === OUTLINE_14), composed)
|
|
const composedLabels = await styles('[data-composed-controls] .hm-sw-header__field-label')
|
|
check('Composed Controls: field labels stay 12px 400', composedLabels.every(l => l.weight === '400' && l.size === '12px'), composedLabels)
|
|
report.composedCut = composed.filter(v => v.cut).map(v => `${v.text} (${v.textW} / ${v.boxW}px)`)
|
|
console.log('INFO Composed Controls values wider than their (unchanged) box at 700:', report.composedCut.join(', ') || 'none')
|
|
const fields = page.locator('[data-composed-controls] .hm-sw-header__field')
|
|
for (let i = 0, n = await fields.count(); i < n; i++) {
|
|
const f = fields.nth(i), v = f.locator('.hm-sw-header__field-value').first()
|
|
await stableOnHover(`Composed field ${await f.getAttribute('data-menu')}`, f, v)
|
|
}
|
|
|
|
// ---------- 2. Footer ----------
|
|
await goto('sw-footer--full-footer')
|
|
const work = page.locator('.sw-footer-story-workspace .hm-sw-footer')
|
|
for (const side of ['start', 'end']) {
|
|
const btn = work.locator(side === 'end' ? '.hm-sw-footer-side[data-side="end"]' : '.hm-sw-footer-side:not([data-side="end"])')
|
|
const read = () => btn.evaluate(e => [e.querySelector('.hm-sw-footer-side__title'), e.querySelector('.hm-sw-footer-side__subtitle')].map(t => { const c = getComputedStyle(t); return [t.textContent, c.fontWeight, c.fontSize, c.color, c.textShadow.split(', rgb')[0]].join('|') }))
|
|
await away(); const d = await read()
|
|
await btn.hover(); await page.waitForTimeout(110); const h = await read(); await away()
|
|
check(`Footer ${side === 'end' ? '명령어' : '보조기능'}: title 700 (Default + Hover), 전체보기 keeps 400 + 1px outline`, d[0].split('|')[1] === '700' && h[0].split('|')[1] === '700' && d[1].split('|')[1] === '400' && h[1].split('|')[1] === '400' && d[1].endsWith(OUTLINE_12) && h[1].split('|')[3] === 'rgb(255, 92, 0)', { d, h })
|
|
await stableOnHover(`Footer side ${side} title`, btn, btn.locator('.hm-sw-footer-side__title'))
|
|
await stableOnHover(`Footer side ${side} 전체보기`, btn, btn.locator('.hm-sw-footer-side__subtitle'))
|
|
}
|
|
const tabs = await styles('.sw-footer-story-workspace .hm-sw-footer-menu__tab')
|
|
report.footerTabs = tabs
|
|
check(`Footer: every category tab (${tabs.length}) 14px 700 + 1.3px outline`, tabs.length >= 10 && tabs.every(t => t.weight === '700' && t.size === '14px' && t.outline === OUTLINE_14), tabs.filter(t => t.weight !== '700'))
|
|
const tabLoc = work.locator('.hm-sw-footer-menu__tab')
|
|
await stableOnHover('Footer tab 수정 (Default → Hover)', tabLoc.nth(1), tabLoc.nth(1))
|
|
// Selected: the tab's text sits at the same place inside its own box, weight unchanged.
|
|
const inBox = loc => loc.evaluate(e => { const g = document.createRange(); g.selectNodeContents(e); const t = g.getBoundingClientRect(), b = e.getBoundingClientRect(); return [t.x - b.x, t.y - b.y, t.width, b.width, b.height, getComputedStyle(e).fontWeight].map(v => typeof v === 'number' ? Math.round(v * 100) / 100 : v).join(',') })
|
|
const before = await inBox(tabLoc.nth(1)); await tabLoc.nth(1).click(); await away(); const after = await inBox(tabLoc.nth(1))
|
|
check('Footer tab 수정 selected: text place / size / weight inside the tab unchanged', before === after && await tabLoc.nth(1).getAttribute('aria-selected') === 'true', { before, after })
|
|
const pocket = await work.locator('.hm-sw-footer-menu').evaluate(m => { const t = m.querySelector('[aria-selected="true"]').getBoundingClientRect(), y = t.y + 4
|
|
const under = x => document.elementsFromPoint(x, y).find(e => e.matches('.hm-sw-footer__vacant, .hm-sw-footer__group'))?.className
|
|
return [under(t.x + 3), under(t.x + t.width / 2), under(t.right - 3)] })
|
|
check('Footer: 수정 pocket surface stays continuous under the Bold tab', pocket.every(p => p === 'hm-sw-footer__group'), pocket)
|
|
await tabLoc.nth(0).click(); await away()
|
|
// 2026-10-22 user request: the Model / Layout tab labels are 700; command and auxiliary labels keep 400.
|
|
const layoutTabs = await styles('.sw-footer-story-workspace .hm-sw-footer-tab__label')
|
|
check(`Footer: Model / Layout tab labels (${layoutTabs.length}) 12px 700`, layoutTabs.length >= 3 && layoutTabs.every(k => k.weight === '700' && k.size === '12px'), layoutTabs.filter(k => k.weight !== '700' || k.size !== '12px'))
|
|
const keep = await styles('.sw-footer-story-workspace :is(.hm-sw-footer-tool__label, .hm-sw-footer-aux__label)')
|
|
check('Footer: command and auxiliary labels keep 400', keep.length > 0 && keep.every(k => k.weight === '400'), keep.filter(k => k.weight !== '400'))
|
|
const area = await work.locator('.hm-sw-footer-menu__viewport').evaluate(e => ({ w: Math.round(e.getBoundingClientRect().width * 10) / 10, h: Math.round(e.getBoundingClientRect().height), content: e.scrollWidth, steps: [...e.parentElement.querySelectorAll('.hm-sw-footer-menu__step')].filter(s => s.getClientRects().length).length }))
|
|
report.footerCategoryArea = area
|
|
console.log('INFO Footer category area at 1920:', JSON.stringify(area))
|
|
// The approved category row is 24px high (measured before and after the Bold change, 2026-10-20).
|
|
check('Footer: category row height unchanged (24px)', area.h === 24, area)
|
|
check('Footer: ◀ ▶ only when the categories overflow their area', (area.content > area.w + 0.5) === (area.steps === 2), area)
|
|
await page.screenshot({ path: path.join(out, 'footer.png'), clip: { x: 0, y: 940, width: 1920, height: 160 } })
|
|
|
|
// ---------- 3. Viewer header ----------
|
|
// 2026-10-21 (approved restructure): 파일 열기 moved into the logo menu (the SWHeader `...` menu, SWHeaderMore), 샘플 into
|
|
// the file-name list (SWHeaderDocumentTitle) and 전체 보기 into More. The header keeps the Bold file name (now the
|
|
// SWHeader file-name button), 레이어 · 설정 · 속성 and Model / Layout; the two menus follow the SWHeader rules, measured
|
|
// on SWHeader itself below and compared row by row.
|
|
const typo = s => ({ weight: s.weight, size: s.size, family: s.family, outline: s.outline })
|
|
// SWHeader references: the responsive `...` menu rows and the file-name list rows.
|
|
await goto('sw-header--responsive-more')
|
|
const refTrigger = page.locator('.hm-sw-header__more-trigger').filter({ visible: true }).first()
|
|
await refTrigger.click(); await page.waitForTimeout(200)
|
|
const refMore = await styles('.hm-sw-header__more-menu .hm-sw-header__more-item > span:first-child')
|
|
check('SWHeader ... menu rows (reference): Regular 400 without outline', refMore.length > 0 && refMore.every(r => r.weight === '400' && r.outline === 'none'), refMore)
|
|
await page.keyboard.press('Escape')
|
|
await goto('sw-header--document-dropdown')
|
|
await page.locator('.hm-sw-header__document-button').first().click(); await page.waitForTimeout(200)
|
|
const refDocs = await styles('.hm-sw-header__document-menu .hm-sw-header__document-item-name')
|
|
check('SWHeader file-name list rows (reference): 14px + 1.3px outline, one weight', refDocs.length > 0 && refDocs.every(r => r.size === '14px' && r.outline === OUTLINE_14 && r.weight === refDocs[0].weight), refDocs)
|
|
// 2026-10-21: the logo menu takes the viewer dropdown type (SWViewerMenu, "상단 메뉴 + Dropdown"); its rows and label are the reference.
|
|
await goto('sw-viewer--dropdowns')
|
|
await page.getByRole('button', { name: '도면' }).click(); await page.waitForTimeout(200)
|
|
const refViewerItem = await styles('.hm-sw-viewer-menu .hm-sw-viewer-menu__text')
|
|
const refViewerLabel = await styles('.hm-sw-viewer-menu .hm-sw-viewer-menu__label')
|
|
check('SWViewerMenu rows / label (reference): 14px 400 and 12px 500, no outline', refViewerItem.length > 0 && refViewerItem.every(r => r.size === '14px' && r.weight === '400' && r.outline === 'none')
|
|
&& refViewerLabel.length > 0 && refViewerLabel.every(r => r.size === '12px' && r.weight === '500' && r.outline === 'none'), { refViewerItem: refViewerItem.slice(0, 2), refViewerLabel })
|
|
await page.keyboard.press('Escape')
|
|
report.reference = { more: refMore[0], documents: refDocs[0], viewerItem: refViewerItem[0], viewerLabel: refViewerLabel[0] }
|
|
|
|
await goto('sw-viewer--viewer-header')
|
|
// The 1440px Story frame at full width (as the viewer suites): the header has room for every label.
|
|
await page.addStyleTag({ content: '.sb-page { --sb-page-max-width: 100vw; }' }); await page.waitForTimeout(200)
|
|
const vh = page.locator('.hm-sw-viewer-header').first()
|
|
const vNames = await styles('.hm-sw-viewer-header >> nth=0 >> :is(.hm-sw-header__document-name, .hm-sw-viewer-header__document-name, .hm-sw-viewer-header__label, .hm-sw-viewer-header__tab)')
|
|
report.viewer = vNames
|
|
// 2026-10-21 measured steps: at 1440 the header has room, so 전체 보기 is a header label again (Bold like the others), and
|
|
// the file open button after the logo shows 파일 열기 on the SWHeader file-name surface (Bold + outline like the file name).
|
|
const want = ['C0060202-001-평면및종단면도.dwg', '파일 열기', '전체 보기', '레이어', '설정', '속성']
|
|
const labels = vNames.map(v => v.text)
|
|
check('Viewer: file name, 파일 열기, 전체 보기 · 레이어 · 설정 · 속성, Model · Layout are 14px 700 + 1.3px outline', want.every(w => labels.includes(w)) && labels.some(t => t.startsWith('Model')) && labels.some(t => t.startsWith('Layout')) && vNames.every(v => v.weight === '700' && v.size === '14px' && v.outline === OUTLINE_14), vNames)
|
|
check('Viewer: 샘플 is no longer a header label', !labels.includes('샘플'), labels)
|
|
const counts = await styles('.hm-sw-viewer-header >> nth=0 >> .hm-sw-viewer-header__count')
|
|
// 2026-10-23 Figma 7652:40344: counts are 14px (the name's size), still 400 with the 1px outline.
|
|
check('Viewer: tab counts 14px 400 (Figma 7652:40344)', counts.length > 0 && counts.every(c => c.weight === '400' && c.size === '14px'), counts)
|
|
// File open button (파일 열기, the logo menu is gone): hover keeps its label in place, size and weight.
|
|
const openButton = vh.locator('[data-document-open]')
|
|
await stableOnHover('Viewer 파일 열기', openButton, openButton.locator('.hm-sw-viewer-header__open-label'))
|
|
// File-name list (샘플): rows exactly as the SWHeader file-name list rows.
|
|
const docButton2 = vh.locator('.hm-sw-viewer-header__document .hm-sw-header__document-button')
|
|
await stableOnHover('Viewer file name', docButton2, docButton2.locator('.hm-sw-header__document-name'))
|
|
await docButton2.click(); await page.waitForTimeout(150)
|
|
const sampleRows = await styles('.hm-sw-viewer-header >> nth=0 >> .hm-sw-header__document-item-name')
|
|
report.viewerSamples = sampleRows
|
|
check('Viewer sample list: 3 rows = SWHeader file-name list rows (weight / size / family / outline)', sampleRows.length === 3 && sampleRows.every(r => JSON.stringify(typo(r)) === JSON.stringify(typo(refDocs[0])) && !r.cut), { sampleRows, ref: refDocs[0] })
|
|
const sampleRow = vh.locator('.hm-sw-header__document-item').first()
|
|
await stableOnHover('Viewer sample row', sampleRow, sampleRow.locator('.hm-sw-header__document-item-name'))
|
|
await page.keyboard.press('Escape'); await page.waitForTimeout(100)
|
|
const lab = vh.locator('.hm-sw-viewer-header__label', { hasText: '레이어' }).first()
|
|
await stableOnHover('Viewer 레이어', lab.locator('xpath=ancestor::button[1]'), lab)
|
|
const tab = vh.locator('.hm-sw-viewer-header__tab', { hasText: 'Layout' }).first()
|
|
await stableOnHover('Viewer Layout tab', tab, tab)
|
|
// More stays: narrow header shows the More button, it opens and lists the moved items.
|
|
await vh.evaluate(e => { e.style.width = '700px' })
|
|
const more = vh.getByRole('button', { name: /추가 도구/ })
|
|
// 2026-10-20: the header re-measures through a ResizeObserver; wait for More instead of a fixed 200ms (the check below
|
|
// still fails when it never appears). The fixed wait failed intermittently on the same build.
|
|
await more.waitFor({ state: 'visible', timeout: 3000 }).catch(() => {})
|
|
check('Viewer: More button shown at a narrow width', await more.count() === 1 && await more.isVisible())
|
|
await more.click(); await page.waitForTimeout(200)
|
|
const moreItems = await page.locator('.hm-sw-viewer-menu [role="menuitem"], .hm-sw-viewer-menu [role="menuitemradio"], .hm-sw-viewer-menu [role="menuitemcheckbox"]').count()
|
|
check('Viewer: More opens with its entries', moreItems > 0, moreItems)
|
|
await page.keyboard.press('Escape'); await page.waitForTimeout(100)
|
|
check('Viewer: Escape closes More', await page.locator('.hm-sw-viewer-menu').filter({ visible: true }).count() === 0)
|
|
await vh.evaluate(e => { e.style.width = '' })
|
|
check('Viewer: no page errors', errors.length === 0, errors)
|
|
|
|
// ---------- 4. Recent files + preview ----------
|
|
await goto('sw-header--home-menu')
|
|
const recentTile = page.locator('.hm-sw-home-menu > .hm-sw-home-menu__sections .hm-sw-home-menu__item', { hasText: '최근파일' }).filter({ visible: true }).first()
|
|
if (await recentTile.getAttribute('data-active') !== 'true') { await recentTile.click(); await page.waitForTimeout(300) }
|
|
const rNames = await styles('.hm-sw-recent-files__name'), rPaths = await styles('.hm-sw-recent-files__path')
|
|
report.recent = { names: rNames.slice(0, 3), paths: rPaths.slice(0, 3) }
|
|
check(`Recent files: names (${rNames.length}) 14px 700 + 1.3px outline`, rNames.length >= 3 && rNames.every(n => n.weight === '700' && n.size === '14px' && n.outline === OUTLINE_14), rNames.slice(0, 2))
|
|
check(`Recent files: paths (${rPaths.length}) 12px 700 + 1px outline, ellipsis kept`, rPaths.length >= 3 && rPaths.every(p => p.weight === '700' && p.size === '12px' && p.outline === OUTLINE_12), rPaths.slice(0, 2))
|
|
// 2026-10-21 (user decision): each recent-file list shows only the 3 most recent files (15 in the Story data).
|
|
const perList = await page.locator('.hm-sw-recent-files__list').evaluateAll(lists => lists.map(l => l.querySelectorAll('.hm-sw-recent-files__row').length))
|
|
check('Recent files: each list shows 3 rows', perList.length > 0 && perList.every(n => n === 3), perList)
|
|
const row = page.locator('.hm-sw-recent-files__row').first()
|
|
const rowGeo = () => row.evaluate(r => [r.getBoundingClientRect().height, ...[...r.querySelectorAll('.hm-sw-recent-files__name, .hm-sw-recent-files__path')].map(e => Math.round(e.getBoundingClientRect().width))].join(','))
|
|
const rowBefore = await rowGeo()
|
|
await stableOnHover('Recent file name', row, row.locator('.hm-sw-recent-files__name'))
|
|
await row.hover(); await page.waitForTimeout(250)
|
|
const preview = await page.locator('.hm-sw-recent-files__preview').evaluate(p => { const t = p.querySelector('.hm-sw-recent-files__preview-title'), c = getComputedStyle(t), b = p.getBoundingClientRect(), body = p.querySelector('.hm-sw-recent-files__preview-body').getBoundingClientRect()
|
|
return { title: t.textContent, weight: c.fontWeight, size: c.fontSize, color: c.color, outline: c.textShadow.split(', rgb')[0], titleW: t.getBoundingClientRect().width, overflow: c.textOverflow, w: b.width, h: b.height, bodyH: body.height, bodyEmpty: p.querySelector('.hm-sw-recent-files__preview-body').childElementCount === 0 && !p.querySelector('.hm-sw-recent-files__preview-body').textContent } })
|
|
report.preview = preview
|
|
// 2026-10-21 (user decision): the preview frame stays on hover; its 243px body is empty (no image / thumbnail).
|
|
check('Recent preview: title 14px 700 + 1.3px outline, width 294 with ellipsis, empty body 243px', preview.bodyEmpty && preview.weight === '700' && preview.size === '14px' && preview.outline === OUTLINE_14 && preview.titleW === 294 && preview.overflow === 'ellipsis' && preview.bodyH === 243, preview)
|
|
await page.screenshot({ path: path.join(out, 'recent-files.png'), clip: { x: 0, y: 0, width: 1100, height: 900 } })
|
|
check('Recent files: rows keep 50px and their text widths', rowBefore === await rowGeo() && rowBefore.startsWith('50,'), rowBefore)
|
|
await row.press('Enter').catch(() => {})
|
|
check('Recent files / preview: no page errors', errors.length === 0, errors)
|
|
|
|
await writeFile(path.join(out, 'text-weight.json'), JSON.stringify({ checks, report }, null, 2))
|
|
const failed = checks.filter(c => !c.ok).length
|
|
console.log(`${checks.length - failed}/${checks.length} PASS`)
|
|
await browser.close(); server.close()
|
|
process.exitCode = failed ? 1 : 0
|