diff --git a/packages/angular_devkit/build_angular/src/karma/code-coverage_spec.ts b/packages/angular_devkit/build_angular/src/karma/code-coverage_spec.ts deleted file mode 100644 index 94939966d391..000000000000 --- a/packages/angular_devkit/build_angular/src/karma/code-coverage_spec.ts +++ /dev/null @@ -1,215 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect } from '@angular-devkit/architect'; -import { normalize, virtualFs } from '@angular-devkit/core'; -import { last, tap } from 'rxjs/operators'; -import { promisify } from 'util'; -import { createArchitect, host, karmaTargetSpec } from '../test-utils'; - -// In each of the test below we'll have to call setTimeout to wait for the coverage -// analysis to be done. This is because karma-coverage performs the analysis -// asynchronously but the promise that it returns is not awaited by Karma. -// Coverage analysis begins when onRunComplete() is invoked, and output files -// are subsequently written to disk. For more information, see -// https://github.com/karma-runner/karma-coverage/blob/32acafa90ed621abd1df730edb44ae55a4009c2c/lib/reporter.js#L221 - -const setTimeoutPromise = promisify(setTimeout); - -describe('Karma Builder code coverage', () => { - const coverageFilePath = normalize('coverage/lcov.info'); - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - - afterEach(() => host.restore().toPromise()); - - it('supports code coverage option', async () => { - const run = await architect.scheduleTarget(karmaTargetSpec, { codeCoverage: true }); - - const { success } = await run.result; - expect(success).toBe(true); - - await run.stop(); - - await setTimeoutPromise(1000); - - const exists = host.scopedSync().exists(coverageFilePath); - expect(exists).toBe(true, `${coverageFilePath} does not exist`); - - if (exists) { - const content = virtualFs.fileBufferToString(host.scopedSync().read(coverageFilePath)); - expect(content).toContain('app.component.ts'); - expect(content).toContain('test.ts'); - } - }); - - it('supports code coverage exclude option', async () => { - const overrides = { - codeCoverage: true, - codeCoverageExclude: ['**/test.ts'], - }; - - const run = await architect.scheduleTarget(karmaTargetSpec, overrides); - - const { success } = await run.result; - expect(success).toBe(true); - - await run.stop(); - - await setTimeoutPromise(1000); - - const exists = host.scopedSync().exists(coverageFilePath); - expect(exists).toBe(true); - - if (exists) { - const content = virtualFs.fileBufferToString(host.scopedSync().read(coverageFilePath)); - expect(content).not.toContain('test.ts'); - } - }); - - it(`should collect coverage from paths in 'sourceRoot'`, async () => { - const files: { [path: string]: string } = { - './dist/my-lib/index.d.ts': ` - export declare const title = 'app'; - `, - './dist/my-lib/index.js': ` - export const title = 'app'; - `, - './src/app/app.component.ts': ` - import { Component } from '@angular/core'; - import { title } from 'my-lib'; - - @Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] - }) - export class AppComponent { - title = title; - } - `, - }; - - host.writeMultipleFiles(files); - - host.replaceInFile( - 'tsconfig.json', - /"baseUrl": ".\/",/, - ` - "baseUrl": "./", - "paths": { - "my-lib": [ - "./dist/my-lib" - ] - }, - `, - ); - - const run = await architect.scheduleTarget(karmaTargetSpec, { codeCoverage: true }); - - const { success } = await run.result; - expect(success).toBe(true); - - await run.stop(); - - await setTimeoutPromise(1000); - - const exists = host.scopedSync().exists(coverageFilePath); - expect(exists).toBe(true); - - if (exists) { - const content = virtualFs.fileBufferToString(host.scopedSync().read(coverageFilePath)); - expect(content).not.toContain('my-lib'); - } - }); - - it('should exit with non-zero code when coverage is below threshold', async () => { - host.replaceInFile( - 'karma.conf.js', - 'coverageReporter: {', - ` - coverageReporter: { - check: { - global: { - statements: 100, - lines: 100, - branches: 100, - functions: 100 - } - }, - `, - ); - - host.appendToFile( - 'src/app/app.component.ts', - ` - export function nonCovered(): boolean { - return true; - } - `, - ); - - const run = await architect.scheduleTarget(karmaTargetSpec, { codeCoverage: true }); - - // In incremental mode, karma-coverage does not have the ability to mark a - // run as failed if code coverage does not pass. This is because it does - // the coverage asynchoronously and Karma does not await the promise - // returned by the plugin. - expect((await run.result).success).toBeTrue(); - - // However the program must exit with non-zero exit code. - // This is a more common use case of coverage testing and must be supported. - await run.output - .pipe( - last(), - tap((buildEvent) => expect(buildEvent.success).toBeFalse()), - ) - .toPromise(); - - await run.stop(); - }); - - it('is able to process coverage plugin provided as string', async () => { - host.replaceInFile( - 'karma.conf.js', - /plugins: \[.+?\]/s, - `plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('@angular-devkit/build-angular/plugins/karma'), - 'karma-coverage', // instead of require('karma-coverage') - ]`, - ); - const run = await architect.scheduleTarget(karmaTargetSpec, { codeCoverage: true }); - - const { success } = await run.result; - expect(success).toBe(true); - await run.stop(); - }); - - it('is able to process coverage plugins provided as string karma-*', async () => { - host.replaceInFile( - 'karma.conf.js', - /plugins: \[.+?\]/s, - `plugins: [ - 'karma-*', - require('@angular-devkit/build-angular/plugins/karma'), - ]`, - ); - const run = await architect.scheduleTarget(karmaTargetSpec, { codeCoverage: true }); - - const { success } = await run.result; - expect(success).toBe(true); - await run.stop(); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/karma/rebuilds_spec.ts b/packages/angular_devkit/build_angular/src/karma/rebuilds_spec.ts deleted file mode 100644 index 5825132f333c..000000000000 --- a/packages/angular_devkit/build_angular/src/karma/rebuilds_spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect } from '@angular-devkit/architect'; -import { Subject, timer } from 'rxjs'; -import { - catchError, - debounceTime, - delay, - map, - switchMap, - takeUntil, - takeWhile, - tap, -} from 'rxjs/operators'; -import { createArchitect, host, karmaTargetSpec } from '../test-utils'; - -describe('Karma Builder watch mode', () => { - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - - afterEach(() => host.restore().toPromise()); - - it('performs initial build', async () => { - const run = await architect.scheduleTarget(karmaTargetSpec, { watch: true }); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); - - await run.stop(); - }); - - it('recovers from compilation failures in watch mode', async () => { - let buildCount = 0; - let phase = 1; - - // The current linux-based CI environments may not fully settled in regards to filesystem - // changes from previous tests which reuse the same directory and fileset. - // The initial delay helps mitigate false positive rebuild triggers in such scenarios. - const { run } = await timer(1000) - .pipe( - switchMap(() => architect.scheduleTarget(karmaTargetSpec, { watch: true })), - switchMap((run) => run.output.pipe(map((output) => ({ run, output })))), - debounceTime(500), - tap(({ output }) => { - buildCount += 1; - switch (phase) { - case 1: - // Karma run should succeed. - // Add a compilation error. - expect(output.success).toBe(true); - // Add an syntax error to a non-main file. - host.appendToFile('src/app/app.component.spec.ts', `]]]`); - phase = 2; - break; - - case 2: - // Karma run should fail due to compilation error. Fix it. - expect(output.success).toBe(false); - host.replaceInFile('src/app/app.component.spec.ts', `]]]`, ''); - phase = 3; - break; - - case 3: - // Karma run should succeed again. - expect(output.success).toBe(true); - phase = 4; - break; - } - }), - takeWhile(() => phase < 4), - catchError((_, caught) => { - fail(`stuck at phase ${phase} [builds: ${buildCount}]`); - - return caught; - }), - ) - .toPromise(); - - await run.stop(); - }); - - it('does not rebuild when nothing changed', async () => { - let phase = 1; - - const stopSubject = new Subject(); - const stop$ = stopSubject.asObservable().pipe(delay(5000)); - - // The current linux-based CI environments may not fully settled in regards to filesystem - // changes from previous tests which reuse the same directory and fileset. - // The initial delay helps mitigate false positive rebuild triggers in such scenarios. - const { run } = await timer(1000) - .pipe( - switchMap(() => architect.scheduleTarget(karmaTargetSpec, { watch: true })), - switchMap((run) => run.output.pipe(map((output) => ({ run, output })))), - debounceTime(500), - tap(({ output }) => { - switch (phase) { - case 1: - // Karma run should succeed. - expect(output.success).toBe(true); - // Touch the file. - host.appendToFile('src/app/app.component.spec.ts', ``); - // Signal the stopper, which delays emission by 5s. - // If there's no rebuild within that time then the test is successful. - stopSubject.next(); - phase = 2; - break; - - case 2: - // Should never trigger this second build. - fail('Should not trigger second build.'); - break; - } - }), - takeUntil(stop$), - ) - .toPromise(); - - await run.stop(); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/karma/selected_spec.ts b/packages/angular_devkit/build_angular/src/karma/selected_spec.ts deleted file mode 100644 index 5905d0aacb86..000000000000 --- a/packages/angular_devkit/build_angular/src/karma/selected_spec.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect } from '@angular-devkit/architect'; -import { logging } from '@angular-devkit/core'; -import { createArchitect, host, karmaTargetSpec } from '../test-utils'; - -describe('Karma Builder', () => { - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - - afterEach(() => host.restore().toPromise()); - - describe('with include option', () => { - it('should fail when include does not match any files', async () => { - const overrides = { - include: ['abc.spec.ts', 'def.spec.ts'], - }; - const run = await architect.scheduleTarget(karmaTargetSpec, overrides); - - await expectAsync(run.result).toBeRejectedWithError( - `Specified patterns: "abc.spec.ts, def.spec.ts" did not match any spec files.`, - ); - - await run.stop(); - }); - - it('should fail when main test file does not include require.context usage', async () => { - let lastErrorLogEntry: logging.LogEntry | undefined; - const logger = new logging.Logger('test'); - logger.subscribe((m) => { - if (m.level === 'error') { - lastErrorLogEntry = m; - } - }); - - const mockedRequireContext = 'Object.assign(() => { }, { keys: () => [] as string[] })'; - const regex = /require\.context\(.*/; - host.replaceInFile('src/test.ts', regex, mockedRequireContext); - - const overrides = { - include: ['**/*.spec.ts'], - }; - - const run = await architect.scheduleTarget(karmaTargetSpec, overrides, { - logger, - }); - - await expectAsync(run.result).toBeResolved(); - - expect(lastErrorLogEntry && lastErrorLogEntry.message).toContain( - 'const context = require.context', - ); - expect(lastErrorLogEntry && lastErrorLogEntry.message).toContain( - "The 'include' option requires that the 'main' file for tests includes the below line:", - ); - - await run.stop(); - }); - - it('should work with test.ts that filters found keys', async () => { - // the replacement below is only to prove a point that resulting test.ts file will compile! - host.replaceInFile( - 'src/test.ts', - 'context.keys().map(context);', - 'context.keys().filter(k => !!k).map(context);', - ); - - const overrides = { - include: ['src/app/app.component.spec.ts'], - }; - const logger = new logging.Logger('test'); - logger.subscribe((m) => { - if (m.level === 'error') { - fail(m); - } - }); - const run = await architect.scheduleTarget(karmaTargetSpec, overrides, { - logger, - }); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); - - await run.stop(); - }); - - [ - { - test: 'relative path from workspace to spec', - input: ['src/app/app.component.spec.ts'], - }, - { - test: 'relative path from workspace to file', - input: ['src/app/app.component.ts'], - }, - { - test: 'relative path from project root to spec', - input: ['app/services/test.service.spec.ts'], - }, - { - test: 'relative path from project root to file', - input: ['app/services/test.service.ts'], - }, - { - test: 'relative path from workspace to directory', - input: ['src/app/services'], - }, - { - test: 'relative path from project root to directory', - input: ['app/services'], - }, - { - test: 'glob with spec suffix', - input: ['**/*.pipe.spec.ts', '**/*.pipe.spec.ts', '**/*test.service.spec.ts'], - }, - ].forEach((options, index) => { - it(`should work with ${options.test} (${index})`, async () => { - host.writeMultipleFiles({ - 'src/app/services/test.service.spec.ts': ` - describe('TestService', () => { - it('should succeed', () => { - expect(true).toBe(true); - }); - });`, - 'src/app/failing.service.spec.ts': ` - describe('FailingService', () => { - it('should be ignored', () => { - expect(true).toBe(false); - }); - });`, - 'src/app/property.pipe.spec.ts': ` - describe('PropertyPipe', () => { - it('should succeed', () => { - expect(true).toBe(true); - }); - });`, - }); - - const overrides = { - include: options.input, - }; - const logger = new logging.Logger('test'); - logger.subscribe((m) => { - if (m.level === 'error') { - fail(m); - } - }); - const run = await architect.scheduleTarget(karmaTargetSpec, overrides, { - logger, - }); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); - - await run.stop(); - }); - }); - }); -}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/behavior/errors_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/behavior/errors_spec.ts new file mode 100644 index 000000000000..b9fd460fa7c7 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/behavior/errors_spec.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Behavior: "Errors"', () => { + it('should fail when there is a TypeScript error', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + await harness.appendToFile('src/app/app.component.spec.ts', `console.lo('foo')`); + + const { result } = await harness.executeOnce({ + outputLogsOnFailure: false, + }); + + expect(result?.success).toBeFalse(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/behavior/rebuilds_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/behavior/rebuilds_spec.ts new file mode 100644 index 000000000000..10bd13eabf76 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/behavior/rebuilds_spec.ts @@ -0,0 +1,56 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { concatMap, count, debounceTime, take, timeout } from 'rxjs/operators'; +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Behavior: "Rebuilds"', () => { + it('recovers from compilation failures in watch mode', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + watch: true, + }); + + const goodFile = await harness.readFile('src/app/app.component.spec.ts'); + + const buildCount = await harness + .execute({ outputLogsOnFailure: false }) + .pipe( + timeout(60000), + debounceTime(500), + concatMap(async ({ result }, index) => { + switch (index) { + case 0: + // Karma run should succeed. + // Add a compilation error. + expect(result?.success).toBeTrue(); + // Add an syntax error to a non-main file. + await harness.appendToFile('src/app/app.component.spec.ts', `error`); + break; + + case 1: + expect(result?.success).toBeFalse(); + await harness.writeFile('src/app/app.component.spec.ts', goodFile); + break; + + case 2: + expect(result?.success).toBeTrue(); + break; + } + }), + take(3), + count(), + ) + .toPromise(); + + expect(buildCount).toBe(3); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/options/assets_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/options/assets_spec.ts new file mode 100644 index 000000000000..ddf04ff5e8b2 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/options/assets_spec.ts @@ -0,0 +1,101 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Option: "assets"', () => { + it('includes assets', async () => { + await harness.writeFiles({ + './src/string-file-asset.txt': 'string-file-asset.txt', + './src/string-folder-asset/file.txt': 'string-folder-asset.txt', + './src/glob-asset.txt': 'glob-asset.txt', + './src/folder/folder-asset.txt': 'folder-asset.txt', + './src/output-asset.txt': 'output-asset.txt', + './src/app/app.module.ts': ` + import { BrowserModule } from '@angular/platform-browser'; + import { NgModule } from '@angular/core'; + import { HttpClientModule } from '@angular/common/http'; + import { AppComponent } from './app.component'; + + @NgModule({ + declarations: [ + AppComponent + ], + imports: [ + BrowserModule, + HttpClientModule + ], + providers: [], + bootstrap: [AppComponent] + }) + export class AppModule { } + `, + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { HttpClient } from '@angular/common/http'; + + @Component({ + selector: 'app-root', + template: '

