Agent skill
test-coverage
Manages test coverage with Playwright E2E and Vitest unit tests. Tracks which files need tests, provides templates with fixture-based cleanup, enforces multi-viewport testing and database validation.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/test-coverage
SKILL.md
Test Coverage - Testing Management System
Purpose
This skill manages test coverage with Playwright E2E and Vitest unit tests:
- Detects new files that need tests
- Maps components consuming API routes (tRPC/REST)
- Tracks which pages have E2E coverage
- Generates test templates with cleanup
- Validates real authentication usage (MANDATORY)
- Ensures database validation after UI actions
- Enforces multi-viewport testing
CRITICAL RULES
MANDATORY - NO EXCEPTIONS:
- CLEANUP ALL TEST DATA - Use fixture-based tracking
- VERIFY IN DATABASE - Check DB state after UI actions
- TEST ALL VIEWPORTS - Desktop, tablet, iPhone SE minimum
- REAL AUTH ONLY - Never mock authentication
- UNIQUE DATA - Timestamps in emails/names
- NO SKIP - Never use
.skip()or.only()
Test Structure
tests/
├── unit/ # Unit tests (Vitest)
│ └── *.test.ts
└── e2e/ # E2E tests (Playwright)
├── fixtures/
│ ├── index.ts # Custom fixtures (auth, db, cleanup)
│ ├── auth.fixture.ts # Authentication helpers
│ └── db.fixture.ts # Database connection & cleanup
├── pages/ # Page Object Model
│ ├── base.page.ts # Base page with common methods
│ ├── login.page.ts
│ └── register.page.ts
├── flows/ # User flow tests
│ ├── auth.spec.ts # Login, register, logout
│ ├── crud.spec.ts # Create, read, update, delete
│ └── permissions.spec.ts
├── api/ # API-only tests (no UI)
│ ├── rest.spec.ts # REST API tests
│ └── trpc.spec.ts # tRPC API tests
└── playwright.config.ts
Cleanup Fixture (MANDATORY)
Every project MUST have cleanup fixtures:
// tests/e2e/fixtures/index.ts
import { test as base, expect } from '@playwright/test';
import { MongoClient, Db, ObjectId } from 'mongodb';
type TestFixtures = {
db: Db;
createdIds: Map<string, ObjectId[]>;
trackCreated: (collection: string, id: ObjectId) => void;
};
export const test = base.extend<TestFixtures>(
{
db: async ({}, use) => {
const client = await MongoClient.connect(process.env.MONGODB_URI!);
const db = client.db();
await use(db);
await client.close();
},
createdIds: async ({}, use) => {
const ids = new Map<string, ObjectId[]>();
await use(ids);
},
trackCreated: async ({ createdIds }, use) => {
const track = (collection: string, id: ObjectId) => {
const existing = createdIds.get(collection) || [];
existing.push(id);
createdIds.set(collection, existing);
};
await use(track);
},
// AUTO-CLEANUP runs EVEN IF test fails
},
async ({ db, createdIds }, use) => {
await use();
for (const [collection, ids] of createdIds.entries()) {
if (ids.length > 0) {
await db.collection(collection).deleteMany({
_id: { $in: ids },
});
}
}
}
);
export { expect };
Auth Helper (MANDATORY)
// tests/e2e/fixtures/auth.fixture.ts
import { Page } from '@playwright/test';
export interface TestUser {
name: string;
email: string;
password: string;
}
export function generateTestUser(): TestUser {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(7);
return {
name: `Test User ${timestamp}`,
email: `testuser_${timestamp}_${random}@test.com`,
password: 'TestPassword123!',
};
}
export async function registerUser(page: Page, user: TestUser): Promise<void> {
await page.goto('/auth/register');
await page.getByTestId('name-input').fill(user.name);
await page.getByTestId('email-input').fill(user.email);
await page.getByTestId('password-input').fill(user.password);
await page.getByTestId('confirm-password-input').fill(user.password);
await page.getByTestId('submit-button').click();
await page.waitForURL(/\/app/, { timeout: 10000 });
}
export async function loginUser(page: Page, user: TestUser): Promise<void> {
await page.goto('/auth/login');
await page.getByTestId('email-input').fill(user.email);
await page.getByTestId('password-input').fill(user.password);
await page.getByTestId('submit-button').click();
await page.waitForURL(/\/app/, { timeout: 10000 });
}
Test Templates
E2E Flow Test (with cleanup)
import { test, expect } from '../fixtures';
import { generateTestUser, registerUser } from '../fixtures/auth.fixture';
test.describe('[Feature] Flow', () => {
test('complete user journey', async ({ page, db, trackCreated }) => {
const user = generateTestUser();
// 1. Register
await registerUser(page, user);
// 2. Verify in database
const dbUser = await db.collection('users').findOne({
email: user.email,
});
expect(dbUser).toBeTruthy();
trackCreated('users', dbUser!._id); // TRACK FOR CLEANUP
// 3. Create item
await page.goto('/items/new');
await page.getByTestId('title-input').fill('Test Item');
await page.getByTestId('submit-button').click();
// 4. Verify item in DB
const item = await db.collection('items').findOne({
userId: dbUser!._id,
});
expect(item).toBeTruthy();
trackCreated('items', item!._id); // TRACK FOR CLEANUP
// Test continues... cleanup is automatic
});
});
Multi-Viewport Test
import { test, expect } from '../fixtures';
test.describe('Responsive Design', () => {
const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 800 },
];
for (const viewport of viewports) {
test(`layout works on ${viewport.name}`, async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto('/');
if (viewport.width < 768) {
// Mobile: hamburger menu
await expect(page.getByTestId('hamburger-menu')).toBeVisible();
await expect(page.getByTestId('sidebar')).toBeHidden();
} else {
// Desktop: sidebar visible
await expect(page.getByTestId('sidebar')).toBeVisible();
}
});
}
});
API Test (REST)
import { test, expect } from '@playwright/test';
test.describe('REST API', () => {
test('requires authentication', async ({ request }) => {
const response = await request.get('/api/users');
expect(response.status()).toBe(401);
});
test('validates input', async ({ request }) => {
const response = await request.post('/api/users', {
data: { email: 'invalid' },
});
expect(response.status()).toBe(400);
});
});
API Test (tRPC)
import { test, expect } from '@playwright/test';
test.describe('tRPC API', () => {
test('query without auth fails', async ({ request }) => {
const response = await request.get('/api/trpc/user.me');
expect(response.status()).toBe(401);
});
test('mutation validates input', async ({ request }) => {
const response = await request.post('/api/trpc/user.create', {
data: { json: { name: '' } },
});
const body = await response.json();
expect(body.error).toBeDefined();
});
});
Security Test
import { test, expect } from '../fixtures';
test.describe('Security - Forbidden Requests', () => {
test('cannot access other users data', async ({ page, db }) => {
const userA = await createTestUser(db);
const userB = await createTestUser(db);
await loginAs(page, userA);
// Try to access user B's data
const response = await page.request.get(`/api/users/${userB._id}`);
expect(response.status()).toBe(403);
// Verify nothing changed in DB
const unchanged = await db.collection('users').findOne({
_id: userB._id,
});
expect(unchanged).toEqual(userB);
});
});
Unit Test
import { describe, it, expect } from 'vitest';
describe('[Feature]', () => {
describe('success cases', () => {
it('should [expected behavior] when [condition]', () => {
// Arrange
// Act
// Assert
});
});
describe('error cases', () => {
it('should throw when [invalid condition]', () => {
expect(() => fn(invalid)).toThrow();
});
});
});
Files That NEED Tests
| Type | Location | Test Expected | Required |
|---|---|---|---|
| API Route | server/routers/*.ts |
tests/unit/*.test.ts |
YES |
| API Route | app/api/**/*.ts |
tests/e2e/api/*.spec.ts |
YES |
| Model | server/models/*.ts |
tests/unit/*.test.ts |
YES |
| Page | app/**/page.tsx |
tests/e2e/flows/*.spec.ts |
YES |
| Component | components/**/*.tsx |
tests/e2e/*.spec.ts |
If interactive |
| Hook | hooks/*.ts |
tests/unit/*.test.ts |
YES |
| Util | lib/*.ts |
tests/unit/*.test.ts |
If exported |
Required Flows (E2E)
Every app MUST have tests for:
- Registration - Create new user, verify in DB
- Login/Logout - Auth state changes correctly
- CRUD Create - Item created, visible, in DB
- CRUD Read - Item displayed correctly
- CRUD Update - Item updated, changes in DB
- CRUD Delete - Item removed from DB
- Permissions - Forbidden requests blocked
- Responsive - Works on all viewports
Required data-testid
<!-- Forms -->
<input data-testid="name-input" />
<input data-testid="email-input" />
<input data-testid="password-input" />
<input data-testid="confirm-password-input" />
<button data-testid="submit-button" />
<!-- Feedback -->
<div data-testid="error-message" />
<div data-testid="success-message" />
<div data-testid="loading-spinner" />
<!-- Navigation -->
<nav data-testid="sidebar" />
<button data-testid="hamburger-menu" />
<nav data-testid="mobile-nav" />
<button data-testid="logout-button" />
<!-- Actions -->
<button data-testid="delete-button" />
<button data-testid="edit-button" />
<button data-testid="confirm-delete" />
Playwright Commands
# Install
bun add -D @playwright/test
bunx playwright install
# Run
bunx playwright test # All tests
bunx playwright test --ui # UI mode (recommended)
bunx playwright test --headed # See browser
bunx playwright test --debug # Debug mode
# Specific
bunx playwright test flows/auth # Specific folder
bunx playwright test --project="iPhone SE" # Specific viewport
# Reports
bunx playwright show-report
Checklist
Before Commit
- All new features have E2E tests?
- Tests use fixtures for cleanup?
- All created data is tracked and cleaned?
- Database state verified after UI actions?
- Tests run on all viewports?
- Forbidden requests tested?
- No
.skip()in tests? -
bunx playwright testpasses? -
bun run testpasses?
FORBIDDEN (Never Do)
// WRONG - Skipping tests
test.skip("should work when authenticated", ...);
// WRONG - Mocking auth
const mockAuth = () => { /* fake */ };
// WRONG - Fixed test user
const testUser = { email: "[email protected]" };
// WRONG - No cleanup
const user = await db.create({ ... }); // Orphaned!
// WRONG - No DB validation
await page.click('submit');
// Just trust the UI? NO!
Progressive Disclosure
For detailed patterns and templates, see:
- reference/playwright-patterns.md - Page Object Model, fixtures, API testing
- scripts/coverage-check.sh - Coverage threshold checker
Quick Commands
# Check coverage meets threshold
bash .claude/skills/test-coverage/scripts/coverage-check.sh 80
# Run E2E tests
bunx playwright test
# Run with UI
bunx playwright test --ui
Version
- v3.1.0 - Added progressive disclosure with reference files and scripts
- v3.0.0 - Complete E2E architecture with cleanup, DB validation, multi-viewport
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?