| 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:
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.
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:
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.
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
import { describe, it, test, expect } from "vitest";
test("adds numbers", () => {
expect(1 + 1).toBe(2);
});
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
expect(value).toBe(exact);
expect(value).toEqual(deep);
expect(value).toMatchObject(partial);
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeGreaterThan(3);
expect(value).toBeCloseTo(0.3, 5);
expect(str).toMatch(/pattern/);
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
expect(obj).toHaveProperty("key");
expect(obj).toMatchSnapshot();
expect(() => fn()).toThrow();
expect(() => fn()).toThrow("message");
await expect(promise).resolves.toBe(value);
await expect(promise).rejects.toThrow();
Hooks
import { beforeAll, beforeEach, afterEach, afterAll, vi } from "vitest";
beforeAll(async () => {
await setupDatabase();
});
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
});
afterAll(async () => {
await teardownDatabase();
});
Mocking
import { vi } from "vitest";
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
mockFn.mockResolvedValue(data);
mockFn.mockImplementation((x) => x * 2);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(arg1, arg2);
expect(mockFn).toHaveBeenCalledTimes(3);
vi.mock("@coop/shared", () => ({
useAuth: vi.fn(() => ({ user: mockUser })),
}));
const spy = vi.spyOn(object, "method");
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
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
bun run test
bun run test --watch
bun run test -- coop.test.ts
bun run test --coverage
bun run test --ui
bun run test -t "should validate"
Coverage Requirements
| Package | Critical Paths | Overall |
|---|
| App | 80%+ | 70%+ |
| Extension | 70%+ | 70%+ |
| Shared | 80%+ (auth/crypto: 100%) | 70%+ |
Measuring coverage:
bun run test --coverage
open coverage/index.html
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
Validation Commands
Documentation & Communication
Coverage & Pass Thresholds
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