Adds the multiplayer functionality the user requested:
- LobbyRoomList UI component (create / list / join via boardgame.io's
built-in Lobby REST API). Credentials are server-issued; no more hardcoded
p0/p1 in OnlineConfig.
- LobbyClient re-export wrapper in @tts-like/engine (typed factory
createLobbyClient).
- CORS origin default: dev-friendly regex list (boardgame.io's
isOriginAllowed treats literal '*' as exact-match, which silently blocks
everything; we now use RegExp matching localhost + Tauri schemes).
- War shuffle accepts an injected random fn so unit tests are deterministic;
fixed a tie-handling bug where piles were cleared without redistributing
cards (now piles stay on tie for a "war" round).
- Engine unit tests (vitest + Local master for 2-client sync):
packages/engine/src/games/{drag-test,war,mill}.test.ts (34 cases)
- Server REST tests (vitest + supertest, port 0 random):
packages/server/src/server.test.ts (9 cases)
- Playwright E2E (apps/web/e2e/*): 3 specs covering create → join →
sync for War / DragTest / Mill across two browser contexts.
- Dockerfile fix: pnpm deploy --prod leaves boardgame.io as a symlink to
.pnpm/boardgame.io@…; copy -rL materialises the dist so the runtime
image can resolve boardgame.io/server.
- Root scripts: dev:server, test:e2e, verify (ts + test + e2e).
60 lines
2.3 KiB
TypeScript
60 lines
2.3 KiB
TypeScript
/**
|
||
* Mill — 联机 E2E
|
||
*
|
||
* 验证联机模式下 MillBoard 渲染正确(24 个交叉点)。
|
||
* 完整下棋流程覆盖在前面的单元测试(vitest + Local master)里。
|
||
*/
|
||
|
||
import { test, expect, Browser } from '@playwright/test';
|
||
|
||
async function setupOnlineGame(browser: Browser, gameKey: string) {
|
||
const ctx = await browser.newContext();
|
||
const page = await ctx.newPage();
|
||
await page.goto('/');
|
||
await page.getByTestId('game-select').selectOption(gameKey);
|
||
await page.getByTestId('mode-online').click();
|
||
await expect(page.getByTestId('lobby-room-list')).toBeVisible();
|
||
return page;
|
||
}
|
||
|
||
async function joinAsFirstPlayer(page: any, playerSlot: '0' | '1') {
|
||
if (playerSlot === '0') {
|
||
await page.getByTestId('lobby-create-room').click();
|
||
} else {
|
||
await expect(async () => {
|
||
await page.getByTestId('lobby-refresh').click();
|
||
const roomChips = await page.locator('[data-testid^="lobby-room-"]').all();
|
||
const ids = await Promise.all(roomChips.map((c: any) => c.getAttribute('data-testid')));
|
||
const real = ids.filter((id: string | null) =>
|
||
id && !id.endsWith('-list') && !id.endsWith('-refresh') && !id.endsWith('-room'),
|
||
);
|
||
expect(real.length).toBeGreaterThan(0);
|
||
}).toPass({ timeout: 10_000 });
|
||
|
||
const rooms = page.locator('[data-testid^="lobby-room-"]:not([data-testid="lobby-room-list"])');
|
||
const count = await rooms.count();
|
||
expect(count).toBeGreaterThan(0);
|
||
const firstRoom = rooms.first();
|
||
const matchIDAttr = await firstRoom.getAttribute('data-testid');
|
||
const matchID = matchIDAttr!.replace(/^lobby-room-/, '');
|
||
await page.getByTestId(`lobby-join-1-${matchID}`).click();
|
||
}
|
||
|
||
await expect(page.getByTestId('online-need-room')).not.toBeVisible({ timeout: 15_000 });
|
||
}
|
||
|
||
test.describe('Mill — 2-player multiplayer E2E', () => {
|
||
test('both clients see the mill board after joining same room', async ({ browser }) => {
|
||
const p0 = await setupOnlineGame(browser, 'mill');
|
||
const p1 = await setupOnlineGame(browser, 'mill');
|
||
|
||
await joinAsFirstPlayer(p0, '0');
|
||
await joinAsFirstPlayer(p1, '1');
|
||
|
||
// 两 tab 都应能渲染 SVG 棋盘(MillBoard 用 SVG)
|
||
const svgCount0 = await p0.locator('svg').count();
|
||
const svgCount1 = await p1.locator('svg').count();
|
||
expect(svgCount0).toBeGreaterThan(0);
|
||
expect(svgCount1).toBeGreaterThan(0);
|
||
});
|
||
}); |