@angular/build 21.0.0-next.4 → 21.0.0-next.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +8 -8
  2. package/src/builders/karma/application_builder.js +5 -1
  3. package/src/builders/karma/coverage.js +1 -1
  4. package/src/builders/karma/find-tests.d.ts +1 -9
  5. package/src/builders/karma/find-tests.js +6 -106
  6. package/src/builders/unit-test/builder.js +20 -2
  7. package/src/builders/unit-test/options.d.ts +16 -2
  8. package/src/builders/unit-test/options.js +37 -4
  9. package/src/builders/unit-test/runners/karma/executor.js +26 -3
  10. package/src/builders/unit-test/runners/karma/index.js +1 -1
  11. package/src/builders/unit-test/runners/vitest/browser-provider.d.ts +4 -1
  12. package/src/builders/unit-test/runners/vitest/browser-provider.js +6 -2
  13. package/src/builders/unit-test/runners/vitest/build-options.js +6 -6
  14. package/src/builders/unit-test/runners/vitest/executor.js +30 -8
  15. package/src/builders/unit-test/runners/vitest/index.js +1 -1
  16. package/src/builders/unit-test/schema.d.ts +93 -13
  17. package/src/builders/unit-test/schema.js +12 -12
  18. package/src/builders/unit-test/schema.json +126 -33
  19. package/src/builders/unit-test/test-discovery.d.ts +25 -1
  20. package/src/builders/unit-test/test-discovery.js +194 -5
  21. package/src/tools/angular/transformers/jit-bootstrap-transformer.js +1 -1
  22. package/src/tools/angular/transformers/jit-resource-transformer.js +1 -1
  23. package/src/tools/babel/plugins/pure-toplevel-functions.d.ts +0 -1
  24. package/src/tools/babel/plugins/pure-toplevel-functions.js +21 -5
  25. package/src/tools/esbuild/angular/compiler-plugin.js +38 -14
  26. package/src/tools/esbuild/javascript-transformer-worker.js +2 -8
  27. package/src/tools/esbuild/stylesheets/less-language.js +2 -26
  28. package/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js +2 -1
  29. package/src/utils/normalize-cache.js +1 -1
  30. package/src/utils/server-rendering/utils.d.ts +1 -1
  31. package/src/utils/service-worker.d.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/build",
3
- "version": "21.0.0-next.4",
3
+ "version": "21.0.0-next.6",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,7 +23,7 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.2100.0-next.4",
26
+ "@angular-devkit/architect": "0.2100.0-next.6",
27
27
  "@babel/core": "7.28.4",
28
28
  "@babel/helper-annotate-as-pure": "7.27.3",
29
29
  "@babel/helper-split-export-declaration": "7.24.7",
@@ -31,7 +31,7 @@
31
31
  "@vitejs/plugin-basic-ssl": "2.1.0",
32
32
  "beasties": "0.3.5",
33
33
  "browserslist": "^4.26.0",
34
- "esbuild": "0.25.9",
34
+ "esbuild": "0.25.10",
35
35
  "https-proxy-agent": "7.0.6",
36
36
  "istanbul-lib-instrument": "6.0.3",
37
37
  "jsonc-parser": "3.3.1",
@@ -41,12 +41,12 @@
41
41
  "parse5-html-rewriting-stream": "8.0.0",
42
42
  "picomatch": "4.0.3",
43
43
  "piscina": "5.1.3",
44
- "rolldown": "1.0.0-beta.38",
45
- "sass": "1.92.1",
44
+ "rolldown": "1.0.0-beta.41",
45
+ "sass": "1.93.2",
46
46
  "semver": "7.7.2",
47
47
  "source-map-support": "0.5.21",
48
48
  "tinyglobby": "0.2.15",
49
- "vite": "7.1.5",
49
+ "vite": "7.1.7",
50
50
  "watchpack": "2.4.4"
51
51
  },
