Skip to content

Commit 7ca5af0

Browse files
committed
refactor(@angular/build): improve Vitest configuration merging
This commit refactors the Vitest configuration handling within the unit test builder to more reliably merge the CLI-generated configuration with a user's `vitest-base.config.ts` file. The new implementation uses Vitest's `mergeConfig` utility to create a layered configuration, ensuring that essential CLI settings (such as test entry points and in-memory file providers) are preserved while still allowing users to customize other options. This prevents user configurations from inadvertently overriding critical builder settings, leading to a more stable and predictable testing experience.
1 parent a9acc2e commit 7ca5af0

File tree

4 files changed

+70
-53
lines changed

4 files changed

+70
-53
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,14 +226,15 @@ export class VitestExecutor implements TestExecutor {
226226
watch: null,
227227
},
228228
plugins: [
229-
createVitestConfigPlugin({
229+
await createVitestConfigPlugin({
230230
browser: browserOptions.browser,
231231
coverage,
232232
projectName,
233233
projectSourceRoot: this.options.projectSourceRoot,
234234
optimizeDepsInclude: this.externalMetadata.explicitBrowser,
235235
reporters,
236236
setupFiles: testSetupFiles,
237+
hasRunnerConfig: !!externalConfigPath,
237238
projectPlugins,
238239
include: [...this.testFileToEntryPoint.keys()].filter(
239240
// Filter internal entries

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

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ interface VitestConfigPluginOptions {
4343
projectPlugins: Exclude<UserWorkspaceConfig['plugins'], undefined>;
4444
include: string[];
4545
optimizeDepsInclude: string[];
46+
hasRunnerConfig: boolean;
4647
}
4748

4849
async function findTestEnvironment(
@@ -58,7 +59,9 @@ async function findTestEnvironment(
5859
}
5960
}
6061

61-
export function createVitestConfigPlugin(options: VitestConfigPluginOptions): VitestPlugins[0] {
62+
export async function createVitestConfigPlugin(
63+
options: VitestConfigPluginOptions,
64+
): Promise<VitestPlugins[0]> {
6265
const {
6366
include,
6467
browser,
@@ -69,6 +72,8 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi
6972
projectSourceRoot,
7073
} = options;
7174

75+
const { mergeConfig } = await import('vitest/config');
76+
7277
return {
7378
name: 'angular:vitest-configuration',
7479
async config(config) {
@@ -90,17 +95,6 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi
9095
delete testConfig.include;
9196
}
9297

93-
// The user's setup files should be appended to the CLI's setup files.
94-
const combinedSetupFiles = [...setupFiles];
95-
if (testConfig?.setupFiles) {
96-
if (typeof testConfig.setupFiles === 'string') {
97-
combinedSetupFiles.push(testConfig.setupFiles);
98-
} else if (Array.isArray(testConfig.setupFiles)) {
99-
combinedSetupFiles.push(...testConfig.setupFiles);
100-
}
101-
delete testConfig.setupFiles;
102-
}
103-
10498
// Merge user-defined plugins from the Vitest config with the CLI's internal plugins.
10599
if (config.plugins) {
106100
const userPlugins = config.plugins.filter(
@@ -115,38 +109,48 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi
115109
if (userPlugins.length > 0) {
116110
projectPlugins.push(...userPlugins);
117111
}
112+
delete config.plugins;
118113
}
119114

120115
const projectResolver = createRequire(projectSourceRoot + '/').resolve;
121116

122-
const projectConfig: UserWorkspaceConfig = {
117+
const projectDefaults: UserWorkspaceConfig = {
118+
test: {
119+
setupFiles,
120+
globals: true,
121+
// Default to `false` to align with the Karma/Jasmine experience.
122+
isolate: false,
123+
},
124+
optimizeDeps: {
125+
noDiscovery: true,
126+
include: options.optimizeDepsInclude,
127+
},
128+
};
129+
130+
const projectOverrides: UserWorkspaceConfig = {
123131
test: {
124-
...testConfig,
125132
name: projectName,
126-
setupFiles: combinedSetupFiles,
127133
include,
128-
globals: testConfig?.globals ?? true,
129-
// Default to `false` to align with the Karma/Jasmine experience.
130-
isolate: testConfig?.isolate ?? false,
134+
// CLI provider browser options override, if present
131135
...(browser ? { browser } : {}),
132136
// If the user has not specified an environment, use a smart default.
133137
...(!testConfig?.environment
134138
? { environment: await findTestEnvironment(projectResolver) }
135139
: {}),
136140
},
137-
optimizeDeps: {
138-
noDiscovery: true,
139-
include: options.optimizeDepsInclude,
140-
},
141141
plugins: projectPlugins,
142142
};
143143

144+
const projectBase = mergeConfig(projectDefaults, config);
145+
const projectConfig = mergeConfig(projectBase, projectOverrides);
146+
// The environments field should not be propagated into the project config
147+
delete projectConfig['environments'];
148+
144149
return {
145150
test: {
146151
coverage: await generateCoverageOption(options.coverage, projectName),
147152
// eslint-disable-next-line @typescript-eslint/no-explicit-any
148153
...(reporters ? ({ reporters } as any) : {}),
149-
...(browser ? { browser } : {}),
150154
projects: [projectConfig],
151155
},
152156
};

packages/angular/build/src/builders/unit-test/tests/options/browsers_spec.ts

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,53 +16,33 @@ import {
1616
} from '../setup';
1717

1818
describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
19-
xdescribe('Option: "browsers"', () => {
19+
describe('Option: "browsers"', () => {
2020
beforeEach(async () => {
2121
setupApplicationTarget(harness);
2222
});
2323

24-
it('should use jsdom when browsers is not provided', async () => {
24+
it('should use DOM emulation when browsers is not provided', async () => {
2525
harness.useTarget('test', {
2626
...BASE_OPTIONS,
2727
browsers: undefined,
2828
});
2929

30-
const { result, logs } = await harness.executeOnce();
30+
const { result } = await harness.executeOnce();
3131
expect(result?.success).toBeTrue();
32-
expectLog(logs, 'Using jsdom in Node.js for test execution.');
3332
});
3433

35-
it('should fail when browsers is empty', async () => {
36-
harness.useTarget('test', {
37-
...BASE_OPTIONS,
38-
browsers: [],
39-
});
40-
41-
await expectAsync(harness.executeOnce()).toBeRejectedWithError(
42-
/must NOT have fewer than 1 items/,
43-
);
44-
});
45-
46-
it('should launch a browser when provided', async () => {
34+
it('should fail when a browser is requested but no provider is installed', async () => {
4735
harness.useTarget('test', {
4836
...BASE_OPTIONS,
4937
browsers: ['chrome'],
5038
});
5139

5240
const { result, logs } = await harness.executeOnce();
53-
expect(result?.success).toBeTrue();
54-
expectLog(logs, /Starting browser "chrome"/);
55-
});
56-
57-
it('should launch a browser in headless mode when specified', async () => {
58-
harness.useTarget('test', {
59-
...BASE_OPTIONS,
60-
browsers: ['chromeheadless'],
61-
});
62-
63-
const { result, logs } = await harness.executeOnce();
64-
expect(result?.success).toBeTrue();
65-
expectLog(logs, /Starting browser "chrome" in headless mode/);
41+
expect(result?.success).toBeFalse();
42+
expectLog(
43+
logs,
44+
`The "browsers" option requires either "@vitest/browser-playwright", "@vitest/browser-webdriverio", or "@vitest/browser-preview" to be installed`,
45+
);
6646
});
6747
});
6848
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import assert from 'node:assert/strict';
2+
import { applyVitestBuilder } from '../../utils/vitest';
3+
import { ng } from '../../utils/process';
4+
import { installPackage } from '../../utils/packages';
5+
import { writeFile } from '../../utils/fs';
6+
7+
export default async function (): Promise<void> {
8+
await applyVitestBuilder();
9+
await installPackage('webdriverio');
10+
await installPackage('@vitest/browser-webdriverio');
11+
12+
await ng('generate', 'component', 'my-comp');
13+
14+
await writeFile(
15+
'src/setup1.ts',
16+
`
17+
import { getTestBed } from '@angular/core/testing';
18+
19+
getTestBed().configureTestingModule({});
20+
`,
21+
);
22+
23+
const { stdout } = await ng(
24+
'test',
25+
'--browsers',
26+
'chromeHeadless',
27+
'--setup-files',
28+
'src/setup1.ts',
29+
);
30+
31+
assert.match(stdout, /2 passed/, 'Expected 2 tests to pass.');
32+
}

0 commit comments

Comments
 (0)