vitest 4.1.0-beta.1 → 4.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/LICENSE.md +36 -0
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +2 -2
  4. package/dist/chunks/acorn.B2iPLyUM.js +5958 -0
  5. package/dist/chunks/{base.CBRNZa3k.js → base.DiopZV8F.js} +48 -14
  6. package/dist/chunks/{benchmark.B3N2zMcH.js → benchmark.BoqSLF53.js} +1 -1
  7. package/dist/chunks/{browser.d.8hOapKZr.d.ts → browser.d.BE4kbYok.d.ts} +2 -1
  8. package/dist/chunks/{cac.B1v3xxoC.js → cac.C4jjt2RX.js} +797 -13
  9. package/dist/chunks/{cli-api.B4CqEpI6.js → cli-api.ChbI1JU9.js} +322 -124
  10. package/dist/chunks/{config.d.idH22YSr.d.ts → config.d.Cr1Ep39N.d.ts} +6 -1
  11. package/dist/chunks/{console.uGgdMhyZ.js → console.CNlG1KsP.js} +2 -2
  12. package/dist/chunks/{constants.D_Q9UYh-.js → constants.B63TT-Bl.js} +1 -1
  13. package/dist/chunks/coverage.tyqbzn4W.js +1001 -0
  14. package/dist/chunks/{creator.C7WwjkuR.js → creator.yyCHuw5R.js} +1 -1
  15. package/dist/chunks/{global.d.B15mdLcR.d.ts → global.d.JeWMqlOm.d.ts} +1 -1
  16. package/dist/chunks/{globals.DjuGMoMc.js → globals.C6Ecf1TO.js} +6 -6
  17. package/dist/chunks/{index.Dm4xqZ0s.js → index.B-iBE_Gx.js} +20 -4
  18. package/dist/chunks/{coverage.BMlOMIWl.js → index.BCY_7LL2.js} +5 -969
  19. package/dist/chunks/{index.BiOAd_ki.js → index.CAN630q3.js} +7 -7
  20. package/dist/chunks/{index.DyBZXrH3.js → index.CFulQRmC.js} +1 -1
  21. package/dist/chunks/{index.BEFi2-_3.js → index.CouFDptX.js} +2 -2
  22. package/dist/chunks/{init-forks.CHeQ9Moq.js → init-forks.BnCXPazU.js} +1 -1
  23. package/dist/chunks/{init-threads.uZiNAuPk.js → init-threads.Cyh2PqXi.js} +1 -1
  24. package/dist/chunks/{init.DVtKdFty.js → init.B95Mm0Iz.js} +47 -9
  25. package/dist/chunks/native.mV0-490A.js +148 -0
  26. package/dist/chunks/nativeModuleMocker.D_q5sFv6.js +206 -0
  27. package/dist/chunks/nativeModuleRunner.BIakptoF.js +36 -0
  28. package/dist/chunks/{node.Ce0vMQM7.js → node.CrSEwhm4.js} +1 -1
  29. package/dist/chunks/{plugin.d.D8KU2PY_.d.ts → plugin.d.C9o5bttz.d.ts} +1 -1
  30. package/dist/chunks/{reporters.d.Db3MiIWX.d.ts → reporters.d.7faYdkxy.d.ts} +120 -51
  31. package/dist/chunks/{rpc.HLmECnw_.js → rpc.DcRWTy5G.js} +1 -1
  32. package/dist/chunks/{rpc.d.RH3apGEf.d.ts → rpc.d.CM7x9-sm.d.ts} +1 -0
  33. package/dist/chunks/{setup-common.BcqLPsn5.js → setup-common.cvFp-ao9.js} +2 -2
  34. package/dist/chunks/{startModuleRunner.C5CcWyXW.js → startVitestModuleRunner.BK-u7y4N.js} +163 -372
  35. package/dist/chunks/{test.prxIahgM.js → test.G82XYNFk.js} +9 -4
  36. package/dist/chunks/{utils.DvEY5TfP.js → utils.DT4VyRyl.js} +5 -1
  37. package/dist/chunks/{vm.CrifS09m.js → vm.BdLtzhnj.js} +13 -6
  38. package/dist/chunks/{worker.d.Bji1eq5g.d.ts → worker.d.CPzI2ZzJ.d.ts} +2 -2
  39. package/dist/cli.js +4 -3
  40. package/dist/config.d.ts +8 -8
  41. package/dist/config.js +1 -1
  42. package/dist/coverage.d.ts +7 -5
  43. package/dist/coverage.js +5 -4
  44. package/dist/index.d.ts +18 -23
  45. package/dist/index.js +5 -5
  46. package/dist/module-evaluator.d.ts +10 -1
  47. package/dist/node.d.ts +9 -9
  48. package/dist/node.js +18 -16
  49. package/dist/nodejs-worker-loader.js +41 -0
  50. package/dist/reporters.d.ts +5 -5
  51. package/dist/reporters.js +2 -2
  52. package/dist/runners.d.ts +2 -1
  53. package/dist/runners.js +4 -4
  54. package/dist/runtime.js +4 -5
  55. package/dist/snapshot.js +2 -2
  56. package/dist/suite.js +2 -2
  57. package/dist/worker.d.ts +6 -6
  58. package/dist/worker.js +25 -18
  59. package/dist/workers/forks.js +21 -14
  60. package/dist/workers/runVmTests.js +7 -7
  61. package/dist/workers/threads.js +21 -14
  62. package/dist/workers/vmForks.js +14 -10
  63. package/dist/workers/vmThreads.js +14 -10
  64. package/package.json +17 -14
  65. package/suppress-warnings.cjs +1 -0
