diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index bf0b1807..8ae3476a 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -29,6 +29,8 @@ type NavigationFixture = { permission: number; nationLevel: number; stage: number; + tournamentType?: 0 | 1 | 2 | 3; + tournamentWinnerId?: number; npcMode: number; generalMeCalls: number; operations: string[]; @@ -758,7 +760,16 @@ const installFixture = async (page: Page, state: NavigationFixture) => { canSecret: state.permission >= 2, }); } - if (operation === 'tournament.getState') return response({ stage: state.stage }); + if (operation === 'tournament.getState') { + if (state.stage === 0 && state.tournamentType === undefined && state.tournamentWinnerId === undefined) { + return response(null); + } + return response({ + stage: state.stage, + type: state.tournamentType ?? 0, + winnerId: state.tournamentWinnerId, + }); + } return response({ ok: true }); }); operations.forEach((operation, index) => { @@ -1273,9 +1284,9 @@ test('desktop menus preserve ref columns, prefix-safe routes, and controlled dro ); await tournamentToggle.click(); await expect(tournamentToggle).toHaveAttribute('aria-expanded', 'true'); - await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]')).toHaveText( - '토너먼트' - ); + await expect( + nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]') + ).toHaveText('토너먼트'); await expect(nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]')).toHaveAttribute( 'href', `${basePath}/betting` @@ -1437,6 +1448,95 @@ test('shows the persisted official game index beside the scenario title without } }); +test('tournament split main action follows recruitment, betting, finals, and tournament type on desktop and mobile', async ({ + page, +}, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 1, + tournamentType: 3, + npcMode: 1, + scenarioTitle: '토너먼트 동적 메뉴 검증 시나리오', + generalMeCalls: 0, + operations: [], + }; + await installFixture(page, state); + + const cases = [ + { stage: 1, type: 3 as const, label: '설 전', route: '/tournament' }, + { stage: 5, type: 2 as const, label: '일 기 토', route: '/tournament' }, + { stage: 6, type: 2 as const, label: '베 팅 장', route: '/betting' }, + { stage: 7, type: 2 as const, label: '일 기 토', route: '/tournament' }, + { stage: 10, type: 3 as const, label: '설 전', route: '/tournament' }, + { stage: 0, type: 3 as const, label: '설 전', route: '/tournament', winnerId: 17 }, + ]; + + for (const viewport of [ + { width: 1200, height: 900 }, + { width: 500, height: 900 }, + ]) { + await page.setViewportSize(viewport); + for (const lifecycle of cases) { + state.stage = lifecycle.stage; + state.tournamentType = lifecycle.type; + state.tournamentWinnerId = lifecycle.winnerId; + if (page.url() === 'about:blank') { + await waitForMain(page); + } else { + await page.reload(); + await waitForMain(page); + } + + const nationMenu = page.locator('.main-nation-menu:visible'); + const main = nationMenu.locator('[data-navigation-id="tournament"]'); + await expect(main).toHaveText(lifecycle.label); + await expect(main).toHaveAttribute('href', `${basePath}${lifecycle.route}`); + await expect(main).toHaveClass(/highlight/); + + const toggle = nationMenu.locator('[data-menu-id="tournament-betting"]'); + await toggle.click(); + const tournamentItem = nationMenu.locator( + '#nation-menu-tournament-betting [data-navigation-id="tournament-menu"]' + ); + const bettingItem = nationMenu.locator('#nation-menu-tournament-betting [data-navigation-id="betting"]'); + if (lifecycle.stage === 6) { + await expect(tournamentItem).not.toHaveClass(/highlight/); + await expect(bettingItem).toHaveClass(/highlight/); + } else { + await expect(tournamentItem).toHaveClass(/highlight/); + await expect(bettingItem).not.toHaveClass(/highlight/); + } + await page.keyboard.press('Escape'); + + if (viewport.width === 500) { + const nationTrigger = page.locator('[data-bottom-menu="nation"]'); + await nationTrigger.click(); + const mobileTournament = page.locator('#mobile-nation-menu [data-navigation-id="tournament-menu"]'); + const mobileBetting = page.locator('#mobile-nation-menu [data-navigation-id="betting"]'); + if (lifecycle.stage === 6) { + await expect(mobileTournament).not.toHaveClass(/highlight/); + await expect(mobileBetting).toHaveClass(/highlight/); + } else { + await expect(mobileTournament).toHaveClass(/highlight/); + await expect(mobileBetting).not.toHaveClass(/highlight/); + } + await page.keyboard.press('Escape'); + } + } + + await page + .locator('.main-nation-menu:visible [data-menu-id="tournament-betting"]') + .locator('..') + .screenshot({ + path: artifactRoot + ? resolve(artifactRoot, `tournament-dynamic-main-${viewport.width}.png`) + : testInfo.outputPath(`tournament-dynamic-main-${viewport.width}.png`), + }); + } +}); + test('shows game index zero for a profile whose first game starts at zero', async ({ page }) => { const state: NavigationFixture = { officerLevel: 5, @@ -3986,9 +4086,7 @@ for (const viewport of [ await picker.getByLabel('장비 종류', { exact: true }).selectOption('weapon'); const equipment = picker.getByLabel('장비', { exact: true }); await equipment.selectOption('청룡언월도'); - const optionLabelBeforeRefresh = await equipment - .locator('option[value="청룡언월도"]') - .textContent(); + const optionLabelBeforeRefresh = await equipment.locator('option[value="청룡언월도"]').textContent(); await equipment.evaluate((element) => { const select = element as HTMLSelectElement; const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value'); @@ -4118,8 +4216,8 @@ test('keeps an Android Chromium native command select untouched while a turn sig await waitForMain(mobilePage); await expect .poll(() => - mobilePage.evaluate( - () => (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime() + mobilePage.evaluate(() => + (window as unknown as { __hasMainRealtime: () => boolean }).__hasMainRealtime() ) ) .toBe(true); diff --git a/app/game-frontend/src/components/main/MainMobileBottomBar.vue b/app/game-frontend/src/components/main/MainMobileBottomBar.vue index 4ccd28c7..21559738 100644 --- a/app/game-frontend/src/components/main/MainMobileBottomBar.vue +++ b/app/game-frontend/src/components/main/MainMobileBottomBar.vue @@ -4,9 +4,9 @@ import { legacyNationTextColor } from '../../utils/legacyNationColor'; import MainNavigationLink from './MainNavigationLink.vue'; import { buildGlobalNavigation, + buildNationNavigation, isNationNavigationEnabled, isNavigationConfigured, - nationNavigation, quickNavigation, type MainNavigationLink as MainNavigationLinkItem, type MainNavigationEntry, @@ -18,6 +18,7 @@ import { useMenuPopup } from './useMenuPopup'; const props = defineProps<{ access: NationNavigationAccess; tournamentStage: number; + tournamentType: number | null; nationColor: string; npcMode: number; realtimeEnabled: boolean; @@ -35,6 +36,7 @@ const emit = defineEmits<{ const { setRoot, openId, close, toggle } = useMenuPopup(); const globalEntries = computed(() => buildGlobalNavigation(props.npcMode, props.entries)); +const nationEntries = computed(() => buildNationNavigation(props.tournamentStage, props.tournamentType)); const nationMenuColor = computed(() => props.nationColor || '#000000'); const nationMenuTextColor = computed(() => legacyNationTextColor(nationMenuColor.value)); const isActive = (link: MainNavigationLinkItem) => @@ -150,7 +152,7 @@ const onAction = (action: NonNullable) => {