Skip to content

Commit 0aab115

Browse files
committed
fix(@angular/build): enhance Vitest config merging and validation
This change improves how user-defined Vitest configurations work with the Angular CLI's unit test builder. The builder now checks for and handles specific options in `vitest-base.config.ts`. It detects the `test.projects` option, logs a warning, and removes it to prevent conflicts. The `test.include` option is handled in a similar way, ensuring the builder's test discovery is used. Any `test.setupFiles` from `vitest-base.config.ts` are now added to the CLI's setup files, supporting both single string and array formats. User-defined Vite plugins from `vitest-base.config.ts` are also combined with the builder's plugins, with a filter to prevent duplicating internal CLI plugins. These updates give users more flexibility to customize their Vitest setup while keeping the Angular CLI's test builder predictable.
1 parent 53bdbf1 commit 0aab115

File tree

2 files changed

+221
-2
lines changed

2 files changed

+221
-2
lines changed

packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface VitestConfigPluginOptions {
4040
projectSourceRoot: string;
4141
reporters?: string[] | [string, object][];
4242
setupFiles: string[];
43-
projectPlugins: VitestPlugins;
43+
projectPlugins: Exclude<UserWorkspaceConfig['plugins'], undefined>;
4444
include: string[];
4545
}
4646

@@ -73,13 +73,56 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi
7373
async config(config) {
7474
const testConfig = config.test;
7575

76+
if (testConfig?.projects?.length) {
77+
this.warn(
78+
'The "test.projects" option in the Vitest configuration file is not supported. ' +
79+
'The Angular CLI Test system will construct its own project configuration.',
80+
);
81+
delete testConfig.projects;
82+
}
83+
84+
if (testConfig?.include) {
85+
this.warn(
86+
'The "test.include" option in the Vitest configuration file is not supported. ' +
87+
'The Angular CLI Test system will manage test file discovery.',
88+
);
89+
delete testConfig.include;
90+
}
91+
92+
// The user's setup files should be appended to the CLI's setup files.
93+
const combinedSetupFiles = [...setupFiles];
94+
if (testConfig?.setupFiles) {
95+
if (typeof testConfig.setupFiles === 'string') {
96+
combinedSetupFiles.push(testConfig.setupFiles);
97+
} else if (Array.isArray(testConfig.setupFiles)) {
98+
combinedSetupFiles.push(...testConfig.setupFiles);
99+
}
100+
delete testConfig.setupFiles;
101+
}
102+
103+
// Merge user-defined plugins from the Vitest config with the CLI's internal plugins.
104+
if (config.plugins) {
105+
const userPlugins = config.plugins.filter(
106+
(plugin) =>
107+
// Only inspect objects with a `name` property as these would be the internal injected plugins
108+
!plugin ||
109+
typeof plugin !== 'object' ||
110+
!('name' in plugin) ||
111+
(!plugin.name.startsWith('angular:') && !plugin.name.startsWith('vitest')),
112+
);
113+
114+
if (userPlugins.length > 0) {
115+
projectPlugins.push(...userPlugins);
116+
}
117+
}
118+
76119
const projectResolver = createRequire(projectSourceRoot + '/').resolve;
77120

78121
const projectConfig: UserWorkspaceConfig = {
79122
test: {
80123
...testConfig,
81124
name: projectName,
82-
setupFiles,
125+
setupFiles: combinedSetupFiles,
83126
include,
84127
globals: testConfig?.globals ?? true,
85128
...(browser ? { browser } : {}),
@@ -99,6 +142,7 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi
99142
coverage: await generateCoverageOption(options.coverage, projectName),
100143
// eslint-disable-next-line @typescript-eslint/no-explicit-any
101144
...(reporters ? ({ reporters } as any) : {}),
145+
...(browser ? { browser } : {}),
102146
projects: [projectConfig],
103147
},
104148
};

packages/angular/build/src/builders/unit-test/tests/behavior/runner-config-vitest_spec.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
8888
const results = JSON.parse(harness.readFile('vitest-results.json'));
8989
expect(results.numPassedTests).toBe(1);
9090
});
91+
9192
it('should allow overriding builder options via runnerConfig file', async () => {
9293
harness.useTarget('test', {
9394
...BASE_OPTIONS,
@@ -142,5 +143,179 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
142143
const { result } = await harness.executeOnce();
143144
expect(result?.success).toBeFalse();
144145
});
146+
147+
it('should warn and ignore "test.projects" option from runnerConfig file', async () => {
148+
harness.useTarget('test', {
149+
...BASE_OPTIONS,
150+
runnerConfig: 'vitest.config.ts',
151+
});
152+
153+
harness.writeFile(
154+
'vitest.config.ts',
155+
`
156+
import { defineConfig } from 'vitest/config';
157+
export default defineConfig({
158+
test: {
159+
projects: ['./foo.config.ts'],
160+
},
161+
});
162+
`,
163+
);
164+
165+
const { result, logs } = await harness.executeOnce();
166+
expect(result?.success).toBeTrue();
167+
168+
// TODO: Re-enable once Vite logs are remapped through build system
169+
// expect(logs).toContain(
170+
// jasmine.objectContaining({
171+
// level: 'warn',
172+
// message: jasmine.stringMatching(
173+
// 'The "test.projects" option in the Vitest configuration file is not supported.',
174+
// ),
175+
// }),
176+
// );
177+
});
178+
179+
it('should warn and ignore "test.include" option from runnerConfig file', async () => {
180+
harness.useTarget('test', {
181+
...BASE_OPTIONS,
182+
runnerConfig: 'vitest.config.ts',
183+
});
184+
185+
harness.writeFile(
186+
'vitest.config.ts',
187+
`
188+
import { defineConfig } from 'vitest/config';
189+
export default defineConfig({
190+
test: {
191+
include: ['src/app/non-existent.spec.ts'],
192+
},
193+
});
194+
`,
195+
);
196+
197+
const { result, logs } = await harness.executeOnce();
198+
expect(result?.success).toBeTrue();
199+
200+
// TODO: Re-enable once Vite logs are remapped through build system
201+
// expect(logs).toContain(
202+
// jasmine.objectContaining({
203+
// level: 'warn',
204+
// message: jasmine.stringMatching(
205+
// 'The "test.include" option in the Vitest configuration file is not supported.',
206+
// ),
207+
// }),
208+
// );
209+
});
210+
211+
it(`should append "test.setupFiles" (string) from runnerConfig to the CLI's setup`, async () => {
212+
harness.useTarget('test', {
213+
...BASE_OPTIONS,
214+
runnerConfig: 'vitest.config.ts',
215+
});
216+
217+
harness.writeFile(
218+
'vitest.config.ts',
219+
`
220+
import { defineConfig } from 'vitest/config';
221+
export default defineConfig({
222+
test: {
223+
setupFiles: './src/app/custom-setup.ts',
224+
},
225+
});
226+
`,
227+
);
228+
229+
harness.writeFile('src/app/custom-setup.ts', `(globalThis as any).customSetupLoaded = true;`);
230+
231+
harness.writeFile(
232+
'src/app/app.component.spec.ts',
233+
`
234+
import { test, expect } from 'vitest';
235+
test('should have custom setup loaded', () => {
236+
expect((globalThis as any).customSetupLoaded).toBe(true);
237+
});
238+
`,
239+
);
240+
241+
const { result } = await harness.executeOnce();
242+
expect(result?.success).toBeTrue();
243+
});
244+
245+
it(`should append "test.setupFiles" (array) from runnerConfig to the CLI's setup`, async () => {
246+
harness.useTarget('test', {
247+
...BASE_OPTIONS,
248+
runnerConfig: 'vitest.config.ts',
249+
});
250+
251+
harness.writeFile(
252+
'vitest.config.ts',
253+
`
254+
import { defineConfig } from 'vitest/config';
255+
export default defineConfig({
256+
test: {
257+
setupFiles: ['./src/app/custom-setup-1.ts', './src/app/custom-setup-2.ts'],
258+
},
259+
});
260+
`,
261+
);
262+
263+
harness.writeFile('src/app/custom-setup-1.ts', `(globalThis as any).customSetup1 = true;`);
264+
harness.writeFile('src/app/custom-setup-2.ts', `(globalThis as any).customSetup2 = true;`);
265+
266+
harness.writeFile(
267+
'src/app/app.component.spec.ts',
268+
`
269+
import { test, expect } from 'vitest';
270+
test('should have custom setups loaded', () => {
271+
expect((globalThis as any).customSetup1).toBe(true);
272+
expect((globalThis as any).customSetup2).toBe(true);
273+
});
274+
`,
275+
);
276+
277+
const { result } = await harness.executeOnce();
278+
expect(result?.success).toBeTrue();
279+
});
280+
281+
it('should merge and apply custom Vite plugins from runnerConfig file', async () => {
282+
harness.useTarget('test', {
283+
...BASE_OPTIONS,
284+
runnerConfig: 'vitest.config.ts',
285+
});
286+
287+
harness.writeFile(
288+
'vitest.config.ts',
289+
`
290+
import { defineConfig } from 'vitest/config';
291+
export default defineConfig({
292+
plugins: [
293+
{
294+
name: 'my-custom-transform-plugin',
295+
transform(code, id) {
296+
if (code.includes('__PLACEHOLDER__')) {
297+
return code.replace('__PLACEHOLDER__', 'transformed by custom plugin');
298+
}
299+
},
300+
},
301+
],
302+
});
303+
`,
304+
);
305+
306+
harness.writeFile(
307+
'src/app/app.component.spec.ts',
308+
`
309+
import { test, expect } from 'vitest';
310+
test('should have been transformed by custom plugin', () => {
311+
const placeholder = '__PLACEHOLDER__';
312+
expect(placeholder).toBe('transformed by custom plugin');
313+
});
314+
`,
315+
);
316+
317+
const { result } = await harness.executeOnce();
318+
expect(result?.success).toBeTrue();
319+
});
145320
});
146321
});

0 commit comments

Comments
 (0)