From d50cff86402beccb5b1eafc844c667e71e47d64d Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 14 Sep 2026 13:57:43 +0000 Subject: [PATCH] =?UTF-8?q?=EA=B0=9C=EC=9D=B8=C2=B7=EC=82=AC=EB=A0=B9?= =?UTF-8?q?=ED=84=B4=20=EB=8C=80=EC=83=81=20=EC=84=A0=ED=83=9D=EC=97=90=20?= =?UTF-8?q?=EC=B4=88=EC=84=B1=20=EA=B2=80=EC=83=89=EA=B3=BC=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=A0=84=ED=99=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../e2e/commandArguments.spec.ts | 111 ++++++++++++++++ .../components/main/CommandArgumentForm.vue | 122 +++++++++++++++++- .../src/utils/commandTargetSearch.ts | 47 +++++++ .../test/commandTargetSearch.test.ts | 21 +++ 4 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 app/game-frontend/src/utils/commandTargetSearch.ts create mode 100644 app/game-frontend/test/commandTargetSearch.test.ts diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index d9c410a2..e85d8d8b 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -3601,3 +3601,114 @@ test('NPC prefixes stay single in chief command cards', async ({ page }, testInf await page.screenshot({ path: testInfo.outputPath(`npc-chief-prefix-${width}.png`), fullPage: true }); } }); + +for (const width of [1200, 390]) { + test.describe(`target search viewport ${width}`, () => { + test.use({ isMobile: width < 500, hasTouch: width < 500 }); + test(`target search preserves selection and searches Korean initials in both editors at ${width}px`, async ({ + page, + }, testInfo) => { + const requests = await install(page); + await page.setViewportSize({ width, height: 900 }); + await page.goto(gamePath('/')); + const initialScale = await page.evaluate(() => visualViewport?.scale); + await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click(); + let picker = page.getByTestId('command-picker'); + await picker.getByRole('button', { name: /화계/ }).click(); + let form = picker.getByTestId('command-argument-form'); + const toggle = form.getByRole('button', { name: '검색 꺼짐', exact: true }); + await expect(form.locator('input[type=search]')).toHaveCount(0); + await toggle.click(); + let input = form.locator('input[type=search]'); + await expect(input).not.toBeFocused(); + await expect(input).toHaveCSS('font-size', '16px'); + await input.fill('ㅎㅊ'); + let results = form.getByTestId('city-target-list'); + await expect(results.locator('button')).toHaveCount(1); + await expect(results).toContainText('허창'); + await results.getByRole('button').click(); + await expect(form.locator('#command-arg-destCityId')).toHaveValue('2'); + await input.fill('없는도시'); + await expect(results).toContainText('검색 결과가 없습니다.'); + await expect(form.locator('#command-arg-destCityId')).toHaveValue('2'); + await form.getByRole('button', { name: '지우기', exact: true }).click(); + await expect(results.locator('button')).toHaveCount(3); + await input.fill('GC'); + await expect(results.locator('button')).toHaveCount(1); + await input.press('Enter'); + await expect(input).not.toBeFocused(); + await expect(picker).toBeVisible(); + await form.getByRole('button', { name: '검색 켜짐', exact: true }).click(); + await expect(input).toHaveCount(0); + await expect(form.locator('#command-arg-destCityId')).toHaveValue('2'); + await picker.getByRole('button', { name: '입력', exact: true }).click(); + await expect.poll(() => JSON.stringify(requests)).toContain('"destCityId":2'); + + await page.goto(gamePath('/chief-center')); + await page.getByRole('button', { name: '1턴 명령 입력', exact: true }).click(); + picker = page.getByTestId('command-picker'); + await picker.getByRole('button', { name: /^(?:국가:)?인사$/, exact: true }).click(); + await picker.getByRole('button', { name: /발령/ }).click(); + form = picker.getByTestId('command-argument-form'); + await form.getByRole('button', { name: '검색 꺼짐', exact: true }).click(); + const generalSearch = form.locator('#command-search-destGeneralId'); + const citySearch = form.locator('#command-search-destCityId'); + await generalSearch.fill('ㅊㄹㄷ'); + await expect(form.getByTestId('general-target-list')).toContainText('청룡대'); + await generalSearch.fill('ㄱㅇ'); + await citySearch.fill('ㅎㅊ'); + await expect(form.getByTestId('general-target-list').locator('button')).toHaveCount(1); + await expect(form.getByTestId('city-target-list').locator('button')).toHaveCount(1); + await form.getByTestId('general-target-list').getByRole('button').click(); + await form.getByTestId('city-target-list').getByRole('button').click(); + await expect(form.locator('#command-arg-destGeneralId')).toHaveValue('2'); + await expect(form.locator('#command-arg-destCityId')).toHaveValue('2'); + await page.evaluate(() => document.fonts.ready); + const geometry = await form.evaluate((element) => ({ + html: element.outerHTML, + inputs: [...element.querySelectorAll('input[type=search]')].map((input) => { + const rect = input.getBoundingClientRect(); + return { + x: rect.x, + right: rect.right, + width: rect.width, + fontSize: getComputedStyle(input).fontSize, + }; + }), + viewport: { width: innerWidth, scale: visualViewport?.scale }, + })); + expect(geometry.inputs.every((input) => input.x >= 0 && input.right <= geometry.viewport.width)).toBe(true); + expect(geometry.viewport.scale).toBe(initialScale); + await writeFile(testInfo.outputPath(`search-${width}.json`), JSON.stringify(geometry, null, 2)); + await form.screenshot({ path: testInfo.outputPath(`search-${width}.png`) }); + await picker.getByRole('button', { name: '입력', exact: true }).click(); + await expect.poll(() => JSON.stringify(requests)).toContain('"destGeneralId":2,"destCityId":2'); + await page.reload(); + await page.getByRole('button', { name: '2턴 명령 입력', exact: true }).click(); + picker = page.getByTestId('command-picker'); + await picker.getByRole('button', { name: /^(?:국가:)?외교$/, exact: true }).click(); + await picker.getByRole('button', { name: /선전포고/ }).click(); + form = picker.getByTestId('command-argument-form'); + await expect(form.getByRole('button', { name: '검색 켜짐', exact: true })).toHaveAttribute( + 'aria-pressed', + 'true' + ); + input = form.locator('input[type=search]'); + await expect(input).toHaveValue(''); + await expect(input).not.toBeFocused(); + await input.fill('ㅂㄱㅊㄱ'); + results = form.getByTestId('nation-target-list'); + await expect(results.locator('button')).toHaveCount(1); + await expect(results).toContainText('현재 불가'); + await results.getByRole('button').click(); + await expect(form.locator('#command-arg-destNationId')).toHaveValue('3'); + await input.fill('적국'); + await expect(results.locator('button')).toHaveCount(1); + await results.getByRole('button').click(); + await expect(form.locator('#command-arg-destNationId')).toHaveValue('2'); + await input.press('Escape'); + await expect(input).toHaveValue(''); + await expect(picker).toBeVisible(); + }); + }); +} diff --git a/app/game-frontend/src/components/main/CommandArgumentForm.vue b/app/game-frontend/src/components/main/CommandArgumentForm.vue index 34bfec2b..95b0eb82 100644 --- a/app/game-frontend/src/components/main/CommandArgumentForm.vue +++ b/app/game-frontend/src/components/main/CommandArgumentForm.vue @@ -1,5 +1,7 @@