@@ -116,9 +116,153 @@ export class VitestExecutor implements TestExecutor {
116
116
await this . vitest ?. close ( ) ;
117
117
}
118
118
119
+ private prepareSetupFiles ( ) : string [ ] {
120
+ const { setupFiles } = this . options ;
121
+ // Add setup file entries for TestBed initialization and project polyfills
122
+ const testSetupFiles = [ 'init-testbed.js' , ...setupFiles ] ;
123
+
124
+ // TODO: Provide additional result metadata to avoid needing to extract based on filename
125
+ if ( this . buildResultFiles . has ( 'polyfills.js' ) ) {
126
+ testSetupFiles . unshift ( 'polyfills.js' ) ;
127
+ }
128
+
129
+ return testSetupFiles ;
130
+ }
131
+
132
+ private createVitestPlugins (
133
+ testSetupFiles : string [ ] ,
134
+ browserOptions : Awaited < ReturnType < typeof setupBrowserConfiguration > > ,
135
+ ) : NonNullable < import ( 'vite' ) . PluginOption > [ ] {
136
+ const { workspaceRoot, codeCoverage } = this . options ;
137
+
138
+ return [
139
+ {
140
+ name : 'angular:project-init' ,
141
+ // Type is incorrect. This allows a Promise<void>.
142
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
143
+ configureVitest : async ( context ) => {
144
+ // Create a subproject that can be configured with plugins for browser mode.
145
+ // Plugins defined directly in the vite overrides will not be present in the
146
+ // browser specific Vite instance.
147
+ const [ project ] = await context . injectTestProjects ( {
148
+ test : {
149
+ name : this . projectName ,
150
+ root : workspaceRoot ,
151
+ globals : true ,
152
+ setupFiles : testSetupFiles ,
153
+ // Use `jsdom` if no browsers are explicitly configured.
154
+ // `node` is effectively no "environment" and the default.
155
+ environment : browserOptions . browser ? 'node' : 'jsdom' ,
156
+ browser : browserOptions . browser ,
157
+ include : this . options . include ,
158
+ ...( this . options . exclude ? { exclude : this . options . exclude } : { } ) ,
159
+ } ,
160
+ plugins : [
161
+ {
162
+ name : 'angular:test-in-memory-provider' ,
163
+ enforce : 'pre' ,
164
+ resolveId : ( id , importer ) => {
165
+ if ( importer && id . startsWith ( '.' ) ) {
166
+ let fullPath ;
167
+ let relativePath ;
168
+ if ( this . testFileToEntryPoint . has ( importer ) ) {
169
+ fullPath = toPosixPath ( path . join ( this . options . workspaceRoot , id ) ) ;
170
+ relativePath = path . normalize ( id ) ;
171
+ } else {
172
+ fullPath = toPosixPath ( path . join ( path . dirname ( importer ) , id ) ) ;
173
+ relativePath = path . relative ( this . options . workspaceRoot , fullPath ) ;
174
+ }
175
+ if ( this . buildResultFiles . has ( toPosixPath ( relativePath ) ) ) {
176
+ return fullPath ;
177
+ }
178
+ }
179
+
180
+ if ( this . testFileToEntryPoint . has ( id ) ) {
181
+ return id ;
182
+ }
183
+
184
+ assert (
185
+ this . buildResultFiles . size > 0 ,
186
+ 'buildResult must be available for resolving.' ,
187
+ ) ;
188
+ const relativePath = path . relative ( this . options . workspaceRoot , id ) ;
189
+ if ( this . buildResultFiles . has ( toPosixPath ( relativePath ) ) ) {
190
+ return id ;
191
+ }
192
+ } ,
193
+ load : async ( id ) => {
194
+ assert (
195
+ this . buildResultFiles . size > 0 ,
196
+ 'buildResult must be available for in-memory loading.' ,
197
+ ) ;
198
+
199
+ // Attempt to load as a source test file.
200
+ const entryPoint = this . testFileToEntryPoint . get ( id ) ;
201
+ let outputPath ;
202
+ if ( entryPoint ) {
203
+ outputPath = entryPoint + '.js' ;
204
+ } else {
205
+ // Attempt to load as a built artifact.
206
+ const relativePath = path . relative ( this . options . workspaceRoot , id ) ;
207
+ outputPath = toPosixPath ( relativePath ) ;
208
+ }
209
+
210
+ const outputFile = this . buildResultFiles . get ( outputPath ) ;
211
+ if ( outputFile ) {
212
+ const sourceMapPath = outputPath + '.map' ;
213
+ const sourceMapFile = this . buildResultFiles . get ( sourceMapPath ) ;
214
+ const code =
215
+ outputFile . origin === 'memory'
216
+ ? Buffer . from ( outputFile . contents ) . toString ( 'utf-8' )
217
+ : await readFile ( outputFile . inputPath , 'utf-8' ) ;
218
+ const map = sourceMapFile
219
+ ? sourceMapFile . origin === 'memory'
220
+ ? Buffer . from ( sourceMapFile . contents ) . toString ( 'utf-8' )
221
+ : await readFile ( sourceMapFile . inputPath , 'utf-8' )
222
+ : undefined ;
223
+
224
+ return {
225
+ code,
226
+ map : map ? JSON . parse ( map ) : undefined ,
227
+ } ;
228
+ }
229
+ } ,
230
+ } ,
231
+ {
232
+ name : 'angular:html-index' ,
233
+ transformIndexHtml : ( ) => {
234
+ // Add all global stylesheets
235
+ if ( this . buildResultFiles . has ( 'styles.css' ) ) {
236
+ return [
237
+ {
238
+ tag : 'link' ,
239
+ attrs : { href : 'styles.css' , rel : 'stylesheet' } ,
240
+ injectTo : 'head' ,
241
+ } ,
242
+ ] ;
243
+ }
244
+
245
+ return [ ] ;
246
+ } ,
247
+ } ,
248
+ ] ,
249
+ } ) ;
250
+
251
+ // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests.
252
+ // Vite does this as a convenience but is problematic for the bundling strategy employed by the
253
+ // builder's test setup. To workaround this, the excludes are adjusted here to only automatically
254
+ // exclude the TypeScript source test files.
255
+ project . config . coverage . exclude = [
256
+ ...( codeCoverage ?. exclude ?? [ ] ) ,
257
+ '**/*.{test,spec}.?(c|m)ts' ,
258
+ ] ;
259
+ } ,
260
+ } ,
261
+ ] ;
262
+ }
263
+
119
264
private async initializeVitest ( ) : Promise < Vitest > {
120
- const { codeCoverage, reporters, workspaceRoot, setupFiles, browsers, debug, watch } =
121
- this . options ;
265
+ const { codeCoverage, reporters, workspaceRoot, browsers, debug, watch } = this . options ;
122
266
123
267
let vitestNodeModule ;
124
268
try {
@@ -148,13 +292,9 @@ export class VitestExecutor implements TestExecutor {
148
292
this . buildResultFiles . size > 0 ,
149
293
'buildResult must be available before initializing vitest' ,
150
294
) ;
151
- // Add setup file entries for TestBed initialization and project polyfills
152
- const testSetupFiles = [ 'init-testbed.js' , ...setupFiles ] ;
153
295
154
- // TODO: Provide additional result metadata to avoid needing to extract based on filename
155
- if ( this . buildResultFiles . has ( 'polyfills.js' ) ) {
156
- testSetupFiles . unshift ( 'polyfills.js' ) ;
157
- }
296
+ const testSetupFiles = this . prepareSetupFiles ( ) ;
297
+ const plugins = this . createVitestPlugins ( testSetupFiles , browserOptions ) ;
158
298
159
299
const debugOptions = debug
160
300
? {
@@ -185,130 +325,7 @@ export class VitestExecutor implements TestExecutor {
185
325
// be enabled as it controls other internal behavior related to rerunning tests.
186
326
watch : null ,
187
327
} ,
188
- plugins : [
189
- {
190
- name : 'angular:project-init' ,
191
- // Type is incorrect. This allows a Promise<void>.
192
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
193
- configureVitest : async ( context ) => {
194
- // Create a subproject that can be configured with plugins for browser mode.
195
- // Plugins defined directly in the vite overrides will not be present in the
196
- // browser specific Vite instance.
197
- const [ project ] = await context . injectTestProjects ( {
198
- test : {
199
- name : this . projectName ,
200
- root : workspaceRoot ,
201
- globals : true ,
202
- setupFiles : testSetupFiles ,
203
- // Use `jsdom` if no browsers are explicitly configured.
204
- // `node` is effectively no "environment" and the default.
205
- environment : browserOptions . browser ? 'node' : 'jsdom' ,
206
- browser : browserOptions . browser ,
207
- include : this . options . include ,
208
- ...( this . options . exclude ? { exclude : this . options . exclude } : { } ) ,
209
- } ,
210
- plugins : [
211
- {
212
- name : 'angular:test-in-memory-provider' ,
213
- enforce : 'pre' ,
214
- resolveId : ( id , importer ) => {
215
- if ( importer && id . startsWith ( '.' ) ) {
216
- let fullPath ;
217
- let relativePath ;
218
- if ( this . testFileToEntryPoint . has ( importer ) ) {
219
- fullPath = toPosixPath ( path . join ( this . options . workspaceRoot , id ) ) ;
220
- relativePath = path . normalize ( id ) ;
221
- } else {
222
- fullPath = toPosixPath ( path . join ( path . dirname ( importer ) , id ) ) ;
223
- relativePath = path . relative ( this . options . workspaceRoot , fullPath ) ;
224
- }
225
- if ( this . buildResultFiles . has ( toPosixPath ( relativePath ) ) ) {
226
- return fullPath ;
227
- }
228
- }
229
-
230
- if ( this . testFileToEntryPoint . has ( id ) ) {
231
- return id ;
232
- }
233
-
234
- assert (
235
- this . buildResultFiles . size > 0 ,
236
- 'buildResult must be available for resolving.' ,
237
- ) ;
238
- const relativePath = path . relative ( this . options . workspaceRoot , id ) ;
239
- if ( this . buildResultFiles . has ( toPosixPath ( relativePath ) ) ) {
240
- return id ;
241
- }
242
- } ,
243
- load : async ( id ) => {
244
- assert (
245
- this . buildResultFiles . size > 0 ,
246
- 'buildResult must be available for in-memory loading.' ,
247
- ) ;
248
-
249
- // Attempt to load as a source test file.
250
- const entryPoint = this . testFileToEntryPoint . get ( id ) ;
251
- let outputPath ;
252
- if ( entryPoint ) {
253
- outputPath = entryPoint + '.js' ;
254
- } else {
255
- // Attempt to load as a built artifact.
256
- const relativePath = path . relative ( this . options . workspaceRoot , id ) ;
257
- outputPath = toPosixPath ( relativePath ) ;
258
- }
259
-
260
- const outputFile = this . buildResultFiles . get ( outputPath ) ;
261
- if ( outputFile ) {
262
- const sourceMapPath = outputPath + '.map' ;
263
- const sourceMapFile = this . buildResultFiles . get ( sourceMapPath ) ;
264
- const code =
265
- outputFile . origin === 'memory'
266
- ? Buffer . from ( outputFile . contents ) . toString ( 'utf-8' )
267
- : await readFile ( outputFile . inputPath , 'utf-8' ) ;
268
- const map = sourceMapFile
269
- ? sourceMapFile . origin === 'memory'
270
- ? Buffer . from ( sourceMapFile . contents ) . toString ( 'utf-8' )
271
- : await readFile ( sourceMapFile . inputPath , 'utf-8' )
272
- : undefined ;
273
-
274
- return {
275
- code,
276
- map : map ? JSON . parse ( map ) : undefined ,
277
- } ;
278
- }
279
- } ,
280
- } ,
281
- {
282
- name : 'angular:html-index' ,
283
- transformIndexHtml : ( ) => {
284
- // Add all global stylesheets
285
- if ( this . buildResultFiles . has ( 'styles.css' ) ) {
286
- return [
287
- {
288
- tag : 'link' ,
289
- attrs : { href : 'styles.css' , rel : 'stylesheet' } ,
290
- injectTo : 'head' ,
291
- } ,
292
- ] ;
293
- }
294
-
295
- return [ ] ;
296
- } ,
297
- } ,
298
- ] ,
299
- } ) ;
300
-
301
- // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests.
302
- // Vite does this as a convenience but is problematic for the bundling strategy employed by the
303
- // builder's test setup. To workaround this, the excludes are adjusted here to only automatically
304
- // exclude the TypeScript source test files.
305
- project . config . coverage . exclude = [
306
- ...( codeCoverage ?. exclude ?? [ ] ) ,
307
- '**/*.{test,spec}.?(c|m)ts' ,
308
- ] ;
309
- } ,
310
- } ,
311
- ] ,
328
+ plugins,
312
329
} ,
313
330
) ;
314
331
}
0 commit comments