@vitest/browser 2.0.0-beta.1 → 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,8 +1,13 @@
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
 
@@ -17,6 +22,7 @@ function normalizeWindowsPath(input = "") {
17
22
  const _UNC_REGEX = /^[/\\]{2}/;
18
23
  const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
19
24
  const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
25
+ const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
20
26
  const normalize = function(path) {
21
27
  if (path.length === 0) {
22
28
  return ".";
@@ -151,78 +157,261 @@ function normalizeString(path, allowAboveRoot) {
151
157
  const isAbsolute = function(p) {
152
158
  return _IS_ABSOLUTE_RE.test(p);
153
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
+ };
154
183
  const basename = function(p, extension) {
155
184
  const lastSegment = normalizeWindowsPath(p).split("/").pop();
156
185
  return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
157
186
  };
158
187
 
159
- /**
160
- * @param {import('estree').Node} param
161
- * @returns {string[]}
162
- */
163
- function extract_names(param) {
164
- return extract_identifiers(param).map((node) => node.name);
165
- }
166
-
167
- /**
168
- * @param {import('estree').Node} param
169
- * @param {import('estree').Identifier[]} nodes
170
- * @returns {import('estree').Identifier[]}
171
- */
172
- function extract_identifiers(param, nodes = []) {
173
- switch (param.type) {
174
- case 'Identifier':
175
- nodes.push(param);
176
- break;
177
-
178
- case 'MemberExpression':
179
- let object = param;
180
- while (object.type === 'MemberExpression') {
181
- object = /** @type {any} */ (object.object);
182
- }
183
- nodes.push(/** @type {any} */ (object));
184
- break;
185
-
186
- case 'ObjectPattern':
187
- for (const prop of param.properties) {
188
- if (prop.type === 'RestElement') {
189
- extract_identifiers(prop.argument, nodes);
190
- } else {
191
- extract_identifiers(prop.value, nodes);
192
- }
193
- }
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
+ };
194
204
 
195
- 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
+ };
196
229
 
197
- case 'ArrayPattern':
198
- for (const element of param.elements) {
199
- if (element) extract_identifiers(element, nodes);
200
- }
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
+ };
201
296
 
202
- 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
+ }
203
341
 
204
- case 'RestElement':
205
- extract_identifiers(param.argument, nodes);
206
- break;
342
+ var builtinCommands = {
343
+ readFile,
344
+ removeFile,
345
+ writeFile,
346
+ sendKeys,
347
+ __vitest_click: click,
348
+ __vitest_screenshot: screenshot
349
+ };
207
350
 
208
- case 'AssignmentPattern':
209
- extract_identifiers(param.left, nodes);
210
- break;
211
- }
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
212
390
 
213
- 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}`)}'`;
214
412
  }
215
413
 
