From faf32af2b7ecaf716cc9b7ae6c46104abe493a23 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:12:54 -0500 Subject: [PATCH] fix(@angular/build): explicitly fail when using Vitest runtime mocking The Angular unit-test builder pre-bundles tests, which prevents Vitest's runtime mocking features (like `vi.mock`) from working correctly as they rely on module graph manipulation that occurs before bundling. To prevent confusion and silent failures, a patch is now injected that causes `vi.mock` and related methods to throw a descriptive error explaining the limitation and suggesting the use of Angular TestBed for mocking instead. Fixes #31609 --- .../unit-test/runners/vitest/build-options.ts | 13 ++++++ .../unit-test/runners/vitest/executor.ts | 2 +- .../behavior/vitest-mock-unsupported_spec.ts | 41 +++++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/unit-test/tests/behavior/vitest-mock-unsupported_spec.ts 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 756037b1e390..3aa7e2c8947e 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 @@ -113,6 +113,7 @@ export async function getVitestBuildOptions( } entryPoints.set('init-testbed', 'angular:test-bed-init'); + entryPoints.set('vitest-mock-patch', 'angular:vitest-mock-patch'); // The 'vitest' package is always external for testing purposes const externalDependencies = ['vitest']; @@ -153,10 +154,22 @@ export async function getVitestBuildOptions( buildOptions.polyfills, ); + const mockPatchContents = ` + import { vi } from 'vitest'; + const error = new Error( + 'The "vi.mock" and related methods are not supported with the Angular unit-test system. Please use Angular TestBed for mocking.'); + vi.mock = () => { throw error; }; + vi.doMock = () => { throw error; }; + vi.importMock = () => { throw error; }; + vi.unmock = () => { throw error; }; + vi.doUnmock = () => { throw error; }; + `; + return { buildOptions, virtualFiles: { 'angular:test-bed-init': testBedInitContents, + 'angular:vitest-mock-patch': mockPatchContents, }, testEntryPointMappings: entryPoints, }; 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 6389a08e4e0e..503aa5da9071 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 @@ -147,7 +147,7 @@ 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', ...setupFiles]; + const testSetupFiles = ['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/behavior/vitest-mock-unsupported_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-mock-unsupported_spec.ts new file mode 100644 index 000000000000..30565429f2ca --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/behavior/vitest-mock-unsupported_spec.ts @@ -0,0 +1,41 @@ +import { execute } from '../../index'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Behavior: "Vitest mocking unsupported"', () => { + beforeEach(() => { + setupApplicationTarget(harness); + }); + + it('should fail when vi.mock is used', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + harness.writeFile( + 'src/app/mock-throw.spec.ts', + ` + import { vi } from 'vitest'; + vi.mock('./something', () => ({})); + `, + ); + + // Overwrite default to avoid noise + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { describe, it, expect } from 'vitest'; + describe('Ignored', () => { it('pass', () => expect(true).toBe(true)); }); + `, + ); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeFalse(); + }); + }); +});