SW package 0.2.0 integrated improvement (Task A, steps 1-8). All new props are
optional; existing SW Stories are pixel-identical to the previous build (28/28).
- Build reproducibility: dist text normalized to LF, dist/build-info.json
(commit, dirty, tool versions, no timestamp), packages/sw/.gitattributes
(eol=lf). LF checkouts give identical dist and tarball.
- SWHeader: documentOpenPlacement ("menu" default | "header": Open right of
the logo in the existing document-button look, reusing onDocumentOpen and
documentOpenLabel, no duplicate Open in the dropdown), leading slot,
hiddenControls sub-items documentNew / documentOpen / layerFreeze /
layerLock. Open moves into More on measured overflow (with hysteresis).
- showNew / showOpen (SWHeaderDocumentTitle), showFreeze / showLock (layer
menu, field, property window list).
- SWToolButton (text / pill / document) and SWToolGroup (radio selection),
reusing the existing toggle, pill and file-name looks.
- SWPopup / SWPopupSection / SWPopupRow on the shared property window frame;
SWHeaderPropertyPanel gains children, title, autoFocus and optional
selection / properties / groups. Escape stack for several popups.
- Field dropdowns inside a popup use the shared fixed overlay
(useSWFixedDropdownPosition), so the scrolling body no longer clips them.
- AI guide, examples E/F, README, Storybook (SW/Header/Options, SW/Tool,
SW/Popup) and verification record docs/sw/sw-integrated-improvement-2026-10-19.md.
- Regression scripts rescoped for the intended changes (unused-removal 81/81,
overview 52/54 with the two pre-existing historical assertions).
Known limitation (unresolved): tools / leading content (incl. SWToolGroup)
does not move into More. With a wide tool bar the configured fields shrink
instead; in Example F the document name and layer field reach 0px at 390px.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
93 lines
8.9 KiB
JavaScript
93 lines
8.9 KiB
JavaScript
// SW/Popup checks for integrated-improvement.browser.mjs (2026-10-19): frame identity, free content (SW + HM UI),
|
|
// callbacks, dropdown clipping, Escape / focus with several popups, scrolling.
|
|
import path from 'node:path'
|
|
|
|
export async function popupChecks({ page, goto, check, log, rect, out, report }) {
|
|
await goto('sw-popup--custom-content')
|
|
const frames = page.locator('[data-popups] .hm-sw-property')
|
|
const frame = await frames.evaluateAll(list => list.map(e => { const s = getComputedStyle(e), t = getComputedStyle(e.querySelector('.hm-sw-property__title')); return { w: e.getBoundingClientRect().width, bg: s.backgroundColor, border: s.borderTopColor, radius: s.borderRadius, shadow: s.boxShadow, title: t.color, weight: t.fontWeight, titlebar: e.querySelector('.hm-sw-property__titlebar').getBoundingClientRect().height, role: e.getAttribute('role'), modal: e.getAttribute('aria-modal') } }))
|
|
report.data.popupFrames = frame
|
|
const same = frame.every(f => JSON.stringify(f) === JSON.stringify(frame[2]))
|
|
check('popup: SWPopup frames match the property window frame (314, surface, border, radius, title)', frame.length === 3 && same && frame[0].w === 314 && frame[0].bg === 'rgb(228, 219, 201)' && frame[0].border === 'rgb(166, 125, 3)' && frame[0].radius === '4px' && frame[0].title === 'rgb(255, 198, 0)' && frame[0].titlebar === 30 && frame[0].role === 'dialog' && frame[0].modal === 'false', frame)
|
|
const settings = page.locator('[data-popup="settings"]')
|
|
const commit = async (locator, value) => { await locator.click(); await locator.fill(value); await locator.press('Enter') }
|
|
await commit(settings.getByLabel('이름'), '평면도-02')
|
|
await commit(settings.getByLabel('축척'), '50')
|
|
check('popup content: SWHeaderInput rows commit through the host callbacks', (await log()).includes('name=평면도-02') && (await log()).includes('scale=50'))
|
|
const rowStyle = await settings.locator('.hm-sw-property__row').first().evaluate(e => ({ label: getComputedStyle(e.querySelector('label')).width, minH: getComputedStyle(e).minHeight, box: getComputedStyle(e.querySelector('.hm-sw-property__value')).height }))
|
|
check('popup content: SWPopupRow is the property row (72px label, 32px row, 28px box)', rowStyle.label === '72px' && rowStyle.minH === '32px' && rowStyle.box === '28px', rowStyle)
|
|
await settings.locator('[data-ui-switch]').click()
|
|
check('popup content: HM UI Switch works inside', (await log()).includes('snap=false'))
|
|
await settings.locator('[data-ui-select]').click()
|
|
await page.getByRole('option', { name: 'cm' }).click()
|
|
check('popup content: HM UI Select (portal) works inside', (await log()).includes('unit=cm'))
|
|
await settings.locator('[data-ui-slider] [role="slider"]').focus()
|
|
await page.keyboard.press('ArrowRight')
|
|
await page.waitForTimeout(100)
|
|
check('popup content: HM UI Slider works inside', /opacity=70/.test(await log()), await log())
|
|
await settings.locator('[data-ui-apply]').click()
|
|
check('popup content: HM UI Button works inside', (await log()).includes('apply'))
|
|
const layerField = settings.locator('[data-popup-layer] button.hm-sw-header__field')
|
|
await layerField.click()
|
|
const fieldBox = await rect(layerField), menu = settings.locator('.hm-sw-header__layer-menu')
|
|
const menuBox = await rect(menu)
|
|
const hit = await menu.locator('.hm-sw-header__layer-row').last().evaluate(row => { const r = row.getBoundingClientRect(); const e = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2); return row.contains(e) })
|
|
check('popup dropdown: SW layer dropdown opens right under its field (fixed overlay) and is not clipped', await menu.evaluate(e => getComputedStyle(e).position) === 'fixed' && Math.abs(menuBox.y - (fieldBox.y + fieldBox.h)) < 1 && Math.abs(menuBox.x - fieldBox.x) < 1 && hit, { fieldBox, menuBox, hit })
|
|
await page.locator('[data-popups]').screenshot({ path: path.join(out, 'popup-custom-dropdown.png') })
|
|
await menu.locator('.hm-sw-header__layer-item', { hasText: 'DIM' }).click()
|
|
check('popup dropdown: selection callback, list closes, popup stays', (await log()).includes('layer=dim') && await settings.locator('.hm-sw-header__layer-menu').count() === 0 && await settings.isVisible())
|
|
await settings.locator('[data-popup-color] button.hm-sw-header__field').click()
|
|
const colorPanel = settings.locator('.hm-sw-header__menu-panel')
|
|
check('popup dropdown: SW color dropdown becomes a fixed overlay', await colorPanel.evaluate(e => getComputedStyle(e).position) === 'fixed')
|
|
await colorPanel.getByLabel('초록').click()
|
|
check('popup dropdown: color chip callback', (await log()).includes('color=green'))
|
|
await page.keyboard.press('Escape')
|
|
check('popup dropdown: Escape closes the dropdown only', await settings.locator('.hm-sw-header__menu-panel').count() === 0 && await settings.isVisible())
|
|
|
|
const object = page.locator('[data-popup="object"]')
|
|
check('object popup: read-only rows', await object.getByLabel('이름').evaluate(e => e.readOnly) && await object.getByLabel('길이').evaluate(e => e.readOnly))
|
|
await commit(object.getByLabel('X'), '1500')
|
|
check('object popup: number row commits', (await log()).includes('x=1500'))
|
|
const compact = await object.getByLabel('Y').evaluate(e => ({ cls: e.className, w: e.getBoundingClientRect().width }))
|
|
check('object popup: compact row uses the 41px hug field', compact.cls.includes('hm-sw-property__compact') && compact.w < 60, compact)
|
|
const panel = page.locator('[data-popups] .hm-sw-property').nth(2)
|
|
const order = await panel.locator('.hm-sw-property__body > *').evaluateAll(list => list.map(e => e.getAttribute('aria-label')))
|
|
check('property panel + children: children follow the property groups in the same body', JSON.stringify(order) === '["공통","메모"]', order)
|
|
await commit(panel.getByLabel('설명'), '내벽')
|
|
check('property panel + children: child content callback', (await log()).includes('memo=내벽'))
|
|
await settings.locator('.hm-sw-property__close').click()
|
|
check('popup: close button calls onClose', (await log()).includes('settings.onClose'))
|
|
await page.locator('[data-popups]').screenshot({ path: path.join(out, 'popup-custom-content.png') })
|
|
|
|
await goto('sw-popup--multiple-popups')
|
|
const popup = id => page.locator(`[data-popup="${id}"]`)
|
|
const b = popup('b')
|
|
await b.locator('[data-popup-layer] button.hm-sw-header__field').click()
|
|
const bMenu = b.locator('.hm-sw-header__layer-menu'), bHit = await bMenu.locator('.hm-sw-header__layer-footer').evaluate(row => { const r = row.getBoundingClientRect(); return row.contains(document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)) })
|
|
const bBox = await rect(b), menuB = await rect(bMenu)
|
|
check('short popup: dropdown extends past the popup and stays fully visible (no clipping)', menuB.y + menuB.h > bBox.y + bBox.h && bHit, { bBox, menuB, bHit })
|
|
await page.locator('[data-stack]').screenshot({ path: path.join(out, 'popup-short-dropdown.png') })
|
|
await page.keyboard.press('Escape')
|
|
check('several popups: Escape in a dropdown closes only the dropdown', await bMenu.count() === 0 && await popup('a').count() === 1 && await b.count() === 1 && await popup('c').count() === 1)
|
|
const aInput = popup('a').getByLabel('A 값')
|
|
await aInput.click(); await aInput.fill('A2'); await aInput.press('Escape')
|
|
check('several popups: Escape on an edited input cancels the edit only', await aInput.inputValue() === 'A' && await popup('a').count() === 1)
|
|
await aInput.press('Escape')
|
|
check('several popups: Escape inside a popup closes that popup only', (await log()).endsWith('close(a)') && await b.count() === 1 && await popup('c').count() === 1, await log())
|
|
await page.locator('body').click({ position: { x: 5, y: 5 } })
|
|
await page.keyboard.press('Escape')
|
|
// B was used last (its dropdown), so B is the top popup, not C (opened last).
|
|
check('several popups: Escape outside closes only the top popup (last used: B)', (await log()).endsWith('close(b)') && await popup('c').count() === 1, await log())
|
|
await page.locator('[data-reopen]').click()
|
|
await popup('a').locator('.hm-sw-property__titlebar').click()
|
|
await page.locator('body').click({ position: { x: 5, y: 5 } })
|
|
await page.keyboard.press('Escape')
|
|
check('several popups: the popup used last becomes the top', (await log()).endsWith('close(a)'), await log())
|
|
const c = popup('c').locator('.hm-sw-property__body')
|
|
const scroll = await c.evaluate(e => ({ sh: e.scrollHeight, ch: e.clientHeight, overflow: getComputedStyle(e).overflowY }))
|
|
await c.evaluate(e => { e.scrollTop = e.scrollHeight })
|
|
const lastVisible = await popup('c').getByLabel('필드 30').evaluate(e => { const r = e.getBoundingClientRect(); return e.closest('.hm-sw-property__row').contains(document.elementFromPoint(r.x + 4, r.y + r.height / 2)) })
|
|
check('long content: the body scrolls inside the frame and the last row is reachable', scroll.sh > scroll.ch && scroll.overflow === 'auto' && lastVisible && (await rect(popup('c'))).h === 360, { scroll, lastVisible })
|
|
await page.locator('[data-stack]').screenshot({ path: path.join(out, 'popup-stack.png') })
|
|
}
|