132 lines
4.2 KiB
Markdown
132 lines
4.2 KiB
Markdown
# Test Rewrite Spec — async turn.js writes own logs
|
|||
|
|
|
||
|
|
## New turn.js API
|
||
|
|
|
||
|
|
ALL mutating funcs now `async`, take `ctx` last param, write encounter + log internally, return `newEnc`.
|
||
|
|
|
||
|
|
```js
|
||
|
|
// OLD
|
||
|
|
const r = nextTurn(enc);
|
||
|
|
enc = { ...enc, ...r.patch };
|
||
|
|
// caller had to write log separately
|
||
|
|
|
||
|
|
// NEW
|
||
|
|
enc = await nextTurn(enc, ctx);
|
||
|
|
// encounter + log already written inside func
|
||
|
|
```
|
||
|
|
|
||
|
|
### Func signatures (all async unless noted)
|
||
|
|
- `startEncounter(enc, ctx)` → newEnc
|
||
|
|
- `nextTurn(enc, ctx)` → newEnc
|
||
|
|
- `togglePause(enc, ctx)` → newEnc
|
||
|
|
- `addParticipant(enc, participant, ctx)` → newEnc
|
||
|
|
- `addParticipants(enc, newParticipants[], ctx)` → newEnc
|
||
|
|
- `updateParticipant(enc, id, updatedData, ctx)` → newEnc
|
||
|
|
- `removeParticipant(enc, id, ctx)` → newEnc
|
||
|
|
- `toggleParticipantActive(enc, id, ctx)` → newEnc
|
||
|
|
- `applyHpChange(enc, id, changeType, amount, ctx)` → newEnc (no-op = returns enc unchanged, no write)
|
||
|
|
- `deathSave(enc, id, type, n, ctx)` → `{ enc, status, isDying }` (object now!)
|
||
|
|
- `toggleCondition(enc, id, conditionId, ctx)` → newEnc
|
||
|
|
- `reorderParticipants(enc, draggedId, targetId, ctx)` → newEnc (no-op = returns enc, no write)
|
||
|
|
- `endEncounter(enc, ctx)` → newEnc
|
||
|
|
|
||
|
|
### Pure helpers (sync, unchanged)
|
||
|
|
- `makeParticipant`, `buildCharacterParticipant`, `buildMonsterParticipant`
|
||
|
|
- `generateId`, `rollD20`, `formatInitMod`
|
||
|
|
- `sortParticipantsByInitiative`, `syncTurnOrder`, `computeTurnOrderAfterRemoval`
|
||
|
|
- `snapshotOf`, `buildEntry`
|
||
|
|
|
||
|
|
### Display lifecycle (async, no combat log)
|
||
|
|
- `activateDisplay({campaignId, encounterId}, ctx)` → void
|
||
|
|
- `clearDisplay(ctx)` → void
|
||
|
|
- `toggleHidePlayerHp(currentValue, ctx)` → void
|
||
|
|
|
||
|
|
## Test helper: shared/tests/_helpers.js
|
||
|
|
|
||
|
|
```js
|
||
|
|
const { createMockStorage, mockCtx, readyEnc } = require('./_helpers');
|
||
|
|
const { storage, ctx } = mockCtx(); // ctx = {storage, encPath:'encounters/e1', logPath:'logs', displayPath:'activeDisplay/status'}
|
||
|
|
const enc = readyEnc(); // {id:'e1', name:'TestEnc', round:0, ... 2 participants p1(Bob,init10) p2(Mob,init5)}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Mock storage methods
|
||
|
|
- `storage.calls` — array of `{fn, path, data}` ordered
|
||
|
|
- `storage.logs()` — array of log entries written (addDoc data)
|
||
|
|
- `storage.updatesFor(prefix)` — updateDoc calls matching path prefix
|
||
|
|
- `storage.docs` — Map path→data
|
||
|
|
|
||
|
|
## Transform patterns
|
||
|
|
|
||
|
|
### Pattern 1: state assertion
|
||
|
|
```js
|
||
|
|
// OLD
|
||
|
|
const r = startEncounter(enc);
|
||
|
|
expect(r.patch.round).toBe(1);
|
||
|
|
|
||
|
|
// NEW
|
||
|
|
const newEnc = await startEncounter(enc, ctx);
|
||
|
|
expect(newEnc.round).toBe(1);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 2: log assertion
|
||
|
|
```js
|
||
|
|
// OLD
|
||
|
|
const r = startEncounter(enc);
|
||
|
|
expect(r.log.type).toBe('start_encounter');
|
||
|
|
|
||
|
|
// NEW
|
||
|
|
const newEnc = await startEncounter(enc, ctx);
|
||
|
|
expect(storage.logs()).toHaveLength(1);
|
||
|
|
expect(storage.logs()[0].type).toBe('start_encounter');
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 3: chained calls (sequence)
|
||
|
|
```js
|
||
|
|
// OLD
|
||
|
|
let enc = readyEnc();
|
||
|
|
enc = { ...enc, ...startEncounter(enc).patch };
|
||
|
|
enc = { ...enc, ...nextTurn(enc).patch };
|
||
|
|
|
||
|
|
// NEW
|
||
|
|
let enc = readyEnc();
|
||
|
|
enc = await startEncounter(enc, ctx);
|
||
|
|
enc = await nextTurn(enc, ctx);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 4: no-op
|
||
|
|
```js
|
||
|
|
// OLD (returned {patch:null, log:null})
|
||
|
|
const r = applyHpChange(enc, 'p1', 'damage', 0);
|
||
|
|
expect(r.patch).toBeNull();
|
||
|
|
|
||
|
|
// NEW (returns enc unchanged, no write)
|
||
|
|
const newEnc = await applyHpChange(enc, 'p1', 'damage', 0, ctx);
|
||
|
|
expect(newEnc).toBe(enc); // same ref = no write
|
||
|
|
expect(storage.calls.filter(c => c.fn === 'updateDoc')).toHaveLength(0);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pattern 5: deathSave (returns object now)
|
||
|
|
```js
|
||
|
|
// OLD
|
||
|
|
const r = deathSave(enc, 'p1', 'success', 1);
|
||
|
|
expect(r.status).toBe('pending');
|
||
|
|
|
||
|
|
// NEW
|
||
|
|
const { enc: newEnc, status, isDying } = await deathSave(enc, 'p1', 'success', 1, ctx);
|
||
|
|
expect(status).toBe('pending');
|
||
|
|
```
|
||
|
|
|
||
|
|
## Static guards
|
||
|
|
|
||
|
|
- `static.no-sort.test.js` — KEEP. Still valid (scans turn.js for stray `.sort(`).
|
||
|
|
- `static.no-unlogged.test.js` — DELETE. Old `{patch, log}` shape gone. Every func writes log internally via `commit()`. No way to "forget".
|
||
|
|
- `static.eslint.test.js` — KEEP.
|
||
|
|
|
||
|
|
## Constraints
|
||
|
|
|
||
|
|
- KEEP test names (describe/it blocks) — same coverage intent.
|
||
|
|
- KEEP assertions — same values checked, just via newEnc or storage.logs().
|
||
|
|
- Fresh `mockCtx()` per test (isolation).
|
||
|
|
- For seeded-RNG combat tests: keep the LCG seed + rand helpers, just swap call pattern.
|
||
|
|
- `await` every mutating func call.
|