diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 5ff9ac1b443f..0fbd83083e42 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -40,6 +40,7 @@ export class VitestExecutor implements TestExecutor { explicitBrowser: [], explicitServer: [], }; + private readonly logLevel: number; // This is a reverse map of the entry points created in `build-options.ts`. // It is used by the in-memory provider plugin to map the requested test file @@ -54,19 +55,38 @@ export class VitestExecutor implements TestExecutor { testEntryPointMappings: Map | undefined, logger: BuilderContext['logger'], ) { + const level = parseInt(process.env['NG_TEST_LOG'] ?? '0', 10); + this.logLevel = isNaN(level) ? 0 : level; + this.projectName = projectName; this.options = options; this.logger = logger; + this.log(1, 'VitestExecutor instantiated.'); + this.log(2, 'NormalizedUnitTestBuilderOptions:', options); + if (testEntryPointMappings) { for (const [entryPoint, testFile] of testEntryPointMappings) { this.testFileToEntryPoint.set(testFile, entryPoint); this.entryPointToTestFile.set(entryPoint + '.js', testFile); } + this.log(2, 'Test entry point mappings:', Object.fromEntries(testEntryPointMappings)); + } + } + + private log(level: number, message: string, data?: object) { + if (this.logLevel < level) { + return; } + + const formattedMessage = `[VitestExecutor:L${level}] ${message}`; + // Custom formatting for data object to ensure it's readable + const logData = data ? JSON.stringify(data, null, 2) : ''; + this.logger.info(`${formattedMessage}${logData ? `\n${logData}` : ''}`); } async *execute(buildResult: FullResult | IncrementalResult): AsyncIterable { + this.log(1, `Executing test run (kind: ${buildResult.kind}).`); this.normalizePath ??= (await import('vite')).normalizePath; if (buildResult.kind === ResultKind.Full) { @@ -74,7 +94,17 @@ export class VitestExecutor implements TestExecutor { for (const [path, file] of Object.entries(buildResult.files)) { this.buildResultFiles.set(this.normalizePath(path), file); } + this.log(1, `Full build results received. Total files: ${this.buildResultFiles.size}.`); } else { + this.log( + 1, + `Incremental build results received.` + + `Added: ${buildResult.added.length}, Modified: ${buildResult.modified.length}, Removed: ${buildResult.removed.length}.`, + ); + this.log(2, 'Added files:', buildResult.added); + this.log(2, 'Modified files:', buildResult.modified); + this.log(2, 'Removed files:', buildResult.removed); + for (const file of buildResult.removed) { this.buildResultFiles.delete(this.normalizePath(file.path)); } @@ -84,6 +114,7 @@ export class VitestExecutor implements TestExecutor { } updateExternalMetadata(buildResult, this.externalMetadata, undefined, true); + this.log(2, 'Updated external metadata:', this.externalMetadata); // Reset the exit code to allow for a clean state. // This is necessary because Vitest may set the exit code on failure, which can @@ -103,7 +134,16 @@ export class VitestExecutor implements TestExecutor { // We need to find the original source file path to pass to Vitest. const source = this.entryPointToTestFile.get(modifiedFile); if (source) { + this.log( + 2, + `Mapped output file '${modifiedFile}' to source file '${source}' for re-run.`, + ); modifiedSourceFiles.add(source); + } else { + this.log( + 2, + `Could not map output file '${modifiedFile}' to a source file. It may not be a test file.`, + ); } vitest.invalidateFile( this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)), @@ -120,7 +160,11 @@ export class VitestExecutor implements TestExecutor { } if (specsToRerun.length > 0) { + this.log(1, `Re-running ${specsToRerun.length} test specifications.`); + this.log(2, 'Specs to rerun:', specsToRerun); testResults = await vitest.rerunTestSpecifications(specsToRerun); + } else { + this.log(1, 'No test specifications to rerun.'); } } @@ -128,20 +172,26 @@ export class VitestExecutor implements TestExecutor { const testModules = testResults?.testModules ?? this.vitest.state.getTestModules(); let success = testModules.every((testModule) => testModule.ok()); + let finalResultReason = 'All tests passed.'; + // Vitest does not return a failure result when coverage thresholds are not met. // Instead, it sets the process exit code to 1. // We check this exit code to determine if the test run should be considered a failure. if (success && process.exitCode === 1) { success = false; + finalResultReason = 'Test run failed due to unmet coverage thresholds.'; // Reset the exit code to prevent it from carrying over to subsequent runs/builds process.exitCode = 0; } + this.log(1, `Test run finished with success: ${success}. Reason: ${finalResultReason}`); yield { success }; } async [Symbol.asyncDispose](): Promise { + this.log(1, 'Disposing VitestExecutor: Closing Vitest instance.'); await this.vitest?.close(); + this.log(1, 'Vitest instance closed.'); } private prepareSetupFiles(): string[] { @@ -154,10 +204,13 @@ export class VitestExecutor implements TestExecutor { testSetupFiles.unshift('polyfills.js'); } + this.log(1, 'Prepared setup files:', testSetupFiles); + return testSetupFiles; } private async initializeVitest(): Promise { + this.log(1, 'Initializing Vitest.'); const { coverage, reporters, @@ -180,6 +233,7 @@ export class VitestExecutor implements TestExecutor { vitestNodeModule = await import('vitest/node'); } catch (error: unknown) { assertIsError(error); + this.log(1, `Failed to import 'vitest/node'. Error code: ${error.code}`); if (error.code !== 'ERR_MODULE_NOT_FOUND') { throw error; } @@ -199,6 +253,7 @@ export class VitestExecutor implements TestExecutor { browserViewport, ); if (browserOptions.errors?.length) { + this.log(1, 'Browser configuration errors found.', { errors: browserOptions.errors }); throw new Error(browserOptions.errors.join('\n')); } @@ -207,7 +262,9 @@ export class VitestExecutor implements TestExecutor { this.logger.info(message); } } + this.log(1, 'Browser configuration complete.', { config: browserOptions.browser }); + this.log(1, `Verifying build results. File count: ${this.buildResultFiles.size}.`); assert( this.buildResultFiles.size > 0, 'buildResult must be available before initializing vitest', @@ -235,6 +292,8 @@ export class VitestExecutor implements TestExecutor { ? await findVitestBaseConfig([projectRoot, workspaceRoot]) : runnerConfig; + this.log(1, 'External Vitest configuration path:', { externalConfigPath }); + let project = projectName; if (debug && browserOptions.browser?.instances) { if (browserOptions.browser.instances.length > 1) { @@ -246,6 +305,7 @@ export class VitestExecutor implements TestExecutor { // When running browser tests, Vitest appends the browser name to the project identifier. // The project name must match this augmented name to ensure the correct project is targeted. project = `${projectName} (${browserOptions.browser.instances[0].browser})`; + this.log(1, 'Adjusted project name for debugging with browser:', { project }); } // Filter internal entries and setup files from the include list @@ -256,43 +316,46 @@ export class VitestExecutor implements TestExecutor { !internalEntries.some((internal) => entry.startsWith(internal)) && !setupFileSet.has(entry) ); }); + this.log(2, 'Included test files (after filtering):', include); - return startVitest( - 'test', - undefined, - { - config: externalConfigPath, - root: workspaceRoot, - project, - outputFile, - cache: cacheOptions.enabled ? undefined : false, - testNamePattern: this.options.filter, - watch, - ...(typeof ui === 'boolean' ? { ui } : {}), - ...debugOptions, - }, - { - // Note `.vitest` is auto appended to the path. - cacheDir: cacheOptions.path, - server: { - // Disable the actual file watcher. The boolean watch option above should still - // be enabled as it controls other internal behavior related to rerunning tests. - watch: null, - }, - plugins: [ - await createVitestConfigPlugin({ - browser: browserOptions.browser, - coverage, - projectName, - projectSourceRoot, - optimizeDepsInclude: this.externalMetadata.implicitBrowser, - reporters, - setupFiles: testSetupFiles, - projectPlugins, - include, - }), - ], + const vitestConfig = { + config: externalConfigPath, + root: workspaceRoot, + project, + outputFile, + cache: cacheOptions.enabled ? undefined : (false as const), + testNamePattern: this.options.filter, + watch, + ...(typeof ui === 'boolean' ? { ui } : {}), + ...debugOptions, + }; + const vitestServerConfig = { + // Note `.vitest` is auto appended to the path. + cacheDir: cacheOptions.path, + server: { + // Disable the actual file watcher. The boolean watch option above should still + // be enabled as it controls other internal behavior related to rerunning tests. + watch: null, }, - ); + plugins: [ + await createVitestConfigPlugin({ + browser: browserOptions.browser, + coverage, + projectName, + projectSourceRoot, + optimizeDepsInclude: this.externalMetadata.implicitBrowser, + reporters, + setupFiles: testSetupFiles, + projectPlugins, + include, + }), + ], + }; + + this.log(1, 'Calling startVitest with final configuration.'); + this.log(2, 'Vitest config:', vitestConfig); + this.log(2, 'Vitest server config:', vitestServerConfig); + + return startVitest('test', undefined, vitestConfig, vitestServerConfig); } }