216
- const viInjectedKey = "__vi_inject__";
217
- const viExportAllHelper = "__vitest_browser_runner__.exportAll";
218
- const skipHijack = [
219
- "/@vite/client",
220
- "/@vite/env",
221
- /vite\/dist\/client/
222
- ];
223
- function injectVitestModule(code, id, parse) {
224
- if (skipHijack.some((skip) => id.match(skip)))
225
- return;
414
+ function injectDynamicImport(code, id, parse) {
226
415
  const s = new MagicString(code);
227
416
  let ast;
228
417
  try {
@@ -232,165 +421,16 @@ function injectVitestModule(code, id, parse) {
232
421
  ${err.message}`);
233
422
  return;
234
423
  }
235
- let uid = 0;
236
- const idToImportMap = /* @__PURE__ */ new Map();
237
- const declaredConst = /* @__PURE__ */ new Set();
238
- const hoistIndex = 0;
239
- const transformImportDeclaration = (node) => {
240
- const source = node.source.value;
241
- if (skipHijack.some((skip) => source.match(skip)))
242
- return null;
243
- const importId = `__vi_esm_${uid++}__`;
244
- const hasSpecifiers = node.specifiers.length > 0;
245
- const code2 = hasSpecifiers ? `import { ${viInjectedKey} as ${importId} } from '${source}'
246
- ` : `import '${source}'
247
- `;
248
- return {
249
- code: code2,
250
- id: importId
251
- };
252
- };
253
- function defineImport(node) {
254
- const declaration = transformImportDeclaration(node);
255
- if (!declaration)
256
- return null;
257
- s.appendLeft(hoistIndex, declaration.code);
258
- return declaration.id;
259
- }
260
- function defineImportAll(source) {
261
- const importId = `__vi_esm_${uid++}__`;
262
- s.appendLeft(hoistIndex, `const { ${viInjectedKey}: ${importId} } = await import(${JSON.stringify(source)});
263
- `);
264
- return importId;
265
- }
266
- function defineExport(position, name, local = name) {
267
- s.appendLeft(
268
- position,
269
- `
270
- Object.defineProperty(${viInjectedKey}, "${name}", { enumerable: true, configurable: true, get(){ return ${local} }});`
271
- );
272
- }
273
- for (const node of ast.body) {
274
- if (node.type === "ImportDeclaration") {
275
- const importId = defineImport(node);
276
- if (!importId)
277
- continue;
278
- s.remove(node.start, node.end);
279
- for (const spec of node.specifiers) {
280
- if (spec.type === "ImportSpecifier") {
281
- idToImportMap.set(
282
- spec.local.name,
283
- `${importId}.${spec.imported.name}`
284
- );
285
- } else if (spec.type === "ImportDefaultSpecifier") {
286
- idToImportMap.set(spec.local.name, `${importId}.default`);
287
- } else {
288
- idToImportMap.set(spec.local.name, importId);
289
- }
290
- }
291
- }
292
- }
293
- for (const node of ast.body) {
294
- if (node.type === "ExportNamedDeclaration") {
295
- if (node.declaration) {
296
- if (node.declaration.type === "FunctionDeclaration" || node.declaration.type === "ClassDeclaration") {
297
- defineExport(node.end, node.declaration.id.name);
298
- } else {
299
- for (const declaration of node.declaration.declarations) {
300
- const names = extract_names(declaration.id);
301
- for (const name of names)
302
- defineExport(node.end, name);
303
- }
304
- }
305
- s.remove(node.start, node.declaration.start);
306
- } else {
307
- s.remove(node.start, node.end);
308
- if (node.source) {
309
- const importId = defineImportAll(node.source.value);
310
- for (const spec of node.specifiers) {
311
- defineExport(
312
- hoistIndex,
313
- spec.exported.name,
314
- `${importId}.${spec.local.name}`
315
- );
316
- }
317
- } else {
318
- for (const spec of node.specifiers) {
319
- const local = spec.local.name;
320
- const binding = idToImportMap.get(local);
321
- defineExport(node.end, spec.exported.name, binding || local);
322
- }
323
- }
324
- }
325
- }
326
- if (node.type === "ExportDefaultDeclaration") {
327
- const expressionTypes = ["FunctionExpression", "ClassExpression"];
328
- if ("id" in node.declaration && node.declaration.id && !expressionTypes.includes(node.declaration.type)) {
329
- const { name } = node.declaration.id;
330
- s.remove(
331
- node.start,
332
- node.start + 15
333
- /* 'export default '.length */
334
- );
335
- s.append(
336
- `
337
- Object.defineProperty(${viInjectedKey}, "default", { enumerable: true, configurable: true, value: ${name} });`
338
- );
339
- } else {
340
- s.update(
341
- node.start,
342
- node.start + 14,
343
- `${viInjectedKey}.default =`
344
- );
345
- s.append(`
346
- export default { ${viInjectedKey}: ${viInjectedKey}.default };
347
- `);
348
- }
349
- }
350
- if (node.type === "ExportAllDeclaration") {
351
- s.remove(node.start, node.end);
352
- const importId = defineImportAll(node.source.value);
353
- if (node.exported)
354
- defineExport(hoistIndex, node.exported.name, `${importId}`);
355
- else
356
- s.appendLeft(hoistIndex, `${viExportAllHelper}(${viInjectedKey}, ${importId});
357
- `);
358
- }
359
- }
360
424
  esmWalker(ast, {
361
- onIdentifier(id2, info, parentStack) {
362
- const binding = idToImportMap.get(id2.name);
363
- if (!binding)
364
- return;
365
- if (info.hasBindingShortcut) {
366
- s.appendLeft(id2.end, `: ${binding}`);
367
- } else if (info.classDeclaration) {
368
- if (!declaredConst.has(id2.name)) {
369
- declaredConst.add(id2.name);
370
- const topNode = parentStack[parentStack.length - 2];
371
- s.prependRight(topNode.start, `const ${id2.name} = ${binding};
372
- `);
373
- }
374
- } else if (
375
- // don't transform class name identifier
376
- !info.classExpression
377
- ) {
378
- s.update(id2.start, id2.end, binding);
379
- }
380
- },
381
425
  // TODO: make env updatable
382
426
  onImportMeta() {
383
427
  },
384
428
  onDynamicImport(node) {
385
- const replace = "__vitest_browser_runner__.wrapModule(import(";
429
+ const replace = "__vitest_browser_runner__.wrapModule(() => import(";
386
430
  s.overwrite(node.start, node.source.start, replace);
387
431
  s.overwrite(node.end - 1, node.end, "))");
388
432
  }
389
433
  });
390
- s.prepend(`const ${viInjectedKey} = { [Symbol.toStringTag]: "Module" };
391
- `);
392
- s.append(`
393
- export { ${viInjectedKey} }`);
394
434
  return {
395
435
  ast,
396
436
  code: s.toString(),
@@ -398,6 +438,23 @@ export { ${viInjectedKey} }`);
398
438
  };
399
439
  }
400
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
+
401
458
  var index = (project, base = "/") => {
402
459
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
403
460
  const distRoot = resolve(pkgRoot, "dist");
@@ -413,9 +470,12 @@ var index = (project, base = "/") => {
413
470
  }
414
471
  },
415
472
  async configureServer(server) {
416
- const testerHtml = readFile(resolve(distRoot, "client/tester.html"), "utf8");
417
- const runnerHtml = readFile(resolve(distRoot, "client/index.html"), "utf8");
418
- 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
+ })();
419
479
  const favicon = `${base}favicon.svg`;
