Skip to content

Commit ae35543

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. (cherry picked from commit 0aab115)
1 parent a44f8fa commit ae35543

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)