vitest 5.0.0-beta.5 → 5.0.0-beta.6

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 (46) hide show
  1. package/dist/browser.d.ts +2 -2
  2. package/dist/browser.js +2 -2
  3. package/dist/chunks/{artifact.C0OHWdod.js → artifact.BNnEI1sJ.js} +5 -5
  4. package/dist/chunks/{base.BPbmtl-C.js → base.BKfXyFN7.js} +7 -7
  5. package/dist/chunks/{browser.d.B-p-y0sB.d.ts → browser.d.DoM6vBOs.d.ts} +3 -3
  6. package/dist/chunks/{cac.DYQRhgb-.js → cac.DKrA6qEB.js} +6 -6
  7. package/dist/chunks/{cli-api.SMst4llK.js → cli-api.cZcvaUFI.js} +280 -76
  8. package/dist/chunks/{config.d.Bkl6J5iR.d.ts → config.d.CKWK3nld.d.ts} +6 -2
  9. package/dist/chunks/{coverage.d.Dn6XbyQq.d.ts → coverage.d.CCNrKR65.d.ts} +1 -1
  10. package/dist/chunks/{creator.CJiHagNv.js → creator.C4nK7Krt.js} +3 -3
  11. package/dist/chunks/{defaults.CUUnbOrq.js → defaults.B_pFOTYy.js} +1 -1
  12. package/dist/chunks/{globals.D2wDMk06.js → globals.CzUA1xn8.js} +4 -4
  13. package/dist/chunks/{index.DugGyGng.js → index.B4UTDegh.js} +3 -3
  14. package/dist/chunks/{index.CXzZ7KB8.js → index.D6NDM5O1.js} +497 -118
  15. package/dist/chunks/{index.DzNNaFWy.js → index.DNRmy2jU.js} +22 -7
  16. package/dist/chunks/{index.6dZyLbho.js → index.Djaij2xr.js} +58 -16
  17. package/dist/chunks/{init-forks.CDWjf0q6.js → init-forks.BG4bCDny.js} +1 -1
  18. package/dist/chunks/{init-threads.B-vZ8N2q.js → init-threads.DLtIP5Qq.js} +1 -1
  19. package/dist/chunks/{init.gslB1aqB.js → init.DTAQFCjl.js} +2 -2
  20. package/dist/chunks/{nativeModuleMocker.CrcWvO03.js → nativeModuleMocker.BKtCThoO.js} +1 -1
  21. package/dist/chunks/{plugin.d.BuiCQSo8.d.ts → plugin.d.Bpge8Jye.d.ts} +51 -30
  22. package/dist/chunks/{rpc.d.BLxNuPAq.d.ts → rpc.d.C6-dGZ2f.d.ts} +1 -1
  23. package/dist/chunks/{setup-common.DRlwpHsL.js → setup-common.ZNdeu5wa.js} +1 -1
  24. package/dist/chunks/{task-utils.d.CLWcZaUf.d.ts → task-utils.d.mMDXJF_I.d.ts} +20 -1
  25. package/dist/chunks/{vm._FqKqltU.js → vm.CeHWcIrU.js} +2 -2
  26. package/dist/chunks/{worker.d.Cb0Z-SnE.d.ts → worker.d.JES7ffPK.d.ts} +2 -2
  27. package/dist/cli.js +1 -1
  28. package/dist/config.cjs +1 -1
  29. package/dist/config.d.ts +8 -8
  30. package/dist/config.js +1 -1
  31. package/dist/index.d.ts +237 -12
  32. package/dist/index.js +6 -6
  33. package/dist/module-evaluator.d.ts +2 -2
  34. package/dist/module-evaluator.js +7 -0
  35. package/dist/node.d.ts +25 -14
  36. package/dist/node.js +9 -8
  37. package/dist/runtime.d.ts +3 -3
  38. package/dist/runtime.js +2 -2
  39. package/dist/worker.d.ts +3 -3
  40. package/dist/worker.js +7 -7
  41. package/dist/workers/forks.js +8 -8
  42. package/dist/workers/runVmTests.js +7 -7
  43. package/dist/workers/threads.js +8 -8
  44. package/dist/workers/vmForks.js +4 -4
  45. package/dist/workers/vmThreads.js +4 -4
  46. package/package.json +17 -17
