| name | new-e2e-spec |
| description | Add a new Playwright E2E spec for a night role, flow, or feature. Use when: adding an e2e test, new playwright spec. |
| argument-hint | Test target description (e.g., guard protection prevents death, 6-player magician swap) |
New E2E Spec Skill
่พๅบ่ฏญ่จ๏ผๆง่กๆฌ skill ่ฟ็จไธญ๏ผๆๆ้ขๅ็จๆท็่พๅบ๏ผ่ฟๅบฆๆฅๅใ่ฏข้ฎใๅฎๆ้็ฅใ้่ฏฏๆ็คบ๏ผไธๅพไฝฟ็จไธญๆใ
End-to-end addition of a Playwright E2E test, from defining the test target to local pass.
When to Use
- User requests adding a new E2E test
- User describes a game flow scenario that needs verification
Architecture Overview
e2e/
โโโ fixtures/app.fixture.ts โ test.extend + createPlayerContexts + closeAll
โโโ pages/ โ Page Objects (class-based)
โ โโโ HomePage.ts
โ โโโ RoomPage.ts
โ โโโ ConfigPage.ts
โ โโโ NightFlowPage.ts
โ โโโ BoardPickerPage.ts
โโโ helpers/
โ โโโ night-setup.ts โ withSetup() harness (creates game, runs body, cleans up)
โ โโโ night-driver.ts โ Role-aware night actions (click seat, wolf vote, reveals)
โ โโโ multi-player.ts โ setupNPlayerGame / setupNPlayerGameWithRoles
โ โโโ waits.ts โ ensureConnected, waitForRoomScreenReady
โ โโโ home.ts โ ensureAnonLogin, registerAutoDismissers
โ โโโ ui.ts โ gotoWithRetry, clickIfVisible, screenshotOnFail
โ โโโ diagnostics.ts โ DiagnosticData logger per player
โโโ specs/ โ Test files (grouped by topic)
Procedure
Phase 1 โ Define Test Target
- Extract the test scenario from user input.
- Determine the following:
| Field | Description | Example |
|---|
| Test category | night-role / flow / feature | night-role |
| Involved roles | Which roles participate | seer, wolf, villager |
| Player count | Minimum players to cover scenario | 3 |
| Expected result | What to assert | reveal shows "ๅฅฝไบบ" |
| Spec filename | Which file to add to or create | night-roles-check.spec.ts |
- Check existing specs for similar tests (avoid duplication).
Phase 2 โ Choose Test Pattern
Night-role tests (most common): Use withSetup + night-driver.
import { expect, test } from '@playwright/test';
import { } from '../helpers/night-driver';
import { withSetup } from '../helpers/night-setup';
test('description', async ({ browser }) => {
await withSetup(
browser,
{
playerCount: N,
configure: async (c) => c.configureCustomTemplate({ wolves: M, gods: [...], villagers: K }),
},
async ({ pages, roleMap }) => {
},
);
});
Non-night tests (room lifecycle, config, etc.): Use app fixture.
import { test } from '../fixtures/app.fixture';
test('description', async ({ app: { page, diag } }) => {
});
Phase 3 โ Implementation
Key Rules (see tests.instructions.md)
- Forbidden
page.waitForTimeout(N) (only exception: โค300ms inside polling loops)
- Forbidden
.isVisible({ timeout: N }) (Playwright ignores this param)
- Forbidden
console.log (use test.step() + testInfo.attach())
- Each spec creates an independent room (test isolation)
- Timeout setting: night tests recommend
test.setTimeout(180_000)
- Use
ensureConnected(page) to ensure WebSocket connection is stable
- Use
waitForRoomScreenReady(page) to wait for room to load
night-driver Common Helpers
| Helper | Purpose |
|---|
findRolePageIndex(map, name) | Find page index by role name |
findAllRolePageIndices(map, name) | Find all pages for a role |
waitForRoleTurn(page, keywords, allPages) | Wait for role's action turn |
clickSeatAndConfirm(page, seat) | Click seat + confirm |
driveWolfVote(pages, wolfIndices, seat) | Wolf vote |
waitForNightEnd(pages) | Wait for dawn |
readAlertText(page) | Read alert dialog text |
dismissAlert(page) | Dismiss alert dialog |
viewLastNightInfo(page) | View last night info |
clickBottomButton(page, text) | Click bottom button |
โ ๏ธ Night Step Order (NIGHT_STEPS)
Roles MUST be driven in the order defined by NIGHT_STEP_ORDER_INTERNAL in packages/game-engine/src/models/roles/spec/plan.ts.
waitForRoleTurn calls tryClickAdvanceButton(includeSkip=true) to advance OTHER pages; wrong order will cause the target role to be auto-skipped.
Check order: grep -n '' packages/game-engine/src/models/roles/spec/plan.ts
Common order reference (position):
crowCurse (15) โ wolfKill (16) โ hiddenWolfReveal (18) โ seerCheck (24)
guardProtect (14) โ wolfKill (16) โ witchSave/witchPoison (20/21)
โ ๏ธ actionKind Driving Patterns (Critical!)
Different actionKind steps have different UI flows. You must drive each according to its pattern:
chooseSeat steps (seer, crow, wolf, etc. choosing targets):
const turn = await waitForRoleTurn(page, ['ๆฅ้ช', '้ๆฉ'], allPages, 120);
await clickSeatAndConfirm(page, targetSeat);
const reveal = await readAlertText(page);
await dismissAlert(page);
Reference: night-roles-check.spec.ts
chooseSeat + skip (not using ability):
const turn = await waitForRoleTurn(page, ['่ฏ
ๅ', '้ๆฉ'], allPages, 120);
await dismissAlert(page);
await clickBottomButton(page, 'ไธ็จๆ่ฝ');
await dismissAlert(page);
Reference: night-roles-block.spec.ts seer/guard skip test
confirm steps (hiddenWolf viewing companions, avenger viewing faction, etc.):
const turn = await waitForRoleTurn(page, ['ๆฅ็', 'ๅไผด'], allPages, 120);
await dismissAlert(page);
await clickBottomButton(page, 'ๆฅ็ๅไผด');
const reveal = await readAlertText(page);
await dismissAlert(page);
Reference: night-roles-hidden-wolf-crow.spec.ts, night-roles-kill.spec.ts (avenger)
wolf kill (dedicated helper):
const wolfTurn = await waitForRoleTurn(pages[wolfIdx]!, ['่ขญๅป', '้ๆฉ'], allPages, 120);
await driveWolfVote(pages, wolfIndices, targetSeat);
ConfigPage Template Configuration
configure: async (c) =>
c.configureCustomTemplate({
wolves: 1,
goodRoles: ['seer', 'witch'],
villagers: 2,
});
Phase 3.5 โ Core Principles Self-Check
Run through the core principles checklist ๐ for all changes made:
- Any band-aid fixes? (Principle 1)
- Used third-party API โ did you check docs? (Principle 2)
- Any
as any / unnecessary ?.? (Principle 3)
- Any error-swallowing catch / failure path without feedback? (Principle 4)
- Do new types/fields propagate through the full pipeline? (Principle 5)
Phase 4 โ Verify
- Run single spec:
pnpm exec playwright test e2e/specs/<file> --reporter=list
- Confirm pass, then check if test timeout needs adjusting
- On failure, use trace (auto-saved in
test-results/) to diagnose
Naming Conventions
- Night-role spec filenames:
night-roles-<category>.spec.ts
- Categories: check / kill / protect / block / treasure / thief-cupid / piper / gargoyle / eclipse-wolf-queen / hidden-wolf-crow
- Non-night specs:
<feature>.spec.ts (e.g., seating.spec.ts, reconnect.spec.ts)
- Test describe/test names use English, describing specific scenario and expected result
โ ๏ธ Common Pitfalls (Must Read)
1. Step order causing roles to be auto-skipped
waitForRoleTurn calls tryClickAdvanceButton(includeSkip=true) for OTHER pages, which clicks "ไธ็จๆ่ฝ". If you drive a later role first, earlier roles will be auto-skipped during the wait.
โ Wrong (crow acts before wolf, driving wolf first causes crow to be skipped):
await waitForRoleTurn(wolfPage, ['่ขญๅป'], pages);
await driveWolfVote(...);
await waitForRoleTurn(crowPage, ['่ฏ
ๅ'], pages);
โ
Correct:
await waitForRoleTurn(crowPage, ['่ฏ
ๅ'], pages);
await clickSeatAndConfirm(crowPage, target);
await waitForRoleTurn(wolfPage, ['่ขญๅป'], pages);
await driveWolfVote(...);
2. Initial alert for chooseSeat/confirm steps
All chooseSeat and confirm steps show a prompt alert on entry ("็ฅ้ไบ" button).
clickSeatAndConfirm handles this internally (auto dismissAlert).
clickBottomButton does NOT auto dismiss (only dismisses during retry).
- Therefore you must manually
await dismissAlert(page) before clickBottomButton.
3. Seat number format
roleMap.get(idx)!.seat โ 0-based seat index
- Seer and similar roles' reveal dialog uses 1-indexed display (
formatSeat(seat) = ${seat+1}ๅท)