diff --git a/packages/angular_devkit/build_angular/package.json b/packages/angular_devkit/build_angular/package.json index 6a996bd7951b..d39d4fb1615b 100644 --- a/packages/angular_devkit/build_angular/package.json +++ b/packages/angular_devkit/build_angular/package.json @@ -81,7 +81,7 @@ "@angular/compiler-cli": "^12.0.0-next", "@angular/localize": "^12.0.0-next", "@angular/service-worker": "^12.0.0-next", - "karma": "^6.0.0", + "karma": "^6.3.0", "ng-packagr": "^12.0.0-next", "protractor": "^7.0.0", "tailwindcss": "^2.0.0", diff --git a/packages/angular_devkit/build_angular/src/karma/index.ts b/packages/angular_devkit/build_angular/src/karma/index.ts index 9a4cc9587ffd..16dbe22d24eb 100644 --- a/packages/angular_devkit/build_angular/src/karma/index.ts +++ b/packages/angular_devkit/build_angular/src/karma/index.ts @@ -72,105 +72,109 @@ export function execute( assertCompatibleAngularVersion(context.workspaceRoot, context.logger); return from(initialize(options, context, transforms.webpackConfiguration)).pipe( - switchMap( - ([karma, webpackConfig]) => - new Observable(subscriber => { - const karmaOptions: KarmaConfigOptions = {}; - - if (options.watch !== undefined) { - karmaOptions.singleRun = !options.watch; - } - - // Convert browsers from a string to an array - if (options.browsers) { - karmaOptions.browsers = options.browsers.split(','); - } - - if (options.reporters) { - // Split along commas to make it more natural, and remove empty strings. - const reporters = options.reporters - .reduce((acc, curr) => acc.concat(curr.split(',')), []) - .filter(x => !!x); - - if (reporters.length > 0) { - karmaOptions.reporters = reporters; - } - } - - // prepend special webpack loader that will transform test.ts - if (options.include && options.include.length > 0) { - const mainFilePath = getSystemPath( - join(normalize(context.workspaceRoot), options.main), - ); - const files = findTests(options.include, dirname(mainFilePath), context.workspaceRoot); - // early exit, no reason to start karma - if (!files.length) { - subscriber.error( - `Specified patterns: "${options.include.join(', ')}" did not match any spec files`, - ); - - return; - } - - // Get the rules and ensure the Webpack configuration is setup properly - const rules = webpackConfig.module?.rules || []; - if (!webpackConfig.module) { - webpackConfig.module = { rules }; - } else if (!webpackConfig.module.rules) { - webpackConfig.module.rules = rules; - } - - rules.unshift({ - test: mainFilePath, - use: { - // cannot be a simple path as it differs between environments - loader: SingleTestTransformLoader, - options: { - files, - logger: context.logger, - }, - }, - }); - } - - // Assign additional karmaConfig options to the local ngapp config - karmaOptions.configFile = resolve(context.workspaceRoot, options.karmaConfig); - - karmaOptions.buildWebpack = { - options, - webpackConfig, - // Pass onto Karma to emit BuildEvents. - successCb: () => subscriber.next({ success: true }), - failureCb: () => subscriber.next({ success: false }), - // Workaround for https://github.com/karma-runner/karma/issues/3154 - // When this workaround is removed, user projects need to be updated to use a Karma - // version that has a fix for this issue. - toJSON: () => {}, - logger: context.logger, - }; - - // Complete the observable once the Karma server returns. - const karmaServer = new karma.Server( - transforms.karmaOptions ? transforms.karmaOptions(karmaOptions) : karmaOptions, - (exitCode: number) => { - subscriber.next({ success: exitCode === 0 }); - subscriber.complete(); - }, + switchMap(async ([karma, webpackConfig]) => { + const karmaOptions: KarmaConfigOptions = {}; + + if (options.watch !== undefined) { + karmaOptions.singleRun = !options.watch; + } + + // Convert browsers from a string to an array + if (options.browsers) { + karmaOptions.browsers = options.browsers.split(','); + } + + if (options.reporters) { + // Split along commas to make it more natural, and remove empty strings. + const reporters = options.reporters + .reduce((acc, curr) => acc.concat(curr.split(',')), []) + .filter(x => !!x); + + if (reporters.length > 0) { + karmaOptions.reporters = reporters; + } + } + + // prepend special webpack loader that will transform test.ts + if (options.include && options.include.length > 0) { + const mainFilePath = getSystemPath( + join(normalize(context.workspaceRoot), options.main), + ); + const files = findTests(options.include, dirname(mainFilePath), context.workspaceRoot); + // early exit, no reason to start karma + if (!files.length) { + throw new Error( + `Specified patterns: "${options.include.join(', ')}" did not match any spec files.`, ); - // karma typings incorrectly define start's return value as void - // tslint:disable-next-line:no-use-of-empty-return-value - const karmaStart = (karmaServer.start() as unknown) as Promise; - - // Cleanup, signal Karma to exit. - return () => { - // Karma only has the `stop` method start with 3.1.1, so we must defensively check. - const karmaServerWithStop = (karmaServer as unknown) as { stop: () => Promise }; - if (typeof karmaServerWithStop.stop === 'function') { - return karmaStart.then(() => karmaServerWithStop.stop()); - } - }; - }), - ), + } + + // Get the rules and ensure the Webpack configuration is setup properly + const rules = webpackConfig.module?.rules || []; + if (!webpackConfig.module) { + webpackConfig.module = { rules }; + } else if (!webpackConfig.module.rules) { + webpackConfig.module.rules = rules; + } + + rules.unshift({ + test: mainFilePath, + use: { + // cannot be a simple path as it differs between environments + loader: SingleTestTransformLoader, + options: { + files, + logger: context.logger, + }, + }, + }); + } + + karmaOptions.buildWebpack = { + options, + webpackConfig, + logger: context.logger, + }; + + // @types/karma doesn't include the last parameter. + // https://github.com/DefinitelyTyped/DefinitelyTyped/pull/52286 + // tslint:disable-next-line: no-any + const config = await (karma.config.parseConfig as any)( + resolve(context.workspaceRoot, options.karmaConfig), + transforms.karmaOptions ? transforms.karmaOptions(karmaOptions) : karmaOptions, + { promiseConfig: true, throwErrors: true }, + ) as Promise; + + return [karma, config] as [typeof karma, KarmaConfigOptions]; + }), + switchMap(([karma, karmaConfig]) => new Observable(subscriber => { + // Pass onto Karma to emit BuildEvents. + karmaConfig.buildWebpack ??= {}; + if (typeof karmaConfig.buildWebpack === 'object') { + // tslint:disable-next-line: no-any + (karmaConfig.buildWebpack as any).failureCb ??= () => subscriber.next({ success: false }); + // tslint:disable-next-line: no-any + (karmaConfig.buildWebpack as any).successCb ??= () => subscriber.next({ success: true }); + } + + // Complete the observable once the Karma server returns. + const karmaServer = new karma.Server( + karmaConfig, + (exitCode: number) => { + subscriber.next({ success: exitCode === 0 }); + subscriber.complete(); + }, + ); + // karma typings incorrectly define start's return value as void + // tslint:disable-next-line:no-use-of-empty-return-value + const karmaStart = (karmaServer.start() as unknown) as Promise; + + // Cleanup, signal Karma to exit. + return () => { + const karmaServerWithStop = (karmaServer as unknown) as { stop: () => Promise }; + + return karmaStart.then(() => karmaServerWithStop.stop()); + }; + })), defaultIfEmpty({ success: false }), ); } diff --git a/packages/angular_devkit/build_angular/src/karma/selected_spec.ts b/packages/angular_devkit/build_angular/src/karma/selected_spec.ts index 0b4de133a0fa..84623f836359 100644 --- a/packages/angular_devkit/build_angular/src/karma/selected_spec.ts +++ b/packages/angular_devkit/build_angular/src/karma/selected_spec.ts @@ -26,8 +26,8 @@ describe('Karma Builder', () => { }; const run = await architect.scheduleTarget(karmaTargetSpec, overrides); - await expectAsync(run.result).toBeRejectedWith( - `Specified patterns: "abc.spec.ts, def.spec.ts" did not match any spec files`, + await expectAsync(run.result).toBeRejectedWithError( + `Specified patterns: "abc.spec.ts, def.spec.ts" did not match any spec files.`, ); await run.stop();