@vitest/browser 5.0.0-beta.6 → 5.0.0-rc.1

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,27 +1,24 @@
1
+ import fs, { promises, existsSync, readFileSync, createReadStream } from 'node:fs';
2
+ import { createRequire } from 'node:module';
1
3
  import { ManualMockedModule, RedirectedModule, AutomockedModule, AutospiedModule, MockerRegistry } from '@vitest/mocker';
2
4
  import { dynamicImportPlugin, ServerMockResolver, interceptorPlugin } from '@vitest/mocker/node';
3
- import c from 'tinyrainbow';
4
- import { isFileLoadingAllowed, isValidApiRequest, isFileServingAllowed, distDir, rolldownVersion, resolveApiServerConfig, resolveFsAllow, createDebugger, createViteLogger, createViteServer } from 'vitest/node';
5
- import { fileURLToPath } from 'node:url';
6
- import fs, { readFileSync, createReadStream, promises, existsSync } from 'node:fs';
7
- import { createRequire } from 'node:module';
8
5
  import { distClientRoot } from '@vitest/ui';
9
- import { slash as slash$1, toArray, deepMerge } from '@vitest/utils/helpers';
10
- import MagicString from 'magic-string';
6
+ import { slash as slash$1, deepMerge, cleanUrl as cleanUrl$1, toArray } from '@vitest/utils/helpers';
11
7
  import sirv from 'sirv';
8
+ import c from 'tinyrainbow';
9
+ import { isFileLoadingAllowed, createDebugger, isValidApiRequest, isCSSRequest, distDir, rolldownVersion, isFileServingAllowed } from 'vitest/node';
10
+ import { fileURLToPath } from 'node:url';
12
11
  import crypto from 'node:crypto';
