Merge Vitest coverage from unit tests (jsdom) and browser component tests.
📝 UPDATE (January 2025):
As of v0.2.0, normalization is now OFF by default. Use
--normalizeif you need to strip import statements and directives.
When running Vitest with both jsdom (unit tests) and browser mode (component tests), the coverage reports have different statement counts:
| Environment | Import Handling |
|---|---|
| jsdom | V8 doesn't count imports as statements |
| Real browser | V8 counts imports as executable statements |
This makes it impossible to accurately merge coverage without normalization.
vitest-coverage-merge provides smart merging of coverage data. When you encounter statement count mismatches, you can use the --normalize flag to strip import statements and Next.js directives ('use client', 'use server') before merging.
npm install -D vitest-coverage-merge# Merge unit and component coverage
npx vitest-coverage-merge coverage/unit coverage/component -o coverage/merged
# Merge multiple sources
npx vitest-coverage-merge coverage/unit coverage/component coverage/e2e -o coverage/all
# Merge with normalization (strips imports/directives)
npx vitest-coverage-merge coverage/unit coverage/component -o coverage/merged --normalizevitest-coverage-merge <dir1> <dir2> [dir3...] -o <output>
Arguments:
<dir1> <dir2> Coverage directories to merge (at least 2 required)
Each directory should contain coverage-final.json
Options:
-o, --output Output directory for merged coverage (required)
--normalize Strip import statements and directives before merging
-h, --help Show help
-v, --version Show version
import { mergeCoverage, normalizeCoverage } from 'vitest-coverage-merge'
// Merge coverage directories
const result = await mergeCoverage({
inputDirs: ['coverage/unit', 'coverage/component'],
outputDir: 'coverage/merged',
normalize: false, // default (set to true to strip imports/directives)
reporters: ['json', 'lcov', 'html'], // default
})
console.log(result.statements.pct) // e.g., 85.5import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'jsdom',
include: ['src/**/*.test.{ts,tsx}'],
exclude: ['src/**/*.browser.test.{ts,tsx}'],
coverage: {
enabled: true,
provider: 'v8',
reportsDirectory: './coverage/unit',
reporter: ['json', 'lcov', 'html'],
},
},
})import { defineConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
export default defineConfig({
test: {
include: ['src/**/*.browser.test.{ts,tsx}'],
coverage: {
enabled: true,
provider: 'v8',
reportsDirectory: './coverage/component',
reporter: ['json', 'lcov', 'html'],
},
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
}){
"scripts": {
"test": "npm run test:unit && npm run test:component",
"test:unit": "vitest run",
"test:component": "vitest run --config vitest.component.config.ts",
"coverage:merge": "vitest-coverage-merge coverage/unit coverage/component -o coverage/merged"
}
}The tool generates:
coverage-final.json- Istanbul coverage datalcov.info- LCOV format for CI toolsindex.html- HTML report (in lcov-report folder)
- Load coverage-final.json from each input directory
- Normalize (optional, with
--normalizeflag) by stripping:- ESM import statements (
import ... from '...') - React/Next.js directives (
'use client','use server') - if present
- ESM import statements (
- Smart merge using one of two strategies:
- Default (no
--normalize): "More items wins" - prefers source with more coverage items, giving you the union of all structures - With
--normalize: "Fewer items wins" - prefers sources without directive statements (browser-style coverage)
- Default (no
- Merge execution counts using max strategy (takes highest count for each item)
- Generate reports (JSON, LCOV, HTML)
Note: This tool works with any ESM-based Vitest project (React, Vue, Svelte, vanilla JS/TS, etc.). The React/Next.js directive stripping only applies if those directives are present in your codebase - for non-React projects, it simply has no effect.
Vitest's --merge-reports is designed for sharded test runs, not for merging coverage from different environments (jsdom vs browser). It doesn't handle the statement count differences caused by how V8 treats imports differently in each environment.
- nextcov - E2E coverage collection for Next.js with Playwright
- @vitest/coverage-v8 - V8 coverage provider for Vitest
MIT