@@ -103,6 +103,57 @@ class AngularAssetsMiddleware {
103103 } ;
104104 }
105105}
106+ class AngularPolyfillsPlugin {
107+ static $inject = [ 'config.files' ] ;
108+ static NAME = 'angular-polyfills' ;
109+ static createPlugin ( polyfillsFile , jasmineCleanupFiles ) {
110+ return {
111+ // This has to be a "reporter" because reporters run _after_ frameworks
112+ // and karma-jasmine-html-reporter injects additional scripts that may
113+ // depend on Jasmine but aren't modules - which means that they would run
114+ // _before_ all module code (including jasmine).
115+ [ `reporter:${ AngularPolyfillsPlugin . NAME } ` ] : [
116+ 'factory' ,
117+ Object . assign ( ( files ) => {
118+ // The correct order is zone.js -> jasmine -> zone.js/testing.
119+ // Jasmine has to see the patched version of the global `setTimeout`
120+ // function so it doesn't cache the unpatched version. And /testing
121+ // needs to see the global `jasmine` object so it can patch it.
122+ const polyfillsIndex = 0 ;
123+ files . splice ( polyfillsIndex , 0 , polyfillsFile ) ;
124+ // Insert just before test_main.js.
125+ const zoneTestingIndex = files . findIndex ( ( f ) => {
126+ if ( typeof f === 'string' ) {
127+ return false ;
128+ }
129+ return f . pattern . endsWith ( '/test_main.js' ) ;
130+ } ) ;
131+ if ( zoneTestingIndex === - 1 ) {
132+ throw new Error ( 'Could not find test entrypoint file.' ) ;
133+ }
134+ files . splice ( zoneTestingIndex , 0 , jasmineCleanupFiles ) ;
135+ // We need to ensure that all files are served as modules, otherwise
136+ // the order in the files list gets really confusing: Karma doesn't
137+ // set defer on scripts, so all scripts with type=js will run first,
138+ // even if type=module files appeared earlier in `files`.
139+ for ( const f of files ) {
140+ if ( typeof f === 'string' ) {
141+ throw new Error ( `Unexpected string-based file: "${ f } "` ) ;
142+ }
143+ if ( f . included === false ) {
144+ // Don't worry about files that aren't included on the initial
145+ // page load. `type` won't affect them.
146+ continue ;
147+ }
148+ if ( 'js' === ( f . type ?? 'js' ) ) {
149+ f . type = 'module' ;
150+ }
151+ }
152+ } , AngularPolyfillsPlugin ) ,
153+ ] ,
154+ } ;
155+ }
156+ }
106157function injectKarmaReporter ( buildOptions , buildIterator , karmaConfig , subscriber ) {
107158 const reporterName = 'angular-progress-notifier' ;
108159 class ProgressNotifierReporter {
@@ -199,9 +250,21 @@ async function getProjectSourceRoot(context) {
199250}
200251function normalizePolyfills ( polyfills ) {
201252 if ( typeof polyfills === 'string' ) {
202- return [ polyfills ] ;
253+ polyfills = [ polyfills ] ;
203254 }
204- return polyfills ?? [ ] ;
255+ else if ( ! polyfills ) {
256+ polyfills = [ ] ;
257+ }
258+ const jasmineGlobalEntryPoint = '@angular-devkit/build-angular/src/builders/karma/jasmine_global.js' ;
259+ const jasmineGlobalCleanupEntrypoint = '@angular-devkit/build-angular/src/builders/karma/jasmine_global_cleanup.js' ;
260+ const zoneTestingEntryPoint = 'zone.js/testing' ;
261+ const polyfillsExludingZoneTesting = polyfills . filter ( ( p ) => p !== zoneTestingEntryPoint ) ;
262+ return [
263+ polyfillsExludingZoneTesting . concat ( [ jasmineGlobalEntryPoint ] ) ,
264+ polyfillsExludingZoneTesting . length === polyfills . length
265+ ? [ jasmineGlobalCleanupEntrypoint ]
266+ : [ jasmineGlobalCleanupEntrypoint , zoneTestingEntryPoint ] ,
267+ ] ;
205268}
206269async function collectEntrypoints ( options , context , projectSourceRoot ) {
207270 // Glob for files to test.
@@ -229,6 +292,10 @@ async function initializeApplication(options, context, karmaOptions, transforms
229292 const instrumentForCoverage = options . codeCoverage
230293 ? createInstrumentationFilter ( projectSourceRoot , getInstrumentationExcludedPaths ( context . workspaceRoot , options . codeCoverageExclude ?? [ ] ) )
231294 : undefined ;
295+ const [ polyfills , jasmineCleanup ] = normalizePolyfills ( options . polyfills ) ;
296+ for ( let idx = 0 ; idx < jasmineCleanup . length ; ++ idx ) {
297+ entryPoints . set ( `jasmine-cleanup-${ idx } ` , jasmineCleanup [ idx ] ) ;
298+ }
232299 const buildOptions = {
233300 assets : options . assets ,
234301 entryPoints,
@@ -245,7 +312,7 @@ async function initializeApplication(options, context, karmaOptions, transforms
245312 } ,
246313 instrumentForCoverage,
247314 styles : options . styles ,
248- polyfills : normalizePolyfills ( options . polyfills ) ,
315+ polyfills,
249316 webWorkerTsConfig : options . webWorkerTsConfig ,
250317 watch : options . watch ?? ! karmaOptions . singleRun ,
251318 stylePreprocessorOptions : options . stylePreprocessorOptions ,
@@ -260,10 +327,24 @@ async function initializeApplication(options, context, karmaOptions, transforms
260327 }
261328 // Write test files
262329 await writeTestFiles ( buildOutput . files , buildOptions . outputPath ) ;
330+ // We need to add this to the beginning *after* the testing framework has
331+ // prepended its files.
332+ const polyfillsFile = {
333+ pattern : `${ outputPath } /polyfills.js` ,
334+ included : true ,
335+ served : true ,
336+ type : 'module' ,
337+ watched : false ,
338+ } ;
339+ const jasmineCleanupFiles = {
340+ pattern : `${ outputPath } /jasmine-cleanup-*.js` ,
341+ included : true ,
342+ served : true ,
343+ type : 'module' ,
344+ watched : false ,
345+ } ;
263346 karmaOptions . files ??= [ ] ;
264347 karmaOptions . files . push (
265- // Serve polyfills first.
266- { pattern : `${ outputPath } /polyfills.js` , type : 'module' , watched : false } ,
267348 // Serve global setup script.
268349 { pattern : `${ outputPath } /${ mainName } .js` , type : 'module' , watched : false } ,
269350 // Serve all source maps.
@@ -305,6 +386,9 @@ async function initializeApplication(options, context, karmaOptions, transforms
305386 parsedKarmaConfig . plugins . push ( AngularAssetsMiddleware . createPlugin ( buildOutput ) ) ;
306387 parsedKarmaConfig . middleware ??= [ ] ;
307388 parsedKarmaConfig . middleware . push ( AngularAssetsMiddleware . NAME ) ;
389+ parsedKarmaConfig . plugins . push ( AngularPolyfillsPlugin . createPlugin ( polyfillsFile , jasmineCleanupFiles ) ) ;
390+ parsedKarmaConfig . reporters ??= [ ] ;
391+ parsedKarmaConfig . reporters . push ( AngularPolyfillsPlugin . NAME ) ;
308392 // When using code-coverage, auto-add karma-coverage.
309393 // This was done as part of the karma plugin for webpack.
310394 if ( options . codeCoverage &&
0 commit comments