Skip to content

coverage-v8: one statement gets two end columns when node and browser projects load the same file #11921

Open

Description

@sumanthratna

Describe the bug

When one source file is loaded by a node project and a browser project, coverage-v8 maps one statement to two different end columns: the node copy ends at a column number, the browser copy ends at null. Istanbul keys statements by the whole range, so the report holds the statement twice.

With sharding this produces false uncovered statements. A shard where the file is loaded but the statement never runs reports 3:20-3:29 with 0 hits. A browser shard reports 3:20-3:null with 1 hit. --merge-reports keeps both, so the merged report shows an uncovered statement while lines stay at 100%. We hit this in CI on 196 statements across 15 files.

It needs a destructuring declaration whose initializer is a call (const { label } = useThing();). Plain declarations map to null in both projects and merge correctly.

Reproduction

https://github.com/sumanthratna/vitest-coverage-browser-end-column (pnpm install && pnpm exec playwright install chromium && pnpm test; the README prints the statement ranges)

src/ctx.ts

export function useThing() { return { label: 'world' }; }

src/plain.ts

import { useThing } from './ctx';
export function greet() {
  const { label } = useThing();
  return label;
}

src/plain.test.ts (node project)

import { expect, test } from 'vitest';
import { greet } from './plain';
test('loads', () => expect(typeof greet).toBe('function'));

src/plain.test.tsx (browser project)

import { expect, test } from 'vitest';
import { greet } from './plain';
test('p', () => expect(greet()).toBe('world'));

vitest.config.ts

import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    projects: [
      { extends: true, test: { name: 'node', include: ['src/**/*.test.ts'] } },
      { extends: true, test: { name: 'browser', include: ['src/**/*.test.tsx'],
        browser: { enabled: true, headless: true, provider: playwright(), instances: [{ browser: 'chromium' }] } } },
    ],
    coverage: { provider: 'v8', include: ['src/plain.ts'], reporter: ['json'] },
  },
});

Run vitest run --coverage. coverage/coverage-final.json for src/plain.ts has three statements for a two-statement file:

statementMap: 3:20-3:29, 4:2-4:null, 3:20-3:null

Sharded (--shard=1/2 and --shard=2/2 with --reporter=blob, then --merge-reports --coverage), the merged report has 3:20-3:29 hits=0 beside 3:20-3:null hits=1.

Expected: one statement at 3:20 with the same end column from both projects, so hits add up.

Same result on 4.1.12 and 5.1.0. A node-only config with the same files maps both shards to 3:20-3:29 and merges correctly.

System Info

vitest 5.1.0 and 4.1.12, @vitest/coverage-v8 (same versions), @vitest/browser-playwright (same versions), playwright 1.64.1, Node 22.23.2, macOS (also seen on GitHub Actions ubuntu-latest).

Used Package Manager

pnpm

Activity

  1. AriPerkkio commented on Nov 9, 2026

    @AriPerkkio
    Member

    This is same as #9366 but happens without any third party plugins. We'll need something like vitest-dev/istanbuljs#25 or source map ending adjustment on oxc side for the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feat: coverageIssues and PRs related to the coverage featureupstream

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions