GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — V8 Coverage showing unexpected results · Issue #32836 · angular/angular-cli

V8 Coverage showing unexpected results #32836

New issue
Copy link
New issue
Copy link
Open
Open
V8 Coverage showing unexpected results#32836
Copy link
Labels
angular/build:unit-testarea: @angular/buildgemini-triagedLabel noting that an issue has been triaged by geminiLabel noting that an issue has been triaged by gemini
Milestone
needsTriage

Description

@daniemo2
daniemo2
opened on Mar 24, 2026
Issue body actions

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

Reacted by Hayley Price, rinckemd, Andrew Gnagy, Ron Olsen, Manoj Sridhar Srinivasan, Nate Radebaugh and Maartje Kanaar

Activity

  1. angular-robot
    added
    gemini-triagedLabel noting that an issue has been triaged by geminiLabel noting that an issue has been triaged by gemini
    on Mar 24, 2026
  2. ngbot
    added this to the needsTriage milestone on Mar 24, 2026
  3. SkyZeroZx commented on Mar 24, 2026

    @SkyZeroZx
    SkyZeroZx
    on Mar 24, 2026
    Contributor
    More actions

    Maybe this angular/angular#67339 should be fixed there?
    You can try it with Angular 21.2.2 , which includes that fix.

  4. daniemo2 commented on Mar 24, 2026

    @daniemo2
    daniemo2
    on Mar 24, 2026
    Author
    More actions

    Thanks for the quick response @SkyZeroZx.

    Apologies, I'm using 21.2.5 for my own project. The previous ng version output above was from the minimal reproduction. I've updated it.

    No improvements with 21.2.5.

  5. thePunderWoman commented on Mar 25, 2026

    @thePunderWoman
    thePunderWoman
    on Mar 25, 2026
    More actions

    This looks to be a CLI related issue. I'll move this issue over to that repo.

  6. thePunderWoman
    transferred this issue fromangular/angularon Mar 25, 2026
  7. angular-robot
    added
    area: @angular/cli
    on Mar 25, 2026
  8. Martinspire commented on Mar 30, 2026

    @Martinspire
    Martinspire
    on Mar 30, 2026
    More actions

    I can confirm this on my repo as well. Its on a lot of files on the export class part of the file. But also on component providers.

    However, I also noticed this on the istanbul coverage provider. Not the same amount (its less) but there are still some lines showing to be uncovered which are definitely covered.

  9. manojsridharsrinivasan commented on Apr 6, 2026

    @manojsridharsrinivasan
    manojsridharsrinivasan
    on Apr 6, 2026
    More actions

    I have the same issue with our project as well. Tried with v8 and istanbul. Both have same issue. Using Angular 21.2.6 and vitest 4.1.1

    Image Image
  10. cexbrayat commented on Apr 10, 2026

    @cexbrayat
    cexbrayat
    on Apr 10, 2026
    Member
    More actions

    Here is a simple but surprising workaround: add fixture.detectChanges() to the test, and branch coverage goes back to 100%.

    Disclaimer: I’m the author of the earlier signal-related coverage issue in core angular/angular#64583, and I spent way too much time digging into this.

    The reason is that this minimal repro only creates the component. With the current ng test + Vitest setup, tests run against AoT-compiled templates by default. In this case the template is compiled to something like:

    template: function App_Template(rf, ctx) {
      if (rf & 1) {
        i0.ɵɵelement(0, "router-outlet");
      }
    }

    You can inspect this with ng test --dump-virtual-files.

    In the test as written, Angular only executes the template in creation mode, so V8 sees the if (rf & 1) branch taken only on its truthy side. The template is never executed in update mode, so the other side is left uncovered. Because this function only exists in generated output, the coverage is remapped back to the component class, which is why the class shows the extra branch and ends up at 9/10.

    If the test triggers change detection, the template runs again in update mode. That makes the rf & 1 condition evaluate false on that pass, so V8 considers both sides covered and branch coverage becomes 10/10.

    So I don’t think this is a CLI bug. At most, Angular could decide to special-case this in generated code, for example by excluding that branch from coverage with an istanbul comment as done in signals, but that would be a compiler/core decision rather than a CLI one.

  11. emilycarter6561-byte commented on Apr 11, 2026

    @emilycarter6561-byte
    emilycarter6561-byte
    on Apr 11, 2026 via email
    More actions
    Thanks for the detailed explanation, @cexbrayat. I can confirm adding fixture.detectChanges() fixes the branch coverage in my project too. Makes sense that it’s related to template execution in AoT rather than a CLI bug.
    …
    On Fri, Apr 10, 2026, 12:41 PM Cédric Exbrayat ***@***.***> wrote: *cexbrayat* left a comment (angular/angular-cli#32836) <#32836 (comment)> Here is a simple but surprising workaround: add fixture.detectChanges() to the test, and branch coverage goes back to 100%. Disclaimer: I’m the author of the earlier signal-related coverage issue in core angular/angular#64583 <angular/angular#64583>, and I spent way too much time digging into this. The reason is that this minimal repro only creates the component. With the current ng test + Vitest setup, tests run against AoT-compiled templates by default. In this case the template is compiled to something like: template: function App_Template(rf, ctx) { if (rf & 1) { i0.ɵɵelement(0, "router-outlet"); }} You can inspect this with ng test --dump-virtual-files. In the test as written, Angular only executes the template in creation mode, so V8 sees the if (rf & 1) branch taken only on its truthy side. The template is never executed in update mode, so the other side is left uncovered. Because this function only exists in generated output, the coverage is remapped back to the component class, which is why the class shows the extra branch and ends up at 9/10. If the test triggers change detection, the template runs again in update mode. That makes the rf & 1 condition evaluate false on that pass, so V8 considers both sides covered and branch coverage becomes 10/10. So I don’t think this is a CLI bug. At most, Angular could decide to special-case this in generated code, for example by excluding that branch from coverage with an istanbul comment as done in signals, but that would be a compiler/core decision rather than a CLI one. — Reply to this email directly, view it on GitHub <#32836 (comment)>, or unsubscribe <https://github.com/notifications/unsubscribe-auth/B7Z2X6AF47JU5ILZQFO6YN34VEW5FAVCNFSM6AAAAACW7POFBOVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DEMRVGYZDOMZVG4> . You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
  12. daniemo2 commented on Apr 13, 2026

    @daniemo2
    daniemo2
    on Apr 13, 2026
    Author
    More actions

    Thanks for the insight @cexbrayat!

    In our unit tests we don't necessarily run tests against the template (just our standard, whether we should is another discussion) and so we rarely call detectChanges as it's unnecessary for us and causes us to have setup component state.

    Perhaps, like you say, there's some solution where it's detected if a test runs in only create mode. But as I imagine the ignore comments are compiled then it's a all or nothing ignore.

    Reacted by Manoj Sridhar Srinivasan
  13. Martinspire commented on Apr 14, 2026

    @Martinspire
    Martinspire
    on Apr 14, 2026
    More actions

    I also see this popping up on components where I absolutely do more testing, but it still shows up.

  14. cexbrayat commented on Apr 14, 2026

    @cexbrayat
    cexbrayat
    on Apr 14, 2026
    Member
    More actions

    @Martinspire you'll need to share a minimal repro if you want someone to take a look (the bare minimum in your component and test to reproduce). I know a few other workarounds, and there are also issues in how the sourcemap are remapped to the source code that are coming from vitest/v8 so maybe you running into that.

  15. manojsridharsrinivasan commented on Apr 14, 2026

    @manojsridharsrinivasan
    manojsridharsrinivasan
    on Apr 14, 2026
    More actions

    I tested this in my Angular CLI + Vitest setup, and the issue still exists when the component test does not use the real template.

    fixture.detectChanges() does fix the remapped branch/function coverage when the actual component template is compiled and executed. But in my case I intentionally override the template in the spec, because I only want to unit test the TypeScript behavior and I do not want HTML/template assertions in these tests.

    I also cannot reliably use the real template here because it pulls in Custom web components, and there are known jsdom limitations around that stack. So for this class of component tests, using the real template is not a practical workaround.

    What I am still seeing is:

    • real template + fixture.detectChanges() => branch coverage goes to 100%
    • blank/fake overridden template => line coverage is fine, but branch/function coverage is still remapped to the component class and stays below 100%

    So from a user perspective, the issue is still effectively present for TypeScript-only component tests that intentionally avoid the real template.

    Reacted by Daniel Morrison, Hayley Price, Maartje Kanaar, rexsuecia and Olivier Braud
  16. alan-agius4
    added
    area: @angular/build
    angular/build:unit-test
    and removed
    area: @angular/cli
    on Apr 28, 2026
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

    angular/build:unit-testarea: @angular/buildgemini-triagedLabel noting that an issue has been triaged by geminiLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

      Milestone

      • needsTriageNo due date

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.