@@ -7,24 +7,25 @@ import { unique, createDefer, slash, cleanUrl, withTrailingSlash, wrapId, isExte
7
7
  import { i as isPackageExists, r as resolveModule } from './index.CesbTg1C.js';
8
8
  import * as vite from 'vite';
9
9
  import { parseAst, searchForWorkspaceRoot, fetchModule, version, createServer, isFileLoadingAllowed, normalizePath as normalizePath$1, isRunnableDevEnvironment, mergeConfig } from 'vite';
10
- import { h as hash, b as resolveCoverageReporters, R as RandomSequencer, c as isBrowserEnabled, r as resolveConfig$1, a as resolveApiServerConfig, f as findConfigFile, p as prompt } from './index.6dZyLbho.js';
10
+ import { h as hash, b as resolveCoverageReporters, A as API_TOKEN_FILE, c as isBrowserEnabled, r as resolveConfig$1, R as RandomSequencer, a as resolveApiServerConfig, f as findConfigFile, p as prompt } from './index.Djaij2xr.js';
11
11
  import * as nodeos from 'node:os';
12
12
  import nodeos__default, { tmpdir, hostname } from 'node:os';
13
13
  import { serializeValue } from '@vitest/utils/serialize';
14
- import { v as version$1 } from './cac.DYQRhgb-.js';
14
+ import { v as version$1 } from './cac.DKrA6qEB.js';
15
15
  import { distDir, rootDir } from '../path.js';
16
- import { w as validateTags, x as limitConcurrency, y as createTagsFilter } from './artifact.C0OHWdod.js';
16
+ import { w as validateTags, x as limitConcurrency, y as createTagsFilter } from './artifact.BNnEI1sJ.js';
17
17
  import { N as NativeModuleRunner } from './nativeModuleRunner.WlMdOB52.js';
18
18
  import { createFileTask as createFileTask$1, createTaskName, calculateSuiteHash, convertTasksToEvents, hasFailed, generateFileHash, getTestName, hasFailedSnapshot, getTests, getSuites, getTasks, getFullName, someTasksAreOnly, interpretTaskModes } from '../task-utils.js';
19
19
  import { Traces } from '../traces.js';
20
20
  import { createDebug } from 'obug';
21
21
  import { rm, readFile, writeFile, rename, stat, unlink, mkdir, readdir, copyFile } from 'node:fs/promises';
22
22
  import c from 'tinyrainbow';
23
+ import crypto, { createHash } from 'node:crypto';
23
24
  import module$1, { createRequire, builtinModules, isBuiltin as isBuiltin$1 } from 'node:module';
24
25
  import url, { fileURLToPath, pathToFileURL } from 'node:url';
25
26
  import pm from 'picomatch';
26
27
  import { glob, isDynamicPattern } from 'tinyglobby';
27
- import { a as coverageConfigDefaults, c as configDefaults } from './defaults.CUUnbOrq.js';
28
+ import { a as coverageConfigDefaults, c as configDefaults } from './defaults.B_pFOTYy.js';
28
29
  import { VitestModuleEvaluator } from '#module-evaluator';
29
30
  import { ModuleRunner } from 'vite/module-runner';
30
31
  import { Console } from 'node:console';
@@ -39,7 +40,6 @@ import { defaultStackIgnorePatterns, parseErrorStacktrace, parseStacktrace } fro
39
40
  import { t as truncateString, e as errorBanner, F as F_POINTER, d as divider, f as formatProjectName, w as withLabel, a as formatTimeString, b as taskFail, s as separator, c as F_CHECK, g as F_DOWN_RIGHT, h as getStateSymbol, r as renderSnapshotSummary, p as padSummaryTitle, i as getStateString$1, j as formatTime, k as countTestErrors, l as F_TREE_NODE_END, m as F_TREE_NODE_MIDDLE, n as noun, o as F_RIGHT } from './utils.DKODp04v.js';
40
41
  import { a as isTTY, b as isWindows, i as isForceColor } from './env.BKKtU2WC.js';
41
42
  import { isatty } from 'node:tty';
42
- import crypto, { createHash } from 'node:crypto';
43
43
  import EventEmitter$1, { EventEmitter } from 'node:events';
44
44
  import { c as createBirpc } from './index.Chj8NDwU.js';
45
45
  import { i as isBuiltin, t as toBuiltin } from './modules.BJuCwlRJ.js';
@@ -47,7 +47,7 @@ import { fork } from 'node:child_process';
47
47
  import { Worker } from 'node:worker_threads';
48
48
  import require$$0$3 from 'events';
49
49
  import require$$1$1 from 'https';
50
- import require$$2 from 'http';
50
+ import require$$2$1 from 'http';
51
51
  import require$$3 from 'net';
52
52
  import require$$4 from 'tls';
53
53
  import require$$1 from 'crypto';
@@ -55,6 +55,7 @@ import require$$0$2 from 'stream';
55
55
  import require$$7 from 'url';
56
56
  import require$$0 from 'zlib';
57
57
  import require$$0$1 from 'buffer';
58
+ import require$$2 from 'util';
58
59
  import { A as API_PATH, c as configFiles, d as defaultBrowserPort, a as defaultPort } from './constants.-juJ8b_4.js';
59
60
  import { isAgent } from 'std-env';
60
61
  import MagicString from 'magic-string';
@@ -2117,6 +2118,7 @@ class BaseCoverageProvider {
2117
2118
  coverageFiles = /* @__PURE__ */ new Map();
2118
2119
  pendingPromises = [];
2119
2120
  coverageFilesDirectory;
2121
+ reportsDirectoryLock;
2120
2122
  roots = [];
2121
2123
  changedFiles;
2122
2124
  _initialize(ctx) {
@@ -2142,6 +2144,7 @@ Update your dependencies and make sure the versions match.`));
2142
2144
  const shard = this.ctx.config.shard;
2143
2145
  const tempDirectory = `.tmp${shard ? `-${shard.index}-${shard.count}` : ""}`;
2144
2146
  this.coverageFilesDirectory = resolve$1(this.options.reportsDirectory, tempDirectory);
2147
+ this.reportsDirectoryLock = new ReportsDirectoryLock(resolve$1(this.options.reportsDirectory));
2145
2148
  // If --project filter is set pick only roots of resolved projects
2146
2149
  this.roots = ctx.config.project?.length ? [...new Set(ctx.projects.map((project) => project.config.root))] : [ctx.config.root];
2147
2150
  }
@@ -2205,6 +2208,7 @@ Update your dependencies and make sure the versions match.`));
2205
2208
  return this.options;
2206
2209
  }
