Skip to content

Commit 93eef39

Browse files
committed
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 3a37cf6 commit 93eef39

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
let vitestCoverageModule;
@@ -190,12 +190,10 @@ export class VitestExecutor implements TestExecutor {
190190
);
191191

192192
const testSetupFiles = this.prepareSetupFiles();
193-
const plugins = createVitestPlugins({
193+
const projectPlugins = createVitestPlugins({
194194
workspaceRoot,
195195
projectSourceRoot: this.options.projectSourceRoot,
196-
projectName: this.projectName,
197-
include: this.options.include,
198-
exclude: this.options.exclude,
196+
projectName,
199197
buildResultFiles: this.buildResultFiles,
200198
testFileToEntryPoint: this.testFileToEntryPoint,
201199
});
@@ -213,7 +211,6 @@ export class VitestExecutor implements TestExecutor {
213211
runnerConfig === true
214212
? await findVitestBaseConfig([this.options.projectRoot, this.options.workspaceRoot])
215213
: runnerConfig;
216-
const projectName = this.projectName;
217214

218215
return startVitest(
219216
'test',
@@ -229,71 +226,23 @@ export class VitestExecutor implements TestExecutor {
229226
...debugOptions,
230227
},
231228
{
232-
test: {
233-
coverage: await generateCoverageOption(coverage, this.projectName),
234-
...(reporters ? { reporters } : {}),
235-
projects: [
236-
{
237-
extends: externalConfigPath || true,
238-
test: {
239-
name: projectName,
240-
globals: true,
241-
setupFiles: testSetupFiles,
242-
...(this.options.exclude ? { exclude: this.options.exclude } : {}),
243-
browser: browserOptions.browser,
244-
// Use `jsdom` if no browsers are explicitly configured.
245-
...(browserOptions.browser ? {} : { environment: 'jsdom' }),
246-
...(this.options.include ? { include: this.options.include } : {}),
247-
},
248-
optimizeDeps: {
249-
noDiscovery: true,
250-
},
251-
plugins,
252-
},
253-
],
254-
},
255229
server: {
256230
// Disable the actual file watcher. The boolean watch option above should still
257231
// be enabled as it controls other internal behavior related to rerunning tests.
258232
watch: null,
259233
},
234+
plugins: [
235+
createVitestConfigPlugin({
236+
browser: browserOptions.browser,
237+
coverage,
238+
projectName,
239+
reporters,
240+
setupFiles: testSetupFiles,
241+
projectPlugins,
242+
include: [...this.testFileToEntryPoint.keys()],
243+
}),
244+
],
260245
},
261246
);
262247
}
263248
}
264-
265-
async function generateCoverageOption(
266-
coverage: NormalizedUnitTestBuilderOptions['coverage'],
267-
projectName: string,
268-
): Promise<VitestCoverageOption> {
269-
let defaultExcludes: string[] = [];
270-
if (coverage.exclude) {
271-
try {
272-
const vitestConfig = await import('vitest/config');
273-
defaultExcludes = vitestConfig.coverageConfigDefaults.exclude;
274-
} catch {}
275-
}
276-
277-
return {
278-
enabled: coverage.enabled,
279-
excludeAfterRemap: true,
280-
include: coverage.include,
281-
reportsDirectory: toPosixPath(path.join('coverage', projectName)),
282-
thresholds: coverage.thresholds,
283-
watermarks: coverage.watermarks,
284-
// Special handling for `exclude`/`reporters` due to an undefined value causing upstream failures
285-
...(coverage.exclude
286-
? {
287-
exclude: [
288-
// Augment the default exclude https://vitest.dev/config/#coverage-exclude
289-
// with the user defined exclusions
290-
...coverage.exclude,
291-
...defaultExcludes,
292-
],
293-
}
294-
: {}),
295-
...(coverage.reporters
296-
? ({ reporter: coverage.reporters } satisfies VitestCoverageOption)
297-
: {}),
298-
};
299-
}

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)