diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 9eb1a552bdd6..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( @@ -100,6 +117,7 @@ export async function* execute( const buildOptions: ApplicationBuilderInternalOptions = { ...buildTargetOptions, watch: normalizedOptions.watch, + incrementalResults: normalizedOptions.watch, outputPath, index: false, browser: undefined, @@ -171,44 +189,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, }; }