2207
2210
  async clean(clean = true) {
2211
+ await this.reportsDirectoryLock.acquire();
2208
2212
  if (clean && existsSync(this.options.reportsDirectory)) await promises.rm(this.options.reportsDirectory, {
2209
2213
  recursive: true,
2210
2214
  force: true,
@@ -2264,10 +2268,14 @@ Update your dependencies and make sure the versions match.`));
2264
2268
  }
2265
2269
  }
2266
2270
  async cleanAfterRun() {
2267
- this.coverageFiles = /* @__PURE__ */ new Map();
2268
- await promises.rm(this.coverageFilesDirectory, { recursive: true });
2269
- // Remove empty reports directory, e.g. when only text-reporter is used
2270
- if (readdirSync(this.options.reportsDirectory).length === 0) await promises.rm(this.options.reportsDirectory, { recursive: true });
2271
+ try {
2272
+ this.coverageFiles = /* @__PURE__ */ new Map();
2273
+ await promises.rm(this.coverageFilesDirectory, { recursive: true });
2274
+ // Remove empty reports directory, e.g. when only text-reporter is used
2275
+ if (readdirSync(this.options.reportsDirectory).length === 0) await promises.rm(this.options.reportsDirectory, { recursive: true });
2276
+ } finally {
2277
+ await this.reportsDirectoryLock.release();
2278
+ }
2271
2279
  }
2272
2280
  async onTestRunStart() {
2273
2281
  if (this.options.changed) try {
@@ -2615,6 +2623,65 @@ function resolveMergeConfig(mod) {
2615
2623
  if (config) return config;
2616
2624
  }
2617
2625
  }
2626
+ class ReportsDirectoryLock {
2627
+ lockFile;
2628
+ constructor(reportsDirectory) {
2629
+ this.reportsDirectory = reportsDirectory;
2630
+ const hash = createHash("sha256").update(reportsDirectory).digest("hex").slice(0, 16);
2631
+ this.lockFile = resolve$1(tmpdir(), `vitest-coverage-${hash}.lock`);
2632
+ }
2633
+ async acquire() {
2634
+ if (await this.tryWrite()) return;
2635
+ const owner = await this.readOwner();
2636
+ // We already hold the lock for this directory (e.g. watch-mode reruns).
2637
+ if (owner?.pid === process.pid) return;
2638
+ // Another running Vitest owns this directory.
2639
+ if (owner && isProcessAlive(owner.pid)) throw this.inUseError(owner);
2640
+ // The lock was left behind by a process that no longer exists. Reclaim it.
2641
+ await promises.rm(this.lockFile, { force: true });
2642
+ if (!await this.tryWrite()) throw this.inUseError(await this.readOwner());
2643
+ }
2644
+ async release() {
2645
+ if ((await this.readOwner())?.pid === process.pid) await promises.rm(this.lockFile, { force: true });
2646
+ }
2647
+ async tryWrite() {
2648
+ const payload = JSON.stringify({
2649
+ pid: process.pid,
2650
+ reportsDirectory: this.reportsDirectory
2651
+ });
2652
+ try {
2653
+ // `wx` fails with EEXIST if the file already exists, so only one process wins.
2654
+ await promises.writeFile(this.lockFile, payload, { flag: "wx" });
2655
+ return true;
2656
+ } catch (error) {
2657
+ if (error.code === "EEXIST") return false;
2658
+ throw error;
2659
+ }
2660
+ }
2661
+ async readOwner() {
2662
+ try {
2663
+ const owner = JSON.parse(await promises.readFile(this.lockFile, "utf-8"));
2664
+ return typeof owner?.pid === "number" ? owner : null;
2665
+ } catch {
2666
+ return null;
2667
+ }
2668
+ }
2669
+ inUseError(owner) {
2670
+ return /* @__PURE__ */ new Error(`The coverage report directory "${this.reportsDirectory}" is already in use by another Vitest process${owner ? ` (pid ${owner.pid})` : ""}. Running coverage for multiple Vitest processes in the same directory at the same time is not supported, because they would delete each other's reports.\nGive each run its own "coverage.reportsDirectory" (e.g. --coverage.reportsDirectory=coverage-${process.pid}) or run them sequentially.`);
2671
+ }
2672
+ }
2673
+ function isProcessAlive(pid) {
2674
+ try {
2675
+ // Sending signal 0 checks if the process exists without actually killing it:
2676
+ // https://nodejs.org/api/process.html#processkillpid-signal
2677
+ process.kill(pid, 0);
2678
+ return true;
2679
+ } catch (error) {
2680
+ // ESRCH means the process is gone. Treat anything else (e.g. EPERM) as alive
2681
+ // so we never reclaim a lock from a process that is still running.
2682
+ return error.code !== "ESRCH";
2683
+ }
2684
+ }
2618
2685
 
2619
2686
  // this is copy pasted from vite
2620
2687
  function normalizeResolvedIdToUrl(environment, resolvedId) {
@@ -4297,13 +4364,14 @@ class Logger {
4297
4364
  const color = this.ctx.config.watch ? "blue" : "cyan";
4298
4365
  const mode = this.ctx.config.watch ? "DEV" : "RUN";
4299
4366
  this.log(withLabel(color, mode, `v${this.ctx.version} `) + c.gray(this.ctx.config.root));
4300
- // Log seed if either files (RandomSequencer) or tests are shuffled
4301
- if (this.ctx.config.sequence.sequencer === RandomSequencer || this.ctx.config.sequence.shuffle) this.log(PAD + c.gray(`Running tests with seed "${this.ctx.config.sequence.seed}"`));
4367
+ const seed = this.ctx.getSeed();
4368
+ if (seed != null) this.log(PAD + c.gray(`Running tests with seed "${seed}"`));
4302
4369
  if (this.ctx.config.ui) {
4303
4370
  const host = this.ctx.config.api?.host || "localhost";
4304
4371
  const port = this.ctx.vite.config.server.port;
4305
- const base = this.ctx.config.uiBase;
4306
- this.log(PAD + c.dim(c.green(`UI started at http://${host}:${c.bold(port)}${base}`)));
4372
+ const url = new URL(this.ctx.config.uiBase, `http://${host}:${port}`);
4373
+ url.searchParams.set("token", this.ctx.config.api.token);
4374
+ this.log(PAD + c.dim(c.green(`UI started at ${url}`)));
4307
4375
  } else if (this.ctx.config.api?.port) {
4308
4376
  const resolvedUrls = this.ctx.vite.resolvedUrls;
4309
4377
  // workaround for https://github.com/vitejs/vite/issues/15438, it was fixed in vite 5.1
@@ -4632,7 +4700,7 @@ class VitestPackageInstaller {
4632
4700
  if (/* @__PURE__ */ isPackageExists(dependency, { paths: [root, __dirname$1] })) return true;
4633
4701
  process.stderr.write(withLabel("red", "MISSING DEPENDENCY", `Cannot find dependency '${dependency}'\n\n`));
4634
4702
  if (!isTTY) return false;
4635
- const { install } = await (await import('./index.6dZyLbho.js').then(function (n) { return n.i; })).default({
4703
+ const { install } = await (await import('./index.Djaij2xr.js').then(function (n) { return n.i; })).default({
4636
4704
  type: "confirm",
4637
4705
  name: "install",
4638
4706
  message: c.reset(`Do you want to install ${c.green(dependency)}?`)
@@ -4744,7 +4812,7 @@ function createBrowserPool(vitest) {
4744
4812
  vitest.onCancel(() => {
4745
4813
  isCancelled = true;
4746
4814
  });
4747
- const initialisedPools = await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
4815
+ const initialisedPools = await Promise.all(Array.from(groupedFiles.entries(), async ([project, files]) => {
4748
4816
  await project._initBrowserProvider();
4749
4817
  if (!project.browser) throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ""}. This is a bug in Vitest. Please, open a new issue with reproduction.`);
4750
4818
  if (isCancelled) return;
@@ -4783,7 +4851,7 @@ function createBrowserPool(vitest) {
4783
4851
  return {
4784
4852
  name: "browser",
4785
4853
  async close() {
4786
- await Promise.all([...providers].map((provider) => provider.close()));
4854
+ await Promise.all(Array.from(providers, (provider) => provider.close()));
4787
4855
  vitest._browserSessions.sessionIds.clear();
4788
4856
  providers.clear();
4789
4857
  vitest.projects.forEach((project) => {
@@ -5126,6 +5194,7 @@ class PoolRunner {
5126
5194
  poolId = void 0;
5127
5195
  project;
5128
5196
  environment;
5197
+ _lastTestFiles;
5129
5198
  _state = RunnerState.IDLE;
5130
5199
  _operationLock = null;
5131
5200
  _terminatePromise = createDefer();
@@ -5148,6 +5217,7 @@ class PoolRunner {
5148
5217
  this.project = options.project;
5149
5218
  this.environment = options.environment;
5150
5219
  const vitest = this.project.vitest;
5220
+ this._lastTestFiles = [];
5151
5221
  this._traces = vitest._traces;
5152
5222
  if (this._traces.isEnabled()) {
5153
5223
  const { span: workerSpan, context } = this._traces.startContextSpan("vitest.worker");
@@ -5201,7 +5271,8 @@ class PoolRunner {
5201
5271
  return span;
5202
5272
  }
5203
5273
  request(method, context) {
5204
- this._otel?.files.push(...context.files.map((f) => f.filepath));
5274
+ this._lastTestFiles = context.files.map((f) => f.filepath);
5275
+ this._otel?.files.push(...this._lastTestFiles);
5205
5276
  return this.postMessage({
5206
5277
  __vitest_worker_request__: true,
5207
5278
  type: method,
@@ -5321,6 +5392,7 @@ class PoolRunner {
5321
5392
  this._state = RunnerState.STOPPED;
5322
5393
  throw error;
5323
5394
  } finally {
5395
+ this._lastTestFiles = [];
5324
5396
  this._operationLock.resolve();
5325
5397
  this._operationLock = null;
5326
5398
  this._otel?.span.end();
@@ -5346,21 +5418,36 @@ class PoolRunner {
5346
5418
  this._eventEmitter.emit("error", error);
5347
5419
  }
5348
5420
  };
5349
- emitUnexpectedExit = () => {
5350
- const error = /* @__PURE__ */ new Error(`Worker exited unexpectedly during ${this._state} state`);
5421
+ emitUnexpectedExit = (code, signal) => {
5422
+ const hasCode = typeof code === "number";
5423
+ const errorDetails = hasCode || signal ? `with ${hasCode ? `exit code ${code} ` : ""}${signal ? `signal ${signal} ` : ""}` : "";
5424
+ const testFileDetails = this._lastTestFiles.length ? ` while running test file${this._lastTestFiles.length === 1 ? "" : "s"} ${this._lastTestFiles.join(", ")}` : "";
5425
+ const error = /* @__PURE__ */ new Error(`Worker exited unexpectedly ${errorDetails}during ${this._state} state${testFileDetails}`);
5351
5426
  this._state = RunnerState.STOPPED;
5352
5427
  this._eventEmitter.emit("error", error);
5353
5428
  };
5354
5429
  waitForStart() {
5355
5430
  return new Promise((resolve, reject) => {
5356
- const onStart = (message) => {
5431
+ const cleanup = () => {
5432
+ this.off("message", onStart);
5433
+ this.off("error", onError);
5434
+ };
5435
+ function onStart(message) {
5357
5436
  if (message.type === "started") {
5358
- this.off("message", onStart);
5437
+ cleanup();
5359
5438
  if (message.error) reject(message.error);
5360
5439
  else resolve();
5361
5440
  }
5362
- };
5441
+ }
5442
+ // The worker can die before it ever reports back (e.g. an invalid
5443
+ // `execArgv` makes Node exit immediately). Reject as soon as that
5444
+ // happens instead of waiting for the start timeout to elapse.
5445
+ function onError(error) {
5446
+ cleanup();
5447
+ reject(error);
5448
+ }
5363
5449
  this.on("message", onStart);
5450
+ this.on("error", onError);
5364
5451
  });
5365
5452
  }
5366
5453
  withTimeout(promise, timeout) {
@@ -5924,7 +6011,7 @@ function deepEqual$1(obj1, obj2) {
5924
6011
  const keys1 = Object.keys(obj1);
5925
6012
  const keys2 = Object.keys(obj2);
5926
6013
  if (keys1.length !== keys2.length) return false;
5927
- for (const key of keys1) if (!Object.prototype.hasOwnProperty.call(obj2, key) || !deepEqual$1(obj1[key], obj2[key])) return false;
6014
+ for (const key of keys1) if (!Object.hasOwn(obj2, key) || !deepEqual$1(obj1[key], obj2[key])) return false;
5928
6015
  return true;
5929
6016
  }
5930
6017
 
@@ -6450,6 +6537,9 @@ function requirePermessageDeflate () {
6450
6537
  * acknowledge disabling of client context takeover
6451
6538
  * @param {Number} [options.concurrencyLimit=10] The number of concurrent
6452
6539
  * calls to zlib
6540
+ * @param {Boolean} [options.isServer=false] Create the instance in either
6541
+ * server or client mode
6542
+ * @param {Number} [options.maxPayload=0] The maximum allowed message length
6453
6543
  * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
6454
6544
  * use of a custom server window size
6455
6545
  * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
@@ -6460,16 +6550,13 @@ function requirePermessageDeflate () {
6460
6550
  * deflate
6461
6551
  * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
6462
6552
  * inflate
6463
- * @param {Boolean} [isServer=false] Create the instance in either server or
6464
- * client mode
6465
- * @param {Number} [maxPayload=0] The maximum allowed message length
6466
6553
  */
6467
- constructor(options, isServer, maxPayload) {
6468
- this._maxPayload = maxPayload | 0;
6554
+ constructor(options) {
6469
6555
  this._options = options || {};
6470
6556
  this._threshold =
6471
6557
  this._options.threshold !== undefined ? this._options.threshold : 1024;
6472
- this._isServer = !!isServer;
6558
+ this._maxPayload = this._options.maxPayload | 0;
6559
+ this._isServer = !!this._options.isServer;
6473
6560
  this._deflate = null;
6474
6561
  this._inflate = null;
6475
6562
 
@@ -7150,6 +7237,10 @@ function requireReceiver () {
7150
7237
  * extensions
7151
7238
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
7152
7239
  * client or server mode
7240
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
7241
+ * buffered data chunks
7242
+ * @param {Number} [options.maxFragments=0] The maximum number of message
7243
+ * fragments
7153
7244
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
7154
7245
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
7155
7246
  * not to skip UTF-8 validation for text and close messages
@@ -7164,6 +7255,8 @@ function requireReceiver () {
7164
7255
  this._binaryType = options.binaryType || BINARY_TYPES[0];
7165
7256
  this._extensions = options.extensions || {};
7166
7257
  this._isServer = !!options.isServer;
7258
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
7259
+ this._maxFragments = options.maxFragments | 0;
7167
7260
  this._maxPayload = options.maxPayload | 0;
7168
7261
  this._skipUTF8Validation = !!options.skipUTF8Validation;
7169
7262
  this[kWebSocket] = undefined;
@@ -7199,6 +7292,22 @@ function requireReceiver () {
7199
7292
  _write(chunk, encoding, cb) {
7200
7293
  if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
7201
7294
 
7295
+ if (
7296
+ this._maxBufferedChunks > 0 &&
7297
+ this._buffers.length >= this._maxBufferedChunks
7298
+ ) {
7299
+ cb(
7300
+ this.createError(
7301
+ RangeError,
7302
+ 'Too many buffered chunks',
7303
+ false,
7304
+ 1008,
7305
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
7306
+ )
7307
+ );
7308
+ return;
7309
+ }
7310
+
7202
7311
  this._bufferedBytes += chunk.length;
7203
7312
  this._buffers.push(chunk);
7204
7313
  this.startLoop(cb);
@@ -7595,6 +7704,22 @@ function requireReceiver () {
7595
7704
  }
7596
7705
 
7597
7706
  if (data.length) {
7707
+ if (
7708
+ this._maxFragments > 0 &&
7709
+ this._fragments.length >= this._maxFragments
7710
+ ) {
7711
+ const error = this.createError(
7712
+ RangeError,
7713
+ 'Too many message fragments',
7714
+ false,
7715
+ 1008,
7716
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
7717
+ );
7718
+
7719
+ cb(error);
7720
+ return;
7721
+ }
7722
+
7598
7723
  //
7599
7724
  // This message is not compressed so its length is the sum of the payload
7600
7725
  // length of all fragments.
@@ -7634,6 +7759,22 @@ function requireReceiver () {
7634
7759
  return;
7635
7760
  }
7636
7761
 
7762
+ if (
7763
+ this._maxFragments > 0 &&
7764
+ this._fragments.length >= this._maxFragments
7765
+ ) {
7766
+ const error = this.createError(
7767
+ RangeError,
7768
+ 'Too many message fragments',
7769
+ false,
7770
+ 1008,
7771
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
7772
+ );
7773
+
7774
+ cb(error);
7775
+ return;
7776
+ }
7777
+
7637
7778
  this._fragments.push(buf);
7638
7779
  }
7639
7780
 
@@ -7828,6 +7969,9 @@ function requireSender () {
7828
7969
 
7829
7970
  const { Duplex } = require$$0$2;
7830
7971
  const { randomFillSync } = require$$1;
7972
+ const {
7973
+ types: { isUint8Array }
7974
+ } = require$$2;
7831
7975
 
7832
7976
  const PerMessageDeflate = requirePermessageDeflate();
7833
7977
  const { EMPTY_BUFFER, kWebSocket, NOOP } = requireConstants();
@@ -8024,8 +8168,10 @@ function requireSender () {
8024
8168
 
8025
8169
  if (typeof data === 'string') {
8026
8170
  buf.write(data, 2);
8027
- } else {
8171
+ } else if (isUint8Array(data)) {
8028
8172
  buf.set(data, 2);
8173
+ } else {
8174
+ throw new TypeError('Second argument must be a string or a Uint8Array');
8029
8175
  }
8030
8176
  }
8031
8177
 
@@ -8949,7 +9095,7 @@ function requireWebsocket () {
8949
9095
 
8950
9096
  const EventEmitter = require$$0$3;
8951
9097
  const https = require$$1$1;
8952
- const http = require$$2;
9098
+ const http = require$$2$1;
8953
9099
  const net = require$$3;
8954
9100
  const tls = require$$4;
8955
9101
  const { randomBytes, createHash } = require$$1;
@@ -9146,6 +9292,10 @@ function requireWebsocket () {
9146
9292
  * multiple times in the same tick
9147
9293
  * @param {Function} [options.generateMask] The function used to generate the
9148
9294
  * masking key
9295
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
9296
+ * buffered data chunks
9297
+ * @param {Number} [options.maxFragments=0] The maximum number of message
9298
+ * fragments
9149
9299
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
9150
9300
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
9151
9301
  * not to skip UTF-8 validation for text and close messages
@@ -9157,6 +9307,8 @@ function requireWebsocket () {
9157
9307
  binaryType: this.binaryType,
9158
9308
  extensions: this._extensions,
9159
9309
  isServer: this._isServer,
9310
+ maxBufferedChunks: options.maxBufferedChunks,
9311
+ maxFragments: options.maxFragments,
9160
9312
  maxPayload: options.maxPayload,
9161
9313
  skipUTF8Validation: options.skipUTF8Validation
9162
9314
  });
@@ -9585,6 +9737,10 @@ function requireWebsocket () {
9585
9737
  * masking key
9586
9738
  * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
9587
9739
  * handshake request
9740
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
9741
+ * buffered data chunks
9742
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
9743
+ * fragments
9588
9744
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
9589
9745
  * size
9590
9746
  * @param {Number} [options.maxRedirects=10] The maximum number of redirects
@@ -9605,6 +9761,8 @@ function requireWebsocket () {
9605
9761
  autoPong: true,
9606
9762
  closeTimeout: CLOSE_TIMEOUT,
9607
9763
  protocolVersion: protocolVersions[1],
9764
+ maxBufferedChunks: 1024 * 1024,
9765
+ maxFragments: 128 * 1024,
9608
9766
  maxPayload: 100 * 1024 * 1024,
9609
9767
  skipUTF8Validation: false,
9610
9768
  perMessageDeflate: true,
@@ -9638,7 +9796,7 @@ function requireWebsocket () {
9638
9796
  } else {
9639
9797
  try {
9640
9798
  parsedUrl = new URL(address);
9641
- } catch (e) {
9799
+ } catch {
9642
9800
  throw new SyntaxError(`Invalid URL: ${address}`);
9643
9801
  }
9644
9802
  }
@@ -9700,11 +9858,11 @@ function requireWebsocket () {
9700
9858
  opts.timeout = opts.handshakeTimeout;
9701
9859
 
9702
9860
  if (opts.perMessageDeflate) {
9703
- perMessageDeflate = new PerMessageDeflate(
9704
- opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
9705
- false,
9706
- opts.maxPayload
9707
- );
9861
+ perMessageDeflate = new PerMessageDeflate({
9862
+ ...opts.perMessageDeflate,
9863
+ isServer: false,
9864
+ maxPayload: opts.maxPayload
9865
+ });
9708
9866
  opts.headers['Sec-WebSocket-Extensions'] = format({
9709
9867
  [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
9710
9868
  });
@@ -9962,6 +10120,8 @@ function requireWebsocket () {
9962
10120
  websocket.setSocket(socket, head, {
9963
10121
  allowSynchronousEvents: opts.allowSynchronousEvents,
9964
10122
  generateMask: opts.generateMask,
10123
+ maxBufferedChunks: opts.maxBufferedChunks,
10124
+ maxFragments: opts.maxFragments,
9965
10125
  maxPayload: opts.maxPayload,
9966
10126
  skipUTF8Validation: opts.skipUTF8Validation
9967
10127
  });
@@ -10511,12 +10671,14 @@ function requireStream () {
10511
10671
 
10512
10672
  requireStream();
10513
10673
 
10674
+ requireExtension();
10675
+
10676
+ requirePermessageDeflate();
10677
+
10514
10678
  requireReceiver();
10515
10679
 
10516
10680
  requireSender();
10517
10681
 
10518
- requireWebsocket();
10519
-
10520
10682
  var subprotocol;
10521
10683
  var hasRequiredSubprotocol;
10522
10684
 
@@ -10587,6 +10749,10 @@ function requireSubprotocol () {
10587
10749
  return subprotocol;
10588
10750
  }
10589
10751
 
10752
+ requireSubprotocol();
10753
+
10754
+ requireWebsocket();
10755
+
10590
10756
  /* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */
10591
10757
 
10592
10758
  var websocketServer;
@@ -10597,7 +10763,7 @@ function requireWebsocketServer () {
10597
10763
  hasRequiredWebsocketServer = 1;
10598
10764
 
10599
10765
  const EventEmitter = require$$0$3;
10600
- const http = require$$2;
10766
+ const http = require$$2$1;
10601
10767
  const { Duplex } = require$$0$2;
10602
10768
  const { createHash } = require$$1;
10603
10769
 
@@ -10637,6 +10803,10 @@ function requireWebsocketServer () {
10637
10803
  * called
10638
10804
  * @param {Function} [options.handleProtocols] A hook to handle protocols
10639
10805
  * @param {String} [options.host] The hostname where to bind the server
10806
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
10807
+ * buffered data chunks
10808
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
10809
+ * fragments
10640
10810
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
10641
10811
  * size
10642
10812
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -10659,6 +10829,8 @@ function requireWebsocketServer () {
10659
10829
  options = {
10660
10830
  allowSynchronousEvents: true,
10661
10831
  autoPong: true,
10832
+ maxBufferedChunks: 1024 * 1024,
10833
+ maxFragments: 128 * 1024,
10662
10834
  maxPayload: 100 * 1024 * 1024,
10663
10835
  skipUTF8Validation: false,
10664
10836
  perMessageDeflate: false,
@@ -10887,11 +11059,11 @@ function requireWebsocketServer () {
10887
11059
  this.options.perMessageDeflate &&
10888
11060
  secWebSocketExtensions !== undefined
10889
11061
  ) {
10890
- const perMessageDeflate = new PerMessageDeflate(
10891
- this.options.perMessageDeflate,
10892
- true,
10893
- this.options.maxPayload
10894
- );
11062
+ const perMessageDeflate = new PerMessageDeflate({
11063
+ ...this.options.perMessageDeflate,
11064
+ isServer: true,
11065
+ maxPayload: this.options.maxPayload
11066
+ });
10895
11067
 
10896
11068
  try {
10897
11069
  const offers = extension.parse(secWebSocketExtensions);
@@ -11018,6 +11190,8 @@ function requireWebsocketServer () {
11018
11190
 
11019
11191
  ws.setSocket(socket, head, {
11020
11192
  allowSynchronousEvents: this.options.allowSynchronousEvents,
11193
+ maxBufferedChunks: this.options.maxBufferedChunks,
11194
+ maxFragments: this.options.maxFragments,
11021
11195
  maxPayload: this.options.maxPayload,
11022
11196
  skipUTF8Validation: this.options.skipUTF8Validation
11023
11197
  });
@@ -11629,11 +11803,11 @@ function serializeConfig(project) {
11629
11803
  expand: config.snapshotOptions.expand ?? globalConfig.snapshotOptions.expand
11630
11804
  },
11631
11805
  sequence: {
11632
- shuffle: globalConfig.sequence.shuffle,
11633
- concurrent: globalConfig.sequence.concurrent,
11806
+ shuffle: config.sequence.shuffle,
11807
+ concurrent: config.sequence.concurrent,
11634
11808
  seed: globalConfig.sequence.seed,
11635
- hooks: globalConfig.sequence.hooks,
11636
- setupFiles: globalConfig.sequence.setupFiles
11809
+ hooks: config.sequence.hooks,
11810
+ setupFiles: config.sequence.setupFiles
11637
11811
  },
11638
11812
  inspect: globalConfig.inspect,
11639
11813
  inspectBrk: globalConfig.inspectBrk,
@@ -12797,7 +12971,10 @@ function WorkspaceVitestPlugin(project, options) {
12797
12971
  ws: false,
12798
12972
  preTransformRequests: false,
12799
12973
  middlewareMode: true,
12800
- fs: { allow: resolveFsAllow(project.vitest.config.root, project.vitest.vite.config.configFile) }
12974
+ fs: {
12975
+ allow: resolveFsAllow(project.vitest.config.root, project.vitest.vite.config.configFile),
12976
+ deny: [API_TOKEN_FILE]
12977
+ }
12801
12978
  },
12802
12979
  environments: { ssr: { resolve: resolveOptions } },
12803
12980
  test: {}
@@ -12911,7 +13088,7 @@ const defaultInline = [
12911
13088
  ];
12912
13089
  const depsExternal = [/\/node_modules\/.*\.cjs\.js$/, /\/node_modules\/.*\.mjs$/];
12913
13090
  function guessCJSversion(id) {
12914
- if (id.match(ESM_EXT_RE)) {
13091
+ if (ESM_EXT_RE.test(id)) {
12915
13092
  for (const i of [
12916
13093
  id.replace(ESM_EXT_RE, ".mjs"),
12917
13094
  id.replace(ESM_EXT_RE, ".umd.js"),
@@ -12919,7 +13096,7 @@ function guessCJSversion(id) {
12919
13096
  id.replace(ESM_EXT_RE, ".js")
12920
13097
  ]) if (existsSync(i)) return i;
12921
13098
  }
12922
- if (id.match(ESM_FOLDER_RE)) {
13099
+ if (ESM_FOLDER_RE.test(id)) {
12923
13100
  for (const i of [
12924
13101
  id.replace(ESM_FOLDER_RE, "/umd/$1"),
12925
13102
  id.replace(ESM_FOLDER_RE, "/cjs/$1"),
@@ -14313,7 +14490,7 @@ class BaseReporter {
14313
14490
  getFullName(test, separator) {
14314
14491
  if (test === test.file) return test.name;
14315
14492
  let name = test.file.name;
14316
- if (test.location) name += c.dim(`:${test.location.line}:${test.location.column}`);
14493
+ if (test.location) name += c.dim(`:${test.location.line}`);
14317
14494
  name += separator;
14318
14495
  name += getTestName(test, separator);
14319
14496
  return name;
@@ -14744,7 +14921,7 @@ function deepEqual(a, b) {
14744
14921
  const keysA = Object.keys(a);
14745
14922
  const keysB = Object.keys(b);
14746
14923
  if (keysA.length !== keysB.length) return false;
14747
- for (const key of keysA) if (!Object.prototype.hasOwnProperty.call(b, key) || !deepEqual(a[key], b[key])) return false;
14924
+ for (const key of keysA) if (!Object.hasOwn(b, key) || !deepEqual(a[key], b[key])) return false;
14748
14925
  return true;
14749
14926
  }
14750
14927
  function sum(items, cb) {
@@ -15673,22 +15850,20 @@ class JsonReporter {
15673
15850
  testResults,
15674
15851
  coverageMap: this.coverageMap
15675
15852
  };
15676
- await this.writeReport(JSON.stringify(result));
15677
- }
15678
- /**
15679
- * Writes the report to an output file if specified in the config,
15680
- * or logs it to the console otherwise.
15681
- * @param report
15682
- */
15683
- async writeReport(report) {
15853
+ const resultString = JSON.stringify(result);
15684
15854
  const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, "json");
15685
15855
  if (outputFile) {
15686
15856
  const reportFile = resolve$1(this.ctx.config.root, outputFile);
15687
15857
  const outputDirectory = dirname(reportFile);
15688
15858
  if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
15689
- await promises.writeFile(reportFile, report, "utf-8");
15859
+ await promises.writeFile(reportFile, resultString, "utf-8");
15690
15860
  this.ctx.logger.log(`JSON report written to ${reportFile}`);
15691
- } else this.ctx.logger.log(report);
15861
+ } else if (this.options.stdout) this.ctx.logger.log(resultString);
15862
+ else {
15863
+ const report = this.ctx.createReport("json");
15864
+ await report.writeFile("output.json", resultString);
15865
+ this.ctx.logger.log(`JSON report written to ${resolve$1(report.root, "output.json")}`);
15866
+ }
15692
15867
  }
15693
15868
  }
15694
15869
 
@@ -15772,6 +15947,8 @@ class JUnitReporter {
15772
15947
  this.reportFile = resolve$1(this.ctx.config.root, outputFile);
15773
15948
  const outputDirectory = dirname(this.reportFile);
15774
15949
  if (!existsSync(outputDirectory)) await promises.mkdir(outputDirectory, { recursive: true });
15950
+ } else if (!this.options.stdout) this.reportFile = resolve$1(this.ctx.createReport("junit").root, "output.xml");
15951
+ if (this.reportFile) {
15775
15952
  this.fileFd = await promises.open(this.reportFile, "w+");
15776
15953
  this.baseLog = async (text) => {
15777
15954
  if (!this.fileFd) this.fileFd = await promises.open(this.reportFile, "w+");
@@ -16151,7 +16328,7 @@ class VerboseReporter extends DefaultReporter {
16151
16328
  if (this.ctx.config.hideSkippedTests && testResult.state === "skipped" && test.options.mode !== "todo") return;
16152
16329
  let title = ` ${this.getEntityPrefix(test)} `;
16153
16330
  title += test.module.task.name;
16154
- if (test.location) title += c.dim(`:${test.location.line}:${test.location.column}`);
16331
+ if (test.location) title += c.dim(`:${test.location.line}`);
16155
16332
  title += separator;
16156
16333
  title += getTestName(test.task, separator);
16157
16334
  title += this.getTestCaseSuffix(test);
@@ -16215,11 +16392,11 @@ function parseFilter(filter) {
16215
16392
  const colonIndex = filter.lastIndexOf(":");
16216
16393
  if (colonIndex === -1) return { filename: filter };
16217
16394
  const [parsedFilename, lineNumber] = [filter.substring(0, colonIndex), filter.substring(colonIndex + 1)];
16218
- if (lineNumber.match(/^\d+$/)) return {
16395
+ if (/^\d+$/.test(lineNumber)) return {
16219
16396
  filename: parsedFilename,
16220
16397
  lineNumber: Number.parseInt(lineNumber)
16221
16398
  };
16222
- else if (lineNumber.match(/^\d+-\d+$/)) throw new RangeLocationFilterProvidedError(filter);
16399
+ else if (/^\d+-\d+$/.test(lineNumber)) throw new RangeLocationFilterProvidedError(filter);
16223
16400
  else return { filename: filter };
16224
16401
  }
16225
16402
  function groupFilters(filters) {
@@ -16647,7 +16824,7 @@ class TestSuite extends SuiteImplementation {
16647
16824
  */
16648
16825
  toTestSpecification() {
16649
16826
  const isTypecheck = this.task.meta.typecheck === true;
16650
- const testIds = [...this.children.allTests()].map((test) => test.id);
16827
+ const testIds = Array.from(this.children.allTests(), (test) => test.id);
16651
16828
  return this.project.createSpecification(this.module.moduleId, { testIds }, isTypecheck ? "typecheck" : void 0);
16652
16829
  }
16653
16830
  /**
@@ -18219,7 +18396,7 @@ class Vitest {
18219
18396
  * Returns the seed, if tests are running in a random order.
18220
18397
  */
18221
18398
  getSeed() {
18222
- return this.config.sequence.seed ?? null;
18399
+ return this.config.sequence.sequencer === RandomSequencer || !!this.config.sequence.shuffle || this.projects.some((p) => !!p.config.sequence.shuffle) ? this.config.sequence.seed : null;
18223
18400
  }
18224
18401
  /** @internal */
18225
18402
  async _reportFileTask(file) {
@@ -18582,7 +18759,7 @@ class Vitest {
18582
18759
  */
18583
18760
  async cancelCurrentRun(reason) {
18584
18761
  this.isCancelling = true;
18585
- this.cancelPromise = Promise.all([...this._onCancelListeners].map((listener) => listener(reason)));
18762
+ this.cancelPromise = Promise.all(Array.from(this._onCancelListeners, (listener) => listener(reason)));
18586
18763
  await this.cancelPromise.finally(() => this.cancelPromise = void 0);
18587
18764
  await this.runningPromise;
18588
18765
  }
@@ -18950,8 +19127,6 @@ async function VitestPlugin(options = {}, vitest = new Vitest(deepClone(options)
18950
19127
  const defines = deleteDefineConfig(viteConfig);
18951
19128
  options.defines = defines;
18952
19129
  options.viteDefine = originalDefine;
18953
- let open = false;
18954
- if (testConfig.ui && testConfig.open) open = testConfig.uiBase ?? "/__vitest__/";
18955
19130
  const resolveOptions = getDefaultResolveOptions();
18956
19131
  let config = {
18957
19132
  base: "/",
@@ -18963,11 +19138,14 @@ async function VitestPlugin(options = {}, vitest = new Vitest(deepClone(options)
18963
19138
  },
18964
19139
  server: {
18965
19140
  ...testConfig.api,
18966
- open,
19141
+ open: false,
18967
19142
  hmr: false,
18968
19143
  ws: testConfig.api?.middlewareMode ? false : void 0,
18969
19144
  preTransformRequests: false,
18970
- fs: { allow: resolveFsAllow(options.root || process.cwd(), testConfig.config) }
19145
+ fs: {
19146
+ allow: resolveFsAllow(options.root || process.cwd(), testConfig.config),
19147
+ deny: [API_TOKEN_FILE]
19148
+ }
18971
19149
  },
18972
19150
  build: {
18973
19151
  outDir: "dummy-non-existing-folder",
@@ -19093,7 +19271,33 @@ async function createVitest(modeOrOptions, optionsOrViteOverrides = {}, viteOver
19093
19271
  };
19094
19272
  try {
19095
19273
  const server = await createViteServer(mergeConfig(config, mergeConfig(viteOverrides, { root: options.root })));
19096
- if (ctx.config.api?.port) await server.listen();
19274
+ if (ctx.config.api?.port) {
19275
+ await server.listen();
19276
+ if (ctx.config.ui && ctx.config.open) {
19277
+ // Note: `tokenCreated` is only an approximation of "the browser is not
19278
+ // authenticated yet". If the user clears cookies while the token file
19279
+ // persists, the clean URL will block until they re-open the `?token=`
19280
+ // URL printed in the terminal.
19281
+ if (ctx.config.api.tokenCreated) {
19282
+ // First run that generated the token: no browser holds the auth
19283
+ // cookie yet, so open the authenticated URL to set it. A new tab
19284
+ // here is fine since no clean-URL tab exists to reuse.
19285
+ const url = new URL(ctx.config.uiBase, "http://localhost");
19286
+ url.searchParams.set("token", ctx.config.api.token);
19287
+ server.config.server.open = `${url.pathname}${url.search}`;
19288
+ } else
19289
+ // Subsequent runs: open the clean UI base URL (without `?token=`)
19290
+ // rather than the authenticated URL printed by the logger. On macOS,
19291
+ // `openBrowser` reuses an existing tab whose URL matches via substring
19292
+ // and reloads it (Vite's `bin/openChrome.js`). Since the 302 redirect
19293
+ // strips the token, an already-authenticated tab lives at the clean
19294
+ // URL, so opening the clean URL matches and reloads it; opening the
19295
+ // token URL would never match and would spawn a new tab on every
19296
+ // restart.
19297
+ server.config.server.open = ctx.config.uiBase;
19298
+ server.openBrowser();
19299
+ }
19300
+ }
19097
19301
  return ctx;
19098
19302
  }
19099
19303
  // Vitest can fail at any point inside "setServer" or inside a custom plugin