420
480
  const testerPrefix = `${base}__vitest_test__/__test__/`;
421
481
  server.middlewares.use((_req, res, next) => {
@@ -426,7 +486,7 @@ var index = (project, base = "/") => {
426
486
  }
427
487
  next();
428
488
  });
429
- let indexScripts;
489
+ let orchestratorScripts;
430
490
  let testerScripts;
431
491
  server.middlewares.use(async (req, res, next) => {
432
492
  if (!req.url)
@@ -436,29 +496,70 @@ var index = (project, base = "/") => {
436
496
  return next();
437
497
  res.setHeader("Cache-Control", "no-cache, max-age=0, must-revalidate");
438
498
  res.setHeader("Content-Type", "text/html; charset=utf-8");
439
- const files = project.browserState?.files ?? [];
440
499
  const config = wrapConfig(project.getSerializableConfig());
441
500
  config.env ??= {};
442
501
  config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || "";
443
- const injector = replacer(await injectorJs, {
444
- __VITEST_CONFIG__: JSON.stringify(config),
445
- __VITEST_FILES__: JSON.stringify(files)
446
- });
502
+ res.removeHeader("X-Frame-Options");
447
503
  if (url.pathname === base) {
448
- if (!indexScripts)
449
- indexScripts = await formatScripts(project.config.browser.indexScripts, server);
450
- 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, {
451
534
  __VITEST_FAVICON__: favicon,
452
535
  __VITEST_TITLE__: "Vitest Browser Runner",
453
- __VITEST_SCRIPTS__: indexScripts,
454
- __VITEST_INJECTOR__: injector
536
+ __VITEST_SCRIPTS__: orchestratorScripts,
537
+ __VITEST_INJECTOR__: injector2,
538
+ __VITEST_CONTEXT_ID__: JSON.stringify(contextId2)
455
539
  });
456
540
  res.write(html2, "utf-8");
457
541
  res.end();
458
542
  return;
459
543
  }
460
- const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length));
461
- 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
+ });
462
563
  if (!testerScripts)
