@vitest/browser 1.6.0 → 2.0.0-beta.10

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,21 +1,28 @@
1
1
  import { fileURLToPath } from 'node:url';
2
- import { readFile } from 'node:fs/promises';
2
+ import { mkdir, readFile as readFile$1 } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
3
4
  import sirv from 'sirv';
5
+ import { isFileServingAllowed, getFilePoolName, distDir } from 'vitest/node';
4
6
  import { coverageConfigDefaults } from 'vitest/config';
5
- import { slash } from '@vitest/utils';
7
+ import { slash, toArray } from '@vitest/utils';
8
+ import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-CXn0ag9T.js';
9
+ import fs, { promises } from 'node:fs';
10
+ import { resolve as resolve$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
6
11
  import MagicString from 'magic-string';
7
12
  import { esmWalker } from '@vitest/utils/ast';
8
13
 
14
+ const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
9
15
  function normalizeWindowsPath(input = "") {
10
- if (!input || !input.includes("\\")) {
16
+ if (!input) {
11
17
  return input;
12
18
  }
13
- return input.replace(/\\/g, "/");
19
+ return input.replace(/\\/g, "/").replace(_DRIVE_LETTER_START_RE, (r) => r.toUpperCase());
14
20
  }
15
21
 
16
22
  const _UNC_REGEX = /^[/\\]{2}/;
17
23
  const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
18
24
  const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
25
+ const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
19
26
  const normalize = function(path) {
20
27
  if (path.length === 0) {
21
28
  return ".";
@@ -65,7 +72,7 @@ const join = function(...arguments_) {
65
72
  return normalize(joined.replace(/\/\/+/g, "/"));
66
73
  };
67
74
  function cwd() {
68
- if (typeof process !== "undefined") {
75
+ if (typeof process !== "undefined" && typeof process.cwd === "function") {
69
76
  return process.cwd().replace(/\\/g, "/");
70
77
  }
71
78
  return "/";
@@ -150,78 +157,261 @@ function normalizeString(path, allowAboveRoot) {
150
157
  const isAbsolute = function(p) {
151
158
  return _IS_ABSOLUTE_RE.test(p);
152
159
  };
160
+ const relative = function(from, to) {
161
+ const _from = resolve(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
162
+ const _to = resolve(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
163
+ if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) {
164
+ return _to.join("/");
165
+ }
166
+ const _fromCopy = [..._from];
167
+ for (const segment of _fromCopy) {
168
+ if (_to[0] !== segment) {
169
+ break;
170
+ }
171
+ _from.shift();
172
+ _to.shift();
173
+ }
174
+ return [..._from.map(() => ".."), ..._to].join("/");
175
+ };
176
+ const dirname = function(p) {
177
+ const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
178
+ if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) {
179
+ segments[0] += "/";
180
+ }
181
+ return segments.join("/") || (isAbsolute(p) ? "/" : ".");
182
+ };
153
183
  const basename = function(p, extension) {
154
184
  const lastSegment = normalizeWindowsPath(p).split("/").pop();
155
185
  return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
156
186
  };
157
187
 
158
- /**
159
- * @param {import('estree').Node} param
160
- * @returns {string[]}
161
- */
162
- function extract_names(param) {
163
- return extract_identifiers(param).map((node) => node.name);
164
- }
165
-
166
- /**
167
- * @param {import('estree').Node} param
168
- * @param {import('estree').Identifier[]} nodes
169
- * @returns {import('estree').Identifier[]}
170
- */
171
- function extract_identifiers(param, nodes = []) {
172
- switch (param.type) {
173
- case 'Identifier':
174
- nodes.push(param);
175
- break;
176
-
177
- case 'MemberExpression':
178
- let object = param;
179
- while (object.type === 'MemberExpression') {
180
- object = /** @type {any} */ (object.object);
181
- }
182
- nodes.push(/** @type {any} */ (object));
183
- break;
184
-
185
- case 'ObjectPattern':
186
- for (const prop of param.properties) {
187
- if (prop.type === 'RestElement') {
188
- extract_identifiers(prop.argument, nodes);
189
- } else {
190
- extract_identifiers(prop.value, nodes);
191
- }
192
- }
188
+ const click = async (context, xpath, options = {}) => {
189
+ const provider = context.provider;
190
+ if (provider instanceof PlaywrightBrowserProvider) {
191
+ const tester = context.tester;
192
+ await tester.locator(`xpath=${xpath}`).click(options);
193
+ return;
194
+ }
195
+ if (provider instanceof WebdriverBrowserProvider) {
196
+ const page = provider.browser;
197
+ const markedXpath = `//${xpath}`;
198
+ const element = await page.$(markedXpath);
199
+ await element.click(options);
200
+ return;
201
+ }
202
+ throw new Error(`Provider "${provider.name}" doesn't support click command`);
203
+ };
193
204
 
194
- break;
205
+ function assertFileAccess(path, project) {
206
+ if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server))
207
+ throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
208
+ }
209
+ const readFile = async ({ project, testPath = process.cwd() }, path, options = {}) => {
210
+ const filepath = resolve$1(dirname$1(testPath), path);
211
+ assertFileAccess(filepath, project);
212
+ if (typeof options === "object" && !options.encoding)
213
+ options.encoding = "utf-8";
214
+ return promises.readFile(filepath, options);
215
+ };
216
+ const writeFile = async ({ project, testPath = process.cwd() }, path, data, options) => {
217
+ const filepath = resolve$1(dirname$1(testPath), path);
218
+ assertFileAccess(filepath, project);
219
+ const dir = dirname$1(filepath);
220
+ if (!fs.existsSync(dir))
221
+ await promises.mkdir(dir, { recursive: true });
222
+ await promises.writeFile(filepath, data, options);
223
+ };
224
+ const removeFile = async ({ project, testPath = process.cwd() }, path) => {
225
+ const filepath = resolve$1(dirname$1(testPath), path);
226
+ assertFileAccess(filepath, project);
227
+ await promises.rm(filepath);
228
+ };
195
229
 
196
- case 'ArrayPattern':
197
- for (const element of param.elements) {
198
- if (element) extract_identifiers(element, nodes);
199
- }
230
+ function isObject(payload) {
231
+ return payload != null && typeof payload === "object";
232
+ }
233
+ function isSendKeysPayload(payload) {
234
+ const validOptions = ["type", "press", "down", "up"];
235
+ if (!isObject(payload))
236
+ throw new Error("You must provide a `SendKeysPayload` object");
237
+ const numberOfValidOptions = Object.keys(payload).filter(
238
+ (key) => validOptions.includes(key)
239
+ ).length;
240
+ const unknownOptions = Object.keys(payload).filter((key) => !validOptions.includes(key));
241
+ if (numberOfValidOptions > 1) {
242
+ throw new Error(
243
+ `You must provide ONLY one of the following properties to pass to the browser runner: ${validOptions.join(
244
+ ", "
245
+ )}.`
246
+ );
247
+ }
248
+ if (numberOfValidOptions === 0) {
249
+ throw new Error(
250
+ `You must provide one of the following properties to pass to the browser runner: ${validOptions.join(
251
+ ", "
252
+ )}.`
253
+ );
254
+ }
255
+ if (unknownOptions.length > 0)
256
+ throw new Error(`Unknown options \`${unknownOptions.join(", ")}\` present.`);
257
+ return true;
258
+ }
259
+ function isTypePayload(payload) {
260
+ return "type" in payload;
261
+ }
262
+ function isPressPayload(payload) {
263
+ return "press" in payload;
264
+ }
265
+ function isDownPayload(payload) {
266
+ return "down" in payload;
267
+ }
268
+ function isUpPayload(payload) {
269
+ return "up" in payload;
270
+ }
271
+ const sendKeys = async ({ provider, contextId }, payload) => {
272
+ if (!isSendKeysPayload(payload) || !payload)
273
+ throw new Error("You must provide a `SendKeysPayload` object");
274
+ if (provider instanceof PlaywrightBrowserProvider) {
275
+ const page = provider.getPage(contextId);
276
+ if (isTypePayload(payload))
277
+ await page.keyboard.type(payload.type);
278
+ else if (isPressPayload(payload))
279
+ await page.keyboard.press(payload.press);
280
+ else if (isDownPayload(payload))
281
+ await page.keyboard.down(payload.down);
282
+ else if (isUpPayload(payload))
283
+ await page.keyboard.up(payload.up);
284
+ } else if (provider instanceof WebdriverBrowserProvider) {
285
+ const browser = provider.browser;
286
+ if (isTypePayload(payload))
287
+ await browser.keys(payload.type.split(""));
288
+ else if (isPressPayload(payload))
289
+ await browser.keys([payload.press]);
290
+ else
291
+ throw new Error('Only "press" and "type" are supported by webdriverio.');
292
+ } else {
293
+ throw new TypeError(`"sendKeys" is not supported for ${provider.name} browser provider.`);
294
+ }
295
+ };
200
296
 
201
- break;
297
+ const screenshot = async (context, name, options = {}) => {
298
+ if (!context.testPath)
299
+ throw new Error(`Cannot take a screenshot without a test path`);
300
+ const path = resolveScreenshotPath(context.testPath, name, context.project.config);
301
+ const savePath = normalize$1(path);
302
+ await mkdir(dirname(path), { recursive: true });
303
+ if (context.provider instanceof PlaywrightBrowserProvider) {
304
+ if (options.element) {
305
+ const { element: elementXpath, ...config } = options;
306
+ const iframe = context.tester;
307
+ const element = iframe.locator(`xpath=${elementXpath}`);
308
+ await element.screenshot({ ...config, path: savePath });
309
+ } else {
310
+ await context.body.screenshot({ ...options, path: savePath });
311
+ }
312
+ return path;
313
+ }
314
+ if (context.provider instanceof WebdriverBrowserProvider) {
315
+ const page = context.provider.browser;
316
+ if (!options.element) {
317
+ const body = await page.$("body");
318
+ await body.saveScreenshot(savePath);
319
+ return path;
320
+ }
321
+ const xpath = `//${options.element}`;
322
+ const element = await page.$(xpath);
323
+ await element.saveScreenshot(savePath);
324
+ return path;
325
+ }
326
+ throw new Error(`Provider "${context.provider.name}" does not support screenshots`);
327
+ };
328
+ function resolveScreenshotPath(testPath, name, config) {
329
+ const dir = dirname(testPath);
330
+ const base = basename(testPath);
331
+ if (config.browser.screenshotDirectory) {
332
+ return resolve(
333
+ config.browser.screenshotDirectory,
334
+ relative(config.root, dir),
335
+ base,
336
+ name
337
+ );
338
+ }
339
+ return resolve(dir, "__screenshots__", base, name);
340
+ }
202
341
 
203
- case 'RestElement':
204
- extract_identifiers(param.argument, nodes);
205
- break;
342
+ var builtinCommands = {
343
+ readFile,
344
+ removeFile,
345
+ writeFile,
346
+ sendKeys,
347
+ __vitest_click: click,
348
+ __vitest_screenshot: screenshot
349
+ };
206
350
 
207
- case 'AssignmentPattern':
208
- extract_identifiers(param.left, nodes);
209
- break;
210
- }
351
+ const VIRTUAL_ID_CONTEXT = "\0@vitest/browser/context";
352
+ const ID_CONTEXT = "@vitest/browser/context";
353
+ const __dirname = dirname(fileURLToPath(import.meta.url));
354
+ function BrowserContext(project) {
355
+ project.config.browser.commands ??= {};
356
+ for (const [name, command] of Object.entries(builtinCommands))
357
+ project.config.browser.commands[name] ??= command;
358
+ for (const command in project.config.browser.commands) {
359
+ if (!/^[a-z_$][\w$]*$/i.test(command))
360
+ throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
361
+ }
362
+ return {
363
+ name: "vitest:browser:virtual-module:context",
364
+ enforce: "pre",
365
+ resolveId(id) {
366
+ if (id === ID_CONTEXT)
367
+ return VIRTUAL_ID_CONTEXT;
368
+ },
369
+ load(id) {
370
+ if (id === VIRTUAL_ID_CONTEXT)
371
+ return generateContextFile.call(this, project);
372
+ }
373
+ };
374
+ }
375
+ async function generateContextFile(project) {
376
+ const commands = Object.keys(project.config.browser.commands ?? {});
377
+ const filepathCode = "__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined";
378
+ const provider = project.browserProvider;
379
+ const commandsCode = commands.filter((command) => !command.startsWith("__vitest")).map((command) => {
380
+ return ` ["${command}"]: (...args) => rpc().triggerCommand(contextId, "${command}", filepath(), args),`;
381
+ }).join("\n");
382
+ const userEventNonProviderImport = await getUserEventImport(provider, this.resolve.bind(this));
383
+ const distContextPath = slash(`/@fs/${resolve(__dirname, "context.js")}`);
384
+ return `
385
+ import { page, userEvent as __userEvent_CDP__ } from '${distContextPath}'
386
+ ${userEventNonProviderImport}
387
+ const filepath = () => ${filepathCode}
388
+ const rpc = () => __vitest_worker__.rpc
389
+ const contextId = __vitest_browser_runner__.contextId
211
390
 
212
- return nodes;
391
+ export const server = {
392
+ platform: ${JSON.stringify(process.platform)},
393
+ version: ${JSON.stringify(process.version)},
394
+ provider: ${JSON.stringify(provider.name)},
395
+ browser: ${JSON.stringify(project.config.browser.name)},
396
+ commands: {
397
+ ${commandsCode}
398
+ }
399
+ }
400
+ export const commands = server.commands
401
+ export const userEvent = ${provider.name === "preview" ? "__vitest_user_event__" : "__userEvent_CDP__"}
402
+ export { page }
403
+ `;
404
+ }
405
+ async function getUserEventImport(provider, resolve2) {
406
+ if (provider.name !== "preview")
407
+ return "";
408
+ const resolved = await resolve2("@testing-library/user-event", __dirname);
409
+ if (!resolved)
410
+ throw new Error(`Failed to resolve user-event package from ${__dirname}`);
411
+ return `import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'`;
213
412
  }
214
413
 
215
- const viInjectedKey = "__vi_inject__";
216
- const viExportAllHelper = "__vitest_browser_runner__.exportAll";
217
- const skipHijack = [
218
- "/@vite/client",
219
- "/@vite/env",
220
- /vite\/dist\/client/
221
- ];
222
- function injectVitestModule(code, id, parse) {
223
- if (skipHijack.some((skip) => id.match(skip)))
224
- return;
414
+ function injectDynamicImport(code, id, parse) {
225
415
  const s = new MagicString(code);
226
416
  let ast;
227
417
  try {
@@ -231,165 +421,16 @@ function injectVitestModule(code, id, parse) {
231
421
  ${err.message}`);
232
422
  return;
233
423
  }
234
- let uid = 0;
235
- const idToImportMap = /* @__PURE__ */ new Map();
236
- const declaredConst = /* @__PURE__ */ new Set();
237
- const hoistIndex = 0;
238
- const transformImportDeclaration = (node) => {
239
- const source = node.source.value;
240
- if (skipHijack.some((skip) => source.match(skip)))
241
- return null;
242
- const importId = `__vi_esm_${uid++}__`;
243
- const hasSpecifiers = node.specifiers.length > 0;
244
- const code2 = hasSpecifiers ? `import { ${viInjectedKey} as ${importId} } from '${source}'
245
- ` : `import '${source}'
246
- `;
247
- return {
248
- code: code2,
249
- id: importId
250
- };
251
- };
252
- function defineImport(node) {
253
- const declaration = transformImportDeclaration(node);
254
- if (!declaration)
255
- return null;
256
- s.appendLeft(hoistIndex, declaration.code);
257
- return declaration.id;
258
- }
259
- function defineImportAll(source) {
260
- const importId = `__vi_esm_${uid++}__`;
261
- s.appendLeft(hoistIndex, `const { ${viInjectedKey}: ${importId} } = await import(${JSON.stringify(source)});
262
- `);
263
- return importId;
264
- }
265
- function defineExport(position, name, local = name) {
266
- s.appendLeft(
267
- position,
268
- `
269
- Object.defineProperty(${viInjectedKey}, "${name}", { enumerable: true, configurable: true, get(){ return ${local} }});`
270
- );
271
- }
272
- for (const node of ast.body) {
273
- if (node.type === "ImportDeclaration") {
274
- const importId = defineImport(node);
275
- if (!importId)
276
- continue;
277
- s.remove(node.start, node.end);
278
- for (const spec of node.specifiers) {
279
- if (spec.type === "ImportSpecifier") {
280
- idToImportMap.set(
281
- spec.local.name,
282
- `${importId}.${spec.imported.name}`
283
- );
284
- } else if (spec.type === "ImportDefaultSpecifier") {
285
- idToImportMap.set(spec.local.name, `${importId}.default`);
286
- } else {
287
- idToImportMap.set(spec.local.name, importId);
288
- }
289
- }
290
- }
291
- }
292
- for (const node of ast.body) {
293
- if (node.type === "ExportNamedDeclaration") {
294
- if (node.declaration) {
295
- if (node.declaration.type === "FunctionDeclaration" || node.declaration.type === "ClassDeclaration") {
296
- defineExport(node.end, node.declaration.id.name);
297
- } else {
298
- for (const declaration of node.declaration.declarations) {
299
- const names = extract_names(declaration.id);
300
- for (const name of names)
301
- defineExport(node.end, name);
302
- }
303
- }
304
- s.remove(node.start, node.declaration.start);
305
- } else {
306
- s.remove(node.start, node.end);
307
- if (node.source) {
308
- const importId = defineImportAll(node.source.value);
309
- for (const spec of node.specifiers) {
310
- defineExport(
311
- hoistIndex,
312
- spec.exported.name,
313
- `${importId}.${spec.local.name}`
314
- );
315
- }
316
- } else {
317
- for (const spec of node.specifiers) {
318
- const local = spec.local.name;
319
- const binding = idToImportMap.get(local);
320
- defineExport(node.end, spec.exported.name, binding || local);
321
- }
322
- }
323
- }
324
- }
325
- if (node.type === "ExportDefaultDeclaration") {
326
- const expressionTypes = ["FunctionExpression", "ClassExpression"];
327
- if ("id" in node.declaration && node.declaration.id && !expressionTypes.includes(node.declaration.type)) {
328
- const { name } = node.declaration.id;
329
- s.remove(
330
- node.start,
331
- node.start + 15
332
- /* 'export default '.length */
333
- );
334
- s.append(
335
- `
336
- Object.defineProperty(${viInjectedKey}, "default", { enumerable: true, configurable: true, value: ${name} });`
337
- );
338
- } else {
339
- s.update(
340
- node.start,
341
- node.start + 14,
342
- `${viInjectedKey}.default =`
343
- );
344
- s.append(`
345
- export default { ${viInjectedKey}: ${viInjectedKey}.default };
346
- `);
347
- }
348
- }
349
- if (node.type === "ExportAllDeclaration") {
350
- s.remove(node.start, node.end);
351
- const importId = defineImportAll(node.source.value);
352
- if (node.exported)
353
- defineExport(hoistIndex, node.exported.name, `${importId}`);
354
- else
355
- s.appendLeft(hoistIndex, `${viExportAllHelper}(${viInjectedKey}, ${importId});
356
- `);
357
- }
358
- }
359
424
  esmWalker(ast, {
360
- onIdentifier(id2, info, parentStack) {
361
- const binding = idToImportMap.get(id2.name);
362
- if (!binding)
363
- return;
364
- if (info.hasBindingShortcut) {
365
- s.appendLeft(id2.end, `: ${binding}`);
366
- } else if (info.classDeclaration) {
367
- if (!declaredConst.has(id2.name)) {
368
- declaredConst.add(id2.name);
369
- const topNode = parentStack[parentStack.length - 2];
370
- s.prependRight(topNode.start, `const ${id2.name} = ${binding};
371
- `);
372
- }
373
- } else if (
374
- // don't transform class name identifier
375
- !info.classExpression
376
- ) {
377
- s.update(id2.start, id2.end, binding);
378
- }
379
- },
380
425
  // TODO: make env updatable
381
426
  onImportMeta() {
382
427
  },
383
428
  onDynamicImport(node) {
384
- const replace = "__vitest_browser_runner__.wrapModule(import(";
429
+ const replace = "__vitest_browser_runner__.wrapModule(() => import(";
385
430
  s.overwrite(node.start, node.source.start, replace);
386
431
  s.overwrite(node.end - 1, node.end, "))");
387
432
  }
388
433
  });
389
- s.prepend(`const ${viInjectedKey} = { [Symbol.toStringTag]: "Module" };
390
- `);
391
- s.append(`
392
- export { ${viInjectedKey} }`);
393
434
  return {
394
435
  ast,
395
436
  code: s.toString(),
@@ -397,6 +438,23 @@ export { ${viInjectedKey} }`);
397
438
  };
398
439
  }
399
440
 
441
+ const regexDynamicImport = /import\s*\(/;
442
+ var DynamicImport = () => {
443
+ return {
444
+ name: "vitest:browser:esm-injector",
445
+ enforce: "post",
446
+ transform(source, id) {
447
+ if (!regexDynamicImport.test(source))
448
+ return;
449
+ return injectDynamicImport(source, id, this.parse);
450
+ }
451
+ };
452
+ };
453
+
454
+ function defineBrowserCommand(fn) {
455
+ return fn;
456
+ }
457
+
400
458
  var index = (project, base = "/") => {
401
459
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
402
460
  const distRoot = resolve(pkgRoot, "dist");
@@ -412,9 +470,12 @@ var index = (project, base = "/") => {
412
470
  }
413
471
  },
414
472
  async configureServer(server) {
415
- const testerHtml = readFile(resolve(distRoot, "client/tester.html"), "utf8");
416
- const runnerHtml = readFile(resolve(distRoot, "client/index.html"), "utf8");
417
- const injectorJs = readFile(resolve(distRoot, "client/esm-client-injector.js"), "utf8");
473
+ const testerHtml = readFile$1(resolve(distRoot, "client/tester.html"), "utf8");
474
+ const orchestratorHtml = project.config.browser.ui ? readFile$1(resolve(distRoot, "client/__vitest__/index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8");
475
+ const injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8");
476
+ const manifest = (async () => {
477
+ return JSON.parse(await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8"));
478
+ })();
418
479
  const favicon = `${base}favicon.svg`;
419
480
  const testerPrefix = `${base}__vitest_test__/__test__/`;
420
481
  server.middlewares.use((_req, res, next) => {
@@ -425,7 +486,7 @@ var index = (project, base = "/") => {
425
486
  }
426
487
  next();
427
488
  });
428
- let indexScripts;
489
+ let orchestratorScripts;
429
490
  let testerScripts;
430
491
  server.middlewares.use(async (req, res, next) => {
431
492
  if (!req.url)
@@ -435,29 +496,70 @@ var index = (project, base = "/") => {
435
496
  return next();
436
497
  res.setHeader("Cache-Control", "no-cache, max-age=0, must-revalidate");
437
498
  res.setHeader("Content-Type", "text/html; charset=utf-8");
438
- const files = project.browserState?.files ?? [];
439
499
  const config = wrapConfig(project.getSerializableConfig());
440
500
  config.env ??= {};
441
501
  config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || "";
442
- const injector = replacer(await injectorJs, {
443
- __VITEST_CONFIG__: JSON.stringify(config),
444
- __VITEST_FILES__: JSON.stringify(files)
445
- });
502
+ res.removeHeader("X-Frame-Options");
446
503
  if (url.pathname === base) {
447
- if (!indexScripts)
448
- indexScripts = await formatScripts(project.config.browser.indexScripts, server);
449
- const html2 = replacer(await runnerHtml, {
504
+ let contextId2 = url.searchParams.get("contextId");
505
+ if (!contextId2)
506
+ contextId2 = project.browserState.keys().next().value ?? "none";
507
+ const files2 = project.browserState.get(contextId2)?.files ?? [];
508
+ const injector2 = replacer(await injectorJs, {
509
+ __VITEST_CONFIG__: JSON.stringify(config),
510
+ __VITEST_VITE_CONFIG__: JSON.stringify({
511
+ root: project.browser.config.root
512
+ }),
513
+ __VITEST_FILES__: JSON.stringify(files2),
514
+ __VITEST_TYPE__: url.pathname === base ? '"orchestrator"' : '"tester"',
515
+ __VITEST_CONTEXT_ID__: JSON.stringify(contextId2)
516
+ });
517
+ res.removeHeader("Content-Security-Policy");
518
+ if (!orchestratorScripts)
519
+ orchestratorScripts = await formatScripts(project.config.browser.orchestratorScripts, server);
520
+ let baseHtml = await orchestratorHtml;
521
+ if (project.config.browser.ui) {
522
+ const manifestContent = await manifest;
523
+ const jsEntry = manifestContent["orchestrator.html"].file;
524
+ baseHtml = baseHtml.replaceAll("./assets/", `${base}__vitest__/assets/`).replace(
525
+ "<!-- !LOAD_METADATA! -->",
526
+ [
527
+ "<script>{__VITEST_INJECTOR__}<\/script>",
528
+ "{__VITEST_SCRIPTS__}",
529
+ `<script type="module" crossorigin src="${jsEntry}"><\/script>`
530
+ ].join("\n")
531
+ );
532
+ }
533
+ const html2 = replacer(baseHtml, {
450
534
  __VITEST_FAVICON__: favicon,
451
535
  __VITEST_TITLE__: "Vitest Browser Runner",
452
- __VITEST_SCRIPTS__: indexScripts,
453
- __VITEST_INJECTOR__: injector
536
+ __VITEST_SCRIPTS__: orchestratorScripts,
537
+ __VITEST_INJECTOR__: injector2,
538
+ __VITEST_CONTEXT_ID__: JSON.stringify(contextId2)
454
539
  });
455
540
  res.write(html2, "utf-8");
456
541
  res.end();
457
542
  return;
458
543
  }
459
- const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length));
460
- const tests = decodedTestFile === "__vitest_all__" || !files.includes(decodedTestFile) ? "__vitest_browser_runner__.files" : JSON.stringify([decodedTestFile]);
544
+ const csp = res.getHeader("Content-Security-Policy");
545
+ if (typeof csp === "string") {
546
+ res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
547
+ }
548
+ const [contextId, testFile] = url.pathname.slice(testerPrefix.length).split("/");
549
+ const decodedTestFile = decodeURIComponent(testFile);
550
+ const testFiles = await project.globTestFiles();
551
+ const tests = decodedTestFile === "__vitest_all__" || !testFiles.includes(decodedTestFile) ? "__vitest_browser_runner__.files" : JSON.stringify([decodedTestFile]);
552
+ const iframeId = JSON.stringify(decodedTestFile);
553
+ const files = project.browserState.get(contextId)?.files ?? [];
554
+ const injector = replacer(await injectorJs, {
555
+ __VITEST_CONFIG__: JSON.stringify(config),
556
+ __VITEST_FILES__: JSON.stringify(files),
557
+ __VITEST_VITE_CONFIG__: JSON.stringify({
558
+ root: project.browser.config.root
559
+ }),
560
+ __VITEST_TYPE__: url.pathname === base ? '"orchestrator"' : '"tester"',
561
+ __VITEST_CONTEXT_ID__: JSON.stringify(contextId)
562
+ });
461
563
  if (!testerScripts)
462
564
  testerScripts = await formatScripts(project.config.browser.testerScripts, server);
463
565
  const html = replacer(await testerHtml, {
@@ -469,6 +571,7 @@ var index = (project, base = "/") => {
469
571
  // TODO: have only a single global variable to not pollute the global scope
470
572
  `<script type="module">
471
573
  __vitest_browser_runner__.runningFiles = ${tests}
574
+ __vitest_browser_runner__.iframeId = ${iframeId}
472
575
  __vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles)
473
576
  <\/script>`
474
577
  )
@@ -500,24 +603,21 @@ var index = (project, base = "/") => {
500
603
  name: "vitest:browser:tests",
501
604
  enforce: "pre",
502
605
  async config() {
503
- const {
504
- include,
505
- exclude,
506
- includeSource,
507
- dir,
508
- root
509
- } = project.config;
510
- const projectRoot = dir || root;
511
- const entries = await project.globAllTestFiles(include, exclude, includeSource, projectRoot);
606
+ const allTestFiles = await project.globTestFiles();
607
+ const browserTestFiles = allTestFiles.filter((file) => getFilePoolName(project, file) === "browser");
608
+ const setupFiles = toArray(project.config.setupFiles);
609
+ const vitestPaths = [
610
+ resolve(distDir, "index.js"),
611
+ resolve(distDir, "browser.js"),
612
+ resolve(distDir, "runners.js"),
613
+ resolve(distDir, "utils.js")
614
+ ];
512
615
  return {
513
616
  optimizeDeps: {
514
617
  entries: [
515
- ...entries,
516
- "vitest",
517
- "vitest/utils",
518
- "vitest/browser",
519
- "vitest/runners",
520
- "@vitest/utils"
618
+ ...browserTestFiles,
619
+ ...setupFiles,
620
+ ...vitestPaths
521
621
  ],
522
622
  exclude: [
523
623
  "vitest",
@@ -525,31 +625,35 @@ var index = (project, base = "/") => {
525
625
  "vitest/browser",
526
626
  "vitest/runners",
527
627
  "@vitest/utils",
528
- // loupe is manually transformed
529
- "loupe"
628
+ "@vitest/runner",
629
+ "@vitest/spy",
630
+ "@vitest/utils/error",
631
+ "@vitest/snapshot",
632
+ "@vitest/expect",
633
+ "std-env",
634
+ "tinybench",
635
+ "tinyspy",
636
+ "pathe",
637
+ "msw",
638
+ "msw/browser"
530
639
  ],
531
640
  include: [
532
641
  "vitest > @vitest/utils > pretty-format",
533
642
  "vitest > @vitest/snapshot > pretty-format",
534
643
  "vitest > @vitest/snapshot > magic-string",
535
- "vitest > diff-sequences",
536
644
  "vitest > pretty-format",
537
645
  "vitest > pretty-format > ansi-styles",
538
646
  "vitest > pretty-format > ansi-regex",
539
- "vitest > chai"
647
+ "vitest > chai",
648
+ "vitest > chai > loupe",
649
+ "vitest > @vitest/runner > p-limit",
650
+ "vitest > @vitest/utils > diff-sequences",
651
+ "@vitest/browser > @testing-library/user-event",
652
+ "@vitest/browser > @testing-library/dom"
540
653
  ]
541
654
  }
542
655
  };
543
656
  },
544
- transform(code, id) {
545
- if (id.includes("loupe/loupe.js")) {
546
- const exportsList = ["custom", "inspect", "registerConstructor", "registerStringTag"];
547
- const codeAppend = exportsList.map((i) => `export const ${i} = globalThis.loupe.${i}`).join("\n");
548
- return `${code}
549
- ${codeAppend}
550
- export default globalThis.loupe`;
551
- }
552
- },
553
657
  async resolveId(id) {
554
658
  if (!/\?browserv=\w+$/.test(id))
555
659
  return;
@@ -562,13 +666,47 @@ export default globalThis.loupe`;
562
666
  }
563
667
  },
564
668
  {
565
- name: "vitest:browser:esm-injector",
566
- enforce: "post",
567
- transform(source, id) {
568
- const hijackESM = project.config.browser.slowHijackESM ?? false;
569
- if (!hijackESM)
570
- return;
571
- return injectVitestModule(source, id, this.parse);
669
+ name: "vitest:browser:resolve-virtual",
670
+ async resolveId(rawId) {
671
+ if (rawId.startsWith("/__virtual_vitest__:")) {
672
+ let id = rawId.slice("/__virtual_vitest__:".length);
673
+ if (id === "mocker-worker.js")
674
+ id = "msw/mockServiceWorker.js";
675
+ const resolved = await this.resolve(
676
+ id,
677
+ distRoot,
678
+ {
679
+ skipSelf: true
680
+ }
681
+ );
682
+ return resolved;
683
+ }
684
+ }
685
+ },
686
+ BrowserContext(project),
687
+ DynamicImport(),
688
+ // TODO: remove this when @testing-library/vue supports ESM
689
+ {
690
+ name: "vitest:browser:support-vue-testing-library",
691
+ config() {
692
+ return {
693
+ optimizeDeps: {
694
+ esbuildOptions: {
695
+ plugins: [
696
+ {
697
+ name: "test-utils-rewrite",
698
+ setup(build) {
699
+ const _require = createRequire(import.meta.url);
700
+ build.onResolve({ filter: /@vue\/test-utils/ }, (args) => {
701
+ const resolved = _require.resolve(args.path, { paths: [args.importer] });
702
+ return { path: resolved };
703
+ });
704
+ }
705
+ }
706
+ ]
707
+ }
708
+ }
709
+ };
572
710
  }
573
711
  }
574
712
  ];
@@ -583,7 +721,7 @@ function resolveCoverageFolder(project) {
583
721
  if (!htmlReporter)
584
722
  return void 0;
585
723
  const root = resolve(
586
- options.root || options.root || process.cwd(),
724
+ options.root || process.cwd(),
587
725
  options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory
588
726
  );
589
727
  const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : void 0;
@@ -599,7 +737,7 @@ function wrapConfig(config) {
599
737
  };
600
738
  }
601
739
  function replacer(code, values) {
602
- return code.replace(/{\s*(\w+)\s*}/g, (_, key) => values[key] ?? "");
740
+ return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? "");
603
741
  }
604
742
  async function formatScripts(scripts, server) {
605
743
  if (!scripts?.length)
@@ -614,4 +752,4 @@ async function formatScripts(scripts, server) {
614
752
  return (await Promise.all(promises)).join("\n");
615
753
  }
616
754
 
617
- export { index as default };
755
+ export { index as default, defineBrowserCommand };