From 1d8f80f3814d6b57b3ec4602dd638d44322462d8 Mon Sep 17 00:00:00 2001 From: hided62 Date: Fri, 21 Aug 2026 01:52:22 +0000 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=20=EB=B2=84=ED=8A=BC=20=ED=96=89=EC=9D=84=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일반 사용자는 두 버튼을 한 행에 유지하고 관리자 링크는 모바일에서 전체폭 둘째 행으로 분리한다. 500px과 더 좁은 viewport의 Chromium 좌표 및 상호작용 회귀를 추가한다. --- .../e2e/lobby-admin-navigation.spec.ts | 106 ++++++++++++++++++ app/gateway-frontend/src/views/LobbyView.vue | 54 ++++++++- 2 files changed, 157 insertions(+), 3 deletions(-) diff --git a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts index e5721eb6..d3ae7b0c 100644 --- a/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts +++ b/app/gateway-frontend/e2e/lobby-admin-navigation.spec.ts @@ -240,6 +240,112 @@ test('desktop administrator sidebar follows the navbar away and then sticks to t await writeFile(testInfo.outputPath('admin-sidebar-scroll-geometry.json'), JSON.stringify(measurements, null, 2)); }); +test('keeps mobile account actions on clean rows for users and administrators', async ({ browser }, testInfo) => { + const measureAccountActions = async (page: Page) => { + const actions = page.locator('.account-actions'); + await expect(actions).toBeVisible(); + return actions.evaluate((element) => { + const rect = (target: Element) => { + const value = target.getBoundingClientRect(); + return { + top: value.top, + right: value.right, + bottom: value.bottom, + left: value.left, + width: value.width, + height: value.height, + }; + }; + const items = Array.from(element.children).map((child) => ({ + ...rect(child), + clientWidth: (child as HTMLElement).clientWidth, + scrollWidth: (child as HTMLElement).scrollWidth, + whiteSpace: getComputedStyle(child).whiteSpace, + })); + return { + container: rect(element), + documentWidth: document.documentElement.scrollWidth, + viewportWidth: window.innerWidth, + items, + }; + }); + }; + + const userContext = await browser.newContext({ viewport: { width: 500, height: 900 } }); + const userPage = await userContext.newPage(); + await installGatewayFixture(userPage, []); + await userPage.goto('lobby'); + const userGeometry = await measureAccountActions(userPage); + expect(userGeometry.documentWidth).toBe(userGeometry.viewportWidth); + expect(userGeometry.items).toHaveLength(2); + expect(userGeometry.items[0]?.top).toBeCloseTo(userGeometry.items[1]?.top ?? 0, 0); + expect(userGeometry.items[0]?.bottom).toBeCloseTo(userGeometry.items[1]?.bottom ?? 0, 0); + expect(userGeometry.items[0]?.width).toBeCloseTo(userGeometry.items[1]?.width ?? 0, 0); + expect(userGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true); + await userPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-user-500px.png'), fullPage: true }); + await userContext.close(); + + const adminContext = await browser.newContext({ viewport: { width: 500, height: 900 } }); + const adminPage = await adminContext.newPage(); + await installGatewayFixture(adminPage, ['superuser']); + await adminPage.goto('lobby'); + const adminGeometry = await measureAccountActions(adminPage); + expect(adminGeometry.documentWidth).toBe(adminGeometry.viewportWidth); + expect(adminGeometry.items).toHaveLength(3); + const [account, logout, admin] = adminGeometry.items; + expect(account?.top).toBeCloseTo(logout?.top ?? 0, 0); + expect(account?.bottom).toBeCloseTo(logout?.bottom ?? 0, 0); + expect(account?.width).toBeCloseTo(logout?.width ?? 0, 0); + expect(admin?.top).toBeCloseTo((logout?.bottom ?? 0) + 16, 0); + expect(admin?.left).toBeCloseTo(account?.left ?? 0, 0); + expect(admin?.right).toBeCloseTo(logout?.right ?? 0, 0); + expect(adminGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true); + expect(account?.whiteSpace).toBe('nowrap'); + expect(admin?.whiteSpace).toBe('nowrap'); + + const adminLink = adminPage.getByRole('link', { name: '관리자 페이지' }); + const baseBackground = await adminLink.evaluate((element) => getComputedStyle(element).backgroundColor); + await adminLink.hover(); + await expect.poll(() => adminLink.evaluate((element) => getComputedStyle(element).backgroundColor)).not.toBe( + baseBackground + ); + await adminLink.focus(); + await expect(adminLink).toBeFocused(); + await adminPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-admin-500px.png'), fullPage: true }); + + await adminPage.setViewportSize({ width: 390, height: 844 }); + const adminNarrowGeometry = await measureAccountActions(adminPage); + expect(adminNarrowGeometry.documentWidth).toBe(adminNarrowGeometry.viewportWidth); + expect(adminNarrowGeometry.items[0]?.top).toBeCloseTo(adminNarrowGeometry.items[1]?.top ?? 0, 0); + expect(adminNarrowGeometry.items[2]?.top).toBeCloseTo( + (adminNarrowGeometry.items[1]?.bottom ?? 0) + 16, + 0 + ); + expect(adminNarrowGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true); + await adminPage.screenshot({ path: testInfo.outputPath('gateway-account-actions-admin-390px.png'), fullPage: true }); + + await adminPage.setViewportSize({ width: 360, height: 800 }); + const adminSmallGeometry = await measureAccountActions(adminPage); + expect(adminSmallGeometry.documentWidth).toBe(adminSmallGeometry.viewportWidth); + expect(adminSmallGeometry.items[1]?.top).toBeCloseTo((adminSmallGeometry.items[0]?.bottom ?? 0) + 16, 0); + expect(adminSmallGeometry.items[2]?.top).toBeCloseTo((adminSmallGeometry.items[1]?.bottom ?? 0) + 16, 0); + expect(adminSmallGeometry.items.every(({ scrollWidth, clientWidth }) => scrollWidth <= clientWidth)).toBe(true); + await writeFile( + testInfo.outputPath('gateway-account-actions-geometry.json'), + JSON.stringify( + { + user: userGeometry, + admin: adminGeometry, + adminNarrow: adminNarrowGeometry, + adminSmall: adminSmallGeometry, + }, + null, + 2 + ) + ); + await adminContext.close(); +}); + test('legacy server operations URL keeps query parameters and redirects to the server list', async ({ page }) => { await installGatewayFixture(page, ['superuser']); diff --git a/app/gateway-frontend/src/views/LobbyView.vue b/app/gateway-frontend/src/views/LobbyView.vue index 078c6dce..98a6c1ae 100644 --- a/app/gateway-frontend/src/views/LobbyView.vue +++ b/app/gateway-frontend/src/views/LobbyView.vue @@ -823,10 +823,10 @@ const handleEnter = async (profile: LobbyProfile, targetPath: string) => { > 계 정 관 리 -
+