{{ asset.content }}

' + }) + export class AppComponent { + public assets = [ + { path: './string-file-asset.txt', content: '' }, + { path: './string-folder-asset/file.txt', content: '' }, + { path: './glob-asset.txt', content: '' }, + { path: './folder/folder-asset.txt', content: '' }, + { path: './output-folder/output-asset.txt', content: '' }, + ]; + constructor(private http: HttpClient) { + this.assets.forEach(asset => http.get(asset.path, { responseType: 'text' }) + .subscribe(res => asset.content = res)); + } + }`, + './src/app/app.component.spec.ts': ` + import { TestBed } from '@angular/core/testing'; + import { HttpClientModule } from '@angular/common/http'; + import { AppComponent } from './app.component'; + + describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + HttpClientModule + ], + declarations: [ + AppComponent + ] + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + assets: [ + 'src/string-file-asset.txt', + 'src/string-folder-asset', + { glob: 'glob-asset.txt', input: 'src/', output: '/' }, + { glob: 'output-asset.txt', input: 'src/', output: '/output-folder' }, + { glob: '**/*', input: 'src/folder', output: '/folder' }, + ], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage-exclude_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage-exclude_spec.ts new file mode 100644 index 000000000000..803f8937132f --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage-exclude_spec.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { promisify } from 'util'; +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +// In each of the test below we'll have to call setTimeout to wait for the coverage +// analysis to be done. This is because karma-coverage performs the analysis +// asynchronously but the promise that it returns is not awaited by Karma. +// Coverage analysis begins when onRunComplete() is invoked, and output files +// are subsequently written to disk. For more information, see +// https://github.com/karma-runner/karma-coverage/blob/32acafa90ed621abd1df730edb44ae55a4009c2c/lib/reporter.js#L221 + +const setTimeoutPromise = promisify(setTimeout); +const coveragePath = 'coverage/lcov.info'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Option: "codeCoverageExclude"', () => { + it('should exclude file from coverage when set', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: true, + codeCoverageExclude: ['**/app.component.ts'], + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + + await setTimeoutPromise(1000); + harness.expectFile(coveragePath).content.not.toContain('app.component.ts'); + }); + + it('should not exclude file from coverage when set', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: true, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + + await setTimeoutPromise(1000); + harness.expectFile(coveragePath).content.toContain('app.component.ts'); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage_spec.ts new file mode 100644 index 000000000000..4222e07523cb --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/options/code-coverage_spec.ts @@ -0,0 +1,160 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { first, last, tap } from 'rxjs/operators'; +import { promisify } from 'util'; +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +// In each of the test below we'll have to call setTimeout to wait for the coverage +// analysis to be done. This is because karma-coverage performs the analysis +// asynchronously but the promise that it returns is not awaited by Karma. +// Coverage analysis begins when onRunComplete() is invoked, and output files +// are subsequently written to disk. For more information, see +// https://github.com/karma-runner/karma-coverage/blob/32acafa90ed621abd1df730edb44ae55a4009c2c/lib/reporter.js#L221 + +const setTimeoutPromise = promisify(setTimeout); +const coveragePath = 'coverage/lcov.info'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Option: "codeCoverage"', () => { + it('should generate coverage report when option is set to true', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + await setTimeoutPromise(1000); + harness.expectFile(coveragePath).toExist(); + }); + + it('should not generate coverage report when option is set to false', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: false, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + + await setTimeoutPromise(1000); + harness.expectFile(coveragePath).toNotExist(); + }); + + it('should not generate coverage report when option is unset', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + + expect(result?.success).toBeTrue(); + + await setTimeoutPromise(1000); + harness.expectFile(coveragePath).toNotExist(); + }); + + it(`should collect coverage from paths in 'sourceRoot'`, async () => { + await harness.writeFiles({ + './dist/my-lib/index.d.ts': ` + export declare const title = 'app'; + `, + './dist/my-lib/index.js': ` + export const title = 'app'; + `, + './src/app/app.component.ts': ` + import { Component } from '@angular/core'; + import { title } from 'my-lib'; + + @Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] + }) + export class AppComponent { + title = title; + } + `, + }); + await harness.modifyFile('tsconfig.json', (content) => + content.replace( + /"baseUrl": ".\/",/, + ` + "baseUrl": "./", + "paths": { + "my-lib": [ + "./dist/my-lib" + ] + }, + `, + ), + ); + + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: true, + }); + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + await setTimeoutPromise(1000); + harness.expectFile(coveragePath).content.not.toContain('my-lib'); + }); + + it('should exit with non-zero code when coverage is below threshold', async () => { + await harness.modifyFile('karma.conf.js', (content) => + content.replace( + 'coverageReporter: {', + `coverageReporter: { + check: { + global: { + statements: 100, + lines: 100, + branches: 100, + functions: 100 + } + }, + `, + ), + ); + + await harness.appendToFile( + 'src/app/app.component.ts', + ` + export function nonCovered(): boolean { + return true; + } + `, + ); + + harness.useTarget('test', { + ...BASE_OPTIONS, + codeCoverage: true, + }); + + const result = await harness.execute(); + await result + .pipe( + // In incremental mode, karma-coverage does not have the ability to mark a + // run as failed if code coverage does not pass. This is because it does + // the coverage asynchoronously and Karma does not await the promise + // returned by the plugin. + + // However the program must exit with non-zero exit code. + // This is a more common use case of coverage testing and must be supported. + last(), + tap((buildEvent) => expect(buildEvent.result?.success).toBeFalse()), + ) + .toPromise(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/options/include_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/options/include_spec.ts new file mode 100644 index 000000000000..f9af40f861c8 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/options/include_spec.ts @@ -0,0 +1,90 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Option: "include"', () => { + it(`should fail when includes doesn't match any files`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + include: ['abc.spec.ts', 'def.spec.ts'], + }); + + const { error } = await harness.executeOnce({ + outputLogsOnException: false, + }); + expect(error?.message).toBe( + 'Specified patterns: "abc.spec.ts, def.spec.ts" did not match any spec files.', + ); + }); + + [ + { + test: 'relative path from workspace to spec', + input: ['src/app/app.component.spec.ts'], + }, + { + test: 'relative path from workspace to file', + input: ['src/app/app.component.ts'], + }, + { + test: 'relative path from project root to spec', + input: ['app/services/test.service.spec.ts'], + }, + { + test: 'relative path from project root to file', + input: ['app/services/test.service.ts'], + }, + { + test: 'relative path from workspace to directory', + input: ['src/app/services'], + }, + { + test: 'relative path from project root to directory', + input: ['app/services'], + }, + { + test: 'glob with spec suffix', + input: ['**/*.pipe.spec.ts', '**/*.pipe.spec.ts', '**/*test.service.spec.ts'], + }, + ].forEach((options, index) => { + it(`should work with ${options.test} (${index})`, async () => { + await harness.writeFiles({ + 'src/app/services/test.service.spec.ts': ` + describe('TestService', () => { + it('should succeed', () => { + expect(true).toBe(true); + }); + });`, + 'src/app/failing.service.spec.ts': ` + describe('FailingService', () => { + it('should be ignored', () => { + expect(true).toBe(false); + }); + });`, + 'src/app/property.pipe.spec.ts': ` + describe('PropertyPipe', () => { + it('should succeed', () => { + expect(true).toBe(true); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + include: options.input, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/options/styles_spec.ts b/packages/angular_devkit/build_angular/src/karma/tests/options/styles_spec.ts new file mode 100644 index 000000000000..fe565324e608 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/options/styles_spec.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { execute } from '../../index'; +import { BASE_OPTIONS, KARMA_BUILDER_INFO, describeBuilder } from '../setup'; + +describeBuilder(execute, KARMA_BUILDER_INFO, (harness) => { + describe('Option: "styles"', () => { + it('includes unnamed styles in compilation', async () => { + await harness.writeFiles({ + 'src/styles.css': 'p {display: none}', + 'src/app/app.component.ts': ` + import { Component } from '@angular/core'; + + @Component({ + selector: 'app-root', + template: '

