@vitest/browser 2.1.0-beta.5 → 2.1.0-beta.7

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.
package/dist/index.js CHANGED
@@ -1,20 +1,23 @@
1
- import { createDebugger, isFileServingAllowed, getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir, createServer } from 'vitest/node';
2
- import fs, { existsSync, readdirSync, readFileSync, promises, lstatSync } from 'node:fs';
1
+ import { createDebugger, isFileServingAllowed, getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir, createViteLogger, createViteServer } from 'vitest/node';
2
+ import c from 'tinyrainbow';
3
+ import fs, { existsSync, promises, readFileSync, lstatSync } from 'node:fs';
3
4
  import { WebSocketServer } from 'ws';
4
- import { builtinModules, createRequire } from 'node:module';
5
+ import { ServerMockResolver, dynamicImportPlugin } from '@vitest/mocker/node';
5
6
  import { readFile as readFile$1, mkdir } from 'node:fs/promises';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { createDefer, slash, toArray } from '@vitest/utils';
8
9
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
10
+ import { createRequire } from 'node:module';
9
11
  import sirv from 'sirv';
10
12
  import { defaultBrowserPort, coverageConfigDefaults } from 'vitest/config';
11
- import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-BdVqnfdE.js';
12
- import { resolve as resolve$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
13
13
  import MagicString from 'magic-string';
14
- import { esmWalker } from '@vitest/utils/ast';
14
+ import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-Cv9wga63.js';
15
+ import { resolve as resolve$1, dirname as dirname$1, basename as basename$1, normalize as normalize$1 } from 'node:path';
15
16
  import crypto from 'node:crypto';
16
17
  import * as nodeos from 'node:os';
17
18
 
19
+ var version = "2.1.0-beta.7";
20
+
18
21
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
19
22
  function normalizeWindowsPath(input = "") {
20
23
  if (!input) {
@@ -403,129 +406,8 @@ const stringify = (value, replacer, space) => {
403
406
  }
404
407
  };
405
408
 
406
- async function resolveMock(project, rawId, importer, hasFactory) {
407
- const { id, fsPath, external } = await resolveId(project, rawId, importer);
408
- if (hasFactory) {
409
- const needsInteropMap = viteDepsInteropMap(project.browser.vite.config);
410
- const needsInterop = needsInteropMap?.get(fsPath) ?? false;
411
- return { type: "factory", resolvedId: id, needsInterop };
412
- }
413
- const mockPath = resolveMockPath(project.config.root, fsPath, external);
414
- return {
415
- type: mockPath === null ? "automock" : "redirect",
416
- mockPath,
417
- resolvedId: id
418
- };
419
- }
420
- async function resolveId(project, rawId, importer) {
421
- const resolved = await project.browser.vite.pluginContainer.resolveId(
422
- rawId,
423
- importer,
424
- {
425
- ssr: false
426
- }
427
- );
428
- return resolveModule(project, rawId, resolved);
429
- }
430
- async function resolveModule(project, rawId, resolved) {
431
- const id = resolved?.id || rawId;
432
- const external = !isAbsolute(id) || isModuleDirectory(project.config, id) ? rawId : null;
433
- return {
434
- id,
435
- fsPath: cleanUrl(id),
436
- external
437
- };
438
- }
439
- function isModuleDirectory(config, path) {
440
- const moduleDirectories = config.server.deps?.moduleDirectories || [
441
- "/node_modules/"
442
- ];
443
- return moduleDirectories.some((dir) => path.includes(dir));
444
- }
445
- function resolveMockPath(root, mockPath, external) {
446
- const path = external || mockPath;
447
- if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) {
448
- const mockDirname = dirname(path);
449
- const mockFolder = join(
450
- root,
451
- "__mocks__",
452
- mockDirname
453
- );
454
- if (!existsSync(mockFolder)) {
455
- return null;
456
- }
457
- const files = readdirSync(mockFolder);
458
- const baseOriginal = basename(path);
459
- for (const file of files) {
460
- const baseFile = basename(file, extname(file));
461
- if (baseFile === baseOriginal) {
462
- return resolve(mockFolder, file);
463
- }
464
- }
465
- return null;
466
- }
467
- const dir = dirname(path);
468
- const baseId = basename(path);
469
- const fullPath = resolve(dir, "__mocks__", baseId);
470
- return existsSync(fullPath) ? fullPath : null;
471
- }
472
- const prefixedBuiltins = /* @__PURE__ */ new Set(["node:test"]);
473
- const builtins = /* @__PURE__ */ new Set([
474
- ...builtinModules,
475
- "assert/strict",
476
- "diagnostics_channel",
477
- "dns/promises",
478
- "fs/promises",
479
- "path/posix",
480
- "path/win32",
481
- "readline/promises",
482
- "stream/consumers",
483
- "stream/promises",
484
- "stream/web",
485
- "timers/promises",
486
- "util/types",
487
- "wasi"
488
- ]);
489
- const NODE_BUILTIN_NAMESPACE = "node:";
490
- function isNodeBuiltin(id) {
491
- if (prefixedBuiltins.has(id)) {
492
- return true;
493
- }
494
- return builtins.has(
495
- id.startsWith(NODE_BUILTIN_NAMESPACE) ? id.slice(NODE_BUILTIN_NAMESPACE.length) : id
496
- );
497
- }
498
- const postfixRE = /[?#].*$/;
499
- function cleanUrl(url) {
500
- return url.replace(postfixRE, "");
501
- }
502
- const metadata = /* @__PURE__ */ new WeakMap();
503
- function viteDepsInteropMap(config) {
504
- if (metadata.has(config)) {
505
- return metadata.get(config);
506
- }
507
- const cacheDirPath = getDepsCacheDir(config);
508
- const metadataPath = resolve(cacheDirPath, "_metadata.json");
509
- if (!existsSync(metadataPath)) {
510
- return null;
511
- }
512
- const { optimized } = JSON.parse(readFileSync(metadataPath, "utf-8"));
513
- const needsInteropMap = /* @__PURE__ */ new Map();
514
- for (const name in optimized) {
515
- const dep = optimized[name];
516
- const file = resolve(cacheDirPath, dep.file);
517
- needsInteropMap.set(file, dep.needsInterop);
518
- }
519
- metadata.set(config, needsInteropMap);
520
- return needsInteropMap;
521
- }
522
- function getDepsCacheDir(config) {
523
- return resolve(config.cacheDir, "deps");
524
- }
525
-
526
409
  const debug$1 = createDebugger("vitest:browser:api");
527
410
  const BROWSER_API_PATH = "/__vitest_browser_api__";
528
- const VALID_ID_PREFIX = "/@id/";
529
411
  function setupBrowserRpc(server) {
530
412
  const project = server.project;
531
413
  const vite = server.vite;
@@ -563,6 +445,9 @@ function setupBrowserRpc(server) {
563
445
  }
564
446
  }
565
447
  function setupClient(sessionId, ws) {
448
+ const mockResolver = new ServerMockResolver(server.vite, {
449
+ moduleDirectories: project.config.server?.deps?.moduleDirectories
450
+ });
566
451
  const rpc = createBirpc(
567
452
  {
568
453
  async onUnhandledError(error, type) {
@@ -622,34 +507,7 @@ function setupBrowserRpc(server) {
622
507
  ctx.cancelCurrentRun(reason);
623
508
  },
624
509
  async resolveId(id, importer) {
625
- const resolved = await vite.pluginContainer.resolveId(
626
- id,
627
- importer,
628
- {
629
- ssr: false
630
- }
631
- );
632
- if (!resolved) {
633
- return null;
634
- }
635
- const isOptimized = resolved.id.startsWith(withTrailingSlash(vite.config.cacheDir));
636
- let url;
637
- const root = vite.config.root;
638
- if (resolved.id.startsWith(withTrailingSlash(root))) {
639
- url = resolved.id.slice(root.length);
640
- } else if (resolved.id !== "/@react-refresh" && isAbsolute(resolved.id) && existsSync(cleanUrl(resolved.id))) {
641
- url = join("/@fs/", resolved.id);
642
- } else {
643
- url = resolved.id;
644
- }
645
- if (url[0] !== "." && url[0] !== "/") {
646
- url = id.startsWith(VALID_ID_PREFIX) ? id : VALID_ID_PREFIX + id.replace("\0", "__x00__");
647
- }
648
- return {
649
- id: resolved.id,
650
- url,
651
- optimized: isOptimized
652
- };
510
+ return mockResolver.resolveId(id, importer);
653
511
  },
654
512
  debug(...args) {
655
513
  ctx.logger.console.debug(...args);
@@ -693,17 +551,11 @@ function setupBrowserRpc(server) {
693
551
  debug$1?.("[%s] Finishing browser tests for context", contextId);
694
552
  return server.state.getContext(contextId)?.resolve();
695
553
  },
696
- resolveMock(rawId, importer, hasFactory) {
697
- return resolveMock(project, rawId, importer, hasFactory);
554
+ resolveMock(rawId, importer, options) {
555
+ return mockResolver.resolveMock(rawId, importer, options);
698
556
  },
699
557
  invalidate(ids) {
700
- ids.forEach((id) => {
701
- const moduleGraph = server.vite.moduleGraph;
702
- const module = moduleGraph.getModuleById(id);
703
- if (module) {
704
- moduleGraph.invalidateModule(module, /* @__PURE__ */ new Set(), Date.now(), true);
705
- }
706
- });
558
+ return mockResolver.invalidate(ids);
707
559
  },
708
560
  // CDP
709
561
  async sendCdpEvent(contextId, event, payload) {
@@ -752,12 +604,6 @@ function stringifyReplace(key, value) {
752
604
  return value;
753
605
  }
754
606
  }
755
- function withTrailingSlash(path) {
756
- if (path[path.length - 1] !== "/") {
757
- return `${path}/`;
758
- }
759
- return path;
760
- }
761
607
 
762
608
  class BrowserServerState {
763
609
  orchestrators = /* @__PURE__ */ new Map();
@@ -1418,11 +1264,6 @@ var DOM_KEY_LOCATION;
1418
1264
  ];
1419
1265
 
1420
1266
  const keyboard = async (context, text, state) => {
1421
- function focusIframe() {
1422
- if (!document.activeElement || document.activeElement.ownerDocument !== document || document.activeElement === document.body) {
1423
- window.focus();
1424
- }
1425
- }
1426
1267
  if (context.provider instanceof PlaywrightBrowserProvider) {
1427
1268
  const frame = await context.frame();
1428
1269
  await frame.evaluate(focusIframe);
@@ -1436,12 +1277,6 @@ const keyboard = async (context, text, state) => {
1436
1277
  context.contextId,
1437
1278
  text,
1438
1279
  async () => {
1439
- function selectAll() {
1440
- const element = document.activeElement;
1441
- if (element && element.select) {
1442
- element.select();
1443
- }
1444
- }
1445
1280
  if (context.provider instanceof PlaywrightBrowserProvider) {
1446
1281
  const frame = await context.frame();
1447
1282
  await frame.evaluate(selectAll);
@@ -1457,7 +1292,7 @@ const keyboard = async (context, text, state) => {
1457
1292
  unreleased: Array.from(pressed)
1458
1293
  };
1459
1294
  };
1460
- async function keyboardImplementation(pressed, provider, contextId, text, selectAll, skipRelease) {
1295
+ async function keyboardImplementation(pressed, provider, contextId, text, selectAll2, skipRelease) {
1461
1296
  if (provider instanceof PlaywrightBrowserProvider) {
1462
1297
  const page = provider.getPage(contextId);
1463
1298
  const actions = parseKeyDef(defaultKeyMap, text);
@@ -1469,7 +1304,7 @@ async function keyboardImplementation(pressed, provider, contextId, text, select
1469
1304
  }
1470
1305
  if (!releasePrevious) {
1471
1306
  if (key === "selectall") {
1472
- await selectAll();
1307
+ await selectAll2();
1473
1308
  continue;
1474
1309
  }
1475
1310
  for (let i = 1; i <= repeat; i++) {
@@ -1507,7 +1342,7 @@ async function keyboardImplementation(pressed, provider, contextId, text, select
1507
1342
  if (key === "selectall") {
1508
1343
  await keyboard2.perform();
1509
1344
  keyboard2 = browser.action("key");
1510
- await selectAll();
1345
+ await selectAll2();
1511
1346
  continue;
1512
1347
  }
1513
1348
  for (let i = 1; i <= repeat; i++) {
@@ -1527,6 +1362,17 @@ async function keyboardImplementation(pressed, provider, contextId, text, select
1527
1362
  pressed
1528
1363
  };
1529
1364
  }
1365
+ function focusIframe() {
1366
+ if (!document.activeElement || document.activeElement.ownerDocument !== document || document.activeElement === document.body) {
1367
+ window.focus();
1368
+ }
1369
+ }
1370
+ function selectAll() {
1371
+ const element = document.activeElement;
1372
+ if (element && element.select) {
1373
+ element.select();
1374
+ }
1375
+ }
1530
1376
 
1531
1377
  const type = async (context, selector, text, options = {}) => {
1532
1378
  const { skipClick = false, skipAutoClose = false } = options;
@@ -1689,6 +1535,131 @@ const hover = async (context, selector, options = {}) => {
1689
1535
  }
1690
1536
  };
1691
1537
 
1538
+ const upload = async (context, selector, files) => {
1539
+ const testPath = context.testPath;
1540
+ if (!testPath) {
1541
+ throw new Error(`Cannot upload files outside of a test`);
1542
+ }
1543
+ const testDir = dirname(testPath);
1544
+ if (context.provider instanceof PlaywrightBrowserProvider) {
1545
+ const { iframe } = context;
1546
+ const playwrightFiles = files.map((file) => {
1547
+ if (typeof file === "string") {
1548
+ return resolve(testDir, file);
1549
+ }
1550
+ return {
1551
+ name: file.name,
1552
+ mimeType: file.mimeType,
1553
+ buffer: Buffer.from(file.base64, "base64")
1554
+ };
1555
+ });
1556
+ await iframe.locator(selector).setInputFiles(playwrightFiles);
1557
+ } else if (context.provider instanceof WebdriverBrowserProvider) {
1558
+ for (const file of files) {
1559
+ if (typeof file !== "string") {
1560
+ throw new TypeError(`The "${context.provider.name}" provider doesn't support uploading files objects. Provide a file path instead.`);
1561
+ }
1562
+ }
1563
+ const element = context.browser.$(selector);
1564
+ for (const file of files) {
1565
+ const filepath = resolve(testDir, file);
1566
+ const remoteFilePath = await context.browser.uploadFile(filepath);
1567
+ await element.addValue(remoteFilePath);
1568
+ }
1569
+ } else {
1570
+ throw new TypeError(`Provider "${context.provider.name}" does not support uploading files via userEvent.upload`);
1571
+ }
1572
+ };
1573
+
1574
+ const types = { "application/andrew-inset": ["ez"], "application/appinstaller": ["appinstaller"], "application/applixware": ["aw"], "application/appx": ["appx"], "application/appxbundle": ["appxbundle"], "application/atom+xml": ["atom"], "application/atomcat+xml": ["atomcat"], "application/atomdeleted+xml": ["atomdeleted"], "application/atomsvc+xml": ["atomsvc"], "application/atsc-dwd+xml": ["dwd"], "application/atsc-held+xml": ["held"], "application/atsc-rsat+xml": ["rsat"], "application/automationml-aml+xml": ["aml"], "application/automationml-amlx+zip": ["amlx"], "application/bdoc": ["bdoc"], "application/calendar+xml": ["xcs"], "application/ccxml+xml": ["ccxml"], "application/cdfx+xml": ["cdfx"], "application/cdmi-capability": ["cdmia"], "application/cdmi-container": ["cdmic"], "application/cdmi-domain": ["cdmid"], "application/cdmi-object": ["cdmio"], "application/cdmi-queue": ["cdmiq"], "application/cpl+xml": ["cpl"], "application/cu-seeme": ["cu"], "application/cwl": ["cwl"], "application/dash+xml": ["mpd"], "application/dash-patch+xml": ["mpp"], "application/davmount+xml": ["davmount"], "application/docbook+xml": ["dbk"], "application/dssc+der": ["dssc"], "application/dssc+xml": ["xdssc"], "application/ecmascript": ["ecma"], "application/emma+xml": ["emma"], "application/emotionml+xml": ["emotionml"], "application/epub+zip": ["epub"], "application/exi": ["exi"], "application/express": ["exp"], "application/fdf": ["fdf"], "application/fdt+xml": ["fdt"], "application/font-tdpfr": ["pfr"], "application/geo+json": ["geojson"], "application/gml+xml": ["gml"], "application/gpx+xml": ["gpx"], "application/gxf": ["gxf"], "application/gzip": ["gz"], "application/hjson": ["hjson"], "application/hyperstudio": ["stk"], "application/inkml+xml": ["ink", "inkml"], "application/ipfix": ["ipfix"], "application/its+xml": ["its"], "application/java-archive": ["jar", "war", "ear"], "application/java-serialized-object": ["ser"], "application/java-vm": ["class"], "application/javascript": ["*js"], "application/json": ["json", "map"], "application/json5": ["json5"], "application/jsonml+json": ["jsonml"], "application/ld+json": ["jsonld"], "application/lgr+xml": ["lgr"], "application/lost+xml": ["lostxml"], "application/mac-binhex40": ["hqx"], "application/mac-compactpro": ["cpt"], "application/mads+xml": ["mads"], "application/manifest+json": ["webmanifest"], "application/marc": ["mrc"], "application/marcxml+xml": ["mrcx"], "application/mathematica": ["ma", "nb", "mb"], "application/mathml+xml": ["mathml"], "application/mbox": ["mbox"], "application/media-policy-dataset+xml": ["mpf"], "application/mediaservercontrol+xml": ["mscml"], "application/metalink+xml": ["metalink"], "application/metalink4+xml": ["meta4"], "application/mets+xml": ["mets"], "application/mmt-aei+xml": ["maei"], "application/mmt-usd+xml": ["musd"], "application/mods+xml": ["mods"], "application/mp21": ["m21", "mp21"], "application/mp4": ["*mp4", "*mpg4", "mp4s", "m4p"], "application/msix": ["msix"], "application/msixbundle": ["msixbundle"], "application/msword": ["doc", "dot"], "application/mxf": ["mxf"], "application/n-quads": ["nq"], "application/n-triples": ["nt"], "application/node": ["cjs"], "application/octet-stream": ["bin", "dms", "lrf", "mar", "so", "dist", "distz", "pkg", "bpk", "dump", "elc", "deploy", "exe", "dll", "deb", "dmg", "iso", "img", "msi", "msp", "msm", "buffer"], "application/oda": ["oda"], "application/oebps-package+xml": ["opf"], "application/ogg": ["ogx"], "application/omdoc+xml": ["omdoc"], "application/onenote": ["onetoc", "onetoc2", "onetmp", "onepkg"], "application/oxps": ["oxps"], "application/p2p-overlay+xml": ["relo"], "application/patch-ops-error+xml": ["xer"], "application/pdf": ["pdf"], "application/pgp-encrypted": ["pgp"], "application/pgp-keys": ["asc"], "application/pgp-signature": ["sig", "*asc"], "application/pics-rules": ["prf"], "application/pkcs10": ["p10"], "application/pkcs7-mime": ["p7m", "p7c"], "application/pkcs7-signature": ["p7s"], "application/pkcs8": ["p8"], "application/pkix-attr-cert": ["ac"], "application/pkix-cert": ["cer"], "application/pkix-crl": ["crl"], "application/pkix-pkipath": ["pkipath"], "application/pkixcmp": ["pki"], "application/pls+xml": ["pls"], "application/postscript": ["ai", "eps", "ps"], "application/provenance+xml": ["provx"], "application/pskc+xml": ["pskcxml"], "application/raml+yaml": ["raml"], "application/rdf+xml": ["rdf", "owl"], "application/reginfo+xml": ["rif"], "application/relax-ng-compact-syntax": ["rnc"], "application/resource-lists+xml": ["rl"], "application/resource-lists-diff+xml": ["rld"], "application/rls-services+xml": ["rs"], "application/route-apd+xml": ["rapd"], "application/route-s-tsid+xml": ["sls"], "application/route-usd+xml": ["rusd"], "application/rpki-ghostbusters": ["gbr"], "application/rpki-manifest": ["mft"], "application/rpki-roa": ["roa"], "application/rsd+xml": ["rsd"], "application/rss+xml": ["rss"], "application/rtf": ["rtf"], "application/sbml+xml": ["sbml"], "application/scvp-cv-request": ["scq"], "application/scvp-cv-response": ["scs"], "application/scvp-vp-request": ["spq"], "application/scvp-vp-response": ["spp"], "application/sdp": ["sdp"], "application/senml+xml": ["senmlx"], "application/sensml+xml": ["sensmlx"], "application/set-payment-initiation": ["setpay"], "application/set-registration-initiation": ["setreg"], "application/shf+xml": ["shf"], "application/sieve": ["siv", "sieve"], "application/smil+xml": ["smi", "smil"], "application/sparql-query": ["rq"], "application/sparql-results+xml": ["srx"], "application/sql": ["sql"], "application/srgs": ["gram"], "application/srgs+xml": ["grxml"], "application/sru+xml": ["sru"], "application/ssdl+xml": ["ssdl"], "application/ssml+xml": ["ssml"], "application/swid+xml": ["swidtag"], "application/tei+xml": ["tei", "teicorpus"], "application/thraud+xml": ["tfi"], "application/timestamped-data": ["tsd"], "application/toml": ["toml"], "application/trig": ["trig"], "application/ttml+xml": ["ttml"], "application/ubjson": ["ubj"], "application/urc-ressheet+xml": ["rsheet"], "application/urc-targetdesc+xml": ["td"], "application/voicexml+xml": ["vxml"], "application/wasm": ["wasm"], "application/watcherinfo+xml": ["wif"], "application/widget": ["wgt"], "application/winhlp": ["hlp"], "application/wsdl+xml": ["wsdl"], "application/wspolicy+xml": ["wspolicy"], "application/xaml+xml": ["xaml"], "application/xcap-att+xml": ["xav"], "application/xcap-caps+xml": ["xca"], "application/xcap-diff+xml": ["xdf"], "application/xcap-el+xml": ["xel"], "application/xcap-ns+xml": ["xns"], "application/xenc+xml": ["xenc"], "application/xfdf": ["xfdf"], "application/xhtml+xml": ["xhtml", "xht"], "application/xliff+xml": ["xlf"], "application/xml": ["xml", "xsl", "xsd", "rng"], "application/xml-dtd": ["dtd"], "application/xop+xml": ["xop"], "application/xproc+xml": ["xpl"], "application/xslt+xml": ["*xsl", "xslt"], "application/xspf+xml": ["xspf"], "application/xv+xml": ["mxml", "xhvml", "xvml", "xvm"], "application/yang": ["yang"], "application/yin+xml": ["yin"], "application/zip": ["zip"], "audio/3gpp": ["*3gpp"], "audio/aac": ["adts", "aac"], "audio/adpcm": ["adp"], "audio/amr": ["amr"], "audio/basic": ["au", "snd"], "audio/midi": ["mid", "midi", "kar", "rmi"], "audio/mobile-xmf": ["mxmf"], "audio/mp3": ["*mp3"], "audio/mp4": ["m4a", "mp4a"], "audio/mpeg": ["mpga", "mp2", "mp2a", "mp3", "m2a", "m3a"], "audio/ogg": ["oga", "ogg", "spx", "opus"], "audio/s3m": ["s3m"], "audio/silk": ["sil"], "audio/wav": ["wav"], "audio/wave": ["*wav"], "audio/webm": ["weba"], "audio/xm": ["xm"], "font/collection": ["ttc"], "font/otf": ["otf"], "font/ttf": ["ttf"], "font/woff": ["woff"], "font/woff2": ["woff2"], "image/aces": ["exr"], "image/apng": ["apng"], "image/avci": ["avci"], "image/avcs": ["avcs"], "image/avif": ["avif"], "image/bmp": ["bmp", "dib"], "image/cgm": ["cgm"], "image/dicom-rle": ["drle"], "image/dpx": ["dpx"], "image/emf": ["emf"], "image/fits": ["fits"], "image/g3fax": ["g3"], "image/gif": ["gif"], "image/heic": ["heic"], "image/heic-sequence": ["heics"], "image/heif": ["heif"], "image/heif-sequence": ["heifs"], "image/hej2k": ["hej2"], "image/hsj2": ["hsj2"], "image/ief": ["ief"], "image/jls": ["jls"], "image/jp2": ["jp2", "jpg2"], "image/jpeg": ["jpeg", "jpg", "jpe"], "image/jph": ["jph"], "image/jphc": ["jhc"], "image/jpm": ["jpm", "jpgm"], "image/jpx": ["jpx", "jpf"], "image/jxr": ["jxr"], "image/jxra": ["jxra"], "image/jxrs": ["jxrs"], "image/jxs": ["jxs"], "image/jxsc": ["jxsc"], "image/jxsi": ["jxsi"], "image/jxss": ["jxss"], "image/ktx": ["ktx"], "image/ktx2": ["ktx2"], "image/png": ["png"], "image/sgi": ["sgi"], "image/svg+xml": ["svg", "svgz"], "image/t38": ["t38"], "image/tiff": ["tif", "tiff"], "image/tiff-fx": ["tfx"], "image/webp": ["webp"], "image/wmf": ["wmf"], "message/disposition-notification": ["disposition-notification"], "message/global": ["u8msg"], "message/global-delivery-status": ["u8dsn"], "message/global-disposition-notification": ["u8mdn"], "message/global-headers": ["u8hdr"], "message/rfc822": ["eml", "mime"], "model/3mf": ["3mf"], "model/gltf+json": ["gltf"], "model/gltf-binary": ["glb"], "model/iges": ["igs", "iges"], "model/jt": ["jt"], "model/mesh": ["msh", "mesh", "silo"], "model/mtl": ["mtl"], "model/obj": ["obj"], "model/prc": ["prc"], "model/step+xml": ["stpx"], "model/step+zip": ["stpz"], "model/step-xml+zip": ["stpxz"], "model/stl": ["stl"], "model/u3d": ["u3d"], "model/vrml": ["wrl", "vrml"], "model/x3d+binary": ["*x3db", "x3dbz"], "model/x3d+fastinfoset": ["x3db"], "model/x3d+vrml": ["*x3dv", "x3dvz"], "model/x3d+xml": ["x3d", "x3dz"], "model/x3d-vrml": ["x3dv"], "text/cache-manifest": ["appcache", "manifest"], "text/calendar": ["ics", "ifb"], "text/coffeescript": ["coffee", "litcoffee"], "text/css": ["css"], "text/csv": ["csv"], "text/html": ["html", "htm", "shtml"], "text/jade": ["jade"], "text/javascript": ["js", "mjs"], "text/jsx": ["jsx"], "text/less": ["less"], "text/markdown": ["md", "markdown"], "text/mathml": ["mml"], "text/mdx": ["mdx"], "text/n3": ["n3"], "text/plain": ["txt", "text", "conf", "def", "list", "log", "in", "ini"], "text/richtext": ["rtx"], "text/rtf": ["*rtf"], "text/sgml": ["sgml", "sgm"], "text/shex": ["shex"], "text/slim": ["slim", "slm"], "text/spdx": ["spdx"], "text/stylus": ["stylus", "styl"], "text/tab-separated-values": ["tsv"], "text/troff": ["t", "tr", "roff", "man", "me", "ms"], "text/turtle": ["ttl"], "text/uri-list": ["uri", "uris", "urls"], "text/vcard": ["vcard"], "text/vtt": ["vtt"], "text/wgsl": ["wgsl"], "text/xml": ["*xml"], "text/yaml": ["yaml", "yml"], "video/3gpp": ["3gp", "3gpp"], "video/3gpp2": ["3g2"], "video/h261": ["h261"], "video/h263": ["h263"], "video/h264": ["h264"], "video/iso.segment": ["m4s"], "video/jpeg": ["jpgv"], "video/jpm": ["*jpm", "*jpgm"], "video/mj2": ["mj2", "mjp2"], "video/mp2t": ["ts"], "video/mp4": ["mp4", "mp4v", "mpg4"], "video/mpeg": ["mpeg", "mpg", "mpe", "m1v", "m2v"], "video/ogg": ["ogv"], "video/quicktime": ["qt", "mov"], "video/webm": ["webm"] };
1575
+ Object.freeze(types);
1576
+
1577
+ var __classPrivateFieldGet = (null && null.__classPrivateFieldGet) || function (receiver, state, kind, f) {
1578
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
1579
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
1580
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
1581
+ };
1582
+ var _Mime_extensionToType, _Mime_typeToExtension, _Mime_typeToExtensions;
1583
+ class Mime {
1584
+ constructor(...args) {
1585
+ _Mime_extensionToType.set(this, new Map());
1586
+ _Mime_typeToExtension.set(this, new Map());
1587
+ _Mime_typeToExtensions.set(this, new Map());
1588
+ for (const arg of args) {
1589
+ this.define(arg);
1590
+ }
1591
+ }
1592
+ define(typeMap, force = false) {
1593
+ for (let [type, extensions] of Object.entries(typeMap)) {
1594
+ type = type.toLowerCase();
1595
+ extensions = extensions.map((ext) => ext.toLowerCase());
1596
+ if (!__classPrivateFieldGet(this, _Mime_typeToExtensions, "f").has(type)) {
1597
+ __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").set(type, new Set());
1598
+ }
1599
+ const allExtensions = __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").get(type);
1600
+ let first = true;
1601
+ for (let extension of extensions) {
1602
+ const starred = extension.startsWith('*');
1603
+ extension = starred ? extension.slice(1) : extension;
1604
+ allExtensions?.add(extension);
1605
+ if (first) {
1606
+ __classPrivateFieldGet(this, _Mime_typeToExtension, "f").set(type, extension);
1607
+ }
1608
+ first = false;
1609
+ if (starred)
1610
+ continue;
1611
+ const currentType = __classPrivateFieldGet(this, _Mime_extensionToType, "f").get(extension);
1612
+ if (currentType && currentType != type && !force) {
1613
+ throw new Error(`"${type} -> ${extension}" conflicts with "${currentType} -> ${extension}". Pass \`force=true\` to override this definition.`);
1614
+ }
1615
+ __classPrivateFieldGet(this, _Mime_extensionToType, "f").set(extension, type);
1616
+ }
1617
+ }
1618
+ return this;
1619
+ }
1620
+ getType(path) {
1621
+ if (typeof path !== 'string')
1622
+ return null;
1623
+ const last = path.replace(/^.*[/\\]/, '').toLowerCase();
1624
+ const ext = last.replace(/^.*\./, '').toLowerCase();
1625
+ const hasPath = last.length < path.length;
1626
+ const hasDot = ext.length < last.length - 1;
1627
+ if (!hasDot && hasPath)
1628
+ return null;
1629
+ return __classPrivateFieldGet(this, _Mime_extensionToType, "f").get(ext) ?? null;
1630
+ }
1631
+ getExtension(type) {
1632
+ if (typeof type !== 'string')
1633
+ return null;
1634
+ type = type?.split?.(';')[0];
1635
+ return ((type && __classPrivateFieldGet(this, _Mime_typeToExtension, "f").get(type.trim().toLowerCase())) ?? null);
1636
+ }
1637
+ getAllExtensions(type) {
1638
+ if (typeof type !== 'string')
1639
+ return null;
1640
+ return __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").get(type.toLowerCase()) ?? null;
1641
+ }
1642
+ _freeze() {
1643
+ this.define = () => {
1644
+ throw new Error('define() not allowed for built-in Mime objects. See https://github.com/broofa/mime/blob/main/README.md#custom-mime-instances');
1645
+ };
1646
+ Object.freeze(this);
1647
+ for (const extensions of __classPrivateFieldGet(this, _Mime_typeToExtensions, "f").values()) {
1648
+ Object.freeze(extensions);
1649
+ }
1650
+ return this;
1651
+ }
1652
+ _getTestState() {
1653
+ return {
1654
+ types: __classPrivateFieldGet(this, _Mime_extensionToType, "f"),
1655
+ extensions: __classPrivateFieldGet(this, _Mime_typeToExtension, "f"),
1656
+ };
1657
+ }
1658
+ }
1659
+ _Mime_extensionToType = new WeakMap(), _Mime_typeToExtension = new WeakMap(), _Mime_typeToExtensions = new WeakMap();
1660
+
1661
+ var mime = new Mime(types)._freeze();
1662
+
1692
1663
  function assertFileAccess(path, project) {
1693
1664
  if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server)) {
1694
1665
  throw new Error(
@@ -1718,12 +1689,22 @@ const removeFile = async ({ project, testPath = process.cwd() }, path) => {
1718
1689
  assertFileAccess(filepath, project);
1719
1690
  await promises.rm(filepath);
1720
1691
  };
1692
+ const _fileInfo = async ({ project, testPath = process.cwd() }, path, encoding) => {
1693
+ const filepath = resolve$1(dirname$1(testPath), path);
1694
+ assertFileAccess(filepath, project);
1695
+ const content = await promises.readFile(filepath, encoding || "base64");
1696
+ return {
1697
+ content,
1698
+ basename: basename$1(filepath),
1699
+ mime: mime.getType(filepath)
1700
+ };
1701
+ };
1721
1702
 
1722
1703
  const screenshot = async (context, name, options = {}) => {
1723
1704
  if (!context.testPath) {
1724
1705
  throw new Error(`Cannot take a screenshot without a test path`);
1725
1706
  }
1726
- const path = options.path ? resolve(context.testPath, options.path) : resolveScreenshotPath(
1707
+ const path = options.path ? resolve(dirname(context.testPath), options.path) : resolveScreenshotPath(
1727
1708
  context.testPath,
1728
1709
  name,
1729
1710
  context.project.config
@@ -1786,6 +1767,8 @@ var builtinCommands = {
1786
1767
  readFile,
1787
1768
  removeFile,
1788
1769
  writeFile,
1770
+ __vitest_fileInfo: _fileInfo,
1771
+ __vitest_upload: upload,
1789
1772
  __vitest_click: click,
1790
1773
  __vitest_dblClick: dblClick,
1791
1774
  __vitest_tripleClick: tripleClick,
@@ -1869,15 +1852,32 @@ function getUserEvent(provider) {
1869
1852
  if (provider.name !== "preview") {
1870
1853
  return "__userEvent_CDP__";
1871
1854
  }
1872
- return `{
1855
+ return String.raw`{
1873
1856
  ..._userEventSetup,
1874
1857
  setup() {
1875
1858
  const userEvent = __vitest_user_event__.setup()
1876
1859
  userEvent.setup = this.setup
1877
1860
  userEvent.fill = this.fill.bind(userEvent)
1861
+ userEvent._upload = userEvent.upload.bind(userEvent)
1862
+ userEvent.upload = this.upload.bind(userEvent)
1878
1863
  userEvent.dragAndDrop = this.dragAndDrop
1879
1864
  return userEvent
1880
1865
  },
1866
+ async upload(element, file) {
1867
+ const uploadPromise = (Array.isArray(file) ? file : [file]).map(async (file) => {
1868
+ if (typeof file !== 'string') {
1869
+ return file
1870
+ }
1871
+
1872
+ const { content: base64, basename, mime } = await rpc().triggerCommand(contextId, "__vitest_fileInfo", filepath(), [file, 'base64'])
1873
+ const fileInstance = fetch(base64)
1874
+ .then(r => r.blob())
1875
+ .then(blob => new File([blob], basename, { type: mime }))
1876
+ return fileInstance
1877
+ })
1878
+ const uploadFiles = await Promise.all(uploadPromise)
1879
+ return this._upload(element, uploadFiles)
1880
+ },
1881
1881
  async fill(element, text) {
1882
1882
  await this.clear(element)
1883
1883
  await this.type(element, text)
@@ -1902,53 +1902,6 @@ const _userEventSetup = __vitest_user_event__.setup()
1902
1902
  `;
1903
1903
  }
1904
1904
 
1905
- function injectDynamicImport(code, id, parse) {
1906
- const s = new MagicString(code);
1907
- let ast;
1908
- try {
1909
- ast = parse(code);
1910
- } catch (err) {
1911
- console.error(`Cannot parse ${id}:
1912
- ${err.message}`);
1913
- return;
1914
- }
1915
- esmWalker(ast, {
1916
- // TODO: make env updatable
1917
- onImportMeta() {
1918
- },
1919
- onDynamicImport(node) {
1920
- const replaceString = "__vitest_browser_runner__.wrapModule(() => import(";
1921
- const importSubstring = code.substring(node.start, node.end);
1922
- const hasIgnore = importSubstring.includes("/* @vite-ignore */");
1923
- s.overwrite(
1924
- node.start,
1925
- node.source.start,
1926
- replaceString + (hasIgnore ? "/* @vite-ignore */ " : "")
1927
- );
1928
- s.overwrite(node.end - 1, node.end, "))");
1929
- }
1930
- });
1931
- return {
1932
- ast,
1933
- code: s.toString(),
1934
- map: s.generateMap({ hires: "boundary", source: id })
1935
- };
1936
- }
1937
-
1938
- const regexDynamicImport = /import\s*\(/;
1939
- var DynamicImport = () => {
1940
- return {
1941
- name: "vitest:browser:esm-injector",
1942
- enforce: "post",
1943
- transform(source, id) {
1944
- if (!regexDynamicImport.test(source)) {
1945
- return;
1946
- }
1947
- return injectDynamicImport(source, id, this.parse);
1948
- }
1949
- };
1950
- };
1951
-
1952
1905
  async function resolveOrchestrator(server, url, res) {
1953
1906
  const project = server.project;
1954
1907
  let contextId = url.searchParams.get("contextId");
@@ -2012,7 +1965,7 @@ async function resolveTester(server, url, res) {
2012
1965
  const { contextId, testFile } = server.resolveTesterUrl(url.pathname);
2013
1966
  const project = server.project;
2014
1967
  const state = server.state;
2015
- const testFiles = await project.globTestFiles();
1968
+ const { testFiles } = await project.globTestFiles();
2016
1969
  const tests = testFile === "__vitest_all__" || !testFiles.includes(testFile) ? "__vitest_browser_runner__.files" : JSON.stringify([testFile]);
2017
1970
  const iframeId = JSON.stringify(testFile);
2018
1971
  const context = state.getContext(contextId);
@@ -2174,7 +2127,7 @@ var BrowserPlugin = (browserServer, base = "/") => {
2174
2127
  name: "vitest:browser:tests",
2175
2128
  enforce: "pre",
2176
2129
  async config() {
2177
- const allTestFiles = await project.globTestFiles();
2130
+ const { testFiles: allTestFiles } = await project.globTestFiles();
2178
2131
  const browserTestFiles = allTestFiles.filter(
2179
2132
  (file) => getFilePoolName(project, file) === "browser"
2180
2133
  );
@@ -2194,6 +2147,28 @@ var BrowserPlugin = (browserServer, base = "/") => {
2194
2147
  resolve(distDir, "utils.js"),
2195
2148
  ...project.config.snapshotSerializers || []
2196
2149
  ];
2150
+ const exclude = [
2151
+ "vitest",
2152
+ "vitest/utils",
2153
+ "vitest/browser",
2154
+ "vitest/runners",
2155
+ "@vitest/browser",
2156
+ "@vitest/browser/client",
2157
+ "@vitest/utils",
2158
+ "@vitest/utils/source-map",
2159
+ "@vitest/runner",
2160
+ "@vitest/spy",
2161
+ "@vitest/utils/error",
2162
+ "@vitest/snapshot",
2163
+ "@vitest/expect",
2164
+ "std-env",
2165
+ "tinybench",
2166
+ "tinyspy",
2167
+ "tinyrainbow",
2168
+ "pathe",
2169
+ "msw",
2170
+ "msw/browser"
2171
+ ];
2197
2172
  if (project.config.diff) {
2198
2173
  entries.push(project.config.diff);
2199
2174
  }
@@ -2204,11 +2179,13 @@ var BrowserPlugin = (browserServer, base = "/") => {
2204
2179
  const path = tryResolve("@vitest/coverage-v8", [project.ctx.config.root]);
2205
2180
  if (path) {
2206
2181
  entries.push(path);
2182
+ exclude.push("@vitest/coverage-v8/browser");
2207
2183
  }
2208
2184
  } else if (provider === "istanbul") {
2209
2185
  const path = tryResolve("@vitest/coverage-istanbul", [project.ctx.config.root]);
2210
2186
  if (path) {
2211
2187
  entries.push(path);
2188
+ exclude.push("@vitest/coverage-istanbul");
2212
2189
  }
2213
2190
  } else if (provider === "custom" && coverage.customProviderModule) {
2214
2191
  entries.push(coverage.customProviderModule);
@@ -2230,28 +2207,6 @@ var BrowserPlugin = (browserServer, base = "/") => {
2230
2207
  if (vue) {
2231
2208
  include.push(vue);
2232
2209
  }
2233
- const exclude = [
2234
- "vitest",
2235
- "vitest/utils",
2236
- "vitest/browser",
2237
- "vitest/runners",
2238
- "@vitest/browser",
2239
- "@vitest/browser/client",
2240
- "@vitest/utils",
2241
- "@vitest/utils/source-map",
2242
- "@vitest/runner",
2243
- "@vitest/spy",
2244
- "@vitest/utils/error",
2245
- "@vitest/snapshot",
2246
- "@vitest/expect",
2247
- "std-env",
2248
- "tinybench",
2249
- "tinyspy",
2250
- "tinyrainbow",
2251
- "pathe",
2252
- "msw",
2253
- "msw/browser"
2254
- ];
2255
2210
  const svelte = tryResolve("vitest-browser-svelte", [project.ctx.config.root]);
2256
2211
  if (svelte) {
2257
2212
  exclude.push(svelte);
@@ -2307,7 +2262,9 @@ var BrowserPlugin = (browserServer, base = "/") => {
2307
2262
  }
2308
2263
  },
2309
2264
  BrowserContext(browserServer),
2310
- DynamicImport(),
2265
+ dynamicImportPlugin({
2266
+ globalThisAccessor: '"__vitest_browser_runner__"'
2267
+ }),
2311
2268
  {
2312
2269
  name: "vitest:browser:config",
2313
2270
  enforce: "post",
@@ -2343,6 +2300,23 @@ var BrowserPlugin = (browserServer, base = "/") => {
2343
2300
  };
2344
2301
  }
2345
2302
  },
2303
+ {
2304
+ name: "vitest:browser:in-source-tests",
2305
+ transform(code, id) {
2306
+ if (!project.isTestFile(id) || !code.includes("import.meta.vitest")) {
2307
+ return;
2308
+ }
2309
+ const s = new MagicString(code, { filename: cleanUrl(id) });
2310
+ s.prepend(
2311
+ `import.meta.vitest = __vitest_index__;
2312
+ `
2313
+ );
2314
+ return {
2315
+ code: s.toString(),
2316
+ map: s.generateMap({ hires: true })
2317
+ };
2318
+ }
2319
+ },
2346
2320
  // TODO: remove this when @testing-library/vue supports ESM
2347
2321
  {
2348
2322
  name: "vitest:browser:support-testing-library",
@@ -2420,14 +2394,18 @@ function resolveCoverageFolder(project) {
2420
2394
  }
2421
2395
  return [resolve(root, subdir), `/${basename(root)}/${subdir}/`];
2422
2396
  }
2397
+ const postfixRE = /[?#].*$/;
2398
+ function cleanUrl(url) {
2399
+ return url.replace(postfixRE, "");
2400
+ }
2423
2401
 
2424
2402
  const debug = createDebugger("vitest:browser:pool");
2403
+ async function waitForTests(method, contextId, project, files) {
2404
+ const context = project.browser.state.createAsyncContext(method, contextId, files);
2405
+ return await context;
2406
+ }
2425
2407
  function createBrowserPool(ctx) {
2426
2408
  const providers = /* @__PURE__ */ new Set();
2427
- const waitForTests = async (method, contextId, project, files) => {
2428
- const context = project.browser.state.createAsyncContext(method, contextId, files);
2429
- return await context;
2430
- };
2431
2409
  const executeTests = async (method, project, files) => {
2432
2410
  ctx.state.clearFiles(project, files);
2433
2411
  const browser = project.browser;
@@ -2441,6 +2419,20 @@ function createBrowserPool(ctx) {
2441
2419
  `Can't find browser origin URL for project "${project.getName()}" when running tests for files "${files.join('", "')}"`
2442
2420
  );
2443
2421
  }
2422
+ async function setBreakpoint(contextId, file) {
2423
+ if (!project.config.inspector.waitForDebugger) {
2424
+ return;
2425
+ }
2426
+ if (!provider.getCDPSession) {
2427
+ throw new Error("Unable to set breakpoint, CDP not supported");
2428
+ }
2429
+ const session = await provider.getCDPSession(contextId);
2430
+ await session.send("Debugger.enable", {});
2431
+ await session.send("Debugger.setBreakpointByUrl", {
2432
+ lineNumber: 0,
2433
+ urlRegex: escapePathToRegexp(file)
2434
+ });
2435
+ }
2444
2436
  const filesPerThread = Math.ceil(files.length / threadsCount);
2445
2437
  const chunks = [];
2446
2438
  for (let i = 0; i < files.length; i += filesPerThread) {
@@ -2477,7 +2469,7 @@ function createBrowserPool(ctx) {
2477
2469
  );
2478
2470
  const url = new URL("/", origin);
2479
2471
  url.searchParams.set("contextId", contextId);
2480
- const page = provider.openPage(contextId, url.toString()).then(() => waitPromise);
2472
+ const page = provider.openPage(contextId, url.toString(), () => setBreakpoint(contextId, files2[0])).then(() => waitPromise);
2481
2473
  promises.push(page);
2482
2474
  }
2483
2475
  });
@@ -2522,17 +2514,47 @@ function createBrowserPool(ctx) {
2522
2514
  collectTests: (files) => runWorkspaceTests("collect", files)
2523
2515
  };
2524
2516
  }
2517
+ function escapePathToRegexp(path) {
2518
+ return path.replace(/[/\\.?*()^${}|[\]+]/g, "\\$&");
2519
+ }
2525
2520
 
2526
2521
  async function createBrowserServer(project, configFile, prePlugins = [], postPlugins = []) {
2522
+ if (project.ctx.version !== version) {
2523
+ project.ctx.logger.warn(
2524
+ c.yellow(
2525
+ `Loaded ${c.inverse(c.yellow(` vitest@${project.ctx.version} `))} and ${c.inverse(c.yellow(` @vitest/browser@${version} `))}.
2526
+ Running mixed versions is not supported and may lead into bugs
2527
+ Update your dependencies and make sure the versions match.`
2528
+ )
2529
+ );
2530
+ }
2527
2531
  const server = new BrowserServer(project, "/");
2528
- const root = project.config.root;
2529
- await project.ctx.packageInstaller.ensureInstalled("@vitest/browser", root);
2530
2532
  const configPath = typeof configFile === "string" ? configFile : false;
2531
- const vite = await createServer({
2533
+ const logLevel = process.env.VITEST_BROWSER_DEBUG ?? "info";
2534
+ const logger = createViteLogger(logLevel);
2535
+ const vite = await createViteServer({
2532
2536
  ...project.options,
2533
2537
  // spread project config inlined in root workspace config
2534
2538
  base: "/",
2535
- logLevel: process.env.VITEST_BROWSER_DEBUG ?? "info",
2539
+ logLevel,
2540
+ customLogger: {
2541
+ ...logger,
2542
+ info(msg, options) {
2543
+ logger.info(msg, options);
2544
+ if (msg.includes("optimized dependencies changed. reloading")) {
2545
+ logger.warn(
2546
+ [
2547
+ c.yellow(`
2548
+ ${c.bold("[vitest]")} Vite unexpectedly reloaded a test. This may cause tests to fail, lead to flaky behaviour or duplicated test runs.
2549
+ `),
2550
+ c.yellow(`For a stable experience, please add mentioned dependencies to your config's ${c.bold("`optimizeDeps.include`")} field manually.
2551
+
2552
+ `)
2553
+ ].join("")
2554
+ );
2555
+ }
2556
+ }
2557
+ },
2536
2558
  mode: project.config.mode,
2537
2559
  configFile: configPath,
2538
2560
  // watch is handled by Vitest