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
132 changes: 98 additions & 34 deletions packages/angular/build/src/builders/unit-test/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ async function* runBuildAndTest(
context: BuilderContext,
extensions: ApplicationBuilderExtensions | undefined,
): AsyncIterable<BuilderOutput> {
let consecutiveErrorCount = 0;
for await (const buildResult of buildApplicationInternal(
applicationBuildOptions,
context,
Expand All @@ -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;
}
}
}

Expand All @@ -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;
}
Expand All @@ -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 };
}
}
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) => {
xdescribe('Option: "include"', () => {
describe('Option: "include"', () => {
beforeEach(async () => {
setupApplicationTarget(harness);
});
Expand All @@ -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'],
Expand All @@ -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({
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) => {
xdescribe('Option: "providersFile"', () => {
describe('Option: "providersFile"', () => {
beforeEach(async () => {
setupApplicationTarget(harness);
});
Expand All @@ -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"'),
}),
);
});

Expand All @@ -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',
Expand Down