66 * found in the LICENSE file at https://angular.io/license
77 */
88import { BuilderContext , BuilderOutput , createBuilder } from '@angular-devkit/architect' ;
9- import { resolve } from 'path' ;
9+ import { experimental , getSystemPath , join } from '@angular-devkit/core' ;
10+ import { dirname , resolve } from 'path' ;
1011import { Observable , from } from 'rxjs' ;
1112import { defaultIfEmpty , switchMap } from 'rxjs/operators' ;
1213import * as webpack from 'webpack' ;
@@ -17,14 +18,19 @@ import {
1718 getTestConfig ,
1819 getWorkerConfig ,
1920} from '../angular-cli-files/models/webpack-configs' ;
21+ import {
22+ SingleTestTransformLoader ,
23+ SingleTestTransformLoaderOptions ,
24+ } from '../angular-cli-files/plugins/single-test-transform' ;
25+ import { findTests } from '../angular-cli-files/utilities/find-tests' ;
2026import { Schema as BrowserBuilderOptions } from '../browser/schema' ;
2127import { ExecutionTransformer } from '../transforms' ;
2228import { assertCompatibleAngularVersion } from '../utils/version' ;
2329import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config' ;
2430import { Schema as KarmaBuilderOptions } from './schema' ;
2531
2632// tslint:disable-next-line:no-implicit-dependencies
27- export type KarmaConfigOptions = import ( 'karma' ) . ConfigOptions & {
33+ export type KarmaConfigOptions = import ( 'karma' ) . ConfigOptions & {
2834 buildWebpack ?: unknown ;
2935 configFile ?: string ;
3036} ;
@@ -34,12 +40,12 @@ async function initialize(
3440 context : BuilderContext ,
3541 webpackConfigurationTransformer ?: ExecutionTransformer < webpack . Configuration > ,
3642 // tslint:disable-next-line:no-implicit-dependencies
37- ) : Promise < [ typeof import ( 'karma' ) , webpack . Configuration ] > {
38- const { config } = await generateBrowserWebpackConfigFromContext (
43+ ) : Promise < [ experimental . workspace . Workspace , typeof import ( 'karma' ) , webpack . Configuration ] > {
44+ const { config, workspace } = await generateBrowserWebpackConfigFromContext (
3945 // only two properties are missing:
4046 // * `outputPath` which is fixed for tests
4147 // * `budgets` which might be incorrect due to extra dev libs
42- { ...options as unknown as BrowserBuilderOptions , outputPath : '' , budgets : undefined } ,
48+ { ...( ( options as unknown ) as BrowserBuilderOptions ) , outputPath : '' , budgets : undefined } ,
4349 context ,
4450 wco => [
4551 getCommonConfig ( wco ) ,
@@ -54,6 +60,7 @@ async function initialize(
5460 const karma = await import ( 'karma' ) ;
5561
5662 return [
63+ workspace ,
5764 karma ,
5865 webpackConfigurationTransformer ? await webpackConfigurationTransformer ( config [ 0 ] ) : config [ 0 ] ,
5966 ] ;
@@ -63,71 +70,110 @@ export function execute(
6370 options : KarmaBuilderOptions ,
6471 context : BuilderContext ,
6572 transforms : {
66- webpackConfiguration ?: ExecutionTransformer < webpack . Configuration > ,
73+ webpackConfiguration ?: ExecutionTransformer < webpack . Configuration > ;
6774 // The karma options transform cannot be async without a refactor of the builder implementation
68- karmaOptions ?: ( options : KarmaConfigOptions ) => KarmaConfigOptions ,
75+ karmaOptions ?: ( options : KarmaConfigOptions ) => KarmaConfigOptions ;
6976 } = { } ,
7077) : Observable < BuilderOutput > {
7178 // Check Angular version.
7279 assertCompatibleAngularVersion ( context . workspaceRoot , context . logger ) ;
7380
7481 return from ( initialize ( options , context , transforms . webpackConfiguration ) ) . pipe (
75- switchMap ( ( [ karma , webpackConfig ] ) => new Observable < BuilderOutput > ( subscriber => {
76- const karmaOptions : KarmaConfigOptions = { } ;
77-
78- if ( options . watch !== undefined ) {
79- karmaOptions . singleRun = ! options . watch ;
80- }
81-
82- // Convert browsers from a string to an array
83- if ( options . browsers ) {
84- karmaOptions . browsers = options . browsers . split ( ',' ) ;
85- }
86-
87- if ( options . reporters ) {
88- // Split along commas to make it more natural, and remove empty strings.
89- const reporters = options . reporters
90- . reduce < string [ ] > ( ( acc , curr ) => acc . concat ( curr . split ( ',' ) ) , [ ] )
91- . filter ( x => ! ! x ) ;
92-
93- if ( reporters . length > 0 ) {
94- karmaOptions . reporters = reporters ;
95- }
96- }
97-
98- // Assign additional karmaConfig options to the local ngapp config
99- karmaOptions . configFile = resolve ( context . workspaceRoot , options . karmaConfig ) ;
100-
101- karmaOptions . buildWebpack = {
102- options,
103- webpackConfig,
104- // Pass onto Karma to emit BuildEvents.
105- successCb : ( ) => subscriber . next ( { success : true } ) ,
106- failureCb : ( ) => subscriber . next ( { success : false } ) ,
107- // Workaround for https://github.com/karma-runner/karma/issues/3154
108- // When this workaround is removed, user projects need to be updated to use a Karma
109- // version that has a fix for this issue.
110- toJSON : ( ) => { } ,
111- logger : context . logger ,
112- } ;
113-
114- // Complete the observable once the Karma server returns.
115- const karmaServer = new karma . Server (
116- transforms . karmaOptions ? transforms . karmaOptions ( karmaOptions ) : karmaOptions ,
117- ( ) => subscriber . complete ( ) ) ;
118- // karma typings incorrectly define start's return value as void
119- // tslint:disable-next-line:no-use-of-empty-return-value
120- const karmaStart = karmaServer . start ( ) as unknown as Promise < void > ;
121-
122- // Cleanup, signal Karma to exit.
123- return ( ) => {
124- // Karma only has the `stop` method start with 3.1.1, so we must defensively check.
125- const karmaServerWithStop = karmaServer as unknown as { stop : ( ) => Promise < void > } ;
126- if ( typeof karmaServerWithStop . stop === 'function' ) {
127- return karmaStart . then ( ( ) => karmaServerWithStop . stop ( ) ) ;
128- }
129- } ;
130- } ) ) ,
82+ switchMap (
83+ ( [ workspace , karma , webpackConfig ] ) =>
84+ new Observable < BuilderOutput > ( subscriber => {
85+ const karmaOptions : KarmaConfigOptions = { } ;
86+
87+ if ( options . watch !== undefined ) {
88+ karmaOptions . singleRun = ! options . watch ;
89+ }
90+
91+ // Convert browsers from a string to an array
92+ if ( options . browsers ) {
93+ karmaOptions . browsers = options . browsers . split ( ',' ) ;
94+ }
95+
96+ if ( options . reporters ) {
97+ // Split along commas to make it more natural, and remove empty strings.
98+ const reporters = options . reporters
99+ . reduce < string [ ] > ( ( acc , curr ) => acc . concat ( curr . split ( ',' ) ) , [ ] )
100+ . filter ( x => ! ! x ) ;
101+
102+ if ( reporters . length > 0 ) {
103+ karmaOptions . reporters = reporters ;
104+ }
105+ }
106+
107+ // prepend special webpack loader that will transform test.ts
108+ if (
109+ webpackConfig &&
110+ webpackConfig . module &&
111+ options . include &&
112+ options . include . length > 0
113+ ) {
114+ const mainFilePath = getSystemPath ( join ( workspace . root , options . main ) ) ;
115+ const files = findTests (
116+ options . include ,
117+ dirname ( mainFilePath ) ,
118+ getSystemPath ( workspace . root ) ,
119+ ) ;
120+ // early exit, no reason to start karma
121+ if ( ! files . length ) {
122+ subscriber . error (
123+ `Specified patterns: "${ options . include . join ( ', ' ) } " did not match any spec files` ,
124+ ) ;
125+
126+ return ;
127+ }
128+
129+ webpackConfig . module . rules . unshift ( {
130+ test : path => path === mainFilePath ,
131+ use : {
132+ // cannot be a simple path as it differs between environments
133+ loader : SingleTestTransformLoader ,
134+ options : {
135+ files,
136+ logger : context . logger ,
137+ } as SingleTestTransformLoaderOptions ,
138+ } ,
139+ } ) ;
140+ }
141+
142+ // Assign additional karmaConfig options to the local ngapp config
143+ karmaOptions . configFile = resolve ( context . workspaceRoot , options . karmaConfig ) ;
144+
145+ karmaOptions . buildWebpack = {
146+ options,
147+ webpackConfig,
148+ // Pass onto Karma to emit BuildEvents.
149+ successCb : ( ) => subscriber . next ( { success : true } ) ,
150+ failureCb : ( ) => subscriber . next ( { success : false } ) ,
151+ // Workaround for https://github.com/karma-runner/karma/issues/3154
152+ // When this workaround is removed, user projects need to be updated to use a Karma
153+ // version that has a fix for this issue.
154+ toJSON : ( ) => { } ,
155+ logger : context . logger ,
156+ } ;
157+
158+ // Complete the observable once the Karma server returns.
159+ const karmaServer = new karma . Server (
160+ transforms . karmaOptions ? transforms . karmaOptions ( karmaOptions ) : karmaOptions ,
161+ ( ) => subscriber . complete ( ) ,
162+ ) ;
163+ // karma typings incorrectly define start's return value as void
164+ // tslint:disable-next-line:no-use-of-empty-return-value
165+ const karmaStart = ( karmaServer . start ( ) as unknown ) as Promise < void > ;
166+
167+ // Cleanup, signal Karma to exit.
168+ return ( ) => {
169+ // Karma only has the `stop` method start with 3.1.1, so we must defensively check.
170+ const karmaServerWithStop = ( karmaServer as unknown ) as { stop : ( ) => Promise < void > } ;
171+ if ( typeof karmaServerWithStop . stop === 'function' ) {
172+ return karmaStart . then ( ( ) => karmaServerWithStop . stop ( ) ) ;
173+ }
174+ } ;
175+ } ) ,
176+ ) ,
131177 defaultIfEmpty ( { success : false } ) ,
132178 ) ;
133179}
0 commit comments