From d4114030ed09e1bda8286c7318f0ec0ac95bf8e2 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 12 Sep 2025 19:34:19 -0400 Subject: [PATCH] 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. A warning is added for the Karma runner to inform users that the option is not yet supported there. --- goldens/public-api/angular/build/index.api.md | 1 + .../build/src/builders/unit-test/options.ts | 3 +- .../unit-test/runners/karma/executor.ts | 6 ++ .../unit-test/runners/vitest/executor.ts | 1 + .../build/src/builders/unit-test/schema.json | 4 ++ .../unit-test/tests/options/filter_spec.ts | 62 +++++++++++++++++++ 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/unit-test/tests/options/filter_spec.ts diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index e88df0d8f87c..0214ecf7c7ca 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -222,6 +222,7 @@ export type UnitTestBuilderOptions = { codeCoverageReporters?: SchemaCodeCoverageReporter[]; debug?: boolean; exclude?: string[]; + filter?: string; include?: string[]; progress?: boolean; providersFile?: string; diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index bcf334dc357e..612def1697e8 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -33,7 +33,7 @@ export async function normalizeOptions( const buildTargetSpecifier = options.buildTarget ?? `::development`; const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build'); - const { tsConfig, runner, reporters, browsers, progress } = options; + const { tsConfig, runner, reporters, browsers, progress, filter } = options; return { // Project/workspace information @@ -45,6 +45,7 @@ export async function normalizeOptions( buildTarget, include: options.include ?? ['**/*.spec.ts'], exclude: options.exclude, + filter, runnerName: runner, codeCoverage: options.codeCoverage ? { diff --git a/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts index 934e4da994f2..ec6e0f7340dd 100644 --- a/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts @@ -33,6 +33,12 @@ export class KarmaExecutor implements TestExecutor { ); } + if (unitTestOptions.filter) { + context.logger.warn( + 'The "karma" test runner does not support the "filter" option. The option will be ignored.', + ); + } + const buildTargetOptions = (await context.validateOptions( await context.getTargetOptions(unitTestOptions.buildTarget), await context.getBuilderNameForTarget(unitTestOptions.buildTarget), diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 6dec1f546004..acba6463525a 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -187,6 +187,7 @@ export class VitestExecutor implements TestExecutor { project: ['base', this.projectName], name: 'base', include: [], + testNamePattern: this.options.filter, reporters: reporters ?? ['default'], watch, coverage: generateCoverageOption(codeCoverage), diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index f49eaeda1baa..bc97098c7de9 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -41,6 +41,10 @@ }, "description": "Globs of files to exclude, relative to the project root." }, + "filter": { + "type": "string", + "description": "A regular expression to match against test names, running only matching tests." + }, "watch": { "type": "boolean", "description": "Re-run tests when source files change. Defaults to `true` in TTY environments and `false` otherwise." diff --git a/packages/angular/build/src/builders/unit-test/tests/options/filter_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/filter_spec.ts new file mode 100644 index 000000000000..abcfe5976f94 --- /dev/null +++ b/packages/angular/build/src/builders/unit-test/tests/options/filter_spec.ts @@ -0,0 +1,62 @@ +/** + * @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.dev/license + */ + +import { execute } from '../../builder'; +import { + BASE_OPTIONS, + describeBuilder, + UNIT_TEST_BUILDER_INFO, + setupApplicationTarget, +} from '../setup'; + +describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { + describe('Option: "filter"', () => { + beforeEach(async () => { + setupApplicationTarget(harness); + + await harness.writeFiles({ + 'src/app/pass.spec.ts': ` + describe('Passing Suite', () => { + it('should pass', () => { + expect(true).toBe(true); + }); + }); + `, + 'src/app/fail.spec.ts': ` + describe('Failing Suite', () => { + it('should fail', () => { + expect(true).toBe(false); + }); + }); + `, + }); + }); + + it('should only run tests that match the filter regex', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + // This filter should only match the 'should pass' test + filter: 'pass$', + }); + + const { result } = await harness.executeOnce(); + // The overall result should be success because the failing test was filtered out. + expect(result?.success).toBe(true); + }); + + it('should run all tests when no filter is provided', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + }); + + const { result } = await harness.executeOnce(); + // The overall result should be failure because the failing test was included. + expect(result?.success).toBe(false); + }); + }); +});