Skip to content

Commit ae1b00d

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular/build): improve vitest runnerConfig support via plugin
Refactors the Vitest unit test runner to use a dedicated plugin for merging builder-defined configurations with user-provided configurations from a `runnerConfig` file. Previously, the configuration merging logic was handled directly in the executor, which had limitations and did not always correctly apply user overrides. By moving this logic into a Vitest plugin, we leverage Vitest's intended extension mechanism, ensuring a more robust and predictable merge of configurations. This significantly improves the ability for users to customize their test setup. Adds a new test suite to verify that common custom configurations, such as custom reporters, file exclusions, option overrides, and environment settings, are correctly applied from a `vitest.config.ts` file.
1 parent 882698b commit ae1b00d

File tree

3 files changed

+256
-74
lines changed

3 files changed

+256
-74
lines changed

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

Lines changed: 19 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ import type { BuilderOutput } from '@angular-devkit/architect';
1010
import assert from 'node:assert';
1111
import path from 'node:path';
1212
import { isMatch } from 'picomatch';
13-
import type { InlineConfig, Vitest } from 'vitest/node';
13+
import type { Vitest } from 'vitest/node';
1414
import { assertIsError } from '../../../../utils/error';
15-
import { toPosixPath } from '../../../../utils/path';
1615
import {
1716
type FullResult,
1817
type IncrementalResult,
@@ -23,9 +22,7 @@ import { NormalizedUnitTestBuilderOptions } from '../../options';
2322
import type { TestExecutor } from '../api';
2423
import { setupBrowserConfiguration } from './browser-provider';
2524
import { findVitestBaseConfig } from './configuration';
26-
import { createVitestPlugins } from './plugins';
27-
28-
type VitestCoverageOption = Exclude<InlineConfig['coverage'], undefined>;
25+
import { createVitestConfigPlugin, createVitestPlugins } from './plugins';
2926

3027
export class VitestExecutor implements TestExecutor {
3128
private vitest: Vitest | undefined;
@@ -89,7 +86,9 @@ export class VitestExecutor implements TestExecutor {
8986
if (source) {
9087
modifiedSourceFiles.add(source);
9188
}
92-
vitest.invalidateFile(toPosixPath(path.join(this.options.workspaceRoot, modifiedFile)));
89+
vitest.invalidateFile(
90+
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
91+
);
9392
}
9493

9594
const specsToRerun = [];
@@ -141,6 +140,7 @@ export class VitestExecutor implements TestExecutor {
141140
browserViewport,
142141
ui,
143142
} = this.options;
143+
const projectName = this.projectName;
144144

145145
let vitestNodeModule;
146146
try {
@@ -173,12 +173,10 @@ export class VitestExecutor implements TestExecutor {
173173
);
174174

175175
const testSetupFiles = this.prepareSetupFiles();
176-
const plugins = createVitestPlugins({
176+
const projectPlugins = createVitestPlugins({
177177
workspaceRoot,
178178
projectSourceRoot: this.options.projectSourceRoot,
179-
projectName: this.projectName,
180-
include: this.options.include,
181-
exclude: this.options.exclude,
179+
projectName,
182180
buildResultFiles: this.buildResultFiles,
183181
testFileToEntryPoint: this.testFileToEntryPoint,
184182
});
@@ -196,7 +194,6 @@ export class VitestExecutor implements TestExecutor {
196194
runnerConfig === true
197195
? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot])
198196
: runnerConfig;
199-
const projectName = this.projectName;
200197

201198
return startVitest(
202199
'test',
@@ -212,71 +209,23 @@ export class VitestExecutor implements TestExecutor {
212209
...debugOptions,
213210
},
214211
{
215-
test: {
216-
coverage: await generateCoverageOption(coverage, this.projectName),
217-
...(reporters ? { reporters } : {}),
218-
projects: [
219-
{
220-
extends: externalConfigPath || true,
221-
test: {
222-
name: projectName,
223-
globals: true,
224-
setupFiles: testSetupFiles,
225-
...(this.options.exclude ? { exclude: this.options.exclude } : {}),
226-
browser: browserOptions.browser,
227-
// Use `jsdom` if no browsers are explicitly configured.
228-
...(browserOptions.browser ? {} : { environment: 'jsdom' }),
229-
...(this.options.include ? { include: this.options.include } : {}),
230-
},
231-
optimizeDeps: {
232-
noDiscovery: true,
233-
},
234-
plugins,
235-
},
236-
],
237-
},
238212
server: {
239213
// Disable the actual file watcher. The boolean watch option above should still
240214
// be enabled as it controls other internal behavior related to rerunning tests.
241215
watch: null,
242216
},
217+
plugins: [
218+
createVitestConfigPlugin({
219+
browser: browserOptions.browser,
220+
coverage,
221+
projectName,
222+
reporters,
223+
setupFiles: testSetupFiles,
224+
projectPlugins,
225+
include: [...this.testFileToEntryPoint.keys()],
226+
}),
227+
],
243228
},
244229
);
245230
}
246231
}
247-
248-
async function generateCoverageOption(
249-
coverage: NormalizedUnitTestBuilderOptions['coverage'],
250-
projectName: string,
251-
): Promise<VitestCoverageOption> {
252-
let defaultExcludes: string[] = [];
253-
if (coverage.exclude) {
254-
try {
255-
const vitestConfig = await import('vitest/config');
256-
defaultExcludes = vitestConfig.coverageConfigDefaults.exclude;
257-
} catch {}
258-
}
259-
260-
return {
261-
enabled: coverage.enabled,
262-
excludeAfterRemap: true,
263-
include: coverage.include,
264-
reportsDirectory: toPosixPath(path.join('coverage', projectName)),
265-
thresholds: coverage.thresholds,
266-
watermarks: coverage.watermarks,
267-
// Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures
268-
...(coverage.exclude
269-
? {
270-
exclude: [
271-
// Augment the default exclude https://vitest.dev/config/#coverage-exclude
272-
// with the user defined exclusions
273-
...coverage.exclude,
274-
...defaultExcludes,
275-
],
276-
}
277-
: {}),
278-
...(coverage.reporters
279-
? ({ reporter: coverage.reporters } satisfies VitestCoverageOption)
280-
: {}),
281-
};
282-
}

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

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,76 @@
99
import assert from 'node:assert';
1010
import { readFile } from 'node:fs/promises';
1111
import path from 'node:path';
12-
import type { VitestPlugin } from 'vitest/node';
12+
import type {
13+
BrowserConfigOptions,
14+
InlineConfig,
15+
UserWorkspaceConfig,
16+
VitestPlugin,
17+
} from 'vitest/node';
1318
import { createBuildAssetsMiddleware } from '../../../../tools/vite/middlewares/assets-middleware';
1419
import { toPosixPath } from '../../../../utils/path';
1520
import type { ResultFile } from '../../../application/results';
1621
import type { NormalizedUnitTestBuilderOptions } from '../../options';
17-
import type { BrowserConfiguration } from './browser-provider';
1822

1923
type VitestPlugins = Awaited<ReturnType<typeof VitestPlugin>>;
2024

2125
interface PluginOptions {
2226
workspaceRoot: string;
2327
projectSourceRoot: string;
2428
projectName: string;
25-
include?: string[];
26-
exclude?: string[];
2729
buildResultFiles: ReadonlyMap<string, ResultFile>;
2830
testFileToEntryPoint: ReadonlyMap<string, string>;
2931
}
3032

33+
type VitestCoverageOption = Exclude<InlineConfig['coverage'], undefined>;
34+
35+
interface VitestConfigPluginOptions {
36+
browser: BrowserConfigOptions | undefined;
37+
coverage: NormalizedUnitTestBuilderOptions['coverage'];
38+
projectName: string;
39+
reporters?: string[] | [string, object][];
40+
setupFiles: string[];
41+
projectPlugins: VitestPlugins;
42+
include: string[];
43+
}
44+
45+
export function createVitestConfigPlugin(options: VitestConfigPluginOptions): VitestPlugins[0] {
46+
const { include, browser, projectName, reporters, setupFiles, projectPlugins } = options;
47+
48+
return {
49+
name: 'angular:vitest-configuration',
50+
async config(config) {
51+
const testConfig = config.test;
52+
53+
const projectConfig: UserWorkspaceConfig = {
54+
test: {
55+
...testConfig,
56+
name: projectName,
57+
setupFiles,
58+
include,
59+
globals: testConfig?.globals ?? true,
60+
...(browser ? { browser } : {}),
61+
// If the user has not specified an environment, use `jsdom`.
62+
...(!testConfig?.environment ? { environment: 'jsdom' } : {}),
63+
},
64+
optimizeDeps: {
65+
noDiscovery: true,
66+
},
67+
plugins: projectPlugins,
68+
};
69+
70+
return {
71+
test: {
72+
coverage: await generateCoverageOption(options.coverage, projectName),
73+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74+
...(reporters ? ({ reporters } as any) : {}),
75+
projects: [projectConfig],
76+
},
77+
};
78+
},
79+
};
80+
}
81+
3182
export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins {
3283
const { workspaceRoot, buildResultFiles, testFileToEntryPoint } = pluginOptions;
3384

@@ -134,3 +185,39 @@ export function createVitestPlugins(pluginOptions: PluginOptions): VitestPlugins
134185
},
135186
];
136187
}
188+
189+
async function generateCoverageOption(
190+
coverage: NormalizedUnitTestBuilderOptions['coverage'],
191+
projectName: string,
192+
): Promise<VitestCoverageOption> {
193+
let defaultExcludes: string[] = [];
194+
if (coverage.exclude) {
195+
try {
196+
const vitestConfig = await import('vitest/config');
197+
defaultExcludes = vitestConfig.coverageConfigDefaults.exclude;
198+
} catch {}
199+
}
200+
201+
return {
202+
enabled: coverage.enabled,
203+
excludeAfterRemap: true,
204+
include: coverage.include,
205+
reportsDirectory: toPosixPath(path.join('coverage', projectName)),
206+
thresholds: coverage.thresholds,
207+
watermarks: coverage.watermarks,
208+
// Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures
209+
...(coverage.exclude
210+
? {
211+
exclude: [
212+
// Augment the default exclude https://vitest.dev/config/#coverage-exclude
213+
// with the user defined exclusions
214+
...coverage.exclude,
215+
...defaultExcludes,
216+
],
217+
}
218+
: {}),
219+
...(coverage.reporters
220+
? ({ reporter: coverage.reporters } satisfies VitestCoverageOption)
221+
: {}),
222+
};
223+
}

0 commit comments

Comments
 (0)