Skip to content

Commit e43ed90

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 344306c commit e43ed90

File tree

6 files changed

+106
-3
lines changed

6 files changed

+106
-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
progress?: boolean;
227228
providersFile?: string;

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

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

36-
const { tsConfig, runner, reporters, browsers, progress } = options;
36+
const { tsConfig, runner, reporters, browsers, progress, filter } = options;
3737

3838
return {
3939
// Project/workspace information
@@ -45,6 +45,7 @@ export async function normalizeOptions(
4545
buildTarget,
4646
include: options.include ?? ['**/*.spec.ts'],
4747
exclude: options.exclude,
48+
filter,
4849
runnerName: runner,
4950
codeCoverage: options.codeCoverage
5051
? {

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

@@ -65,9 +65,31 @@ export class KarmaExecutor implements TestExecutor {
6565
aot: buildTargetOptions.aot,
6666
};
6767

68+
const transformOptions = {
69+
karmaOptions: (options) => {
70+
if (unitTestOptions.filter) {
71+
let filter = unitTestOptions.filter;
72+
if (filter[0] === '/' && filter.at(-1) === '/') {
73+
this.context.logger.warn(
74+
'The `--filter` option is always a regular expression.' +
75+
'Leading and trailing `/` are not required and will be ignored.',
76+
);
77+
} else {
78+
filter = `/${filter}/`;
79+
}
80+
81+
options.client ??= {};
82+
options.client.args ??= [];
83+
options.client.args.push('--grep', filter);
84+
}
85+
86+
return options;
87+
},
88+
} satisfies KarmaBuilderTransformsOptions;
89+
6890
const { execute } = await import('../../../karma');
6991

70-
yield* execute(karmaOptions, context);
92+
yield* execute(karmaOptions, context, transformOptions);
7193
}
7294

7395
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
watch,
192193
coverage: generateCoverageOption(codeCoverage),

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": "Globs of files to exclude, relative to the project root."
4343
},
44+
"filter": {
45+
"type": "string",
46+
"description": "A regular expression to match against test names, running only matching tests."
47+
},
4448
"watch": {
4549
"type": "boolean",
4650
"description": "Re-run tests when source files change. Defaults to `true` in TTY environments and `false` otherwise."
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
it('should work with karma runner', async () => {
63+
harness.useTarget('test', {
64+
...BASE_OPTIONS,
65+
// This filter should only match the 'should pass' test
66+
filter: 'pass$',
67+
});
68+
69+
const { result } = await harness.executeOnce();
70+
// The overall result should be success because the failing test was filtered out.
71+
expect(result?.success).toBe(true);
72+
});
73+
});
74+
});

0 commit comments

Comments
 (0)