diff --git a/apps/web/src/styles/global.css b/apps/web/src/styles/global.css index 057101c..87a061d 100644 --- a/apps/web/src/styles/global.css +++ b/apps/web/src/styles/global.css @@ -1010,6 +1010,11 @@ button.header-user { .replay-readonly { display: contents; } +/* Prompts that waited on a live player (무르기·무승부 요청, 뱅! 선택 창…) can't be answered in a replay: don't let them cover the screen. */ +.replay-readonly .sheet-backdrop, +.replay-readonly .bg-modal-back { + display: none; +} .replay-readonly button, .replay-readonly [role='button'], .replay-readonly svg { diff --git a/e2e/replay.e2e.ts b/e2e/replay.e2e.ts index f070b28..9155dca 100644 --- a/e2e/replay.e2e.ts +++ b/e2e/replay.e2e.ts @@ -2,6 +2,7 @@ * E2E (docs/09 §8): game record replay. * 1) 오목 to the end with two browsers (PC + phone), open 다시 보기 from the result sheet and from 내 정보, * step through it (buttons, slider, keyboard, play) and check the board changes. + * A asks to take back a move once and B declines: B's replay must not show that request as a sheet over the screen. * 2) 원카드 (hidden hands): each player's replay shows only their own cards. * Run: E2E_PORT=4532 bun e2e/replay.e2e.ts (needs apps/web/dist built) */ @@ -102,6 +103,14 @@ try { for (let i = 0; i < 4; i++) { await a.getByText('내 차례예요').waitFor(); await tapCell(a, 5 + i, 7, false); + if (i === 3) { + // A asks to take the 4th stone back; B declines (the replay keeps that moment). + await waitStones(b, 7); + await a.getByRole('button', { name: /↩ 무르기/ }).click(); + await b.getByText('상대가 1수 무르기를 요청했어요').waitFor(); + await b.getByRole('button', { name: '거절' }).tap(); + await a.getByText('상대의 답을 기다리는 중…').waitFor({ state: 'detached' }); + } await b.getByText('내 차례예요').waitFor(); await tapCell(b, 5 + i, 3, true); } @@ -117,7 +126,7 @@ try { await a.waitForURL(/\/replay\/[0-9A-Z]{26}$/); replayUrl = a.url(); await a.getByText('게임을 시작했어요.').waitFor(); - expectEq((await count(a)).replace(/\s+/g, ' '), '시작 / 전체 9번', 'count at start'); + expectEq((await count(a)).replace(/\s+/g, ' '), '시작 / 전체 11번', 'count at start'); await waitStones(a, 0); await a.getByRole('button', { name: '다음' }).click(); await waitStones(a, 1); @@ -137,7 +146,7 @@ try { await step('A(PC): 슬라이더 · 방향키 · 재생/멈춤 · 판은 눌러도 그대로', async () => { await a.getByLabel('몇 번째 장면인지 고르기').fill('4'); await waitStones(a, 4); - expectEq((await count(a)).replace(/\s+/g, ' '), '4번째 / 전체 9번', 'count after slider'); + expectEq((await count(a)).replace(/\s+/g, ' '), '4번째 / 전체 11번', 'count after slider'); await a.locator('body').click({ position: { x: 5, y: 300 } }); await a.keyboard.press('ArrowRight'); await waitStones(a, 5); @@ -174,6 +183,14 @@ try { await waitStones(b, 2); await b.getByText('내가 움직였어요.').waitFor(); await b.screenshot({ path: join(SHOTS, 'replay-3-omok-phone.png') }); + // The moment A asked to take a move back: no "수락/거절" sheet covering the screen, the controls still work. + await b.getByLabel('몇 번째 장면인지 고르기').fill('8'); + await waitStones(b, 7); + expectEq((await count(b)).replace(/\s+/g, ' '), '8번째 / 전체 11번', 'count at the undo request'); + if (await b.getByRole('dialog').count()) throw new Error('the undo request sheet shows in the replay'); + await b.screenshot({ path: join(SHOTS, 'replay-3b-undo-request-phone.png') }); + await b.getByRole('button', { name: '다음' }).tap(); + expectEq((await count(b)).replace(/\s+/g, ' '), '9번째 / 전체 11번', 'count after 다음 at the undo request'); await b.getByRole('button', { name: '끝으로' }).tap(); await waitStones(b, 9); });