66 * found in the LICENSE file at https://angular.dev/license
77 */
88
9+ /**
10+ * @fileoverview
11+ * Provides Vitest-specific build options and virtual file contents for Angular unit testing.
12+ */
13+
914import { createRequire } from 'node:module' ;
1015import path from 'node:path' ;
1116import { toPosixPath } from '../../../../utils/path' ;
@@ -15,6 +20,15 @@ import { NormalizedUnitTestBuilderOptions } from '../../options';
1520import { findTests , getTestEntrypoints } from '../../test-discovery' ;
1621import { RunnerOptions } from '../api' ;
1722
23+ /**
24+ * Creates the virtual file contents to initialize the Angular testing environment (TestBed).
25+ *
26+ * @param providersFile Optional path to a file that exports default providers.
27+ * @param projectSourceRoot The root directory of the project source.
28+ * @param teardown Whether to configure TestBed to destroy after each test.
29+ * @param zoneTestingStrategy How zone.js should be loaded during initialization.
30+ * @returns The string content of the virtual initialization file.
31+ */
1832function createTestBedInitVirtualFile (
1933 providersFile : string | undefined ,
2034 projectSourceRoot : string ,
@@ -29,6 +43,17 @@ function createTestBedInitVirtualFile(
2943 providersImport = `import providers from './${ importPath } ';` ;
3044 }
3145
46+ let zoneTestingSnippet = '' ;
47+ if ( zoneTestingStrategy === 'static' ) {
48+ zoneTestingSnippet = `import 'zone.js/testing';` ;
49+ } else if ( zoneTestingStrategy === 'dynamic' ) {
50+ zoneTestingSnippet = `if (typeof Zone !== 'undefined') {
51+ // 'zone.js/testing' is used to initialize the ZoneJS testing environment.
52+ // It must be imported dynamically to avoid a static dependency on 'zone.js'.
53+ await import('zone.js/testing');
54+ }` ;
55+ }
56+
3257 return `
3358 // Initialize the Angular testing environment
3459 import { NgModule, provideZoneChangeDetection } from '@angular/core';
@@ -37,18 +62,7 @@ function createTestBedInitVirtualFile(
3762 import { afterEach, beforeEach } from 'vitest';
3863 ${ providersImport }
3964
40- ${
41- zoneTestingStrategy === 'static'
42- ? `import 'zone.js/testing';`
43- : zoneTestingStrategy === 'dynamic'
44- ? `
45- if (typeof Zone !== 'undefined') {
46- // 'zone.js/testing' is used to initialize the ZoneJS testing environment.
47- // It must be imported dynamically to avoid a static dependency on 'zone.js'.
48- await import('zone.js/testing');
49- }`
50- : ''
51- }
65+ ${ zoneTestingSnippet }
5266
5367 // The beforeEach and afterEach hooks are registered outside the globalThis guard.
5468 // This ensures that the hooks are always applied, even in non-isolated browser environments.
@@ -81,6 +95,13 @@ function createTestBedInitVirtualFile(
8195 ` ;
8296}
8397
98+ /**
99+ * Adjusts output hashing settings for testing purposes. For example, ensuring media
100+ * is continued to be hashed to avoid overwriting assets, but turning off JavaScript hashing.
101+ *
102+ * @param hashing The original OutputHashing configuration.
103+ * @returns The adjusted OutputHashing configuration.
104+ */
84105function adjustOutputHashing ( hashing ?: OutputHashing ) : OutputHashing {
85106 switch ( hashing ) {
86107 case OutputHashing . All :
@@ -92,6 +113,45 @@ function adjustOutputHashing(hashing?: OutputHashing): OutputHashing {
92113 }
93114}
94115
116+ /**
117+ * Resolves the Zone.js testing strategy by inspecting polyfills and resolving zone.js package.
118+ *
119+ * @param buildOptions The partial application builder options.
120+ * @param projectSourceRoot The root directory of the project source.
121+ * @returns The resolved zone testing strategy ('none', 'static', 'dynamic').
122+ */
123+ function getZoneTestingStrategy (
124+ buildOptions : Partial < ApplicationBuilderInternalOptions > ,
125+ projectSourceRoot : string ,
126+ ) : 'none' | 'static' | 'dynamic' {
127+ if ( buildOptions . polyfills ?. includes ( 'zone.js/testing' ) ) {
128+ return 'none' ;
129+ }
130+
131+ if ( buildOptions . polyfills ?. includes ( 'zone.js' ) ) {
132+ return 'static' ;
133+ }
134+
135+ try {
136+ const projectRequire = createRequire ( path . join ( projectSourceRoot , 'package.json' ) ) ;
137+ projectRequire . resolve ( 'zone.js' ) ;
138+
139+ return 'dynamic' ;
140+ } catch {
141+ return 'none' ;
142+ }
143+ }
144+
145+ /**
146+ * Generates options and virtual files for the Vitest test runner.
147+ *
148+ * Discovers specs matchers, creates entry points, decides polyfills strategy, and orchestrates
149+ * internal ApplicationBuilder options.
150+ *
151+ * @param options The normalized unit test builder options.
152+ * @param baseBuildOptions The base build config to derive testing config from.
153+ * @returns An async RunnerOptions configuration.
154+ */
95155export async function getVitestBuildOptions (
96156 options : NormalizedUnitTestBuilderOptions ,
97157 baseBuildOptions : Partial < ApplicationBuilderInternalOptions > ,
@@ -162,20 +222,7 @@ export async function getVitestBuildOptions(
162222 } ;
163223
164224 // Inject the zone.js testing polyfill if Zone.js is installed.
165- let zoneTestingStrategy : 'none' | 'static' | 'dynamic' ;
166- if ( buildOptions . polyfills ?. includes ( 'zone.js/testing' ) ) {
167- zoneTestingStrategy = 'none' ;
168- } else if ( buildOptions . polyfills ?. includes ( 'zone.js' ) ) {
169- zoneTestingStrategy = 'static' ;
170- } else {
171- try {
172- const projectRequire = createRequire ( path . join ( projectSourceRoot , 'package.json' ) ) ;
173- projectRequire . resolve ( 'zone.js' ) ;
174- zoneTestingStrategy = 'dynamic' ;
175- } catch {
176- zoneTestingStrategy = 'none' ;
177- }
178- }
225+ const zoneTestingStrategy = getZoneTestingStrategy ( buildOptions , projectSourceRoot ) ;
179226
180227 const testBedInitContents = createTestBedInitVirtualFile (
181228 providersFile ,
0 commit comments