From 62e0f24b936c70192c642cbaf96cce1e6c01a437 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:21:28 -0400 Subject: [PATCH] refactor(@angular/build): improve readability of Vitest executor The `initializeVitest` method within the Vitest test executor was overly large and handled multiple distinct responsibilities, including setup file preparation and Vite plugin configuration. This change refactors the method by extracting these responsibilities into separate private methods: `prepareSetupFiles` and `createVitestPlugins`. This improves the overall readability, testability, and maintainability of the executor by better separating concerns without changing its behavior. --- packages/angular/build/BUILD.bazel | 2 +- .../unit-test/runners/vitest/executor.ts | 281 ++++++++++-------- 2 files changed, 150 insertions(+), 133 deletions(-) diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index a27739236642..f270816d860f 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -320,7 +320,7 @@ jasmine_test( jasmine_test( name = "unit-test_integration_tests", - size = "small", + size = "medium", data = [":unit-test_integration_test_lib"], shard_count = 5, ) 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 7147bed34390..27db9cb84c50 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 @@ -116,9 +116,153 @@ export class VitestExecutor implements TestExecutor { await this.vitest?.close(); } + private prepareSetupFiles(): string[] { + const { setupFiles } = this.options; + // Add setup file entries for TestBed initialization and project polyfills + const testSetupFiles = ['init-testbed.js', ...setupFiles]; + + // TODO: Provide additional result metadata to avoid needing to extract based on filename + if (this.buildResultFiles.has('polyfills.js')) { + testSetupFiles.unshift('polyfills.js'); + } + + return testSetupFiles; + } + + private createVitestPlugins( + testSetupFiles: string[], + browserOptions: Awaited>, + ): NonNullable[] { + const { workspaceRoot, codeCoverage } = this.options; + + return [ + { + name: 'angular:project-init', + // Type is incorrect. This allows a Promise. + // eslint-disable-next-line @typescript-eslint/no-misused-promises + configureVitest: async (context) => { + // Create a subproject that can be configured with plugins for browser mode. + // Plugins defined directly in the vite overrides will not be present in the + // browser specific Vite instance. + const [project] = await context.injectTestProjects({ + test: { + name: this.projectName, + root: workspaceRoot, + globals: true, + setupFiles: testSetupFiles, + // Use `jsdom` if no browsers are explicitly configured. + // `node` is effectively no "environment" and the default. + environment: browserOptions.browser ? 'node' : 'jsdom', + browser: browserOptions.browser, + include: this.options.include, + ...(this.options.exclude ? { exclude: this.options.exclude } : {}), + }, + plugins: [ + { + name: 'angular:test-in-memory-provider', + enforce: 'pre', + resolveId: (id, importer) => { + if (importer && id.startsWith('.')) { + let fullPath; + let relativePath; + if (this.testFileToEntryPoint.has(importer)) { + fullPath = toPosixPath(path.join(this.options.workspaceRoot, id)); + relativePath = path.normalize(id); + } else { + fullPath = toPosixPath(path.join(path.dirname(importer), id)); + relativePath = path.relative(this.options.workspaceRoot, fullPath); + } + if (this.buildResultFiles.has(toPosixPath(relativePath))) { + return fullPath; + } + } + + if (this.testFileToEntryPoint.has(id)) { + return id; + } + + assert( + this.buildResultFiles.size > 0, + 'buildResult must be available for resolving.', + ); + const relativePath = path.relative(this.options.workspaceRoot, id); + if (this.buildResultFiles.has(toPosixPath(relativePath))) { + return id; + } + }, + load: async (id) => { + assert( + this.buildResultFiles.size > 0, + 'buildResult must be available for in-memory loading.', + ); + + // Attempt to load as a source test file. + const entryPoint = this.testFileToEntryPoint.get(id); + let outputPath; + if (entryPoint) { + outputPath = entryPoint + '.js'; + } else { + // Attempt to load as a built artifact. + const relativePath = path.relative(this.options.workspaceRoot, id); + outputPath = toPosixPath(relativePath); + } + + const outputFile = this.buildResultFiles.get(outputPath); + if (outputFile) { + const sourceMapPath = outputPath + '.map'; + const sourceMapFile = this.buildResultFiles.get(sourceMapPath); + const code = + outputFile.origin === 'memory' + ? Buffer.from(outputFile.contents).toString('utf-8') + : await readFile(outputFile.inputPath, 'utf-8'); + const map = sourceMapFile + ? sourceMapFile.origin === 'memory' + ? Buffer.from(sourceMapFile.contents).toString('utf-8') + : await readFile(sourceMapFile.inputPath, 'utf-8') + : undefined; + + return { + code, + map: map ? JSON.parse(map) : undefined, + }; + } + }, + }, + { + name: 'angular:html-index', + transformIndexHtml: () => { + // Add all global stylesheets + if (this.buildResultFiles.has('styles.css')) { + return [ + { + tag: 'link', + attrs: { href: 'styles.css', rel: 'stylesheet' }, + injectTo: 'head', + }, + ]; + } + + return []; + }, + }, + ], + }); + + // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests. + // Vite does this as a convenience but is problematic for the bundling strategy employed by the + // builder's test setup. To workaround this, the excludes are adjusted here to only automatically + // exclude the TypeScript source test files. + project.config.coverage.exclude = [ + ...(codeCoverage?.exclude ?? []), + '**/*.{test,spec}.?(c|m)ts', + ]; + }, + }, + ]; + } + private async initializeVitest(): Promise { - const { codeCoverage, reporters, workspaceRoot, setupFiles, browsers, debug, watch } = - this.options; + const { codeCoverage, reporters, workspaceRoot, browsers, debug, watch } = this.options; let vitestNodeModule; try { @@ -148,13 +292,9 @@ export class VitestExecutor implements TestExecutor { this.buildResultFiles.size > 0, 'buildResult must be available before initializing vitest', ); - // Add setup file entries for TestBed initialization and project polyfills - const testSetupFiles = ['init-testbed.js', ...setupFiles]; - // TODO: Provide additional result metadata to avoid needing to extract based on filename - if (this.buildResultFiles.has('polyfills.js')) { - testSetupFiles.unshift('polyfills.js'); - } + const testSetupFiles = this.prepareSetupFiles(); + const plugins = this.createVitestPlugins(testSetupFiles, browserOptions); const debugOptions = debug ? { @@ -185,130 +325,7 @@ export class VitestExecutor implements TestExecutor { // be enabled as it controls other internal behavior related to rerunning tests. watch: null, }, - plugins: [ - { - name: 'angular:project-init', - // Type is incorrect. This allows a Promise. - // eslint-disable-next-line @typescript-eslint/no-misused-promises - configureVitest: async (context) => { - // Create a subproject that can be configured with plugins for browser mode. - // Plugins defined directly in the vite overrides will not be present in the - // browser specific Vite instance. - const [project] = await context.injectTestProjects({ - test: { - name: this.projectName, - root: workspaceRoot, - globals: true, - setupFiles: testSetupFiles, - // Use `jsdom` if no browsers are explicitly configured. - // `node` is effectively no "environment" and the default. - environment: browserOptions.browser ? 'node' : 'jsdom', - browser: browserOptions.browser, - include: this.options.include, - ...(this.options.exclude ? { exclude: this.options.exclude } : {}), - }, - plugins: [ - { - name: 'angular:test-in-memory-provider', - enforce: 'pre', - resolveId: (id, importer) => { - if (importer && id.startsWith('.')) { - let fullPath; - let relativePath; - if (this.testFileToEntryPoint.has(importer)) { - fullPath = toPosixPath(path.join(this.options.workspaceRoot, id)); - relativePath = path.normalize(id); - } else { - fullPath = toPosixPath(path.join(path.dirname(importer), id)); - relativePath = path.relative(this.options.workspaceRoot, fullPath); - } - if (this.buildResultFiles.has(toPosixPath(relativePath))) { - return fullPath; - } - } - - if (this.testFileToEntryPoint.has(id)) { - return id; - } - - assert( - this.buildResultFiles.size > 0, - 'buildResult must be available for resolving.', - ); - const relativePath = path.relative(this.options.workspaceRoot, id); - if (this.buildResultFiles.has(toPosixPath(relativePath))) { - return id; - } - }, - load: async (id) => { - assert( - this.buildResultFiles.size > 0, - 'buildResult must be available for in-memory loading.', - ); - - // Attempt to load as a source test file. - const entryPoint = this.testFileToEntryPoint.get(id); - let outputPath; - if (entryPoint) { - outputPath = entryPoint + '.js'; - } else { - // Attempt to load as a built artifact. - const relativePath = path.relative(this.options.workspaceRoot, id); - outputPath = toPosixPath(relativePath); - } - - const outputFile = this.buildResultFiles.get(outputPath); - if (outputFile) { - const sourceMapPath = outputPath + '.map'; - const sourceMapFile = this.buildResultFiles.get(sourceMapPath); - const code = - outputFile.origin === 'memory' - ? Buffer.from(outputFile.contents).toString('utf-8') - : await readFile(outputFile.inputPath, 'utf-8'); - const map = sourceMapFile - ? sourceMapFile.origin === 'memory' - ? Buffer.from(sourceMapFile.contents).toString('utf-8') - : await readFile(sourceMapFile.inputPath, 'utf-8') - : undefined; - - return { - code, - map: map ? JSON.parse(map) : undefined, - }; - } - }, - }, - { - name: 'angular:html-index', - transformIndexHtml: () => { - // Add all global stylesheets - if (this.buildResultFiles.has('styles.css')) { - return [ - { - tag: 'link', - attrs: { href: 'styles.css', rel: 'stylesheet' }, - injectTo: 'head', - }, - ]; - } - - return []; - }, - }, - ], - }); - - // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests. - // Vite does this as a convenience but is problematic for the bundling strategy employed by the - // builder's test setup. To workaround this, the excludes are adjusted here to only automatically - // exclude the TypeScript source test files. - project.config.coverage.exclude = [ - ...(codeCoverage?.exclude ?? []), - '**/*.{test,spec}.?(c|m)ts', - ]; - }, - }, - ], + plugins, }, ); }