diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index afd6bcc7..99bcf16b 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -1271,7 +1271,7 @@ test('내 정보&설정의 지난 플레이는 기본 탐색과 분리되어 오 } }); -test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 Ref 순서로 복원한다', async ({ page }) => { +test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버튼으로 재정렬하고 기본 순서로 복원한다', async ({ page }) => { const state: FixtureState = { permission: 'head', myset: 3, settingMutations: [], accessPages: [] }; await install(page, state); await page.setViewportSize({ width: 390, height: 844 }); @@ -1334,7 +1334,7 @@ test('내 정보&설정에서 모바일 메인 패널을 드래그하거나 버 .toEqual(['messages', 'commands', 'nation-menu', 'nation', 'general', 'map', 'city', 'records', 'global-menu']); await page.getByRole('button', { name: '순서 바꾸기', exact: true }).click(); - await dialog.getByRole('button', { name: 'Ref 초깃값' }).click(); + await dialog.getByRole('button', { name: '기본값', exact: true }).click(); await expect.poll(readOrder).toEqual(defaultOrder); await dialog.getByRole('button', { name: '적용', exact: true }).click(); await expect diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index a7fb2e50..81a3271b 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -792,6 +792,103 @@ const gridColumnCount = async (page: Page, selector: string) => .first() .evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(' ').length); +const setMobilePanelOrder = async (page: Page, order: readonly string[]) => { + await page.evaluate((nextOrder) => { + localStorage.setItem('sam.mobileMainPanelOrder.v1', JSON.stringify(nextOrder)); + document.dispatchEvent(new CustomEvent('sam-mobile-main-panel-order-changed')); + }, order); +}; + +const inspectMobilePanelLayout = async (page: Page) => + page.locator('.layout-mobile').evaluate((container) => { + const containerStyle = getComputedStyle(container); + const panels = [...container.querySelectorAll(':scope > [data-mobile-panel-id]')].map( + (element, domIndex) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + const content = element.firstElementChild as HTMLElement | null; + const contentStyle = content ? getComputedStyle(content) : null; + return { + id: element.dataset.mobilePanelId ?? '', + domIndex, + top: rect.top, + bottom: rect.bottom, + left: rect.left, + right: rect.right, + width: rect.width, + height: rect.height, + display: style.display, + position: style.position, + inset: [style.top, style.right, style.bottom, style.left], + order: style.order, + transform: style.transform, + float: style.cssFloat, + gridRow: `${style.gridRowStart} / ${style.gridRowEnd}`, + gridColumn: `${style.gridColumnStart} / ${style.gridColumnEnd}`, + marginTop: style.marginTop, + marginBottom: style.marginBottom, + content: contentStyle + ? { + position: contentStyle.position, + order: contentStyle.order, + transform: contentStyle.transform, + marginTop: contentStyle.marginTop, + marginBottom: contentStyle.marginBottom, + height: contentStyle.height, + } + : null, + }; + } + ); + return { + container: { + display: containerStyle.display, + flexDirection: containerStyle.flexDirection, + position: containerStyle.position, + transform: containerStyle.transform, + }, + panels, + visualOrder: [...panels] + .sort((left, right) => left.top - right.top || left.left - right.left) + .map(({ id }) => id), + }; + }); + +const expectMobilePanelVisualOrder = async (page: Page, expectedOrder: readonly string[]) => { + await expect + .poll(() => + page + .locator('.layout-mobile > [data-mobile-panel-id]') + .evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-panel-id'))) + ) + .toEqual(expectedOrder); + const audit = await inspectMobilePanelLayout(page); + expect(audit.container).toEqual({ + display: 'flex', + flexDirection: 'column', + position: 'static', + transform: 'none', + }); + expect(audit.panels.map(({ id }) => id)).toEqual(expectedOrder); + expect(audit.visualOrder).toEqual(expectedOrder); + expect(audit.panels.every(({ left, right, width }) => left >= 0 && right <= 500 && width === 500)).toBe(true); + expect( + audit.panels.every((panel, index) => index === 0 || panel.top >= audit.panels[index - 1]!.bottom) + ).toBe(true); + for (const panel of audit.panels) { + expect(panel.display, `${panel.id}: display`).not.toBe('none'); + expect(['static', 'relative'], `${panel.id}: position`).toContain(panel.position); + expect( + panel.inset.every((value) => value === 'auto' || value === '0px'), + `${panel.id}: inset ${panel.inset.join(' ')}` + ).toBe(true); + expect(panel.order, `${panel.id}: order`).toBe('0'); + expect(panel.transform, `${panel.id}: transform`).toBe('none'); + expect(panel.float, `${panel.id}: float`).toBe('none'); + } + return audit; +}; + const raisedButtonState = async (target: Locator) => target.evaluate((element) => { const rect = element.getBoundingClientRect(); @@ -2213,49 +2310,45 @@ test('the 939/940 boundary switches to the Ref-style 500px single document', asy ]) { await expect(page.locator(selector)).toBeVisible(); } - const readMobilePanelOrder = () => - page - .locator('.layout-mobile > [data-mobile-panel-id]') - .evaluateAll((elements) => elements.map((element) => element.getAttribute('data-mobile-panel-id'))); - await expect - .poll(readMobilePanelOrder) - .toEqual(['commands', 'nation-menu', 'nation', 'general', 'city', 'map', 'records', 'global-menu', 'messages']); - await page.evaluate(() => { - localStorage.setItem( - 'sam.mobileMainPanelOrder.v1', - JSON.stringify([ - 'messages', - 'map', - 'commands', - 'nation-menu', - 'nation', - 'general', - 'city', - 'records', - 'global-menu', - ]) + const defaultOrder = [ + 'commands', + 'nation-menu', + 'nation', + 'general', + 'city', + 'map', + 'records', + 'global-menu', + 'messages', + ]; + const customOrder = [ + 'messages', + 'map', + 'commands', + 'nation-menu', + 'nation', + 'general', + 'city', + 'records', + 'global-menu', + ]; + const reverseOrder = [...defaultOrder].reverse(); + const mobilePanelAudits = { + default: await expectMobilePanelVisualOrder(page, defaultOrder), + custom: null as Awaited> | null, + reverse: null as Awaited> | null, + }; + await setMobilePanelOrder(page, customOrder); + mobilePanelAudits.custom = await expectMobilePanelVisualOrder(page, customOrder); + await setMobilePanelOrder(page, reverseOrder); + mobilePanelAudits.reverse = await expectMobilePanelVisualOrder(page, reverseOrder); + if (artifactRoot) { + await mkdir(artifactRoot, { recursive: true }); + await writeFile( + resolve(artifactRoot, `${basePath.slice(1)}-mobile-panel-css-order-audit.json`), + `${JSON.stringify(mobilePanelAudits, null, 2)}\n` ); - document.dispatchEvent(new CustomEvent('sam-mobile-main-panel-order-changed')); - }); - await expect - .poll(readMobilePanelOrder) - .toEqual(['messages', 'map', 'commands', 'nation-menu', 'nation', 'general', 'city', 'records', 'global-menu']); - const customPanelGeometry = await page.locator('.layout-mobile > [data-mobile-panel-id]').evaluateAll((elements) => - elements.map((element) => { - const rect = element.getBoundingClientRect(); - return { - id: element.getAttribute('data-mobile-panel-id'), - top: rect.top, - bottom: rect.bottom, - left: rect.left, - right: rect.right, - }; - }) - ); - expect(customPanelGeometry.every(({ left, right }) => left >= 0 && right <= 500)).toBe(true); - expect( - customPanelGeometry.every((panel, index) => index === 0 || panel.top >= customPanelGeometry[index - 1]!.bottom) - ).toBe(true); + } await persistArtifact(page, `${basePath.slice(1)}-mobile-500`); }); @@ -2556,6 +2649,27 @@ test('real mobile devices initially fit the complete 500px game canvas', async ( expect(mainGeometry.documentScrollWidth).toBeLessThanOrEqual(mainGeometry.innerWidth); expect(mainGeometry.canvas).toEqual({ left: 0, right: 500, width: 500 }); expect(mainGeometry.canvas.right).toBeLessThanOrEqual((mainGeometry.visualViewportWidth ?? 0) + 0.01); + let physicalPanelOrderAudit: unknown = null; + if (deviceWidth === 390) { + const defaultOrder = [ + 'commands', + 'nation-menu', + 'nation', + 'general', + 'city', + 'map', + 'records', + 'global-menu', + 'messages', + ]; + const reverseOrder = [...defaultOrder].reverse(); + const defaultAudit = await expectMobilePanelVisualOrder(mobilePage, defaultOrder); + await setMobilePanelOrder(mobilePage, reverseOrder); + const reverseAudit = await expectMobilePanelVisualOrder(mobilePage, reverseOrder); + physicalPanelOrderAudit = { default: defaultAudit, reverse: reverseAudit }; + await setMobilePanelOrder(mobilePage, defaultOrder); + await expectMobilePanelVisualOrder(mobilePage, defaultOrder); + } if (artifactRoot) { await mkdir(artifactRoot, { recursive: true }); await mobilePage.screenshot({ @@ -2599,7 +2713,11 @@ test('real mobile devices initially fit the complete 500px game canvas', async ( } } - measurements[String(deviceWidth)] = { main: mainGeometry, routes: routeGeometry }; + measurements[String(deviceWidth)] = { + main: mainGeometry, + mobilePanelOrder: physicalPanelOrderAudit, + routes: routeGeometry, + }; await context.close(); } diff --git a/app/game-frontend/src/views/MyPageView.vue b/app/game-frontend/src/views/MyPageView.vue index 9798859c..28d367d7 100644 --- a/app/game-frontend/src/views/MyPageView.vue +++ b/app/game-frontend/src/views/MyPageView.vue @@ -743,7 +743,7 @@ onMounted(() => {
- +