From 6aa8648dbb00e9f47d0a0fdcb1e36b616a74b948 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:44:27 -0400 Subject: [PATCH] fix(@angular/build): ensure @angular/compiler is loaded for Vitest TestBed initialization Provide a dedicated setup file to import '@angular/compiler' prior to TestBed initialization. This ensures the compiler facade is published before 'init-testbed.js' or any shared code-split chunks (such as those configured via 'providersFile') are evaluated, avoiding JIT compilation errors when injectable dependencies are resolved during test initialization. --- .../unit-test/runners/vitest/build-options.ts | 5 ++ .../unit-test/runners/vitest/executor.ts | 7 ++- .../tests/options/providers-file_spec.ts | 54 +++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts index b3a7d8f37662..efff72e5d8bb 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts @@ -230,6 +230,10 @@ export async function getVitestBuildOptions( } } + // The Angular compiler facade must be loaded in a dedicated setup file before TestBed initialization. + // This ensures the compiler facade is published before 'init-testbed' or any shared code-split chunks + // (such as those from a configured 'providersFile') are evaluated. + entryPoints.set('init-compiler', 'angular:compiler-init'); entryPoints.set('init-testbed', 'angular:test-bed-init'); entryPoints.set('vitest-mock-patch', 'angular:vitest-mock-patch'); @@ -326,6 +330,7 @@ export async function getVitestBuildOptions( return { buildOptions, virtualFiles: { + 'angular:compiler-init': "import '@angular/compiler';", 'angular:test-bed-init': testBedInitContents, 'angular:vitest-mock-patch': mockPatchContents, }, diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 55003b4fc82a..aa201e24c578 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -243,7 +243,12 @@ export class VitestExecutor implements TestExecutor { private prepareSetupFiles(): string[] { const { setupFiles } = this.options; // Add setup file entries for TestBed initialization and project polyfills - const testSetupFiles = ['init-testbed.js', 'vitest-mock-patch.js', ...setupFiles]; + const testSetupFiles = [ + 'init-compiler.js', + 'init-testbed.js', + 'vitest-mock-patch.js', + ...setupFiles, + ]; // TODO: Provide additional result metadata to avoid needing to extract based on filename if (this.buildResultFiles.has('polyfills.js')) { diff --git a/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts index d69f6480c54d..6485975270e8 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts @@ -60,5 +60,59 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); }); + + it('should work with providers that inject services requiring JIT compilation', async () => { + await harness.writeFiles({ + 'src/test.service.ts': ` + import { Injectable, inject } from '@angular/core'; + import { Router } from '@angular/router'; + + @Injectable({ providedIn: 'root' }) + export class TestService { + router = inject(Router); + } + `, + 'src/test.providers.ts': ` + import { TestService } from './test.service'; + export default [TestService]; + `, + 'src/app/app.component.spec.ts': ` + import { TestBed } from '@angular/core/testing'; + import { AppComponent } from './app.component'; + import { TestService } from '../test.service'; + import { describe, expect, it } from 'vitest'; + + describe('AppComponent', () => { + it('should create the app and inject TestService', () => { + TestBed.configureTestingModule({ + declarations: [AppComponent], + }); + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.componentInstance; + const testService = TestBed.inject(TestService); + expect(app).toBeTruthy(); + expect(testService).toBeTruthy(); + expect(testService.router).toBeTruthy(); + }); + }); + `, + }); + + await harness.modifyFile('src/tsconfig.spec.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('test.service.ts', 'test.providers.ts'); + + return JSON.stringify(tsConfig); + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + providersFile: 'src/test.providers.ts', + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); }); });