Skip to content

Commit 9337f19

Browse files
committed
feat(@angular/cli): option to build and test only specified spec files
1 parent 8e3efaf commit 9337f19

File tree

5 files changed

+163
-0
lines changed

5 files changed

+163
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 { logging } from '@angular-devkit/core';
9+
import { getOptions } from 'loader-utils';
10+
import { loader } from 'webpack';
11+
12+
export interface SingleTestTransformLoaderOptions {
13+
files: string[];
14+
logger: logging.Logger;
15+
}
16+
17+
export default function loader(this: loader.LoaderContext, source: string) {
18+
const options = getOptions(this) as SingleTestTransformLoaderOptions;
19+
const start = 'import \'';
20+
const end = '\';';
21+
const testCode = start + options.files
22+
.map(path => `./${path.replace('.ts', '')}`)
23+
.join(`${end}\n${start}`) + end;
24+
25+
options.logger.error(source);
26+
27+
// TODO: maybe a documented 'marker/comment' inside test.ts would be nicer?
28+
let mockedRequireContext = '{ keys: () => ({ map: (_a) => { } }) };';
29+
mockedRequireContext += process.platform === 'win32' ? '\r\n' : '\n';
30+
source = source.replace(/require\.context\(.*/, mockedRequireContext + testCode);
31+
32+
33+
return source;
34+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 * as glob from 'glob';
9+
import { join } from 'path';
10+
11+
export function findSpecs(input: string, workspaceRoot: string, sourceRoot: string): string[] {
12+
13+
input = input.replace(/\\/g, '/'); // normalize path
14+
// TODO: handle directory
15+
// remove source root to support absolute paths
16+
if (sourceRoot && input.startsWith(sourceRoot + '/')) {
17+
input = input.substr(sourceRoot.length + 1); // +1 to include slash
18+
}
19+
20+
if (input.endsWith('.ts') && input.indexOf('.spec.ts') === -1) {
21+
input = input.substr(0, input.length - 2) + 'spec.ts';
22+
} else if (input.indexOf('.spec') === -1) {
23+
input += '.spec.ts';
24+
}
25+
26+
const files = glob.sync(input, { cwd: join(workspaceRoot, sourceRoot) });
27+
28+
return files;
29+
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import {
1717
getTestConfig,
1818
getWorkerConfig,
1919
} from '../angular-cli-files/models/webpack-configs';
20+
import {
21+
SingleTestTransformLoaderOptions,
22+
} from '../angular-cli-files/plugins/single-test-transform';
23+
import { findSpecs } from '../angular-cli-files/utilities/find-specs';
2024
import { Schema as BrowserBuilderOptions } from '../browser/schema';
2125
import { ExecutionTransformer } from '../transforms';
2226
import { Version } from '../utils/version';
@@ -95,6 +99,28 @@ export function execute(
9599
}
96100
}
97101

102+
// generate new entry point with files matching provided glob
103+
if (webpackConfig && webpackConfig.module && options.spec) {
104+
const sourceRoot = 'src'; // or project/abc -> TODO: read from angular.sjon
105+
const files = findSpecs(options.spec, context.workspaceRoot, sourceRoot);
106+
// early exit, no reason to start karma
107+
if (!files.length) {
108+
subscriber.error(`Specified pattern: "${options.spec}" did not match any spec files`);
109+
110+
return;
111+
}
112+
webpackConfig.module.rules.unshift({
113+
test: /test\.ts$/,
114+
use: {
115+
loader: resolve(__dirname, '../angular-cli-files/plugins/single-test-transform.ts'),
116+
options: {
117+
files,
118+
logger: context.logger,
119+
} as SingleTestTransformLoaderOptions,
120+
},
121+
});
122+
}
123+
98124
// Assign additional karmaConfig options to the local ngapp config
99125
karmaOptions.configFile = resolve(context.workspaceRoot, options.karmaConfig);
100126

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
"type": "string",
6464
"description": "Defines the build environment."
6565
},
66+
"spec": {
67+
"type": "string",
68+
"description": "Glob of files to include"
69+
},
6670
"sourceMap": {
6771
"description": "Output sourcemaps.",
6872
"default": true,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 { Architect } from '@angular-devkit/architect';
9+
import { createArchitect, host, karmaTargetSpec } from '../utils';
10+
11+
describe('Karma Builder', () => {
12+
let architect: Architect;
13+
14+
beforeEach(async () => {
15+
await host.initialize().toPromise();
16+
architect = (await createArchitect(host.root())).architect;
17+
});
18+
19+
afterEach(() => host.restore().toPromise());
20+
21+
it('should fail when spec does not match any files', async () => {
22+
const overrides = {
23+
spec: 'abc.spec.ts',
24+
};
25+
const run = await architect.scheduleTarget(karmaTargetSpec, overrides);
26+
27+
await expectAsync(run.result)
28+
.toBeRejectedWith(`Specified pattern: "abc.spec.ts" did not match any spec files`);
29+
30+
await run.stop();
31+
});
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, async () => {
58+
const overrides = {
59+
spec: options.path,
60+
};
61+
const run = await architect.scheduleTarget(karmaTargetSpec, overrides);
62+
63+
await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
64+
65+
await run.stop();
66+
}, 30000);
67+
});
68+
});
69+
70+
});

0 commit comments

Comments
 (0)