52
52
  "optionalDependencies": {
@@ -60,7 +60,7 @@
60
60
  "@angular/platform-browser": "^21.0.0-next.0",
61
61
  "@angular/platform-server": "^21.0.0-next.0",
62
62
  "@angular/service-worker": "^21.0.0-next.0",
63
- "@angular/ssr": "^21.0.0-next.4",
63
+ "@angular/ssr": "^21.0.0-next.6",
64
64
  "karma": "^6.4.0",
65
65
  "less": "^4.2.0",
66
66
  "ng-packagr": "^21.0.0-next.0",
@@ -112,7 +112,7 @@
112
112
  "type": "git",
113
113
  "url": "https://github.com/angular/angular-cli.git"
114
114
  },
115
- "packageManager": "pnpm@10.17.0",
115
+ "packageManager": "pnpm@10.17.1",
116
116
  "engines": {
117
117
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
118
118
  "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
@@ -155,14 +155,18 @@ async function setupBuildOptions(options, context, projectSourceRoot, outputPath
155
155
  return { buildOptions, mainName };
156
156
  }
157
157
  async function runEsbuild(buildOptions, context, projectSourceRoot) {
158
+ const usesZoneJS = buildOptions.polyfills?.includes('zone.js');
158
159
  const virtualTestBedInit = (0, virtual_module_plugin_1.createVirtualModulePlugin)({
159
160
  namespace: 'angular:test-bed-init',
160
161
  loadContent: async () => {
161
162
  const contents = [
162
163
  // Initialize the Angular testing environment
164
+ `import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';`,
163
165
  `import { getTestBed } from '@angular/core/testing';`,
164
166
  `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
165
- `getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`,
167
+ `@NgModule({ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}], })`,
168
+ `export class TestModule {}`,
169
+ `getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`,
166
170
  ` errorOnUnknownElements: true,`,
167
171
  ` errorOnUnknownProperties: true,`,
168
172
  `});`,
@@ -17,7 +17,7 @@ const tinyglobby_1 = require("tinyglobby");
17
17
  function createInstrumentationFilter(includedBasePath, excludedPaths) {
18
18
  return (request) => {
19
19
  return (!excludedPaths.has(request) &&
20
- !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]/.test(request) &&
20
+ !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]|[\\/]\.angular[\\/]/.test(request) &&
21
21
  request.startsWith(includedBasePath));
22
22
  };
23
23
  }
@@ -5,12 +5,4 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
- export declare function findTests(include: string[], exclude: string[], workspaceRoot: string, projectSourceRoot: string): Promise<string[]>;
9
- interface TestEntrypointsOptions {
10
- projectSourceRoot: string;
11
- workspaceRoot: string;
12
- removeTestExtension?: boolean;
13
- }
14
- /** Generate unique bundle names for a set of test files. */
15
- export declare function getTestEntrypoints(testFiles: string[], { projectSourceRoot, workspaceRoot, removeTestExtension }: TestEntrypointsOptions): Map<string, string>;
16
- export {};
8
+ export { findTests, getTestEntrypoints } from '../unit-test/test-discovery';
@@ -7,109 +7,9 @@
7
7
  * found in the LICENSE file at https://angular.dev/license
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.findTests = findTests;
11
- exports.getTestEntrypoints = getTestEntrypoints;
12
- const node_fs_1 = require("node:fs");
13
- const node_path_1 = require("node:path");
14
- const tinyglobby_1 = require("tinyglobby");
15
- const path_1 = require("../../utils/path");
16
- /* Go through all patterns and find unique list of files */
17
- async function findTests(include, exclude, workspaceRoot, projectSourceRoot) {
18
- const matchingTestsPromises = include.map((pattern) => findMatchingTests(pattern, exclude, workspaceRoot, projectSourceRoot));
19
- const files = await Promise.all(matchingTestsPromises);
20
- // Unique file names
21
- return [...new Set(files.flat())];
22
- }
23
- /** Generate unique bundle names for a set of test files. */
24
- function getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot, removeTestExtension }) {
25
- const seen = new Set();
26
- return new Map(Array.from(testFiles, (testFile) => {
27
- const relativePath = removeRoots(testFile, [projectSourceRoot, workspaceRoot])
28
- // Strip leading dots and path separators.
29
- .replace(/^[./\\]+/, '')
30
- // Replace any path separators with dashes.
31
- .replace(/[/\\]/g, '-');
32
- let fileName = (0, node_path_1.basename)(relativePath, (0, node_path_1.extname)(relativePath));
33
- if (removeTestExtension) {
34
- fileName = fileName.replace(/\.(spec|test)$/, '');
35
- }
36
- const baseName = `spec-${fileName}`;
37
- let uniqueName = baseName;
38
- let suffix = 2;
39
- while (seen.has(uniqueName)) {
40
- uniqueName = `${baseName}-${suffix}`.replace(/([^\w](?:spec|test))-([\d]+)$/, '-$2$1');
41
- ++suffix;
42
- }
43
- seen.add(uniqueName);
44
- return [uniqueName, testFile];
45
- }));
46
- }
47
- const removeLeadingSlash = (pattern) => {
48
- if (pattern.charAt(0) === '/') {
49
- return pattern.substring(1);
50
- }
51
- return pattern;
52
- };
53
- const removeRelativeRoot = (path, root) => {
54
- if (path.startsWith(root)) {
55
- return path.substring(root.length);
56
- }
57
- return path;
58
- };
59
- function removeRoots(path, roots) {
60
- for (const root of roots) {
61
- if (path.startsWith(root)) {
62
- return path.substring(root.length);
63
- }
64
- }
65
- return (0, node_path_1.basename)(path);
66
- }
67
- async function findMatchingTests(pattern, ignore, workspaceRoot, projectSourceRoot) {
68
- // normalize pattern, glob lib only accepts forward slashes
69
- let normalizedPattern = (0, path_1.toPosixPath)(pattern);
70
- normalizedPattern = removeLeadingSlash(normalizedPattern);
71
- const relativeProjectRoot = (0, path_1.toPosixPath)((0, node_path_1.relative)(workspaceRoot, projectSourceRoot) + '/');
72
- // remove relativeProjectRoot to support relative paths from root
73
- // such paths are easy to get when running scripts via IDEs
74
- normalizedPattern = removeRelativeRoot(normalizedPattern, relativeProjectRoot);
75
- // special logic when pattern does not look like a glob
76
- if (!(0, tinyglobby_1.isDynamicPattern)(normalizedPattern)) {
77
- if (await isDirectory((0, node_path_1.join)(projectSourceRoot, normalizedPattern))) {
78
- normalizedPattern = `${normalizedPattern}/**/*.spec.@(ts|tsx)`;
79
- }
80
- else {
81
- // see if matching spec file exists
82
- const fileExt = (0, node_path_1.extname)(normalizedPattern);
83
- // Replace extension to `.spec.ext`. Example: `src/app/app.component.ts`-> `src/app/app.component.spec.ts`
84
- const potentialSpec = (0, node_path_1.join)(projectSourceRoot, (0, node_path_1.dirname)(normalizedPattern), `${(0, node_path_1.basename)(normalizedPattern, fileExt)}.spec${fileExt}`);
85
- if (await exists(potentialSpec)) {
86
- return [potentialSpec];
87
- }
88
- }
89
- }
90
- // normalize the patterns in the ignore list
91
- const normalizedIgnorePatternList = ignore.map((pattern) => removeRelativeRoot(removeLeadingSlash((0, path_1.toPosixPath)(pattern)), relativeProjectRoot));
92
- return (0, tinyglobby_1.glob)(normalizedPattern, {
93
- cwd: projectSourceRoot,
94
- absolute: true,
95
- ignore: ['**/node_modules/**', ...normalizedIgnorePatternList],
96
- });
97
- }
98
- async function isDirectory(path) {
99
- try {
100
- const stats = await node_fs_1.promises.stat(path);
101
- return stats.isDirectory();
102
- }
103
- catch {
104
- return false;
105
- }
106
- }
107
- async function exists(path) {
108
- try {
109
- await node_fs_1.promises.access(path, node_fs_1.constants.F_OK);
110
- return true;
111
- }
112
- catch {
113
- return false;
114
- }
115
- }
10
+ exports.getTestEntrypoints = exports.findTests = void 0;
11
+ // This file is a compatibility layer that re-exports the test discovery logic from its new location.
12
+ // This is necessary to avoid breaking the Karma builder, which still depends on this file.
13
+ var test_discovery_1 = require("../unit-test/test-discovery");
14
+ Object.defineProperty(exports, "findTests", { enumerable: true, get: function () { return test_discovery_1.findTests; } });
15
+ Object.defineProperty(exports, "getTestEntrypoints", { enumerable: true, get: function () { return test_discovery_1.getTestEntrypoints; } });
@@ -107,6 +107,7 @@ const application_1 = require("../application");
107
107
  const results_1 = require("../application/results");
108
108
  const options_1 = require("./options");
109
109
  const dependency_checker_1 = require("./runners/dependency-checker");
110
+ const test_discovery_1 = require("./test-discovery");
110
111
  async function loadTestRunner(runnerName) {
111
112
  // Harden against directory traversal
112
113
  if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) {
@@ -226,6 +227,15 @@ async function* execute(options, context, extensions) {
226
227
  yield { success: false };
227
228
  return;
228
229
  }
230
+ if (normalizedOptions.listTests) {
231
+ const testFiles = await (0, test_discovery_1.findTests)(normalizedOptions.include, normalizedOptions.exclude ?? [], normalizedOptions.workspaceRoot, normalizedOptions.projectSourceRoot);
232
+ context.logger.info('Discovered test files:');
233
+ for (const file of testFiles) {
234
+ context.logger.info(` ${node_path_1.default.relative(normalizedOptions.workspaceRoot, file)}`);
235
+ }
236
+ yield { success: true };
237
+ return;
238
+ }
229
239
  if (runner.isStandalone) {
230
240
  try {
231
241
  const env_1 = { stack: [], error: void 0, hasError: false };
@@ -256,7 +266,15 @@ async function* execute(options, context, extensions) {
256
266
  // Get base build options from the buildTarget
257
267
  let buildTargetOptions;
258
268
  try {
259
- buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
269
+ const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget);
270
+ if (builderName !== '@angular/build:application' &&
271
+ // TODO: Add comprehensive support for ng-packagr.
272
+ builderName !== '@angular/build:ng-packagr') {
273
+ context.logger.warn(`The 'buildTarget' is configured to use '${builderName}', which is not supported. ` +
274
+ `The 'unit-test' builder is designed to work with '@angular/build:application'. ` +
275
+ 'Unexpected behavior or build failures may occur.');
276
+ }
277
+ buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), builderName));
260
278
  }
261
279
  catch (e) {
262
280
  (0, error_1.assertIsError)(e);
@@ -293,8 +311,8 @@ async function* execute(options, context, extensions) {
293
311
  ...buildTargetOptions,
294
312
  ...runnerBuildOptions,
295
313
  watch: normalizedOptions.watch,
296
- tsConfig: normalizedOptions.tsConfig,
297
314
  progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress,
315
+ ...(normalizedOptions.tsConfig ? { tsConfig: normalizedOptions.tsConfig } : {}),
298
316
  };
299
317
  const dumpDirectory = normalizedOptions.dumpVirtualFiles
300
318
  ? node_path_1.default.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files')
@@ -18,19 +18,33 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s
18
18
  exclude: string[] | undefined;
19
19
  filter: string | undefined;
20
20
  runnerName: import("./schema").Runner;
21
- codeCoverage: {
21
+ coverage: {
22
+ all: boolean | undefined;
22
23
  exclude: string[] | undefined;
24
+ include: string[] | undefined;
23
25
  reporters: [string, Record<string, unknown>][] | undefined;
26
+ thresholds: import("./schema").CoverageThresholds | undefined;
27
+ watermarks: {
28
+ statements?: [number, number];
29
+ branches?: [number, number];
30
+ functions?: [number, number];
31
+ lines?: [number, number];
32
+ };
24
33
  } | undefined;
25
- tsConfig: string;
34
+ tsConfig: string | undefined;
26
35
  buildProgress: boolean | undefined;
27
36
  reporters: [string, Record<string, unknown>][] | undefined;
28
37
  outputFile: string | undefined;
29
38
  browsers: string[] | undefined;
39
+ browserViewport: {
40
+ width: number;
41
+ height: number;
42
+ } | undefined;
30
43
  watch: boolean;
31
44
  debug: boolean;
32
45
  providersFile: string | undefined;
33
46
  setupFiles: string[];
34
47
  dumpVirtualFiles: boolean | undefined;
48
+ listTests: boolean | undefined;
35
49
  }>;
36
50
  export declare function injectTestingPolyfills(polyfills?: string[]): string[];
@@ -13,10 +13,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.normalizeOptions = normalizeOptions;
14
14
  exports.injectTestingPolyfills = injectTestingPolyfills;
15
15
  const architect_1 = require("@angular-devkit/architect");
16
+ const node_fs_1 = require("node:fs");
16
17
  const node_path_1 = __importDefault(require("node:path"));
17
18
  const normalize_cache_1 = require("../../utils/normalize-cache");
18
19
  const project_metadata_1 = require("../../utils/project-metadata");
19
20
  const tty_1 = require("../../utils/tty");
21
+ async function exists(path) {
22
+ try {
23
+ await node_fs_1.promises.access(path, node_fs_1.constants.F_OK);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
20
30
  function normalizeReporterOption(reporters) {
21
31
  return reporters?.map((entry) => typeof entry === 'string'
22
32
  ? [entry, {}]
@@ -33,7 +43,22 @@ async function normalizeOptions(context, projectName, options) {
33
43
  // Target specifier defaults to the current project's build target using a development configuration
34
44
  const buildTargetSpecifier = options.buildTarget ?? `::development`;
35
45
  const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, projectName, 'build');
36
- const { tsConfig, runner, browsers, progress, filter } = options;
46
+ const { runner, browsers, progress, filter, browserViewport } = options;
47
+ const [width, height] = browserViewport?.split('x').map(Number) ?? [];
48
+ let tsConfig = options.tsConfig;
49
+ if (tsConfig) {
50
+ const fullTsConfigPath = node_path_1.default.join(workspaceRoot, tsConfig);
51
+ if (!(await exists(fullTsConfigPath))) {
52
+ throw new Error(`The specified tsConfig file '${tsConfig}' does not exist.`);
53
+ }
54
+ }
55
+ else {
56
+ const tsconfigSpecPath = node_path_1.default.join(projectRoot, 'tsconfig.spec.json');
57
+ if (await exists(tsconfigSpecPath)) {
58
+ // The application builder expects a path relative to the workspace root.
59
+ tsConfig = node_path_1.default.relative(workspaceRoot, tsconfigSpecPath);
60
+ }
61
+ }
37
62
  return {
38
63
  // Project/workspace information
39
64
  workspaceRoot,
@@ -46,10 +71,16 @@ async function normalizeOptions(context, projectName, options) {
46
71
  exclude: options.exclude,
47
72
  filter,
48
73
  runnerName: runner,
49
- codeCoverage: options.codeCoverage
74
+ coverage: options.coverage
50
75
  ? {
51
- exclude: options.codeCoverageExclude,
52
- reporters: normalizeReporterOption(options.codeCoverageReporters),
76
+ all: options.coverageAll,
77
+ exclude: options.coverageExclude,
78
+ include: options.coverageInclude,
79
+ reporters: normalizeReporterOption(options.coverageReporters),
80
+ thresholds: options.coverageThresholds,
81
+ // The schema generation tool doesn't support tuple types for items, but the schema validation
82
+ // does ensure that the array has exactly two numbers.
83
+ watermarks: options.coverageWatermarks,
53
84
  }
54
85
  : undefined,
55
86
  tsConfig,
@@ -57,6 +88,7 @@ async function normalizeOptions(context, projectName, options) {
57
88
  reporters: normalizeReporterOption(options.reporters),
58
89
  outputFile: options.outputFile,
59
90
  browsers,
91
+ browserViewport: width && height ? { width, height } : undefined,
60
92
  watch: options.watch ?? (0, tty_1.isTTY)(),
61
93
  debug: options.debug ?? false,
62
94
  providersFile: options.providersFile && node_path_1.default.join(workspaceRoot, options.providersFile),
@@ -64,6 +96,7 @@ async function normalizeOptions(context, projectName, options) {
64
96
  ? options.setupFiles.map((setupFile) => node_path_1.default.join(workspaceRoot, setupFile))
65
97
  : [],
66
98
  dumpVirtualFiles: options.dumpVirtualFiles,
99
+ listTests: options.listTests,
67
100
  };
68
101
  }
69
102
  function injectTestingPolyfills(polyfills = []) {
@@ -50,15 +50,24 @@ class KarmaExecutor {
50
50
  }
51
51
  async *execute() {
52
52
  const { context, options: unitTestOptions } = this;
53
+ if (unitTestOptions.browserViewport) {
54
+ context.logger.warn('The "karma" test runner does not support the "browserViewport" option. The option will be ignored.');
55
+ }
53
56
  if (unitTestOptions.debug) {
54
57
  context.logger.warn('The "karma" test runner does not support the "debug" option. The option will be ignored.');
55
58
  }
56
59
  if (unitTestOptions.setupFiles.length) {
57
60
  context.logger.warn('The "karma" test runner does not support the "setupFiles" option. The option will be ignored.');
58
61
  }
62
+ if (unitTestOptions.coverage?.all) {
63
+ context.logger.warn('The "karma" test runner does not support the "coverageAll" option. The option will be ignored.');
64
+ }
65
+ if (unitTestOptions.coverage?.include) {
66
+ context.logger.warn('The "karma" test runner does not support the "coverageInclude" option. The option will be ignored.');
67
+ }
59
68
  const buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(unitTestOptions.buildTarget), await context.getBuilderNameForTarget(unitTestOptions.buildTarget)));
60
69
  const karmaOptions = {
61
- tsConfig: unitTestOptions.tsConfig,
70
+ tsConfig: unitTestOptions.tsConfig ?? buildTargetOptions.tsConfig,
62
71
  polyfills: buildTargetOptions.polyfills,
63
72
  assets: buildTargetOptions.assets,
64
73
  scripts: buildTargetOptions.scripts,
@@ -76,8 +85,8 @@ class KarmaExecutor {
76
85
  poll: buildTargetOptions.poll,
77
86
  preserveSymlinks: buildTargetOptions.preserveSymlinks,
78
87
  browsers: unitTestOptions.browsers?.join(','),
79
- codeCoverage: !!unitTestOptions.codeCoverage,
80
- codeCoverageExclude: unitTestOptions.codeCoverage?.exclude,
88
+ codeCoverage: !!unitTestOptions.coverage,
89
+ codeCoverageExclude: unitTestOptions.coverage?.exclude,
81
90
  fileReplacements: buildTargetOptions.fileReplacements,
82
91
  reporters: unitTestOptions.reporters?.map((reporter) => {
83
92
  // Karma only supports string reporters.
@@ -104,6 +113,20 @@ class KarmaExecutor {
104
113
  options.client.args ??= [];
105
114
  options.client.args.push('--grep', filter);
106
115
  }
116
+ // Add coverage options
117
+ if (unitTestOptions.coverage) {
118
+ const { thresholds, watermarks } = unitTestOptions.coverage;
119
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
120
+ const coverageReporter = (options.coverageReporter ??= {});
121
+ if (thresholds) {
122
+ coverageReporter.check = thresholds.perFile
123
+ ? { each: thresholds }
124
+ : { global: thresholds };
125
+ }
126
+ if (watermarks) {
127
+ coverageReporter.watermarks = watermarks;
128
+ }
129
+ }
107
130
  return options;
108
131
  },
109
132
  };
@@ -26,7 +26,7 @@ const KarmaTestRunner = {
26
26
  checker.check(launcherName);
27
27
  }
28
28
  }
29
- if (options.codeCoverage) {
29
+ if (options.coverage) {
30
30
  checker.check('karma-coverage');
31
31
  }
32
32
  checker.report();
@@ -9,4 +9,7 @@ export interface BrowserConfiguration {
9
9
  browser?: import('vitest/node').BrowserConfigOptions;
10
10
  errors?: string[];
11
11
  }
12
- export declare function setupBrowserConfiguration(browsers: string[] | undefined, debug: boolean, projectSourceRoot: string): BrowserConfiguration;
12
+ export declare function setupBrowserConfiguration(browsers: string[] | undefined, debug: boolean, projectSourceRoot: string, viewport: {
13
+ width: number;
14
+ height: number;
15
+ } | undefined): BrowserConfiguration;
@@ -28,7 +28,7 @@ function normalizeBrowserName(browserName) {
28
28
  const normalized = browserName.toLowerCase();
29
29
  return normalized.replace(/headless$/, '');
30
30
  }
31
- function setupBrowserConfiguration(browsers, debug, projectSourceRoot) {
31
+ function setupBrowserConfiguration(browsers, debug, projectSourceRoot, viewport) {
32
32
  if (browsers === undefined) {
33
33
  return {};
34
34
  }
@@ -57,10 +57,14 @@ function setupBrowserConfiguration(browsers, debug, projectSourceRoot) {
57
57
  if (errors) {
58
58
  return { errors };
59
59
  }
60
+ const isCI = !!process.env['CI'];
61
+ const headless = isCI || browsers.some((name) => name.toLowerCase().includes('headless'));
60
62
  const browser = {
61
63
  enabled: true,
62
64
  provider,
63
- headless: browsers.some((name) => name.toLowerCase().includes('headless')),
65
+ headless,
66
+ ui: !headless,
67
+ viewport,
64
68
  instances: browsers.map((browserName) => ({
65
69
  browser: normalizeBrowserName(browserName),
66
70
  })),
@@ -16,7 +16,8 @@ const path_1 = require("../../../../utils/path");
16
16
  const schema_1 = require("../../../application/schema");
17
17
  const options_1 = require("../../options");
18
18
  const test_discovery_1 = require("../../test-discovery");
19
- function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
19
+ function createTestBedInitVirtualFile(providersFile, projectSourceRoot, polyfills = []) {
20
+ const usesZoneJS = polyfills.includes('zone.js');
20
21
  let providersImport = 'const providers = [];';
21
22
  if (providersFile) {
22
23
  const relativePath = node_path_1.default.relative(projectSourceRoot, providersFile);
@@ -26,7 +27,7 @@ function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
26
27
  }
27
28
  return `
28
29
  // Initialize the Angular testing environment
29
- import { NgModule } from '@angular/core';
30
+ import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';
30
31
  import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';
31
32
  import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
32
33
  ${providersImport}
@@ -34,7 +35,7 @@ function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
34
35
  beforeEach(getCleanupHook(false));
35
36
  afterEach(getCleanupHook(true));
36
37
  @NgModule({
37
- providers,
38
+ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}...providers],
38
39
  })
