@@ -20,7 +20,7 @@ import { randomUUID } from 'crypto';
2020import glob from 'fast-glob' ;
2121import * as fs from 'fs/promises' ;
2222import { IncomingMessage , ServerResponse } from 'http' ;
23- import type { Config , ConfigOptions , InlinePluginDef } from 'karma' ;
23+ import type { Config , ConfigOptions , FilePattern , InlinePluginDef } from 'karma' ;
2424import * as path from 'path' ;
2525import { Observable , Subscriber , catchError , defaultIfEmpty , from , of , switchMap } from 'rxjs' ;
2626import { Configuration } from 'webpack' ;
@@ -106,6 +106,41 @@ class AngularAssetsMiddleware {
106106 }
107107}
108108
109+ class AngularPolyfillsFramework {
110+ static readonly $inject = [ 'config.files' ] ;
111+
112+ static readonly NAME = 'angular-test-polyfills' ;
113+
114+ static createPlugin (
115+ polyfillsFile : FilePattern ,
116+ zoneTestingFile : FilePattern | null ,
117+ ) : InlinePluginDef {
118+ return {
119+ [ `framework:${ AngularPolyfillsFramework . NAME } ` ] : [
120+ 'factory' ,
121+ Object . assign ( ( files : ( string | FilePattern ) [ ] ) => {
122+ // The correct order is zone.js -> jasmine -> zone.js/testing.
123+ files . unshift ( polyfillsFile ) ;
124+ if ( zoneTestingFile ) {
125+ const jasmineBootIndex = files . findIndex ( ( f ) => {
126+ if ( typeof f === 'string' ) {
127+ return false ;
128+ }
129+ return f . pattern . endsWith ( 'karma-jasmine/lib/boot.js' ) ;
130+ } ) ;
131+ if ( jasmineBootIndex === - 1 ) {
132+ // Insert after polyfills.
133+ files . splice ( 1 , 0 , zoneTestingFile ) ;
134+ } else {
135+ files . splice ( jasmineBootIndex + 1 , 0 , zoneTestingFile ) ;
136+ }
137+ }
138+ } , AngularPolyfillsFramework ) ,
139+ ] ,
140+ } ;
141+ }
142+ }
143+
109144function injectKarmaReporter (
110145 buildOptions : BuildOptions ,
111146 buildIterator : AsyncIterator < Result > ,
@@ -247,12 +282,20 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
247282 return path . join ( context . workspaceRoot , sourceRoot ) ;
248283}
249284
250- function normalizePolyfills ( polyfills : string | string [ ] | undefined ) : string [ ] {
285+ function normalizePolyfills ( polyfills : string | string [ ] | undefined ) : [ string [ ] , string | null ] {
251286 if ( typeof polyfills === 'string' ) {
252- return [ polyfills ] ;
287+ polyfills = [ polyfills ] ;
288+ } else if ( ! polyfills ) {
289+ polyfills = [ ] ;
253290 }
254291
255- return polyfills ?? [ ] ;
292+ const zoneTestingEntryPoint = 'zone.js/testing' ;
293+ const polyfillsExludingZoneTesting = polyfills . filter ( ( p ) => p !== zoneTestingEntryPoint ) ;
294+
295+ return [
296+ polyfillsExludingZoneTesting ,
297+ polyfillsExludingZoneTesting . length === polyfills . length ? null : zoneTestingEntryPoint ,
298+ ] ;
256299}
257300
258301async function collectEntrypoints (
@@ -311,6 +354,11 @@ async function initializeApplication(
311354 )
312355 : undefined ;
313356
357+ const [ polyfills , zoneTesting ] = normalizePolyfills ( options . polyfills ) ;
358+ if ( zoneTesting ) {
359+ entryPoints . set ( 'zone-testing' , zoneTesting ) ;
360+ }
361+
314362 const buildOptions : BuildOptions = {
315363 assets : options . assets ,
316364 entryPoints,
@@ -327,7 +375,7 @@ async function initializeApplication(
327375 } ,
328376 instrumentForCoverage,
329377 styles : options . styles ,
330- polyfills : normalizePolyfills ( options . polyfills ) ,
378+ polyfills,
331379 webWorkerTsConfig : options . webWorkerTsConfig ,
332380 watch : options . watch ?? ! karmaOptions . singleRun ,
333381 stylePreprocessorOptions : options . stylePreprocessorOptions ,
@@ -349,10 +397,26 @@ async function initializeApplication(
349397 // Write test files
350398 await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
351399
400+ // We need to add this to the beginning *after* the testing framework has
401+ // prepended its files.
402+ const polyfillsFile : FilePattern = {
403+ pattern : `${ outputPath } /polyfills.js` ,
404+ included : true ,
405+ served : true ,
406+ watched : false ,
407+ } ;
408+ const zoneTestingFile : FilePattern | null = zoneTesting
409+ ? {
410+ pattern : `${ outputPath } /zone-testing.js` ,
411+ included : true ,
412+ served : true ,
413+ type : 'module' ,
414+ watched : false ,
415+ }
416+ : null ;
417+
352418 karmaOptions . files ??= [ ] ;
353419 karmaOptions . files . push (
354- // Serve polyfills first.
355- { pattern : `${ outputPath } /polyfills.js` , type : 'module' , watched : false } ,
356420 // Serve global setup script.
357421 { pattern : `${ outputPath } /${ mainName } .js` , type : 'module' , watched : false } ,
358422 // Serve all source maps.
@@ -413,6 +477,11 @@ async function initializeApplication(
413477 parsedKarmaConfig . middleware ??= [ ] ;
414478 parsedKarmaConfig . middleware . push ( AngularAssetsMiddleware . NAME ) ;
415479
480+ parsedKarmaConfig . plugins . push (
481+ AngularPolyfillsFramework . createPlugin ( polyfillsFile , zoneTestingFile ) ,
482+ ) ;
483+ parsedKarmaConfig . frameworks . push ( AngularPolyfillsFramework . NAME ) ;
484+
416485 // When using code-coverage, auto-add karma-coverage.
417486 // This was done as part of the karma plugin for webpack.
418487 if (
0 commit comments