다시보기: 대국 중 답을 받던 창(무르기·무승부 요청, 뱅! 선택 창)이 화면을 덮지 않게

- 다시보기는 게임의 Side/Board를 읽기 전용으로 그리는데, 그 장면의 요청 창(Sheet, 뱅! 모달)도 함께 떠서
  화면 전체를 덮고 클릭을 막았음(읽기 전용 영역이라 수락/거절도 안 됨). .replay-readonly 안의 창은 숨김
- e2e(replay): 오목 중 무르기 요청→거절을 넣고, 폰 다시보기에서 그 장면에 창이 없고 [다음]이 되는지 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-08 10:23:14 +09:00
parent beaee4675d
commit 4e4c195c79
2 changed files with 24 additions and 2 deletions

View File

@@ -1010,6 +1010,11 @@ button.header-user {
.replay-readonly { .replay-readonly {
display: contents; 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 button,
.replay-readonly [role='button'], .replay-readonly [role='button'],
.replay-readonly svg { .replay-readonly svg {

View File

@@ -2,6 +2,7 @@
* E2E (docs/09 §8): game record replay. * E2E (docs/09 §8): game record replay.
* 1) 오목 to the end with two browsers (PC + phone), open 다시 보기 from the result sheet and from 내 정보, * 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. * 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. * 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) * 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++) { for (let i = 0; i < 4; i++) {
await a.getByText('내 차례예요').waitFor(); await a.getByText('내 차례예요').waitFor();
await tapCell(a, 5 + i, 7, false); 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 b.getByText('내 차례예요').waitFor();
await tapCell(b, 5 + i, 3, true); await tapCell(b, 5 + i, 3, true);
} }
@@ -117,7 +126,7 @@ try {
await a.waitForURL(/\/replay\/[0-9A-Z]{26}$/); await a.waitForURL(/\/replay\/[0-9A-Z]{26}$/);
replayUrl = a.url(); replayUrl = a.url();
await a.getByText('게임을 시작했어요.').waitFor(); 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 waitStones(a, 0);
await a.getByRole('button', { name: '다음' }).click(); await a.getByRole('button', { name: '다음' }).click();
await waitStones(a, 1); await waitStones(a, 1);
@@ -137,7 +146,7 @@ try {
await step('A(PC): 슬라이더 · 방향키 · 재생/멈춤 · 판은 눌러도 그대로', async () => { await step('A(PC): 슬라이더 · 방향키 · 재생/멈춤 · 판은 눌러도 그대로', async () => {
await a.getByLabel('몇 번째 장면인지 고르기').fill('4'); await a.getByLabel('몇 번째 장면인지 고르기').fill('4');
await waitStones(a, 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.locator('body').click({ position: { x: 5, y: 300 } });
await a.keyboard.press('ArrowRight'); await a.keyboard.press('ArrowRight');
await waitStones(a, 5); await waitStones(a, 5);
@@ -174,6 +183,14 @@ try {
await waitStones(b, 2); await waitStones(b, 2);
await b.getByText('내가 움직였어요.').waitFor(); await b.getByText('내가 움직였어요.').waitFor();
await b.screenshot({ path: join(SHOTS, 'replay-3-omok-phone.png') }); 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 b.getByRole('button', { name: '끝으로' }).tap();
await waitStones(b, 9); await waitStones(b, 9);
}); });