Skip to content

Commit a908bf3

Browse files
committed
feat(@angular/build): add 'filter' option to unit-test builder
This change introduces a new `filter` option to the `unit-test` builder. This allows users to specify a regular expression to match against test description names, providing a way to run a subset of tests. The option is implemented for the Vitest runner by passing the value to the `testNamePattern` configuration option. For the Karma runner, the client args `--grep` option is used.
1 parent 15940dc commit a908bf3

File tree

6 files changed

+94
-3
lines changed

6 files changed

+94
-3
lines changed

goldens/public-api/angular/build/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ export type UnitTestBuilderOptions = {
222222
codeCoverageReporters?: SchemaCodeCoverageReporter[];
223223
debug?: boolean;
224224
exclude?: string[];
225+
filter?: string;
225226
include?: string[];
226227
outputFile?: string;
227228
progress?: boolean;

packages/angular/build/src/builders/unit-test/options.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export async function normalizeOptions(
4343
const buildTargetSpecifier = options.buildTarget ?? `::development`;
4444
const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build');
4545

46-
const { tsConfig, runner, browsers, progress } = options;
46+
const { tsConfig, runner, browsers, progress, filter } = options;
4747

4848
return {
4949
// Project/workspace information
@@ -55,6 +55,7 @@ export async function normalizeOptions(
5555
buildTarget,
5656
include: options.include ?? ['**/*.spec.ts'],
5757
exclude: options.exclude,
58+
filter,
5859
runnerName: runner,
5960
codeCoverage: options.codeCoverage
6061
? {

packages/angular/build/src/builders/unit-test/runners/karma/executor.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
1010
import type { ApplicationBuilderInternalOptions } from '../../../application/options';
11-
import type { KarmaBuilderOptions } from '../../../karma';
11+
import type { KarmaBuilderOptions, KarmaBuilderTransformsOptions } from '../../../karma';
1212
import { NormalizedUnitTestBuilderOptions } from '../../options';
1313
import type { TestExecutor } from '../api';
1414

@@ -74,9 +74,31 @@ export class KarmaExecutor implements TestExecutor {
7474
aot: buildTargetOptions.aot,
7575
};
7676

77+
const transformOptions = {
78+
karmaOptions: (options) => {
79+
if (unitTestOptions.filter) {
80+
let filter = unitTestOptions.filter;
81+
if (filter[0] === '/' && filter.at(-1) === '/') {
82+
this.context.logger.warn(
83+
'The `--filter` option is always a regular expression.' +
84+
'Leading and trailing `/` are not required and will be ignored.',
85+
);
86+
} else {
87+
filter = `/${filter}/`;
88+
}
89+
90+
options.client ??= {};
91+
options.client.args ??= [];
92+
options.client.args.push('--grep', filter);
93+
}
94+
95+
return options;
96+
},
97+
} satisfies KarmaBuilderTransformsOptions;
98+
7799
const { execute } = await import('../../../karma');
78100

79-
yield* execute(karmaOptions, context);
101+
yield* execute(karmaOptions, context, transformOptions);
80102
}
81103

82104
async [Symbol.asyncDispose](): Promise<void> {

packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export class VitestExecutor implements TestExecutor {
187187
project: ['base', this.projectName],
188188
name: 'base',
189189
include: [],
190+
testNamePattern: this.options.filter,
190191
reporters: reporters ?? ['default'],
191192
outputFile,
192193
watch,

packages/angular/build/src/builders/unit-test/schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
},
4242
"description": "Specifies glob patterns of files to exclude from testing, relative to the project root."
4343
},
44+
"filter": {
45+
"type": "string",
46+
"description": "Specifies a regular expression pattern to match against test suite and test names. Only tests with a name matching the pattern will be executed. For example, `^App` will run only tests in suites beginning with 'App'."
47+
},
4448
"watch": {
4549
"type": "boolean",
4650
"description": "Enables watch mode, which re-runs tests when source files change. Defaults to `true` in TTY environments and `false` otherwise."
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC 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.dev/license
7+
*/
8+
9+
import { execute } from '../../builder';
10+
import {
11+
BASE_OPTIONS,
12+
describeBuilder,
13+
UNIT_TEST_BUILDER_INFO,
14+
setupApplicationTarget,
15+
} from '../setup';
16+
17+
describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
18+
describe('Option: "filter"', () => {
19+
beforeEach(async () => {
20+
setupApplicationTarget(harness);
21+
22+
await harness.writeFiles({
23+
'src/app/pass.spec.ts': `
24+
describe('Passing Suite', () => {
25+
it('should pass', () => {
26+
expect(true).toBe(true);
27+
});
28+
});
29+
`,
30+
'src/app/fail.spec.ts': `
31+
describe('Failing Suite', () => {
32+
it('should fail', () => {
33+
expect(true).toBe(false);
34+
});
35+
});
36+
`,
37+
});
38+
});
39+
40+
it('should only run tests that match the filter regex', async () => {
41+
harness.useTarget('test', {
42+
...BASE_OPTIONS,
43+
// This filter should only match the 'should pass' test
44+
filter: 'pass$',
45+
});
46+
47+
const { result } = await harness.executeOnce();
48+
// The overall result should be success because the failing test was filtered out.
49+
expect(result?.success).toBe(true);
50+
});
51+
52+
it('should run all tests when no filter is provided', async () => {
53+
harness.useTarget('test', {
54+
...BASE_OPTIONS,
55+
});
56+
57+
const { result } = await harness.executeOnce();
58+
// The overall result should be failure because the failing test was included.
59+
expect(result?.success).toBe(false);
60+
});
61+
});
62+
});

0 commit comments

Comments
 (0)