From 378b40ae7da5fbaac86372d5885e9a11c99928a1 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:42:34 -0400 Subject: [PATCH] feat(@angular/build): allow options for unit test reporters This change enhances the `reporters` option in the unit-test builder to support passing an options object, similar to the existing `codeCoverageReporters` option. Users can now specify a reporter as a tuple of `[name, options]`. - The `schema.json` is updated to allow either a string or a `[string, object]` tuple in the `reporters` array. An `enum` is provided for common reporters while still allowing custom string paths. - The option normalization logic in `options.ts` is refactored into a shared helper function to handle both `reporters` and `codeCoverageReporters`, reducing code duplication. - The Karma runner, which does not support reporter options, is updated to safely ignore them and warn the user. --- goldens/public-api/angular/build/index.api.md | 2 +- .../build/src/builders/unit-test/options.ts | 20 ++++--- .../unit-test/runners/karma/executor.ts | 11 +++- .../build/src/builders/unit-test/schema.json | 39 ++++++++++++- .../unit-test/tests/options/reporters_spec.ts | 58 ++++++++++++++----- 5 files changed, 106 insertions(+), 24 deletions(-) diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index e88df0d8f87c..bd073aa6ffa6 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -225,7 +225,7 @@ export type UnitTestBuilderOptions = { include?: string[]; progress?: boolean; providersFile?: string; - reporters?: string[]; + reporters?: SchemaReporter[]; runner: Runner; setupFiles?: string[]; tsConfig: 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..0b4f183bddbe 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -15,6 +15,16 @@ import type { Schema as UnitTestBuilderOptions } from './schema'; export type NormalizedUnitTestBuilderOptions = Awaited>; +function normalizeReporterOption( + reporters: unknown[] | undefined, +): [string, Record][] | undefined { + return reporters?.map((entry) => + typeof entry === 'string' + ? ([entry, {}] as [string, Record]) + : (entry as [string, Record]), + ); +} + export async function normalizeOptions( context: BuilderContext, projectName: string, @@ -33,7 +43,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, browsers, progress } = options; return { // Project/workspace information @@ -49,16 +59,12 @@ export async function normalizeOptions( codeCoverage: options.codeCoverage ? { exclude: options.codeCoverageExclude, - reporters: options.codeCoverageReporters?.map((entry) => - typeof entry === 'string' - ? ([entry, {}] as [string, Record]) - : (entry as [string, Record]), - ), + reporters: normalizeReporterOption(options.codeCoverageReporters), } : undefined, tsConfig, buildProgress: progress, - reporters, + reporters: normalizeReporterOption(options.reporters), browsers, watch: options.watch ?? isTTY(), debug: options.debug ?? false, 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..195a20eede37 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 @@ -60,7 +60,16 @@ export class KarmaExecutor implements TestExecutor { codeCoverage: !!unitTestOptions.codeCoverage, codeCoverageExclude: unitTestOptions.codeCoverage?.exclude, fileReplacements: buildTargetOptions.fileReplacements, - reporters: unitTestOptions.reporters, + reporters: unitTestOptions.reporters?.map((reporter) => { + // Karma only supports string reporters. + if (Object.keys(reporter[1]).length > 0) { + context.logger.warn( + `The "karma" test runner does not support options for the "${reporter[0]}" reporter. The options will be ignored.`, + ); + } + + return reporter[0]; + }), webWorkerTsConfig: buildTargetOptions.webWorkerTsConfig, aot: buildTargetOptions.aot, }; diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index f49eaeda1baa..7c73433ed15b 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -88,9 +88,40 @@ }, "reporters": { "type": "array", - "description": "Test runner reporters to use. Directly passed to the test runner.", + "description": "Specifies the reporters to use during test execution. Each reporter can be a string representing its name, or a tuple containing the name and an options object. Built-in reporters include 'default', 'verbose', 'dots', 'json', 'junit', 'tap', 'tap-flat', and 'html'. You can also provide a path to a custom reporter.", "items": { - "type": "string" + "oneOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/reporters-enum" + }, + { + "type": "string" + } + ] + }, + { + "type": "array", + "minItems": 1, + "maxItems": 2, + "items": [ + { + "anyOf": [ + { + "$ref": "#/definitions/reporters-enum" + }, + { + "type": "string" + } + ] + }, + { + "type": "object" + } + ] + } + ] } }, "providersFile": { @@ -124,6 +155,10 @@ "json", "json-summary" ] + }, + "reporters-enum": { + "type": "string", + "enum": ["default", "verbose", "dots", "json", "junit", "tap", "tap-flat", "html"] } } } diff --git a/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts index 657d688fb6dc..89ac5804e37e 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/reporters_spec.ts @@ -15,34 +15,66 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "reporters"', () => { - beforeEach(async () => { + describe('Option: "reporters"', () => { + beforeEach(() => { setupApplicationTarget(harness); }); - it('should use the default reporter when none is specified', async () => { + it(`should support a single reporter`, async () => { harness.useTarget('test', { ...BASE_OPTIONS, + reporters: ['json'], }); - const { result, logs } = await harness.executeOnce(); + const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/DefaultReporter/) }), - ); }); - it('should use a custom reporter when specified', async () => { + it(`should support multiple reporters`, async () => { harness.useTarget('test', { ...BASE_OPTIONS, - reporters: ['json'], + reporters: ['json', 'verbose'], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it(`should support a single reporter with options`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + reporters: [['json', { outputFile: 'a.json' }]], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('a.json').toExist(); + }); + + it(`should support multiple reporters with options`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + reporters: [ + ['json', { outputFile: 'a.json' }], + ['junit', { outputFile: 'a.xml' }], + ], + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + harness.expectFile('a.json').toExist(); + harness.expectFile('a.xml').toExist(); + }); + + it(`should support multiple reporters with and without options`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + reporters: [['json', { outputFile: 'a.json' }], 'verbose', 'default'], }); - const { result, logs } = await harness.executeOnce(); + const { result } = await harness.executeOnce(); expect(result?.success).toBeTrue(); - expect(logs).toContain( - jasmine.objectContaining({ message: jasmine.stringMatching(/JsonReporter/) }), - ); + harness.expectFile('a.json').toExist(); }); }); });