From f1d2ef737ae1af2a826066bcf23fdf3585e3b7de Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 8 May 2025 11:55:12 -0400 Subject: [PATCH 1/2] fix(@angular/build): allow vitest-based unit testing to use watch option When using the `application` build system with the experimental `unit-test` vitest support, the `watch` option will now be passed through to the underlying test runner. This allows vitest to be used for watch-based test development. Incremental test file updates from the build system are also enabled in watch mode as well to remove the need to rewrite all output files on a rebuild. --- .../build/src/builders/unit-test/builder.ts | 77 +++++++++++-------- .../build/src/builders/unit-test/options.ts | 6 +- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 9eb1a552bdd6..a572b12b30e9 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -100,6 +100,7 @@ export async function* execute( const buildOptions: ApplicationBuilderInternalOptions = { ...buildTargetOptions, watch: normalizedOptions.watch, + incrementalResults: normalizedOptions.watch, outputPath, index: false, browser: undefined, @@ -171,44 +172,52 @@ export async function* execute( }; } - for await (const result of buildApplicationInternal(buildOptions, context, extensions)) { - if (result.kind === ResultKind.Failure) { - continue; - } else if (result.kind !== ResultKind.Full) { - assert.fail('A full build result is required from the application builder.'); - } - - assert(result.files, 'Builder did not provide result files.'); - - await writeTestFiles(result.files, outputPath); - - const setupFiles = ['init-testbed.js']; - if (buildTargetOptions?.polyfills?.length) { - setupFiles.push('polyfills.js'); - } + const setupFiles = ['init-testbed.js']; + if (buildTargetOptions?.polyfills?.length) { + setupFiles.push('polyfills.js'); + } - instance ??= await startVitest('test', undefined /* cliFilters */, undefined /* options */, { - test: { - root: outputPath, - setupFiles, - // Use `jsdom` if no browsers are explicitly configured. - // `node` is effectively no "environment" and the default. - environment: browser ? 'node' : 'jsdom', - watch: normalizedOptions.watch, - browser, - reporters: normalizedOptions.reporters ?? ['default'], - coverage: { - enabled: normalizedOptions.codeCoverage, - exclude: normalizedOptions.codeCoverageExclude, - excludeAfterRemap: true, + try { + for await (const result of buildApplicationInternal(buildOptions, context, extensions)) { + if (result.kind === ResultKind.Failure) { + continue; + } else if (result.kind !== ResultKind.Full && result.kind !== ResultKind.Incremental) { + assert.fail( + 'A full and/or incremental build result is required from the application builder.', + ); + } + assert(result.files, 'Builder did not provide result files.'); + + await writeTestFiles(result.files, outputPath); + + instance ??= await startVitest('test', undefined /* cliFilters */, undefined /* options */, { + test: { + root: outputPath, + setupFiles, + // Use `jsdom` if no browsers are explicitly configured. + // `node` is effectively no "environment" and the default. + environment: browser ? 'node' : 'jsdom', + watch: normalizedOptions.watch, + browser, + reporters: normalizedOptions.reporters ?? ['default'], + coverage: { + enabled: normalizedOptions.codeCoverage, + exclude: normalizedOptions.codeCoverageExclude, + excludeAfterRemap: true, + }, }, - }, - }); + }); - // Check if all the tests pass to calculate the result - const testModules = instance.state.getTestModules(); + // Check if all the tests pass to calculate the result + const testModules = instance.state.getTestModules(); - yield { success: testModules.every((testModule) => testModule.ok()) }; + yield { success: testModules.every((testModule) => testModule.ok()) }; + } + } finally { + if (normalizedOptions.watch) { + // Vitest will automatically close if not using watch mode + await instance?.close(); + } } } diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index 2cd09a3b03e9..6bfe7361eebb 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -32,7 +32,8 @@ export async function normalizeOptions( const buildTargetSpecifier = options.buildTarget ?? `::development`; const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build'); - const { codeCoverage, codeCoverageExclude, tsConfig, runner, reporters, browsers } = options; + const { codeCoverage, codeCoverageExclude, tsConfig, runner, reporters, browsers, watch } = + options; return { // Project/workspace information @@ -50,8 +51,7 @@ export async function normalizeOptions( tsConfig, reporters, browsers, - // TODO: Implement watch support - watch: false, + watch, }; } From 28da4f6b4a11a3c5165833b78a90d3715974e97a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 8 May 2025 12:05:18 -0400 Subject: [PATCH 2/2] fix(@angular/build): show unit-test error for missing vitest package When using the experimental `unit-test` builder with `vitest` as the runner, an error message will now be shown if the `vitest` package cannot be loaded. The error message includes a suggestion to install the package if not present. --- .../build/src/builders/unit-test/builder.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index a572b12b30e9..f103b6ddc8af 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -12,6 +12,7 @@ import { randomUUID } from 'node:crypto'; import { createRequire } from 'node:module'; import path from 'node:path'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; +import { assertIsError } from '../../utils/error'; import { loadEsmModule } from '../../utils/load-esm'; import { buildApplicationInternal } from '../application'; import type { @@ -31,6 +32,7 @@ export type { UnitTestOptions }; /** * @experimental Direct usage of this function is considered experimental. */ +// eslint-disable-next-line max-lines-per-function export async function* execute( options: UnitTestOptions, context: BuilderContext, @@ -84,7 +86,22 @@ export async function* execute( const entryPoints = getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot }); entryPoints.set('init-testbed', 'angular:test-bed-init'); - const { startVitest } = await loadEsmModule('vitest/node'); + let vitestNodeModule; + try { + vitestNodeModule = await loadEsmModule('vitest/node'); + } catch (error: unknown) { + assertIsError(error); + if (error.code !== 'ERR_MODULE_NOT_FOUND') { + throw error; + } + + context.logger.error( + 'The `vitest` package was not found. Please install the package and rerun the test command.', + ); + + return; + } + const { startVitest } = vitestNodeModule; // Setup test file build options based on application build target options const buildTargetOptions = (await context.validateOptions(