diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts index 9772b6294089..e425bdf95e4d 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -40,7 +40,7 @@ interface VitestConfigPluginOptions { projectSourceRoot: string; reporters?: string[] | [string, object][]; setupFiles: string[]; - projectPlugins: VitestPlugins; + projectPlugins: Exclude; include: string[]; } @@ -73,13 +73,56 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi async config(config) { const testConfig = config.test; + if (testConfig?.projects?.length) { + this.warn( + 'The "test.projects" option in the Vitest configuration file is not supported. ' + + 'The Angular CLI Test system will construct its own project configuration.', + ); + delete testConfig.projects; + } + + if (testConfig?.include) { + this.warn( + 'The "test.include" option in the Vitest configuration file is not supported. ' + + 'The Angular CLI Test system will manage test file discovery.', + ); + delete testConfig.include; + } + + // The user's setup files should be appended to the CLI's setup files. + const combinedSetupFiles = [...setupFiles]; + if (testConfig?.setupFiles) { + if (typeof testConfig.setupFiles === 'string') { + combinedSetupFiles.push(testConfig.setupFiles); + } else if (Array.isArray(testConfig.setupFiles)) { + combinedSetupFiles.push(...testConfig.setupFiles); + } + delete testConfig.setupFiles; + } + + // Merge user-defined plugins from the Vitest config with the CLI's internal plugins. + if (config.plugins) { + const userPlugins = config.plugins.filter( + (plugin) => + // Only inspect objects with a `name` property as these would be the internal injected plugins + !plugin || + typeof plugin !== 'object' || + !('name' in plugin) || + (!plugin.name.startsWith('angular:') && !plugin.name.startsWith('vitest')), + ); + + if (userPlugins.length > 0) { + projectPlugins.push(...userPlugins); + } + } + const projectResolver = createRequire(projectSourceRoot + '/').resolve; const projectConfig: UserWorkspaceConfig = { test: { ...testConfig, name: projectName, - setupFiles, + setupFiles: combinedSetupFiles, include, globals: testConfig?.globals ?? true, ...(browser ? { browser } : {}), @@ -99,6 +142,7 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi coverage: await generateCoverageOption(options.coverage, projectName), // eslint-disable-next-line @typescript-eslint/no-explicit-any ...(reporters ? ({ reporters } as any) : {}), + ...(browser ? { browser } : {}), projects: [projectConfig], }, }; diff --git a/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts b/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts index e2a50001dd9e..ec3a52e7686b 100644 --- a/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts +++ b/packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts @@ -88,6 +88,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const results = JSON.parse(harness.readFile('vitest-results.json')); expect(results.numPassedTests).toBe(1); }); + it('should allow overriding builder options via runnerConfig file', async () => { harness.useTarget('test', { ...BASE_OPTIONS, @@ -142,5 +143,179 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => { const { result } = await harness.executeOnce(); expect(result?.success).toBeFalse(); }); + + it('should warn and ignore "test.projects" option from runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + projects: ['./foo.config.ts'], + }, + }); + `, + ); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // TODO: Re-enable once Vite logs are remapped through build system + // expect(logs).toContain( + // jasmine.objectContaining({ + // level: 'warn', + // message: jasmine.stringMatching( + // 'The "test.projects" option in the Vitest configuration file is not supported.', + // ), + // }), + // ); + }); + + it('should warn and ignore "test.include" option from runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + include: ['src/app/non-existent.spec.ts'], + }, + }); + `, + ); + + const { result, logs } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + // TODO: Re-enable once Vite logs are remapped through build system + // expect(logs).toContain( + // jasmine.objectContaining({ + // level: 'warn', + // message: jasmine.stringMatching( + // 'The "test.include" option in the Vitest configuration file is not supported.', + // ), + // }), + // ); + }); + + it(`should append "test.setupFiles" (string) from runnerConfig to the CLI's setup`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + setupFiles: './src/app/custom-setup.ts', + }, + }); + `, + ); + + harness.writeFile('src/app/custom-setup.ts', `(globalThis as any).customSetupLoaded = true;`); + + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { test, expect } from 'vitest'; + test('should have custom setup loaded', () => { + expect((globalThis as any).customSetupLoaded).toBe(true); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it(`should append "test.setupFiles" (array) from runnerConfig to the CLI's setup`, async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + test: { + setupFiles: ['./src/app/custom-setup-1.ts', './src/app/custom-setup-2.ts'], + }, + }); + `, + ); + + harness.writeFile('src/app/custom-setup-1.ts', `(globalThis as any).customSetup1 = true;`); + harness.writeFile('src/app/custom-setup-2.ts', `(globalThis as any).customSetup2 = true;`); + + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { test, expect } from 'vitest'; + test('should have custom setups loaded', () => { + expect((globalThis as any).customSetup1).toBe(true); + expect((globalThis as any).customSetup2).toBe(true); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); + + it('should merge and apply custom Vite plugins from runnerConfig file', async () => { + harness.useTarget('test', { + ...BASE_OPTIONS, + runnerConfig: 'vitest.config.ts', + }); + + harness.writeFile( + 'vitest.config.ts', + ` + import { defineConfig } from 'vitest/config'; + export default defineConfig({ + plugins: [ + { + name: 'my-custom-transform-plugin', + transform(code, id) { + if (code.includes('__PLACEHOLDER__')) { + return code.replace('__PLACEHOLDER__', 'transformed by custom plugin'); + } + }, + }, + ], + }); + `, + ); + + harness.writeFile( + 'src/app/app.component.spec.ts', + ` + import { test, expect } from 'vitest'; + test('should have been transformed by custom plugin', () => { + const placeholder = '__PLACEHOLDER__'; + expect(placeholder).toBe('transformed by custom plugin'); + }); + `, + ); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + }); }); });