@vitest/browser 5.0.0-beta.5 → 5.0.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,15 +1,15 @@
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 { isValidApiRequest, isFileServingAllowed, distDir, rolldownVersion, resolveApiServerConfig, resolveFsAllow, isFileLoadingAllowed, 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';
12
+ import MagicString from 'magic-string';
13
13
  import { readFile as readFile$1, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
14
14
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
15
15
  import { extractSourcemapFromFile } from '@vitest/utils/source-map/node';
@@ -19,8 +19,6 @@ import { PNG } from 'pngjs';
19
19
  import { diff } from '@blazediff/core';
20
20
  import { WebSocketServer } from 'ws';
21
21
 
22
- var version = "5.0.0-beta.5";
23
-
24
22
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
25
23
  function normalizeWindowsPath(input = "") {
26
24
  if (!input) {
@@ -211,6 +209,8 @@ const basename = function(p, extension) {
211
209
  return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
212
210
  };
213
211
 
212
+ var version = "5.0.0-beta.7";
213
+
214
214
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
215
215
  const distRoot = resolve(pkgRoot, "dist");
216
216
 
@@ -675,6 +675,17 @@ async function getBrowserProvider(options, project) {
675
675
  function slash(path) {
676
676
  return path.replace(/\\/g, "/").replace(/\/+/g, "/");
677
677
  }
678
+ function assertBrowserFileAccess(project, path) {
679
+ const normalized = slash(path);
680
+ if (!isFileLoadingAllowed(project.vite.config, normalized) && !isFileLoadingAllowed(project.vitest.vite.config, normalized)) {
681
+ throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
682
+ }
683
+ }
684
+ function assertBrowserApiWrite(project, path) {
685
+ if (!project.config.api.allowWrite || !project.vitest.config.api.allowWrite) {
686
+ 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.`);
687
+ }
688
+ }
678
689
 
679
690
  async function resolveOrchestrator(globalServer, url, res) {
680
691
  const sessionId = url.searchParams.get("sessionId");
@@ -802,16 +813,21 @@ async function resolveTester(globalServer, url, res, next) {
802
813
  __VITEST_PROVIDED_CONTEXT__: "{}",
803
814
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
804
815
  });
805
- const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
806
816
  try {
807
- const url = join("/@fs/", browserProject.testerFilepath);
808
- const indexhtml = await browserProject.vite.transformIndexHtml(url, testerHtml);
817
+ browserProject.testerHtmlTransformed ??= (async () => {
818
+ const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
819
+ const url = join("/@fs/", browserProject.testerFilepath);
820
+ return await browserProject.vite.transformIndexHtml(url, testerHtml);
821
+ })();
822
+ const indexhtml = await browserProject.testerHtmlTransformed;
809
823
  const html = replacer(indexhtml, {
810
824
  __VITEST_FAVICON__: globalServer.faviconUrl,
811
825
  __VITEST_INJECTOR__: injector
812
826
  });
813
827
  return html;
814
828
  } catch (err) {
829
+ // don't cache a rejection — the next request should retry the transform
830
+ browserProject.testerHtmlTransformed = undefined;
815
831
  session.fail(err);
816
832
  next(err);
817
833
  }
@@ -838,49 +854,41 @@ function createTesterMiddleware(browserServer) {
838
854
 
839
855
  const VIRTUAL_ID_CONTEXT = "\0vitest/browser";
840
856
  const ID_CONTEXT = "vitest/browser";
841
- // for libraries that use an older import but are not type checked
842
- const DEPRECATED_ID_CONTEXT = "@vitest/browser/context";
843
- const DEPRECATED_VIRTUAL_ID_UTILS = "\0@vitest/browser/utils";
844
- const DEPRECATED_ID_UTILS = "@vitest/browser/utils";
845
857
  const __dirname$1 = dirname(fileURLToPath(import.meta.url));
846
- function BrowserContext(globalServer) {
858
+ function BrowserContext(contribution) {
847
859
  return {
848
860
  name: "vitest:browser:virtual-module:context",
849
861
  enforce: "pre",
850
- resolveId(id, importer) {
851
- if (id === ID_CONTEXT) {
852
- return VIRTUAL_ID_CONTEXT;
853
- }
854
- if (id === DEPRECATED_ID_CONTEXT) {
855
- if (importer && !importer.includes("/node_modules/")) {
856
- 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.`);
862
+ resolveId: {
863
+ order: "pre",
864
+ handler(id) {
865
+ if (id === ID_CONTEXT) {
866
+ return VIRTUAL_ID_CONTEXT;
857
867
  }
858
- return VIRTUAL_ID_CONTEXT;
859
- }
860
- if (id === DEPRECATED_ID_UTILS) {
861
- return DEPRECATED_VIRTUAL_ID_UTILS;
862
868
  }
863
869
  },
864
870
  load(id) {
871
+ const globalServer = contribution.parent;
865
872
  if (id === VIRTUAL_ID_CONTEXT) {
866
873
  return generateContextFile.call(this, globalServer);
867
874
  }
868
- if (id === DEPRECATED_VIRTUAL_ID_UTILS) {
869
- return `
870
- import { utils } from 'vitest/browser'
871
- export const getElementLocatorSelectors = utils.getElementLocatorSelectors
872
- export const debug = utils.debug
873
- export const prettyDOM = utils.prettyDOM
874
- export const getElementError = utils.getElementError
875
- `;
876
- }
877
875
  }
878
876
  };
879
877
  }
880
878
  async function generateContextFile(globalServer) {
881
879
  const commands = Object.keys(globalServer.commands);
882
- const provider = [...globalServer.children][0].provider;
883
- const providerName = provider?.name || "preview";
880
+ // The provider instance is initialized lazily when a page opens, so reading
881
+ // `child.provider` here races and can be `undefined` before the first page is
882
+ // opened. The provider name is uniform across a project's instances and is
883
+ // already resolved on the config, so read it from there for this shared
884
+ // (cached) context module. The instance is still used below for the
885
+ // preview-only user-event import.
886
+ const child = [...globalServer.children][0];
887
+ const provider = child.provider;
888
+ const providerName = child.config.browser.provider?.name;
889
+ if (!providerName) {
890
+ 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.`);
891
+ }
884
892
  const commandsCode = commands.filter((command) => !command.startsWith("__vitest")).map((command) => {
885
893
  return ` ["${command}"]: (...args) => __vitest_browser_runner__.commands.triggerCommand("${command}", args),`;
886
894
  }).join("\n");
@@ -915,256 +923,41 @@ async function getUserEventImport(provider, resolve) {
915
923
  throw new Error(`Failed to resolve user-event package from ${previewDistRoot}`);
916
924
  }
917
925
  return `\
918
- import { userEvent as __vitest_user_event__ } from '${slash$1(`/@fs/${resolved.id}`)}'
919
- const _userEventSetup = __vitest_user_event__
926
+ import { userEvent as _userEventSetup } from '${slash$1(`/@fs/${resolved.id}`)}'
920
927
  `;
921
928
  }
922
929
 
923
- const versionRegexp = /(?:\?|&)v=\w{8}/;
924
- var BrowserPlugin = (parentServer, base = "/") => {
925
- function isPackageExists(pkg, root) {
926
- return parentServer.vitest.packageInstaller.isPackageExists?.(pkg, { paths: [root] });
927
- }
930
+ var BrowserPlugin = (contribution) => {
928
931
  return [
929
- {
930
- enforce: "pre",
931
- name: "vitest:browser",
932
- async configureServer(server) {
933
- parentServer.setServer(server);
934
- // eslint-disable-next-line prefer-arrow-callback
935
- server.middlewares.use(function vitestHeaders(_req, res, next) {
936
- const headers = server.config.server.headers;
937
- if (headers) {
938
- for (const name in headers) {
939
- res.setHeader(name, headers[name]);
940
- }
941
- }
942
- next();
943
- });
944
- // strip _vitest_original query added by importActual so that
945
- // the plugin pipeline sees the original import id (e.g. virtual modules's load hook).
946
- server.middlewares.use((req, _res, next) => {
947
- if (req.url?.includes("_vitest_original") && parentServer.project.config.browser.provider?.name === "playwright") {
948
- req.url = req.url.replace(/[?&]_vitest_original(?=[&#]|$)/, "").replace(/[?&]ext\b[^&#]*/, "").replace(/\?$/, "");
949
- }
950
- next();
951
- });
952
- server.middlewares.use(createOrchestratorMiddleware(parentServer));
953
- server.middlewares.use(createTesterMiddleware(parentServer));
954
- server.middlewares.use(`${base}favicon.svg`, (_, res) => {
955
- const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
956
- res.write(content, "utf-8");
957
- res.end();
958
- });
959
- // Serve coverage HTML at ./coverage if configured
960
- const coverageHtmlDir = parentServer.vitest.config.coverage?.htmlDir;
961
- if (coverageHtmlDir) {
962
- server.middlewares.use("/__vitest_test__/coverage", sirv(coverageHtmlDir, {
963
- single: true,
964
- dev: true,
965
- setHeaders: (res) => {
966
- const csp = res.getHeader("Content-Security-Policy");
967
- if (typeof csp === "string") {
968
- // add frame-ancestors to allow the iframe to be loaded by Vitest,
969
- // but keep the rest of the CSP
970
- res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
971
- }
972
- res.setHeader("Cache-Control", "public,max-age=0,must-revalidate");
973
- }
974
- }));
975
- }
976
- server.middlewares.use((req, res, next) => {
977
- // 9000 mega head move
978
- // Vite always caches optimized dependencies, but users might mock
979
- // them in _some_ tests, while keeping original modules in others
980
- // there is no way to configure that in Vite, so we patch it here
981
- // to always ignore the cache-control set by Vite in the next middleware
982
- if (req.url && versionRegexp.test(req.url) && !req.url.includes("chunk-")) {
983
- res.setHeader("Cache-Control", "no-cache");
984
- const setHeader = res.setHeader.bind(res);
985
- res.setHeader = function(name, value) {
986
- if (name === "Cache-Control") {
987
- return res;
988
- }
989
- return setHeader(name, value);
990
- };
991
- }
992
- next();
993
- });
994
- // handle attachments the same way as in packages/ui/node/index.ts
995
- server.middlewares.use((req, res, next) => {
996
- if (!req.url) {
997
- return next();
998
- }
999
- const url = new URL(req.url, "http://localhost");
1000
- if (url.pathname !== "/__vitest_attachment__") {
1001
- return next();
1002
- }
1003
- const path = url.searchParams.get("path");
1004
- const contentType = url.searchParams.get("contentType");
1005
- if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
1006
- return next();
1007
- }
1008
- const fsPath = decodeURIComponent(path);
1009
- if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
1010
- return next();
1011
- }
1012
- try {
1013
- res.setHeader("content-type", contentType);
1014
- return createReadStream(fsPath).pipe(res).on("close", () => res.end());
1015
- } catch (err) {
1016
- return next(err);
1017
- }
1018
- });
1019
- }
1020
- },
1021
932
  {
1022
933
  name: "vitest:browser:tests",
1023
934
  enforce: "pre",
1024
- async config() {
1025
- // this plugin can be used in different projects, but all of them
1026
- // have the same `include` pattern, so it doesn't matter which project we use
1027
- const project = parentServer.project;
1028
- // only glob benchmarks when a browser-enabled bench project exists in
1029
- // the workspace — keeps the optimizeDeps entries symmetrical across
1030
- // the test/bench project clones so the optimizer doesn't re-scan when
1031
- // the user switches modes
1032
- const hasBrowserBenchProject = parentServer.vitest.projects.some((p) => p.config.browser.enabled && p.config.benchmark.enabled);
1033
- const benchInclude = hasBrowserBenchProject ? project.config.benchmark.include : [];
1034
- const dir = project.config.dir || project.config.root;
1035
- const [{ testFiles: browserTestFiles }, browserBenchFiles] = await Promise.all([project.globTestFiles(), benchInclude.length > 0 ? project.globFiles(benchInclude, project.config.benchmark.exclude ?? project.config.exclude, dir) : []]);
1036
- const setupFiles = toArray(project.config.setupFiles);
1037
- // replace env values - cannot be reassign at runtime
1038
- const define = {};
1039
- for (const env in project.config.env || {}) {
1040
- const stringValue = JSON.stringify(project.config.env[env]);
1041
- define[`import.meta.env.${env}`] = stringValue;
1042
- }
1043
- const entries = [
1044
- ...new Set([...browserTestFiles, ...browserBenchFiles]),
1045
- ...setupFiles,
1046
- resolve(distDir, "index.js"),
1047
- resolve(distDir, "browser.js"),
1048
- resolve(distDir, "runners.js"),
1049
- resolve(distDir, "utils.js"),
1050
- ...project.config.snapshotSerializers || []
1051
- ];
1052
- const exclude = [
1053
- "vitest",
1054
- "vitest/browser",
1055
- "vitest/internal/browser",
1056
- "vite/module-runner",
1057
- "@vitest/browser/utils",
1058
- "@vitest/browser/context",
1059
- "@vitest/browser/client",
1060
- "@vitest/utils",
1061
- "@vitest/utils/source-map",
1062
- "@vitest/spy",
1063
- "@vitest/utils/error",
1064
- "std-env",
1065
- "tinybench",
1066
- "tinyspy",
1067
- "tinyrainbow",
1068
- "pathe",
1069
- "msw",
1070
- "msw/browser"
1071
- ];
1072
- if (typeof project.config.diff === "string") {
1073
- entries.push(project.config.diff);
1074
- }
1075
- if (parentServer.vitest.coverageProvider) {
1076
- const coverage = parentServer.vitest._coverageOptions;
1077
- const provider = coverage.provider;
1078
- if (provider === "v8") {
1079
- const path = tryResolve("@vitest/coverage-v8", [parentServer.config.root]);
1080
- if (path) {
1081
- entries.push(path);
1082
- exclude.push("@vitest/coverage-v8/browser");
1083
- }
1084
- } else if (provider === "istanbul") {
1085
- const path = tryResolve("@vitest/coverage-istanbul", [parentServer.config.root]);
1086
- if (path) {
1087
- entries.push(path);
1088
- exclude.push("@vitest/coverage-istanbul");
1089
- }
1090
- } else if (provider === "custom" && coverage.customProviderModule) {
1091
- entries.push(coverage.customProviderModule);
935
+ resolveId: {
936
+ order: "pre",
937
+ handler(id) {
938
+ if (!/\?browserv=\w+$/.test(id)) {
939
+ return;
1092
940
  }
1093
- }
1094
- const include = [
1095
- "vitest > expect-type",
1096
- "vitest > magic-string",
1097
- "vitest > chai"
1098
- ];
1099
- const provider = parentServer.config.browser.provider || [...parentServer.children][0]?.provider;
1100
- if (provider?.name === "preview") {
1101
- include.push("@vitest/browser-preview > @testing-library/user-event", "@vitest/browser-preview > @testing-library/dom");
1102
- }
1103
- const fileRoot = browserTestFiles[0] ? dirname(browserTestFiles[0]) : project.config.root;
1104
- const svelte = isPackageExists("vitest-browser-svelte", fileRoot);
1105
- if (svelte) {
1106
- exclude.push("vitest-browser-svelte");
1107
- }
1108
- // since we override the resolution in the esbuild plugin, Vite can no longer optimizer it
1109
- const vue = isPackageExists("vitest-browser-vue", fileRoot);
1110
- if (vue) {
1111
- // we override them in the esbuild plugin so optimizer can no longer intercept it
1112
- include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
1113
- }
1114
- const vueTestUtils = isPackageExists("@vue/test-utils", fileRoot);
1115
- if (vueTestUtils) {
1116
- include.push("@vue/test-utils");
1117
- }
1118
- const otelConfig = project.config.experimental.openTelemetry;
1119
- if (otelConfig?.enabled && otelConfig.browserSdkPath) {
1120
- entries.push(otelConfig.browserSdkPath);
1121
- include.push("@opentelemetry/api");
1122
- } else {
1123
- exclude.push("@opentelemetry/api");
1124
- }
1125
- return {
1126
- define,
1127
- resolve: { dedupe: ["vitest"] },
1128
- optimizeDeps: {
1129
- entries,
1130
- exclude,
1131
- include
941
+ let useId = id.slice(0, id.lastIndexOf("?"));
942
+ if (useId.startsWith("/@fs/")) {
943
+ useId = useId.slice(5);
1132
944
  }
1133
- };
1134
- },
1135
- async resolveId(id) {
1136
- if (!/\?browserv=\w+$/.test(id)) {
1137
- return;
1138
- }
1139
- let useId = id.slice(0, id.lastIndexOf("?"));
1140
- if (useId.startsWith("/@fs/")) {
1141
- useId = useId.slice(5);
1142
- }
1143
- if (/^\w:/.test(useId)) {
1144
- useId = useId.replace(/\\/g, "/");
1145
- }
1146
- return useId;
1147
- }
1148
- },
1149
- {
1150
- name: "vitest:browser:resolve-virtual",
1151
- async resolveId(rawId) {
1152
- if (rawId === "/mockServiceWorker.js") {
1153
- return this.resolve("msw/mockServiceWorker.js", distRoot, { skipSelf: true });
945
+ if (/^\w:/.test(useId)) {
946
+ useId = useId.replace(/\\/g, "/");
947
+ }
948
+ return useId;
1154
949
  }
1155
950
  }
1156
951
  },
1157
952
  {
1158
953
  name: "vitest:browser:assets",
1159
- configureServer(server) {
1160
- server.middlewares.use("/__vitest__", sirv(distClientRoot));
1161
- },
1162
954
  resolveId(id) {
1163
955
  if (id.startsWith("/__vitest_browser__/")) {
1164
956
  return resolve(distRoot, "client", id.slice(1));
1165
957
  }
1166
958
  },
1167
959
  transform(code, id) {
960
+ const parentServer = contribution.parent;
1168
961
  if (id.includes(parentServer.vite.config.cacheDir) && id.includes("loupe.js")) {
1169
962
  // loupe bundle has a nasty require('util') call that leaves a warning in the console
1170
963
  const utilRequire = "nodeUtil = require_util();";
@@ -1172,45 +965,16 @@ var BrowserPlugin = (parentServer, base = "/") => {
1172
965
  }
1173
966
  }
1174
967
  },
1175
- BrowserContext(parentServer),
968
+ BrowserContext(contribution),
1176
969
  dynamicImportPlugin({
1177
970
  globalThisAccessor: "\"__vitest_browser_runner__\"",
1178
- filter(id, environment) {
1179
- if (environment.name !== "client") {
1180
- return false;
1181
- }
971
+ filter(id) {
1182
972
  if (id.includes(distRoot)) {
1183
973
  return false;
1184
974
  }
1185
975
  return true;
1186
976
  }
1187
977
  }),
1188
- {
1189
- name: "vitest:browser:config",
1190
- enforce: "post",
1191
- async config(viteConfig) {
1192
- // Enables using ignore hint for coverage providers with @preserve keyword
1193
- // Only set esbuild options when not using rolldown-vite (Vite 8+),
1194
- // which uses oxc for transformation instead of esbuild
1195
- if (!rolldownVersion && viteConfig.esbuild !== false) {
1196
- viteConfig.esbuild ||= {};
1197
- viteConfig.esbuild.legalComments = "inline";
1198
- }
1199
- const defaultPort = parentServer.vitest.state._data.browserLastPort++;
1200
- const api = resolveApiServerConfig(viteConfig.test?.browser || {}, defaultPort);
1201
- viteConfig.server = {
1202
- ...viteConfig.server,
1203
- port: defaultPort,
1204
- ...api,
1205
- middlewareMode: false,
1206
- open: false
1207
- };
1208
- viteConfig.server.fs ??= {};
1209
- viteConfig.server.fs.allow = viteConfig.server.fs.allow || [];
1210
- viteConfig.server.fs.allow.push(...resolveFsAllow(parentServer.vitest.config.root, parentServer.vitest.vite.config.configFile), distRoot);
1211
- return { resolve: { alias: viteConfig.test?.alias } };
1212
- }
1213
- },
1214
978
  {
1215
979
  name: "vitest:browser:in-source-tests",
1216
980
  transform: {
@@ -1243,130 +1007,6 @@ var BrowserPlugin = (parentServer, base = "/") => {
1243
1007
  }
1244
1008
  }
1245
1009
  },
1246
- {
1247
- name: "vitest:browser:transform-tester-html",
1248
- enforce: "pre",
1249
- async transformIndexHtml(html, ctx) {
1250
- const projectBrowser = [...parentServer.children].find((server) => {
1251
- return ctx.filename === server.testerFilepath;
1252
- });
1253
- if (!projectBrowser) {
1254
- return;
1255
- }
1256
- const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
1257
- const testerTags = [];
1258
- const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
1259
- if (!isDefaultTemplate) {
1260
- const manifestContent = parentServer.manifest instanceof Promise ? await parentServer.manifest : parentServer.manifest;
1261
- const testerEntry = manifestContent["tester/tester.html"];
1262
- testerTags.push({
1263
- tag: "script",
1264
- attrs: {
1265
- type: "module",
1266
- crossorigin: "",
1267
- src: `${parentServer.base}${testerEntry.file}`
1268
- },
1269
- injectTo: "head"
1270
- });
1271
- for (const importName of testerEntry.imports || []) {
1272
- const entryManifest = manifestContent[importName];
1273
- if (entryManifest) {
1274
- testerTags.push({
1275
- tag: "link",
1276
- attrs: {
1277
- href: `${parentServer.base}${entryManifest.file}`,
1278
- rel: "modulepreload",
1279
- crossorigin: ""
1280
- },
1281
- injectTo: "head"
1282
- });
1283
- }
1284
- }
1285
- } else {
1286
- // inject the reset style only in the default template,
1287
- // allowing users to customize the style in their own template
1288
- testerTags.push({
1289
- tag: "style",
1290
- children: `
1291
- html {
1292
- padding: 0;
1293
- margin: 0;
1294
- }
1295
- body {
1296
- padding: 0;
1297
- margin: 0;
1298
- min-height: 100vh;
1299
- }`,
1300
- injectTo: "head"
1301
- });
1302
- }
1303
- return [
1304
- {
1305
- tag: "script",
1306
- children: "{__VITEST_INJECTOR__}",
1307
- injectTo: "head-prepend"
1308
- },
1309
- {
1310
- tag: "script",
1311
- children: stateJs,
1312
- injectTo: "head-prepend"
1313
- },
1314
- {
1315
- tag: "script",
1316
- attrs: {
1317
- type: "module",
1318
- src: parentServer.errorCatcherUrl
1319
- },
1320
- injectTo: "head"
1321
- },
1322
- {
1323
- tag: "script",
1324
- attrs: {
1325
- type: "module",
1326
- src: parentServer.matchersUrl
1327
- },
1328
- injectTo: "head"
1329
- },
1330
- ...parentServer.initScripts.map((script) => ({
1331
- tag: "script",
1332
- attrs: {
1333
- type: "module",
1334
- src: join("/@fs/", script)
1335
- },
1336
- injectTo: "head"
1337
- })),
1338
- ...testerTags
1339
- ].filter((s) => s != null);
1340
- }
1341
- },
1342
- {
1343
- name: "vitest:browser:support-testing-library",
1344
- enforce: "pre",
1345
- config() {
1346
- const rolldownPlugin = {
1347
- name: "vue-test-utils-rewrite",
1348
- resolveId: {
1349
- filter: { id: /^@vue\/(test-utils|compiler-core)$/ },
1350
- handler(source, importer) {
1351
- const resolved = getRequire().resolve(source, { paths: [importer] });
1352
- return resolved;
1353
- }
1354
- }
1355
- };
1356
- const esbuildPlugin = {
1357
- name: "test-utils-rewrite",
1358
- setup(build) {
1359
- // test-utils: resolve to CJS instead of the browser because the browser version expects a global Vue object
1360
- // compiler-core: only CJS version allows slots as strings
1361
- build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
1362
- const resolved = getRequire().resolve(args.path, { paths: [args.importer] });
1363
- return { path: resolved };
1364
- });
1365
- }
1366
- };
1367
- return { optimizeDeps: rolldownVersion ? { rolldownOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } } };
1368
- }
1369
- },
1370
1010
  {
1371
1011
  name: "vitest:browser:__vitest_browser_import_meta_env_init__",
1372
1012
  transform: { handler(code) {
@@ -1379,21 +1019,6 @@ body {
1379
1019
  }
1380
1020
  ];
1381
1021
  };
1382
- function tryResolve(path, paths) {
1383
- try {
1384
- const _require = getRequire();
1385
- return _require.resolve(path, { paths });
1386
- } catch {
1387
- return undefined;
1388
- }
1389
- }
1390
- let _require;
1391
- function getRequire() {
1392
- if (!_require) {
1393
- _require = createRequire(import.meta.url);
1394
- }
1395
- return _require;
1396
- }
1397
1022
  const postfixRE = /[?#].*$/;
1398
1023
  function cleanUrl(url) {
1399
1024
  return url.replace(postfixRE, "");
@@ -1439,19 +1064,6 @@ class BrowserServerCDPHandler {
1439
1064
  }
1440
1065
  }
1441
1066
 
1442
- const _startV8Coverage = async (context) => {
1443
- const session = await context.__ensureCDPHandler();
1444
- await session.send("Profiler.enable");
1445
- await session.send("Profiler.startPreciseCoverage", {
1446
- callCount: true,
1447
- detailed: true
1448
- });
1449
- };
1450
- const _takeV8Coverage = async (context) => {
1451
- const session = await context.__ensureCDPHandler();
1452
- return session.send("Profiler.takePreciseCoverage");
1453
- };
1454
-
1455
1067
  const types = {
1456
1068
  'application/andrew-inset': ['ez'],
1457
1069
  'application/appinstaller': ['appinstaller'],
@@ -1904,19 +1516,9 @@ _Mime_extensionToType = new WeakMap(), _Mime_typeToExtension = new WeakMap(), _M
1904
1516
 
1905
1517
  var mime = new Mime(types)._freeze();
1906
1518
 
1907
- function assertFileAccess(path, project) {
1908
- if (!isFileLoadingAllowed(project.vite.config, path) && !isFileLoadingAllowed(project.vitest.vite.config, path)) {
1909
- throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
1910
- }
1911
- }
1912
- function assertWrite(path, project) {
1913
- if (!project.config.browser.api.allowWrite || !project.vitest.config.api.allowWrite) {
1914
- throw new Error(`Cannot modify file "${path}". File writing is disabled because server is exposed to the internet, see https://vitest.dev/config/browser/api.`);
1915
- }
1916
- }
1917
1519
  const readFile = async ({ project }, path, options = {}) => {
1918
1520
  const filepath = resolve$1(project.config.root, path);
1919
- assertFileAccess(slash(filepath), project);
1521
+ assertBrowserFileAccess(project, filepath);
1920
1522
  // never return a Buffer
1921
1523
  if (typeof options === "object" && !options.encoding) {
1922
1524
  options.encoding = "utf-8";
@@ -1924,9 +1526,9 @@ const readFile = async ({ project }, path, options = {}) => {
1924
1526
  return promises.readFile(filepath, options);
1925
1527
  };
1926
1528
  const writeFile = async ({ project }, path, data, options) => {
1927
- assertWrite(path, project);
1529
+ assertBrowserApiWrite(project, path);
1928
1530
  const filepath = resolve$1(project.config.root, path);
1929
- assertFileAccess(slash(filepath), project);
1531
+ assertBrowserFileAccess(project, filepath);
1930
1532
  const dir = dirname$1(filepath);
1931
1533
  if (!fs.existsSync(dir)) {
1932
1534
  await promises.mkdir(dir, { recursive: true });
@@ -1934,14 +1536,14 @@ const writeFile = async ({ project }, path, data, options) => {
1934
1536
  await promises.writeFile(filepath, data, options);
1935
1537
  };
1936
1538
  const removeFile = async ({ project }, path) => {
1937
- assertWrite(path, project);
1539
+ assertBrowserApiWrite(project, path);
1938
1540
  const filepath = resolve$1(project.config.root, path);
1939
- assertFileAccess(slash(filepath), project);
1541
+ assertBrowserFileAccess(project, filepath);
1940
1542
  await promises.rm(filepath);
1941
1543
  };
1942
1544
  const _fileInfo = async ({ project }, path, encoding) => {
1943
1545
  const filepath = resolve$1(project.config.root, path);
1944
- assertFileAccess(slash(filepath), project);
1546
+ assertBrowserFileAccess(project, filepath);
1945
1547
  const content = await promises.readFile(filepath, encoding || "base64");
1946
1548
  return {
1947
1549
  content,
@@ -2106,7 +1708,7 @@ function resolveOptions({ context, name, options, testName }) {
2106
1708
  ext: `.${extension}`,
2107
1709
  platform: platform(),
2108
1710
  root,
2109
- screenshotDirectory: relative(root, join(root, context.project.config.browser.screenshotDirectory ?? "__screenshots__")),
1711
+ screenshotDirectory: context.project.config.browser.expect?.toMatchScreenshot?.screenshotDirectory ?? "__screenshots__",
2110
1712
  attachmentsDir: relative(root, context.project.config.attachmentsDir),
2111
1713
  testFileDirectory: relative(root, dirname(context.testPath)),
2112
1714
  testFileName: basename(context.testPath),
@@ -2213,7 +1815,7 @@ function asyncTimeout(timeout) {
2213
1815
  if (timeout === 0) {
2214
1816
  resolve(null);
2215
1817
  } else {
2216
- setTimeout(() => resolve(null), timeout);
1818
+ setTimeout(resolve, timeout, null);
2217
1819
  }
2218
1820
  });
2219
1821
  }
@@ -2287,7 +1889,7 @@ const screenshotMatcher = async (context, name, testName, options) => {
2287
1889
  comparator,
2288
1890
  comparatorOptions
2289
1891
  });
2290
- await performSideEffects(outcome, codec);
1892
+ await performSideEffects(outcome, codec, context.project);
2291
1893
  return buildOutput(outcome, timeout);
2292
1894
  };
2293
1895
  /**
@@ -2377,17 +1979,17 @@ async function determineOutcome({ comparator, comparatorOptions, paths, referenc
2377
1979
  *
2378
1980
  * Only `missing-reference`, `update-reference`, and `mismatch` write files. Successful matches produce no side effects.
2379
1981
  */
2380
- async function performSideEffects(outcome, codec) {
1982
+ async function performSideEffects(outcome, codec, project) {
2381
1983
  switch (outcome.type) {
2382
1984
  case "missing-reference":
2383
1985
  case "update-reference": {
2384
- await writeScreenshot(outcome.reference.path, await encodeScreenshot(outcome.reference, codec));
1986
+ await writeScreenshot(outcome.reference.path, await encodeScreenshot(outcome.reference, codec), project);
2385
1987
  break;
2386
1988
  }
2387
1989
  case "mismatch": {
2388
- await writeScreenshot(outcome.actual.path, await encodeScreenshot(outcome.actual, codec));
1990
+ await writeScreenshot(outcome.actual.path, await encodeScreenshot(outcome.actual, codec), project);
2389
1991
  if (outcome.diff) {
2390
- await writeScreenshot(outcome.diff.path, await codec.encode(outcome.diff.image, {}));
1992
+ await writeScreenshot(outcome.diff.path, await codec.encode(outcome.diff.image, {}), project);
2391
1993
  }
2392
1994
  break;
2393
1995
  }
@@ -2549,8 +2151,10 @@ async function takeScreenshotData({ buffer, codec, context, element, name, scree
2549
2151
  };
2550
2152
  }
2551
2153
  /** Writes encoded images to disk, creating parent directories as needed. */
2552
- async function writeScreenshot(path, image) {
2154
+ async function writeScreenshot(path, image, project) {
2553
2155
  try {
2156
+ assertBrowserApiWrite(project, path);
2157
+ assertBrowserFileAccess(project, path);
2554
2158
  await mkdir(dirname(path), { recursive: true });
2555
2159
  await writeFile$1(path, image);
2556
2160
  } catch (cause) {
@@ -2598,8 +2202,6 @@ var builtinCommands = {
2598
2202
  readFile,
2599
2203
  removeFile,
2600
2204
  writeFile,
2601
- __vitest_startV8Coverage: _startV8Coverage,
2602
- __vitest_takeV8Coverage: _takeV8Coverage,
2603
2205
  __vitest_markTrace: _markTrace,
2604
2206
  __vitest_groupTraceStart: _groupTraceStart,
2605
2207
  __vitest_groupTraceEnd: _groupTraceEnd,
@@ -2617,6 +2219,10 @@ class BrowserServerState {
2617
2219
  class ProjectBrowser {
2618
2220
  testerHtml;
2619
2221
  testerFilepath;
2222
+ // the tester template is read once per server, so the transformed HTML
2223
+ // is stable too — computing it for every iframe request pays the whole
2224
+ // transformIndexHtml plugin pipeline each time
2225
+ testerHtmlTransformed;
2620
2226
  provider;
2621
2227
  vitest;
2622
2228
  vite;
@@ -2718,21 +2324,27 @@ class ParentBrowserProject {
2718
2324
  config;
2719
2325
  // cache for non-vite source maps
2720
2326
  sourceMapCache = new Map();
2721
- constructor(project, base) {
2722
- this.project = project;
2327
+ constructor(ctx, base) {
2723
2328
  this.base = base;
2724
- this.vitest = project.vitest;
2725
- this.config = project.config;
2329
+ this.vitest = ctx.vitest;
2330
+ this.config = ctx.config;
2726
2331
  this.stackTraceOptions = {
2727
- frameFilter: project.config.onStackTrace,
2332
+ frameFilter: this.config.onStackTrace,
2728
2333
  getSourceMap: (id) => {
2729
2334
  if (this.sourceMapCache.has(id)) {
2730
2335
  return this.sourceMapCache.get(id);
2731
2336
  }
2732
2337
  const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
2733
- // handle non-inline source map such as pre-bundled deps in node_modules/.vite
2734
- if (result && !result.map) {
2735
- const filePath = id.split("?")[0];
2338
+ const filePath = id.split("?")[0];
2339
+ // prefer the map stored on disk when the transform pipeline can't
2340
+ // provide a usable one:
2341
+ // - pre-bundled deps: the pipeline map resolves back into the
2342
+ // optimizer cache, while the map esbuild wrote next to the file
2343
+ // points at the real package sources
2344
+ // - an empty `mappings` means the map was intentionally not served
2345
+ // to the browser (`vitest:browser:framework-sourcemaps`), but disk
2346
+ // is still the source of truth for error stack traces
2347
+ if (result && (!result.map || result.map.mappings === "" || filePath.startsWith(this.vite.config.cacheDir))) {
2736
2348
  const extracted = extractSourcemapFromFile(result.code, filePath);
2737
2349
  this.sourceMapCache.set(id, extracted?.map);
2738
2350
  return extracted?.map;
@@ -2763,11 +2375,11 @@ class ParentBrowserProject {
2763
2375
  this.commands[name] ??= command;
2764
2376
  }
2765
2377
  // validate names because they can't be used as identifiers
2766
- for (const command in project.config.browser.commands) {
2378
+ for (const command in this.config.browser.commands) {
2767
2379
  if (!/^[a-z_$][\w$]*$/i.test(command)) {
2768
2380
  throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
2769
2381
  }
2770
- this.commands[command] = project.config.browser.commands[command];
2382
+ this.commands[command] = this.config.browser.commands[command];
2771
2383
  }
2772
2384
  this.prefixTesterUrl = `${base || "/"}`;
2773
2385
  this.prefixOrchestratorUrl = `${base}__vitest_test__/`;
@@ -2775,7 +2387,7 @@ class ParentBrowserProject {
2775
2387
  this.manifest = (async () => {
2776
2388
  return JSON.parse(await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8"));
2777
2389
  })().then((manifest) => this.manifest = manifest);
2778
- 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);
2390
+ 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);
2779
2391
  this.injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8").then((js) => this.injectorJs = js);
2780
2392
  this.errorCatcherUrl = join("/@fs/", resolve(distRoot, "client/error-catcher.js"));
2781
2393
  this.matchersUrl = join("/@fs/", distRoot, "expect-element.js");
@@ -3153,14 +2765,14 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3153
2765
  }
3154
2766
  }
3155
2767
  function canWrite(project) {
3156
- return project.config.browser.api.allowWrite && project.vitest.config.browser.api.allowWrite && project.config.api.allowWrite && project.vitest.config.api.allowWrite;
2768
+ return project.config.api.allowWrite && project.vitest.config.api.allowWrite;
3157
2769
  }
3158
2770
  function isCdpAllowed(project) {
3159
- 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;
2771
+ return project.config.api.allowExec && project.vitest.config.api.allowExec && project.config.api.allowWrite && project.vitest.config.api.allowWrite;
3160
2772
  }
3161
2773
  function assertCdpAllowed(project) {
3162
2774
  if (!isCdpAllowed(project)) {
3163
- throw new Error(`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/browser/api.`);
2775
+ throw new Error(`Cannot use CDP because browser API write or exec operations are disabled. See https://vitest.dev/config/api.`);
3164
2776
  }
3165
2777
  }
3166
2778
  function setupClient(project, rpcId, ws, options) {
@@ -3196,13 +2808,23 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3196
2808
  if (!canWrite(project)) {
3197
2809
  if (artifact.type === "internal:annotation" && artifact.annotation.attachment) {
3198
2810
  artifact.annotation.attachment = undefined;
3199
- vitest.logger.error(`[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/browser/api.`);
2811
+ vitest.logger.error(`[vitest] Cannot record annotation attachment because file writing is disabled. See https://vitest.dev/config/api.`);
3200
2812
  }
3201
2813
  // remove attachments if cannot write
3202
2814
  if (artifact.attachments?.length) {
3203
2815
  const attachments = artifact.attachments.map((n) => n.path).filter((r) => !!r).join("\", \"");
3204
2816
  artifact.attachments = [];
3205
- 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.`);
2817
+ 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.`);
2818
+ }
2819
+ } else {
2820
+ // attachment files are copied into `attachmentsDir`, so confine
2821
+ // client-supplied paths to Vite's `server.fs` boundary
2822
+ const attachments = artifact.type === "internal:annotation" ? artifact.annotation.attachment ? [artifact.annotation.attachment] : [] : artifact.attachments ?? [];
2823
+ for (const attachment of attachments) {
2824
+ const path = attachment.path;
2825
+ if (path && !path.startsWith("http://") && !path.startsWith("https://")) {
2826
+ checkFileAccess(resolve(project.config.root, path));
2827
+ }
3206
2828
  }
3207
2829
  }
3208
2830
  return vitest._testRun.recordArtifact(id, artifact);
@@ -3216,7 +2838,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3216
2838
  },
3217
2839
  async writeBenchmarkResult(relativePath, data) {
3218
2840
  if (!canWrite(project)) {
3219
- vitest.logger.error(`[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/browser/api.`);
2841
+ vitest.logger.error(`[vitest] Cannot write benchmark artifact "${relativePath}" because file writing is disabled. See https://vitest.dev/config/api.`);
3220
2842
  return;
3221
2843
  }
3222
2844
  checkFileAccess(project.benchmark.resolve(relativePath));
@@ -3258,7 +2880,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3258
2880
  async saveSnapshotFile(id, content) {
3259
2881
  checkFileAccess(id);
3260
2882
  if (!canWrite(project)) {
3261
- 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.`);
2883
+ 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.`);
3262
2884
  return;
3263
2885
  }
3264
2886
  await promises.mkdir(dirname(id), { recursive: true });
@@ -3267,7 +2889,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3267
2889
  async removeSnapshotFile(id) {
3268
2890
  checkFileAccess(id);
3269
2891
  if (!canWrite(project)) {
3270
- 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.`);
2892
+ 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.`);
3271
2893
  return;
3272
2894
  }
3273
2895
  if (!existsSync(id)) {
@@ -3448,66 +3070,437 @@ function stringifyReplace(key, value) {
3448
3070
  function defineBrowserCommand(fn) {
3449
3071
  return fn;
3450
3072
  }
3451
- const createBrowserServer = async (options) => {
3452
- const project = options.project;
3453
- const configFile = project.vite.config.configFile;
3454
- if (project.vitest.version !== version) {
3455
- 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."));
3456
- }
3457
- const server = new ParentBrowserProject(project, "/");
3458
- const configPath = typeof configFile === "string" ? configFile : false;
3459
- const logLevel = process.env.VITEST_BROWSER_DEBUG ?? "info";
3460
- const logger = createViteLogger(project.vitest.logger, logLevel, { allowClearScreen: false });
3073
+ const versionRegexp = /(?:\?|&)v=\w{8}/;
3074
+ /**
3075
+ * The browser provider's `serverFactory`. Returns a `BrowserServerContribution`
3076
+ * that Vitest core uses to build the SINGLE Vite server shared by `project.vite`
3077
+ * and `project.browser.vite`. This factory does NOT create a server (core does);
3078
+ * it only contributes config, plugins, the parent factory, and the RPC setup.
3079
+ */
3080
+ const createBrowserServer = async () => {
3461
3081
  const mockerRegistry = new MockerRegistry();
3462
- let cacheDir;
3463
- const vite = await createViteServer({
3464
- ...project.options,
3465
- define: project.config.viteDefine,
3466
- base: "/",
3467
- root: project.config.root,
3468
- logLevel,
3469
- customLogger: {
3470
- ...logger,
3471
- info(msg, options) {
3472
- logger.info(msg, options);
3473
- if (msg.includes("optimized dependencies changed. reloading")) {
3474
- 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(""));
3082
+ const contribution = {
3083
+ async transformIndexHtml(ctx) {
3084
+ const parentServer = contribution.parent;
3085
+ const projectBrowser = [...parentServer.children].find((server) => {
3086
+ return ctx.filename === server.testerFilepath;
3087
+ });
3088
+ if (!projectBrowser) {
3089
+ return;
3090
+ }
3091
+ const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
3092
+ const testerTags = [];
3093
+ const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
3094
+ if (!isDefaultTemplate) {
3095
+ const manifestContent = parentServer.manifest instanceof Promise ? await parentServer.manifest : parentServer.manifest;
3096
+ const testerEntry = manifestContent["tester/tester.html"];
3097
+ testerTags.push({
3098
+ tag: "script",
3099
+ attrs: {
3100
+ type: "module",
3101
+ crossorigin: "",
3102
+ src: `${parentServer.base}${testerEntry.file}`
3103
+ },
3104
+ injectTo: "head"
3105
+ });
3106
+ for (const importName of testerEntry.imports || []) {
3107
+ const entryManifest = manifestContent[importName];
3108
+ if (entryManifest) {
3109
+ testerTags.push({
3110
+ tag: "link",
3111
+ attrs: {
3112
+ href: `${parentServer.base}${entryManifest.file}`,
3113
+ rel: "modulepreload",
3114
+ crossorigin: ""
3115
+ },
3116
+ injectTo: "head"
3117
+ });
3118
+ }
3119
+ }
3120
+ } else {
3121
+ // inject the reset style only in the default template,
3122
+ // allowing users to customize the style in their own template
3123
+ testerTags.push({
3124
+ tag: "style",
3125
+ children: `
3126
+ html {
3127
+ padding: 0;
3128
+ margin: 0;
3129
+ }
3130
+ body {
3131
+ padding: 0;
3132
+ margin: 0;
3133
+ min-height: 100vh;
3134
+ }`,
3135
+ injectTo: "head"
3136
+ });
3137
+ }
3138
+ return [
3139
+ {
3140
+ tag: "script",
3141
+ children: "{__VITEST_INJECTOR__}",
3142
+ injectTo: "head-prepend"
3143
+ },
3144
+ {
3145
+ tag: "script",
3146
+ children: stateJs,
3147
+ injectTo: "head-prepend"
3148
+ },
3149
+ {
3150
+ tag: "script",
3151
+ attrs: {
3152
+ type: "module",
3153
+ src: parentServer.errorCatcherUrl
3154
+ },
3155
+ injectTo: "head"
3156
+ },
3157
+ {
3158
+ tag: "script",
3159
+ attrs: {
3160
+ type: "module",
3161
+ src: parentServer.matchersUrl
3162
+ },
3163
+ injectTo: "head"
3164
+ },
3165
+ ...parentServer.initScripts.map((script) => ({
3166
+ tag: "script",
3167
+ attrs: {
3168
+ type: "module",
3169
+ src: join("/@fs/", script)
3170
+ },
3171
+ injectTo: "head"
3172
+ })),
3173
+ ...testerTags
3174
+ ].filter((s) => s != null);
3175
+ },
3176
+ configureServer(server) {
3177
+ const parentServer = contribution.parent;
3178
+ parentServer.setServer(server);
3179
+ // eslint-disable-next-line prefer-arrow-callback
3180
+ server.middlewares.use(function vitestHeaders(_req, res, next) {
3181
+ const headers = server.config.server.headers;
3182
+ if (headers) {
3183
+ for (const name in headers) {
3184
+ res.setHeader(name, headers[name]);
3185
+ }
3186
+ }
3187
+ next();
3188
+ });
3189
+ // strip _vitest_original query added by importActual so that
3190
+ // the plugin pipeline sees the original import id (e.g. virtual modules's load hook).
3191
+ server.middlewares.use((req, _res, next) => {
3192
+ if (req.url?.includes("_vitest_original") && parentServer.config.browser.provider?.name === "playwright") {
3193
+ req.url = req.url.replace(/[?&]_vitest_original(?=[&#]|$)/, "").replace(/[?&]ext\b[^&#]*/, "").replace(/\?$/, "");
3194
+ }
3195
+ next();
3196
+ });
3197
+ server.middlewares.use(createOrchestratorMiddleware(parentServer));
3198
+ server.middlewares.use(createTesterMiddleware(parentServer));
3199
+ server.middlewares.use(`/favicon.svg`, (_, res) => {
3200
+ const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
3201
+ res.write(content, "utf-8");
3202
+ res.end();
3203
+ });
3204
+ // Serve coverage HTML at ./coverage if configured
3205
+ const coverageHtmlDir = parentServer.vitest.config.coverage?.htmlDir;
3206
+ if (coverageHtmlDir) {
3207
+ server.middlewares.use("/__vitest_test__/coverage", sirv(coverageHtmlDir, {
3208
+ single: true,
3209
+ dev: true,
3210
+ setHeaders: (res) => {
3211
+ const csp = res.getHeader("Content-Security-Policy");
3212
+ if (typeof csp === "string") {
3213
+ // add frame-ancestors to allow the iframe to be loaded by Vitest,
3214
+ // but keep the rest of the CSP
3215
+ res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
3216
+ }
3217
+ res.setHeader("Cache-Control", "public,max-age=0,must-revalidate");
3218
+ }
3219
+ }));
3220
+ }
3221
+ server.middlewares.use((req, res, next) => {
3222
+ // 9000 mega head move
3223
+ // Vite always caches optimized dependencies, but users might mock
3224
+ // them in _some_ tests, while keeping original modules in others
3225
+ // there is no way to configure that in Vite, so we patch it here
3226
+ // to always ignore the cache-control set by Vite in the next middleware
3227
+ if (req.url && versionRegexp.test(req.url) && !req.url.includes("chunk-")) {
3228
+ res.setHeader("Cache-Control", "no-cache");
3229
+ const setHeader = res.setHeader.bind(res);
3230
+ res.setHeader = function(name, value) {
3231
+ if (name === "Cache-Control") {
3232
+ return res;
3233
+ }
3234
+ return setHeader(name, value);
3235
+ };
3236
+ }
3237
+ next();
3238
+ });
3239
+ // handle attachments the same way as in packages/ui/node/index.ts
3240
+ server.middlewares.use((req, res, next) => {
3241
+ if (!req.url) {
3242
+ return next();
3243
+ }
3244
+ const url = new URL(req.url, "http://localhost");
3245
+ if (url.pathname !== "/__vitest_attachment__") {
3246
+ return next();
3247
+ }
3248
+ const path = url.searchParams.get("path");
3249
+ const contentType = url.searchParams.get("contentType");
3250
+ if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
3251
+ return next();
3252
+ }
3253
+ const fsPath = decodeURIComponent(path);
3254
+ if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
3255
+ return next();
3256
+ }
3257
+ try {
3258
+ res.setHeader("content-type", contentType);
3259
+ return createReadStream(fsPath).pipe(res).on("close", () => res.end());
3260
+ } catch (err) {
3261
+ return next(err);
3262
+ }
3263
+ });
3264
+ // When the Vitest UI (`test.ui`) is enabled, the `vitest:ui` plugin owns
3265
+ // `/__vitest__` (including the token-injected index.html and its assets),
3266
+ // so registering sirv here would shadow it and serve the page without a token.
3267
+ if (!parentServer.vitest.config.ui) {
3268
+ server.middlewares.use("/__vitest__", sirv(distClientRoot));
3269
+ }
3270
+ },
3271
+ async config(viteConfig) {
3272
+ const testConfig = viteConfig.test || {};
3273
+ const config = {
3274
+ resolve: {
3275
+ alias: testConfig.alias,
3276
+ dedupe: ["vitest"]
3277
+ },
3278
+ server: {
3279
+ fs: { allow: [distRoot] },
3280
+ middlewareMode: false,
3281
+ watch: null,
3282
+ forwardConsole: false
3475
3283
  }
3284
+ };
3285
+ // Enables using the @preserve ignore hint for coverage providers. Only for
3286
+ // esbuild (rolldown-vite uses oxc instead).
3287
+ if (!rolldownVersion && viteConfig.esbuild !== false) {
3288
+ config.esbuild = { legalComments: "inline" };
3289
+ }
3290
+ const define = {};
3291
+ const envVars = testConfig.env || {};
3292
+ for (const env in envVars) {
3293
+ define[`import.meta.env.${env}`] = JSON.stringify(envVars[env]);
3476
3294
  }
3295
+ config.define = define;
3296
+ return config;
3477
3297
  },
3478
- mode: project.config.mode,
3479
- configFile: configPath,
3480
- configLoader: project.vite.config.inlineConfig.configLoader,
3481
- server: {
3482
- ...project.options?.server,
3483
- hmr: false,
3484
- watch: null
3298
+ resolveOptimizeDeps(projectConfigs, testFiles, harness) {
3299
+ return resolveBrowserOptimizeDeps(projectConfigs, testFiles, harness);
3300
+ },
3301
+ plugins: [],
3302
+ createParent({ config, vitest }) {
3303
+ if (vitest.version !== version) {
3304
+ 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."));
3305
+ }
3306
+ return new ParentBrowserProject({
3307
+ config,
3308
+ vitest
3309
+ }, "/");
3485
3310
  },
3486
- cacheDir: project.vite.config.cacheDir,
3487
- plugins: [
3488
- {
3489
- name: "vitest-internal:browser-cacheDir",
3490
- configResolved(config) {
3491
- cacheDir = config.cacheDir;
3311
+ setupRpc(parent) {
3312
+ setupBrowserRpc(parent, mockerRegistry);
3313
+ }
3314
+ };
3315
+ contribution.plugins = [
3316
+ ...BrowserPlugin(contribution),
3317
+ interceptorPlugin({ registry: mockerRegistry }),
3318
+ {
3319
+ name: "vitest:browser:framework-sourcemaps",
3320
+ enforce: "post",
3321
+ transform(code, id) {
3322
+ const parentServer = contribution.parent;
3323
+ // In a headless run nothing can open devtools, so sourcemaps of
3324
+ // Vitest's own pre-built modules are never consumed: their stack
3325
+ // frames are filtered by stackIgnorePatterns. Generating and
3326
+ // inlining these maps costs server CPU and multiplies the bytes
3327
+ // the browser downloads by ~5 for every fresh browser context.
3328
+ // Sourcemaps of user files and (by default) their dependencies are
3329
+ // kept — they point error stacks and devtools at original sources.
3330
+ if (!parentServer || !isHeadlessServer(parentServer) || parentServer.vitest.config.inspector.enabled) {
3331
+ return null;
3492
3332
  }
3493
- },
3494
- ...options.mocksPlugins({ filter(id) {
3495
- if (id.includes(distRoot) || id.includes(cacheDir)) {
3496
- return false;
3333
+ if (isCSSRequest(id)) {
3334
+ return null;
3497
3335
  }
3498
- return true;
3499
- } }),
3500
- options.metaEnvReplacer(),
3501
- ...project.options?.plugins || [],
3502
- BrowserPlugin(server),
3503
- interceptorPlugin({ registry: mockerRegistry }),
3504
- options.coveragePlugin()
3505
- ]
3506
- });
3507
- await vite.listen();
3508
- setupBrowserRpc(server, mockerRegistry);
3509
- return server;
3336
+ const path = cleanUrl$1(id);
3337
+ if (path.startsWith(distRoot) || path.startsWith(distDir)) {
3338
+ return {
3339
+ code,
3340
+ map: { mappings: "" }
3341
+ };
3342
+ }
3343
+ // users that never debug into node_modules can drop dependency
3344
+ // sourcemaps entirely; `server.sourcemapIgnoreList` (default:
3345
+ // node_modules) can opt paths back in even then, e.g. with
3346
+ // `preserveSymlinks` where workspace code keeps its node_modules
3347
+ // path and would be wrongly treated as a dependency
3348
+ if (parentServer.config.browser.dependencySourcemaps === false && path.includes("/node_modules/") && (parentServer.vite.config.server.sourcemapIgnoreList(path, path) ?? true)) {
3349
+ return {
3350
+ code,
3351
+ map: { mappings: "" }
3352
+ };
3353
+ }
3354
+ return null;
3355
+ }
3356
+ }
3357
+ ];
3358
+ return contribution;
3510
3359
  };
3360
+ function isHeadlessServer(parentServer) {
3361
+ if (!parentServer.config.browser.headless) {
3362
+ return false;
3363
+ }
3364
+ // sibling instances share this server (and its module graph cache, so a
3365
+ // late-spawned instance would receive already-cached transforms) and can
3366
+ // override `headless` — check the static instance options instead of the
3367
+ // lazily populated `children` to stay deterministic across runs
3368
+ const instances = parentServer.config.browser.instances ?? [];
3369
+ return instances.every((instance) => instance.headless !== false);
3370
+ }
3371
+ function resolveBrowserOptimizeDeps(projectConfigs, testFiles, harness) {
3372
+ // `testFiles` are globbed by the core package and aggregated across every
3373
+ // project that shares this browser Vite server (instance and benchmark
3374
+ // variants). The remaining options are shared by those projects, so the first
3375
+ // config is representative.
3376
+ const testConfig = projectConfigs[0];
3377
+ const root = testConfig.root || process.cwd();
3378
+ const setupFiles = new Set(projectConfigs.flatMap((config) => toArray(config.setupFiles || [])));
3379
+ const entries = [
3380
+ ...testFiles,
3381
+ ...setupFiles,
3382
+ resolve(distDir, "index.js"),
3383
+ resolve(distDir, "browser.js"),
3384
+ resolve(distDir, "runners.js"),
3385
+ resolve(distDir, "utils.js"),
3386
+ ...testConfig.snapshotSerializers || []
3387
+ ];
3388
+ // Keep these external (never pre-bundle by optimizer):
3389
+ // - vitest/browser, @vitest/browser/context, @vitest/browser/utils are
3390
+ // VIRTUAL modules generated per-server (see pluginContext.ts) — optimizer
3391
+ // cannot resolve/run their `load`, it would freeze stale/empty content.
3392
+ // - vite/module-runner is small enough to not need pre-bundling.
3393
+ // - msw is a large, side-effectful service-worker library.
3394
+ const exclude = [
3395
+ "vitest/browser",
3396
+ "vite/module-runner",
3397
+ "@vitest/browser/utils",
3398
+ "@vitest/browser/context",
3399
+ "msw",
3400
+ "msw/browser"
3401
+ ];
3402
+ if (typeof testConfig.diff === "string") {
3403
+ entries.push(testConfig.diff);
3404
+ }
3405
+ if (testConfig.coverage?.enabled) {
3406
+ const provider = testConfig.coverage.provider ?? "v8";
3407
+ if (provider === "v8") {
3408
+ const path = tryResolve("@vitest/coverage-v8", [root]);
3409
+ if (path) {
3410
+ entries.push(path);
3411
+ exclude.push("@vitest/coverage-v8/browser");
3412
+ }
3413
+ } else if (provider === "istanbul") {
3414
+ const path = tryResolve("@vitest/coverage-istanbul", [root]);
3415
+ if (path) {
3416
+ entries.push(path);
3417
+ exclude.push("@vitest/coverage-istanbul");
3418
+ }
3419
+ } else if (provider === "custom" && testConfig.coverage.customProviderModule) {
3420
+ entries.push(testConfig.coverage.customProviderModule);
3421
+ }
3422
+ }
3423
+ // Pre-bundle the vitest runtime so the browser fetches a few optimized
3424
+ // chunks instead of ~20 separately-served dist chunks (faster startup).
3425
+ // `vitest`, `vitest/internal/browser` and `@vitest/browser/client` are
3426
+ // optimized together in a single pass, so esbuild dedupes their shared
3427
+ // stateful chunks (the test collector, the runner, the RPC client) to a
3428
+ // single instance — preserving module identity between the test files'
3429
+ // `import 'vitest'` and the tester. Their transitive deps (@vitest/utils,
3430
+ // @vitest/spy, pathe, tinyrainbow, …) are inlined into these bundles.
3431
+ const include = [
3432
+ "vitest > expect-type",
3433
+ "vitest > magic-string",
3434
+ "vitest > chai",
3435
+ "vitest",
3436
+ "vitest/internal/browser",
3437
+ "@vitest/browser/client"
3438
+ ];
3439
+ const provider = testConfig.browser?.provider;
3440
+ if (provider?.name === "preview") {
3441
+ include.push("@vitest/browser-preview > @testing-library/user-event", "@vitest/browser-preview > @testing-library/dom");
3442
+ }
3443
+ const isPackageExists = (pkg) => {
3444
+ return harness.packageInstaller.isPackageExists(pkg, { paths: [root] });
3445
+ };
3446
+ if (isPackageExists("vitest-browser-svelte")) {
3447
+ exclude.push("vitest-browser-svelte");
3448
+ }
3449
+ // since we override the resolution in the esbuild plugin, Vite can no longer optimize it
3450
+ if (isPackageExists("vitest-browser-vue")) {
3451
+ include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
3452
+ }
3453
+ if (isPackageExists("@vue/test-utils")) {
3454
+ include.push("@vue/test-utils");
3455
+ }
3456
+ const otelConfig = testConfig.experimental?.openTelemetry;
3457
+ if (otelConfig?.enabled && otelConfig.browserSdkPath) {
3458
+ entries.push(otelConfig.browserSdkPath);
3459
+ include.push("@opentelemetry/api");
3460
+ } else {
3461
+ exclude.push("@opentelemetry/api");
3462
+ }
3463
+ // resolve @vue/(test-utils|compiler-core) to CJS instead of the browser build:
3464
+ // test-utils' browser build expects a global Vue; compiler-core's CJS build is
3465
+ // the only one that allows slots as strings.
3466
+ const rolldownPlugin = {
3467
+ name: "vue-test-utils-rewrite",
3468
+ resolveId: {
3469
+ filter: { id: /^@vue\/(test-utils|compiler-core)$/ },
3470
+ handler(source, importer) {
3471
+ return getRequire().resolve(source, { paths: [importer] });
3472
+ }
3473
+ }
3474
+ };
3475
+ const esbuildPlugin = {
3476
+ name: "test-utils-rewrite",
3477
+ setup(build) {
3478
+ build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
3479
+ return { path: getRequire().resolve(args.path, { paths: [args.importer] }) };
3480
+ });
3481
+ }
3482
+ };
3483
+ return {
3484
+ entries,
3485
+ exclude,
3486
+ include,
3487
+ ...rolldownVersion ? { rolldownOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } }
3488
+ };
3489
+ }
3490
+ function tryResolve(path, paths) {
3491
+ try {
3492
+ return getRequire().resolve(path, { paths });
3493
+ } catch {
3494
+ return undefined;
3495
+ }
3496
+ }
3497
+ let _require;
3498
+ function getRequire() {
3499
+ if (!_require) {
3500
+ _require = createRequire(import.meta.url);
3501
+ }
3502
+ return _require;
3503
+ }
3511
3504
  function defineBrowserProvider(options) {
3512
3505
  return {
3513
3506
  ...options,
@@ -3516,4 +3509,4 @@ function defineBrowserProvider(options) {
3516
3509
  };
3517
3510
  }
3518
3511
 
3519
- export { createBrowserServer, defineBrowserCommand, defineBrowserProvider, parseKeyDef, resolveScreenshotPath };
3512
+ export { assertBrowserApiWrite, assertBrowserFileAccess, createBrowserServer, defineBrowserCommand, defineBrowserProvider, parseKeyDef, resolveScreenshotPath };