diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 34c3b21a..1aa67dbd 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -3083,6 +3083,51 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn await captureProgress('mobile-500'); }); +test('main layout and map use the same mobile/desktop boundary', async ({ page }, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 6, + npcMode: 1, + scenarioTitle: '모바일 검증 시나리오', + lastExecuted: null, + latestVote: null, + generalMeCalls: 0, + operations: [], + validMapImages: true, + }; + await installFixture(page, state); + await page.setViewportSize({ width: 1000, height: 900 }); + await waitForMain(page); + await expect(page.locator('.layout-desktop [data-main-target="map"] .map-area')).toBeVisible(); + expect( + await page + .locator('.layout-desktop [data-main-target="map"] .map-area') + .evaluate((element) => element.getBoundingClientRect().width) + ).toBe(700); + await page.screenshot({ path: testInfo.outputPath('screen-mode-desktop-1000.png'), fullPage: true }); + + await page.setViewportSize({ width: 940, height: 900 }); + await expect(page.locator('.layout-desktop')).toBeVisible(); + await expect(page.locator('.layout-desktop [data-main-target="map"] .map-area')).toBeVisible(); + expect( + await page + .locator('.layout-desktop [data-main-target="map"] .map-area') + .evaluate((element) => element.getBoundingClientRect().width) + ).toBeGreaterThan(500); + + await page.setViewportSize({ width: 939, height: 900 }); + await expect(page.locator('.layout-mobile')).toBeVisible(); + await expect(page.locator('.layout-mobile [data-main-target="map"] .map-area')).toBeVisible(); + expect( + await page + .locator('.layout-mobile [data-main-target="map"] .map-area') + .evaluate((element) => element.getBoundingClientRect().width) + ).toBe(500); + await page.screenshot({ path: testInfo.outputPath('screen-mode-mobile-939.png'), fullPage: true }); +}); + test('the 939/940 boundary switches to the Ref-style 500px single document', async ({ page }) => { const state: NavigationFixture = { officerLevel: 5, @@ -3630,12 +3675,20 @@ test('automatic screen mode switches wide mobile screens to the 1000px layout at npcMode: 1, generalMeCalls: 0, operations: [], + validMapImages: true, }; await installFixture(mobilePage, state); await waitForMain(mobilePage); const expectedWideLayout = deviceWidth >= 700; await expect(mobilePage.locator(expectedWideLayout ? '.layout-desktop' : '.layout-mobile')).toBeVisible(); + const activeMap = mobilePage.locator( + `${expectedWideLayout ? '.layout-desktop' : '.layout-mobile'} [data-main-target="map"] .map-area` + ); + await expect(activeMap).toBeVisible(); + expect(await activeMap.evaluate((element) => element.getBoundingClientRect().width)).toBe( + expectedWideLayout ? 700 : 500 + ); expect( await mobilePage.evaluate(() => document.querySelector('meta[name="viewport"]')?.content) ).toBe(expectedWideLayout ? 'width=1000' : 'width=device-width, initial-scale=1'); @@ -3664,6 +3717,10 @@ test('automatic screen mode switches wide mobile screens to the 1000px layout at document.dispatchEvent(new CustomEvent('tryChangeScreenMode')); }); await expect(mobilePage.locator('.layout-mobile')).toBeVisible(); + await expect(mobilePage.locator('.layout-mobile [data-main-target="map"] .map-area')).toHaveCSS( + 'width', + '500px' + ); expect( await mobilePage.evaluate( () => document.querySelector('meta[name="viewport"]')?.content @@ -3686,6 +3743,10 @@ test('automatic screen mode switches wide mobile screens to the 1000px layout at document.dispatchEvent(new CustomEvent('tryChangeScreenMode')); }); await expect(mobilePage.locator('.layout-desktop')).toBeVisible(); + await expect(mobilePage.locator('.layout-desktop [data-main-target="map"] .map-area')).toHaveCSS( + 'width', + '700px' + ); expect( await mobilePage.evaluate( () => document.querySelector('meta[name="viewport"]')?.content diff --git a/app/game-frontend/src/components/main/MapViewer.vue b/app/game-frontend/src/components/main/MapViewer.vue index d757caee..5de4e844 100644 --- a/app/game-frontend/src/components/main/MapViewer.vue +++ b/app/game-frontend/src/components/main/MapViewer.vue @@ -9,6 +9,7 @@ import { useMapViewerStore } from '../../stores/mapViewer'; import { buildAssetUrl } from '../../utils/mapAssets'; import { resolveMapBackgroundPath, resolveMapSeason, resolveNextMapSeason } from '../../utils/mapBackground'; import { configuredGameAssetUrl } from '../../utils/imageAssets'; +import { SCREEN_MODE_DESKTOP_MEDIA_QUERY } from '../../utils/screenModeViewport'; interface MapSummary { year: number; @@ -133,7 +134,7 @@ const preloadDecodedImage = (url: string): Promise => { return pending; }; -const isWide = useMediaQuery('(min-width: 1024px)'); +const isWide = useMediaQuery(SCREEN_MODE_DESKTOP_MEDIA_QUERY); const mapStore = useMapViewerStore(); const { showCityName, diff --git a/app/game-frontend/src/utils/screenModeViewport.ts b/app/game-frontend/src/utils/screenModeViewport.ts index 260fec55..d72978ba 100644 --- a/app/game-frontend/src/utils/screenModeViewport.ts +++ b/app/game-frontend/src/utils/screenModeViewport.ts @@ -1,5 +1,8 @@ export const SCREEN_MODE_KEY = 'sam.screenMode'; export const SCREEN_MODE_CHANGE_EVENT = 'tryChangeScreenMode'; +export const SCREEN_MODE_DESKTOP_MIN_WIDTH = 940; +export const SCREEN_MODE_DESKTOP_MEDIA_QUERY = `(min-width: ${SCREEN_MODE_DESKTOP_MIN_WIDTH}px)`; +export const SCREEN_MODE_MOBILE_MEDIA_QUERY = `(max-width: ${SCREEN_MODE_DESKTOP_MIN_WIDTH - 0.02}px)`; export type ScreenMode = 'auto' | '500px' | '1000px'; diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 0411673b..4548993e 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -37,11 +37,12 @@ import { MOBILE_MAIN_PANEL_ORDER_STORAGE_KEY, type MobileMainPanelId, } from '../utils/mobileMainPanelOrder'; +import { SCREEN_MODE_MOBILE_MEDIA_QUERY } from '../utils/screenModeViewport'; const session = useSessionStore(); const dashboard = useMainDashboardStore(); const { info: showInfoToast } = useGameFeedback(); -const isMobile = useMediaQuery('(max-width: 939.98px)'); +const isMobile = useMediaQuery(SCREEN_MODE_MOBILE_MEDIA_QUERY); const npcMode = ref(0); const globalNavigation = ref(defaultGlobalNavigation); diff --git a/app/game-frontend/test/screenModeViewport.test.ts b/app/game-frontend/test/screenModeViewport.test.ts index 45e426a9..0ac60f72 100644 --- a/app/game-frontend/test/screenModeViewport.test.ts +++ b/app/game-frontend/test/screenModeViewport.test.ts @@ -5,8 +5,17 @@ import { normalizeScreenMode, resolveAutoViewportContent, resolveViewportContent, + SCREEN_MODE_DESKTOP_MEDIA_QUERY, + SCREEN_MODE_DESKTOP_MIN_WIDTH, + SCREEN_MODE_MOBILE_MEDIA_QUERY, } from '../src/utils/screenModeViewport.ts'; +void test('main and map layouts share one 939/940 screen-mode boundary', () => { + assert.equal(SCREEN_MODE_DESKTOP_MIN_WIDTH, 940); + assert.equal(SCREEN_MODE_DESKTOP_MEDIA_QUERY, '(min-width: 940px)'); + assert.equal(SCREEN_MODE_MOBILE_MEDIA_QUERY, '(max-width: 939.98px)'); +}); + void test('automatic mode follows the Ref physical-screen thresholds', () => { assert.equal(resolveAutoViewportContent({ deviceWidth: 390, viewportHeight: 844 }), 'width=500'); assert.equal(