Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions goldens/public-api/angular/build/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,13 @@ export type NgPackagrBuilderOptions = {
export type UnitTestBuilderOptions = {
browsers?: string[];
buildTarget: string;
codeCoverage?: boolean;
codeCoverageExclude?: string[];
codeCoverageReporters?: SchemaCodeCoverageReporter[];
coverage?: boolean;
coverageAll?: boolean;
coverageExclude?: string[];
coverageInclude?: string[];
coverageReporters?: SchemaCoverageReporter[];
coverageThresholds?: CoverageThresholds;
coverageWatermarks?: CoverageWatermarks;
debug?: boolean;
dumpVirtualFiles?: boolean;
exclude?: string[];
Expand Down
17 changes: 14 additions & 3 deletions packages/angular/build/src/builders/unit-test/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,21 @@ export async function normalizeOptions(
exclude: options.exclude,
filter,
runnerName: runner,
codeCoverage: options.codeCoverage
coverage: options.coverage
? {
exclude: options.codeCoverageExclude,
reporters: normalizeReporterOption(options.codeCoverageReporters),
all: options.coverageAll,
exclude: options.coverageExclude,
include: options.coverageInclude,
reporters: normalizeReporterOption(options.coverageReporters),
thresholds: options.coverageThresholds,
// The schema generation tool doesn't support tuple types for items, but the schema validation
// does ensure that the array has exactly two numbers.
watermarks: options.coverageWatermarks as {
statements?: [number, number];
branches?: [number, number];
functions?: [number, number];
lines?: [number, number];
},
}
: undefined,
tsConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ export class KarmaExecutor implements TestExecutor {
);
}

if (unitTestOptions.coverage?.all) {
context.logger.warn(
'The "karma" test runner does not support the "coverageAll" option. The option will be ignored.',
);
}

if (unitTestOptions.coverage?.include) {
context.logger.warn(
'The "karma" test runner does not support the "coverageInclude" option. The option will be ignored.',
);
}

const buildTargetOptions = (await context.validateOptions(
await context.getTargetOptions(unitTestOptions.buildTarget),
await context.getBuilderNameForTarget(unitTestOptions.buildTarget),
Expand All @@ -57,8 +69,8 @@ export class KarmaExecutor implements TestExecutor {
poll: buildTargetOptions.poll,
preserveSymlinks: buildTargetOptions.preserveSymlinks,
browsers: unitTestOptions.browsers?.join(','),
codeCoverage: !!unitTestOptions.codeCoverage,
codeCoverageExclude: unitTestOptions.codeCoverage?.exclude,
codeCoverage: !!unitTestOptions.coverage,
codeCoverageExclude: unitTestOptions.coverage?.exclude,
fileReplacements: buildTargetOptions.fileReplacements,
reporters: unitTestOptions.reporters?.map((reporter) => {
// Karma only supports string reporters.
Expand Down Expand Up @@ -92,6 +104,23 @@ export class KarmaExecutor implements TestExecutor {
options.client.args.push('--grep', filter);
}

// Add coverage options
if (unitTestOptions.coverage) {
const { thresholds, watermarks } = unitTestOptions.coverage;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const coverageReporter = ((options as any).coverageReporter ??= {});

if (thresholds) {
coverageReporter.check = thresholds.perFile
? { each: thresholds }
: { global: thresholds };
}

if (watermarks) {
coverageReporter.watermarks = watermarks;
}
}

return options;
},
} satisfies KarmaBuilderTransformsOptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const KarmaTestRunner: TestRunner = {
}
}

if (options.codeCoverage) {
if (options.coverage) {
checker.check('karma-coverage');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ export class VitestExecutor implements TestExecutor {
}

private async initializeVitest(): Promise<Vitest> {
const { codeCoverage, reporters, outputFile, workspaceRoot, browsers, debug, watch } =
this.options;
const { coverage, reporters, outputFile, workspaceRoot, browsers, debug, watch } = this.options;
let vitestNodeModule;
try {
vitestNodeModule = await loadEsmModule<typeof import('vitest/node')>('vitest/node');
Expand Down Expand Up @@ -190,7 +189,7 @@ export class VitestExecutor implements TestExecutor {
reporters: reporters ?? ['default'],
outputFile,
watch,
coverage: generateCoverageOption(codeCoverage, this.projectName),
coverage: generateCoverageOption(coverage, this.projectName),
...debugOptions,
},
{
Expand All @@ -206,23 +205,27 @@ export class VitestExecutor implements TestExecutor {
}

function generateCoverageOption(
codeCoverage: NormalizedUnitTestBuilderOptions['codeCoverage'],
coverage: NormalizedUnitTestBuilderOptions['coverage'],
projectName: string,
): VitestCoverageOption {
if (!codeCoverage) {
if (!coverage) {
return {
enabled: false,
};
}

return {
enabled: true,
all: coverage.all,
excludeAfterRemap: true,
include: coverage.include,
reportsDirectory: toPosixPath(path.join('coverage', projectName)),
thresholds: coverage.thresholds,
watermarks: coverage.watermarks,
// Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures
...(codeCoverage.exclude ? { exclude: codeCoverage.exclude } : {}),
...(codeCoverage.reporters
? ({ reporter: codeCoverage.reporters } satisfies VitestCoverageOption)
...(coverage.exclude ? { exclude: coverage.exclude } : {}),
...(coverage.reporters
? ({ reporter: coverage.reporters } satisfies VitestCoverageOption)
: {}),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const VitestTestRunner: TestRunner = {
checker.check('jsdom');
}

if (options.codeCoverage) {
if (options.coverage) {
checker.check('@vitest/coverage-v8');
}

Expand Down
86 changes: 80 additions & 6 deletions packages/angular/build/src/builders/unit-test/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,33 @@
"description": "Enables debugging mode for tests, allowing the use of the Node Inspector.",
"default": false
},
"codeCoverage": {
"coverage": {
"type": "boolean",
"description": "Enables code coverage reporting for tests.",
"description": "Enables coverage reporting for tests.",
"default": false
},
"codeCoverageExclude": {
"coverageAll": {
"type": "boolean",
"description": "Includes all files that match the `coverageInclude` pattern in the coverage report, not just those touched by tests.",
"default": true
},
"coverageInclude": {
"type": "array",
"description": "Specifies glob patterns of files to exclude from the code coverage report.",
"description": "Specifies glob patterns of files to include in the coverage report.",
"items": {
"type": "string"
}
},
"codeCoverageReporters": {
"coverageExclude": {
"type": "array",
"description": "Specifies the reporters to use for code coverage results. Each reporter can be a string representing its name, or a tuple containing the name and an options object. Built-in reporters include 'html', 'lcov', 'lcovonly', 'text', 'text-summary', 'cobertura', 'json', and 'json-summary'.",
"description": "Specifies glob patterns of files to exclude from the coverage report.",
"items": {
"type": "string"
}
},
"coverageReporters": {
"type": "array",
"description": "Specifies the reporters to use for coverage results. Each reporter can be a string representing its name, or a tuple containing the name and an options object. Built-in reporters include 'html', 'lcov', 'lcovonly', 'text', 'text-summary', 'cobertura', 'json', and 'json-summary'.",
"items": {
"oneOf": [
{
Expand Down Expand Up @@ -108,6 +120,68 @@
]
}
},
"coverageThresholds": {
"type": "object",
"description": "Specifies minimum coverage thresholds that must be met. If thresholds are not met, the builder will exit with an error.",
"properties": {
"perFile": {
"type": "boolean",
"description": "When true, thresholds are enforced for each file individually."
},
"statements": {
"type": "number",
"description": "Minimum percentage of statements covered."
},
"branches": {
"type": "number",
"description": "Minimum percentage of branches covered."
},
"functions": {
"type": "number",
"description": "Minimum percentage of functions covered."
},
"lines": {
"type": "number",
"description": "Minimum percentage of lines covered."
}
},
"additionalProperties": false
},
"coverageWatermarks": {
"type": "object",
"description": "Specifies coverage watermarks for the HTML reporter. These determine the color coding for high, medium, and low coverage.",
"properties": {
"statements": {
"type": "array",
"description": "The high and low watermarks for statements coverage. `[low, high]`",
"items": { "type": "number" },
"minItems": 2,
"maxItems": 2
},
"branches": {
"type": "array",
"description": "The high and low watermarks for branches coverage. `[low, high]`",
"items": { "type": "number" },
"minItems": 2,
"maxItems": 2
},
"functions": {
"type": "array",
"description": "The high and low watermarks for functions coverage. `[low, high]`",
"items": { "type": "number" },
"minItems": 2,
"maxItems": 2
},
"lines": {
"type": "array",
"description": "The high and low watermarks for lines coverage. `[low, high]`",
"items": { "type": "number" },
"minItems": 2,
"maxItems": 2
}
},
"additionalProperties": false
},
"reporters": {
"type": "array",
"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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '../setup';

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
describe('Option: "codeCoverageExclude"', () => {
describe('Option: "coverageExclude"', () => {
beforeEach(async () => {
setupApplicationTarget(harness);
await harness.writeFiles({
Expand All @@ -26,7 +26,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
it('should not exclude any files from coverage when not provided', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
codeCoverage: true,
coverage: true,
});

const { result } = await harness.executeOnce();
Expand All @@ -38,8 +38,8 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
it('should exclude files from coverage that match the glob pattern', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
codeCoverage: true,
codeCoverageExclude: ['**/error.ts'],
coverage: true,
coverageExclude: ['**/error.ts'],
});

const { result } = await harness.executeOnce();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@ import {
} from '../setup';

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
describe('Option: "codeCoverageReporters"', () => {
describe('Option: "coverageReporters"', () => {
beforeEach(async () => {
setupApplicationTarget(harness);
});

it('should generate a json summary report when specified', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
codeCoverage: true,
codeCoverageReporters: ['json-summary'] as any,
coverage: true,
coverageReporters: ['json-summary'] as any,
});

const { result } = await harness.executeOnce();
Expand All @@ -35,8 +35,8 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
it('should generate multiple reports when specified', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
codeCoverage: true,
codeCoverageReporters: ['json-summary', 'lcov'] as any,
coverage: true,
coverageReporters: ['json-summary', 'lcov'] as any,
});

const { result } = await harness.executeOnce();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,26 @@ import {
} from '../setup';

describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
describe('Option: "codeCoverage"', () => {
describe('Option: "coverage"', () => {
beforeEach(async () => {
setupApplicationTarget(harness);
});

it('should not generate a code coverage report when codeCoverage is false', async () => {
it('should not generate a code coverage report when coverage is false', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
codeCoverage: false,
coverage: false,
});

const { result } = await harness.executeOnce();
expect(result?.success).toBeTrue();
expect(harness.hasFile('coverage/test/index.html')).toBeFalse();
});

it('should generate a code coverage report when codeCoverage is true', async () => {
it('should generate a code coverage report when coverage is true', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
codeCoverage: true,
coverage: true,
});

const { result } = await harness.executeOnce();
Expand Down