463
564
  testerScripts = await formatScripts(project.config.browser.testerScripts, server);
464
565
  const html = replacer(await testerHtml, {
@@ -470,6 +571,7 @@ var index = (project, base = "/") => {
470
571
  // TODO: have only a single global variable to not pollute the global scope
471
572
  `<script type="module">
472
573
  __vitest_browser_runner__.runningFiles = ${tests}
574
+ __vitest_browser_runner__.iframeId = ${iframeId}
473
575
  __vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles)
474
576
  <\/script>`
475
577
  )
@@ -501,24 +603,21 @@ var index = (project, base = "/") => {
501
603
  name: "vitest:browser:tests",
502
604
  enforce: "pre",
503
605
  async config() {
504
- const {
505
- include,
506
- exclude,
507
- includeSource,
508
- dir,
509
- root
510
- } = project.config;
511
- const projectRoot = dir || root;
512
- 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
+ ];
513
615
  return {
514
616
  optimizeDeps: {
515
617
  entries: [
516
- ...entries,
517
- "vitest",
518
- "vitest/utils",
519
- "vitest/browser",
520
- "vitest/runners",
521
- "@vitest/utils"
618
+ ...browserTestFiles,
619
+ ...setupFiles,
620
+ ...vitestPaths
522
621
  ],
523
622
  exclude: [
524
623
  "vitest",
@@ -526,31 +625,35 @@ var index = (project, base = "/") => {
526
625
  "vitest/browser",
527
626
  "vitest/runners",
528
627
  "@vitest/utils",
529
- // loupe is manually transformed
530
- "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"
531
639
  ],
532
640
  include: [
533
641
  "vitest > @vitest/utils > pretty-format",
534
642
  "vitest > @vitest/snapshot > pretty-format",
535
643
  "vitest > @vitest/snapshot > magic-string",
536
- "vitest > diff-sequences",
537
644
  "vitest > pretty-format",
538
645
  "vitest > pretty-format > ansi-styles",
539
646
  "vitest > pretty-format > ansi-regex",
540
- "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"
541
653
  ]
542
654
  }
543
655
  };
544
656
  },
545
- transform(code, id) {
546
- if (id.includes("loupe/loupe.js")) {
547
- const exportsList = ["custom", "inspect", "registerConstructor", "registerStringTag"];
548
- const codeAppend = exportsList.map((i) => `export const ${i} = globalThis.loupe.${i}`).join("\n");
549
- return `${code}
550
- ${codeAppend}
551
- export default globalThis.loupe`;
552
- }
553
- },
554
657
  async resolveId(id) {
555
658
  if (!/\?browserv=\w+$/.test(id))
556
659
  return;
@@ -563,13 +666,47 @@ export default globalThis.loupe`;
563
666
  }
564
667
  },
565
668
  {
566
- name: "vitest:browser:esm-injector",
567
- enforce: "post",
568
- transform(source, id) {
569
- const hijackESM = project.config.browser.slowHijackESM ?? false;
570
- if (!hijackESM)
571
- return;
572
- 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
+ };
573
710
  }
574
711
  }
575
712
  ];
@@ -584,7 +721,7 @@ function resolveCoverageFolder(project) {
584
721
  if (!htmlReporter)
585
722
  return void 0;
586
723
  const root = resolve(
587
- options.root || options.root || process.cwd(),
724
+ options.root || process.cwd(),
588
725
  options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory
589
726
  );
590
727
  const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : void 0;
@@ -600,7 +737,7 @@ function wrapConfig(config) {
600
737
  };
601
738
  }
602
739
  function replacer(code, values) {
603
- return code.replace(/{\s*(\w+)\s*}/g, (_, key) => values[key] ?? "");
740
+ return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? "");
604
741
  }
605
742
  async function formatScripts(scripts, server) {
606
743
  if (!scripts?.length)
@@ -615,4 +752,4 @@ async function formatScripts(scripts, server) {
615
752
  return (await Promise.all(promises)).join("\n");
616
753
  }
617
754
 
618
- export { index as default };
755
+ export { index as default, defineBrowserCommand };