Run Tests
When to Use
- •After adding a new hook action, component, or genre pack
- •After fixing a bug (verify the regression test passes)
- •When a CI check fails but you're not sure why
- •Before committing to confirm nothing regressed
Commands
# Run all tests once npm test # Watch mode (re-runs on save) npm run test:watch # Single file npm test -- src/hooks/useGameState.test.ts # With verbose output npm test -- --reporter=verbose # With coverage npm run test:coverage
Diagnose Failures Automatically
node .github/skills/run-tests/scripts/diagnose-failures.cjs
Outputs ✅/❌ per failure with pattern-matched hints for the most common causes.
Common Failure Patterns and Fixes
1. Missing TooltipProvider wrapper
Symptom:
Warning: Missing TooltipProvider
or a component rendering incorrectly without tooltip content.
Fix: Wrap in withTooltip() helper at the top of the test file:
import { TooltipProvider } from '@/components/ui/tooltip';
function withTooltip(ui: React.ReactElement) {
return render(<TooltipProvider>{ui}</TooltipProvider>);
}
// Usage in test:
withTooltip(<Meter label="Tension" value={5} max={10} type="tension" onAdjust={vi.fn()} tooltipText="" />);
Apply withTooltip() to: Meter, GameCard (compact mode), and any widget that uses <Tooltip>.
2. Icon-only button not found
Symptom:
TestingLibraryElementError: Unable to find an accessible element with the role "button" and name /\+/
Cause: The button renders a Lucide SVG icon — no text content exists.
Fix: Query by aria-label:
// ❌ Wrong
screen.getByRole("button", { name: /\+/ });
screen.getByRole("button", { name: /−|-/ });
// ✅ Correct
screen.getByRole("button", { name: /increase tension/i });
screen.getByRole("button", { name: /decrease coherence/i });
The Meter component sets aria-label="Increase <Label>" / "Decrease <Label>".
3. State update outside act()
Symptom:
Warning: An update to ... inside a test was not wrapped in act(...)
Fix: Always wrap hook action calls:
// ❌ Wrong
result.current.adjustMeter("tension", 1);
// ✅ Correct
act(() => result.current.adjustMeter("tension", 1));
4. resetGame localStorage assertion failing
Symptom: Test expects phase: 'setup' but gets a string in localStorage.
Fix: Assert on state fields, not localStorage JSON:
// ❌ Fragile
expect(localStorage.getItem("noir-narrative-game")).toBeNull();
// ✅ Correct
expect(result.current.gameState.phase).toBe("setup");
expect(result.current.gameState.players).toHaveLength(0);
5. Card not found in player's hand
Symptom:
Error: Card "Resolve" not found in player 0's hand
Cause: The card might not be in the hand yet, or the name is wrong (names are case-sensitive).
Fix: Use the findCard helper and check that the card name matches the content exactly:
const card = findCard(result, 0, "Resolve"); // exact name from gameContent.ts
if (!card) throw new Error("Card not found — check gameContent.ts for name");
6. Hook test timing / async
If a hook action triggers async behaviour (future feature), wrap in await act(async () => ...). Currently all actions are synchronous.
Test File Map
| What broke | Test file to check |
|---|---|
| Game rule / meter / fact / scene | src/hooks/useGameState.test.ts |
| Card renders / buttons / modals | src/components/game/components.test.tsx |
| Genre pack missing content | src/data/gameContent.test.ts |
Adding a Regression Test
When fixing a bug, always add a test first that fails before the fix:
it("does not silently skip draw when deck is empty", () => {
// arrange: exhaust the deck
// act: trigger a draw
// assert: player hand maintained (no silent skip)
});
After the fix, the test must pass. Commit both together.