// 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')) // 2026-10-20: the unit select is SWSelect (the Viewer 축척 지정 select), no longer the HM UI Select. await settings.locator('[data-sw-select="unit"]').selectOption('cm') check('popup content: SWSelect (unit) works inside', (await log()).includes('unit=cm') && await settings.getByLabel('단위').inputValue() === 'cm') // 2026-10-20: the popup gauge is SWSlider (native range; keyboard steps commit through onValueCommit). await settings.locator('[data-popup-slider]').focus() await page.keyboard.press('ArrowRight') await page.waitForTimeout(100) check('popup content: SWSlider works inside', /opacity=70/.test(await log()) && await settings.locator('[data-popup-slider]').evaluate(e => e.classList.contains('hm-sw-slider') && e.dataset.appearance === 'panel' && e.value === '70'), await log()) check('popup content: no HM UI Slider left inside the popups', await page.locator('[data-popups] [data-slot="slider"]').count() === 0) await settings.locator('[data-ui-apply]').click() check('popup content: HM UI Button works inside', (await log()).includes('apply')) // 2026-10-20: popup layer / color are the property window pickers (SWPopupPropertyRow); SWHeader field dropdowns are // header-only. The same clipping / overlay / callback / Escape requirements apply to the popup pickers. check('popup dropdown: no SWHeader field dropdown inside the popups', await page.locator('[data-popups] .hm-sw-header__field').count() === 0) const layerField = settings.locator('[data-property-id="layer"] .hm-sw-property__trigger') await layerField.click() const fieldBox = await rect(settings.locator('[data-property-id="layer"] .hm-sw-property__value')), menu = page.locator('.hm-sw-property__drop[data-picker="layer"]') 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: popup layer picker opens right under its field (fixed overlay, right edge) 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 + menuBox.w) - (fieldBox.x + fieldBox.w)) < 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 menu.count() === 0 && await settings.isVisible()) await settings.locator('[data-property-id="color"] .hm-sw-property__swatch-area').click() const colorPanel = page.locator('.hm-sw-property__drop[data-picker="color"]') check('popup dropdown: popup color picker 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=0,255,0'), await log()) if (await colorPanel.count()) { await colorPanel.getByLabel('초록').focus(); await page.keyboard.press('Escape') } check('popup dropdown: Escape closes the dropdown only', await colorPanel.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-property-id="layer"] .hm-sw-property__trigger').click() const bMenu = page.locator('.hm-sw-property__drop[data-picker="layer"]'), 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) // 2026-10-21 (user decision): Popup A's value is read-only plain text (no input, no red, no focus ring). The Story has // no editable input any more (A's was the only one): Escape on an edited input is covered by property-fields / // overview; here Escape inside A is pressed on its close button. const aValue = await popup('a').locator('[data-readonly-value="a"]').evaluate(e => ({ tag: e.tagName, tab: e.tabIndex, inputs: e.closest('[data-popup]').querySelectorAll('.hm-sw-property__body input').length, color: getComputedStyle(e).color })) check('several popups: Popup A shows its read-only value as plain text (not focusable, not red)', aValue.tag === 'SPAN' && aValue.tab === -1 && aValue.inputs === 0 && aValue.color !== 'rgb(255, 0, 0)', aValue) await popup('a').locator('button').first().focus() await page.keyboard.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()) // Reloading the Story reopens A, B and C (the page has no reopen button since 2026-10-21). await goto('sw-popup--multiple-popups') 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') }) }