JavaScript & TypeScript · Comparison
Vitest coverage: V8 vs Istanbul
Use V8 on Node.js or Chromium, and Istanbul where V8 coverage isn't available, such as Firefox. Since Vitest 3.2 both produce the same reports.
V8 vs Istanbul: which provider should you use?
Start with V8 on Node.js or Chromium. Use Istanbul when your test environment cannot expose V8 coverage, such as Firefox. These are two ways to collect coverage for Vitest, not different test runners.
Since Vitest 3.2, the V8 provider uses AST-based remapping to produce the same coverage reports as Istanbul. “Istanbul is more accurate” is no longer a good general reason to switch. That is specific to Vitest, not every V8-based tool. See the official provider comparison.
Set up coverage for your source files
Install the coverage package that matches your Vitest version. For Vitest 5.0.0:
npm install -D @vitest/coverage-v8@5.0.0
npx vitest run --coverage
Merge this coverage section into your existing configuration:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.{js,ts,jsx,tsx}'],
reporter: ['text', 'html', 'json-summary'],
},
},
});
Use your actual source directory. Without an explicit include pattern,
unimported source files may be absent from the report. For Istanbul, install
the matching @vitest/coverage-istanbul package and change provider to
'istanbul'. Keep the tests and source scope unchanged.
Your agent can run these commands too. Current Vitest also adapts its text reporter for agents. Agent-readable output alone is not a reason to add Supercov.
Same tests, three coverage reports
Download the comparison, extract
it and open the javascript folder. It contains a checkout function, two
Vitest tests and pinned dependencies. Run:
npm ci
npx vitest run --coverage --coverage.provider=v8
npx vitest run --coverage --coverage.provider=istanbul
npx supercov -- npx vitest run
npx supercov runs latest
The rule is simple: checkout needs a signed-in customer whose session has not expired. It returns a status and the total:
export function checkout(signedIn, expired, price, quantity) {
if (signedIn && !expired) {
const total = price * quantity;
return { status: 'confirmed', total };
}
return { status: 'denied', total: 0 };
}
The starter tests check a valid session and a signed-out visitor. Both check the status; neither checks the total.
These are measured results using Vitest 5.0.0 and Supercov 2.0.0. The Vitest runs are from September 14, 2026; Supercov’s were repeated with 2.0.0 on September 25 and matched. All three runs use the same Vitest tests.
| Report | Lines | Branches | MC/DC |
|---|---|---|---|
| Vitest V8 | 100% (4/4) | 100% (4/4) | Not reported |
| Vitest Istanbul | 100% (4/4) | 100% (4/4) | Not reported |
| Supercov wrapping Vitest | 100% (5/5) | 100% (2/2) | 50% (1/2) |
Tools count executable lines and branches differently. Those denominators are not an accuracy ranking. The useful difference here is the additional question MC/DC asks.
What test is missing?
Inspect the decision:
npx supercov runs latest decision src/session.js:2
An excerpt from the actual output:
signedIn && !expired
C1 covered: signedIn
C2 MISSING: !expired
confidence executed
vectors:
TT -> T tests=1 confidence=executed
F- -> F tests=1 confidence=executed
No test tries a signed-in customer with an expired session. The expression
!expired is never shown changing the outcome. Add this case to the suite:
test('rejects an expired session', () => {
const order = checkout(true, true, 25, 2);
assert.equal(order.status, 'denied');
});
The download already imports test from Vitest and assert from
node:assert/strict. The MC/DC guide explains how the three
cases establish independent condition coverage.
A second gap: a wrong total still passes
The original tests also pass if price * quantity becomes
price * quantity - 1. Everything still runs, and checkout still returns
'confirmed'. None of the original assertions check the amount.
The completed tests in completed-tests.txt add the expiry case and check
order.total === 50 for a price of 25 and quantity of 2. Replace
tests/session.test.js with that file’s contents, then repeat the commands.
| Check | Starter tests | Completed tests |
|---|---|---|
| Tests against the original code | Pass: 2 tests | Pass: 3 tests |
| Native line and branch coverage | 100% | 100% |
| MC/DC | 50% (1/2) | 100% (2/2) |
| Remove the expiry guard | Still pass | Fail |
| Subtract 1 from the total | Still pass | Fail |
We ran both code changes separately and restored the source between checks. These are two selected faults, not a general mutation score or a demonstration that Supercov automatically finds every weak assertion.
Assertions were not assessed in this comparison, which was recorded with
Supercov 2.0. The reports say assertions are unassessed, not 0% or 100%. To
assess them, run runs latest assertions assess; see
assertion coverage.
Browser Mode and component tests
Both providers work in Vitest Browser Mode, where your component tests run in a real browser. So does Supercov: it measures a browser-mode run per test, with any provider and no extra configuration, and reports MC/DC and assertion coverage there too.
One measurement detail matters more in component tests than anywhere else. A JSX tree is a single statement, so everything rendered inside it counts as covered the moment the component renders once – including an expression that never evaluated.

That is also the seam a UI assertion attaches to. toHaveAccessibleName is
about the attribute and toHaveTextContent about the child; with one statement
for the whole tree, neither can be named apart from the other, so neither can
be credited to the test that checks it.
Expressions that cannot independently fail to evaluate stay out of the count:
{value} is reached exactly when the tree is, and onClick={() => save()} is
measured where the handler is called rather than where it is created. See
supported suites for the rules.
When is another tool worth adding?
Keep native coverage for finding code that never runs. Add MC/DC when you want to inspect individual decision conditions. Use assertion coverage to find executed statements no test is judged to catch a change to; use mutation testing to run particular code changes against the suite.
| Feature | Vitest · V85.0.0 | Vitest · Istanbul5.0.0 | Supercov2.0.0 |
|---|---|---|---|
| Code quality scoring | Not built in | Not built in | Supported |
| Security review | Not built in | Not built in | Supported |
| Line coverage | Supported | Supported | Supported |
| Branch coverage | Supported | Supported | Supported |
| MC/DC | Not built in | Not built in | Supported |
| Per-test coverage | Not built in | Not built in | Supported |
| Assertion coverage | Not built in | Not built in | Supported |
| Reports | Supported | Supported | Supported |
✓ Supported · × Not built in. Standard coverage workflows in the versions shown; custom plugins are outside this table.
Vitest uses its standard V8 or Istanbul coverage provider here. Supercov works with your existing test runner.
Per-test coverage means individual tests, not just test files. Supercov needs a supported runner. Assertion coverage is Jev’s judgment, test by test, of whether changing each executed statement would make the test fail; it is not a mutation score or a correctness guarantee. Security review comes from Supercov’s Jev checks; reports means a browsable HTML report from the tool itself.
The recording contains the source, exact commands, full output and before-and-after tests. This small example measures coverage criteria, not general accuracy or speed.
For a broader choice, see QA tools for coding agents. If you are choosing a test runner rather than a coverage provider, see Vitest vs Jest.
Example evidence: source, commands and full results (JSON) · runnable examples (ZIP). Measured September 14, 2026; Supercov runs repeated with 2.0.0 on September 25, unchanged. Same-runner fixtures, not a performance benchmark or a general conformance test.