diff --git a/app/game-frontend/e2e/commandArguments.spec.ts b/app/game-frontend/e2e/commandArguments.spec.ts index 83434adb..ba38de0f 100644 --- a/app/game-frontend/e2e/commandArguments.spec.ts +++ b/app/game-frontend/e2e/commandArguments.spec.ts @@ -987,6 +987,20 @@ const install = async ( if (name === 'messages.getContacts') return response({ nation: [] }); if (name === 'board.getAccess') return response({ canMeeting: false, canSecret: false }); if (name === 'tournament.getState') return response({ stage: 0 }); + if (name === 'turns.reserved.shiftGeneral') { + const input = (body as Record)[ + String(names.indexOf(name)) + ]; + expect(route.request().method()).toBe('POST'); + expect(input.generalId).toBe(generalId); + expect(input.expectedRevision).toBe(generalRevision); + expect( + Number.isInteger(input.amount) && Math.abs(input.amount) >= 1 && Math.abs(input.amount) <= 6 + ).toBe(true); + requests.push(input); + generalRevision += 1; + return response({ ok: true, revision: generalRevision, turns: generalTurns, autorunLimit: 2403 }); + } if (name === 'turns.reserved.setGeneralBulk') { requests.push(body); if (rejectGeneral) return errorResponse(name, '대상 도시를 선택할 수 없습니다.'); @@ -1019,6 +1033,80 @@ const install = async ( return requests; }; +for (const width of [1200, 500]) { + test(`split turn shift applies one turn or the chosen count in both modes at ${width}px`, async ({ + page, + }, testInfo) => { + const requests = await install(page); + const shifts = () => + requests.filter((entry) => typeof entry === 'object' && entry !== null && 'amount' in entry); + await page.setViewportSize({ width, height: 900 }); + await page.goto('/'); + const editor = page.locator('[data-command-scope="general"]:visible'); + const evidence: unknown[] = []; + for (const advanced of [false, true]) { + if (advanced) await editor.getByRole('button', { name: '고급 모드', exact: true }).click(); + for (const [index, label] of ['당기기', '미루기'].entries()) { + const direction = index === 0 ? -1 : 1; + const control = editor.locator('.bottom-shift-control').nth(index); + const main = control.getByRole('button', { name: label, exact: true }); + const menu = control.locator('details'); + const toggle = menu.locator('summary'); + const before = shifts().length; + await main.click(); + await expect.poll(() => shifts().length).toBe(before + 1); + expect(shifts().at(-1)).toMatchObject({ amount: direction }); + await expect(menu).not.toHaveAttribute('open'); + await toggle.focus(); + await page.keyboard.press('Enter'); + await expect(menu).toHaveAttribute('open'); + expect(shifts().length).toBe(before + 1); + await expect(menu.locator('.menu-items > button')).toHaveText([ + '1턴', + '2턴', + '3턴', + '4턴', + '5턴', + '6턴', + ]); + const geometry = await control.evaluate((element) => { + const main = element.querySelector('button')!; + const toggle = element.querySelector('summary')!; + const menu = element.querySelector('.menu-items')!; + return { + main: main.getBoundingClientRect().toJSON(), + toggle: toggle.getBoundingClientRect().toJSON(), + menu: menu.getBoundingClientRect().toJSON(), + style: { radius: getComputedStyle(main).borderRadius, font: getComputedStyle(main).fontFamily }, + overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth, + }; + }); + expect(geometry.main.right).toBeCloseTo(geometry.toggle.left, 1); + expect(geometry.menu.right).toBeLessThanOrEqual(width); + expect(geometry.menu.left).toBeGreaterThanOrEqual(0); + expect(geometry.overflow).toBeLessThanOrEqual(0); + evidence.push({ advanced, label, geometry }); + await page.screenshot({ + path: testInfo.outputPath(`split-${advanced}-${index}-${width}.png`), + fullPage: true, + }); + await menu.getByRole('button', { name: '6턴', exact: true }).click(); + await expect.poll(() => shifts().length).toBe(before + 2); + expect(shifts().at(-1)).toMatchObject({ amount: direction * 6 }); + await expect(menu).not.toHaveAttribute('open'); + await main.click(); + await expect.poll(() => shifts().length).toBe(before + 3); + expect(shifts().at(-1)).toMatchObject({ amount: direction }); + } + } + await writeFile(testInfo.outputPath('split-evidence.json'), JSON.stringify(evidence, null, 2)); + await writeFile( + testInfo.outputPath('split-editor.html'), + await editor.evaluate((element) => element.outerHTML) + ); + }); +} + test('offers 12 repeat turns and six shift turns for general turns while keeping the chief range', async ({ page }) => { await install(page); await page.setViewportSize({ width: 1200, height: 900 }); diff --git a/app/game-frontend/e2e/mainNavigation.spec.ts b/app/game-frontend/e2e/mainNavigation.spec.ts index 73edc0d5..c1f4eb4b 100644 --- a/app/game-frontend/e2e/mainNavigation.spec.ts +++ b/app/game-frontend/e2e/mainNavigation.spec.ts @@ -4393,10 +4393,12 @@ test('all main Lumen button families share the rounded pressed geometry', async [ '당기기', page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '당기기' }), + { radius: '5.25px 0px 0px 5.25px' }, ], [ '미루기', page.locator('[data-main-target="commands"] .bottom-actions').getByRole('button', { name: '미루기' }), + { radius: '5.25px 0px 0px 5.25px' }, ], [ '펼치기', @@ -4457,7 +4459,7 @@ test('all main Lumen button families share the rounded pressed geometry', async filter: 'none', }); if (label === '당기기' || label === '미루기') { - expect(base.width, `${label} fills its menu column`).toBeCloseTo(base.parentWidth, 2); + expect(base.width, `${label} leaves room for the split button`).toBeCloseTo(base.parentWidth - 28, 2); } await control.focus(); @@ -4702,7 +4704,7 @@ test('mobile main Lumen button families keep the same state geometry without ove await expect(control).toHaveClass(/legacy-button/u); await expect(control).toHaveCSS( 'border-radius', - index === 7 ? '0px 5.25px 5.25px 0px' : index === 8 ? '5.25px 0px 0px 5.25px' : '5.25px' + index === 7 ? '0px 5.25px 5.25px 0px' : [4, 5, 8].includes(index) ? '5.25px 0px 0px 5.25px' : '5.25px' ); await expect(control).toHaveCSS('border-bottom-width', '4px'); } diff --git a/app/game-frontend/src/components/command/ReservedCommandEditor.vue b/app/game-frontend/src/components/command/ReservedCommandEditor.vue index 7f4529c5..43ea8848 100644 --- a/app/game-frontend/src/components/command/ReservedCommandEditor.vue +++ b/app/game-frontend/src/components/command/ReservedCommandEditor.vue @@ -701,38 +701,38 @@ const clickOutsideMenu = (event: Event) => {
-
- 당기기 - -
-
- 미루기 - -
+ + + + +
@@ -1098,8 +1098,29 @@ const clickOutsideMenu = (event: Event) => { gap: 4px; padding-top: 3px; } +.bottom-shift-control { + display: flex; + min-width: 0; +} +.shift-main { + flex: 1; + min-width: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.bottom-shift-menu { + flex: 0 0 28px; +} .bottom-shift-menu > summary { width: 100%; + padding-inline: 0; + border-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.bottom-shift-menu .menu-items { + left: auto; + right: 0; } .command-picker { position: absolute;