@@ -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 { KarmaConfigOptions } from './index' ;
2830
2931const localResolve = createRequire ( __filename ) . resolve ;
3032const isWindows = process . platform === 'win32' ;
@@ -275,21 +277,23 @@ function injectKarmaReporter(
275277}
276278
277279export function execute (
278- options : NormalizedKarmaBuilderOptions ,
280+ options : KarmaBuilderOptions ,
279281 context : BuilderContext ,
280- karmaOptions : ConfigOptions ,
281282 transforms : {
282283 // The karma options transform cannot be async without a refactor of the builder implementation
283284 karmaOptions ?: ( options : ConfigOptions ) => ConfigOptions ;
284285 } = { } ,
285286) : AsyncIterable < BuilderOutput > {
287+ const normalizedOptions = normalizeOptions ( context , options ) ;
288+ const karmaOptions = getBaseKarmaOptions ( normalizedOptions , context ) ;
289+
286290 let karmaServer : Server ;
287291
288292 return new ReadableStream ( {
289293 async start ( controller ) {
290294 let init ;
291295 try {
292- init = await initializeApplication ( options , context , karmaOptions , transforms ) ;
296+ init = await initializeApplication ( normalizedOptions , context , karmaOptions , transforms ) ;
293297 } catch ( err ) {
294298 if ( err instanceof ApplicationBuildError ) {
295299 controller . enqueue ( { success : false , message : err . message } ) ;
@@ -336,13 +340,9 @@ async function getProjectSourceRoot(context: BuilderContext): Promise<string> {
336340 return projectSourceRoot ;
337341}
338342
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-
343+ function normalizePolyfills (
344+ polyfills : string [ ] | undefined = [ ] ,
345+ ) : [ polyfills : string [ ] , jasmineCleanup : string [ ] ] {
346346 const jasmineGlobalEntryPoint = localResolve ( './polyfills/jasmine_global.js' ) ;
347347 const jasmineGlobalCleanupEntrypoint = localResolve ( './polyfills/jasmine_global_cleanup.js' ) ;
348348 const sourcemapEntrypoint = localResolve ( './polyfills/init_sourcemaps.js' ) ;
@@ -423,13 +423,7 @@ async function initializeApplication(
423423 index : false ,
424424 outputHashing : OutputHashing . None ,
425425 optimization : false ,
426- sourceMap : options . codeCoverage
427- ? {
428- scripts : true ,
429- styles : true ,
430- vendor : true ,
431- }
432- : options . sourceMap ,
426+ sourceMap : options . sourceMap ,
433427 instrumentForCoverage,
434428 styles : options . styles ,
435429 scripts : options . scripts ,
@@ -551,7 +545,7 @@ async function initializeApplication(
551545 }
552546
553547 const parsedKarmaConfig : Config & ConfigOptions = await karma . config . parseConfig (
554- options . karmaConfig && path . resolve ( context . workspaceRoot , options . karmaConfig ) ,
548+ options . karmaConfig ,
555549 transforms . karmaOptions ? transforms . karmaOptions ( karmaOptions ) : karmaOptions ,
556550 { promiseConfig : true , throwErrors : true } ,
557551 ) ;
@@ -718,3 +712,84 @@ function getInstrumentationExcludedPaths(root: string, excludedPaths: string[]):
718712
719713 return excluded ;
720714}
715+ function getBaseKarmaOptions (
716+ options : NormalizedKarmaBuilderOptions ,
717+ context : BuilderContext ,
718+ ) : KarmaConfigOptions {
719+ const singleRun = ! options . watch ;
720+
721+ // Determine project name from builder context target
722+ const projectName = context . target ?. project ;
723+ if ( ! projectName ) {
724+ throw new Error ( `The 'karma' builder requires a target to be specified.` ) ;
725+ }
726+
727+ const karmaOptions : KarmaConfigOptions = options . karmaConfig
728+ ? { }
729+ : getBuiltInKarmaConfig ( context . workspaceRoot , projectName ) ;
730+
731+ karmaOptions . singleRun = singleRun ;
732+
733+ // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
734+ // for single run executions. Not clearing context for multi-run (watched) builds allows the
735+ // Jasmine Spec Runner to be visible in the browser after test execution.
736+ karmaOptions . client ??= { } ;
737+ karmaOptions . client . clearContext ??= singleRun ?? false ; // `singleRun` defaults to `false` per Karma docs.
738+
739+ // Convert browsers from a string to an array
740+ if ( options . browsers ) {
741+ karmaOptions . browsers = options . browsers ;
742+ }
743+
744+ if ( options . reporters ) {
745+ karmaOptions . reporters = options . reporters ;
746+ }
747+
748+ return karmaOptions ;
749+ }
750+
751+ function getBuiltInKarmaConfig (
752+ workspaceRoot : string ,
753+ projectName : string ,
754+ ) : ConfigOptions & Record < string , unknown > {
755+ let coverageFolderName = projectName . charAt ( 0 ) === '@' ? projectName . slice ( 1 ) : projectName ;
756+ coverageFolderName = coverageFolderName . toLowerCase ( ) ;
757+
758+ const workspaceRootRequire = createRequire ( workspaceRoot + '/' ) ;
759+
760+ // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template
761+ return {
762+ basePath : '' ,
763+ rootUrl : '/' ,
764+ frameworks : [ 'jasmine' ] ,
765+ plugins : [
766+ 'karma-jasmine' ,
767+ 'karma-chrome-launcher' ,
768+ 'karma-jasmine-html-reporter' ,
769+ 'karma-coverage' ,
770+ ] . map ( ( p ) => workspaceRootRequire ( p ) ) ,
771+ jasmineHtmlReporter : {
772+ suppressAll : true , // removes the duplicated traces
773+ } ,
774+ coverageReporter : {
775+ dir : path . join ( workspaceRoot , 'coverage' , coverageFolderName ) ,
776+ subdir : '.' ,
777+ reporters : [ { type : 'html' } , { type : 'text-summary' } ] ,
778+ } ,
779+ reporters : [ 'progress' , 'kjhtml' ] ,
780+ browsers : [ 'Chrome' ] ,
781+ customLaunchers : {
782+ // Chrome configured to run in a bazel sandbox.
783+ // Disable the use of the gpu and `/dev/shm` because it causes Chrome to
784+ // crash on some environments.
785+ // See:
786+ // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips
787+ // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t
788+ ChromeHeadlessNoSandbox : {
789+ base : 'ChromeHeadless' ,
790+ flags : [ '--no-sandbox' , '--headless' , '--disable-gpu' , '--disable-dev-shm-usage' ] ,
791+ } ,
792+ } ,
793+ restartOnFileChange : true ,
794+ } ;
795+ }
0 commit comments