- name
- testing
- description
- Testing patterns - TDD workflow, Vitest unit tests, Playwright E2E, mock strategies, and coverage targets. Use when writing tests, setting up test infrastructure, following TDD, or diagnosing test failures.
- version
- 1.0.0
- status
- active
- packages
- ["shared","app","extension"]
- dependencies
- []
- last_updated
- 2026-05-17
- last_verified
- 2026-05-17
# Testing Skill
Complete testing guide: TDD workflow, unit testing with Vitest, E2E testing with Playwright.
---
## Activation
When invoked:
- Decide the test type (unit/integration/E2E) based on impact and speed.
- Locate existing test utilities in `packages/shared/src/__tests__/test-utils/`.
- Prefer TDD for new behavior (RED -> GREEN -> REFACTOR).
## Part 1: Test-Driven Development (TDD)
### The Iron Law
```
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
```
Write code before the test? Delete it. Start over. No exceptions.
### When to Use TDD
**Always:**
- New features
- Bug fixes
- Refactoring
- Behavior changes
**Exceptions (ask your human partner):**
- Throwaway prototypes
- Generated code
- Configuration files
### Red-Green-Refactor
#### RED - Write Failing Test
Write one minimal test showing what should happen:
```typescript
// Good: Clear name, tests real behavior, one thing
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
```
#### Verify RED - Watch It Fail
**MANDATORY. Never skip.**
```bash
bun run test -- path/to/test.test.ts
```
> **CRITICAL: `bun test` vs `bun run test`** -- `bun test` invokes bun's built-in test runner which **ignores vitest config** (no jsdom, no aliases, no setup files). `bun run test` runs the package.json `"test"` script (vitest). **Always use `bun run test`** for vitest-based packages (shared, app, extension). To run a single test file: `bun run test -- path/to/test.test.ts`.
Confirm:
- Test fails (not errors)
- Failure message is expected
- Fails because feature missing (not typos)
#### GREEN - Minimal Code
Write simplest code to pass the test:
```typescript
// Good: Just enough to pass
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
let lastError: unknown;
for (let i = 0; i < 3; i++) {
try {
return await fn();
} catch (e) {
lastError = e;
}
}
throw lastError;
}
```
#### Verify GREEN - Watch It Pass
**MANDATORY.**
```bash
bun run test -- path/to/test.test.ts
```
#### REFACTOR - Clean Up
After green only: remove duplication, improve names, extract helpers.
Keep tests green. Don't add behavior.
### Common Rationalizations
| Excuse | Reality |
|--------|---------|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Already manually tested" | Ad-hoc != systematic. No record, can't re-run. |
| "TDD will slow me down" | TDD faster than debugging. |
### Bug Fix Workflow
```
1. Write failing test that reproduces the bug
2. Run test -> watch it fail (confirms bug exists)
3. Fix the bug with minimal code
4. Run test -> watch it pass
5. Test now prevents regression forever
```
**Never fix bugs without a test.**
---
## Part 2: Unit Testing with Vitest
### Why Vitest
- **Vite-native**: Same transformation pipeline as your app
- **Fast HMR**: Tests rerun only on affected changes
- **Jest-compatible**: Familiar API
- **TypeScript native**: No extra config
### Core API
```typescript
import { describe, it, test, expect } from "vitest";
// Basic test
test("adds numbers", () => {
expect(1 + 1).toBe(2);
});
// With describe grouping
describe("CoopService", () => {
it("creates a coop with Safe address", async () => {
const coop = await createCoop(testConfig);
expect(coop.safeAddress).toBeDefined();
});
it.skip("skipped test", () => {});
it.only("focused test", () => {});
});
```
### Assertions
```typescript
// Equality
expect(value).toBe(exact); // === comparison
expect(value).toEqual(deep); // Deep equality
expect(value).toMatchObject(partial); // Partial match
// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
// Numbers
expect(value).toBeGreaterThan(3);
expect(value).toBeCloseTo(0.3, 5); // Floating point
// Strings/Arrays
expect(str).toMatch(/pattern/);
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
// Objects
expect(obj).toHaveProperty("key");
expect(obj).toMatchSnapshot();
// Errors
expect(() => fn()).toThrow();
expect(() => fn()).toThrow("message");
// Async
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow();
```
### Hooks
```typescript
import { beforeAll, beforeEach, afterEach, afterAll, vi } from "vitest";
beforeAll(async () => {
await setupDatabase();
});
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
// Cleanup
});
afterAll(async () => {
await teardownDatabase();
});
```
### Mocking
```typescript
import { vi } from "vitest";
// Mock function
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue(data);
mockFn.mockImplementation((x) => x * 2);
// Assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(arg1, arg2);
expect(mockFn).toHaveBeenCalledTimes(3);
// Mock module
vi.mock("@coop/shared", () => ({
useAuth: vi.fn(() => ({ user: mockUser })),
}));
// Spy on existing method
const spy = vi.spyOn(object, "method");
// Fake timers
vi.useFakeTimers();
vi.advanceTimersByTime(1000);
vi.useRealTimers();
```
### Coop Patterns
> For mock factories, hook testing, cleanup patterns, mutation error testing, and offline scenarios, see [vitest-patterns.md](./vitest-patterns.md)
**Key patterns summary:**
- **Hook testing**: Use `renderHook` + wrapper providers. Always `waitFor` async results.
- **Component testing**: Use `render` + `screen` queries. Prefer role-based selectors.
- **Mock factories**: Use `createMockCoop`, `createMockTab` etc. from `test-utils/mock-factories.ts`.
- **Cleanup testing (Rules 1-3)**: Verify timer cleanup, event listener removal, and isMounted guards on unmount.
- **Offline scenarios**: Use `fake-indexeddb/auto` + `vi.spyOn(navigator, "onLine", "get")` to simulate offline.
- **Yjs sync testing**: Test CRDT merge behavior with separate Y.Doc instances.
**Critical paths** requiring 80%+ coverage (100% for auth/crypto): authentication (passkey), coop creation/join, tab collection, Yjs sync, Dexie persistence, Safe operations, publish flow.
### Commands
```bash
bun run test # Run all tests
bun run test --watch # Watch mode
bun run test -- coop.test.ts # Run specific file
bun run test --coverage # With coverage
bun run test --ui # UI mode
bun run test -t "should validate" # Filter by name
```
### Coverage Requirements
| Package | Critical Paths | Overall |
|---------|----------------|---------|
| **App** | 80%+ | 70%+ |
| **Extension** | 70%+ | 70%+ |
| **Shared** | 80%+ (auth/crypto: 100%) | 70%+ |
**Measuring coverage:**
```bash
bun run test --coverage # Run with coverage
open coverage/index.html # View HTML report
```
---
## Part 3: E2E Testing with Playwright
Keep E2E focused on critical user journeys. Use page objects, role-based selectors, and explicit waits.
See `.claude/skills/testing/references/playwright.md` for the full Playwright guide (pyramid, selectors, network mocking, a11y, debugging).
### Browser, Chrome, And Computer Use Lanes
Choose the proof surface before choosing the command:
| Lane | Use When | Commands / Evidence |
|------|----------|---------------------|
| Browser-first local app | Receiver PWA, app shell, docs, and public localhost routes that do not require sign-in. | `bun run test:e2e:receiver-pwa-eval`, `bun run validate e2e:receiver-pwa-eval`, Browser screenshot or Playwright trace. |
| Extension GUI | Popup, sidepanel, extension-card errors, toolbar grants, screenshot capture, and WXT-launched Chromium behavior. | `bun run test:e2e:popup`, `bun run test:e2e:extension`, Computer Use or headed Chromium screenshots when real extension chrome matters. |
| Visual regression | Popup/sidepanel layout and screenshot-baseline checks. | `bun run test:visual`; update baselines only when the visual change is intentional and reviewed. |
| Signed-in/profiled browser | Flows that depend on Chrome profile state, cookies, or authenticated pages. | Codex Chrome extension evidence with browser/profile recorded. |
| Installed PWA / OS prompts | Installed app shell, real media/file-picker prompts, browser permission prompts, and cross-app flows. | Computer Use evidence with target app, route, screenshot, and proof limits. |
Do not claim GUI success from unit, typecheck, lint, or headless-only proof when the changed surface
depends on real browser chrome, extension permissions, or OS prompts.
---
## Verification Checklist
Before marking work complete:
### TDD Requirements
- [ ] Every new function/method has a test
- [ ] Watched each test fail before implementing
- [ ] Wrote minimal code to pass each test
- [ ] All tests pass
- [ ] Tests use real code (mocks only if unavoidable)
- [ ] Edge cases and errors covered
### Validation Commands
- [ ] Run `bun format && bun lint && bun run test` -- no errors/warnings
- [ ] Package-specific: `cd packages/[pkg] && npx tsc --noEmit`
- [ ] UI-impacting changes include Browser, Playwright, Chrome, or Computer Use proof for the
affected surface
### Documentation & Communication
- [ ] Update relevant documentation when behavior changes
- [ ] Surface remaining risks, manual steps, or test gaps in handoff
### Coverage & Pass Thresholds
- [ ] Coverage meets package target (70-100% depending on criticality)
Can't check all boxes? You skipped TDD. Start over.
## Anti-Patterns
- Writing implementation before a failing test is in place
- Keeping placeholder assertions (`expect(true).toBe(true)`)
- Ignoring cleanup tests for timers/listeners/async hooks
- Using brittle snapshots where behavioral assertions are required
- Declaring completion without running `bun run test`, `bun lint`, and build checks
- Using Computer Use for local app routes when Browser or a Playwright mirror would give cleaner,
repeatable proof
## Related Skills
- `react` -- React Testing Library patterns and component testing
- `data-layer` -- Testing offline scenarios, Dexie with fake-indexeddb, Yjs sync
- `browser-verification` -- Rendered browser, Chrome, and Computer Use proof selection
- `xstate` -- Testing state machines with Vitest
GitHubで見る