Skip to content

Commit f3cd052

Browse files
committed
feat(@angular/cli): option to build and test only specified spec files
1 parent 63748ab commit f3cd052

12 files changed

Lines changed: 191 additions & 2 deletions

File tree

packages/angular_devkit/build_angular/src/angular-cli-files/plugins/karma.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import * as webpack from 'webpack';
1414
const webpackDevMiddleware = require('webpack-dev-middleware');
1515

1616
import { KarmaWebpackFailureCb } from './karma-webpack-failure-cb';
17+
import { NormalizedKarmaBuilderSchema } from '../../karma/schema';
1718
import { statsErrorsToString } from '../utilities/stats';
1819
import { getWebpackStatsConfig } from '../models/webpack-configs/stats';
1920
import { createConsoleLogger } from '@angular-devkit/core/node';
@@ -63,7 +64,7 @@ const init: any = (config: any, emitter: any, customFileHandlers: any) => {
6364
` be used from within Angular CLI and will not work correctly outside of it.`
6465
)
6566
}
66-
const options = config.buildWebpack.options;
67+
const options = config.buildWebpack.options as NormalizedKarmaBuilderSchema;
6768
const logger: logging.Logger = config.buildWebpack.logger || createConsoleLogger();
6869
successCb = config.buildWebpack.successCb;
6970
failureCb = config.buildWebpack.failureCb;

