From 9534b71358d81a67bfd56136e8652d02b5b44693 Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 17 Aug 2026 10:28:47 +0000 Subject: [PATCH] =?UTF-8?q?fix(game-frontend):=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=95=98=EB=8B=A8=20=EB=A9=94=EB=89=B4=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=EB=A1=9C=20=ED=8E=BC=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문서 끝의 반복 공통 메뉴만 dropup으로 배치하고 상단과 중간 메뉴의 dropdown 동작은 유지한다. 데스크톱과 모바일 Chromium에서 popup 위치, caret, shadow 방향을 검증한다. --- app/game-frontend/e2e/mainNavigation.spec.ts | 78 +++++++++++++++++++ .../src/components/main/MainGlobalMenu.vue | 11 +++ 2 files changed, 89 insertions(+) diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index eac9a843..1675c1d5 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -748,6 +748,7 @@ const persistArtifact = async (page: Page, name: string) => { return { viewport: { width: innerWidth, height: innerHeight }, global: describe('.main-global-menu'), + bottomGlobalPopup: describe('[data-menu-position="bottom"] .main-menu-popup__list'), nation: describe('.main-nation-menu'), bottom: describe('.main-mobile-bottom'), globalPopup: describe('#mobile-global-menu'), @@ -914,6 +915,20 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro await gameInfoButton.press('Enter'); await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true'); await expect(global.locator('#global-menu-game-info')).toBeVisible(); + const topMenuGeometry = await gameInfoButton.evaluate((button) => { + const popup = button.parentElement?.querySelector('.main-menu-popup__list'); + const caret = button.querySelector('.menu-caret'); + if (!popup || !caret) throw new Error('top global menu popup geometry is incomplete'); + return { + trigger: button.getBoundingClientRect().toJSON(), + popup: popup.getBoundingClientRect().toJSON(), + caretBorderTopWidth: getComputedStyle(caret).borderTopWidth, + caretBorderBottomWidth: getComputedStyle(caret).borderBottomWidth, + }; + }); + expect(topMenuGeometry.popup.top).toBeGreaterThanOrEqual(topMenuGeometry.trigger.bottom + 1); + expect(topMenuGeometry.caretBorderTopWidth).toBe('4px'); + expect(topMenuGeometry.caretBorderBottomWidth).toBe('0px'); await page.keyboard.press('Escape'); await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'false'); await expect(gameInfoButton).toBeFocused(); @@ -921,9 +936,72 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro await gameInfoButton.click(); await page.getByRole('heading', { name: '메인 화면 검증 시나리오' }).click(); await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'false'); + + const bottomGlobal = page.locator('[data-menu-position="bottom"]'); + const bottomGameInfoButton = bottomGlobal.locator('[data-menu-id="game-info"]'); + await bottomGameInfoButton.click(); + await expect(bottomGameInfoButton).toHaveAttribute('aria-expanded', 'true'); + await expect(bottomGlobal.locator('#global-menu-game-info')).toBeVisible(); + const bottomMenuGeometry = await bottomGameInfoButton.evaluate((button) => { + const popup = button.parentElement?.querySelector('.main-menu-popup__list'); + const caret = button.querySelector('.menu-caret'); + if (!popup || !caret) throw new Error('bottom global menu popup geometry is incomplete'); + return { + trigger: button.getBoundingClientRect().toJSON(), + popup: popup.getBoundingClientRect().toJSON(), + caretBorderTopWidth: getComputedStyle(caret).borderTopWidth, + caretBorderBottomWidth: getComputedStyle(caret).borderBottomWidth, + boxShadow: getComputedStyle(popup).boxShadow, + }; + }); + expect(bottomMenuGeometry.popup.bottom).toBeLessThanOrEqual(bottomMenuGeometry.trigger.top - 1); + expect(bottomMenuGeometry.caretBorderTopWidth).toBe('0px'); + expect(bottomMenuGeometry.caretBorderBottomWidth).toBe('4px'); + expect(bottomMenuGeometry.boxShadow).toContain('0px -8px 18px'); await persistArtifact(page, `${basePath.slice(1)}-desktop-1200`); }); +test('the repeated bottom global menu opens upward on the mobile document', async ({ page }, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 1, + npcMode: 1, + scenarioTitle: '하단 메뉴 방향 검증 시나리오', + generalMeCalls: 0, + operations: [], + }; + await installFixture(page, state); + await page.setViewportSize({ width: 500, height: 900 }); + await waitForMain(page); + + const bottomGlobal = page.locator('[data-menu-position="bottom"]'); + const gameInfoButton = bottomGlobal.locator('[data-menu-id="game-info"]'); + await gameInfoButton.click(); + await expect(gameInfoButton).toHaveAttribute('aria-expanded', 'true'); + await expect(bottomGlobal.locator('#global-menu-game-info')).toBeVisible(); + const geometry = await gameInfoButton.evaluate((button) => { + const popup = button.parentElement?.querySelector('.main-menu-popup__list'); + const caret = button.querySelector('.menu-caret'); + if (!popup || !caret) throw new Error('mobile bottom global menu popup geometry is incomplete'); + return { + trigger: button.getBoundingClientRect().toJSON(), + popup: popup.getBoundingClientRect().toJSON(), + caretBorderTopWidth: getComputedStyle(caret).borderTopWidth, + caretBorderBottomWidth: getComputedStyle(caret).borderBottomWidth, + viewportHeight: window.innerHeight, + }; + }); + expect(geometry.popup.bottom).toBeLessThanOrEqual(geometry.trigger.top - 1); + expect(geometry.popup.top).toBeGreaterThanOrEqual(0); + expect(geometry.popup.bottom).toBeLessThanOrEqual(geometry.viewportHeight); + expect(geometry.caretBorderTopWidth).toBe('0px'); + expect(geometry.caretBorderBottomWidth).toBe('4px'); + await bottomGlobal.screenshot({ path: testInfo.outputPath('mobile-bottom-global-dropup.png') }); + await persistArtifact(page, `${basePath.slice(1)}-mobile-bottom-dropup`); +}); + test('main general card uses local turn time and command clock tracks corrected server time', async ({ page }) => { const state: NavigationFixture = { officerLevel: 0, diff --git a/app/game-frontend/src/components/main/MainGlobalMenu.vue b/app/game-frontend/src/components/main/MainGlobalMenu.vue index a149751d..0ff82433 100644 --- a/app/game-frontend/src/components/main/MainGlobalMenu.vue +++ b/app/game-frontend/src/components/main/MainGlobalMenu.vue @@ -184,6 +184,17 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props list-style: none; } +.main-global-menu[data-menu-position='bottom'] .main-menu-popup__list { + top: auto; + bottom: calc(100% + 2px); + box-shadow: 0 -8px 18px rgb(0 0 0 / 45%); +} + +.main-global-menu[data-menu-position='bottom'] .menu-caret { + border-top-width: 0; + border-bottom: 4px solid currentColor; +} + .main-menu-split .main-menu-popup__list { right: 0; left: auto;