웹: PC 중심 화면으로 재구성
- 공통 상단 메뉴(사이트 이름·홈·게임 목록·관리자·어디서나 코드 입력·내 정보) - 홈: 방 만들기 + 코드 입력 카드, 게임 격자, 지금 열린 공개 방 목록(/api/public-rooms) - 대기실: 두 칸(초대·설정 바로 수정·규칙 요약 / 자리·시작 버튼·채팅 항상 보임) - 게임 화면: 판(화면 높이에 맞춤, 좌표 표시) + 오른쪽 패널(상태·선수·시계·버튼·기보·채팅) - 오목: 마우스 올리면 미리 보기 돌, PC는 한 번 클릭으로 착수(두 번 눌러 확정은 터치 기기 기본값, 기기별 설정), GameUI에 Side 패널 추가 - 채팅: PC는 패널로 항상 표시(Enter로 바로 입력), 휴대폰은 시트 - 관리자: 왼쪽 사이드바, 사용자 표, 규칙 입력 두 칸 격자, 저장 바 고정 - 좁은 화면은 같은 화면을 한 칸으로 접어서 지원(방 상단 줄 줄바꿈 수정) - E2E: PC 1440×900 기준(방장 PC, 친구 휴대폰), 관리자 E2E도 PC Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,7 @@ const step = async (name: string, fn: () => Promise<void>) => {
|
||||
};
|
||||
|
||||
try {
|
||||
const adminCtx = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true, locale: 'ko-KR' });
|
||||
const adminCtx = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'ko-KR' });
|
||||
// Fake Discord: Playwright does not route redirect targets, so intercept our own /start, let the server
|
||||
// set its state cookie, and bounce straight back to the callback with the superadmin's ID as the code.
|
||||
await adminCtx.route(`${ORIGIN}/api/auth/discord/start*`, async (route) => {
|
||||
@@ -70,7 +70,7 @@ try {
|
||||
});
|
||||
|
||||
await step('게임 설정: 오목 렌주룰 고정 + 한 수 45초 저장', async () => {
|
||||
await a.getByRole('link', { name: '게임' }).click();
|
||||
await a.locator('.admin-side').getByRole('link', { name: '게임', exact: true }).click();
|
||||
await a.getByRole('button', { name: /오목/ }).click();
|
||||
await a.getByText('기본 규칙').waitFor();
|
||||
const rule = a.locator('div.stack:has(> div.row > strong:text-is("규칙"))').first();
|
||||
@@ -78,13 +78,13 @@ try {
|
||||
await rule.getByLabel(/방장 변경 금지/).check();
|
||||
const per = a.locator('div.stack:has(> div.row > strong:text-is("한 수 제한(초)"))').first();
|
||||
await per.locator('input[type=number]').fill('45');
|
||||
await a.screenshot({ path: join(SHOTS, 'admin-2-game-editor.png'), fullPage: true });
|
||||
await a.screenshot({ path: join(SHOTS, 'admin-2-game-editor.png') });
|
||||
await a.getByRole('button', { name: '저장', exact: true }).click();
|
||||
await a.getByText('저장했어요.').waitFor();
|
||||
});
|
||||
|
||||
await step('관리자 탭: 디스코드 ID로 어드민 추가', async () => {
|
||||
await a.getByRole('link', { name: '관리자' }).click();
|
||||
await a.locator('.admin-side').getByRole('link', { name: '관리자', exact: true }).click();
|
||||
await a.getByLabel('디스코드 ID(숫자)').fill('222222222222222222');
|
||||
await a.getByLabel('메모(선택)').fill('친구');
|
||||
await a.getByRole('button', { name: '추가', exact: true }).click();
|
||||
@@ -93,7 +93,7 @@ try {
|
||||
});
|
||||
|
||||
await step('기록 탭에 변경 내역', async () => {
|
||||
await a.getByRole('link', { name: '기록' }).click();
|
||||
await a.locator('.admin-side').getByRole('link', { name: '기록', exact: true }).click();
|
||||
await a.getByText('어드민 추가').waitFor();
|
||||
await a.getByText('게임 설정 변경').waitFor();
|
||||
});
|
||||
|
||||
@@ -34,19 +34,29 @@ const step = async (name: string, fn: () => Promise<void>) => {
|
||||
}
|
||||
};
|
||||
|
||||
async function tapCell(page: Page, x: number, y: number, n = 15) {
|
||||
const PAD = 0.9; // board viewBox padding (coordinate labels)
|
||||
async function cellPoint(page: Page, x: number, y: number, n = 15) {
|
||||
const box = (await page.locator('svg[role="grid"]').boundingBox())!;
|
||||
const span = n - 1 + 1.2;
|
||||
const px = box.x + ((x + 0.6) / span) * box.width;
|
||||
const py = box.y + ((y + 0.6) / span) * box.height;
|
||||
await page.mouse.click(px, py);
|
||||
await page.waitForTimeout(60);
|
||||
await page.mouse.click(px, py); // second tap confirms (tap-confirm mode)
|
||||
const span = n - 1 + 2 * PAD;
|
||||
return { px: box.x + ((x + PAD) / span) * box.width, py: box.y + ((y + PAD) / span) * box.height };
|
||||
}
|
||||
/** PC: one mouse click places. Phone: tap shows a preview, second tap confirms. */
|
||||
async function tapCell(page: Page, x: number, y: number, touch: boolean) {
|
||||
const { px, py } = await cellPoint(page, x, y);
|
||||
if (touch) {
|
||||
await page.touchscreen.tap(px, py);
|
||||
await page.waitForTimeout(60);
|
||||
await page.touchscreen.tap(px, py);
|
||||
} else {
|
||||
await page.mouse.move(px, py);
|
||||
await page.mouse.click(px, py);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const ctxA = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true, locale: 'ko-KR' });
|
||||
const ctxB = await browser.newContext({ viewport: { width: 1280, height: 860 }, locale: 'ko-KR' });
|
||||
// A = PC (primary target, docs/07 §3), B = phone.
|
||||
const ctxA = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'ko-KR' });
|
||||
const ctxB = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, isMobile: true, hasTouch: true, locale: 'ko-KR' });
|
||||
const a = await ctxA.newPage();
|
||||
const b = await ctxB.newPage();
|
||||
const errors: string[] = [];
|
||||
@@ -55,13 +65,14 @@ try {
|
||||
let code = '';
|
||||
await step('A: 홈 → 방 만들기 → 닉네임 → 대기실', async () => {
|
||||
await a.goto(ORIGIN + '/');
|
||||
await a.getByText('게임 고르기').waitFor();
|
||||
await a.screenshot({ path: join(SHOTS, '0-home-pc.png') });
|
||||
await a.getByRole('button', { name: '+ 방 만들기' }).click();
|
||||
await a.getByLabel('내 이름(닉네임)').fill('호랑이');
|
||||
await a.getByRole('button', { name: '이 이름으로 시작하기' }).click();
|
||||
await a.waitForURL(/\/r\/\d{6}$/);
|
||||
code = a.url().slice(-6);
|
||||
await a.getByText('친구에게 이 코드를 알려 주세요').waitFor();
|
||||
await a.screenshot({ path: join(SHOTS, '1-lobby-mobile.png') });
|
||||
});
|
||||
|
||||
await step('B: 링크로 입장 → 닉네임 → 자동 착석', async () => {
|
||||
@@ -69,16 +80,18 @@ try {
|
||||
await b.getByText('방에 초대받았어요').waitFor();
|
||||
await b.getByLabel('내 이름(닉네임)').fill('고양이');
|
||||
await b.getByRole('button', { name: '이 이름으로 입장하기' }).click();
|
||||
await b.getByText('고양이').first().waitFor();
|
||||
await a.getByText('고양이').waitFor();
|
||||
await b.getByLabel('자리').getByText('고양이').waitFor();
|
||||
await a.getByLabel('자리').getByText('고양이').waitFor();
|
||||
await b.screenshot({ path: join(SHOTS, '5-lobby-phone.png') });
|
||||
});
|
||||
|
||||
await step('A: 설정(방장 흑, 자유룰) → 시작', async () => {
|
||||
await a.getByRole('button', { name: '설정 바꾸기' }).click();
|
||||
// PC: the host edits settings inline in the lobby.
|
||||
await a.getByRole('button', { name: '자유룰' }).click();
|
||||
await a.getByRole('button', { name: '방장이 흑' }).click();
|
||||
await a.getByRole('button', { name: '저장' }).click();
|
||||
await a.getByRole('button', { name: '설정 저장' }).click();
|
||||
await a.getByText('자유룰 · 15줄').waitFor();
|
||||
await a.screenshot({ path: join(SHOTS, '1-lobby-pc.png') });
|
||||
await a.getByRole('button', { name: '▶ 게임 시작' }).click();
|
||||
await a.getByRole('button', { name: '시작하기' }).click();
|
||||
await a.getByText('내 차례예요').waitFor();
|
||||
@@ -87,11 +100,19 @@ try {
|
||||
|
||||
await step('대국: 흑 가로 5목', async () => {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await tapCell(a, 5 + i, 7);
|
||||
await tapCell(a, 5 + i, 7, false);
|
||||
await b.getByText('내 차례예요').waitFor();
|
||||
await tapCell(b, 5 + i, 3);
|
||||
await tapCell(b, 5 + i, 3, true);
|
||||
await a.getByText('내 차례예요').waitFor();
|
||||
if (i === 1) await a.screenshot({ path: join(SHOTS, '2-game-mobile.png') });
|
||||
if (i === 1) {
|
||||
await a.locator('input[aria-label="메시지"]').fill('잘 부탁해요!');
|
||||
await a.keyboard.press('Enter');
|
||||
await b.waitForTimeout(100);
|
||||
const { px, py } = await cellPoint(a, 7, 9);
|
||||
await a.mouse.move(px, py); // hover preview stone
|
||||
await a.screenshot({ path: join(SHOTS, '2-game-pc.png') });
|
||||
await b.screenshot({ path: join(SHOTS, '6-game-phone.png') });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,12 +124,12 @@ try {
|
||||
});
|
||||
|
||||
await step('흑 승리 → 결과 화면', async () => {
|
||||
await tapCell(a, 9, 7);
|
||||
await tapCell(a, 9, 7, false);
|
||||
await a.getByText('🎉 이겼어요!').waitFor();
|
||||
await b.getByText('아쉽게 졌어요').waitFor();
|
||||
await a.waitForTimeout(500); // let the sheet animation finish
|
||||
await a.screenshot({ path: join(SHOTS, '3-result-mobile.png') });
|
||||
await b.screenshot({ path: join(SHOTS, '4-result-desktop.png') });
|
||||
await a.screenshot({ path: join(SHOTS, '3-result-pc.png') });
|
||||
await b.screenshot({ path: join(SHOTS, '4-result-phone.png') });
|
||||
});
|
||||
|
||||
await step('한 판 더 → 새 게임 시작', async () => {
|
||||
|
||||
Reference in New Issue
Block a user