diff --git a/packages/@angular/cli/blueprints/ng/files/__path__/tsconfig.spec.json b/packages/@angular/cli/blueprints/ng/files/__path__/tsconfig.spec.json index 5dc8eeb57474..0bba9ea2069c 100644 --- a/packages/@angular/cli/blueprints/ng/files/__path__/tsconfig.spec.json +++ b/packages/@angular/cli/blueprints/ng/files/__path__/tsconfig.spec.json @@ -8,7 +8,8 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "lib": [ - "es2016" + "es2016", + "dom" ],<% } %> "outDir": "<%= relativeRootPath %>/out-tsc/spec", "module": "commonjs", @@ -23,6 +24,7 @@ "test.ts" ], "include": [ - "**/*.spec.ts" + "**/*.spec.ts", + "**/*.d.ts" ] } diff --git a/packages/@angular/cli/models/webpack-configs/test.ts b/packages/@angular/cli/models/webpack-configs/test.ts index 9c16aeb0925e..add32bd23cb0 100644 --- a/packages/@angular/cli/models/webpack-configs/test.ts +++ b/packages/@angular/cli/models/webpack-configs/test.ts @@ -4,6 +4,7 @@ import * as webpack from 'webpack'; import { CliConfig } from '../config'; import { WebpackTestOptions } from '../webpack-test-config'; +import { KarmaWebpackEmitlessError } from '../../plugins/karma-webpack-emitless-error'; /** * Enumerate loaders and their dependencies from this file to let the dependency validator @@ -57,7 +58,8 @@ export function getTestConfig(testConfig: WebpackTestOptions) { new webpack.SourceMapDevToolPlugin({ filename: null, // if no value is provided the sourcemap is inlined test: /\.(ts|js)($|\?)/i // process .js and .ts files only - }) + }), + new KarmaWebpackEmitlessError() ] }; } diff --git a/packages/@angular/cli/models/webpack-test-config.ts b/packages/@angular/cli/models/webpack-test-config.ts index d7d3e81b7dd1..a5b9049f62db 100644 --- a/packages/@angular/cli/models/webpack-test-config.ts +++ b/packages/@angular/cli/models/webpack-test-config.ts @@ -28,6 +28,7 @@ export class WebpackTestConfig extends NgCliWebpackConfig { ]; this.config = webpackMerge(webpackConfigs); + delete this.config.entry; // Remove any instance of CommonsChunkPlugin, not needed with karma-webpack. this.config.plugins = this.config.plugins.filter((plugin: any) => diff --git a/packages/@angular/cli/plugins/karma-webpack-emitless-error.ts b/packages/@angular/cli/plugins/karma-webpack-emitless-error.ts new file mode 100644 index 000000000000..d028d22fc7f9 --- /dev/null +++ b/packages/@angular/cli/plugins/karma-webpack-emitless-error.ts @@ -0,0 +1,20 @@ +// Don't emit anything when there are compilation errors. This is useful for preventing Karma +// from re-running tests when there is a compilation error. +// Workaround for https://github.com/webpack-contrib/karma-webpack/issues/49 + +export class KarmaWebpackEmitlessError { + constructor() { } + + apply(compiler: any): void { + compiler.plugin('done', (stats: any) => { + if (stats.compilation.errors.length > 0) { + stats.stats = [{ + toJson: function () { + return this; + }, + assets: [] + }]; + } + }); + } +} diff --git a/packages/@angular/cli/plugins/karma-webpack-throw-error.ts b/packages/@angular/cli/plugins/karma-webpack-throw-error.ts new file mode 100644 index 000000000000..601e1ec4c79f --- /dev/null +++ b/packages/@angular/cli/plugins/karma-webpack-throw-error.ts @@ -0,0 +1,14 @@ +// Force Webpack to throw compilation errors. Useful with karma-webpack when in single-run mode. +// Workaround for https://github.com/webpack-contrib/karma-webpack/issues/66 + +export class KarmaWebpackThrowError { + constructor() { } + + apply(compiler: any): void { + compiler.plugin('done', (stats: any) => { + if (stats.compilation.errors.length > 0) { + throw new Error(stats.compilation.errors.map((err: any) => err.message || err)); + } + }); + } +} diff --git a/packages/@angular/cli/plugins/karma.ts b/packages/@angular/cli/plugins/karma.ts index a16af88deab5..42303db5a9d8 100644 --- a/packages/@angular/cli/plugins/karma.ts +++ b/packages/@angular/cli/plugins/karma.ts @@ -5,6 +5,7 @@ import * as glob from 'glob'; import { Pattern } from './glob-copy-webpack-plugin'; import { extraEntryParser } from '../models/webpack-configs/utils'; import { WebpackTestConfig, WebpackTestOptions } from '../models/webpack-test-config'; +import { KarmaWebpackThrowError } from './karma-webpack-throw-error'; const getAppFromConfig = require('../utilities/app-utils').getAppFromConfig; @@ -102,6 +103,11 @@ const init: any = (config: any) => { } }; + // If Karma is being ran in single run mode, throw errors. + if (config.singleRun) { + webpackConfig.plugins.push(new KarmaWebpackThrowError()); + } + config.webpack = Object.assign(webpackConfig, config.webpack); config.webpackMiddleware = Object.assign(webpackMiddlewareConfig, config.webpackMiddleware); diff --git a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts index 06168b9b3f24..be7ec112beb3 100644 --- a/packages/@ngtools/webpack/src/extract_i18n_plugin.ts +++ b/packages/@ngtools/webpack/src/extract_i18n_plugin.ts @@ -46,7 +46,8 @@ export class ExtractI18nPlugin implements Tapable { if (!options.hasOwnProperty('tsConfigPath')) { throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); } - this._tsConfigPath = options.tsConfigPath; + // TS represents paths internally with '/' and expects the tsconfig path to be in this format + this._tsConfigPath = options.tsConfigPath.replace(/\\/g, '/'); // Check the base path. const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 2addb6a29018..055c675ccee3 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -96,7 +96,8 @@ export class AotPlugin implements Tapable { if (!options.hasOwnProperty('tsConfigPath')) { throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); } - this._tsConfigPath = options.tsConfigPath; + // TS represents paths internally with '/' and expects the tsconfig path to be in this format + this._tsConfigPath = options.tsConfigPath.replace(/\\/g, '/'); // Check the base path. const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); diff --git a/tests/e2e/tests/build/aot/exclude.ts b/tests/e2e/tests/build/aot/exclude.ts index 6f1518e9302c..98904d0f9f22 100644 --- a/tests/e2e/tests/build/aot/exclude.ts +++ b/tests/e2e/tests/build/aot/exclude.ts @@ -23,6 +23,7 @@ export default function () { })) .then(() => updateJsonFile('src/tsconfig.json', tsconfigJson => { delete tsconfigJson['exclude']; + delete tsconfigJson['compilerOptions']['types']; })) .then(() => ng('build', '--aot')) .then(() => !ejected && ng('test', '--single-run')); diff --git a/tests/e2e/tests/test/test-fail-single-run.ts b/tests/e2e/tests/test/test-fail-single-run.ts new file mode 100644 index 000000000000..bdcbb14604df --- /dev/null +++ b/tests/e2e/tests/test/test-fail-single-run.ts @@ -0,0 +1,10 @@ +import { ng } from '../../utils/process'; +import { writeFile } from '../../utils/fs'; +import { expectToFail } from '../../utils/utils'; + + +export default function () { + // Fails on single run with broken compilation. + return writeFile('src/app.component.spec.ts', '
definitely not typescript
') + .then(() => expectToFail(() => ng('test', '--single-run'))); +} diff --git a/tests/e2e/tests/test/test-fail-watch.ts b/tests/e2e/tests/test/test-fail-watch.ts new file mode 100644 index 000000000000..207588351ef8 --- /dev/null +++ b/tests/e2e/tests/test/test-fail-watch.ts @@ -0,0 +1,28 @@ +import { + killAllProcesses, + waitForAnyProcessOutputToMatch, + silentExecAndWaitForOutputToMatch +} from '../../utils/process'; +import { expectToFail } from '../../utils/utils'; +import { readFile, writeFile } from '../../utils/fs'; + + +// Karma is only really finished with a run when it shows a non-zero total time in the first slot. +const karmaGoodRegEx = /Executed 3 of 3 SUCCESS \(\d+\.\d+ secs/; + +export default function () { + let originalSpec: string; + return silentExecAndWaitForOutputToMatch('ng', ['test', '--no-progress'], karmaGoodRegEx) + .then(() => readFile('src/app/app.component.spec.ts')) + .then((data) => originalSpec = data) + // Trigger a failed rebuild, which shouldn't run tests again. + .then(() => writeFile('src/app/app.component.spec.ts', 'definitely not typescript
')) + .then(() => expectToFail(() => waitForAnyProcessOutputToMatch(karmaGoodRegEx, 10000))) + // Restore working spec. + .then(() => writeFile('src/app/app.component.spec.ts', originalSpec)) + .then(() => waitForAnyProcessOutputToMatch(karmaGoodRegEx, 10000)) + .then(() => killAllProcesses(), (err: any) => { + killAllProcesses(); + throw err; + }); +}