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, }, ); }