Hello World

' + }) + export class AppComponent { + } + `, + 'src/app/app.component.spec.ts': ` + import { TestBed } from '@angular/core/testing'; + import { AppComponent } from './app.component'; + + describe('AppComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + ], + declarations: [ + AppComponent + ] + }).compileComponents(); + }); + + it('should not contain text that is hidden via css', () => { + const fixture = TestBed.createComponent(AppComponent); + expect(fixture.nativeElement.innerText).not.toContain('Hello World'); + }); + });`, + }); + + harness.useTarget('test', { + ...BASE_OPTIONS, + styles: ['src/styles.css'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + }); +}); diff --git a/packages/angular_devkit/build_angular/src/karma/tests/setup.ts b/packages/angular_devkit/build_angular/src/karma/tests/setup.ts new file mode 100644 index 000000000000..50a5eba70500 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/karma/tests/setup.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import { Schema } from '../schema'; + +export { describeBuilder } from '../../testing'; + +export const KARMA_BUILDER_INFO = Object.freeze({ + name: '@angular-devkit/build-angular:karma', + schemaPath: __dirname + '/../schema.json', +}); + +/** + * Contains all required karma builder fields. + * Also disables progress reporting to minimize logging output. + */ +export const BASE_OPTIONS = Object.freeze({ + main: 'src/test.ts', + polyfills: 'src/polyfills.ts', + tsConfig: 'src/tsconfig.spec.json', + karmaConfig: 'karma.conf.js', + browsers: 'ChromeHeadlessCI', + progress: false, + watch: false, +}); diff --git a/packages/angular_devkit/build_angular/src/karma/works_spec.ts b/packages/angular_devkit/build_angular/src/karma/works_spec.ts deleted file mode 100644 index f2193209743e..000000000000 --- a/packages/angular_devkit/build_angular/src/karma/works_spec.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import { Architect } from '@angular-devkit/architect'; -import { createArchitect, host, karmaTargetSpec } from '../test-utils'; - -describe('Karma Builder', () => { - let architect: Architect; - - beforeEach(async () => { - await host.initialize().toPromise(); - architect = (await createArchitect(host.root())).architect; - }); - - afterEach(() => host.restore().toPromise()); - - it('runs', async () => { - const run = await architect.scheduleTarget(karmaTargetSpec); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); - - await run.stop(); - }); - - it('fails with broken compilation', async () => { - host.writeMultipleFiles({ - 'src/app/app.component.spec.ts': '

definitely not typescript

', - }); - - const run = await architect.scheduleTarget(karmaTargetSpec); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: false })); - - await run.stop(); - }); - - it('generates and uses global styles', async () => { - host.writeMultipleFiles({ - 'src/styles.css': 'p {display: none}', - 'src/app/app.component.ts': ` - import { Component } from '@angular/core'; - - @Component({ - selector: 'app-root', - template: '

Hello World

' - }) - export class AppComponent { - } - `, - 'src/app/app.component.spec.ts': ` - import { TestBed } from '@angular/core/testing'; - import { AppComponent } from './app.component'; - - describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - ], - declarations: [ - AppComponent - ] - }).compileComponents(); - }); - - it('should not contain text that is hidden via css', () => { - const fixture = TestBed.createComponent(AppComponent); - expect(fixture.nativeElement.innerText).not.toContain('Hello World'); - }); - });`, - }); - - const run = await architect.scheduleTarget(karmaTargetSpec); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); - - await run.stop(); - }); - - it('generates and uses assets', async () => { - const assets: { [path: string]: string } = { - './src/string-file-asset.txt': 'string-file-asset.txt', - './src/string-folder-asset/file.txt': 'string-folder-asset.txt', - './src/glob-asset.txt': 'glob-asset.txt', - './src/folder/folder-asset.txt': 'folder-asset.txt', - './src/output-asset.txt': 'output-asset.txt', - }; - host.writeMultipleFiles(assets); - host.writeMultipleFiles({ - 'src/app/app.module.ts': ` - import { BrowserModule } from '@angular/platform-browser'; - import { NgModule } from '@angular/core'; - import { HttpClientModule } from '@angular/common/http'; - import { AppComponent } from './app.component'; - - @NgModule({ - declarations: [ - AppComponent - ], - imports: [ - BrowserModule, - HttpClientModule - ], - providers: [], - bootstrap: [AppComponent] - }) - export class AppModule { } - `, - 'src/app/app.component.ts': ` - import { Component } from '@angular/core'; - import { HttpClient } from '@angular/common/http'; - - @Component({ - selector: 'app-root', - template: '

{{asset.content }}

' - }) - export class AppComponent { - public assets = [ - { path: './string-file-asset.txt', content: '' }, - { path: './string-folder-asset/file.txt', content: '' }, - { path: './glob-asset.txt', content: '' }, - { path: './folder/folder-asset.txt', content: '' }, - { path: './output-folder/output-asset.txt', content: '' }, - ]; - constructor(private http: HttpClient) { - this.assets.forEach(asset => http.get(asset.path, { responseType: 'text' }) - .subscribe(res => asset.content = res)); - } - }`, - 'src/app/app.component.spec.ts': ` - import { TestBed } from '@angular/core/testing'; - import { HttpClientModule } from '@angular/common/http'; - import { AppComponent } from './app.component'; - - describe('AppComponent', () => { - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ - HttpClientModule - ], - declarations: [ - AppComponent - ] - }).compileComponents(); - }); - - it('should create the app', () => { - const fixture = TestBed.createComponent(AppComponent); - const app = fixture.debugElement.componentInstance; - expect(app).toBeTruthy(); - }); - });`, - }); - - const overrides = { - assets: [ - 'src/string-file-asset.txt', - 'src/string-folder-asset', - { glob: 'glob-asset.txt', input: 'src/', output: '/' }, - { glob: 'output-asset.txt', input: 'src/', output: '/output-folder' }, - { glob: '**/*', input: 'src/folder', output: '/folder' }, - ], - }; - - const run = await architect.scheduleTarget(karmaTargetSpec, overrides); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); - - await run.stop(); - }); - - it('allows file replacements', async () => { - host.writeMultipleFiles({ - 'src/meaning-too.ts': 'export var meaning = 42;', - 'src/meaning.ts': `export var meaning = 10;`, - - 'src/test.ts': ` - import { meaning } from './meaning'; - - describe('Test file replacement', () => { - it('should replace file', () => { - expect(meaning).toBe(42); - }); - }); - `, - }); - - const overrides = { - fileReplacements: [ - { - replace: '/src/meaning.ts', - with: '/src/meaning-too.ts', - }, - ], - }; - - const run = await architect.scheduleTarget(karmaTargetSpec, overrides); - - await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true })); - - await run.stop(); - }); -});