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).
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
/**
|
|
* DragTest — 联机 E2E
|
|
*
|
|
* 流程:
|
|
* - Tab 0 创建并 join P0
|
|
* - Tab 1 刷新后 join P1
|
|
* - 验证双方都看到 5 张牌在 hand
|
|
*/
|
|
|
|
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 {
|
|
// Tab 1: 等房间列表里出现 room
|
|
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('DragTest — 2-player multiplayer E2E', () => {
|
|
test('both clients see the same 5-card hand initially', async ({ browser }) => {
|
|
const p0 = await setupOnlineGame(browser, 'drag-test');
|
|
const p1 = await setupOnlineGame(browser, 'drag-test');
|
|
|
|
await joinAsFirstPlayer(p0, '0');
|
|
await joinAsFirstPlayer(p1, '1');
|
|
|
|
// 双方都看到 hand zone 含 5 张牌
|
|
await expect(p0.getByTestId('zone-hand')).toHaveAttribute('data-count', '5');
|
|
await expect(p1.getByTestId('zone-hand')).toHaveAttribute('data-count', '5');
|
|
});
|
|
}); |