V8 Coverage showing unexpected results

Open
#32,836 11 comments 7 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Quiet
Tech stack
angular, typescript

Research direction

Start with the Angular 21 reproduction using ng test --coverage, then inspect angular.json and vitest.config.ts alongside the changes to app.ts and app.spec.ts. Compare the V8 branch report for the empty component with the configured coverage includes and excludes. Done means establishing whether the result is expected V8 behavior or an Angular CLI unit-test builder defect, with a reproducible case and expected coverage behavior documented.

Written by the indexing model from the issue text.

Description

angular/build:unit-test area: @angular/build gemini-triaged
Which @angular/* package(s) are the source of the bug?

Don't known / other

Is this a regression?

No

Description

I'm in the process of migrating from jasmine/karma with istanbul coverage to vitest with v8 coverage. I'm currently migrating around 2500 unit tests. With Istanbul I had over 95% coverage across statements, branches, functions and lines. With vitest I'm seeing lower coverage, mainly on the branches with 90%.

I've noticed that even empty classes (template only) show 90% branch coverage (9/10).

A minimal reproduction:

  • Create a new Angular 21 application: ng new ng21
  • Install v8 coverage npm i --save-dev @vitest/coverage-v8
  • Remove the protected title variable from app.ts -- protected readonly title = signal('ng21');
  • Remove the should render title test from app.spec.ts
  • Run ng test --coverage

This is the coverage I see:
Image

Similar from my project:
Image

with the spec file as:

import { TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
import { HeaderComponent as ClsHeaderComponent, TenantDropdownComponent } from '@cls/ui-util';
import { MockComponent } from '@cls/ui-util/testing';
import { SearchComponent } from '../../schedule/components/search/search.component';

describe('HeaderComponent', () => {
    beforeEach(async () => {
        await TestBed.configureTestingModule({
            imports: [HeaderComponent],
            providers: []
        })
            .overrideComponent(HeaderComponent, {
                remove: { imports: [SearchComponent, ClsHeaderComponent, TenantDropdownComponent] },
                add: {
                    imports: [
                        MockComponent({ selector: 'app-search' }) as any,
                        MockComponent({ selector: 'cls-header' }) as any,
                        MockComponent({ selector: 'cls-tenant-dropdown' }) as any
                    ]
                }
            })
            .compileComponents();
    });

    function setup() {
        const fixture = TestBed.createComponent(HeaderComponent);

        return {
            fixture,
            app: fixture.componentInstance,
            compiled: fixture.debugElement.nativeElement
        };
    }

    it('should create the app', () => {
        const { app } = setup();
        expect(app).toBeTruthy();
    });
});

angular.json

"test": {
  "builder": "@angular/build:unit-test",
  "options": {
    "browsers": ["chromium"],
    "buildTarget": "admin-ui:build",
    "headless": true,
    "tsConfig": "./tsconfig.spec.json",
    "runnerConfig": "./vitest.config.ts",
    "coverage": true,
    "coverageInclude": ["src/app/**/*.ts"],
    "coverageExclude": ["index.ts", "main.ts", "polyfills.ts", "**/*.routes.ts", "**/*.model.ts", "**/*.interface.ts"],
    "coverageReporters": ["text", "html", "lcov"],
    "coverageThresholds": {
      "statements": 90,
      "branches": 90,
      "functions": 90,
      "lines": 90
    }
  }
}

vitest.config.ts

import { defineConfig } from 'vitest/config';

export default defineConfig({
    test: {
        environment: 'jsdom',
        globals: true,
        setupFiles: ['src/test-setup.ts'],
        restoreMocks: true,
        coverage: {
            provider: 'v8'
        },
        browser: {
            screenshotFailures: false
        }
    }
});

Have I misconfigured something? Do I have an incorrect assumption of v8 coverage or is there an issue here?

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw

Please provide the environment you discovered this bug in (run ng version)
Angular CLI       : 21.2.3
Angular           : 21.2.5
Node.js           : 22.13.1
Package Manager   : npm 10.9.2
Operating System  : darwin arm64

┌───────────────────────────────────┬───────────────────┬───────────────────┐
│ Package                           │ Installed Version │ Requested Version │
├───────────────────────────────────┼───────────────────┼───────────────────┤
│ @angular/animations               │ 21.2.5            │ ~21.2.0           │
│ @angular/build                    │ 21.2.3            │ ^21.2.0           │
│ @angular/cdk                      │ 21.2.3            │ ~21.2.0           │
│ @angular/cli                      │ 21.2.3            │ ~21.2.0           │
│ @angular/common                   │ 21.2.5            │ ~21.2.0           │
│ @angular/compiler                 │ 21.2.5            │ ~21.2.0           │
│ @angular/compiler-cli             │ 21.2.5            │ ~21.2.0           │
│ @angular/core                     │ 21.2.5            │ ~21.2.0           │
│ @angular/forms                    │ 21.2.5            │ ~21.2.0           │
│ @angular/language-service         │ 21.2.5            │ ~21.2.0           │
│ @angular/platform-browser         │ 21.2.5            │ ~21.2.0           │
│ @angular/platform-browser-dynamic │ 21.2.5            │ ~21.2.0           │
│ @angular/router                   │ 21.2.5            │ ~21.2.0           │
│ rxjs                              │ 7.8.2             │ ~7.8.2            │
│ typescript                        │ 5.9.3             │ ~5.9.3            │
│ vitest                            │ 4.1.0             │ ^4.0.18           │
│ zone.js                           │ 0.15.1            │ ~0.15.0           │
└───────────────────────────────────┴───────────────────┴──────────────────
Anything else?

No response

Dominant language
TypeScript
Stars
27k
Forks
11.8k
Avg merge
16h 35m
Merged PRs (30d)
176

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from angular/angular-cli

All issues in angular/angular-cli

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.