diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 0457dbce..4db5f643 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -78,6 +78,7 @@ type NavigationFixture = { messages?: unknown; messageContacts?: unknown; autorunLimit?: number | null; + autorunUser?: { limitMinutes: number; options: string[] } | null; dashboardResponses?: Array<{ bytes: number; contextKind: string | null; @@ -633,6 +634,7 @@ const installFixture = async (page: Page, state: NavigationFixture) => { clockStartsAt: state.clockStartsAt ?? null, turnEngineRunning: state.turnEngineRunning === undefined ? true : state.turnEngineRunning, scenarioTitle: state.scenarioTitle ?? '', + autorunUser: state.autorunUser ?? null, }); } if (operation === 'dashboard.getContextBundleDelta') { @@ -1370,6 +1372,62 @@ test('the private-message notice moves a mobile reader to the private section', expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(500); }); +test('main info fills the eighth slot with Ref-compatible autorun status and an accessible detail tooltip', async ({ + page, +}, testInfo) => { + const state: NavigationFixture = { + officerLevel: 5, + permission: 2, + nationLevel: 3, + stage: 0, + npcMode: 1, + autorunUser: { + limitMinutes: 1_440, + options: ['chief', 'battle', 'train', 'recruit', 'recruit_high', 'warp', 'develop'], + }, + generalMeCalls: 0, + operations: [], + }; + await installFixture(page, state); + await page.setViewportSize({ width: 1200, height: 900 }); + await waitForMain(page); + + const gameInfo = page.locator('.legacy-game-info'); + const status = page.locator('[data-main-autorun-status]'); + const tooltip = page.getByRole('tooltip', { + name: '내정, 순간이동, 모병, 훈련/사기진작, 출병, 사령턴, 24시간 유효', + }); + await expect(gameInfo.locator(':scope > *')).toHaveCount(8); + await expect(status).toHaveText(/기타 설정: 자율행동/u); + await expect(status).toHaveAttribute('tabindex', '0'); + await expect(tooltip).toBeHidden(); + + const desktopBefore = await gameInfo.boundingBox(); + await status.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText('내정, 순간이동, 모병, 훈련/사기진작, 출병, 사령턴, 24시간 유효'); + expect(await status.evaluate((element) => getComputedStyle(element).overflow)).toBe('visible'); + expect(await gameInfo.boundingBox()).toEqual(desktopBefore); + await page.screenshot({ path: testInfo.outputPath('main-autorun-hover-desktop-1200.png'), fullPage: false }); + + await page.setViewportSize({ width: 500, height: 900 }); + await status.focus(); + await expect(tooltip).toBeVisible(); + expect(await status.evaluate((element) => getComputedStyle(element).overflow)).toBe('visible'); + const mobileTooltip = await tooltip.boundingBox(); + expect(mobileTooltip?.x).toBeGreaterThanOrEqual(16); + expect(mobileTooltip ? mobileTooltip.x + mobileTooltip.width : Infinity).toBeLessThanOrEqual(484); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(500); + await page.screenshot({ path: testInfo.outputPath('main-autorun-focus-mobile-500.png'), fullPage: false }); + + state.autorunUser = null; + await page.reload(); + await waitForMain(page); + await expect(status).toHaveText('기타 설정: 표준'); + await expect(status).not.toHaveAttribute('tabindex', '0'); + await expect(page.getByRole('tooltip')).toHaveCount(0); +}); + test('desktop menus preserve ref columns, prefix-safe routes, and controlled dropdown behavior', async ({ page, }, testInfo) => { diff --git a/app/game-frontend/src/components/main/MainAutorunStatus.vue b/app/game-frontend/src/components/main/MainAutorunStatus.vue new file mode 100644 index 00000000..e7244994 --- /dev/null +++ b/app/game-frontend/src/components/main/MainAutorunStatus.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/app/game-frontend/src/utils/autorunUser.ts b/app/game-frontend/src/utils/autorunUser.ts new file mode 100644 index 00000000..aa2cc4cb --- /dev/null +++ b/app/game-frontend/src/utils/autorunUser.ts @@ -0,0 +1,25 @@ +export interface AutorunUserSummary { + limitMinutes: number; + options: string[]; +} + +export const formatAutorunUserDetail = (autorun: AutorunUserSummary): string => { + const enabled = new Set(autorun.options); + const labels: string[] = []; + if (enabled.has('develop')) labels.push('내정'); + if (enabled.has('warp')) labels.push('순간이동'); + if (enabled.has('recruit_high')) labels.push('모병'); + else if (enabled.has('recruit')) labels.push('징병'); + if (enabled.has('train')) labels.push('훈련/사기진작'); + if (enabled.has('battle')) labels.push('출병'); + if (enabled.has('chief')) labels.push('사령턴'); + + const limit = + autorun.limitMinutes >= 43_200 + ? '항상 유효' + : autorun.limitMinutes % 60 === 0 + ? `${autorun.limitMinutes / 60}시간 유효` + : `${autorun.limitMinutes}분 유효`; + labels.push(limit); + return labels.join(', '); +}; diff --git a/app/game-frontend/src/views/MainView.vue b/app/game-frontend/src/views/MainView.vue index 4548993e..a3a2406b 100644 --- a/app/game-frontend/src/views/MainView.vue +++ b/app/game-frontend/src/views/MainView.vue @@ -18,6 +18,7 @@ import MainGlobalMenu from '../components/main/MainGlobalMenu.vue'; import MainNationMenu from '../components/main/MainNationMenu.vue'; import MainMobileBottomBar from '../components/main/MainMobileBottomBar.vue'; import MainTurnControls from '../components/main/MainTurnControls.vue'; +import MainAutorunStatus from '../components/main/MainAutorunStatus.vue'; import { defaultGlobalNavigation, type MainNavigationEntry, @@ -257,6 +258,7 @@ watch( 국가: {{ lobbyInfo.nationCnt }} 사실/가상: {{ lobbyInfo.fictionMode }} {{ lobbyInfo.otherTextInfo || '진행 정보 없음' }} + @@ -728,11 +730,13 @@ button { } .legacy-game-info { + position: relative; + z-index: 100; display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); box-sizing: border-box; height: 18px; - overflow: hidden; + overflow: visible; border-top: 1px solid #666; background: #302016 var(--sammo-texture-walnut); color: #fff; @@ -747,6 +751,10 @@ button { white-space: nowrap; } +.legacy-game-info > .main-autorun-status { + overflow: visible; +} + .warning { color: #f5d08a; font-size: 0.85rem; diff --git a/app/game-frontend/test/autorunUser.test.ts b/app/game-frontend/test/autorunUser.test.ts new file mode 100644 index 00000000..62720d20 --- /dev/null +++ b/app/game-frontend/test/autorunUser.test.ts @@ -0,0 +1,18 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { formatAutorunUserDetail } from '../src/utils/autorunUser.ts'; + +void test('uses the Ref and Gateway autorun option order and lets recruit_high replace recruit', () => { + assert.equal( + formatAutorunUserDetail({ + limitMinutes: 1_440, + options: ['chief', 'recruit', 'battle', 'train', 'recruit_high', 'warp', 'develop'], + }), + '내정, 순간이동, 모병, 훈련/사기진작, 출병, 사령턴, 24시간 유효' + ); +}); + +void test('formats minute and always-valid autorun limits like Ref', () => { + assert.equal(formatAutorunUserDetail({ limitMinutes: 90, options: ['recruit'] }), '징병, 90분 유효'); + assert.equal(formatAutorunUserDetail({ limitMinutes: 43_200, options: ['develop'] }), '내정, 항상 유효'); +});