| name | coverage-guard |
| description | Use when the user wants to check test coverage, enforce 100% coverage, find uncovered code, add missing tests, or increase code coverage. Works with vitest, jest, react-scripts, and other test runners. Also for 'coverage', 'test coverage', 'cover', 'untested', 'uncovered', 'add tests for', 'increase coverage', 'coverage report', 'write tests', 'test all files'. |
| license | MIT |
| compatibility | opencode, claude-code, cursor, windsurf, github-copilot |
| metadata | {"category":"testing","audience":"developers"} |
Coverage Guard
Ensure 100% test coverage for any JavaScript/TypeScript project. Automatically detects the test runner, sets up test infrastructure if missing, writes tests for uncovered components, and loops until every file reaches 100% coverage.
Overview
This skill follows a complete pipeline:
- Detect runner & infrastructure → 2. Scan source files & tests → 3. Setup if missing → 4. Generate new tests → 5. Run coverage → 6. Fill gaps → 7. Verify → repeat until 100%
Step-by-step Workflow
Phase 1: Detect environment
Check package.json in the project root to determine:
cat package.json
Identify the test runner and coverage tool:
| Indicator | Runner | Coverage command |
|---|
"vitest" in devDependencies | Vitest | npx vitest --coverage |
"jest" in devDependencies | Jest | npx jest --coverage |
"react-scripts" in devDependencies | react-scripts (Jest) | npx react-scripts test --coverage |
"next" in devDependencies | Next.js (Jest) | npx next test --coverage or check jest.config |
"@angular/core" in dependencies | Angular (Karma/Jasmine) | ng test --code-coverage |
"@playwright/test" in devDependencies | Playwright | npx playwright test (no built-in coverage; use @playwright/test with istanbul) |
"mocha" in devDependencies | Mocha | npx mocha (coverage via c8/nyc) |
"cypress" in devDependencies | Cypress | npx cypress run (coverage via @cypress/code-coverage) |
"ava" in devDependencies | AVA | npx ava (coverage via c8) |
| None found | None → proceed to Phase 2 → Phase 3 setup | |
Phase 2: Scan source files and existing tests
Find all source files (components, modules, utilities) that need tests:
find src -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) ! -name "*.test.*" ! -name "*.spec.*" ! -name "*.d.ts" ! -path "*/node_modules/*" ! -path "*/dist/*" ! -path "*/build/*" ! -path "*/.next/*"
Find all existing test files:
find src -type f \( -name "*.test.*" -o -name "*.spec.*" \) ! -path "*/node_modules/*"
Map each source file to a test file:
src/components/Button.tsx → src/components/Button.test.tsx or src/components/__tests__/Button.test.tsx
src/utils/format.ts → src/utils/format.test.ts or src/utils/__tests__/format.test.ts
- Follow the existing project convention for test file placement
Classify each source file:
- Has test → add to coverage analysis list
- No test → mark for test generation
Phase 3: Setup test infrastructure (if missing)
If no test runner was detected in Phase 1, ask the user interactively:
? No test framework detected. Which one would you like to set up?
1) Vitest (Recommended — fast, modern, native ESM)
2) Jest (Most widely used)
3) react-scripts (Create React App)
4) Node Built-in (Zero dependencies — uses node:test + node:assert, Node 20+)
5) None — skip, I'll set it up manually
TypeScript note for option 4: If the project is pure JS, zero dependencies needed. If TypeScript, tsx is required as a lightweight dev dependency (just a TS runner, not a test framework), or use Node 22+ --experimental-strip-types.
Once the user selects, install dependencies and create config:
Vitest setup:
npm install -D vitest @vitest/coverage-v8
Create vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/*.config.*',
'**/*.d.ts',
],
},
},
})
Add to package.json scripts:
{
"scripts": {
"test": "vitest",
"test:coverage": "vitest --coverage"
}
}
Jest setup:
npm install -D jest @types/jest ts-jest
npx ts-jest config:init
npm install -D jest @testing-library/react @testing-library/jest-dom jest-environment-jsdom
Create jest.config.ts:
export default {
testEnvironment: 'jsdom',
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
coverageReporters: ['text', 'json', 'html'],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
}
Add to package.json scripts:
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
}
}
react-scripts setup:
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
Coverage comes built-in:
{
"scripts": {
"test": "react-scripts test",
"test:coverage": "react-scripts test --coverage"
}
}
Node Built-in setup (pure JS — zero dependencies):
No packages to install. Add to package.json scripts:
{
"scripts": {
"test": "node --test",
"test:coverage": "node --test --experimental-test-coverage"
}
}
For Node 22+, --experimental-test-coverage is stable and can be used without the flag: check your Node version:
node --version
For Node 23+, --experimental-test-coverage is no longer needed (coverage is built-in).
Node Built-in setup (TypeScript project — minimal dependency):
Install tsx (lightweight TS runner, NOT a test framework):
npm install -D tsx
Add to package.json scripts:
{
"scripts": {
"test": "tsx --test",
"test:coverage": "tsx --test --experimental-test-coverage"
}
}
Alternatively, Node 22.6+ supports --experimental-strip-types which allows running .ts files directly without tsx:
{ "scripts": { "test": "node --experimental-strip-types --test", "test:coverage": "node --experimental-strip-types --test --experimental-test-coverage" } }
After setup, run a quick test to confirm everything works:
npm test
If it fails, fix configuration issues before proceeding.
Phase 4: Write tests for untested source files
For each source file that has no test file, analyze the source code:
- Read the source file
- Identify:
- All exported functions, classes, components
- Return types and possible return paths
- Parameters and their types (required, optional, default values)
- Props interface (for React components)
- Async behavior (Promises, async/await)
- Side effects (API calls, localStorage, etc.)
- Error states and edge cases
- Generate the test file following the project's conventions:
- Use the detected test runner's API (
describe/it/expect for Jest/Vitest, etc.)
- For React: use
@testing-library/react (render, screen, fireEvent)
- Mock external dependencies
- Cover all branches, edge cases, and error paths
React component example (src/components/Button.tsx → src/components/Button.test.tsx):
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button', () => {
it('renders with label', () => {
render(<Button label="Click me" />)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('calls onClick when clicked', () => {
const onClick = vi.fn()
render(<Button label="Click" onClick={onClick} />)
fireEvent.click(screen.getByText('Click'))
expect(onClick).toHaveBeenCalledTimes(1)
})
it('is disabled when disabled prop is true', () => {
render(<Button label="Click" disabled />)
expect(screen.getByText('Click')).toBeDisabled()
})
})
Node Built-in example — pure JS (src/utils/format.js → src/utils/format.test.js):
const { describe, it, mock } = require('node:test')
const assert = require('node:assert/strict')
const { formatCurrency } = require('./format')
describe('formatCurrency', () => {
it('formats integer amount', () => {
assert.strictEqual(formatCurrency(1000), '$1,000.00')
})
it('handles zero', () => {
assert.strictEqual(formatCurrency(0), '$0.00')
})
it('handles negative values', () => {
assert.strictEqual(formatCurrency(-50), '-$50.00')
})
})
For ESM ("type": "module" in package.json):
import { describe, it, mock } from 'node:test'
import assert from 'node:assert/strict'
import { formatCurrency } from './format.js'
Node Built-in example — TypeScript (src/utils/format.ts → src/utils/format.test.ts):
import { describe, it, mock } from 'node:test'
import assert from 'node:assert/strict'
import { formatCurrency } from './format'
describe('formatCurrency', () => {
it('formats integer amount', () => {
assert.strictEqual(formatCurrency(1000), '$1,000.00')
})
it('handles zero', () => {
assert.strictEqual(formatCurrency(0), '$0.00')
})
it('handles negative values', () => {
assert.strictEqual(formatCurrency(-50), '-$50.00')
})
})
Utility function example (src/utils/format.ts → src/utils/format.test.ts):
import { formatCurrency, truncate } from './format'
describe('formatCurrency', () => {
it('formats integer amount', () => {
expect(formatCurrency(1000)).toBe('$1,000.00')
})
it('formats decimal amount', () => {
expect(formatCurrency(99.95)).toBe('$99.95')
})
it('handles zero', () => {
expect(formatCurrency(0)).toBe('$0.00')
})
it('handles negative values', () => {
expect(formatCurrency(-50)).toBe('-$50.00')
})
})
Phase 5: Run coverage report
Run the appropriate coverage command based on detected runner:
| Runner | Command |
|---|---|---|
| Vitest | npx vitest --coverage --reporter=verbose |
| Jest | npx jest --coverage --verbose |
| react-scripts | npx react-scripts test --coverage --watchAll=false |
| Node Built-in (JS) | node --test --experimental-test-coverage |
| Node Built-in (TS via tsx) | tsx --test --experimental-test-coverage |
| Node Built-in (TS via strip-types) | node --experimental-strip-types --test --experimental-test-coverage |
| Others | Use the project's configured coverage command |
Parse the output to identify:
- Files below 100% coverage
- Uncovered lines, branches, functions, and statements
- Specific line numbers that need tests
- Files with 0% coverage (no tests exist)
Phase 6: Analyze and fill coverage gaps
For each file below 100% (including files from Phase 4 that now have tests but are not at 100%):
- Read the source file and its test file side-by-side
- Understand what the uncovered lines/functions/branches do
- Identify which edge cases are missing from tests
- Write targeted tests to cover the gaps:
- Uncovered branches: Test both
if and else paths, ternary conditions
- Uncovered functions: Test all possible return paths
- Uncovered lines: Trace the logic and write a case that reaches them
- Uncovered async paths: Test both resolved and rejected Promise branches
- Uncovered error handling: Test error callbacks, catch blocks, error boundaries
Use the detected test runner's patterns:
- Vitest:
vi.mock(), vi.spyOn(), vi.fn()
- Jest:
jest.mock(), jest.spyOn(), jest.fn()
- react-scripts: Same as Jest
- Node Built-in:
mock.fn(), mock.method(), mock.getter() / mock.setter() (from node:test)
Phase 7: Verify
Re-run coverage after adding/updating tests:
npx vitest --coverage
npx jest --coverage
node --test --experimental-test-coverage
npx tsx --test --experimental-test-coverage
Confirm all files show 100% for statements, branches, functions, and lines.
Phase 8: Repeat
If coverage is still below 100%, loop back to Phase 6.
Configuration by runner
Vitest coverage config
Ensure vitest.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/*.config.*',
'**/*.d.ts',
],
},
},
})
Jest coverage config
Ensure jest.config.ts contains:
export default {
collectCoverage: true,
coverageReporters: ['text', 'json', 'html'],
collectCoverageFrom: [
'src/**/*.{ts,tsx,js,jsx}',
'!src/**/*.d.ts',
'!src/**/*.test.*',
'!src/**/*.spec.*',
],
}
Tips
- Start with files that have existing tests but low coverage — quickest wins
- Then tackle untested files with the simplest logic first
- For React components: test rendering, user interactions, conditional rendering, error states, and empty/loading states
- For utility functions: test edge cases (empty strings, null/undefined, boundary values, special characters)
- For API services: test success response, error response, loading state, timeout, and network failure
- If dead code is found (impossible to cover), suggest removing it or adding a coverage ignore comment:
- Vitest/V8:
/* c8 ignore next */ or /* c8 ignore next 3 */
- Jest/Istanbul:
/* istanbul ignore next */ or /* istanbul ignore if */
- Node Built-in (V8):
/* c8 ignore next */ (same as Vitest/V8 since both use V8 coverage)
- Do NOT modify source code logic — only add or update tests
- Follow the project's existing test patterns (file naming, folder structure, mocking style)
- For files with 0 tests, create a comprehensive test file that hits all the major paths first, then iterate
- Node Built-in note:
node:test uses describe/it (or test) from the node:test module. Assertions use node:assert (assert.strictEqual, assert.deepEqual, assert.throws, etc.). For mocking: mock.fn(), mock.method(). No expect or vi globals. More info: https://nodejs.org/api/test.html
Detection priority
When scanning for source files, check in this order of common directory structures:
src/
app/ (Next.js app directory)
components/
lib/
utils/
server/ (Next.js/Node.js)
pages/ (Next.js pages directory)
- Root-level
*.ts / *.tsx files
Example
User: "coverage report shows login.ts at 72%, fix it"
Agent actions:
- Run
npx vitest --coverage --reporter=verbose, see login.ts has uncovered lines 45-52 (error branch of validateEmail)
- Read source, understand
validateEmail throws on invalid emails
- Add test cases for invalid email inputs in
login.test.ts
- Re-run coverage — login.ts now at 100%
User: "we use jest, check coverage"
- Run
npx jest --coverage --verbose, identify uncovered files
- Find
src/components/Header.tsx at 0% — no test exists
- Read Header.tsx source, create
src/components/Header.test.tsx
- Run coverage again — Header.tsx now at 100%
User: "there's no test setup in this project, add coverage"
- Detect no test runner in package.json
- Ask user: "No test framework detected. Install Vitest? (Recommended)"
- User confirms → install vitest + @vitest/coverage-v8, create vitest.config.ts
- Scan src/, find 8 source files, 0 test files
- Write tests for all 8 files
- Run coverage — all at 100%