39
40
  export class TestModule {}
40
41
  getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
@@ -54,7 +55,7 @@ function adjustOutputHashing(hashing) {
54
55
  }
55
56
  }
56
57
  async function getVitestBuildOptions(options, baseBuildOptions) {
57
- const { workspaceRoot, projectSourceRoot, include, exclude = [], watch, tsConfig, providersFile, } = options;
58
+ const { workspaceRoot, projectSourceRoot, include, exclude = [], watch, providersFile } = options;
58
59
  // Find test files
59
60
  const testFiles = await (0, test_discovery_1.findTests)(include, exclude, workspaceRoot, projectSourceRoot);
60
61
  if (testFiles.length === 0) {
@@ -86,12 +87,11 @@ async function getVitestBuildOptions(options, baseBuildOptions) {
86
87
  sourceMap: { scripts: true, vendor: false, styles: false },
87
88
  outputHashing: adjustOutputHashing(baseBuildOptions.outputHashing),
88
89
  optimization: false,
89
- tsConfig,
90
90
  entryPoints,
91
91
  externalDependencies: ['vitest', '@vitest/browser/context'],
92
92
  };
93
93
  buildOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildOptions.polyfills);
94
- const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot);
94
+ const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot, buildOptions.polyfills);
95
95
  return {
96
96
  buildOptions,
97
97
  virtualFiles: {
@@ -101,7 +101,7 @@ class VitestExecutor {
101
101
  return testSetupFiles;
102
102
  }
103
103
  async initializeVitest() {
104
- const { codeCoverage, reporters, outputFile, workspaceRoot, browsers, debug, watch } = this.options;
104
+ const { coverage, reporters, outputFile, workspaceRoot, browsers, debug, watch, browserViewport, } = this.options;
105
105
  let vitestNodeModule;
106
106
  try {
107
107
  vitestNodeModule = await (0, load_esm_1.loadEsmModule)('vitest/node');
@@ -115,7 +115,7 @@ class VitestExecutor {
115
115
  }
116
116
  const { startVitest } = vitestNodeModule;
117
117
  // Setup vitest browser options if configured
118
- const browserOptions = (0, browser_provider_1.setupBrowserConfiguration)(browsers, debug, this.options.projectSourceRoot);
118
+ const browserOptions = (0, browser_provider_1.setupBrowserConfiguration)(browsers, debug, this.options.projectSourceRoot, browserViewport);
119
119
  if (browserOptions.errors?.length) {
120
120
  throw new Error(browserOptions.errors.join('\n'));
121
121
  }
@@ -148,7 +148,7 @@ class VitestExecutor {
148
148
  reporters: reporters ?? ['default'],
149
149
  outputFile,
150
150
  watch,
151
- coverage: generateCoverageOption(codeCoverage),
151
+ coverage: await generateCoverageOption(coverage, this.projectName),
152
152
  ...debugOptions,
153
153
  }, {
154
154
  server: {
@@ -161,19 +161,41 @@ class VitestExecutor {
161
161
  }
162
162
  }
163
163
  exports.VitestExecutor = VitestExecutor;
164
- function generateCoverageOption(codeCoverage) {
165
- if (!codeCoverage) {
164
+ async function generateCoverageOption(coverage, projectName) {
165
+ if (!coverage) {
166
166
  return {
167
167
  enabled: false,
168
168
  };
169
169
  }
170
+ let defaultExcludes = [];
171
+ if (coverage.exclude) {
172
+ try {
173
+ const vitestConfig = await (0, load_esm_1.loadEsmModule)('vitest/config');
174
+ defaultExcludes = vitestConfig.coverageConfigDefaults.exclude;
175
+ }
176
+ catch { }
177
+ }
170
178
  return {
171
179
  enabled: true,
180
+ all: coverage.all,
172
181
  excludeAfterRemap: true,
182
+ include: coverage.include,
183
+ reportsDirectory: (0, path_1.toPosixPath)(node_path_1.default.join('coverage', projectName)),
184
+ thresholds: coverage.thresholds,
185
+ watermarks: coverage.watermarks,
173
186
  // Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures
174
- ...(codeCoverage.exclude ? { exclude: codeCoverage.exclude } : {}),
175
- ...(codeCoverage.reporters
176
- ? { reporter: codeCoverage.reporters }
187
+ ...(coverage.exclude
188
+ ? {
189
+ exclude: [
190
+ // Augment the default exclude https://vitest.dev/config/#coverage-exclude
191
+ // with the user defined exclusions
192
+ ...coverage.exclude,
193
+ ...defaultExcludes,
194
+ ],
195
+ }
196
+ : {}),
197
+ ...(coverage.reporters
198
+ ? { reporter: coverage.reporters }
177
199
  : {}),
178
200
  };
179
201
  }
@@ -30,7 +30,7 @@ const VitestTestRunner = {
30
30
  // JSDOM is used when no browsers are specified
31
31
  checker.check('jsdom');
32
32
  }
33
- if (options.codeCoverage) {
33
+ if (options.coverage) {
34
34
  checker.check('@vitest/coverage-v8');
35
35
  }
36
36
  checker.report();