@@ -12,7 +12,7 @@ import { randomUUID } from 'node:crypto';
1212import * as fs from 'node:fs/promises' ;
1313import type { IncomingMessage , ServerResponse } from 'node:http' ;
1414import { createRequire } from 'node:module' ;
15- import * as path from 'node:path' ;
15+ import path from 'node:path' ;
1616import { ReadableStreamController } from 'node:stream/web' ;
1717import { globSync } from 'tinyglobby' ;
1818import { BuildOutputFileType } from '../../tools/esbuild/bundler-context' ;
@@ -24,7 +24,9 @@ import { ApplicationBuilderInternalOptions } from '../application/options';
2424import { Result , ResultFile , ResultKind } from '../application/results' ;
2525import { OutputHashing } from '../application/schema' ;
2626import { findTests , getTestEntrypoints } from './find-tests' ;
27- import { NormalizedKarmaBuilderOptions } from './options' ;
27+ import { NormalizedKarmaBuilderOptions , normalizeOptions } from './options' ;
28+ import { Schema as KarmaBuilderOptions } from './schema' ;
29+ import type { KarmaBuilderTransformsOptions } from './index' ;
2830
2931const localResolve = createRequire ( __filename ) . resolve ;
3032const isWindows = process . platform === 'win32' ;
@@ -275,21 +277,20 @@ function injectKarmaReporter(
275277}
276278
277279export function execute (
278- options : NormalizedKarmaBuilderOptions ,
280+ options : KarmaBuilderOptions ,
279281 context : BuilderContext ,
280- karmaOptions : ConfigOptions ,
281- transforms : {
282- // The karma options transform cannot be async without a refactor of the builder implementation
283- karmaOptions ?: ( options : ConfigOptions ) => ConfigOptions ;
284- } = { } ,
282+ transforms ?: KarmaBuilderTransformsOptions ,
285283) : AsyncIterable < BuilderOutput > {
284+ const normalizedOptions = normalizeOptions ( context , options ) ;
285+ const karmaOptions = getBaseKarmaOptions ( normalizedOptions , context ) ;
286+
286287 let karmaServer : Server ;
287288
288289 return new ReadableStream ( {
289290 async start ( controller ) {
290291 let init ;
291292 try {
292- init = await initializeApplication ( options , context , karmaOptions , transforms ) ;
293+ init = await initializeApplication ( normalizedOptions , context , karmaOptions , transforms ) ;
293294 } catch ( err ) {
294295 if ( err instanceof ApplicationBuildError ) {
295296 controller . enqueue ( { success : false , message : err . message } ) ;
@@ -336,13 +337,9 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
336337 return projectSourceRoot ;
337338}
338339
339- function normalizePolyfills ( polyfills : string | string [ ] | undefined ) : [ string [ ] , string [ ] ] {
340- if ( typeof polyfills === 'string' ) {
341- polyfills = [ polyfills ] ;
342- } else if ( ! polyfills ) {
343- polyfills = [ ] ;
344- }
345-
340+ function normalizePolyfills (
341+ polyfills : string [ ] = [ ] ,
342+ ) : [ polyfills : string [ ] , jasmineCleanup : string [ ] ] {
346343 const jasmineGlobalEntryPoint = localResolve ( './polyfills/jasmine_global.js' ) ;
347344 const jasmineGlobalCleanupEntrypoint = localResolve ( './polyfills/jasmine_global_cleanup.js' ) ;
348345 const sourcemapEntrypoint = localResolve ( './polyfills/init_sourcemaps.js' ) ;
@@ -379,9 +376,7 @@ async function initializeApplication(
379376 options : NormalizedKarmaBuilderOptions ,
380377 context : BuilderContext ,
381378 karmaOptions : ConfigOptions ,
382- transforms : {
383- karmaOptions ?: ( options : ConfigOptions ) => ConfigOptions ;
384- } = { } ,
379+ transforms ?: KarmaBuilderTransformsOptions ,
385380) : Promise <
386381 [ typeof import ( 'karma' ) , Config & ConfigOptions , BuildOptions , AsyncIterator < Result > | null ]
387382> {
@@ -423,13 +418,7 @@ async function initializeApplication(
423418 index : false ,
424419 outputHashing : OutputHashing . None ,
425420 optimization : false ,
426- sourceMap : options . codeCoverage
427- ? {
428- scripts : true ,
429- styles : true ,
430- vendor : true ,
431- }
432- : options . sourceMap ,
421+ sourceMap : options . sourceMap ,
433422 instrumentForCoverage,
434423 styles : options . styles ,
435424 scripts : options . scripts ,
@@ -551,8 +540,8 @@ async function initializeApplication(
551540 }
552541
553542 const parsedKarmaConfig : Config & ConfigOptions = await karma . config . parseConfig (
554- options . karmaConfig && path . resolve ( context . workspaceRoot , options . karmaConfig ) ,
555- transforms . karmaOptions ? transforms . karmaOptions ( karmaOptions ) : karmaOptions ,
543+ options . karmaConfig ,
544+ transforms ? .karmaOptions ? await transforms . karmaOptions ( karmaOptions ) : karmaOptions ,
556545 { promiseConfig : true , throwErrors : true } ,
557546 ) ;
558547
@@ -718,3 +707,82 @@ function getInstrumentationExcludedPaths(root: string, excludedPaths: string[]):
718707
719708 return excluded ;
720709}
710+ function getBaseKarmaOptions (
711+ options : NormalizedKarmaBuilderOptions ,
712+ context : BuilderContext ,
713+ ) : ConfigOptions {
714+ // Determine project name from builder context target
715+ const projectName = context . target ?. project ;
716+ if ( ! projectName ) {
717+ throw new Error ( `The 'karma' builder requires a target to be specified.` ) ;
718+ }
719+
720+ const karmaOptions : ConfigOptions = options . karmaConfig
721+ ? { }
722+ : getBuiltInKarmaConfig ( context . workspaceRoot , projectName ) ;
723+
724+ const singleRun = ! options . watch ;
725+ karmaOptions . singleRun = singleRun ;
726+
727+ // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
728+ // for single run executions. Not clearing context for multi-run (watched) builds allows the
729+ // Jasmine Spec Runner to be visible in the browser after test execution.
730+ karmaOptions . client ??= { } ;
731+ karmaOptions . client . clearContext ??= singleRun ;
732+
733+ // Convert browsers from a string to an array
734+ if ( options . browsers ) {
735+ karmaOptions . browsers = options . browsers ;
736+ }
737+
738+ if ( options . reporters ) {
739+ karmaOptions . reporters = options . reporters ;
740+ }
741+
742+ return karmaOptions ;
743+ }
744+
745+ function getBuiltInKarmaConfig (
746+ workspaceRoot : string ,
747+ projectName : string ,
748+ ) : ConfigOptions & Record < string , unknown > {
749+ let coverageFolderName = projectName . charAt ( 0 ) === '@' ? projectName . slice ( 1 ) : projectName ;
750+ coverageFolderName = coverageFolderName . toLowerCase ( ) ;
751+
752+ const workspaceRootRequire = createRequire ( workspaceRoot + '/' ) ;
753+
754+ // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template
755+ return {
756+ basePath : '' ,
757+ frameworks : [ 'jasmine' ] ,
758+ plugins : [
759+ 'karma-jasmine' ,
760+ 'karma-chrome-launcher' ,
761+ 'karma-jasmine-html-reporter' ,
762+ 'karma-coverage' ,
763+ ] . map ( ( p ) => workspaceRootRequire ( p ) ) ,
764+ jasmineHtmlReporter : {
765+ suppressAll : true , // removes the duplicated traces
766+ } ,
767+ coverageReporter : {
768+ dir : path . join ( workspaceRoot , 'coverage' , coverageFolderName ) ,
769+ subdir : '.' ,
770+ reporters : [ { type : 'html' } , { type : 'text-summary' } ] ,
771+ } ,
772+ reporters : [ 'progress' , 'kjhtml' ] ,
773+ browsers : [ 'Chrome' ] ,
774+ customLaunchers : {
775+ // Chrome configured to run in a bazel sandbox.
776+ // Disable the use of the gpu and `/dev/shm` because it causes Chrome to
777+ // crash on some environments.
778+ // See:
779+ // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips
780+ // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t
781+ ChromeHeadlessNoSandbox : {
782+ base : 'ChromeHeadless' ,
783+ flags : [ '--no-sandbox' , '--headless' , '--disable-gpu' , '--disable-dev-shm-usage' ] ,
784+ } ,
785+ } ,
786+ restartOnFileChange : true ,
787+ } ;
788+ }
0 commit comments