@@ -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,42 @@ 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+
130+ return f . pattern . endsWith ( 'karma-jasmine/lib/boot.js' ) ;
131+ } ) ;
132+ if ( jasmineBootIndex === - 1 ) {
133+ // Insert after polyfills.
134+ files . splice ( 1 , 0 , zoneTestingFile ) ;
135+ } else {
136+ files . splice ( jasmineBootIndex + 1 , 0 , zoneTestingFile ) ;
137+ }
138+ }
139+ } , AngularPolyfillsFramework ) ,
140+ ] ,
141+ } ;
142+ }
143+ }
144+
109145function injectKarmaReporter (
110146 buildOptions : BuildOptions ,
111147 buildIterator : AsyncIterator < Result > ,
@@ -247,12 +283,20 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
247283 return path . join ( context . workspaceRoot , sourceRoot ) ;
248284}
249285
250- function normalizePolyfills ( polyfills : string | string [ ] | undefined ) : string [ ] {
286+ function normalizePolyfills ( polyfills : string | string [ ] | undefined ) : [ string [ ] , string | null ] {
251287 if ( typeof polyfills === 'string' ) {
252- return [ polyfills ] ;
288+ polyfills = [ polyfills ] ;
289+ } else if ( ! polyfills ) {
290+ polyfills = [ ] ;
253291 }
254292
255- return polyfills ?? [ ] ;
293+ const zoneTestingEntryPoint = 'zone.js/testing' ;
294+ const polyfillsExludingZoneTesting = polyfills . filter ( ( p ) => p !== zoneTestingEntryPoint ) ;
295+
296+ return [
297+ polyfillsExludingZoneTesting ,
298+ polyfillsExludingZoneTesting . length === polyfills . length ? null : zoneTestingEntryPoint ,
299+ ] ;
256300}
257301
258302async function collectEntrypoints (
@@ -311,6 +355,11 @@ async function initializeApplication(
311355 )
312356 : undefined ;
313357
358+ const [ polyfills , zoneTesting ] = normalizePolyfills ( options . polyfills ) ;
359+ if ( zoneTesting ) {
360+ entryPoints . set ( 'zone-testing' , zoneTesting ) ;
361+ }
362+
314363 const buildOptions : BuildOptions = {
315364 assets : options . assets ,
316365 entryPoints,
@@ -327,7 +376,7 @@ async function initializeApplication(
327376 } ,
328377 instrumentForCoverage,
329378 styles : options . styles ,
330- polyfills : normalizePolyfills ( options . polyfills ) ,
379+ polyfills,
331380 webWorkerTsConfig : options . webWorkerTsConfig ,
332381 watch : options . watch ?? ! karmaOptions . singleRun ,
333382 stylePreprocessorOptions : options . stylePreprocessorOptions ,
@@ -349,10 +398,26 @@ async function initializeApplication(
349398 // Write test files
350399 await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
351400
401+ // We need to add this to the beginning *after* the testing framework has
402+ // prepended its files.
403+ const polyfillsFile : FilePattern = {
404+ pattern : `${ outputPath } /polyfills.js` ,
405+ included : true ,
406+ served : true ,
407+ watched : false ,
408+ } ;
409+ const zoneTestingFile : FilePattern | null = zoneTesting
410+ ? {
411+ pattern : `${ outputPath } /zone-testing.js` ,
412+ included : true ,
413+ served : true ,
414+ type : 'module' ,
415+ watched : false ,
416+ }
417+ : null ;
418+
352419 karmaOptions . files ??= [ ] ;
353420 karmaOptions . files . push (
354- // Serve polyfills first.
355- { pattern : `${ outputPath } /polyfills.js` , type : 'module' , watched : false } ,
356421 // Serve global setup script.
357422 { pattern : `${ outputPath } /${ mainName } .js` , type : 'module' , watched : false } ,
358423 // Serve all source maps.
@@ -413,6 +478,11 @@ async function initializeApplication(
413478 parsedKarmaConfig . middleware ??= [ ] ;
414479 parsedKarmaConfig . middleware . push ( AngularAssetsMiddleware . NAME ) ;
415480
481+ parsedKarmaConfig . plugins . push (
482+ AngularPolyfillsFramework . createPlugin ( polyfillsFile , zoneTestingFile ) ,
483+ ) ;
484+ parsedKarmaConfig . frameworks . push ( AngularPolyfillsFramework . NAME ) ;
485+
416486 // When using code-coverage, auto-add karma-coverage.
417487 // This was done as part of the karma plugin for webpack.
418488 if (
0 commit comments