From 4573cedcb430ee2c7eedd5576f3ccf3f805c9e75 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 17 Aug 2026 01:29:12 +0000 Subject: [PATCH] =?UTF-8?q?fix(game-ui):=20=ED=84=B4=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=EA=B8=B0=20=EB=B2=84=ED=8A=BC=20=EB=88=8C=EB=A6=BC=20=EA=B9=8A?= =?UTF-8?q?=EC=9D=B4=EB=A5=BC=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 카테고리와 각 명령 버튼을 공통 Lumen 하단면과 hover, pointer-down 이동 계약에 연결합니다. --- app/game-frontend/e2e/mainNavigation.spec.ts | 158 +++++++++++++++++- .../src/components/main/CommandSelectForm.vue | 46 +++-- 2 files changed, 176 insertions(+), 28 deletions(-) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 07bdd257..c30f466e 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -1,6 +1,6 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; -import { expect, test, type Page, type Route } from '@playwright/test'; +import { expect, test, type Locator, type Page, type Route } from '@playwright/test'; const response = (data: unknown) => ({ result: { data } }); const errorResponse = (path: string, message: string) => ({ @@ -619,6 +619,97 @@ const gridColumnCount = async (page: Page, selector: string) => .first() .evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length); +const raisedButtonState = async (target: Locator) => + target.evaluate((element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + top: rect.top, + bottom: rect.bottom, + height: rect.height, + backgroundColor: style.backgroundColor, + borderTopWidth: style.borderTopWidth, + borderLeftWidth: style.borderLeftWidth, + borderBottomWidth: style.borderBottomWidth, + borderBottomColor: style.borderBottomColor, + borderRadius: style.borderRadius, + marginTop: style.marginTop, + paddingTop: style.paddingTop, + paddingBottom: style.paddingBottom, + classNames: [...element.classList], + }; + }); + +const pointerDownButtonState = async (page: Page, target: Locator) => { + const box = await target.boundingBox(); + if (!box) throw new Error('raised button is not measurable'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + const state = await raisedButtonState(target); + await page.mouse.move(1, 1); + await page.mouse.up(); + return state; +}; + +const persistEnlargedRaisedButtonProbe = async (page: Page, source: Locator, name: string) => { + if (!artifactRoot) return; + const target = resolve(artifactRoot); + await mkdir(target, { recursive: true }); + const probeId = `raised-button-probe-${name}`; + const hostId = `${probeId}-host`; + await source.evaluate( + (element, ids) => { + const host = document.createElement('div'); + host.id = ids.hostId; + Object.assign(host.style, { + position: 'fixed', + inset: '20px auto auto 20px', + width: '390px', + height: '170px', + padding: '10px', + background: '#000', + zIndex: '2147483647', + overflow: 'hidden', + }); + const stage = document.createElement('div'); + Object.assign(stage.style, { + display: 'flow-root', + width: '90px', + transform: 'scale(4)', + transformOrigin: 'top left', + }); + const probe = element.cloneNode(true) as HTMLElement; + probe.id = ids.probeId; + probe.classList.remove('active'); + probe.removeAttribute('disabled'); + probe.style.width = '90px'; + stage.append(probe); + host.append(stage); + document.body.append(host); + }, + { hostId, probeId } + ); + + const host = page.locator(`#${hostId}`); + const probe = page.locator(`#${probeId}`); + const states: Record>> = {}; + states.default = await raisedButtonState(probe); + await host.screenshot({ path: resolve(target, `${name}-large-default.png`) }); + await probe.hover(); + states.hover = await raisedButtonState(probe); + await host.screenshot({ path: resolve(target, `${name}-large-hover.png`) }); + const box = await probe.boundingBox(); + if (!box) throw new Error('enlarged raised button is not measurable'); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + states.pointerDown = await raisedButtonState(probe); + await host.screenshot({ path: resolve(target, `${name}-large-pointer-down.png`) }); + await page.mouse.move(1, 1); + await page.mouse.up(); + await writeFile(resolve(target, `${name}-large-states.json`), `${JSON.stringify(states, null, 2)}\n`); + await host.evaluate((element) => element.remove()); +}; + const persistArtifact = async (page: Page, name: string) => { if (!artifactRoot) return; const target = resolve(artifactRoot); @@ -1033,7 +1124,7 @@ test('pure NPC message senders are not rendered as reply targets', async ({ page await persistArtifact(page, `${basePath.slice(1)}-npc-reply-targets-desktop-1200`); }); -test('main reserved-turn picker renders the Ref general category order', async ({ page }) => { +test('main reserved-turn picker renders the Ref category order and raised button depth', async ({ page }) => { const state: NavigationFixture = { officerLevel: 1, permission: 0, @@ -1081,16 +1172,68 @@ test('main reserved-turn picker renders the Ref general category order', async ( expect(desktopGeometry.columns.split(' ')).toHaveLength(3); expect(desktopGeometry.rows).toBe(2); expect(desktopGeometry.horizontalOverflow).toBeLessThanOrEqual(0); - expect(desktopGeometry.categoryButton).toEqual({ height: 32, paddingTop: '6px', paddingBottom: '6px' }); + expect(desktopGeometry.categoryButton).toEqual({ height: 35.5, paddingTop: '5.25px', paddingBottom: '5.25px' }); expect(desktopGeometry.commandButton).toEqual(desktopGeometry.categoryButton); const strategyCategory = picker.getByRole('button', { name: '계략', exact: true }); + await page.mouse.move(1, 1); + const categoryDefault = await raisedButtonState(strategyCategory); + expect(categoryDefault).toMatchObject({ + height: 35.5, + backgroundColor: 'rgb(23, 61, 39)', + borderTopWidth: '0px', + borderLeftWidth: '1px', + borderBottomWidth: '4px', + borderBottomColor: 'rgb(21, 55, 35)', + borderRadius: '5.25px', + marginTop: '0px', + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--lumen']), + }); await strategyCategory.hover(); + const categoryHover = await raisedButtonState(strategyCategory); + expect(categoryHover).toMatchObject({ height: 34.5, borderBottomWidth: '3px', marginTop: '1px' }); + expect(categoryHover.top).toBe(categoryDefault.top + 1); + expect(categoryHover.bottom).toBe(categoryDefault.bottom); + const categoryPointerDown = await pointerDownButtonState(page, strategyCategory); + expect(categoryPointerDown).toMatchObject({ height: 33.5, borderBottomWidth: '2px', marginTop: '2px' }); + expect(categoryPointerDown.top).toBe(categoryDefault.top + 2); + expect(categoryPointerDown.bottom).toBe(categoryDefault.bottom); + await page.keyboard.press('Tab'); await strategyCategory.focus(); await expect(strategyCategory).toBeFocused(); + await expect.poll(() => strategyCategory.evaluate((element) => element.matches(':focus-visible'))).toBe(true); await strategyCategory.click(); await expect(strategyCategory).toHaveClass(/active/); await expect(picker.locator('.command-item')).toHaveText(['화계']); + await page.mouse.move(1, 1); + const commandButton = picker.locator('.command-item').first(); + const commandDefault = await raisedButtonState(commandButton); + expect(commandDefault).toMatchObject({ + height: 35.5, + backgroundColor: 'rgb(48, 32, 22)', + borderTopWidth: '0px', + borderLeftWidth: '1px', + borderBottomWidth: '4px', + borderBottomColor: 'rgb(43, 29, 20)', + borderRadius: '5.25px', + marginTop: '0px', + classNames: expect.arrayContaining(['legacy-button', 'legacy-button--lumen']), + }); + await commandButton.hover(); + const commandHover = await raisedButtonState(commandButton); + expect(commandHover).toMatchObject({ height: 34.5, borderBottomWidth: '3px', marginTop: '1px' }); + expect(commandHover.top).toBe(commandDefault.top + 1); + expect(commandHover.bottom).toBe(commandDefault.bottom); + const commandPointerDown = await pointerDownButtonState(page, commandButton); + expect(commandPointerDown).toMatchObject({ height: 33.5, borderBottomWidth: '2px', marginTop: '2px' }); + expect(commandPointerDown.top).toBe(commandDefault.top + 2); + expect(commandPointerDown.bottom).toBe(commandDefault.bottom); + await page.keyboard.press('Tab'); + await commandButton.focus(); + await expect(commandButton).toBeFocused(); + await expect.poll(() => commandButton.evaluate((element) => element.matches(':focus-visible'))).toBe(true); + await persistEnlargedRaisedButtonProbe(page, strategyCategory, 'turn-selector-category'); + await persistEnlargedRaisedButtonProbe(page, commandButton, 'turn-selector-command'); await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-desktop-1200`); await page.setViewportSize({ width: 500, height: 900 }); @@ -1118,7 +1261,11 @@ test('main reserved-turn picker renders the Ref general category order', async ( }; }); expect(mobileGeometry.horizontalOverflow).toBeLessThanOrEqual(0); - expect(mobileGeometry.categoryButton).toEqual({ height: 32, paddingTop: '6px', paddingBottom: '6px' }); + expect(mobileGeometry.categoryButton).toEqual({ + height: 35.5, + paddingTop: '5.25px', + paddingBottom: '5.25px', + }); expect(mobileGeometry.commandButton).toEqual(mobileGeometry.categoryButton); await persistArtifact(page, `${basePath.slice(1)}-main-reserved-ref-categories-mobile-500`); }); @@ -1327,6 +1474,9 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn await page.locator('[data-main-target="commands"] .select-command').click(); const picker = page.getByTestId('command-picker'); await expect(picker).toBeVisible(); + // The trigger can end up directly above a newly opened category button. + // Measure the default grid after leaving the intentional Lumen hover state. + await page.mouse.move(1, 1); const pickerGeometry = await picker.evaluate((element) => { const rect = element.getBoundingClientRect(); const editor = element.closest('.reserved-command-editor')?.getBoundingClientRect(); diff --git a/app/game-frontend/src/components/main/CommandSelectForm.vue b/app/game-frontend/src/components/main/CommandSelectForm.vue index 50ab4929..23ee56af 100644 --- a/app/game-frontend/src/components/main/CommandSelectForm.vue +++ b/app/game-frontend/src/components/main/CommandSelectForm.vue @@ -130,7 +130,12 @@ const commandTitle = (command: CommandAvailability) =>