/** * Holdem — 联机 E2E * * 验证最短一手牌的完整联机流程: * 创建 2 人房 → P1 加入 → 双方自动就座 → 房主开始 → 盲注 → P1 弃牌 → * P0 弃牌胜 → 回 waiting 阶段。 * * 单元测试(holdem.test.ts)覆盖牌型评估、边池、按钮轮转等细节; * 此 E2E 只验证联机接线与 UI 的关键 testid 串得通。 */ import { test, expect, Browser } from '@playwright/test'; /** 开一个新 context(独立 localStorage / credentials),选 Holdem + 联机。 */ async function setupOnlineClient(browser: Browser) { const ctx = await browser.newContext(); const page = await ctx.newPage(); await page.goto('/'); await page.getByTestId('game-select').selectOption('holdem'); await page.getByTestId('mode-online').click(); await expect(page.getByTestId('lobby-room-list')).toBeVisible(); return page; } /** P0:设 2 座 → 创建房间(自动 join 成 P0)。 */ async function createRoomAsP0(page: import('@playwright/test').Page) { // Holdem 创建表单有座位选择器(默认 9),选 2 以走最快的 heads-up 流程 await page.getByTestId('lobby-create-seats').selectOption('2'); await page.getByTestId('lobby-create-room').click(); // 创建后应进入"在房间"状态,并显示 HoldemBoard await expect(page.getByTestId('lobby-current-room')).toBeVisible({ timeout: 15_000 }); await expect(page.getByTestId('holdem-board')).toBeVisible({ timeout: 15_000 }); } /** P1:刷新列表 → 加入第一个房间的 P1 座位。 */ async function joinRoomAsP1(page: import('@playwright/test').Page) { await expect(async () => { await page.getByTestId('lobby-refresh').click(); // 房间 chip 的 testid 形如 lobby-room-,排除 list/refresh/current-room const rooms = page.locator('[data-testid^="lobby-room-"]:not([data-testid="lobby-room-list"])'); expect(await rooms.count()).toBeGreaterThan(0); }).toPass({ timeout: 10_000 }); const rooms = page.locator('[data-testid^="lobby-room-"]:not([data-testid="lobby-room-list"])'); const firstRoomAttr = await rooms.first().getAttribute('data-testid'); const matchID = firstRoomAttr!.replace(/^lobby-room-/, ''); await page.getByTestId(`lobby-join-1-${matchID}`).click(); await expect(page.getByTestId('holdem-board')).toBeVisible({ timeout: 15_000 }); } /** 等待某个座位就座(auto sitDown 由 HoldemBoard mount 时触发)。 */ async function expectSeated(page: import('@playwright/test').Page, pid: string) { // 没有直接的 "seated" testid,但开始按钮要 eligibleCount>=2 才会出现; // 间接验证:等 holdem-start(P0 视角)或 holdem-sit 消失(已就座)。 // 这里仅作宽松等待,让 server 状态同步。 await expect(page.getByTestId(`holdem-seat-${pid}`)).toBeVisible({ timeout: 10_000 }); } test.describe('Holdem — 2-player multiplayer E2E', () => { test('create → join → sit → start → blinds → fold win → waiting', async ({ browser }) => { const p0 = await setupOnlineClient(browser); const p1 = await setupOnlineClient(browser); await createRoomAsP0(p0); await joinRoomAsP1(p1); // 双方自动就座(HoldemBoard mount 时 sitDown) await expectSeated(p0, '0'); await expectSeated(p1, '1'); // 等双方就座都同步到 P0 视角(holdem-start 从 disabled 变 enabled), // 然后房主点开始 → 进入 preflop,出现行动者高亮。 await expect(async () => { const startBtn = p0.getByTestId('holdem-start'); await expect(startBtn).toBeEnabled({ timeout: 2_000 }); await startBtn.click(); // 开始成功后任一 tab 应出现行动者高亮 const a0 = await p0.locator('[data-actor="true"]').count(); const a1 = await p1.locator('[data-actor="true"]').count(); expect(a0 + a1).toBeGreaterThan(0); }).toPass({ timeout: 15_000 }); // 确认双方都进入了下注阶段(行动者高亮)。heads-up 中 SB(P1)先行动。 await expect(async () => { const p1Actor = await p1.locator('[data-testid="holdem-seat-1"][data-actor="true"]').count(); const p0Actor = await p0.locator('[data-testid="holdem-seat-0"][data-actor="true"]').count(); // 至少一个 tab 看到行动者 expect(p1Actor + p0Actor).toBeGreaterThan(0); }).toPass({ timeout: 10_000 }); // 当前行动者点"弃牌" → 另一方弃牌胜 → 回 waiting const foldP1 = async () => { const btn = p1.getByTestId('holdem-btn-fold'); if (await btn.isVisible({ timeout: 1_000 }).catch(() => false)) { await btn.click(); return true; } return false; }; const foldP0 = async () => { const btn = p0.getByTestId('holdem-btn-fold'); if (await btn.isVisible({ timeout: 1_000 }).catch(() => false)) { await btn.click(); return true; } return false; }; // 轮流尝试弃牌(谁先有按钮谁弃),直到回到 waiting let folded = false; for (let i = 0; i < 10 && !folded; i++) { if (await foldP1()) folded = true; else if (await foldP0()) folded = true; else await p0.waitForTimeout(500); } expect(folded).toBeTruthy(); // 弃牌胜后应回到 waiting 阶段(holdem-start 重新出现给房主) await expect(async () => { // waiting 阶段房主可见开始按钮 const startBtn = p0.getByTestId('holdem-start'); if (await startBtn.isVisible({ timeout: 1_000 }).catch(() => false)) return; // 或日志里出现"胜"字 const logText = await p0.getByTestId('holdem-log').innerText().catch(() => ''); expect(logText).toContain('胜'); }).toPass({ timeout: 15_000 }); }); });