packages/angular_devkit/build_angular/src/karma/index.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@angular-devkit/architect';
1515
import { Path, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core';
1616
import * as fs from 'fs';
17+
import * as glob from 'glob';
1718
import { Observable, of } from 'rxjs';
1819
import { concatMap } from 'rxjs/operators';
1920
import * as ts from 'typescript'; // tslint:disable-line:no-implicit-dependencies
@@ -76,12 +77,45 @@ export class KarmaBuilder implements Builder<KarmaBuilderSchema> {
7677
}
7778

7879
const sourceRoot = builderConfig.sourceRoot && resolve(root, builderConfig.sourceRoot);
80+
const webpackConfig = this.buildWebpackConfig(root, projectRoot, sourceRoot, host, options);
81+
82+
// generate new entry point with files matching provided glob
83+
if (options.spec) {
84+
const mainEntry = webpackConfig.entry.main;
85+
const newMainEntry = webpackConfig.entry.main.replace(/\.ts$/, '.generated.ts');
86+
// replace original entry with generated one
87+
webpackConfig.entry.main = newMainEntry;
88+
89+
try {
90+
this.createOrUpdateGeneratedTestFile(
91+
options.spec,
92+
getSystemPath(sourceRoot || projectRoot),
93+
builderConfig.sourceRoot,
94+
mainEntry,
95+
newMainEntry,
96+
);
97+
98+
// early exit if we are only supposed to update generated file
99+
if (options.specUpdate) {
100+
obs.next({ success: true, result: 'specs updated' });
101+
obs.complete();
102+
103+
return;
104+
}
105+
} catch (err) {
106+
this.context.logger.error(err.message);
107+
obs.next({ success: false });
108+
obs.complete();
109+
110+
return;
111+
}
112+
}
79113

80114
karmaOptions.buildWebpack = {
81115
root: getSystemPath(root),
82116
projectRoot: getSystemPath(projectRoot),
83117
options,
84-
webpackConfig: this.buildWebpackConfig(root, projectRoot, sourceRoot, host, options),
118+
webpackConfig,
85119
// Pass onto Karma to emit BuildEvents.
86120
successCb: () => obs.next({ success: true }),
87121
failureCb: () => obs.next({ success: false }),
@@ -160,6 +194,45 @@ export class KarmaBuilder implements Builder<KarmaBuilderSchema> {
160194

161195
return webpackMerge(webpackConfigs);
162196
}
197+
198+
createOrUpdateGeneratedTestFile(
199+
pattern: string,
200+
path: string,
201+
sourceRoot: Path | undefined,
202+
mainEntry: string,
203+
newMainEntry: string,
204+
) {
205+
let template = fs.readFileSync(mainEntry).toString();
206+
// remove source root to support absolute paths
207+
if (sourceRoot && pattern.startsWith(sourceRoot + '/')) {
208+
pattern = pattern.substr(sourceRoot.length + 1); // +1 to include slash
209+
}
210+
if (pattern.endsWith('.ts') && pattern.indexOf('.spec.ts') === -1) {
211+
pattern = pattern.substr(0, pattern.length - 2) + 'spec.ts';
212+
} else if (pattern.indexOf('.spec') === -1) {
213+
pattern += '.spec.ts';
214+
}
215+
216+
const files = glob.sync(pattern, { cwd: path });
217+
if (!files.length) {
218+
throw new Error('Specified spec glob does not match any files');
219+
}
220+
221+
const start = 'import \'';
222+
const end = '\';';
223+
const testCode = start + files
224+
.map(path => `./${path.replace('.ts', '')}`)
225+
.join(`${end}\n${start}`) + end;
226+
// TODO: maybe a documented 'marker/comment' inside test.ts would be nicer
227+
// or run typescript compiler and make changes based on the tree?
228+
let mockedRequireContext = '{ keys: () => ({ map: (_a: any) => { } }) };';
229+
mockedRequireContext += process.platform === 'win32' ? '\r\n' : '\n';
230+
template = template
231+
.replace(/declare\s+const\s+require:\s+any;/, '')
232+
.replace(/require\.context\(.*/, mockedRequireContext + testCode);
233+
234+
fs.writeFileSync(newMainEntry, template);
235+
}
163236
}
164237

165238
export default KarmaBuilder;

packages/angular_devkit/build_angular/src/karma/schema.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ export interface KarmaBuilderSchema extends Pick<BrowserBuilderSchema,
4040
* Karma reporters to use. Directly passed to the karma runner.
4141
*/
4242
reporters?: string[];
43+
44+
/**
45+
* Glob of files to include
46+
*/
47+
spec?: string;
48+
49+
/**
50+
* Should this only update generated test file?
51+
*/
52+
specUpdate?: boolean;
4353
}
4454

4555
export type KarmaSourceMapOptions = boolean | KarmaSourceMapObject;

packages/angular_devkit/build_angular/src/karma/schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@
6262
"type": "string",
6363
"description": "Defines the build environment."
6464
},
65+
"spec": {
66+
"type": "string",
67+
"description": "Glob of files to include"
68+
},
69+
"specUpdate": {
70+
"type": "boolean",
71+
"description": "Should this only update generated test file?",
72+
"default": false
73+
},
6574
"sourceMap": {
6675
"description": "Output sourcemaps.",
6776
"default": true,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import { DefaultTimeout, runTargetSpec } from '@angular-devkit/architect/testing';
9+
import { logging, normalize, virtualFs } from '@angular-devkit/core';
10+
import { tap } from 'rxjs/operators';
11+
import { NormalizedKarmaBuilderSchema } from '../../src';
12+
import { host, karmaTargetSpec } from '../utils';
13+
14+
describe('Karma Builder', () => {
15+
beforeEach(done => host.initialize().toPromise().then(done, done.fail));
16+
afterEach(done => host.restore().toPromise().then(done, done.fail));
17+
18+
it('should fail spec option doesn\'t match any files', async (done) => {
19+
const overrides: Partial<NormalizedKarmaBuilderSchema> = { spec: '---404' };
20+
const logger = new logging.Logger('test');
21+
const loggerSpy = jasmine.createSpy();
22+
logger.subscribe(loggerSpy);
23+
runTargetSpec(host, karmaTargetSpec, overrides, DefaultTimeout, logger).pipe(
24+
tap((buildEvent) => {
25+
expect(buildEvent.success).toBe(false, 'build failed');
26+
expect(loggerSpy).toHaveBeenCalledWith(jasmine.objectContaining({
27+
message: 'Specified spec glob does not match any files',
28+
}));
29+
}),
30+
).toPromise().then(done, done.fail);
31+
}, 30000);
32+
33+
describe('selected tests', () => {
34+
beforeEach(() => {
35+
host.writeMultipleFiles({
36+
'src/app/test.service.spec.ts': `
37+
describe('TestService', () => {
38+
it('should succeed', () => {
39+
expect(true).toBe(true);
40+
});
41+
});`,
42+
'src/app/failing.service.spec.ts': `
43+
describe('FailingService', () => {
44+
it('should be ignored', () => {
45+
expect(true).toBe(false);
46+
});
47+
});`,
48+
});
49+
});
50+
[
51+
{ message: 'absolute path to spec', path: 'src/app/test.service.spec.ts' },
52+
{ message: 'relative path from root to spec', path: 'app/test.service.spec.ts' },
53+
{ message: 'glob without spec suffix', path: '**/test.service' },
54+
{ message: 'glob with spec suffix', path: '**/test.service.spec.ts' },
55+
].forEach((options) => {
56+
57+
it('should work with ' + options.message, (done) => {
58+
const overrides: Partial<NormalizedKarmaBuilderSchema> = { spec: options.path };
59+
runTargetSpec(host, karmaTargetSpec, overrides).pipe(
60+
tap((buildEvent) => {
61+
expect(buildEvent.success).toBe(true, 'build failed');
62+
}),
63+
).toPromise().then(done, done.fail);
64+
}, 30000);
65+
});
66+
});
67+
68+
it('should only update generated file', (done) => {
69+
const overrides: Partial<NormalizedKarmaBuilderSchema> = {
70+
spec: '**/app.component.spec.ts',
71+
specUpdate: true,
72+
};
73+
const newEntryPoint = normalize('src/test.generated.ts');
74+
runTargetSpec(host, karmaTargetSpec, overrides).pipe(
75+
tap((buildEvent) => {
76+
expect(buildEvent.success).toBe(true, 'build failed');
77+
expect(buildEvent.result).toEqual('specs updated');
78+
79+
expect(host.scopedSync().exists(newEntryPoint)).toBe(true);
80+
const content = virtualFs.fileBufferToString(host.scopedSync().read(newEntryPoint));
81+
expect(content).toContain('import \'./app/app.component.spec\';');
82+
expect(content).toContain('{ keys: () => ({ map: (_a: any) => { } }) }');
83+
}),
84+
).toPromise().then(done, done.fail);
85+
}, 30000);
86+
87+
});

packages/schematics/angular/application/files/root/tsconfig.spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"<%= rootInSrc ? '' : 'src/' %>polyfills.ts"
1313
],
1414
"include": [
15+
"<%= rootInSrc ? '' : 'src/' %>test.generated.ts",
1516
"**/*.spec.ts",
1617
"**/*.d.ts"
1718
]

packages/schematics/angular/library/files/__projectRoot__/tsconfig.spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"src/test.ts"
1212
],
1313
"include": [
14+
"src/test.generated.ts",
1415
"**/*.spec.ts",
1516
"**/*.d.ts"
1617
]

packages/schematics/angular/workspace/files/__dot__gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
/tmp
66
/out-tsc
77

8+
# used to run specific tests instead of all
9+
test.generated.ts
10+
811
# dependencies
912
/node_modules
1013

tests/angular_devkit/build_angular/hello-world-app/src/tsconfig.spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"polyfills.ts"
1414
],
1515
"include": [
16+
"test.generated.ts",
1617
"**/*.spec.ts",
1718
"**/*.d.ts"
1819
]

tests/angular_devkit/build_ng_packagr/ng-packaged/projects/lib/tsconfig.spec.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"src/test.ts"
1212
],
1313
"include": [
14+
"test.generated.ts",
1415
"**/*.spec.ts",
1516
"**/*.d.ts"
1617
]

0 commit comments

Comments
 (0)