diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index dab2f0e18e54..175441ea7df2 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -97,6 +97,7 @@ async function* runBuildAndTest( context: BuilderContext, extensions: ApplicationBuilderExtensions | undefined, ): AsyncIterable { + let consecutiveErrorCount = 0; for await (const buildResult of buildApplicationInternal( applicationBuildOptions, context, @@ -117,7 +118,25 @@ async function* runBuildAndTest( assert(buildResult.files, 'Builder did not provide result files.'); // Pass the build artifacts to the executor - yield* executor.execute(buildResult); + try { + yield* executor.execute(buildResult); + + // Successful execution resets the failure counter + consecutiveErrorCount = 0; + } catch (e) { + assertIsError(e); + context.logger.error(`An exception occurred during test execution:\n${e.stack ?? e.message}`); + yield { success: false }; + consecutiveErrorCount++; + } + + if (consecutiveErrorCount >= 3) { + context.logger.error( + 'Test runner process has failed multiple times in a row. Please fix the configuration and restart the process.', + ); + + return; + } } } @@ -141,15 +160,36 @@ export async function* execute( `NOTE: The "unit-test" builder is currently EXPERIMENTAL and not ready for production use.`, ); - const normalizedOptions = await normalizeOptions(context, projectName, options); - const runner = await loadTestRunner(normalizedOptions.runnerName); + // Initialize the test runner and normalize options + let runner; + let normalizedOptions; + try { + normalizedOptions = await normalizeOptions(context, projectName, options); + runner = await loadTestRunner(normalizedOptions.runnerName); + } catch (e) { + assertIsError(e); + context.logger.error( + `An exception occurred during initialization of the test runner:\n${e.stack ?? e.message}`, + ); + yield { success: false }; + + return; + } if (runner.isStandalone) { - await using executor = await runner.createExecutor(context, normalizedOptions, undefined); - yield* executor.execute({ - kind: ResultKind.Full, - files: {}, - }); + try { + await using executor = await runner.createExecutor(context, normalizedOptions, undefined); + yield* executor.execute({ + kind: ResultKind.Full, + files: {}, + }); + } catch (e) { + assertIsError(e); + context.logger.error( + `An exception occurred during standalone test execution:\n${e.stack ?? e.message}`, + ); + yield { success: false }; + } return; } @@ -164,41 +204,65 @@ export async function* execute( } catch (e) { assertIsError(e); context.logger.error( - `Could not load build target options for "${targetStringFromTarget(normalizedOptions.buildTarget)}".\n` + + `Could not load build target options for "${targetStringFromTarget( + normalizedOptions.buildTarget, + )}".\n` + `Please check your 'angular.json' configuration.\n` + `Error: ${e.message}`, ); + yield { success: false }; return; } - // Get runner-specific build options from the hook - const { - buildOptions: runnerBuildOptions, - virtualFiles, - testEntryPointMappings, - } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions); + // Get runner-specific build options + let runnerBuildOptions; + let virtualFiles; + let testEntryPointMappings; + try { + ({ + buildOptions: runnerBuildOptions, + virtualFiles, + testEntryPointMappings, + } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions)); + } catch (e) { + assertIsError(e); + context.logger.error( + `An exception occurred while getting runner-specific build options:\n${e.stack ?? e.message}`, + ); + yield { success: false }; + + return; + } - await using executor = await runner.createExecutor( - context, - normalizedOptions, - testEntryPointMappings, - ); + try { + await using executor = await runner.createExecutor( + context, + normalizedOptions, + testEntryPointMappings, + ); - const finalExtensions = prepareBuildExtensions( - virtualFiles, - normalizedOptions.projectSourceRoot, - extensions, - ); + const finalExtensions = prepareBuildExtensions( + virtualFiles, + normalizedOptions.projectSourceRoot, + extensions, + ); - // Prepare and run the application build - const applicationBuildOptions = { - ...buildTargetOptions, - ...runnerBuildOptions, - watch: normalizedOptions.watch, - tsConfig: normalizedOptions.tsConfig, - progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress, - } satisfies ApplicationBuilderInternalOptions; + // Prepare and run the application build + const applicationBuildOptions = { + ...buildTargetOptions, + ...runnerBuildOptions, + watch: normalizedOptions.watch, + tsConfig: normalizedOptions.tsConfig, + progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress, + } satisfies ApplicationBuilderInternalOptions; - yield* runBuildAndTest(executor, applicationBuildOptions, context, finalExtensions); + yield* runBuildAndTest(executor, applicationBuildOptions, context, finalExtensions); + } catch (e) { + assertIsError(e); + context.logger.error( + `An exception occurred while creating the test executor:\n${e.stack ?? e.message}`, + ); + yield { success: false }; + } } diff --git a/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts index be759449d8bc..f1cdd4a14f0b 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/include_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "include"', () => { + describe('Option: "include"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); @@ -35,18 +35,10 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { test: 'relative path from workspace to spec', input: ['src/app/app.component.spec.ts'], }, - { - test: 'relative path from workspace to file', - input: ['src/app/app.component.ts'], - }, { test: 'relative path from project root to spec', input: ['app/services/test.service.spec.ts'], }, - { - test: 'relative path from project root to file', - input: ['app/services/test.service.ts'], - }, { test: 'relative path from workspace to directory', input: ['src/app/services'], @@ -59,10 +51,6 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { test: 'glob with spec suffix', input: ['**/*.pipe.spec.ts', '**/*.pipe.spec.ts', '**/*test.service.spec.ts'], }, - { - test: 'glob with forward slash and spec suffix', - input: ['/**/*test.service.spec.ts'], - }, ].forEach((options, index) => { it(`should work with ${options.test} (${index})`, async () => { await harness.writeFiles({ diff --git a/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts b/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts index d5b64923d70f..d69f6480c54d 100644 --- a/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/options/providers-file_spec.ts @@ -15,7 +15,7 @@ import { } from '../setup'; describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { - xdescribe('Option: "providersFile"', () => { + describe('Option: "providersFile"', () => { beforeEach(async () => { setupApplicationTarget(harness); }); @@ -26,10 +26,12 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { providersFile: 'src/my.providers.ts', }); - const { result, error } = await harness.executeOnce({ outputLogsOnFailure: false }); - expect(result).toBeUndefined(); - expect(error?.message).toMatch( - `The specified providers file "src/my.providers.ts" does not exist.`, + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); + expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + message: jasmine.stringMatching('Could not resolve "./my.providers"'), + }), ); }); @@ -42,6 +44,14 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { `, }); + await harness.modifyFile('src/tsconfig.spec.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('my.providers.ts'); + + return JSON.stringify(tsConfig); + }); + harness.useTarget('test', { ...BASE_OPTIONS, providersFile: 'src/my.providers.ts',