/** * Holdem — 复现用户报告问题的 E2E(联机,真实 UI + server + engine): * * 1. All-in 后没有摊牌比大小 / 亮牌 * 2. 补码(rebuy)牌局结束后无法点击、房主无法重置 * * 场景:2 人房,起始筹码 30(盲注 10/20),一手 all-in 打到一方破产。 */ import { test, expect, type Browser, type Page, type BrowserContext } from '@playwright/test'; interface Seat { ctx: BrowserContext; page: Page; pid: string; } async function authUser(suffix: string): Promise<{ token: string; username: string }> { const uname = `hk${suffix}${Math.floor(Math.random() * 1e6)}`.slice(0, 18); const pwd = 'pw123456'; const base = process.env.TTS_API_BASE ?? 'http://localhost:8000'; const reg = await fetch(`${base}/api/auth/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: uname, password: pwd }), }); if (reg.ok) { const j = (await reg.json()) as { token: string; username: string }; return { token: j.token, username: j.username }; } const login = await fetch(`${base}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: uname, password: pwd }), }); const j = (await login.json()) as { token: string; username: string }; return { token: j.token, username: j.username }; } async function newSeat(browser: Browser, pid: string, auth: { token: string; username: string }): Promise { const ctx = await browser.newContext(); const page = await ctx.newPage(); await page.addInitScript( ([token, username]) => { localStorage.setItem('tts-like.token', token); localStorage.setItem('tts-like.username', username); }, [auth.token, auth.username] as [string, string], ); await page.goto('/'); await page.getByTestId('game-select').selectOption('holdem'); await page.getByTestId('mode-online').click(); await expect(page.getByTestId('lobby-room-list')).toBeVisible({ timeout: 15_000 }); return { ctx, page, pid }; } test.describe('Holdem — all-in + 破产复现', () => { test('all-in 摊牌 → 亮牌可见 → 破产者补码 → 房主重置 → 再开局', async ({ browser }) => { const auth0 = await authUser('p0'); const auth1 = await authUser('p1'); const p0 = await newSeat(browser, '0', auth0); const p1 = await newSeat(browser, '1', auth1); // P0 创建 2 座房间,起始筹码 30 await p0.page.getByTestId('lobby-create-seats').selectOption('2'); await p0.page.getByTestId('lobby-create-startingStack').fill('30'); await p0.page.getByTestId('lobby-create-room').click(); await expect(p0.page.getByTestId('holdem-board')).toBeVisible({ timeout: 15_000 }); // 读 P0 "在房间" 显示的 matchID(避免误加入上一轮遗留的旧房间) const roomText = await p0.page.getByTestId('lobby-current-room').innerText(); const mid = roomText.match(/在房间\s+(\S+)\s+作为/)?.[1] ?? ''; // P1 加入 P0 那个房间 await expect(async () => { await p1.page.getByTestId('lobby-refresh').click(); const joinBtn = p1.page.getByTestId(`lobby-join-1-${mid}`); expect(await joinBtn.count()).toBeGreaterThan(0); }).toPass({ timeout: 15_000 }); await p1.page.getByTestId(`lobby-join-1-${mid}`).click(); await expect(p1.page.getByTestId('holdem-board')).toBeVisible({ timeout: 15_000 }); // 等双方就座 + 开始按钮可用,然后开始 await expect(async () => { const startBtn = p0.page.getByTestId('holdem-start'); await expect(startBtn).toBeEnabled({ timeout: 2_000 }); await startBtn.click(); }).toPass({ timeout: 20_000 }); // heads-up: 按钮=SB 先行动。等行动条出现。 await expect(async () => { const b0 = p0.page.getByTestId('holdem-btn-allin'); const b1 = p1.page.getByTestId('holdem-btn-allin'); expect(await b0.count() + await b1.count()).toBeGreaterThan(0); }).toPass({ timeout: 15_000 }); // 谁有全下按钮谁全下(SB 先手) const p0HasBtn = await p0.page.getByTestId('holdem-btn-allin').isVisible().catch(() => false); const raiser = p0HasBtn ? p0 : p1; const caller = p0HasBtn ? p1 : p0; await raiser.page.getByTestId('holdem-btn-allin').click(); // 对方跟注 await expect(async () => { const btn = caller.page.getByTestId('holdem-btn-call'); if (await btn.isVisible({ timeout: 1_000 }).catch(() => false)) { await btn.click(); return; } throw new Error('call button not visible yet'); }).toPass({ timeout: 15_000 }); // ===== 关键验证 1: 摊牌结果 ===== // 等 board 出现 5 张公共牌 + 结果面板(两个客户端都要验证) for (const seat of [p0, p1]) { await expect(async () => { const cards = seat.page.locator('[data-testid^="holdem-board-card-"]'); expect(await cards.count()).toBe(5); }).toPass({ timeout: 15_000 }); await expect(async () => { const results = await seat.page.getByTestId('holdem-log').innerText(); expect(results).toContain('赢得'); }).toPass({ timeout: 15_000 }); // 摊牌后 4 张底牌全部可见(亮牌) await expect(async () => { const holeCards = seat.page.locator('[data-testid^="holdem-hole-"]'); expect(await holeCards.count()).toBe(4); }).toPass({ timeout: 5_000 }); } // ===== 关键验证 2: 破产者补码 ===== // 一方筹码为 0( bankrupt),等 waiting 阶段的补码按钮出现 const brokeSeat = (await p0.page.getByTestId('holdem-rebuy').isVisible().catch(() => false)) ? p0 : p1; await expect(brokeSeat.page.getByTestId('holdem-rebuy')).toBeVisible({ timeout: 15_000 }); await brokeSeat.page.getByTestId('holdem-rebuy').click(); // 补码后筹码应恢复 30 await expect(async () => { const stackText = await brokeSeat.page .locator(`[data-testid="holdem-seat-${brokeSeat.pid}"]`) .innerText(); expect(stackText).toContain('筹码 30'); }).toPass({ timeout: 15_000 }); // ===== 关键验证 3: 房主重置房间 ===== await expect(p0.page.getByTestId('holdem-reset')).toBeVisible({ timeout: 5_000 }); await p0.page.getByTestId('holdem-reset').click(); await expect(async () => { const stack0 = await p0.page.getByTestId('holdem-seat-0').innerText(); expect(stack0).toContain('筹码 30'); }).toPass({ timeout: 15_000 }); // ===== 关键验证 4: 重开后能开新一手 ===== await expect(async () => { const startBtn = p0.page.getByTestId('holdem-start'); await expect(startBtn).toBeEnabled({ timeout: 2_000 }); await startBtn.click(); }).toPass({ timeout: 20_000 }); // 进入 preflop(出现行动者高亮) await expect(async () => { const a0 = await p0.page.locator('[data-actor="true"]').count(); const a1 = await p1.page.locator('[data-actor="true"]').count(); expect(a0 + a1).toBeGreaterThan(0); }).toPass({ timeout: 15_000 }); console.log('matchID:', mid); }); });