diff --git a/app/game-frontend/e2e/inGameMenus.spec.ts b/app/game-frontend/e2e/inGameMenus.spec.ts index 202496d2..c5c48941 100644 --- a/app/game-frontend/e2e/inGameMenus.spec.ts +++ b/app/game-frontend/e2e/inGameMenus.spec.ts @@ -1,6 +1,6 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { basename, resolve } from 'node:path'; -import { expect, test, type Page, type Route } from '@playwright/test'; +import { expect, test, type Locator, type Page, type Route } from '@playwright/test'; import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js'; const response = (data: unknown) => ({ result: { data } }); @@ -20,6 +20,23 @@ const persistParityArtifact = async (page: Page, name: string, geometry: unknown ]); }; +const readGeneralPanelImages = async (panel: Locator) => + panel.evaluate((element) => + [...element.querySelectorAll('.general-image')].map((image) => { + const rect = image.getBoundingClientRect(); + const style = getComputedStyle(image); + return { + label: image.getAttribute('aria-label'), + width: rect.width, + height: rect.height, + backgroundImage: style.backgroundImage, + backgroundSize: style.backgroundSize, + pointerEvents: style.pointerEvents, + userSelect: style.userSelect, + }; + }) + ); + type FixtureState = { permission: 'head' | 'member'; myset: number; @@ -545,9 +562,15 @@ test('재야 메인은 국가 틀과 성격·특기 표기명을 Chromium에 표 const generalCard = page.locator('.general-card'); await expect(generalCard).toContainText('성격안전'); - await expect(generalCard).toContainText('전투특기신산'); - await expect(generalCard).toContainText('내정특기상재'); + await expect(generalCard).toContainText('특기상재 / 신산'); await expect(generalCard).not.toContainText('che_'); + await expect(generalCard).toHaveAttribute('data-general-basic-card', ''); + const mainImages = await readGeneralPanelImages(generalCard); + expect(mainImages).toHaveLength(2); + expect(mainImages[0]).toMatchObject({ width: 64, height: 64, pointerEvents: 'none', userSelect: 'none' }); + expect(mainImages[0]?.backgroundImage).toContain('/icons/default.jpg'); + expect(mainImages[1]).toMatchObject({ width: 64, height: 64, pointerEvents: 'none', userSelect: 'none' }); + expect(mainImages[1]?.backgroundImage).toContain('/game/crewtype1.png'); const geometry = await nationCard.evaluate((element) => { const rect = element.getBoundingClientRect(); @@ -587,9 +610,9 @@ test('메인 카드의 국가·수도·관직·계급·병종은 Ref 출력명 await expect(nationCard).toContainText('국가 등급주자사'); const generalCard = page.locator('.general-card'); - await expect(generalCard.locator('.general-title')).toContainText('검증장수 · 간의대부'); + await expect(generalCard.locator('.general-title')).toContainText('검증장수 【 간의대부 | 건강 】'); await expect(generalCard).toContainText('병종보병'); - await expect(generalCard).toContainText('계급29품관'); + await expect(generalCard).toContainText('계급 29품관'); const cityCard = page.locator('.city-card'); await expect(cityCard.locator('.title')).toContainText('【중원 | 특】 업'); @@ -651,6 +674,14 @@ test('내 정보&설정 keeps the legacy 1000px/500px geometry and saves in plac await install(page, state); await page.setViewportSize({ width: 1000, height: 900 }); await page.goto('my-page'); + await expect(page.locator('.general-table')).toHaveAttribute('data-general-basic-card', ''); + const myPageImages = await readGeneralPanelImages(page.locator('.general-table')); + expect(myPageImages.map(({ width, height }) => ({ width, height }))).toEqual([ + { width: 64, height: 64 }, + { width: 64, height: 64 }, + ]); + expect(myPageImages[0]?.backgroundImage).toContain('/icons/default.jpg'); + expect(myPageImages[1]?.backgroundImage).toContain('/game/crewtype1.png'); await expect(page.locator('.legacy-general-details')).toContainText('계급 29품관'); await expect(page.locator('.legacy-general-details')).toContainText('병종 보병'); await expect(page.locator('.item-group')).toContainText('명마'); @@ -1109,10 +1140,15 @@ test('감찰부 keeps the selector interaction and shows the permission error pa await expect(page.locator('.selector-row select').nth(1)).toHaveValue('8'); await page.getByRole('button', { name: '다음 ▶' }).click(); await expect(page.locator('.selector-row select').nth(1)).toHaveValue('7'); - await expect(page.locator('.battle-general-name')).toContainText('검증장수 (간의대부)'); + await expect(page.locator('.battle-general-name')).toContainText('검증장수 【 간의대부 | 건강 】'); await expect(page.locator('.battle-general-extra')).toContainText('계급29품관'); - await expect(page.locator('.battle-general-extra')).toContainText('병종보병'); + await expect(page.locator('.battle-general-card')).toContainText('병종보병'); await expect(page.locator('.battle-general-card')).not.toContainText('che_'); + await expect(page.locator('.battle-general-card')).toHaveAttribute('data-general-basic-card', ''); + const battleImages = await readGeneralPanelImages(page.locator('.battle-general-card')); + expect(battleImages).toHaveLength(2); + expect(battleImages[0]?.backgroundImage).toContain('/icons/default.jpg'); + expect(battleImages[1]?.backgroundImage).toContain('/game/crewtype1.png'); await expect(page.locator('.battle-general-card [role="progressbar"]')).toHaveCount(14); await expect(page.locator('.battle-general-card [aria-label*="1,275,975 (EX+)"]')).toHaveCount(5); expect( diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 15c88ad3..1a63ce7a 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -514,7 +514,7 @@ const installRealtimeHarness = async (page: Page) => { const waitForMain = async (page: Page) => { await page.goto('./'); - await expect(page.getByRole('heading', { name: '전장 현황' })).toBeVisible(); + await expect(page.locator('.game-shell__title')).toBeVisible(); await expect(page.locator('.main-global-menu').first()).toBeVisible(); await expect(page.locator('.main-nation-menu')).toBeVisible(); await expect(page.locator('[data-navigation-id="npc-list"]')).toHaveCount(3); @@ -567,8 +567,24 @@ const persistArtifact = async (page: Page, name: string) => { globalPopup: describe('#mobile-global-menu'), nationPopup: describe('#mobile-nation-menu'), quickPopup: describe('#mobile-quick-menu'), + commandMenu: describe('.reserved-command-editor details[open] .menu-items'), + commandDividers: [...document.querySelectorAll('.reserved-command-editor details[open] .menu-divider')].map( + (element) => { + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + borderTop: style.borderTop, + margin: style.margin, + }; + } + ), }; }); + const commandMenu = page.locator('.reserved-command-editor details[open] .menu-items').first(); + if (await commandMenu.isVisible()) { + await commandMenu.screenshot({ path: resolve(target, `${name}-menu.png`) }); + } await Promise.all([ page.screenshot({ path: resolve(target, `${name}.png`), fullPage: true }), writeFile(resolve(target, `${name}.json`), `${JSON.stringify(geometry, null, 2)}\n`), @@ -928,6 +944,10 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn await tenthTurnButton.click(); const quickPicker = page.getByTestId('command-picker'); await expect(quickPicker).toBeVisible(); + await tenthTurnButton.click(); + await expect(quickPicker).toBeHidden(); + await tenthTurnButton.click(); + await expect(quickPicker).toBeVisible(); const quickPickerAlignment = await quickPicker.evaluate((element) => { const row = element .closest('.reserved-command-editor') @@ -969,6 +989,30 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn expect(advancedControlGeometry.rangeTop).toBe(advancedControlGeometry.recentTop); expect(advancedControlGeometry.advancedTop).toBeGreaterThan(advancedControlGeometry.rangeTop); expect(advancedControlGeometry.advancedBottom).toBeLessThanOrEqual(advancedControlGeometry.queueTop); + const rangeMenu = page.locator('[data-main-target="commands"] .range-menu'); + await rangeMenu.locator('summary').click(); + const rangeDividers = rangeMenu.locator('.menu-divider'); + await expect(rangeDividers).toHaveCount(1); + await expect(rangeDividers.first()).toBeVisible(); + expect(await rangeDividers.first().evaluate((element) => getComputedStyle(element).borderTop)).toBe( + '1px solid rgb(68, 68, 68)' + ); + await persistArtifact(page, `${basePath.slice(1)}-command-range-divider-desktop-1200`); + await rangeMenu.evaluate((element) => ((element as HTMLDetailsElement).open = false)); + await expect(rangeMenu).not.toHaveAttribute('open', ''); + + const selectedMenu = page.locator('[data-main-target="commands"] .selected-menu'); + await selectedMenu.locator('summary').click(); + const selectedMenuDividers = selectedMenu.locator('.menu-divider'); + await expect(selectedMenuDividers).toHaveCount(3); + await expect(selectedMenuDividers.first()).toBeVisible(); + expect(await selectedMenuDividers.first().evaluate((element) => getComputedStyle(element).borderTop)).toBe( + '1px solid rgb(68, 68, 68)' + ); + await persistArtifact(page, `${basePath.slice(1)}-command-selected-dividers-desktop-1200`); + await selectedMenu.evaluate((element) => ((element as HTMLDetailsElement).open = false)); + await expect(selectedMenu).not.toHaveAttribute('open', ''); + await page.locator('[data-main-target="commands"] .select-command').click(); const picker = page.getByTestId('command-picker'); await expect(picker).toBeVisible(); @@ -1157,6 +1201,11 @@ test('main cards and command input stay inside their Ref-sized grid slots', asyn expect(mobileGeometry.controlBoxes).toHaveLength(3); expect(new Set(mobileGeometry.controlBoxes.map(({ y }) => y)).size).toBe(1); await expect(page.locator('[data-main-target="commands"] .edit-column button')).toHaveCount(30); + const mobileTurnButton = page.getByRole('button', { name: '10턴 명령 입력' }); + await mobileTurnButton.click(); + await expect(page.getByTestId('command-picker')).toBeVisible(); + await mobileTurnButton.click(); + await expect(page.getByTestId('command-picker')).toBeHidden(); await captureProgress('mobile-500'); }); diff --git a/app/game-frontend/src/components/command/ReservedCommandEditor.vue b/app/game-frontend/src/components/command/ReservedCommandEditor.vue index 44f462de..a88229b1 100644 --- a/app/game-frontend/src/components/command/ReservedCommandEditor.vue +++ b/app/game-frontend/src/components/command/ReservedCommandEditor.vue @@ -149,6 +149,14 @@ const closePicker = () => { quickTarget.value = null; selectedCommand.value = null; }; +const togglePicker = (turnIndex?: number) => { + const target = turnIndex ?? null; + if (pickerOpen.value && quickTarget.value === target) { + closePicker(); + return; + } + openPicker(turnIndex); +}; const selectCommand = (commandKey: string) => { const command = props.commandTable?.[props.scope] .flatMap((group) => group.values) @@ -305,6 +313,7 @@ const clickOutsideMenu = (event: Event) => { > 짝수턴 +