style(storybook): tidy CENTER, chart, org chart and SW story pages
- CENTER plain page layout and shorter reference demo notes (spec values moved to the CENTER README); chart specimen descriptions trimmed - Org chart page names and previews; SW width rows follow the 30px frame margin (1980 / 1065 / 727px) and the SW tests match the current layout - Story inventory regenerated and WORKING_CONTEXT updated
This commit is contained in:
1 parent
bad2acfc49
commit
ed7e8790ec
40 files changed
+1525
-333
No files matched your search
@@ -89,8 +89,12 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const menu = header.getByRole('menu', { name: '추가 도구' })
|
||||
reference = await face(menu, MORE_ITEM)
|
||||
refTrigger = await more.evaluate(element => { const s = getComputedStyle(element); return `${s.borderTopWidth} ${s.borderTopStyle} ${s.borderTopColor} | ${s.borderTopLeftRadius} | ${s.backgroundImage !== 'none' ? s.backgroundImage : s.backgroundColor} | ${s.boxShadow}` })
|
||||
const responsive = await newPage(1920, 1500)
|
||||
// 2026-11-13: 2500px wide (like viewer.browser.mjs), so the 1920 row, shown only on a page of 1980px or more, is visible too.
|
||||
const responsive = await newPage(2500, 1500)
|
||||
await open(responsive, 'sw-viewer--responsive')
|
||||
// 2026-11-13: since 2026-10-26 More shows only once even the narrowest tabs do not fit (below about 520px), so the 520 row
|
||||
// is narrowed in place to 400px first (as viewer.browser.mjs does) to open the same More menu.
|
||||
await responsive.evaluate(async () => { document.querySelector('[data-width="520"]').style.maxWidth = '400px'; await new Promise(resolve => setTimeout(resolve, 400)) })
|
||||
await responsive.locator('[data-width="520"] .hm-sw-viewer-header').getByRole('button', { name: '추가 도구' }).click()
|
||||
const more520 = responsive.getByRole('menu', { name: '추가 도구' })
|
||||
moreMenu = await face(more520, VIEWER_ITEM, '.hm-sw-viewer-menu__label', '.hm-sw-viewer-menu__separator', '.hm-sw-viewer-menu__icon', '.hm-sw-viewer-menu__text')
|
||||
@@ -129,12 +133,17 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const layout = await header.evaluate(element => {
|
||||
const button = element.querySelector('[data-document-open]'), wrapper = button.parentElement
|
||||
return { surface: button.classList.contains('hm-sw-header__document-button') && wrapper.matches('.hm-sw-header__document[data-tool]'), afterLogo: wrapper.previousElementSibling?.classList.contains('hm-sw-viewer-header__brand'),
|
||||
beforeName: wrapper.nextElementSibling?.classList.contains('hm-sw-viewer-header__document'), label: button.querySelector('.hm-sw-viewer-header__open-label')?.textContent, icons: button.querySelectorAll('.hm-sw-viewer-header__open-icon svg').length }
|
||||
beforeName: wrapper.nextElementSibling?.classList.contains('hm-sw-viewer-header__document'), label: button.querySelector('.hm-sw-viewer-header__open-label')?.textContent,
|
||||
// 2026-11-13: the folder icon carries the viewer icon outline (SWViewerIcon, caf4c32 2026-10-21): one icon (glyph) SVG and
|
||||
// one outline copy behind it, nothing else.
|
||||
icons: button.querySelectorAll('.hm-sw-viewer-header__open-icon svg').length, glyphs: button.querySelectorAll('.hm-sw-viewer-header__open-icon .hm-sw-viewer-icon__glyph svg').length,
|
||||
outlines: button.querySelectorAll('.hm-sw-viewer-header__open-icon .hm-sw-viewer-icon__outline svg').length }
|
||||
})
|
||||
check('file open button after the logo: SWHeader document surface, icon + 파일 열기', layout.surface && layout.afterLogo && layout.beforeName && layout.label === '파일 열기' && layout.icons === 1, layout)
|
||||
const iconBox = await openButton.locator('.hm-sw-viewer-header__open-icon').evaluate(element => { const s = getComputedStyle(element), r = element.getBoundingClientRect(); return { style: `${s.borderTopWidth} ${s.borderTopStyle} ${s.borderTopColor} | ${s.borderTopLeftRadius} | ${s.backgroundImage !== 'none' ? s.backgroundImage : s.backgroundColor} | ${s.boxShadow}`, size: [Math.round(r.width), Math.round(r.height)] } })
|
||||
// 2026-10-21 user request: the icon has no border and no face (transparent); 34px slot kept.
|
||||
check('file open icon: no border, transparent, no shadow; 34px slot', iconBox.style.startsWith('0px none') && iconBox.style.includes('rgba(0, 0, 0, 0)') && iconBox.style.endsWith('none') && iconBox.size.join() === '34,34', iconBox)
|
||||
check('file open button after the logo: SWHeader document surface, icon (1 glyph + 1 outline SVG) + 파일 열기', layout.surface && layout.afterLogo && layout.beforeName && layout.label === '파일 열기' && layout.glyphs === 1 && layout.outlines === 1 && layout.icons === 2, layout)
|
||||
const iconBox = await openButton.locator('.hm-sw-viewer-header__open-icon').evaluate(element => { const s = getComputedStyle(element), r = element.getBoundingClientRect(), b = element.closest('[data-document-open]').getBoundingClientRect(); return { style: `${s.borderTopWidth} ${s.borderTopStyle} ${s.borderTopColor} | ${s.borderTopLeftRadius} | ${s.backgroundImage !== 'none' ? s.backgroundImage : s.backgroundColor} | ${s.boxShadow}`, size: [Math.round(r.width), Math.round(r.height)], button: [Math.round(b.width), Math.round(b.height)] } })
|
||||
// 2026-10-21 user request: the icon has no border and no face (transparent). 2026-11-13: the icon area is the 16 × 16 icon
|
||||
// itself with no margin (0840d76, DESIGN_RULES --hm-sw-viewer-icon-size); the click area is the button, at least 34px each way.
|
||||
check('file open icon: no border, transparent, no shadow; 16 × 16 icon, button click area ≥ 34px', iconBox.style.startsWith('0px none') && iconBox.style.includes('rgba(0, 0, 0, 0)') && iconBox.style.endsWith('none') && iconBox.size.join() === '16,16' && iconBox.button.every(side => side >= 34), iconBox)
|
||||
// File-name area (.hm-sw-viewer-header__document): no left padding, right after the file open button; the file-name
|
||||
// button inside keeps its own padding (user decision 2026-10-21).
|
||||
const nameStart = await header.evaluate(element => { const d = element.querySelector('.hm-sw-viewer-header__document'); return [getComputedStyle(d).paddingLeft, Math.round(d.getBoundingClientRect().left - element.querySelector('[data-document-open]').getBoundingClientRect().right)] })
|
||||
@@ -179,18 +188,21 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const openState = width => header.evaluate(async (element, width) => {
|
||||
element.style.width = `${width}px`
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
const button = element.querySelector('[data-document-open]'), icon = button.querySelector('.hm-sw-viewer-header__open-icon').getBoundingClientRect()
|
||||
return { width, label: !!button.querySelector('.hm-sw-viewer-header__open-label'), aria: button.getAttribute('aria-label'), title: button.title, icon: [Math.round(icon.width), Math.round(icon.height)], overflow: element.scrollWidth - element.clientWidth }
|
||||
const button = element.querySelector('[data-document-open]'), icon = button.querySelector('.hm-sw-viewer-header__open-icon').getBoundingClientRect(), area = button.getBoundingClientRect()
|
||||
return { width, label: !!button.querySelector('.hm-sw-viewer-header__open-label'), aria: button.getAttribute('aria-label'), title: button.title, icon: [Math.round(icon.width), Math.round(icon.height)], button: [Math.round(area.width), Math.round(area.height)], overflow: element.scrollWidth - element.clientWidth }
|
||||
}, width)
|
||||
const sweep = []
|
||||
for (let width = 1440; width >= 400; width -= 20) sweep.push(await openState(width))
|
||||
const back = []
|
||||
for (let width = 400; width <= 1440; width += 20) back.push(await openState(width))
|
||||
const firstIcon = sweep.find(item => !item.label)
|
||||
check('1440: icon + 파일 열기; narrower: icon only (tooltip / name 파일 열기, 34px icon slot kept)', sweep[0].label && firstIcon
|
||||
&& sweep.filter(item => !item.label).every(item => item.aria === '파일 열기' && item.title === '파일 열기' && item.icon.join() === '34,34'), { first: sweep[0], firstIcon })
|
||||
// 2026-11-13: at every width the icon stays the 16 × 16 icon area (0840d76) and the button keeps a click area of at least 34px.
|
||||
check('1440: icon + 파일 열기; narrower: icon only (tooltip / name 파일 열기); 16 × 16 icon and ≥ 34px click area at every width', sweep[0].label && firstIcon
|
||||
&& sweep.filter(item => !item.label).every(item => item.aria === '파일 열기' && item.title === '파일 열기')
|
||||
&& [...sweep, ...back].every(item => item.icon.join() === '16,16' && item.button.every(side => side >= 34)), { first: sweep[0], firstIcon, icons: [...new Set([...sweep, ...back].map(item => item.icon.join()))], minButton: [Math.min(...sweep.map(item => item.button[0])), Math.min(...sweep.map(item => item.button[1]))] })
|
||||
check('label comes back at the same widths when the header grows; no overflow', sweep.every(item => back.find(other => other.width === item.width).label === item.label) && [...sweep, ...back].every(item => item.overflow <= 0))
|
||||
await header.evaluate(element => { element.style.width = '520px' })
|
||||
// 2026-11-13: More shows only below about 520px since 2026-10-26 (viewer.browser.mjs), so the header is set to 400px.
|
||||
await header.evaluate(element => { element.style.width = '400px' })
|
||||
await page.waitForTimeout(60)
|
||||
const more = header.getByRole('button', { name: '추가 도구' })
|
||||
await more.click()
|
||||
@@ -223,7 +235,8 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
// ---------- Responsive ----------
|
||||
{
|
||||
// Tall enough that the 520 row and its popups are inside the viewport (the Story page does not scroll them in).
|
||||
const page = await newPage(1920, 1500)
|
||||
// 2026-11-13: 2500px wide so the 1920 row (shown only on a page of 1980px or more) is measured with the others.
|
||||
const page = await newPage(2500, 1500)
|
||||
await open(page, 'sw-viewer--responsive')
|
||||
const headers = page.locator('[data-width] .hm-sw-viewer-header')
|
||||
const report = []
|
||||
@@ -251,12 +264,21 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
check('layer / settings / properties always in the header', report.every(item => JSON.stringify(item.toggles.map(toggle => toggle.name)) === '["레이어","설정","속성"]'), report.map(item => item.toggles.map(toggle => toggle.name)))
|
||||
// 2026-10-21 measured steps: full → icons only → More, by the measured room (no fixed width). With the file open button
|
||||
// after the logo, this Story's content fits fully at 1920 / 1440, as icons at 1280 / 950, and needs More at 860 / 760 / 520.
|
||||
// 2026-11-13: since 2026-10-26 (user request; viewer.browser.mjs) the file name and the Model / Layout tabs give way first,
|
||||
// so 860 / 760 / 520 keep every item as icons without More; More shows only below about 520px (the 520 row narrowed to 400).
|
||||
check('1920 / 1440: full items with labels (전체 보기 shown), no More; 1280 / 950: icons only, no More', report.slice(0, 2).every(item => item.fitShown && !item.more && item.toggles.every(toggle => toggle.label))
|
||||
&& report.slice(2, 4).every(item => !item.more && item.fitShown && item.toggles.every(toggle => !toggle.label && toggle.title)), report.map(item => [item.width, item.fitShown, item.more]))
|
||||
check('860 / 760: icons only (tooltips kept) + More', report.slice(4, 6).every(item => item.more && item.toggles.every(toggle => !toggle.label && toggle.title)), report.slice(4, 6))
|
||||
check('520: icons only, then More for what still does not fit', report[6].more && report[6].toggles.every(toggle => !toggle.label && toggle.title), report[6])
|
||||
check('860 / 760: icons only (tooltips kept), no More', report.slice(4, 6).every(item => !item.more && item.toggles.every(toggle => !toggle.label && toggle.title)), report.slice(4, 6))
|
||||
check('520: icons only (tooltips kept), no More', !report[6].more && report[6].toggles.every(toggle => !toggle.label && toggle.title), report[6])
|
||||
check('logo, file name and toolbar never overlap', report.every(item => item.logoRight <= item.docLeft + 2 + 0.5 && item.docRight <= item.objectLeft + 0.5), report.map(item => [item.width, item.logoRight, item.docLeft, item.docRight, item.objectLeft]))
|
||||
// Every item is either in the header or in More, never both and never lost.
|
||||
// Every item is either in the header or in More, never both and never lost: checked on the 520 row narrowed to 400px.
|
||||
const narrowed = await page.evaluate(async () => {
|
||||
const wrapper = document.querySelector('[data-width="520"]'); wrapper.style.maxWidth = '400px'
|
||||
await new Promise(resolve => setTimeout(resolve, 400))
|
||||
const header = wrapper.querySelector('.hm-sw-viewer-header')
|
||||
return { width: Math.round(header.getBoundingClientRect().width), overflow: header.scrollWidth - header.clientWidth, more: !!header.querySelector('.hm-sw-viewer-header__more') }
|
||||
})
|
||||
check('520 row narrowed to 400: More shown, no overflow', narrowed.more && narrowed.overflow <= 0, narrowed)
|
||||
const more = headers.nth(6).getByRole('button', { name: '추가 도구' })
|
||||
await more.click()
|
||||
const moreMenu = page.getByRole('menu', { name: '추가 도구' })
|
||||
@@ -264,8 +286,9 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const inHeader = await headers.nth(6).evaluate(element => [...element.querySelectorAll('.hm-sw-viewer-header__object :is(.hm-sw-viewer-header__button:not(.hm-sw-viewer-header__more), [role=radio], .hm-sw-viewer-header__switch-label)')]
|
||||
.map(item => item.getAttribute('aria-label') ?? item.textContent.trim().replace(/[\d,]+$/, '')))
|
||||
const all = ['Model', 'Layout', '전체 보기', '2.5D', '어두운 배경', '레이어', '설정', '속성']
|
||||
check('520: header + More hold every item exactly once (panel toggles never in More)', all.every(name => inHeader.includes(name) !== moreItems.includes(name)) && !moreItems.some(item => ['레이어', '설정', '속성'].includes(item)), { inHeader, moreItems })
|
||||
check('520 row at 400: header + More hold every item exactly once (panel toggles never in More)', all.every(name => inHeader.includes(name) !== moreItems.includes(name)) && !moreItems.some(item => ['레이어', '설정', '속성'].includes(item)), { inHeader, moreItems })
|
||||
await page.keyboard.press('Escape')
|
||||
await page.evaluate(async () => { document.querySelector('[data-width="520"]').style.maxWidth = ''; await new Promise(resolve => setTimeout(resolve, 400)) })
|
||||
// Icon toggle at 520 still reports pressed / commands.
|
||||
const icon = headers.nth(3).getByRole('button', { name: '레이어', exact: true })
|
||||
await icon.click()
|
||||
@@ -289,14 +312,15 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
for (const [name, dialog] of [['레이어', /레이어 \(/], ['설정', '옵션'], ['속성', '속성']]) {
|
||||
const toggle = header.getByRole('button', { name, exact: true })
|
||||
await toggle.hover()
|
||||
const hover = await toggle.evaluate(element => getComputedStyle(element).backgroundColor)
|
||||
// 2026-11-13: Viewer Header hover (caf4c32, 2026-10-21 user request): no face (transparent), text / icon #FFC600.
|
||||
const hover = await toggle.evaluate(element => `${getComputedStyle(element).backgroundColor} / ${getComputedStyle(element).color}`)
|
||||
await toggle.click()
|
||||
const panel = page.getByRole('dialog', { name: dialog })
|
||||
panels.push({ name, iconOnly: (await toggle.locator('.hm-sw-viewer-header__label').count()) === 0, title: await toggle.getAttribute('title'), hover, open: await panel.isVisible(), pressed: await toggle.getAttribute('aria-pressed'), color: await toggle.evaluate(element => getComputedStyle(element).color) })
|
||||
await toggle.click()
|
||||
panels.at(-1).closed = (await panel.count()) === 0 && (await toggle.getAttribute('aria-pressed')) === 'false'
|
||||
}
|
||||
check(`narrow header (${width}px): icon toggles with tooltip, hover, pressed #FFC600, panel open / close`, width < 900 && panels.every(item => item.iconOnly && item.title && item.hover === 'rgb(38, 102, 81)' && item.open && item.pressed === 'true' && item.color === 'rgb(255, 198, 0)' && item.closed), panels)
|
||||
check(`narrow header (${width}px): icon toggles with tooltip, hover (no face, #FFC600), pressed #FFC600, panel open / close`, width < 900 && panels.every(item => item.iconOnly && item.title && item.hover === 'rgba(0, 0, 0, 0) / rgb(255, 198, 0)' && item.open && item.pressed === 'true' && item.color === 'rgb(255, 198, 0)' && item.closed), panels)
|
||||
await page.screenshot({ path: path.join(out, `viewer-narrow-${width}.png`), clip: { ...(await box(header)), height: 56 } })
|
||||
await page.close()
|
||||
}
|
||||
@@ -353,6 +377,11 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
// The header's own width is swept (10px steps) down and back up. Stage per width: 0 full, 1 icons only, 2+ More with
|
||||
// n items. Icons always come before More; narrower never has fewer steps; the way back gives the same state at
|
||||
// every width (no flicker, no stuck item); no overflow. A shorter file name moves every boundary to a narrower width.
|
||||
// 2026-11-13 (current approved rules): the panel toggles turn into icons before 전체 보기 (2026-10-23, user-approved) and a
|
||||
// button that became an icon stays one when narrower; below 560px the file name gives way to 56px (2026-10-26), so items
|
||||
// may come back from More into the header — instead of a falling item count, every width that shows More opens it and every
|
||||
// command must be in the header or in More exactly once. The steps use a header-width basis, not the file name width
|
||||
// (caf4c32, 2026-10-21), so a shorter file name keeps the same boundaries.
|
||||
const page = await newPage(1920, 900)
|
||||
await open(page, 'sw-viewer--viewer-header')
|
||||
const header = page.locator('.hm-sw-viewer-header').first()
|
||||
@@ -365,8 +394,32 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const iconOnly = icons.length > 0 && icons.every(button => !button.querySelector('.hm-sw-viewer-header__label'))
|
||||
const anyIcon = icons.some(button => !button.querySelector('.hm-sw-viewer-header__label'))
|
||||
const more = !!element.querySelector('.hm-sw-viewer-header__more')
|
||||
return { width, shown, iconOnly, anyIcon, more, stage: more ? 2 : iconOnly ? 1 : 0, overflow: Math.max(element.scrollWidth - element.clientWidth, object.scrollWidth - object.clientWidth - 1) }
|
||||
const nameOf = button => button.getAttribute('aria-label') ?? button.textContent.trim()
|
||||
const iconNames = icons.filter(button => !button.querySelector('.hm-sw-viewer-header__label')).map(nameOf)
|
||||
const labelNames = icons.filter(button => button.getClientRects().length && button.querySelector('.hm-sw-viewer-header__label')).map(nameOf)
|
||||
return { width, shown, iconOnly, anyIcon, iconNames, labelNames, more, stage: more ? 2 : iconOnly ? 1 : 0, overflow: Math.max(element.scrollWidth - element.clientWidth, object.scrollWidth - object.clientWidth - 1) }
|
||||
}, width)
|
||||
// Commands shown in the header (visible only; a switch is named by its full label) and in the opened More menu.
|
||||
const COMMANDS = ['Model', 'Layout', '전체 보기', '2.5D', '어두운 배경', '레이어', '설정', '속성']
|
||||
const PANEL_TOGGLES = ['레이어', '설정', '속성']
|
||||
const commandsAt = async width => {
|
||||
const inHeader = await header.evaluate(async (element, width) => {
|
||||
element.style.width = `${width}px`
|
||||
await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))
|
||||
return [...element.querySelectorAll('.hm-sw-viewer-header__object :is(button:not(.hm-sw-viewer-header__more), [role=radio], [role=switch])')]
|
||||
.filter(item => item.getClientRects().length)
|
||||
.map(item => item.getAttribute('role') === 'switch' ? element.querySelector(`label[for="${item.id}"] .hm-sw-viewer-header__switch-label-full`)?.textContent.trim()
|
||||
: (item.getAttribute('aria-label') ?? item.textContent.trim()).replace(/[\d,]+$/, ''))
|
||||
}, width)
|
||||
await header.getByRole('button', { name: '추가 도구' }).click()
|
||||
const menu = page.getByRole('menu', { name: '추가 도구' })
|
||||
await menu.waitFor({ timeout: 2000 })
|
||||
const inMore = (await menu.locator('[role^=menuitem]').allInnerTexts()).map(text => text.split(' · ')[0].trim())
|
||||
const menuBox = await menu.evaluate(element => { const r = element.getBoundingClientRect(); return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, vw: innerWidth, vh: innerHeight } })
|
||||
await page.keyboard.press('Escape')
|
||||
const closed = (await page.getByRole('menu', { name: '추가 도구' }).count()) === 0
|
||||
return { width, inHeader: inHeader.filter(name => COMMANDS.includes(name)), inMore, menuBox, closed, pageOverflow: await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth) }
|
||||
}
|
||||
const sweep = async () => {
|
||||
const down = [], up = []
|
||||
for (let width = 1440; width >= 400; width -= 10) down.push(await state(width))
|
||||
@@ -376,17 +429,33 @@ let reference, viewerMenu, moreMenu, refTrigger
|
||||
const boundaries = list => ({ icons: list.find(item => item.stage >= 1)?.width, more: list.find(item => item.stage === 2)?.width })
|
||||
const long = await sweep()
|
||||
check('sweep: no overflow at any width (400–1440)', [...long.down, ...long.up].every(item => item.overflow <= 0), [...long.down, ...long.up].filter(item => item.overflow > 0).map(item => item.width))
|
||||
check('sweep: icons switch together and always before More', [...long.down, ...long.up].every(item => item.anyIcon === item.iconOnly && (!item.more || item.iconOnly)))
|
||||
check('sweep: narrower never shows more (monotonic: full → icons → More, fewer items)', long.down.every((item, i) => i === 0 || (item.stage >= long.down[i - 1].stage && item.shown <= long.down[i - 1].shown)))
|
||||
check('sweep: icons always before More; panel toggles turn into icons before 전체 보기', [...long.down, ...long.up].every(item => (!item.more || item.iconOnly)
|
||||
&& (!item.iconNames.includes('전체 보기') || PANEL_TOGGLES.every(name => item.iconNames.includes(name)))),
|
||||
[...long.down, ...long.up].filter(item => (item.more && !item.iconOnly) || (item.iconNames.includes('전체 보기') && !PANEL_TOGGLES.every(name => item.iconNames.includes(name)))).map(item => [item.width, item.iconNames]))
|
||||
// A button that became an icon never shows its label again at a narrower width (it may move on into More).
|
||||
check('sweep: a button that became an icon never gets its label back when narrower', long.down.every((item, i) => i === 0 || !long.down[i - 1].iconNames.some(name => item.labelNames.includes(name))),
|
||||
long.down.filter((item, i) => i && long.down[i - 1].iconNames.some(name => item.labelNames.includes(name))).map(item => [item.width, item.labelNames]))
|
||||
// Every width (10px steps) that shows More: More opened for real; each command exactly once in the header or in More, the panel
|
||||
// toggles never in More, the menu inside the viewport and closed by Escape, no page overflow. Items moving back into the header
|
||||
// when the file name gives way (below 560px) are allowed.
|
||||
const moreWidths = long.down.filter(item => item.more).map(item => item.width)
|
||||
const commands = []
|
||||
for (const width of moreWidths) commands.push(await commandsAt(width))
|
||||
const commandIssues = commands.filter(item => !COMMANDS.every(name => item.inHeader.filter(other => other === name).length + item.inMore.filter(other => other === name).length === 1)
|
||||
|| item.inMore.some(name => PANEL_TOGGLES.includes(name)) || item.inHeader.length + item.inMore.length !== COMMANDS.length
|
||||
|| !(item.menuBox.left >= 0 && item.menuBox.top >= 0 && item.menuBox.right <= item.menuBox.vw + 0.5 && item.menuBox.bottom <= item.menuBox.vh + 0.5) || !item.closed || item.pageOverflow > 0)
|
||||
check(`sweep: every command exactly once in the header or More at each More width (${moreWidths.length} widths), menu inside the viewport, Escape closes`, moreWidths.length > 0 && commandIssues.length === 0,
|
||||
commandIssues.length ? commandIssues.slice(0, 3) : { widths: [moreWidths.at(0), moreWidths.at(-1)], sample: commands.at(-1) })
|
||||
check('sweep: growing back gives the same state at every width (reverse order, no stuck item)', long.down.every(item => { const back = long.up.find(other => other.width === item.width); return back.stage === item.stage && back.shown === item.shown }))
|
||||
check('sweep: 1440 full; each stage reached by measured room only', long.down[0].stage === 0 && long.down.at(-1).stage === 2, boundaries(long.down))
|
||||
// A short file name: the same content needs less room, so each step happens at a narrower width.
|
||||
// A short file name: the steps use the header-width basis of the file name area (caf4c32, 2026-10-21: short and long names
|
||||
// give the same result), so every boundary stays where it is with a long name.
|
||||
await header.evaluate(element => { element.style.width = '' })
|
||||
await header.locator('.hm-sw-viewer-header__document .hm-sw-header__document-button').click()
|
||||
await header.getByRole('button', { name: 'simple.dxf' }).click()
|
||||
const short = await sweep()
|
||||
const [a, b] = [boundaries(long.down), boundaries(short.down)]
|
||||
check('sweep: shorter file name moves the steps to narrower widths', b.icons < a.icons && b.more < a.more, { long: a, short: b })
|
||||
check('sweep: file name length does not move the steps (same boundaries for a short name)', a.icons !== undefined && a.more !== undefined && b.icons === a.icons && b.more === a.more, { long: a, short: b })
|
||||
await page.close()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user