13
- import { platform } from 'node:os';
14
- import 'vite';
12
+ import MagicString from 'magic-string';
15
13
  import { readFile as readFile$1, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
16
14
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
17
15
  import { extractSourcemapFromFile } from '@vitest/utils/source-map/node';
18
16
  import { resolve as resolve$1, basename as basename$1, dirname as dirname$1 } from 'node:path';
17
+ import { platform } from 'node:os';
19
18
  import { PNG } from 'pngjs';
20
19
  import { diff } from '@blazediff/core';
21
20
  import { WebSocketServer } from 'ws';
22
21
 
23
- var version = "5.0.0-beta.6";
24
-
25
22
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
26
23
  function normalizeWindowsPath(input = "") {
27
24
  if (!input) {
@@ -212,11 +209,11 @@ const basename = function(p, extension) {
212
209
  return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
213
210
  };
214
211
 
212
+ var version = "5.0.0-rc.1";
213
+
215
214
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
216
215
  const distRoot = resolve(pkgRoot, "dist");
217
216
 
218
- const API_TOKEN_FILE = ".vitest-secret-token";
219
-
220
217
  /// <reference types="../types/index.d.ts" />
221
218
 
222
219
  // (c) 2020-present Andrea Giammarchi
@@ -512,11 +509,10 @@ var DOM_KEY_LOCATION = /*#__PURE__*/ function(DOM_KEY_LOCATION) {
512
509
  }
513
510
  ];
514
511
 
515
- var bracketDict = /*#__PURE__*/ function(bracketDict) {
516
- bracketDict["{"] = "}";
517
- bracketDict["["] = "]";
518
- return bracketDict;
519
- }(bracketDict || {});
512
+ const bracketDict = {
513
+ '{': '}',
514
+ '[': ']'
515
+ };
520
516
  /**
521
517
  * Read the next key definition from user input
522
518
  *
@@ -551,16 +547,16 @@ function readPrintableChar(text, pos, context) {
551
547
  };
552
548
  }
553
549
  function readTag(text, pos, startBracket, context) {
550
+ var _ref;
554
551
  var _text_slice_match, _text_slice_match1;
555
552
  const releasePreviousModifier = text[pos] === '/' ? '/' : '';
556
553
  pos += releasePreviousModifier.length;
557
554
  const escapedDescriptor = startBracket === '{' && text[pos] === '\\';
558
555
  pos += Number(escapedDescriptor);
559
- const descriptor = escapedDescriptor ? text[pos] : (_text_slice_match = text.slice(pos).match(startBracket === '{' ? /^\w+|^[^}>/]/ : /^\w+/)) === null || _text_slice_match === undefined ? undefined : _text_slice_match[0];
556
+ const descriptor = escapedDescriptor ? text[pos] : (_text_slice_match = text.slice(pos).match(startBracket === '{' ? /^\w+|^[^}>/]/ : /^\w+/)) === null || _text_slice_match === void 0 ? void 0 : _text_slice_match[0];
560
557
  assertDescriptor(descriptor, text, pos);
561
558
  pos += descriptor.length;
562
- var _text_slice_match_;
563
- const repeatModifier = (_text_slice_match_ = (_text_slice_match1 = text.slice(pos).match(/^>\d+/)) === null || _text_slice_match1 === undefined ? undefined : _text_slice_match1[0]) !== null && _text_slice_match_ !== undefined ? _text_slice_match_ : '';
559
+ const repeatModifier = (_ref = (_text_slice_match1 = text.slice(pos).match(/^>\d+/)) === null || _text_slice_match1 === void 0 ? void 0 : _text_slice_match1[0]) !== null && _ref !== void 0 ? _ref : '';
564
560
  pos += repeatModifier.length;
565
561
  const releaseSelfModifier = text[pos] === '/' || !repeatModifier && text[pos] === '>' ? text[pos] : '';
566
562
  pos += releaseSelfModifier.length;
@@ -596,7 +592,7 @@ function hasReleaseSelf(releaseSelfModifier, repeatModifier) {
596
592
  }
597
593
  }
598
594
  function getErrorMessage(expected, found, text, context) {
599
- return `Expected ${expected} but found "${found !== null && found !== undefined ? found : ''}" in "${text}"
595
+ return `Expected ${expected} but found "${found !== null && found !== void 0 ? found : ''}" in "${text}"
600
596
  See ${`https://testing-library.com/docs/user-event/keyboard`}
601
597
  for more information about how userEvent parses your input.`;
602
598
  }
@@ -613,18 +609,18 @@ function getErrorMessage(expected, found, text, context) {
613
609
  */ function parseKeyDef$1(keyboardMap, text) {
614
610
  const defs = [];
615
611
  do {
616
- const { type, descriptor, consumedLength, releasePrevious, releaseSelf = true, repeat } = readNextDescriptor(text);
617
612
  var _keyboardMap_find;
613
+ const { type, descriptor, consumedLength, releasePrevious, releaseSelf = true, repeat } = readNextDescriptor(text);
618
614
  const keyDef = (_keyboardMap_find = keyboardMap.find((def)=>{
619
615
  if (type === '[') {
620
616
  var _def_code;
621
- return ((_def_code = def.code) === null || _def_code === undefined ? undefined : _def_code.toLowerCase()) === descriptor.toLowerCase();
617
+ return ((_def_code = def.code) === null || _def_code === void 0 ? void 0 : _def_code.toLowerCase()) === descriptor.toLowerCase();
622
618
  } else if (type === '{') {
623
619
  var _def_key;
624
- return ((_def_key = def.key) === null || _def_key === undefined ? undefined : _def_key.toLowerCase()) === descriptor.toLowerCase();
620
+ return ((_def_key = def.key) === null || _def_key === void 0 ? void 0 : _def_key.toLowerCase()) === descriptor.toLowerCase();
625
621
  }
626
622
  return def.key === descriptor;
627
- })) !== null && _keyboardMap_find !== undefined ? _keyboardMap_find : {
623
+ })) !== null && _keyboardMap_find !== void 0 ? _keyboardMap_find : {
628
624
  key: 'Unknown',
629
625
  code: 'Unknown',
630
626
  [type === '[' ? 'code' : 'key']: descriptor
@@ -685,7 +681,7 @@ function assertBrowserFileAccess(project, path) {
685
681
  }
686
682
  }
687
683
  function assertBrowserApiWrite(project, path) {
688
- if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
684
+ if (!project.config.api.allowWrite || !project.vitest.config.api.allowWrite) {
689
685
  throw new Error(`Cannot modify file "${path}". File writing is disabled because the server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
690
686
  }
691
687
  }
@@ -816,16 +812,21 @@ async function resolveTester(globalServer, url, res, next) {
816
812
  __VITEST_PROVIDED_CONTEXT__: "{}",
817
813
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
818
814
  });
819
- const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
820
815
  try {
821
- const url = join("/@fs/", browserProject.testerFilepath);
822
- const indexhtml = await browserProject.vite.transformIndexHtml(url, testerHtml);
816
+ browserProject.testerHtmlTransformed ??= (async () => {
817
+ const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
818
+ const url = join("/@fs/", browserProject.testerFilepath);
819
+ return await browserProject.vite.transformIndexHtml(url, testerHtml);
820
+ })();
821
+ const indexhtml = await browserProject.testerHtmlTransformed;
823
822
  const html = replacer(indexhtml, {
824
823
  __VITEST_FAVICON__: globalServer.faviconUrl,
825
824
  __VITEST_INJECTOR__: injector
826
825
  });
827
826
  return html;
828
827
  } catch (err) {
828
+ // don't cache a rejection — the next request should retry the transform
829
+ browserProject.testerHtmlTransformed = undefined;
829
830
  session.fail(err);
830
831
  next(err);
831
832
  }
@@ -852,49 +853,41 @@ function createTesterMiddleware(browserServer) {
852
853
 
853
854
  const VIRTUAL_ID_CONTEXT = "\0vitest/browser";
854
855
  const ID_CONTEXT = "vitest/browser";
855
- // for libraries that use an older import but are not type checked
856
- const DEPRECATED_ID_CONTEXT = "@vitest/browser/context";
857
- const DEPRECATED_VIRTUAL_ID_UTILS = "\0@vitest/browser/utils";
858
- const DEPRECATED_ID_UTILS = "@vitest/browser/utils";
859
856
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
860
- function BrowserContext(globalServer) {
857
+ function BrowserContext(contribution) {
861
858
  return {
862
859
  name: "vitest:browser:virtual-module:context",
863
860
  enforce: "pre",
864
- resolveId(id, importer) {
865
- if (id === ID_CONTEXT) {
866
- return VIRTUAL_ID_CONTEXT;
867
- }
868
- if (id === DEPRECATED_ID_CONTEXT) {
869
- if (importer && !importer.includes("/node_modules/")) {
870
- globalServer.vitest.logger.deprecate(`${importer} tries to load a deprecated "${id}" module. ` + `This import will stop working in the next major version. ` + `Please, use "vitest/browser" instead.`);
861
+ resolveId: {
862
+ order: "pre",
863
+ handler(id) {
864
+ if (id === ID_CONTEXT) {
865
+ return VIRTUAL_ID_CONTEXT;
871
866
  }
872
- return VIRTUAL_ID_CONTEXT;
873
- }
874
- if (id === DEPRECATED_ID_UTILS) {
875
- return DEPRECATED_VIRTUAL_ID_UTILS;
876
867
  }
877
868
  },
878
869
  load(id) {
870
+ const globalServer = contribution.parent;
879
871
  if (id === VIRTUAL_ID_CONTEXT) {
880
872
  return generateContextFile.call(this, globalServer);
881
873
  }
882
- if (id === DEPRECATED_VIRTUAL_ID_UTILS) {
883
- return `
884
- import { utils } from 'vitest/browser'
885
- export const getElementLocatorSelectors = utils.getElementLocatorSelectors
886
- export const debug = utils.debug
887
- export const prettyDOM = utils.prettyDOM
888
- export const getElementError = utils.getElementError
889
- `;
890
- }
891
874
  }
892
875
  };
893
876
  }
894
877
  async function generateContextFile(globalServer) {
895
878
  const commands = Object.keys(globalServer.commands);
896
- const provider = [...globalServer.children][0].provider;
897
- const providerName = provider?.name || "preview";
879
+ // The provider instance is initialized lazily when a page opens, so reading
880
+ // `child.provider` here races and can be `undefined` before the first page is
881
+ // opened. The provider name is uniform across a project's instances and is
882
+ // already resolved on the config, so read it from there for this shared
883
+ // (cached) context module. The instance is still used below for the
884
+ // preview-only user-event import.
885
+ const child = [...globalServer.children][0];
886
+ const provider = child.provider;
887
+ const providerName = child.config.browser.provider?.name;
888
+ if (!providerName) {
889
+ throw new Error(`Browser provider is not defined for the project "${child.project.name}". This is a bug in Vitest. Please, open a new issue with a reproduction.`);
890
+ }
898
891
  const commandsCode = commands.filter((command) => !command.startsWith("__vitest")).map((command) => {
899
892
  return ` ["${command}"]: (...args) => __vitest_browser_runner__.commands.triggerCommand("${command}", args),`;
900
893
  }).join("\n");
@@ -929,256 +922,41 @@ async function getUserEventImport(provider, resolve) {
929
922
  throw new Error(`Failed to resolve user-event package from ${previewDistRoot}`);
930
923
  }
931
924
  return `\
932
- import { userEvent as __vitest_user_event__ } from '${slash$1(`/@fs/${resolved.id}`)}'
933
- const _userEventSetup = __vitest_user_event__
925
+ import { userEvent as _userEventSetup } from '${slash$1(`/@fs/${resolved.id}`)}'
934
926
  `;
935
927
  }
936
928
 
937
- const versionRegexp = /(?:\?|&)v=\w{8}/;
938
- var BrowserPlugin = (parentServer, base = "/") => {
939
- function isPackageExists(pkg, root) {
940
- return parentServer.vitest.packageInstaller.isPackageExists?.(pkg, { paths: [root] });
941
- }
929
+ var BrowserPlugin = (contribution) => {
942
930
  return [
943
- {
944
- enforce: "pre",
945
- name: "vitest:browser",
946
- async configureServer(server) {
947
- parentServer.setServer(server);
948
- // eslint-disable-next-line prefer-arrow-callback
949
- server.middlewares.use(function vitestHeaders(_req, res, next) {
950
- const headers = server.config.server.headers;
951
- if (headers) {
952
- for (const name in headers) {
953
- res.setHeader(name, headers[name]);
954
- }
955
- }
956
- next();
957
- });
958
- // strip _vitest_original query added by importActual so that
959
- // the plugin pipeline sees the original import id (e.g. virtual modules's load hook).
960
- server.middlewares.use((req, _res, next) => {
961
- if (req.url?.includes("_vitest_original") && parentServer.project.config.browser.provider?.name === "playwright") {
962
- req.url = req.url.replace(/[?&]_vitest_original(?=[&#]|$)/, "").replace(/[?&]ext\b[^&#]*/, "").replace(/\?$/, "");
963
- }
964
- next();
965
- });
966
- server.middlewares.use(createOrchestratorMiddleware(parentServer));
967
- server.middlewares.use(createTesterMiddleware(parentServer));
968
- server.middlewares.use(`${base}favicon.svg`, (_, res) => {
969
- const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
970
- res.write(content, "utf-8");
971
- res.end();
972
- });
973
- // Serve coverage HTML at ./coverage if configured
974
- const coverageHtmlDir = parentServer.vitest.config.coverage?.htmlDir;
975
- if (coverageHtmlDir) {
976
- server.middlewares.use("/__vitest_test__/coverage", sirv(coverageHtmlDir, {
977
- single: true,
978
- dev: true,
979
- setHeaders: (res) => {
980
- const csp = res.getHeader("Content-Security-Policy");
981
- if (typeof csp === "string") {
982
- // add frame-ancestors to allow the iframe to be loaded by Vitest,
983
- // but keep the rest of the CSP
984
- res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
985
- }
986
- res.setHeader("Cache-Control", "public,max-age=0,must-revalidate");
987
- }
988
- }));
989
- }
990
- server.middlewares.use((req, res, next) => {
991
- // 9000 mega head move
992
- // Vite always caches optimized dependencies, but users might mock
993
- // them in _some_ tests, while keeping original modules in others
994
- // there is no way to configure that in Vite, so we patch it here
995
- // to always ignore the cache-control set by Vite in the next middleware
996
- if (req.url && versionRegexp.test(req.url) && !req.url.includes("chunk-")) {
997
- res.setHeader("Cache-Control", "no-cache");
998
- const setHeader = res.setHeader.bind(res);
999
- res.setHeader = function(name, value) {
1000
- if (name === "Cache-Control") {
1001
- return res;
1002
- }
1003
- return setHeader(name, value);
1004
- };
1005
- }
1006
- next();
1007
- });
1008
- // handle attachments the same way as in packages/ui/node/index.ts
1009
- server.middlewares.use((req, res, next) => {
1010
- if (!req.url) {
1011
- return next();
1012
- }
1013
- const url = new URL(req.url, "http://localhost");
1014
- if (url.pathname !== "/__vitest_attachment__") {
1015
- return next();
1016
- }
1017
- const path = url.searchParams.get("path");
1018
- const contentType = url.searchParams.get("contentType");
1019
- if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
1020
- return next();
1021
- }
1022
- const fsPath = decodeURIComponent(path);
1023
- if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
1024
- return next();
1025
- }
1026
- try {
1027
- res.setHeader("content-type", contentType);
1028
- return createReadStream(fsPath).pipe(res).on("close", () => res.end());
1029
- } catch (err) {
1030
- return next(err);
1031
- }
1032
- });
1033
- }
1034
- },
1035
931
  {
1036
932
  name: "vitest:browser:tests",
1037
933
  enforce: "pre",
1038
- async config() {
1039
- // this plugin can be used in different projects, but all of them
1040
- // have the same `include` pattern, so it doesn't matter which project we use
1041
- const project = parentServer.project;
1042
- // only glob benchmarks when a browser-enabled bench project exists in
1043
- // the workspace — keeps the optimizeDeps entries symmetrical across
1044
- // the test/bench project clones so the optimizer doesn't re-scan when
1045
- // the user switches modes
1046
- const hasBrowserBenchProject = parentServer.vitest.projects.some((p) => p.config.browser.enabled && p.config.benchmark.enabled);
1047
- const benchInclude = hasBrowserBenchProject ? project.config.benchmark.include : [];
1048
- const dir = project.config.dir || project.config.root;
1049
- const [{ testFiles: browserTestFiles }, browserBenchFiles] = await Promise.all([project.globTestFiles(), benchInclude.length > 0 ? project.globFiles(benchInclude, project.config.benchmark.exclude ?? project.config.exclude, dir) : []]);
1050
- const setupFiles = toArray(project.config.setupFiles);
1051
- // replace env values - cannot be reassign at runtime
1052
- const define = {};
1053
- for (const env in project.config.env || {}) {
1054
- const stringValue = JSON.stringify(project.config.env[env]);
1055
- define[`import.meta.env.${env}`] = stringValue;
1056
- }
1057
- const entries = [
1058
- ...new Set([...browserTestFiles, ...browserBenchFiles]),
1059
- ...setupFiles,
1060
- resolve(distDir, "index.js"),
1061
- resolve(distDir, "browser.js"),
1062
- resolve(distDir, "runners.js"),
1063
- resolve(distDir, "utils.js"),
1064
- ...project.config.snapshotSerializers || []
1065
- ];
1066
- const exclude = [
1067
- "vitest",
1068
- "vitest/browser",
1069
- "vitest/internal/browser",
1070
- "vite/module-runner",
1071
- "@vitest/browser/utils",
1072
- "@vitest/browser/context",
1073
- "@vitest/browser/client",
1074
- "@vitest/utils",
1075
- "@vitest/utils/source-map",
1076
- "@vitest/spy",
1077
- "@vitest/utils/error",
1078
- "std-env",
1079
- "tinybench",
1080
- "tinyspy",
1081
- "tinyrainbow",
1082
- "pathe",
1083
- "msw",
1084
- "msw/browser"
1085
- ];
1086
- if (typeof project.config.diff === "string") {
1087
- entries.push(project.config.diff);
1088
- }
1089
- if (parentServer.vitest.coverageProvider) {
1090
- const coverage = parentServer.vitest._coverageOptions;
1091
- const provider = coverage.provider;
1092
- if (provider === "v8") {
1093
- const path = tryResolve("@vitest/coverage-v8", [parentServer.config.root]);
1094
- if (path) {
1095
- entries.push(path);
1096
- exclude.push("@vitest/coverage-v8/browser");
1097
- }
1098
- } else if (provider === "istanbul") {
1099
- const path = tryResolve("@vitest/coverage-istanbul", [parentServer.config.root]);
1100
- if (path) {
1101
- entries.push(path);
1102
- exclude.push("@vitest/coverage-istanbul");
1103
- }
1104
- } else if (provider === "custom" && coverage.customProviderModule) {
1105
- entries.push(coverage.customProviderModule);
934
+ resolveId: {
935
+ order: "pre",
936
+ handler(id) {
937
+ if (!/\?browserv=\w+$/.test(id)) {
938
+ return;
1106
939
  }
1107
- }
1108
- const include = [
1109
- "vitest > expect-type",
1110
- "vitest > magic-string",
1111
- "vitest > chai"
1112
- ];
1113
- const provider = parentServer.config.browser.provider || [...parentServer.children][0]?.provider;
1114
- if (provider?.name === "preview") {
1115
- include.push("@vitest/browser-preview > @testing-library/user-event", "@vitest/browser-preview > @testing-library/dom");
1116
- }
1117
- const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : project.config.root;
1118
- const svelte = isPackageExists("vitest-browser-svelte", fileRoot);
1119
- if (svelte) {
1120
- exclude.push("vitest-browser-svelte");
1121
- }
1122
- // since we override the resolution in the esbuild plugin, Vite can no longer optimizer it
1123
- const vue = isPackageExists("vitest-browser-vue", fileRoot);
1124
- if (vue) {
1125
- // we override them in the esbuild plugin so optimizer can no longer intercept it
1126
- include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
1127
- }
1128
- const vueTestUtils = isPackageExists("@vue/test-utils", fileRoot);
1129
- if (vueTestUtils) {
1130
- include.push("@vue/test-utils");
1131
- }
1132
- const otelConfig = project.config.experimental.openTelemetry;
1133
- if (otelConfig?.enabled && otelConfig.browserSdkPath) {
1134
- entries.push(otelConfig.browserSdkPath);
1135
- include.push("@opentelemetry/api");
1136
- } else {
1137
- exclude.push("@opentelemetry/api");
1138
- }
1139
- return {
1140
- define,
1141
- resolve: { dedupe: ["vitest"] },
1142
- optimizeDeps: {
1143
- entries,
1144
- exclude,
1145
- include
940
+ let useId = id.slice(0, id.lastIndexOf("?"));
941
+ if (useId.startsWith("/@fs/")) {
942
+ useId = useId.slice(5);
1146
943
  }
1147
- };
1148
- },
1149
- async resolveId(id) {
1150
- if (!/\?browserv=\w+$/.test(id)) {
1151
- return;
1152
- }
1153
- let useId = id.slice(0, id.lastIndexOf("?"));
1154
- if (useId.startsWith("/@fs/")) {
1155
- useId = useId.slice(5);
1156
- }
1157
- if (/^\w:/.test(useId)) {
1158
- useId = useId.replace(/\\/g, "/");
1159
- }
1160
- return useId;
1161
- }
1162
- },
1163
- {
1164
- name: "vitest:browser:resolve-virtual",
1165
- async resolveId(rawId) {
1166
- if (rawId === "/mockServiceWorker.js") {
1167
- return this.resolve("msw/mockServiceWorker.js", distRoot, { skipSelf: true });
944
+ if (/^\w:/.test(useId)) {
945
+ useId = useId.replace(/\\/g, "/");
946
+ }
947
+ return useId;
1168
948
  }
1169
949
  }
1170
950
  },
1171
951
  {
1172
952
  name: "vitest:browser:assets",
1173
- configureServer(server) {
1174
- server.middlewares.use("/__vitest__", sirv(distClientRoot));
1175
- },
1176
953
  resolveId(id) {
1177
954
  if (id.startsWith("/__vitest_browser__/")) {
1178
955
  return resolve(distRoot, "client", id.slice(1));
1179
956
  }
1180
957
  },
1181
958
  transform(code, id) {
959
+ const parentServer = contribution.parent;
1182
960
  if (id.includes(parentServer.vite.config.cacheDir) && id.includes("loupe.js")) {
1183
961
  // loupe bundle has a nasty require('util') call that leaves a warning in the console
1184
962
  const utilRequire = "nodeUtil = require_util();";
@@ -1186,47 +964,17 @@ var BrowserPlugin = (parentServer, base = "/") => {
1186
964
  }
1187
965
  }
1188
966
  },
1189
- BrowserContext(parentServer),
967
+ BrowserContext(contribution),
1190
968
  dynamicImportPlugin({
1191
969
  globalThisAccessor: "\"__vitest_browser_runner__\"",
1192
- filter(id, environment) {
1193
- if (environment.name !== "client") {
1194
- return false;
1195
- }
970
+ // injected only into the `client` environment via the browser contribution
971
+ filter(id) {
1196
972
  if (id.includes(distRoot)) {
1197
973
  return false;
1198
974
  }
1199
975
  return true;
1200
976
  }
1201
977
  }),
1202
- {
1203
- name: "vitest:browser:config",
1204
- enforce: "post",
1205
- async config(viteConfig) {
1206
- // Enables using ignore hint for coverage providers with @preserve keyword
1207
- // Only set esbuild options when not using rolldown-vite (Vite 8+),
1208
- // which uses oxc for transformation instead of esbuild
1209
- if (!rolldownVersion && viteConfig.esbuild !== false) {
1210
- viteConfig.esbuild ||= {};
1211
- viteConfig.esbuild.legalComments = "inline";
1212
- }
1213
- const defaultPort = parentServer.vitest.state._data.browserLastPort++;
1214
- const api = resolveApiServerConfig(viteConfig.test?.browser || {}, defaultPort);
1215
- viteConfig.server = {
1216
- ...viteConfig.server,
1217
- port: defaultPort,
1218
- ...api,
1219
- middlewareMode: false,
1220
- open: false
1221
- };
1222
- viteConfig.server.fs ??= {};
1223
- viteConfig.server.fs.allow = viteConfig.server.fs.allow || [];
1224
- viteConfig.server.fs.deny ??= [];
1225
- viteConfig.server.fs.deny.push(API_TOKEN_FILE);
1226
- viteConfig.server.fs.allow.push(...resolveFsAllow(parentServer.vitest.config.root, parentServer.vitest.vite.config.configFile), distRoot);
1227
- return { resolve: { alias: viteConfig.test?.alias } };
1228
- }
1229
- },
1230
978
  {
1231
979
  name: "vitest:browser:in-source-tests",
1232
980
  transform: {
@@ -1259,130 +1007,6 @@ var BrowserPlugin = (parentServer, base = "/") => {
1259
1007
  }
1260
1008
  }
1261
1009
  },
1262
- {
1263
- name: "vitest:browser:transform-tester-html",
1264
- enforce: "pre",
1265
- async transformIndexHtml(html, ctx) {
1266
- const projectBrowser = [...parentServer.children].find((server) => {
1267
- return ctx.filename === server.testerFilepath;
1268
- });
1269
- if (!projectBrowser) {
1270
- return;
1271
- }
1272
- const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
1273
- const testerTags = [];
1274
- const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
1275
- if (!isDefaultTemplate) {
1276
- const manifestContent = parentServer.manifest instanceof Promise ? await parentServer.manifest : parentServer.manifest;
1277
- const testerEntry = manifestContent["tester/tester.html"];
1278
- testerTags.push({
1279
- tag: "script",
1280
- attrs: {
1281
- type: "module",
1282
- crossorigin: "",
1283
- src: `${parentServer.base}${testerEntry.file}`
1284
- },
1285
- injectTo: "head"
1286
- });
1287
- for (const importName of testerEntry.imports || []) {
1288
- const entryManifest = manifestContent[importName];
1289
- if (entryManifest) {
1290
- testerTags.push({
1291
- tag: "link",
1292
- attrs: {
1293
- href: `${parentServer.base}${entryManifest.file}`,
1294
- rel: "modulepreload",
1295
- crossorigin: ""
1296
- },
1297
- injectTo: "head"
1298
- });
1299
- }
1300
- }
1301
- } else {
1302
- // inject the reset style only in the default template,
1303
- // allowing users to customize the style in their own template
1304
- testerTags.push({
1305
- tag: "style",
1306
- children: `
1307
- html {
1308
- padding: 0;
1309
- margin: 0;
1310
- }
1311
- body {
1312
- padding: 0;
1313
- margin: 0;
1314
- min-height: 100vh;
1315
- }`,
1316
- injectTo: "head"
1317
- });
1318
- }
1319
- return [
1320
- {
1321
- tag: "script",
1322
- children: "{__VITEST_INJECTOR__}",
1323
- injectTo: "head-prepend"
1324
- },
1325
- {
1326
- tag: "script",
1327
- children: stateJs,
1328
- injectTo: "head-prepend"
1329
- },
1330
- {
1331
- tag: "script",
1332
- attrs: {
1333
- type: "module",
1334
- src: parentServer.errorCatcherUrl
1335
- },
1336
- injectTo: "head"
1337
- },
1338
- {
1339
- tag: "script",
1340
- attrs: {
1341
- type: "module",
1342
- src: parentServer.matchersUrl
1343
- },
1344
- injectTo: "head"
1345
- },
1346
- ...parentServer.initScripts.map((script) => ({
1347
- tag: "script",
1348
- attrs: {
1349
- type: "module",
1350
- src: join("/@fs/", script)
1351
- },
1352
- injectTo: "head"
1353
- })),
1354
- ...testerTags
1355
- ].filter((s) => s != null);
1356
- }
1357
- },
1358
- {
1359
- name: "vitest:browser:support-testing-library",
1360
- enforce: "pre",
1361
- config() {
1362
- const rolldownPlugin = {
1363
- name: "vue-test-utils-rewrite",
1364
- resolveId: {
1365
- filter: { id: /^@vue\/(test-utils|compiler-core)$/ },
1366
- handler(source, importer) {
1367
- const resolved = getRequire().resolve(source, { paths: [importer] });
1368
- return resolved;
1369
- }
1370
- }
1371
- };
1372
- const esbuildPlugin = {
1373
- name: "test-utils-rewrite",
1374
- setup(build) {
1375
- // test-utils: resolve to CJS instead of the browser because the browser version expects a global Vue object
1376
- // compiler-core: only CJS version allows slots as strings
1377
- build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
1378
- const resolved = getRequire().resolve(args.path, { paths: [args.importer] });
1379
- return { path: resolved };
1380
- });
1381
- }
1382
- };
1383
- return { optimizeDeps: rolldownVersion ? { rolldownOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } } };
1384
- }
1385
- },
1386
1010
  {
1387
1011
  name: "vitest:browser:__vitest_browser_import_meta_env_init__",
1388
1012
  transform: { handler(code) {
@@ -1395,27 +1019,14 @@ body {
1395
1019
  }
1396
1020
  ];
1397
1021
  };
1398
- function tryResolve(path, paths) {
1399
- try {
1400
- const _require = getRequire();
1401
- return _require.resolve(path, { paths });
1402
- } catch {
1403
- return undefined;
1404
- }
1405
- }
1406
- let _require;
1407
- function getRequire() {
1408
- if (!_require) {
1409
- _require = createRequire(import.meta.url);
1410
- }
1411
- return _require;
1412
- }
1413
1022
  const postfixRE = /[?#].*$/;
1414
1023
  function cleanUrl(url) {
1415
1024
  return url.replace(postfixRE, "");
1416
1025
  }
1417
1026
 
1418
1027
  class BrowserServerCDPHandler {
1028
+ session;
1029
+ tester;
1419
1030
  listenerIds = {};
1420
1031
  listeners = {};
1421
1032
  constructor(session, tester) {
@@ -1455,19 +1066,6 @@ class BrowserServerCDPHandler {
1455
1066
  }
1456
1067
  }
1457
1068
 
1458
- const _startV8Coverage = async (context) => {
1459
- const session = await context.__ensureCDPHandler();
1460
- await session.send("Profiler.enable");
1461
- await session.send("Profiler.startPreciseCoverage", {
1462
- callCount: true,
1463
- detailed: true
1464
- });
1465
- };
1466
- const _takeV8Coverage = async (context) => {
1467
- const session = await context.__ensureCDPHandler();
1468
- return session.send("Profiler.takePreciseCoverage");
1469
- };
1470
-
1471
1069
  const types = {
1472
1070
  'application/andrew-inset': ['ez'],
1473
1071
  'application/appinstaller': ['appinstaller'],
@@ -2072,6 +1670,7 @@ function getComparator(comparator, context) {
2072
1670
 
2073
1671
  const defaultOptions = {
2074
1672
  comparatorName: "pixelmatch",
1673
+ // these are handled by each comparator on its own
2075
1674
  comparatorOptions: {},
2076
1675
  screenshotOptions: {
2077
1676
  animations: "disabled",
@@ -2126,6 +1725,7 @@ function resolveOptions({ context, name, options, testName }) {
2126
1725
  resolvedOptions,
2127
1726
  paths: {
2128
1727
  reference: resolvedOptions.resolveScreenshotPath(resolvePathData),
1728
+ // lazily initialize this, as it might not be needed at all
2129
1729
  get diffs() {
2130
1730
  const diffs = {
2131
1731
  reference: resolvedOptions.resolveDiffPath({
@@ -2371,6 +1971,7 @@ async function determineOutcome({ comparator, comparatorOptions, paths, referenc
2371
1971
  diff: comparisonResult.diff && {
2372
1972
  image: {
2373
1973
  data: comparisonResult.diff,
1974
+ // `comparator` only returns pixel data; diff dimensions always match reference
2374
1975
  metadata: reference.metadata
2375
1976
  },
2376
1977
  path: paths.diffs.diff
@@ -2606,8 +2207,7 @@ var builtinCommands = {
2606
2207
  readFile,
2607
2208
  removeFile,
2608
2209
  writeFile,
2609
- __vitest_startV8Coverage: _startV8Coverage,
2610
- __vitest_takeV8Coverage: _takeV8Coverage,
2210
+ // private commands
2611
2211
  __vitest_markTrace: _markTrace,
2612
2212
  __vitest_groupTraceStart: _groupTraceStart,
2613
2213
  __vitest_groupTraceEnd: _groupTraceEnd,
@@ -2623,8 +2223,15 @@ class BrowserServerState {
2623
2223
  }
2624
2224
 
2625
2225
  class ProjectBrowser {
2226
+ parent;
2227
+ project;
2228
+ base;
2626
2229
  testerHtml;
2627
2230
  testerFilepath;
2231
+ // the tester template is read once per server, so the transformed HTML
2232
+ // is stable too — computing it for every iframe request pays the whole
2233
+ // transformIndexHtml plugin pipeline each time
2234
+ testerHtmlTransformed;
2628
2235
  provider;
2629
2236
  vitest;
2630
2237
  vite;
@@ -2706,6 +2313,7 @@ function wrapConfig(config) {
2706
2313
  }
2707
2314
 
2708
2315
  class ParentBrowserProject {
2316
+ base;
2709
2317
  orchestratorScripts;
2710
2318
  faviconUrl;
2711
2319
  prefixOrchestratorUrl;
@@ -2726,21 +2334,27 @@ class ParentBrowserProject {
2726
2334
  config;
2727
2335
  // cache for non-vite source maps
2728
2336
  sourceMapCache = new Map();
2729
- constructor(project, base) {
2730
- this.project = project;
2337
+ constructor(ctx, base) {
2731
2338
  this.base = base;
2732
- this.vitest = project.vitest;
2733
- this.config = project.config;
2339
+ this.vitest = ctx.vitest;
2340
+ this.config = ctx.config;
2734
2341
  this.stackTraceOptions = {
2735
- frameFilter: project.config.onStackTrace,
2342
+ frameFilter: this.config.onStackTrace,
2736
2343
  getSourceMap: (id) => {
2737
2344
  if (this.sourceMapCache.has(id)) {
2738
2345
  return this.sourceMapCache.get(id);
2739
2346
  }
2740
2347
  const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
2741
- // handle non-inline source map such as pre-bundled deps in node_modules/.vite
2742
- if (result && !result.map) {
2743
- const filePath = id.split("?")[0];
2348
+ const filePath = id.split("?")[0];
2349
+ // prefer the map stored on disk when the transform pipeline can't
2350
+ // provide a usable one:
2351
+ // - pre-bundled deps: the pipeline map resolves back into the
2352
+ // optimizer cache, while the map esbuild wrote next to the file
2353
+ // points at the real package sources
2354
+ // - an empty `mappings` means the map was intentionally not served
2355
+ // to the browser (`vitest:browser:framework-sourcemaps`), but disk
2356
+ // is still the source of truth for error stack traces
2357
+ if (result && (!result.map || result.map.mappings === "" || filePath.startsWith(this.vite.config.cacheDir))) {
2744
2358
  const extracted = extractSourcemapFromFile(result.code, filePath);
2745
2359
  this.sourceMapCache.set(id, extracted?.map);
2746
2360
  return extracted?.map;
@@ -2771,11 +2385,11 @@ class ParentBrowserProject {
2771
2385
  this.commands[name] ??= command;
2772
2386
  }
2773
2387
  // validate names because they can't be used as identifiers
2774
- for (const command in project.config.browser.commands) {
2388
+ for (const command in this.config.browser.commands) {
2775
2389
  if (!/^[a-z_$][\w$]*$/i.test(command)) {
2776
2390
  throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
2777
2391
  }
2778
- this.commands[command] = project.config.browser.commands[command];
2392
+ this.commands[command] = this.config.browser.commands[command];
2779
2393
  }
2780
2394
  this.prefixTesterUrl = `${base || "/"}`;
2781
2395
  this.prefixOrchestratorUrl = `${base}__vitest_test__/`;
@@ -2783,7 +2397,7 @@ class ParentBrowserProject {
2783
2397
  this.manifest = (async () => {
2784
2398
  return JSON.parse(await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8"));
2785
2399
  })().then((manifest) => this.manifest = manifest);
2786
- this.orchestratorHtml = (project.config.browser.ui ? readFile$1(resolve(distClientRoot, "index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8")).then((html) => this.orchestratorHtml = html);
2400
+ this.orchestratorHtml = (this.config.browser.ui ? readFile$1(resolve(distClientRoot, "index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8")).then((html) => this.orchestratorHtml = html);
2787
2401
  this.injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8").then((js) => this.injectorJs = js);
2788
2402
  this.errorCatcherUrl = join("/@fs/", resolve(distRoot, "client/error-catcher.js"));
2789
2403
  this.matchersUrl = join("/@fs/", distRoot, "expect-element.js");
@@ -2882,11 +2496,6 @@ class ParentBrowserProject {
2882
2496
  }
2883
2497
  }
2884
2498
 
2885
- //#region src/messages.ts
2886
- const TYPE_REQUEST = "q";
2887
- const TYPE_RESPONSE = "s";
2888
-
2889
- //#endregion
2890
2499
  //#region src/utils.ts
2891
2500
  function createPromiseWithResolvers() {
2892
2501
  let resolve;
@@ -2909,7 +2518,6 @@ function nanoid(size = 21) {
2909
2518
  return id;
2910
2519
  }
2911
2520
 
2912
- //#endregion
2913
2521
  //#region src/main.ts
2914
2522
  const DEFAULT_TIMEOUT = 6e4;
2915
2523
  const defaultSerialize = (i) => i;
@@ -2926,7 +2534,7 @@ function createBirpc($functions, options) {
2926
2534
  const req = {
2927
2535
  m: method,
2928
2536
  a: args,
2929
- t: TYPE_REQUEST
2537
+ t: "q"
2930
2538
  };
2931
2539
  if (optional) req.o = true;
2932
2540
  const send = async (_req) => post(serialize(_req));
@@ -2980,7 +2588,7 @@ function createBirpc($functions, options) {
2980
2588
  $call: (method, ...args) => _call(method, args, false),
2981
2589
  $callOptional: (method, ...args) => _call(method, args, false, true),
2982
2590
  $callEvent: (method, ...args) => _call(method, args, true),
2983
- $callRaw: (options$1) => _call(options$1.method, options$1.args, options$1.event, options$1.optional),
2591
+ $callRaw: (options) => _call(options.method, options.args, options.event, options.optional),
2984
2592
  $rejectPendingCalls,
2985
2593
  get $closed() {
2986
2594
  return $closed;
@@ -2992,7 +2600,7 @@ function createBirpc($functions, options) {
2992
2600
  $functions
2993
2601
  };
2994
2602
  if (proxify) rpc = new Proxy({}, { get(_, method) {
2995
- if (Object.prototype.hasOwnProperty.call(builtinMethods, method)) return builtinMethods[method];
2603
+ if (Object.hasOwn(builtinMethods, method)) return builtinMethods[method];
2996
2604
  if (method === "then" && !eventNames.includes("then") && !("then" in $functions)) return void 0;
2997
2605
  const sendEvent = (...args) => _call(method, args, true);
2998
2606
  if (eventNames.includes(method)) {
@@ -3036,7 +2644,7 @@ function createBirpc($functions, options) {
3036
2644
  if (options.onGeneralError?.call(rpc, e) !== true) throw e;
3037
2645
  return;
3038
2646
  }
3039
- if (msg.t === TYPE_REQUEST) {
2647
+ if (msg.t === "q") {
3040
2648
  const { m: method, a: args, o: optional } = msg;
3041
2649
  let result, error;
3042
2650
  let fn = await (resolver ? resolver.call(rpc, method, $functions[method]) : $functions[method]);
@@ -3053,7 +2661,7 @@ function createBirpc($functions, options) {
3053
2661
  }
3054
2662
  if (!error) try {
3055
2663
  await post(serialize({
3056
- t: TYPE_RESPONSE,
2664
+ t: "s",
3057
2665
  i: msg.i,
3058
2666
  r: result
3059
2667
  }), ...extra);
@@ -3064,7 +2672,7 @@ function createBirpc($functions, options) {
3064
2672
  }
3065
2673
  try {
3066
2674
  await post(serialize({
3067
- t: TYPE_RESPONSE,
2675
+ t: "s",
3068
2676
  i: msg.i,
3069
2677
  e: error
3070
2678
  }), ...extra);
@@ -3161,14 +2769,14 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3161
2769
  }
3162
2770
  }
3163
2771
  function canWrite(project) {
3164
- return project.config.browser.api.allowWrite && project.vitest.config.browser.api.allowWrite && project.config.api.allowWrite && project.vitest.config.api.allowWrite;
2772
+ return project.config.api.allowWrite && project.vitest.config.api.allowWrite;
3165
2773
  }
3166
2774
  function isCdpAllowed(project) {
3167
- return project.config.api.allowExec && project.config.browser.api.allowExec && project.vitest.config.api.allowExec && project.vitest.config.browser.api.allowExec && project.config.api.allowWrite && project.config.browser.api.allowWrite && project.vitest.config.api.allowWrite && project.vitest.config.browser.api.allowWrite;
2775
+ return project.config.api.allowExec && project.vitest.config.api.allowExec && project.config.api.allowWrite && project.vitest.config.api.allowWrite;
3168
2776
  }
3169
2777
  function assertCdpAllowed(project) {
3170
2778
  if (!isCdpAllowed(project)) {
3171
- throw new Error(`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/browser/api.`);
2779
+ throw new Error(`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/api.`);
3172
2780
  }
3173
2781
  }
3174
2782
  function setupClient(project, rpcId, ws, options) {
@@ -3204,13 +2812,13 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3204
2812
  if (!canWrite(project)) {
3205
2813
  if (artifact.type === "internal:annotation" && artifact.annotation.attachment) {
3206
2814
  artifact.annotation.attachment = undefined;
3207
- vitest.logger.error(`[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/browser/api.`);
2815
+ vitest.logger.error(`[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/api.`);
3208
2816
  }
3209
2817
  // remove attachments if cannot write
3210
2818
  if (artifact.attachments?.length) {
3211
2819
  const attachments = artifact.attachments.map((n) => n.path).filter((r) => !!r).join("\", \"");
3212
2820
  artifact.attachments = [];
3213
- vitest.logger.error(`[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/browser/api.`);
2821
+ vitest.logger.error(`[vitest] Cannot record attachments ("${attachments}") because file writing is disabled, removing attachments from artifact "${artifact.type}". See https://vitest.dev/config/api.`);
3214
2822
  }
3215
2823
  } else {
3216
2824
  // attachment files are copied into `attachmentsDir`, so confine
@@ -3234,7 +2842,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3234
2842
  },
3235
2843
  async writeBenchmarkResult(relativePath, data) {
3236
2844
  if (!canWrite(project)) {
3237
- vitest.logger.error(`[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/browser/api.`);
2845
+ vitest.logger.error(`[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/api.`);
3238
2846
  return;
3239
2847
  }
3240
2848
  checkFileAccess(project.benchmark.resolve(relativePath));
@@ -3276,7 +2884,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3276
2884
  async saveSnapshotFile(id, content) {
3277
2885
  checkFileAccess(id);
3278
2886
  if (!canWrite(project)) {
3279
- vitest.logger.error(`[vitest] Cannot save snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
2887
+ vitest.logger.error(`[vitest] Cannot save snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/api.`);
3280
2888
  return;
3281
2889
  }
3282
2890
  await promises.mkdir(dirname(id), { recursive: true });
@@ -3285,7 +2893,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3285
2893
  async removeSnapshotFile(id) {
3286
2894
  checkFileAccess(id);
3287
2895
  if (!canWrite(project)) {
3288
- vitest.logger.error(`[vitest] Cannot remove snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
2896
+ vitest.logger.error(`[vitest] Cannot remove snapshot file "${id}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/api.`);
3289
2897
  return;
3290
2898
  }
3291
2899
  if (!existsSync(id)) {
@@ -3410,6 +3018,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3410
3018
  }
3411
3019
  return mocker.delete(sessionId, id);
3412
3020
  },
3021
+ // CDP
3413
3022
  async sendCdpEvent(sessionId, event, payload) {
3414
3023
  assertCdpAllowed(project);
3415
3024
  const cdp = await globalServer.ensureCDPHandler(sessionId, rpcId);
@@ -3466,66 +3075,458 @@ function stringifyReplace(key, value) {
3466
3075
  function defineBrowserCommand(fn) {
3467
3076
  return fn;
3468
3077
  }
3469
- const createBrowserServer = async (options) => {
3470
- const project = options.project;
3471
- const configFile = project.vite.config.configFile;
3472
- if (project.vitest.version !== version) {
3473
- project.vitest.logger.warn(c.yellow(`Loaded ${c.inverse(c.yellow(` vitest@${project.vitest.version} `))} and ${c.inverse(c.yellow(` @vitest/browser@${version} `))}.` + "\nRunning mixed versions is not supported and may lead into bugs" + "\nUpdate your dependencies and make sure the versions match."));
3474
- }
3475
- const server = new ParentBrowserProject(project, "/");
3476
- const configPath = typeof configFile === "string" ? configFile : false;
3477
- const logLevel = process.env.VITEST_BROWSER_DEBUG ?? "info";
3478
- const logger = createViteLogger(project.vitest.logger, logLevel, { allowClearScreen: false });
3078
+ const versionRegexp = /(?:\?|&)v=\w{8}/;
3079
+ // pin a Cache-Control value and stop Vite's later middleware from overriding it
3080
+ function pinCacheControl(res, value) {
3081
+ res.setHeader("Cache-Control", value);
3082
+ const setHeader = res.setHeader.bind(res);
3083
+ res.setHeader = function(name, ...args) {
3084
+ if (name === "Cache-Control") {
3085
+ return res;
3086
+ }
3087
+ return setHeader(name, ...args);
3088
+ };
3089
+ }
3090
+ /**
3091
+ * The browser provider's `serverFactory`. Returns a `BrowserServerContribution`
3092
+ * that Vitest core uses to build the SINGLE Vite server shared by `project.vite`
3093
+ * and `project.browser.vite`. This factory does NOT create a server (core does);
3094
+ * it only contributes config, plugins, the parent factory, and the RPC setup.
3095
+ */
3096
+ const createBrowserServer = async () => {
3479
3097
  const mockerRegistry = new MockerRegistry();
3480
- let cacheDir;
3481
- const vite = await createViteServer({
3482
- ...project.options,
3483
- define: project.config.viteDefine,
3484
- base: "/",
3485
- root: project.config.root,
3486
- logLevel,
3487
- customLogger: {
3488
- ...logger,
3489
- info(msg, options) {
3490
- logger.info(msg, options);
3491
- if (msg.includes("optimized dependencies changed. reloading")) {
3492
- logger.warn([c.yellow(`\n${c.bold("[vitest]")} Vite unexpectedly reloaded a test. This may cause tests to fail, lead to flaky behaviour or duplicated test runs.\n`), c.yellow(`For a stable experience, please add mentioned dependencies to your config\'s ${c.bold("`optimizeDeps.include`")} field manually.\n\n`)].join(""));
3098
+ const contribution = {
3099
+ async transformIndexHtml(ctx) {
3100
+ const parentServer = contribution.parent;
3101
+ const projectBrowser = [...parentServer.children].find((server) => {
3102
+ return ctx.filename === server.testerFilepath;
3103
+ });
3104
+ if (!projectBrowser) {
3105
+ return;
3106
+ }
3107
+ const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
3108
+ const testerTags = [];
3109
+ const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
3110
+ if (!isDefaultTemplate) {
3111
+ const manifestContent = parentServer.manifest instanceof Promise ? await parentServer.manifest : parentServer.manifest;
3112
+ const testerEntry = manifestContent["tester/tester.html"];
3113
+ testerTags.push({
3114
+ tag: "script",
3115
+ attrs: {
3116
+ type: "module",
3117
+ crossorigin: "",
3118
+ src: `${parentServer.base}${testerEntry.file}`
3119
+ },
3120
+ injectTo: "head"
3121
+ });
3122
+ for (const importName of testerEntry.imports || []) {
3123
+ const entryManifest = manifestContent[importName];
3124
+ if (entryManifest) {
3125
+ testerTags.push({
3126
+ tag: "link",
3127
+ attrs: {
3128
+ href: `${parentServer.base}${entryManifest.file}`,
3129
+ rel: "modulepreload",
3130
+ crossorigin: ""
3131
+ },
3132
+ injectTo: "head"
3133
+ });
3134
+ }
3135
+ }
3136
+ } else {
3137
+ // inject the reset style only in the default template,
3138
+ // allowing users to customize the style in their own template
3139
+ testerTags.push({
3140
+ tag: "style",
3141
+ children: `
3142
+ html {
3143
+ padding: 0;
3144
+ margin: 0;
3145
+ }
3146
+ body {
3147
+ padding: 0;
3148
+ margin: 0;
3149
+ min-height: 100vh;
3150
+ }`,
3151
+ injectTo: "head"
3152
+ });
3153
+ }
3154
+ return [
3155
+ {
3156
+ tag: "script",
3157
+ children: "{__VITEST_INJECTOR__}",
3158
+ injectTo: "head-prepend"
3159
+ },
3160
+ {
3161
+ tag: "script",
3162
+ children: stateJs,
3163
+ injectTo: "head-prepend"
3164
+ },
3165
+ {
3166
+ tag: "script",
3167
+ attrs: {
3168
+ type: "module",
3169
+ src: parentServer.errorCatcherUrl
3170
+ },
3171
+ injectTo: "head"
3172
+ },
3173
+ {
3174
+ tag: "script",
3175
+ attrs: {
3176
+ type: "module",
3177
+ src: parentServer.matchersUrl
3178
+ },
3179
+ injectTo: "head"
3180
+ },
3181
+ ...parentServer.initScripts.map((script) => ({
3182
+ tag: "script",
3183
+ attrs: {
3184
+ type: "module",
3185
+ src: join("/@fs/", script)
3186
+ },
3187
+ injectTo: "head"
3188
+ })),
3189
+ ...testerTags
3190
+ ].filter((s) => s != null);
3191
+ },
3192
+ configureServer(server) {
3193
+ const parentServer = contribution.parent;
3194
+ parentServer.setServer(server);
3195
+ // eslint-disable-next-line prefer-arrow-callback
3196
+ server.middlewares.use(function vitestHeaders(_req, res, next) {
3197
+ const headers = server.config.server.headers;
3198
+ if (headers) {
3199
+ for (const name in headers) {
3200
+ res.setHeader(name, headers[name]);
3201
+ }
3202
+ }
3203
+ next();
3204
+ });
3205
+ // strip _vitest_original query added by importActual so that
3206
+ // the plugin pipeline sees the original import id (e.g. virtual modules's load hook).
3207
+ server.middlewares.use((req, _res, next) => {
3208
+ if (req.url?.includes("_vitest_original") && parentServer.config.browser.provider?.name === "playwright") {
3209
+ req.url = req.url.replace(/[?&]_vitest_original(?=[&#]|$)/, "").replace(/[?&]ext\b[^&#]*/, "").replace(/\?$/, "");
3210
+ }
3211
+ next();
3212
+ });
3213
+ server.middlewares.use(createOrchestratorMiddleware(parentServer));
3214
+ server.middlewares.use(createTesterMiddleware(parentServer));
3215
+ server.middlewares.use(`/favicon.svg`, (_, res) => {
3216
+ const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
3217
+ res.write(content, "utf-8");
3218
+ res.end();
3219
+ });
3220
+ // Serve coverage HTML at ./coverage if configured
3221
+ const coverageHtmlDir = parentServer.vitest.config.coverage?.htmlDir;
3222
+ if (coverageHtmlDir) {
3223
+ server.middlewares.use("/__vitest_test__/coverage", sirv(coverageHtmlDir, {
3224
+ single: true,
3225
+ dev: true,
3226
+ setHeaders: (res) => {
3227
+ const csp = res.getHeader("Content-Security-Policy");
3228
+ if (typeof csp === "string") {
3229
+ // add frame-ancestors to allow the iframe to be loaded by Vitest,
3230
+ // but keep the rest of the CSP
3231
+ res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
3232
+ }
3233
+ res.setHeader("Cache-Control", "public,max-age=0,must-revalidate");
3234
+ }
3235
+ }));
3236
+ }
3237
+ // vitest's own dist files only change with the vitest version and
3238
+ // browser contexts don't outlive the process, so there is no reason
3239
+ // to revalidate them in every tester iframe. Skipped for persistent
3240
+ // contexts and preview providers — their disk cache would survive a vitest upgrade
3241
+ const persistentContext = (parentServer.config.browser.provider?.options)?.persistentContext;
3242
+ const immutablePrefixes = persistentContext || parentServer.config.browser.provider?.name === "preview" ? [] : [
3243
+ "/__vitest_browser__/",
3244
+ `/@fs${distDir}`,
3245
+ `/@fs${distRoot}`
3246
+ ];
3247
+ server.middlewares.use((req, res, next) => {
3248
+ const url = req.url;
3249
+ if (url) {
3250
+ if (immutablePrefixes.some((prefix) => url.startsWith(prefix))) {
3251
+ pinCacheControl(res, "public,max-age=31536000,immutable");
3252
+ } else if (versionRegexp.test(url) && !url.includes("chunk-")) {
3253
+ pinCacheControl(res, "no-cache");
3254
+ }
3255
+ }
3256
+ next();
3257
+ });
3258
+ // handle attachments the same way as in packages/ui/node/index.ts
3259
+ server.middlewares.use((req, res, next) => {
3260
+ if (!req.url) {
3261
+ return next();
3262
+ }
3263
+ const url = new URL(req.url, "http://localhost");
3264
+ if (url.pathname !== "/__vitest_attachment__") {
3265
+ return next();
3266
+ }
3267
+ const path = url.searchParams.get("path");
3268
+ const contentType = url.searchParams.get("contentType");
3269
+ if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
3270
+ return next();
3271
+ }
3272
+ const fsPath = decodeURIComponent(path);
3273
+ if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
3274
+ return next();
3275
+ }
3276
+ try {
3277
+ res.setHeader("content-type", contentType);
3278
+ return createReadStream(fsPath).pipe(res).on("close", () => res.end());
3279
+ } catch (err) {
3280
+ return next(err);
3281
+ }
3282
+ });
3283
+ // When the Vitest UI (`test.ui`) is enabled, the `vitest:ui` plugin owns
3284
+ // `/__vitest__` (including the token-injected index.html and its assets),
3285
+ // so registering sirv here would shadow it and serve the page without a token.
3286
+ if (!parentServer.vitest.config.ui) {
3287
+ server.middlewares.use("/__vitest__", sirv(distClientRoot));
3288
+ }
3289
+ },
3290
+ // Resolution-time config: only what is derivable from the (partial) user
3291
+ // config. The mocks / coverage / meta-env plugins come from the shared
3292
+ // workspace plugin set, not from here.
3293
+ async config(viteConfig) {
3294
+ const testConfig = viteConfig.test || {};
3295
+ const config = {
3296
+ resolve: {
3297
+ alias: testConfig.alias,
3298
+ dedupe: ["vitest"]
3299
+ },
3300
+ server: {
3301
+ fs: { allow: [distRoot] },
3302
+ middlewareMode: false,
3303
+ watch: null,
3304
+ // Vitest forwards browser console logs and unhandled errors through its
3305
+ // own RPC, so Vite's client forwarding (defaults on under agents) would
3306
+ // double-report them and leak into stderr.
3307
+ forwardConsole: false
3493
3308
  }
3309
+ };
3310
+ // Enables using the @preserve ignore hint for coverage providers. Only for
3311
+ // esbuild (rolldown-vite uses oxc instead).
3312
+ if (!rolldownVersion && viteConfig.esbuild !== false) {
3313
+ config.esbuild = { legalComments: "inline" };
3314
+ }
3315
+ const define = {};
3316
+ const envVars = testConfig.env || {};
3317
+ for (const env in envVars) {
3318
+ define[`import.meta.env.${env}`] = JSON.stringify(envVars[env]);
3494
3319
  }
3320
+ config.define = define;
3321
+ return config;
3495
3322
  },
3496
- mode: project.config.mode,
3497
- configFile: configPath,
3498
- configLoader: project.vite.config.inlineConfig.configLoader,
3499
- server: {
3500
- ...project.options?.server,
3501
- hmr: false,
3502
- watch: null
3323
+ resolveOptimizeDeps(projectConfigs, testFiles, harness) {
3324
+ return resolveBrowserOptimizeDeps(projectConfigs, testFiles, harness);
3325
+ },
3326
+ plugins: [],
3327
+ createParent({ config, vitest }) {
3328
+ if (vitest.version !== version) {
3329
+ vitest.logger.warn(c.yellow(`Loaded ${c.inverse(c.yellow(` vitest@${vitest.version} `))} and ${c.inverse(c.yellow(` @vitest/browser@${version} `))}.` + "\nRunning mixed versions is not supported and may lead into bugs" + "\nUpdate your dependencies and make sure the versions match."));
3330
+ }
3331
+ return new ParentBrowserProject({
3332
+ config,
3333
+ vitest
3334
+ }, "/");
3503
3335
  },
3504
- cacheDir: project.vite.config.cacheDir,
3505
- plugins: [
3506
- {
3507
- name: "vitest-internal:browser-cacheDir",
3508
- configResolved(config) {
3509
- cacheDir = config.cacheDir;
3336
+ setupRpc(parent) {
3337
+ setupBrowserRpc(parent, mockerRegistry);
3338
+ }
3339
+ };
3340
+ contribution.plugins = [
3341
+ ...BrowserPlugin(contribution),
3342
+ interceptorPlugin({ registry: mockerRegistry }),
3343
+ {
3344
+ name: "vitest:browser:framework-sourcemaps",
3345
+ enforce: "post",
3346
+ transform(code, id) {
3347
+ const parentServer = contribution.parent;
3348
+ // In a headless run nothing can open devtools, so sourcemaps of
3349
+ // Vitest's own pre-built modules are never consumed: their stack
3350
+ // frames are filtered by stackIgnorePatterns. Generating and
3351
+ // inlining these maps costs server CPU and multiplies the bytes
3352
+ // the browser downloads by ~5 for every fresh browser context.
3353
+ // Sourcemaps of user files and (by default) their dependencies are
3354
+ // kept — they point error stacks and devtools at original sources.
3355
+ if (!parentServer || !isHeadlessServer(parentServer) || parentServer.vitest.config.inspector.enabled) {
3356
+ return null;
3510
3357
  }
3511
- },
3512
- ...options.mocksPlugins({ filter(id) {
3513
- if (id.includes(distRoot) || id.includes(cacheDir)) {
3514
- return false;
3358
+ if (isCSSRequest(id)) {
3359
+ return null;
3515
3360
  }
3516
- return true;
3517
- } }),
3518
- options.metaEnvReplacer(),
3519
- ...project.options?.plugins || [],
3520
- BrowserPlugin(server),
3521
- interceptorPlugin({ registry: mockerRegistry }),
3522
- options.coveragePlugin()
3523
- ]
3524
- });
3525
- await vite.listen();
3526
- setupBrowserRpc(server, mockerRegistry);
3527
- return server;
3361
+ const path = cleanUrl$1(id);
3362
+ if (path.startsWith(distRoot) || path.startsWith(distDir)) {
3363
+ return {
3364
+ code,
3365
+ map: { mappings: "" }
3366
+ };
3367
+ }
3368
+ // users that never debug into node_modules can drop dependency
3369
+ // sourcemaps entirely; `server.sourcemapIgnoreList` (default:
3370
+ // node_modules) can opt paths back in even then, e.g. with
3371
+ // `preserveSymlinks` where workspace code keeps its node_modules
3372
+ // path and would be wrongly treated as a dependency
3373
+ if (parentServer.config.browser.dependencySourcemaps === false && path.includes("/node_modules/") && (parentServer.vite.config.server.sourcemapIgnoreList(path, path) ?? true)) {
3374
+ return {
3375
+ code,
3376
+ map: { mappings: "" }
3377
+ };
3378
+ }
3379
+ return null;
3380
+ }
3381
+ }
3382
+ ];
3383
+ return contribution;
3528
3384
  };
3385
+ function isHeadlessServer(parentServer) {
3386
+ if (!parentServer.config.browser.headless) {
3387
+ return false;
3388
+ }
3389
+ // sibling instances share this server (and its module graph cache, so a
3390
+ // late-spawned instance would receive already-cached transforms) and can
3391
+ // override `headless` — check the static instance options instead of the
3392
+ // lazily populated `children` to stay deterministic across runs
3393
+ const instances = parentServer.config.browser.instances ?? [];
3394
+ return instances.every((instance) => instance.headless !== false);
3395
+ }
3396
+ function resolveBrowserOptimizeDeps(projectConfigs, testFiles, harness) {
3397
+ // `testFiles` are globbed by the core package and aggregated across every
3398
+ // project that shares this browser Vite server (instance and benchmark
3399
+ // variants). The remaining options are shared by those projects, so the first
3400
+ // config is representative.
3401
+ const testConfig = projectConfigs[0];
3402
+ const root = testConfig.root || process.cwd();
3403
+ const setupFiles = new Set(projectConfigs.flatMap((config) => toArray(config.setupFiles || [])));
3404
+ const entries = [
3405
+ ...testFiles,
3406
+ ...setupFiles,
3407
+ resolve(distDir, "index.js"),
3408
+ resolve(distDir, "browser.js"),
3409
+ resolve(distDir, "runners.js"),
3410
+ resolve(distDir, "utils.js"),
3411
+ ...testConfig.snapshotSerializers || []
3412
+ ];
3413
+ // Keep these external (never pre-bundle by optimizer):
3414
+ // - vitest/browser, @vitest/browser/context, @vitest/browser/utils are
3415
+ // VIRTUAL modules generated per-server (see pluginContext.ts) — optimizer
3416
+ // cannot resolve/run their `load`, it would freeze stale/empty content.
3417
+ // - msw is a large, side-effectful service-worker library.
3418
+ const exclude = [
3419
+ "vitest/browser",
3420
+ "@vitest/browser/utils",
3421
+ "@vitest/browser/context",
3422
+ "@vitest/browser/locators",
3423
+ "msw",
3424
+ "msw/browser"
3425
+ ];
3426
+ if (typeof testConfig.diff === "string") {
3427
+ entries.push(testConfig.diff);
3428
+ }
3429
+ if (testConfig.coverage?.enabled) {
3430
+ const provider = testConfig.coverage.provider ?? "v8";
3431
+ if (provider === "v8") {
3432
+ const path = tryResolve("@vitest/coverage-v8", [root]);
3433
+ if (path) {
3434
+ entries.push(path);
3435
+ exclude.push("@vitest/coverage-v8/browser");
3436
+ }
3437
+ } else if (provider === "istanbul") {
3438
+ const path = tryResolve("@vitest/coverage-istanbul", [root]);
3439
+ if (path) {
3440
+ entries.push(path);
3441
+ exclude.push("@vitest/coverage-istanbul");
3442
+ }
3443
+ } else if (provider === "custom" && testConfig.coverage.customProviderModule) {
3444
+ entries.push(testConfig.coverage.customProviderModule);
3445
+ }
3446
+ }
3447
+ // Pre-bundle the vitest runtime so the browser fetches a few optimized
3448
+ // chunks instead of ~20 separately-served dist chunks (faster startup).
3449
+ // `vitest`, `vitest/internal/browser` and `@vitest/browser/client` are
3450
+ // optimized together in a single pass, so esbuild dedupes their shared
3451
+ // stateful chunks (the test collector, the runner, the RPC client) to a
3452
+ // single instance — preserving module identity between the test files'
3453
+ // `import 'vitest'` and the tester. Their transitive deps (@vitest/utils,
3454
+ // @vitest/spy, pathe, tinyrainbow, …) are inlined into these bundles.
3455
+ const include = [
3456
+ "vitest > expect-type",
3457
+ "vitest > magic-string",
3458
+ "vitest > chai",
3459
+ "vitest > vite/module-runner",
3460
+ "vitest",
3461
+ "vitest/internal/browser",
3462
+ "vitest/internal/traces",
3463
+ "@vitest/browser/client"
3464
+ ];
3465
+ const provider = testConfig.browser?.provider;
3466
+ if (provider?.name === "preview") {
3467
+ include.push("@vitest/browser-preview > @testing-library/user-event", "@vitest/browser-preview > @testing-library/dom");
3468
+ }
3469
+ const isPackageExists = (pkg) => {
3470
+ return harness.packageInstaller.isPackageExists(pkg, { paths: [root] });
3471
+ };
3472
+ if (isPackageExists("vitest-browser-svelte")) {
3473
+ exclude.push("vitest-browser-svelte");
3474
+ }
3475
+ // since we override the resolution in the esbuild plugin, Vite can no longer optimize it
3476
+ if (isPackageExists("vitest-browser-vue")) {
3477
+ include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
3478
+ }
3479
+ if (isPackageExists("@vue/test-utils")) {
3480
+ include.push("@vue/test-utils");
3481
+ }
3482
+ const otelConfig = testConfig.experimental?.openTelemetry;
3483
+ if (otelConfig?.enabled && otelConfig.browserSdkPath) {
3484
+ entries.push(otelConfig.browserSdkPath);
3485
+ include.push("@opentelemetry/api");
3486
+ } else {
3487
+ exclude.push("@opentelemetry/api");
3488
+ }
3489
+ // resolve @vue/(test-utils|compiler-core) to CJS instead of the browser build:
3490
+ // test-utils' browser build expects a global Vue; compiler-core's CJS build is
3491
+ // the only one that allows slots as strings.
3492
+ const rolldownPlugin = {
3493
+ name: "vue-test-utils-rewrite",
3494
+ resolveId: {
3495
+ filter: { id: /^@vue\/(test-utils|compiler-core)$/ },
3496
+ handler(source, importer) {
3497
+ return getRequire().resolve(source, { paths: [importer] });
3498
+ }
3499
+ }
3500
+ };
3501
+ const esbuildPlugin = {
3502
+ name: "test-utils-rewrite",
3503
+ setup(build) {
3504
+ build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
3505
+ return { path: getRequire().resolve(args.path, { paths: [args.importer] }) };
3506
+ });
3507
+ }
3508
+ };
3509
+ return {
3510
+ entries,
3511
+ exclude,
3512
+ include,
3513
+ ...rolldownVersion ? { rolldownOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } }
3514
+ };
3515
+ }
3516
+ function tryResolve(path, paths) {
3517
+ try {
3518
+ return getRequire().resolve(path, { paths });
3519
+ } catch {
3520
+ return undefined;
3521
+ }
3522
+ }
3523
+ let _require;
3524
+ function getRequire() {
3525
+ if (!_require) {
3526
+ _require = createRequire(import.meta.url);
3527
+ }
3528
+ return _require;
3529
+ }
3529
3530
  function defineBrowserProvider(options) {
3530
3531
  return {
3531
3532
  ...options,