@@ -0,0 +1,1001 @@
1
+ import { existsSync, promises, readdirSync, writeFileSync } from 'node:fs';
2
+ import module$1 from 'node:module';
3
+ import path from 'node:path';
4
+ import { pathToFileURL, fileURLToPath } from 'node:url';
5
+ import { slash, shuffle, toArray } from '@vitest/utils/helpers';
6
+ import { resolve, relative, normalize } from 'pathe';
7
+ import pm from 'picomatch';
8
+ import { glob } from 'tinyglobby';
9
+ import c from 'tinyrainbow';
10
+ import { c as configDefaults, e as benchmarkConfigDefaults, a as coverageConfigDefaults } from './defaults.BOqNVLsY.js';
11
+ import crypto from 'node:crypto';
12
+ import { r as resolveModule } from './index.BCY_7LL2.js';
13
+ import { mergeConfig } from 'vite';
14
+ import { c as configFiles, d as defaultBrowserPort, a as defaultInspectPort, b as defaultPort } from './constants.B63TT-Bl.js';
15
+ import './env.D4Lgay0q.js';
16
+ import nodeos__default from 'node:os';
17
+ import { isCI, provider } from 'std-env';
18
+ import { r as resolveCoverageProviderModule } from './coverage.D_JHT54q.js';
19
+
20
+ const hash = crypto.hash ?? ((algorithm, data, outputEncoding) => crypto.createHash(algorithm).update(data).digest(outputEncoding));
21
+
22
+ function getWorkersCountByPercentage(percent) {
23
+ const maxWorkersCount = nodeos__default.availableParallelism?.() ?? nodeos__default.cpus().length;
24
+ const workersCountByPercentage = Math.round(Number.parseInt(percent) / 100 * maxWorkersCount);
25
+ return Math.max(1, Math.min(maxWorkersCount, workersCountByPercentage));
26
+ }
27
+
28
+ class BaseSequencer {
29
+ ctx;
30
+ constructor(ctx) {
31
+ this.ctx = ctx;
32
+ }
33
+ // async so it can be extended by other sequelizers
34
+ async shard(files) {
35
+ const { config } = this.ctx;
36
+ const { index, count } = config.shard;
37
+ const [shardStart, shardEnd] = this.calculateShardRange(files.length, index, count);
38
+ return [...files].map((spec) => {
39
+ const specPath = resolve(slash(config.root), slash(spec.moduleId))?.slice(config.root.length);
40
+ return {
41
+ spec,
42
+ hash: hash("sha1", specPath, "hex")
43
+ };
44
+ }).sort((a, b) => a.hash < b.hash ? -1 : a.hash > b.hash ? 1 : 0).slice(shardStart, shardEnd).map(({ spec }) => spec);
45
+ }
46
+ // async so it can be extended by other sequelizers
47
+ async sort(files) {
48
+ const cache = this.ctx.cache;
49
+ return [...files].sort((a, b) => {
50
+ // "sequence.groupOrder" is higher priority
51
+ const groupOrderDiff = a.project.config.sequence.groupOrder - b.project.config.sequence.groupOrder;
52
+ if (groupOrderDiff !== 0) return groupOrderDiff;
53
+ // Projects run sequential
54
+ if (a.project.name !== b.project.name) return a.project.name < b.project.name ? -1 : 1;
55
+ // Isolated run first
56
+ if (a.project.config.isolate && !b.project.config.isolate) return -1;
57
+ if (!a.project.config.isolate && b.project.config.isolate) return 1;
58
+ const keyA = `${a.project.name}:${relative(this.ctx.config.root, a.moduleId)}`;
59
+ const keyB = `${b.project.name}:${relative(this.ctx.config.root, b.moduleId)}`;
60
+ const aState = cache.getFileTestResults(keyA);
61
+ const bState = cache.getFileTestResults(keyB);
62
+ if (!aState || !bState) {
63
+ const statsA = cache.getFileStats(keyA);
64
+ const statsB = cache.getFileStats(keyB);
65
+ // run unknown first
66
+ if (!statsA || !statsB) return !statsA && statsB ? -1 : !statsB && statsA ? 1 : 0;
67
+ // run larger files first
68
+ return statsB.size - statsA.size;
69
+ }
70
+ // run failed first
71
+ if (aState.failed && !bState.failed) return -1;
72
+ if (!aState.failed && bState.failed) return 1;
73
+ // run longer first
74
+ return bState.duration - aState.duration;
75
+ });
76
+ }
77
+ // Calculate distributed shard range [start, end] distributed equally
78
+ calculateShardRange(filesCount, index, count) {
79
+ const baseShardSize = Math.floor(filesCount / count);
80
+ const remainderTestFilesCount = filesCount % count;
81
+ if (remainderTestFilesCount >= index) {
82
+ const shardSize = baseShardSize + 1;
83
+ return [shardSize * (index - 1), shardSize * index];
84
+ }
85
+ const shardStart = remainderTestFilesCount * (baseShardSize + 1) + (index - remainderTestFilesCount - 1) * baseShardSize;
86
+ return [shardStart, shardStart + baseShardSize];
87
+ }
88
+ }
89
+
90
+ class RandomSequencer extends BaseSequencer {
91
+ async sort(files) {
92
+ const { sequence } = this.ctx.config;
93
+ return shuffle(files, sequence.seed);
94
+ }
95
+ }
96
+
97
+ function resolvePath(path, root) {
98
+ return normalize(/* @__PURE__ */ resolveModule(path, { paths: [root] }) ?? resolve(root, path));
99
+ }
100
+ function parseInspector(inspect) {
101
+ if (typeof inspect === "boolean" || inspect === void 0) return {};
102
+ if (typeof inspect === "number") return { port: inspect };
103
+ if (inspect.match(/https?:\//)) throw new Error(`Inspector host cannot be a URL. Use "host:port" instead of "${inspect}"`);
104
+ const [host, port] = inspect.split(":");
105
+ if (!port) return { host };
106
+ return {
107
+ host,
108
+ port: Number(port) || defaultInspectPort
109
+ };
110
+ }
111
+ function resolveApiServerConfig(options, defaultPort) {
112
+ let api;
113
+ if (options.ui && !options.api) api = { port: defaultPort };
114
+ else if (options.api === true) api = { port: defaultPort };
115
+ else if (typeof options.api === "number") api = { port: options.api };
116
+ if (typeof options.api === "object") if (api) {
117
+ if (options.api.port) api.port = options.api.port;
118
+ if (options.api.strictPort) api.strictPort = options.api.strictPort;
119
+ if (options.api.host) api.host = options.api.host;
120
+ } else api = { ...options.api };
121
+ if (api) {
122
+ if (!api.port && !api.middlewareMode) api.port = defaultPort;
123
+ } else api = { middlewareMode: true };
124
+ return api;
125
+ }
126
+ function resolveInlineWorkerOption(value) {
127
+ if (typeof value === "string" && value.trim().endsWith("%")) return getWorkersCountByPercentage(value);
128
+ else return Number(value);
129
+ }
130
+ function resolveConfig$1(vitest, options, viteConfig) {
131
+ const mode = vitest.mode;
132
+ const logger = vitest.logger;
133
+ if (options.dom) {
134
+ if (viteConfig.test?.environment != null && viteConfig.test.environment !== "happy-dom") logger.console.warn(c.yellow(`${c.inverse(c.yellow(" Vitest "))} Your config.test.environment ("${viteConfig.test.environment}") conflicts with --dom flag ("happy-dom"), ignoring "${viteConfig.test.environment}"`));
135
+ options.environment = "happy-dom";
136
+ }
137
+ const resolved = {
138
+ ...configDefaults,
139
+ ...options,
140
+ root: viteConfig.root,
141
+ mode
142
+ };
143
+ if (resolved.retry && typeof resolved.retry === "object" && typeof resolved.retry.condition === "function") {
144
+ logger.console.warn(c.yellow("Warning: retry.condition function cannot be used inside a config file. Use a RegExp pattern instead, or define the function in your test file."));
145
+ resolved.retry = {
146
+ ...resolved.retry,
147
+ condition: void 0
148
+ };
149
+ }
150
+ if (options.pool && typeof options.pool !== "string") {
151
+ resolved.pool = options.pool.name;
152
+ resolved.poolRunner = options.pool;
153
+ }
154
+ if ("poolOptions" in resolved) logger.deprecate("`test.poolOptions` was removed in Vitest 4. All previous `poolOptions` are now top-level options. Please, refer to the migration guide: https://vitest.dev/guide/migration#pool-rework");
155
+ resolved.pool ??= "forks";
156
+ resolved.project = toArray(resolved.project);
157
+ resolved.provide ??= {};
158
+ // shallow copy tags array to avoid mutating user config
159
+ resolved.tags = [...resolved.tags || []];
160
+ const definedTags = /* @__PURE__ */ new Set();
161
+ resolved.tags.forEach((tag) => {
162
+ if (!tag.name || typeof tag.name !== "string") throw new Error(`Each tag defined in "test.tags" must have a "name" property, received: ${JSON.stringify(tag)}`);
163
+ if (definedTags.has(tag.name)) throw new Error(`Tag name "${tag.name}" is already defined in "test.tags". Tag names must be unique.`);
164
+ if (tag.name.match(/\s/)) throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot contain spaces.`);
165
+ if (tag.name.match(/([!()*|&])/)) throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot contain "!", "*", "&", "|", "(", or ")".`);
166
+ if (tag.name.match(/^\s*(and|or|not)\s*$/i)) throw new Error(`Tag name "${tag.name}" is invalid. Tag names cannot be a logical operator like "and", "or", "not".`);
167
+ if (typeof tag.retry === "object" && typeof tag.retry.condition === "function") throw new TypeError(`Tag "${tag.name}": retry.condition function cannot be used inside a config file. Use a RegExp pattern instead, or define the function in your test file.`);
168
+ if (tag.priority != null && (typeof tag.priority !== "number" || tag.priority < 0)) throw new TypeError(`Tag "${tag.name}": priority must be a non-negative number.`);
169
+ definedTags.add(tag.name);
170
+ });
171
+ resolved.name = typeof options.name === "string" ? options.name : options.name?.label || "";
172
+ resolved.color = typeof options.name !== "string" ? options.name?.color : void 0;
173
+ if (resolved.environment === "browser") throw new Error(`Looks like you set "test.environment" to "browser". To enable Browser Mode, use "test.browser.enabled" instead.`);
174
+ const inspector = resolved.inspect || resolved.inspectBrk;
175
+ resolved.inspector = {
176
+ ...resolved.inspector,
177
+ ...parseInspector(inspector),
178
+ enabled: !!inspector,
179
+ waitForDebugger: options.inspector?.waitForDebugger ?? !!resolved.inspectBrk
180
+ };
181
+ if (viteConfig.base !== "/") resolved.base = viteConfig.base;
182
+ resolved.clearScreen = resolved.clearScreen ?? viteConfig.clearScreen ?? true;
183
+ if (options.shard) {
184
+ if (resolved.watch) throw new Error("You cannot use --shard option with enabled watch");
185
+ const [indexString, countString] = options.shard.split("/");
186
+ const index = Math.abs(Number.parseInt(indexString, 10));
187
+ const count = Math.abs(Number.parseInt(countString, 10));
188
+ if (Number.isNaN(count) || count <= 0) throw new Error("--shard <count> must be a positive number");
189
+ if (Number.isNaN(index) || index <= 0 || index > count) throw new Error("--shard <index> must be a positive number less then <count>");
190
+ resolved.shard = {
191
+ index,
192
+ count
193
+ };
194
+ }
195
+ if (resolved.standalone && !resolved.watch) throw new Error(`Vitest standalone mode requires --watch`);
196
+ if (resolved.mergeReports && resolved.watch) throw new Error(`Cannot merge reports with --watch enabled`);
197
+ if (resolved.maxWorkers) resolved.maxWorkers = resolveInlineWorkerOption(resolved.maxWorkers);
198
+ if (!(options.fileParallelism ?? mode !== "benchmark"))
199
+ // ignore user config, parallelism cannot be implemented without limiting workers
200
+ resolved.maxWorkers = 1;
201
+ if (resolved.maxConcurrency === 0) {
202
+ logger.console.warn(c.yellow(`The option "maxConcurrency" cannot be set to 0. Using default value ${configDefaults.maxConcurrency} instead.`));
203
+ resolved.maxConcurrency = configDefaults.maxConcurrency;
204
+ }
205
+ if (resolved.inspect || resolved.inspectBrk) {
206
+ if (resolved.maxWorkers !== 1) {
207
+ const inspectOption = `--inspect${resolved.inspectBrk ? "-brk" : ""}`;
208
+ throw new Error(`You cannot use ${inspectOption} without "--no-file-parallelism"`);
209
+ }
210
+ }
211
+ // apply browser CLI options only if the config already has the browser config and not disabled manually
212
+ if (vitest._cliOptions.browser && resolved.browser && (resolved.browser.enabled !== false || vitest._cliOptions.browser.enabled)) resolved.browser = mergeConfig(resolved.browser, vitest._cliOptions.browser);
213
+ resolved.browser ??= {};
214
+ const browser = resolved.browser;
215
+ if (browser.enabled) {
216
+ const instances = browser.instances;
217
+ if (!browser.instances) browser.instances = [];
218
+ // use `chromium` by default when the preview provider is specified
219
+ // for a smoother experience. if chromium is not available, it will
220
+ // open the default browser anyway
221
+ if (!browser.instances.length && browser.provider?.name === "preview") browser.instances = [{ browser: "chromium" }];
222
+ if (browser.name && instances?.length) {
223
+ // --browser=chromium filters configs to a single one
224
+ browser.instances = browser.instances.filter((instance) => instance.browser === browser.name);
225
+ // if `instances` were defined, but now they are empty,
226
+ // let's throw an error because the filter is invalid
227
+ if (!browser.instances.length) throw new Error([`"browser.instances" was set in the config, but the array is empty. Define at least one browser config.`, ` The "browser.name" was set to "${browser.name}" which filtered all configs (${instances.map((c) => c.browser).join(", ")}). Did you mean to use another name?`].join(""));
228
+ }
229
+ }
230
+ if (resolved.coverage.enabled && resolved.coverage.provider === "istanbul" && resolved.experimental?.viteModuleRunner === false) throw new Error(`"Istanbul" coverage provider is not compatible with "experimental.viteModuleRunner: false". Please, enable "viteModuleRunner" or switch to "v8" coverage provider.`);
231
+ const containsChromium = hasBrowserChromium(vitest, resolved);
232
+ const hasOnlyChromium = hasOnlyBrowserChromium(vitest, resolved);
233
+ // Browser-mode "Chromium" only features:
234
+ if (browser.enabled && (!containsChromium || !hasOnlyChromium)) {
235
+ const browserConfig = `
236
+ {
237
+ browser: {
238
+ provider: ${browser.provider?.name || "preview"}(),
239
+ instances: [
240
+ ${(browser.instances || []).map((i) => `{ browser: '${i.browser}' }`).join(",\n ")}
241
+ ],
242
+ },
243
+ }
244
+ `.trim();
245
+ const preferredProvider = !browser.provider?.name || browser.provider.name === "preview" ? "playwright" : browser.provider.name;
246
+ const correctExample = `
247
+ {
248
+ browser: {
249
+ provider: ${preferredProvider}(),
250
+ instances: [
251
+ { browser: '${preferredProvider === "playwright" ? "chromium" : "chrome"}' }
252
+ ],
253
+ },
254
+ }
255
+ `.trim();
256
+ // requires all projects to be chromium
257
+ if (!hasOnlyChromium && resolved.coverage.enabled && resolved.coverage.provider === "v8") {
258
+ const coverageExample = `
259
+ {
260
+ coverage: {
261
+ provider: 'istanbul',
262
+ },
263
+ }
264
+ `.trim();
265
+ throw new Error(`@vitest/coverage-v8 does not work with\n${browserConfig}\n\nUse either:\n${correctExample}\n\n...or change your coverage provider to:\n${coverageExample}\n`);
266
+ }
267
+ // ignores non-chromium browsers when there is at least one chromium project
268
+ if (!containsChromium && (resolved.inspect || resolved.inspectBrk)) {
269
+ const inspectOption = `--inspect${resolved.inspectBrk ? "-brk" : ""}`;
270
+ throw new Error(`${inspectOption} does not work with\n${browserConfig}\n\nUse either:\n${correctExample}\n\n...or disable ${inspectOption}\n`);
271
+ }
272
+ }
273
+ resolved.coverage.reporter = resolveCoverageReporters(resolved.coverage.reporter);
274
+ if (resolved.coverage.enabled && resolved.coverage.reportsDirectory) {
275
+ const reportsDirectory = resolve(resolved.root, resolved.coverage.reportsDirectory);
276
+ if (reportsDirectory === resolved.root || reportsDirectory === process.cwd()) throw new Error(`You cannot set "coverage.reportsDirectory" as ${reportsDirectory}. Vitest needs to be able to remove this directory before test run`);
277
+ }
278
+ if (resolved.coverage.enabled && resolved.coverage.provider === "custom" && resolved.coverage.customProviderModule) resolved.coverage.customProviderModule = resolvePath(resolved.coverage.customProviderModule, resolved.root);
279
+ resolved.expect ??= {};
280
+ resolved.deps ??= {};
281
+ resolved.deps.moduleDirectories ??= [];
282
+ resolved.deps.optimizer ??= {};
283
+ resolved.deps.optimizer.ssr ??= {};
284
+ resolved.deps.optimizer.ssr.enabled ??= false;
285
+ resolved.deps.optimizer.client ??= {};
286
+ resolved.deps.optimizer.client.enabled ??= false;
287
+ resolved.deps.web ??= {};
288
+ resolved.deps.web.transformAssets ??= true;
289
+ resolved.deps.web.transformCss ??= true;
290
+ resolved.deps.web.transformGlobPattern ??= [];
291
+ resolved.setupFiles = toArray(resolved.setupFiles || []).map((file) => resolvePath(file, resolved.root));
292
+ resolved.globalSetup = toArray(resolved.globalSetup || []).map((file) => resolvePath(file, resolved.root));
293
+ // Add hard-coded default coverage exclusions. These cannot be overidden by user config.
294
+ // Override original exclude array for cases where user re-uses same object in test.exclude.
295
+ resolved.coverage.exclude = [
296
+ ...resolved.coverage.exclude,
297
+ ...resolved.setupFiles.map((file) => `${resolved.coverage.allowExternal ? "**/" : ""}${relative(resolved.root, file)}`),
298
+ ...resolved.include,
299
+ resolved.config && slash(resolved.config),
300
+ ...configFiles,
301
+ "**/virtual:*",
302
+ "**/__x00__*",
303
+ "**/node_modules/**"
304
+ ].filter((pattern) => typeof pattern === "string");
305
+ resolved.forceRerunTriggers = [...resolved.forceRerunTriggers, ...resolved.setupFiles];
306
+ if (resolved.cliExclude) resolved.exclude.push(...resolved.cliExclude);
307
+ if (resolved.runner) resolved.runner = resolvePath(resolved.runner, resolved.root);
308
+ resolved.attachmentsDir = resolve(resolved.root, resolved.attachmentsDir ?? ".vitest-attachments");
309
+ if (resolved.snapshotEnvironment) resolved.snapshotEnvironment = resolvePath(resolved.snapshotEnvironment, resolved.root);
310
+ resolved.testNamePattern = resolved.testNamePattern ? resolved.testNamePattern instanceof RegExp ? resolved.testNamePattern : new RegExp(resolved.testNamePattern) : void 0;
311
+ if (resolved.snapshotFormat && "plugins" in resolved.snapshotFormat) {
312
+ resolved.snapshotFormat.plugins = [];
313
+ // TODO: support it via separate config (like DiffOptions) or via `Function.toString()`
314
+ if (typeof resolved.snapshotFormat.compareKeys === "function") throw new TypeError(`"snapshotFormat.compareKeys" function is not supported.`);
315
+ }
316
+ const UPDATE_SNAPSHOT = resolved.update || process.env.UPDATE_SNAPSHOT;
317
+ resolved.snapshotOptions = {
318
+ expand: resolved.expandSnapshotDiff ?? false,
319
+ snapshotFormat: resolved.snapshotFormat || {},
320
+ updateSnapshot: isCI && !UPDATE_SNAPSHOT ? "none" : UPDATE_SNAPSHOT ? "all" : "new",
321
+ resolveSnapshotPath: options.resolveSnapshotPath,
322
+ snapshotEnvironment: null
323
+ };
324
+ resolved.snapshotSerializers ??= [];
325
+ resolved.snapshotSerializers = resolved.snapshotSerializers.map((file) => resolvePath(file, resolved.root));
326
+ resolved.forceRerunTriggers.push(...resolved.snapshotSerializers);
327
+ if (options.resolveSnapshotPath) delete resolved.resolveSnapshotPath;
328
+ resolved.execArgv ??= [];
329
+ resolved.pool ??= "threads";
330
+ if (resolved.pool === "vmForks" || resolved.pool === "vmThreads" || resolved.pool === "typescript") resolved.isolate = false;
331
+ if (process.env.VITEST_MAX_WORKERS) resolved.maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS);
332
+ if (mode === "benchmark") {
333
+ resolved.benchmark = {
334
+ ...benchmarkConfigDefaults,
335
+ ...resolved.benchmark
336
+ };
337
+ // override test config
338
+ resolved.coverage.enabled = false;
339
+ resolved.typecheck.enabled = false;
340
+ resolved.include = resolved.benchmark.include;
341
+ resolved.exclude = resolved.benchmark.exclude;
342
+ resolved.includeSource = resolved.benchmark.includeSource;
343
+ const reporters = Array.from(new Set([...toArray(resolved.benchmark.reporters), ...toArray(options.reporter)])).filter(Boolean);
344
+ if (reporters.length) resolved.benchmark.reporters = reporters;
345
+ else resolved.benchmark.reporters = ["default"];
346
+ if (options.outputFile) resolved.benchmark.outputFile = options.outputFile;
347
+ // --compare from cli
348
+ if (options.compare) resolved.benchmark.compare = options.compare;
349
+ if (options.outputJson) resolved.benchmark.outputJson = options.outputJson;
350
+ }
351
+ if (typeof resolved.diff === "string") {
352
+ resolved.diff = resolvePath(resolved.diff, resolved.root);
353
+ resolved.forceRerunTriggers.push(resolved.diff);
354
+ }
355
+ resolved.api = {
356
+ ...resolveApiServerConfig(options, defaultPort),
357
+ token: crypto.randomUUID()
358
+ };
359
+ if (options.related) resolved.related = toArray(options.related).map((file) => resolve(resolved.root, file));
360
+ /*
361
+ * Reporters can be defined in many different ways:
362
+ * { reporter: 'json' }
363
+ * { reporter: { onFinish() { method() } } }
364
+ * { reporter: ['json', { onFinish() { method() } }] }
365
+ * { reporter: [[ 'json' ]] }
366
+ * { reporter: [[ 'json' ], 'html'] }
367
+ * { reporter: [[ 'json', { outputFile: 'test.json' } ], 'html'] }
368
+ */
369
+ if (options.reporters) if (!Array.isArray(options.reporters))
370
+ // Reporter name, e.g. { reporters: 'json' }
371
+ if (typeof options.reporters === "string") resolved.reporters = [[options.reporters, {}]];
372
+ else resolved.reporters = [options.reporters];
373
+ else {
374
+ resolved.reporters = [];
375
+ for (const reporter of options.reporters) if (Array.isArray(reporter))
376
+ // Reporter with options, e.g. { reporters: [ [ 'json', { outputFile: 'test.json' } ] ] }
377
+ resolved.reporters.push([reporter[0], reporter[1] || {}]);
378
+ else if (typeof reporter === "string")
379
+ // Reporter name in array, e.g. { reporters: ["html", "json"]}
380
+ resolved.reporters.push([reporter, {}]);
381
+ else
382
+ // Inline reporter, e.g. { reporter: [{ onFinish() { method() } }] }
383
+ resolved.reporters.push(reporter);
384
+ }
385
+ if (mode !== "benchmark") {
386
+ // @ts-expect-error "reporter" is from CLI, should be absolute to the running directory
387
+ // it is passed down as "vitest --reporter ../reporter.js"
388
+ const reportersFromCLI = resolved.reporter;
389
+ const cliReporters = toArray(reportersFromCLI || []).map((reporter) => {
390
+ // ./reporter.js || ../reporter.js, but not .reporters/reporter.js
391
+ if (/^\.\.?\//.test(reporter)) return resolve(process.cwd(), reporter);
392
+ return reporter;
393
+ });
394
+ if (cliReporters.length) {
395
+ // When CLI reporters are specified, preserve options from config file
396
+ const configReportersMap = /* @__PURE__ */ new Map();
397
+ // Build a map of reporter names to their options from the config
398
+ for (const reporter of resolved.reporters) if (Array.isArray(reporter)) {
399
+ const [reporterName, reporterOptions] = reporter;
400
+ if (typeof reporterName === "string") configReportersMap.set(reporterName, reporterOptions);
401
+ }
402
+ resolved.reporters = Array.from(new Set(toArray(cliReporters))).filter(Boolean).map((reporter) => [reporter, configReportersMap.get(reporter) || {}]);
403
+ }
404
+ }
405
+ if (!resolved.reporters.length) {
406
+ resolved.reporters.push(["default", {}]);
407
+ // also enable github-actions reporter as a default
408
+ if (process.env.GITHUB_ACTIONS === "true") resolved.reporters.push(["github-actions", {}]);
409
+ }
410
+ if (resolved.changed) resolved.passWithNoTests ??= true;
411
+ resolved.css ??= {};
412
+ if (typeof resolved.css === "object") {
413
+ resolved.css.modules ??= {};
414
+ resolved.css.modules.classNameStrategy ??= "stable";
415
+ }
416
+ if (resolved.cache !== false) {
417
+ if (resolved.cache && typeof resolved.cache.dir === "string") vitest.logger.deprecate(`"cache.dir" is deprecated, use Vite's "cacheDir" instead if you want to change the cache director. Note caches will be written to "cacheDir\/vitest"`);
418
+ resolved.cache = { dir: viteConfig.cacheDir };
419
+ }
420
+ resolved.sequence ??= {};
421
+ if (resolved.sequence.shuffle && typeof resolved.sequence.shuffle === "object") {
422
+ const { files, tests } = resolved.sequence.shuffle;
423
+ resolved.sequence.sequencer ??= files ? RandomSequencer : BaseSequencer;
424
+ resolved.sequence.shuffle = tests;
425
+ }
426
+ if (!resolved.sequence?.sequencer)
427
+ // CLI flag has higher priority
428
+ resolved.sequence.sequencer = resolved.sequence.shuffle ? RandomSequencer : BaseSequencer;
429
+ resolved.sequence.groupOrder ??= 0;
430
+ resolved.sequence.hooks ??= "stack";
431
+ if (resolved.sequence.sequencer === RandomSequencer) resolved.sequence.seed ??= Date.now();
432
+ resolved.typecheck = {
433
+ ...configDefaults.typecheck,
434
+ ...resolved.typecheck
435
+ };
436
+ resolved.typecheck ??= {};
437
+ resolved.typecheck.enabled ??= false;
438
+ if (resolved.typecheck.enabled) logger.console.warn(c.yellow("Testing types with tsc and vue-tsc is an experimental feature.\nBreaking changes might not follow SemVer, please pin Vitest's version when using it."));
439
+ resolved.browser.enabled ??= false;
440
+ resolved.browser.headless ??= isCI;
441
+ resolved.browser.isolate ??= resolved.isolate ?? true;
442
+ resolved.browser.fileParallelism ??= options.fileParallelism ?? mode !== "benchmark";
443
+ // disable in headless mode by default, and if CI is detected
444
+ resolved.browser.ui ??= resolved.browser.headless === true ? false : !isCI;
445
+ resolved.browser.commands ??= {};
446
+ if (resolved.browser.screenshotDirectory) resolved.browser.screenshotDirectory = resolve(resolved.root, resolved.browser.screenshotDirectory);
447
+ if (resolved.inspector.enabled) resolved.browser.trackUnhandledErrors ??= false;
448
+ resolved.browser.viewport ??= {};
449
+ resolved.browser.viewport.width ??= 414;
450
+ resolved.browser.viewport.height ??= 896;
451
+ resolved.browser.locators ??= {};
452
+ resolved.browser.locators.testIdAttribute ??= "data-testid";
453
+ if (typeof resolved.browser.provider === "string") {
454
+ const source = `@vitest/browser-${resolved.browser.provider}`;
455
+ throw new TypeError(`The \`browser.provider\` configuration was changed to accept a factory instead of a string. Add an import of "${resolved.browser.provider}" from "${source}" instead. See: https://vitest.dev/config/browser/provider`);
456
+ }
457
+ const isPreview = resolved.browser.provider?.name === "preview";
458
+ if (!isPreview && resolved.browser.enabled && provider === "stackblitz") throw new Error(`stackblitz environment does not support the ${resolved.browser.provider?.name} provider. Please, use "@vitest/browser-preview" instead.`);
459
+ if (isPreview && resolved.browser.screenshotFailures === true) {
460
+ console.warn(c.yellow([
461
+ `Browser provider "preview" doesn't support screenshots, `,
462
+ `so "browser.screenshotFailures" option is forcefully disabled. `,
463
+ `Set "browser.screenshotFailures" to false or remove it from the config to suppress this warning.`
464
+ ].join("")));
465
+ resolved.browser.screenshotFailures = false;
466
+ } else resolved.browser.screenshotFailures ??= !isPreview && !resolved.browser.ui;
467
+ if (resolved.browser.provider && resolved.browser.provider.options == null) resolved.browser.provider.options = {};
468
+ resolved.browser.api = resolveApiServerConfig(resolved.browser, defaultBrowserPort) || { port: defaultBrowserPort };
469
+ // enable includeTaskLocation by default in UI mode
470
+ if (resolved.browser.enabled) {
471
+ if (resolved.browser.ui) resolved.includeTaskLocation ??= true;
472
+ } else if (resolved.ui) resolved.includeTaskLocation ??= true;
473
+ if (typeof resolved.browser.trace === "string" || !resolved.browser.trace) resolved.browser.trace = { mode: resolved.browser.trace || "off" };
474
+ if (resolved.browser.trace.tracesDir != null) resolved.browser.trace.tracesDir = resolvePath(resolved.browser.trace.tracesDir, resolved.root);
475
+ if (toArray(resolved.reporters).some((reporter) => {
476
+ if (Array.isArray(reporter)) return reporter[0] === "html";
477
+ return false;
478
+ })) resolved.includeTaskLocation ??= true;
479
+ resolved.server ??= {};
480
+ resolved.server.deps ??= {};
481
+ if (resolved.server.debug?.dump || process.env.VITEST_DEBUG_DUMP) {
482
+ const userFolder = resolved.server.debug?.dump || process.env.VITEST_DEBUG_DUMP;
483
+ resolved.dumpDir = resolve(resolved.root, typeof userFolder === "string" && userFolder !== "true" ? userFolder : ".vitest-dump", resolved.name || "root");
484
+ }
485
+ resolved.testTimeout ??= resolved.browser.enabled ? 15e3 : 5e3;
486
+ resolved.hookTimeout ??= resolved.browser.enabled ? 3e4 : 1e4;
487
+ resolved.experimental ??= {};
488
+ if (resolved.experimental.openTelemetry?.sdkPath) {
489
+ const sdkPath = resolve(resolved.root, resolved.experimental.openTelemetry.sdkPath);
490
+ resolved.experimental.openTelemetry.sdkPath = pathToFileURL(sdkPath).toString();
491
+ }
492
+ if (resolved.experimental.openTelemetry?.browserSdkPath) {
493
+ const browserSdkPath = resolve(resolved.root, resolved.experimental.openTelemetry.browserSdkPath);
494
+ resolved.experimental.openTelemetry.browserSdkPath = browserSdkPath;
495
+ }
496
+ if (resolved.experimental.fsModuleCachePath) resolved.experimental.fsModuleCachePath = resolve(resolved.root, resolved.experimental.fsModuleCachePath);
497
+ return resolved;
498
+ }
499
+ function isBrowserEnabled(config) {
500
+ return Boolean(config.browser?.enabled);
501
+ }
502
+ function resolveCoverageReporters(configReporters) {
503
+ // E.g. { reporter: "html" }
504
+ if (!Array.isArray(configReporters)) return [[configReporters, {}]];
505
+ const resolvedReporters = [];
506
+ for (const reporter of configReporters) if (Array.isArray(reporter))
507
+ // E.g. { reporter: [ ["html", { skipEmpty: true }], ["lcov"], ["json", { file: "map.json" }] ]}
508
+ resolvedReporters.push([reporter[0], reporter[1] || {}]);
509
+ else
510
+ // E.g. { reporter: ["html", "json"]}
511
+ resolvedReporters.push([reporter, {}]);
512
+ return resolvedReporters;
513
+ }
514
+ function isChromiumName(provider, name) {
515
+ if (provider === "playwright") return name === "chromium";
516
+ return name === "chrome" || name === "edge";
517
+ }
518
+ function hasBrowserChromium(vitest, config) {
519
+ const browser = config.browser;
520
+ if (!browser || !browser.provider || browser.provider.name === "preview" || !browser.enabled) return false;
521
+ if (browser.name) return isChromiumName(browser.provider.name, browser.name);
522
+ if (!browser.instances) return false;
523
+ return browser.instances.some((instance) => {
524
+ const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser);
525
+ // browser config is filtered out
526
+ if (!vitest.matchesProjectFilter(name)) return false;
527
+ return isChromiumName(browser.provider.name, instance.browser);
528
+ });
529
+ }
530
+ function hasOnlyBrowserChromium(vitest, config) {
531
+ const browser = config.browser;
532
+ if (!browser || !browser.provider || browser.provider.name === "preview" || !browser.enabled) return false;
533
+ if (browser.name) return isChromiumName(browser.provider.name, browser.name);
534
+ if (!browser.instances) return false;
535
+ return browser.instances.every((instance) => {
536
+ const name = instance.name || (config.name ? `${config.name} (${instance.browser})` : instance.browser);
537
+ // browser config is filtered out
538
+ if (!vitest.matchesProjectFilter(name)) return true;
539
+ return isChromiumName(browser.provider.name, instance.browser);
540
+ });
541
+ }
542
+
543
+ const THRESHOLD_KEYS = [
544
+ "lines",
545
+ "functions",
546
+ "statements",
547
+ "branches"
548
+ ];
549
+ const GLOBAL_THRESHOLDS_KEY = "global";
550
+ const DEFAULT_PROJECT = Symbol.for("default-project");
551
+ let uniqueId = 0;
552
+ async function getCoverageProvider(options, loader) {
553
+ const coverageModule = await resolveCoverageProviderModule(options, loader);
554
+ if (coverageModule) return coverageModule.getProvider();
555
+ return null;
556
+ }
557
+ class BaseCoverageProvider {
558
+ ctx;
559
+ name;
560
+ version;
561
+ options;
562
+ globCache = /* @__PURE__ */ new Map();
563
+ autoUpdateMarker = "\n// __VITEST_COVERAGE_MARKER__";
564
+ coverageFiles = /* @__PURE__ */ new Map();
565
+ pendingPromises = [];
566
+ coverageFilesDirectory;
567
+ roots = [];
568
+ _initialize(ctx) {
569
+ this.ctx = ctx;
570
+ if (ctx.version !== this.version) ctx.logger.warn(c.yellow(`Loaded ${c.inverse(c.yellow(` vitest@${ctx.version} `))} and ${c.inverse(c.yellow(` @vitest/coverage-${this.name}@${this.version} `))}.
571
+ Running mixed versions is not supported and may lead into bugs
572
+ Update your dependencies and make sure the versions match.`));
573
+ const config = ctx._coverageOptions;
574
+ this.options = {
575
+ ...coverageConfigDefaults,
576
+ ...config,
577
+ provider: this.name,
578
+ reportsDirectory: resolve(ctx.config.root, config.reportsDirectory || coverageConfigDefaults.reportsDirectory),
579
+ reporter: resolveCoverageReporters(config.reporter || coverageConfigDefaults.reporter),
580
+ thresholds: config.thresholds && {
581
+ ...config.thresholds,
582
+ lines: config.thresholds["100"] ? 100 : config.thresholds.lines,
583
+ branches: config.thresholds["100"] ? 100 : config.thresholds.branches,
584
+ functions: config.thresholds["100"] ? 100 : config.thresholds.functions,
585
+ statements: config.thresholds["100"] ? 100 : config.thresholds.statements
586
+ }
587
+ };
588
+ const shard = this.ctx.config.shard;
589
+ const tempDirectory = `.tmp${shard ? `-${shard.index}-${shard.count}` : ""}`;
590
+ this.coverageFilesDirectory = resolve(this.options.reportsDirectory, tempDirectory);
591
+ // If --project filter is set pick only roots of resolved projects
592
+ this.roots = ctx.config.project?.length ? [...new Set(ctx.projects.map((project) => project.config.root))] : [ctx.config.root];
593
+ }
594
+ /**
595
+ * Check if file matches `coverage.include` but not `coverage.exclude`
596
+ */
597
+ isIncluded(_filename, root) {
598
+ const roots = root ? [root] : this.roots;
599
+ const filename = slash(_filename);
600
+ const cacheHit = this.globCache.get(filename);
601
+ if (cacheHit !== void 0) return cacheHit;
602
+ // File outside project root with default allowExternal
603
+ if (this.options.allowExternal === false && roots.every((root) => !filename.startsWith(root))) {
604
+ this.globCache.set(filename, false);
605
+ return false;
606
+ }
607
+ // By default `coverage.include` matches all files, except "coverage.exclude"
608
+ const glob = this.options.include || "**";
609
+ const included = pm.isMatch(filename, glob, {
610
+ contains: true,
611
+ dot: true,
612
+ ignore: this.options.exclude
613
+ });
614
+ this.globCache.set(filename, included);
615
+ return included;
616
+ }
617
+ async getUntestedFilesByRoot(testedFiles, include, root) {
618
+ let includedFiles = await glob(include, {
619
+ cwd: root,
620
+ ignore: [...this.options.exclude, ...testedFiles.map((file) => slash(file))],
621
+ absolute: true,
622
+ dot: true,
623
+ onlyFiles: true
624
+ });
625
+ // Run again through picomatch as tinyglobby's exclude pattern is different ({ "exclude": ["math"] } should ignore "src/math.ts")
626
+ includedFiles = includedFiles.filter((file) => this.isIncluded(file, root));
627
+ if (this.ctx.config.changed) includedFiles = (this.ctx.config.related || []).filter((file) => includedFiles.includes(file));
628
+ return includedFiles.map((file) => slash(path.resolve(root, file)));
629
+ }
630
+ async getUntestedFiles(testedFiles) {
631
+ if (this.options.include == null) return [];
632
+ const rootMapper = this.getUntestedFilesByRoot.bind(this, testedFiles, this.options.include);
633
+ return (await Promise.all(this.roots.map(rootMapper))).flatMap((files) => files);
634
+ }
635
+ createCoverageMap() {
636
+ throw new Error("BaseReporter's createCoverageMap was not overwritten");
637
+ }
638
+ async generateReports(_, __) {
639
+ throw new Error("BaseReporter's generateReports was not overwritten");
640
+ }
641
+ async parseConfigModule(_) {
642
+ throw new Error("BaseReporter's parseConfigModule was not overwritten");
643
+ }
644
+ resolveOptions() {
645
+ return this.options;
646
+ }
647
+ async clean(clean = true) {
648
+ if (clean && existsSync(this.options.reportsDirectory)) await promises.rm(this.options.reportsDirectory, {
649
+ recursive: true,
650
+ force: true,
651
+ maxRetries: 10
652
+ });
653
+ if (existsSync(this.coverageFilesDirectory)) await promises.rm(this.coverageFilesDirectory, {
654
+ recursive: true,
655
+ force: true,
656
+ maxRetries: 10
657
+ });
658
+ await promises.mkdir(this.coverageFilesDirectory, { recursive: true });
659
+ this.coverageFiles = /* @__PURE__ */ new Map();
660
+ this.pendingPromises = [];
661
+ }
662
+ onAfterSuiteRun({ coverage, environment, projectName, testFiles }) {
663
+ if (!coverage) return;
664
+ let entry = this.coverageFiles.get(projectName || DEFAULT_PROJECT);
665
+ if (!entry) {
666
+ entry = {};
667
+ this.coverageFiles.set(projectName || DEFAULT_PROJECT, entry);
668
+ }
669
+ const testFilenames = testFiles.join();
670
+ const filename = resolve(this.coverageFilesDirectory, `coverage-${uniqueId++}.json`);
671
+ entry[environment] ??= {};
672
+ // If there's a result from previous run, overwrite it
673
+ entry[environment][testFilenames] = filename;
674
+ const promise = promises.writeFile(filename, JSON.stringify(coverage), "utf-8");
675
+ this.pendingPromises.push(promise);
676
+ }
677
+ async readCoverageFiles({ onFileRead, onFinished, onDebug }) {
678
+ let index = 0;
679
+ const total = this.pendingPromises.length;
680
+ await Promise.all(this.pendingPromises);
681
+ this.pendingPromises = [];
682
+ for (const [projectName, coveragePerProject] of this.coverageFiles.entries()) for (const [environment, coverageByTestfiles] of Object.entries(coveragePerProject)) {
683
+ const filenames = Object.values(coverageByTestfiles);
684
+ const project = this.ctx.getProjectByName(projectName);
685
+ for (const chunk of this.toSlices(filenames, this.options.processingConcurrency)) {
686
+ if (onDebug.enabled) {
687
+ index += chunk.length;
688
+ onDebug(`Reading coverage results ${index}/${total}`);
689
+ }
690
+ await Promise.all(chunk.map(async (filename) => {
691
+ const contents = await promises.readFile(filename, "utf-8");
692
+ onFileRead(JSON.parse(contents));
693
+ }));
694
+ }
695
+ await onFinished(project, environment);
696
+ }
697
+ }
698
+ async cleanAfterRun() {
699
+ this.coverageFiles = /* @__PURE__ */ new Map();
700
+ await promises.rm(this.coverageFilesDirectory, { recursive: true });
701
+ // Remove empty reports directory, e.g. when only text-reporter is used
702
+ if (readdirSync(this.options.reportsDirectory).length === 0) await promises.rm(this.options.reportsDirectory, { recursive: true });
703
+ }
704
+ async onTestFailure() {
705
+ if (!this.options.reportOnFailure) await this.cleanAfterRun();
706
+ }
707
+ async reportCoverage(coverageMap, { allTestsRun }) {
708
+ await this.generateReports(coverageMap || this.createCoverageMap(), allTestsRun);
709
+ if (!(!this.options.cleanOnRerun && this.ctx.config.watch)) await this.cleanAfterRun();
710
+ }
711
+ async reportThresholds(coverageMap, allTestsRun) {
712
+ const resolvedThresholds = this.resolveThresholds(coverageMap);
713
+ this.checkThresholds(resolvedThresholds);
714
+ if (this.options.thresholds?.autoUpdate && allTestsRun) {
715
+ if (!this.ctx.vite.config.configFile) throw new Error("Missing configurationFile. The \"coverage.thresholds.autoUpdate\" can only be enabled when configuration file is used.");
716
+ const configFilePath = this.ctx.vite.config.configFile;
717
+ const configModule = await this.parseConfigModule(configFilePath);
718
+ await this.updateThresholds({
719
+ thresholds: resolvedThresholds,
720
+ configurationFile: configModule,
721
+ onUpdate: () => writeFileSync(configFilePath, configModule.generate().code.replace(this.autoUpdateMarker, ""), "utf-8")
722
+ });
723
+ }
724
+ }
725
+ /**
726
+ * Constructs collected coverage and users' threshold options into separate sets
727
+ * where each threshold set holds their own coverage maps. Threshold set is either
728
+ * for specific files defined by glob pattern or global for all other files.
729
+ */
730
+ resolveThresholds(coverageMap) {
731
+ const resolvedThresholds = [];
732
+ const files = coverageMap.files();
733
+ const globalCoverageMap = this.createCoverageMap();
734
+ for (const key of Object.keys(this.options.thresholds)) {
735
+ if (key === "perFile" || key === "autoUpdate" || key === "100" || THRESHOLD_KEYS.includes(key)) continue;
736
+ const glob = key;
737
+ const globThresholds = resolveGlobThresholds(this.options.thresholds[glob]);
738
+ const globCoverageMap = this.createCoverageMap();
739
+ const matcher = pm(glob);
740
+ const matchingFiles = files.filter((file) => matcher(relative(this.ctx.config.root, file)));
741
+ for (const file of matchingFiles) {
742
+ const fileCoverage = coverageMap.fileCoverageFor(file);
743
+ globCoverageMap.addFileCoverage(fileCoverage);
744
+ }
745
+ resolvedThresholds.push({
746
+ name: glob,
747
+ coverageMap: globCoverageMap,
748
+ thresholds: globThresholds
749
+ });
750
+ }
751
+ // Global threshold is for all files, even if they are included by glob patterns
752
+ for (const file of files) {
753
+ const fileCoverage = coverageMap.fileCoverageFor(file);
754
+ globalCoverageMap.addFileCoverage(fileCoverage);
755
+ }
756
+ resolvedThresholds.unshift({
757
+ name: GLOBAL_THRESHOLDS_KEY,
758
+ coverageMap: globalCoverageMap,
759
+ thresholds: {
760
+ branches: this.options.thresholds?.branches,
761
+ functions: this.options.thresholds?.functions,
762
+ lines: this.options.thresholds?.lines,
763
+ statements: this.options.thresholds?.statements
764
+ }
765
+ });
766
+ return resolvedThresholds;
767
+ }
768
+ /**
769
+ * Check collected coverage against configured thresholds. Sets exit code to 1 when thresholds not reached.
770
+ */
771
+ checkThresholds(allThresholds) {
772
+ for (const { coverageMap, thresholds, name } of allThresholds) {
773
+ if (thresholds.branches === void 0 && thresholds.functions === void 0 && thresholds.lines === void 0 && thresholds.statements === void 0) continue;
774
+ // Construct list of coverage summaries where thresholds are compared against
775
+ const summaries = this.options.thresholds?.perFile ? coverageMap.files().map((file) => ({
776
+ file,
777
+ summary: coverageMap.fileCoverageFor(file).toSummary()
778
+ })) : [{
779
+ file: null,
780
+ summary: coverageMap.getCoverageSummary()
781
+ }];
782
+ // Check thresholds of each summary
783
+ for (const { summary, file } of summaries) for (const thresholdKey of THRESHOLD_KEYS) {
784
+ const threshold = thresholds[thresholdKey];
785
+ if (threshold === void 0) continue;
786
+ /**
787
+ * Positive thresholds are treated as minimum coverage percentages (X means: X% of lines must be covered),
788
+ * while negative thresholds are treated as maximum uncovered counts (-X means: X lines may be uncovered).
789
+ */
790
+ if (threshold >= 0) {
791
+ const coverage = summary.data[thresholdKey].pct;
792
+ if (coverage < threshold) {
793
+ process.exitCode = 1;
794
+ /**
795
+ * Generate error message based on perFile flag:
796
+ * - ERROR: Coverage for statements (33.33%) does not meet threshold (85%) for src/math.ts
797
+ * - ERROR: Coverage for statements (50%) does not meet global threshold (85%)
798
+ */
799
+ let errorMessage = `ERROR: Coverage for ${thresholdKey} (${coverage}%) does not meet ${name === GLOBAL_THRESHOLDS_KEY ? name : `"${name}"`} threshold (${threshold}%)`;
800
+ if (this.options.thresholds?.perFile && file) errorMessage += ` for ${relative("./", file).replace(/\\/g, "/")}`;
801
+ this.ctx.logger.error(errorMessage);
802
+ }
803
+ } else {
804
+ const uncovered = summary.data[thresholdKey].total - summary.data[thresholdKey].covered;
805
+ const absoluteThreshold = threshold * -1;
806
+ if (uncovered > absoluteThreshold) {
807
+ process.exitCode = 1;
808
+ /**
809
+ * Generate error message based on perFile flag:
810
+ * - ERROR: Uncovered statements (33) exceed threshold (30) for src/math.ts
811
+ * - ERROR: Uncovered statements (33) exceed global threshold (30)
812
+ */
813
+ let errorMessage = `ERROR: Uncovered ${thresholdKey} (${uncovered}) exceed ${name === GLOBAL_THRESHOLDS_KEY ? name : `"${name}"`} threshold (${absoluteThreshold})`;
814
+ if (this.options.thresholds?.perFile && file) errorMessage += ` for ${relative("./", file).replace(/\\/g, "/")}`;
815
+ this.ctx.logger.error(errorMessage);
816
+ }
817
+ }
818
+ }
819
+ }
820
+ }
821
+ /**
822
+ * Check if current coverage is above configured thresholds and bump the thresholds if needed
823
+ */
824
+ async updateThresholds({ thresholds: allThresholds, onUpdate, configurationFile }) {
825
+ let updatedThresholds = false;
826
+ const config = resolveConfig(configurationFile);
827
+ assertConfigurationModule(config);
828
+ for (const { coverageMap, thresholds, name } of allThresholds) {
829
+ const summaries = this.options.thresholds?.perFile ? coverageMap.files().map((file) => coverageMap.fileCoverageFor(file).toSummary()) : [coverageMap.getCoverageSummary()];
830
+ const thresholdsToUpdate = [];
831
+ for (const key of THRESHOLD_KEYS) {
832
+ const threshold = thresholds[key] ?? 100;
833
+ /**
834
+ * Positive thresholds are treated as minimum coverage percentages (X means: X% of lines must be covered),
835
+ * while negative thresholds are treated as maximum uncovered counts (-X means: X lines may be uncovered).
836
+ */
837
+ if (threshold >= 0) {
838
+ const actual = Math.min(...summaries.map((summary) => summary[key].pct));
839
+ if (actual > threshold) thresholdsToUpdate.push([key, actual]);
840
+ } else {
841
+ const absoluteThreshold = threshold * -1;
842
+ const actual = Math.max(...summaries.map((summary) => summary[key].total - summary[key].covered));
843
+ if (actual < absoluteThreshold) {
844
+ // If everything was covered, set new threshold to 100% (since a threshold of 0 would be considered as 0%)
845
+ const updatedThreshold = actual === 0 ? 100 : actual * -1;
846
+ thresholdsToUpdate.push([key, updatedThreshold]);
847
+ }
848
+ }
849
+ }
850
+ if (thresholdsToUpdate.length === 0) continue;
851
+ updatedThresholds = true;
852
+ const thresholdFormatter = typeof this.options.thresholds?.autoUpdate === "function" ? this.options.thresholds?.autoUpdate : (value) => value;
853
+ for (const [threshold, newValue] of thresholdsToUpdate) {
854
+ const formattedValue = thresholdFormatter(newValue);
855
+ if (name === GLOBAL_THRESHOLDS_KEY) config.test.coverage.thresholds[threshold] = formattedValue;
856
+ else {
857
+ const glob = config.test.coverage.thresholds[name];
858
+ glob[threshold] = formattedValue;
859
+ }
860
+ }
861
+ }
862
+ if (updatedThresholds) {
863
+ this.ctx.logger.log("Updating thresholds to configuration file. You may want to push with updated coverage thresholds.");
864
+ onUpdate();
865
+ }
866
+ }
867
+ async mergeReports(coverageMaps) {
868
+ const coverageMap = this.createCoverageMap();
869
+ for (const coverage of coverageMaps) coverageMap.merge(coverage);
870
+ await this.generateReports(coverageMap, true);
871
+ }
872
+ hasTerminalReporter(reporters) {
873
+ return reporters.some(([reporter]) => reporter === "text" || reporter === "text-summary" || reporter === "text-lcov" || reporter === "teamcity");
874
+ }
875
+ toSlices(array, size) {
876
+ return array.reduce((chunks, item) => {
877
+ const index = Math.max(0, chunks.length - 1);
878
+ const lastChunk = chunks[index] || [];
879
+ chunks[index] = lastChunk;
880
+ if (lastChunk.length >= size) chunks.push([item]);
881
+ else lastChunk.push(item);
882
+ return chunks;
883
+ }, []);
884
+ }
885
+ // TODO: should this be abstracted in `project`/`vitest` instead?
886
+ // if we decide to keep `viteModuleRunner: false`, we will need to abstract transformation in both main thread and tests
887
+ // custom --import=module.registerHooks need to be transformed as well somehow
888
+ async transformFile(url, project, viteEnvironment) {
889
+ const config = project.config;
890
+ // vite is disabled, should transform manually if possible
891
+ if (config.experimental.viteModuleRunner === false) {
892
+ const pathname = url.split("?")[0];
893
+ const filename = pathname.startsWith("file://") ? fileURLToPath(pathname) : pathname;
894
+ const extension = path.extname(filename);
895
+ if (!(extension === ".ts" || extension === ".mts" || extension === ".cts")) return {
896
+ code: await promises.readFile(filename, "utf-8"),
897
+ map: null
898
+ };
899
+ if (!module$1.stripTypeScriptTypes) throw new Error(`Cannot parse '${url}' because "module.stripTypeScriptTypes" is not supported. TypeScript coverage requires Node.js 22.15 or higher. This is NOT a bug of Vitest.`);
900
+ const isTransform = process.execArgv.includes("--experimental-transform-types") || config.execArgv.includes("--experimental-transform-types") || process.env.NODE_OPTIONS?.includes("--experimental-transform-types") || config.env?.NODE_OPTIONS?.includes("--experimental-transform-types");
901
+ const code = await promises.readFile(filename, "utf-8");
902
+ return {
903
+ code: module$1.stripTypeScriptTypes(code, { mode: isTransform ? "transform" : "strip" }),
904
+ map: null
905
+ };
906
+ }
907
+ if (project.isBrowserEnabled() || viteEnvironment === "__browser__") {
908
+ const result = await (project.browser?.vite.environments.client || project.vite.environments.client).transformRequest(url);
909
+ if (result) return result;
910
+ }
911
+ return project.vite.environments[viteEnvironment].transformRequest(url);
912
+ }
913
+ createUncoveredFileTransformer(ctx) {
914
+ const projects = new Set([...ctx.projects, ctx.getRootProject()]);
915
+ return async (filename) => {
916
+ let lastError;
917
+ for (const project of projects) {
918
+ const root = project.config.root;
919
+ // On Windows root doesn't start with "/" while filenames do
920
+ if (!filename.startsWith(root) && !filename.startsWith(`/${root}`)) continue;
921
+ try {
922
+ const environment = project.config.environment;
923
+ const viteEnvironment = environment === "jsdom" || environment === "happy-dom" ? "client" : "ssr";
924
+ return await this.transformFile(filename, project, viteEnvironment);
925
+ } catch (err) {
926
+ lastError = err;
927
+ }
928
+ }
929
+ // All vite servers failed to transform the file
930
+ throw lastError;
931
+ };
932
+ }
933
+ }
934
+ /**
935
+ * Narrow down `unknown` glob thresholds to resolved ones
936
+ */
937
+ function resolveGlobThresholds(thresholds) {
938
+ if (!thresholds || typeof thresholds !== "object") return {};
939
+ if (100 in thresholds && thresholds[100] === true) return {
940
+ lines: 100,
941
+ branches: 100,
942
+ functions: 100,
943
+ statements: 100
944
+ };
945
+ return {
946
+ lines: "lines" in thresholds && typeof thresholds.lines === "number" ? thresholds.lines : void 0,
947
+ branches: "branches" in thresholds && typeof thresholds.branches === "number" ? thresholds.branches : void 0,
948
+ functions: "functions" in thresholds && typeof thresholds.functions === "number" ? thresholds.functions : void 0,
949
+ statements: "statements" in thresholds && typeof thresholds.statements === "number" ? thresholds.statements : void 0
950
+ };
951
+ }
952
+ function assertConfigurationModule(config) {
953
+ try {
954
+ // @ts-expect-error -- Intentional unsafe null pointer check as wrapped in try-catch
955
+ if (typeof config.test.coverage.thresholds !== "object") throw new TypeError("Expected config.test.coverage.thresholds to be an object");
956
+ } catch (error) {
957
+ const message = error instanceof Error ? error.message : String(error);
958
+ throw new Error(`Unable to parse thresholds from configuration file: ${message}`);
959
+ }
960
+ }
961
+ function resolveConfig(configModule) {
962
+ const mod = configModule.exports.default;
963
+ try {
964
+ // Check for "export default { test: {...} }"
965
+ if (mod.$type === "object") return mod;
966
+ // "export default defineConfig(...)"
967
+ let config = resolveDefineConfig(mod);
968
+ if (config) return config;
969
+ // "export default mergeConfig(..., defineConfig(...))"
970
+ if (mod.$type === "function-call" && mod.$callee === "mergeConfig") {
971
+ config = resolveMergeConfig(mod);
972
+ if (config) return config;
973
+ }
974
+ } catch (error) {
975
+ // Reduce magicast's verbose errors to readable ones
976
+ throw new Error(error instanceof Error ? error.message : String(error));
977
+ }
978
+ throw new Error("Failed to update coverage thresholds. Configuration file is too complex.");
979
+ }
980
+ function resolveDefineConfig(mod) {
981
+ if (mod.$type === "function-call" && mod.$callee === "defineConfig") {
982
+ // "export default defineConfig({ test: {...} })"
983
+ if (mod.$args[0].$type === "object") return mod.$args[0];
984
+ if (mod.$args[0].$type === "arrow-function-expression") {
985
+ if (mod.$args[0].$body.$type === "object")
986
+ // "export default defineConfig(() => ({ test: {...} }))"
987
+ return mod.$args[0].$body;
988
+ // "export default defineConfig(() => mergeConfig({...}, ...))"
989
+ const config = resolveMergeConfig(mod.$args[0].$body);
990
+ if (config) return config;
991
+ }
992
+ }
993
+ }
994
+ function resolveMergeConfig(mod) {
995
+ if (mod.$type === "function-call" && mod.$callee === "mergeConfig") for (const arg of mod.$args) {
996
+ const config = resolveDefineConfig(arg);
997
+ if (config) return config;
998
+ }
999
+ }
1000
+
1001
+ export { BaseCoverageProvider as B, RandomSequencer as R, resolveApiServerConfig as a, BaseSequencer as b, getCoverageProvider as g, hash as h, isBrowserEnabled as i, resolveConfig$1 as r };