@vitest/browser 2.0.0-beta.2 → 2.0.0-beta.5

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,11 @@
1
1
  import { fileURLToPath } from 'node:url';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFile as readFile$1 } from 'node:fs/promises';
3
3
  import sirv from 'sirv';
4
4
  import { coverageConfigDefaults } from 'vitest/config';
5
5
  import { slash } from '@vitest/utils';
6
+ import fs, { promises } from 'node:fs';
7
+ import { resolve as resolve$1, dirname } from 'node:path';
8
+ import { isFileServingAllowed } from 'vitest/node';
6
9
  import MagicString from 'magic-string';
7
10
  import { esmWalker } from '@vitest/utils/ast';
8
11
 
@@ -156,241 +159,347 @@ const basename = function(p, extension) {
156
159
  return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
157
160
  };
158
161
 
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);
162
+ function assertFileAccess(path, project) {
163
+ if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server))
164
+ throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
165
165
  }
166
+ const readFile = async ({ project, testPath = process.cwd() }, path, options = {}) => {
167
+ const filepath = resolve$1(dirname(testPath), path);
168
+ assertFileAccess(filepath, project);
169
+ if (typeof options === "object" && !options.encoding)
170
+ options.encoding = "utf-8";
171
+ return promises.readFile(filepath, options);
172
+ };
173
+ const writeFile = async ({ project, testPath = process.cwd() }, path, data, options) => {
174
+ const filepath = resolve$1(dirname(testPath), path);
175
+ assertFileAccess(filepath, project);
176
+ const dir = dirname(filepath);
177
+ if (!fs.existsSync(dir))
178
+ await promises.mkdir(dir, { recursive: true });
179
+ await promises.writeFile(filepath, data, options);
180
+ };
181
+ const removeFile = async ({ project, testPath = process.cwd() }, path) => {
182
+ const filepath = resolve$1(dirname(testPath), path);
183
+ assertFileAccess(filepath, project);
184
+ await promises.rm(filepath);
185
+ };
166
186
 
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
- }
194
-
195
- break;
196
-
197
- case 'ArrayPattern':
198
- for (const element of param.elements) {
199
- if (element) extract_identifiers(element, nodes);
200
- }
201
-
202
- break;
203
-
204
- case 'RestElement':
205
- extract_identifiers(param.argument, nodes);
206
- break;
207
-
208
- case 'AssignmentPattern':
209
- extract_identifiers(param.left, nodes);
210
- break;
211
- }
212
-
213
- return nodes;
187
+ function isObject(payload) {
188
+ return payload != null && typeof payload === "object";
214
189
  }
215
-
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;
226
- const s = new MagicString(code);
227
- let ast;
228
- try {
229
- ast = parse(code);
230
- } catch (err) {
231
- console.error(`Cannot parse ${id}:
232
- ${err.message}`);
233
- return;
190
+ function isSendKeysPayload(payload) {
191
+ const validOptions = ["type", "press", "down", "up"];
192
+ if (!isObject(payload))
193
+ throw new Error("You must provide a `SendKeysPayload` object");
194
+ const numberOfValidOptions = Object.keys(payload).filter(
195
+ (key) => validOptions.includes(key)
196
+ ).length;
197
+ const unknownOptions = Object.keys(payload).filter((key) => !validOptions.includes(key));
198
+ if (numberOfValidOptions > 1) {
199
+ throw new Error(
200
+ `You must provide ONLY one of the following properties to pass to the browser runner: ${validOptions.join(
201
+ ", "
202
+ )}.`
203
+ );
234
204
  }
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;
205
+ if (numberOfValidOptions === 0) {
206
+ throw new Error(
207
+ `You must provide one of the following properties to pass to the browser runner: ${validOptions.join(
208
+ ", "
209
+ )}.`
210
+ );
259
211
  }
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;
212
+ if (unknownOptions.length > 0)
213
+ throw new Error(`Unknown options \`${unknownOptions.join(", ")}\` present.`);
214
+ return true;
215
+ }
216
+ function isTypePayload(payload) {
217
+ return "type" in payload;
218
+ }
219
+ function isPressPayload(payload) {
220
+ return "press" in payload;
221
+ }
222
+ function isDownPayload(payload) {
223
+ return "down" in payload;
224
+ }
225
+ function isUpPayload(payload) {
226
+ return "up" in payload;
227
+ }
228
+ const sendKeys = async ({ provider }, payload) => {
229
+ if (!isSendKeysPayload(payload) || !payload)
230
+ throw new Error("You must provide a `SendKeysPayload` object");
231
+ if (provider.name === "playwright") {
232
+ const page = provider.page;
233
+ if (isTypePayload(payload))
234
+ await page.keyboard.type(payload.type);
235
+ else if (isPressPayload(payload))
236
+ await page.keyboard.press(payload.press);
237
+ else if (isDownPayload(payload))
238
+ await page.keyboard.down(payload.down);
239
+ else if (isUpPayload(payload))
240
+ await page.keyboard.up(payload.up);
241
+ } else if (provider.name === "webdriverio") {
242
+ const browser = provider.browser;
243
+ if (isTypePayload(payload))
244
+ await browser.keys(payload.type.split(""));
245
+ else if (isPressPayload(payload))
246
+ await browser.keys([payload.press]);
247
+ else
248
+ throw new Error('Only "press" and "type" are supported by webdriverio.');
249
+ } else {
250
+ throw new Error(`"sendKeys" is not supported for ${provider.name} browser provider.`);
265
251
  }
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
- );
252
+ };
253
+
254
+ var builtinCommands = {
255
+ readFile,
256
+ removeFile,
257
+ writeFile,
258
+ sendKeys
259
+ };
260
+
261
+ const VIRTUAL_ID_CONTEXT = "\0@vitest/browser/context";
262
+ const ID_CONTEXT = "@vitest/browser/context";
263
+ function BrowserContext(project) {
264
+ project.config.browser.commands ??= {};
265
+ for (const [name, command] of Object.entries(builtinCommands))
266
+ project.config.browser.commands[name] ??= command;
267
+ for (const command in project.config.browser.commands) {
268
+ if (!/^[a-z_$][\w$]*$/i.test(command))
269
+ throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
272
270
  }
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
- }
271
+ return {
272
+ name: "vitest:browser:virtual-module:context",
273
+ enforce: "pre",
274
+ resolveId(id) {
275
+ if (id === ID_CONTEXT)
276
+ return VIRTUAL_ID_CONTEXT;
277
+ },
278
+ load(id) {
279
+ if (id === VIRTUAL_ID_CONTEXT)
280
+ return generateContextFile(project);
291
281
  }
282
+ };
283
+ }
284
+ function generateContextFile(project) {
285
+ const commands = Object.keys(project.config.browser.commands ?? {});
286
+ const filepathCode = "__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined";
287
+ const commandsCode = commands.map((command) => {
288
+ return ` ["${command}"]: (...args) => rpc().triggerCommand("${command}", ${filepathCode}, args),`;
289
+ }).join("\n");
290
+ return `
291
+ const rpc = () => __vitest_worker__.rpc
292
+ const channel = new BroadcastChannel('vitest')
293
+
294
+ export const server = {
295
+ platform: ${JSON.stringify(process.platform)},
296
+ version: ${JSON.stringify(process.version)},
297
+ provider: ${JSON.stringify(project.browserProvider.name)},
298
+ browser: ${JSON.stringify(project.config.browser.name)},
299
+ commands: {
300
+ ${commandsCode}
292
301
  }
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
- }
302
+ }
303
+ export const commands = server.commands
304
+ export const page = {
305
+ get config() {
306
+ return __vitest_browser_runner__.config
307
+ },
308
+ viewport(width, height) {
309
+ const id = __vitest_browser_runner__.iframeId
310
+ channel.postMessage({ type: 'viewport', width, height, id })
311
+ return new Promise((resolve) => {
312
+ channel.addEventListener('message', function handler(e) {
313
+ if (e.data.type === 'viewport:done' && e.data.id === id) {
314
+ channel.removeEventListener('message', handler)
315
+ resolve()
304
316
  }
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
- }
317
+ if (e.data.type === 'viewport:fail' && e.data.id === id) {
318
+ channel.removeEventListener('message', handler)
319
+ reject(new Error(e.data.error))
323
320
  }
324
- }
321
+ })
322
+ })
323
+ }
324
+ }
325
+ `;
326
+ }
327
+
328
+ function automockModule(code, parse) {
329
+ const ast = parse(code);
330
+ const m = new MagicString(code);
331
+ const allSpecifiers = [];
332
+ let importIndex = 0;
333
+ for (const _node of ast.body) {
334
+ if (_node.type === "ExportAllDeclaration") {
335
+ throw new Error(
336
+ `automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`
337
+ );
325
338
  }
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
- `);
339
+ if (_node.type === "ExportNamedDeclaration") {
340
+ let traversePattern2 = function(expression) {
341
+ if (expression.type === "Identifier") {
342
+ allSpecifiers.push({ name: expression.name });
343
+ } else if (expression.type === "ArrayPattern") {
344
+ expression.elements.forEach((element) => {
345
+ if (!element)
346
+ return;
347
+ traversePattern2(element);
348
+ });
349
+ } else if (expression.type === "ObjectPattern") {
350
+ expression.properties.forEach((property) => {
351
+ if (property.type === "RestElement")
352
+ traversePattern2(property);
353
+ else if (property.type === "Property")
354
+ traversePattern2(property.value);
355
+ else
356
+ ;
357
+ });
358
+ } else if (expression.type === "RestElement") {
359
+ traversePattern2(expression.argument);
360
+ } else if (expression.type === "AssignmentPattern") {
361
+ throw new Error(`AssignmentPattern is not supported. Please open a new bug report.`);
362
+ } else if (expression.type === "MemberExpression") {
363
+ throw new Error(`MemberExpression is not supported. Please open a new bug report.`);
364
+ } else ;
365
+ };
366
+ const node = _node;
367
+ const declaration = node.declaration;
368
+ if (declaration) {
369
+ if (declaration.type === "FunctionDeclaration") {
370
+ allSpecifiers.push({ name: declaration.id.name });
371
+ } else if (declaration.type === "VariableDeclaration") {
372
+ declaration.declarations.forEach((declaration2) => {
373
+ traversePattern2(declaration2.id);
374
+ });
375
+ } else if (declaration.type === "ClassDeclaration") {
376
+ allSpecifiers.push({ name: declaration.id.name });
377
+ } else ;
378
+ m.remove(node.start, declaration.start);
379
+ }
380
+ const specifiers = node.specifiers || [];
381
+ const source = node.source;
382
+ if (!source && specifiers.length) {
383
+ specifiers.forEach((specifier) => {
384
+ const exported = specifier.exported;
385
+ allSpecifiers.push({
386
+ alias: exported.type === "Literal" ? exported.raw : exported.name,
387
+ name: specifier.local.name
388
+ });
389
+ });
390
+ m.remove(node.start, node.end);
391
+ } else if (source && specifiers.length) {
392
+ const importNames = [];
393
+ specifiers.forEach((specifier) => {
394
+ const importedName = `__vitest_imported_${importIndex++}__`;
395
+ const exported = specifier.exported;
396
+ importNames.push([specifier.local.name, importedName]);
397
+ allSpecifiers.push({
398
+ name: importedName,
399
+ alias: exported.type === "Literal" ? exported.raw : exported.name
400
+ });
401
+ });
402
+ const importString = `import { ${importNames.map(([name, alias]) => `${name} as ${alias}`).join(", ")} } from '${source.value}'`;
403
+ m.overwrite(node.start, node.end, importString);
348
404
  }
349
405
  }
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
- `);
406
+ if (_node.type === "ExportDefaultDeclaration") {
407
+ const node = _node;
408
+ const declaration = node.declaration;
409
+ allSpecifiers.push({ name: "__vitest_default", alias: "default" });
410
+ m.overwrite(node.start, declaration.start, `const __vitest_default = `);
358
411
  }
359
412
  }
360
- 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
- `);
413
+ const moduleObject = `
414
+ const __vitest_es_current_module__ = {
415
+ __esModule: true,
416
+ ${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join("\n ")}
417
+ }
418
+ const __vitest_mocked_module__ = __vitest_mocker__.mockObject(__vitest_es_current_module__)
419
+ `;
420
+ const assigning = allSpecifiers.map(({ name }, index) => {
421
+ return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]`;
422
+ }).join("\n");
423
+ const redeclarations = allSpecifiers.map(({ name, alias }, index) => {
424
+ return ` __vitest_mocked_${index}__ as ${alias || name},`;
425
+ }).join("\n");
426
+ const specifiersExports = `
427
+ export {
428
+ ${redeclarations}
429
+ }
430
+ `;
431
+ m.append(moduleObject + assigning + specifiersExports);
432
+ return m;
433
+ }
434
+
435
+ var BrowserMocker = (project) => {
436
+ return [
437
+ {
438
+ name: "vitest:browser:mocker",
439
+ enforce: "pre",
440
+ async load(id) {
441
+ const data = project.browserMocker.mocks.get(id);
442
+ if (!data)
443
+ return;
444
+ const { mock, sessionId } = data;
445
+ if (mock === void 0) {
446
+ const rpc = project.browserRpc.testers.get(sessionId);
447
+ if (!rpc)
448
+ throw new Error(`WebSocket rpc was destroyed for session ${sessionId}`);
449
+ const exports = await rpc.startMocking(id);
450
+ const module = `const module = __vitest_mocker__.get('${id}');`;
451
+ const keys = exports.map((name) => {
452
+ if (name === "default")
453
+ return `export default module['default'];`;
454
+ return `export const ${name} = module['${name}'];`;
455
+ }).join("\n");
456
+ return `${module}
457
+ ${keys}`;
373
458
  }
374
- } else if (
375
- // don't transform class name identifier
376
- !info.classExpression
377
- ) {
378
- s.update(id2.start, id2.end, binding);
459
+ if (mock === null)
460
+ return;
461
+ return readFile$1(mock, "utf-8");
379
462
  }
380
463
  },
464
+ {
465
+ name: "vitest:browser:automocker",
466
+ enforce: "post",
467
+ transform(code, id) {
468
+ const data = project.browserMocker.mocks.get(id);
469
+ if (!data)
470
+ return;
471
+ if (data.mock === null) {
472
+ const m = automockModule(code, this.parse);
473
+ return {
474
+ code: m.toString(),
475
+ map: m.generateMap({ hires: "boundary", source: id })
476
+ };
477
+ }
478
+ }
479
+ }
480
+ ];
481
+ };
482
+
483
+ function injectDynamicImport(code, id, parse) {
484
+ const s = new MagicString(code);
485
+ let ast;
486
+ try {
487
+ ast = parse(code);
488
+ } catch (err) {
489
+ console.error(`Cannot parse ${id}:
490
+ ${err.message}`);
491
+ return;
492
+ }
493
+ esmWalker(ast, {
381
494
  // TODO: make env updatable
382
495
  onImportMeta() {
383
496
  },
384
497
  onDynamicImport(node) {
385
- const replace = "__vitest_browser_runner__.wrapModule(import(";
498
+ const replace = "__vitest_browser_runner__.wrapModule(() => import(";
386
499
  s.overwrite(node.start, node.source.start, replace);
387
500
  s.overwrite(node.end - 1, node.end, "))");
388
501
  }
389
502
  });
390
- s.prepend(`const ${viInjectedKey} = { [Symbol.toStringTag]: "Module" };
391
- `);
392
- s.append(`
393
- export { ${viInjectedKey} }`);
394
503
  return {
395
504
  ast,
396
505
  code: s.toString(),
@@ -398,10 +507,24 @@ export { ${viInjectedKey} }`);
398
507
  };
399
508
  }
400
509
 
510
+ const regexDynamicImport = /import\s*\(/;
511
+ var DynamicImport = () => {
512
+ return {
513
+ name: "vitest:browser:esm-injector",
514
+ enforce: "post",
515
+ transform(source, id) {
516
+ if (!regexDynamicImport.test(source))
517
+ return;
518
+ return injectDynamicImport(source, id, this.parse);
519
+ }
520
+ };
521
+ };
522
+
401
523
  var index = (project, base = "/") => {
402
524
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
403
525
  const distRoot = resolve(pkgRoot, "dist");
404
526
  return [
527
+ ...BrowserMocker(project),
405
528
  {
406
529
  enforce: "pre",
407
530
  name: "vitest:browser",
@@ -413,9 +536,12 @@ var index = (project, base = "/") => {
413
536
  }
414
537
  },
415
538
  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");
539
+ const testerHtml = readFile$1(resolve(distRoot, "client/tester.html"), "utf8");
540
+ const orchestratorHtml = project.config.browser.ui ? readFile$1(resolve(distRoot, "client/__vitest__/index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8");
541
+ const injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8");
542
+ const manifest = (async () => {
543
+ return JSON.parse(await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8"));
544
+ })();
419
545
  const favicon = `${base}favicon.svg`;
420
546
  const testerPrefix = `${base}__vitest_test__/__test__/`;
421
547
  server.middlewares.use((_req, res, next) => {
@@ -442,12 +568,26 @@ var index = (project, base = "/") => {
442
568
  config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || "";
443
569
  const injector = replacer(await injectorJs, {
444
570
  __VITEST_CONFIG__: JSON.stringify(config),
445
- __VITEST_FILES__: JSON.stringify(files)
571
+ __VITEST_FILES__: JSON.stringify(files),
572
+ __VITEST_TYPE__: url.pathname === base ? '"orchestrator"' : '"tester"'
446
573
  });
447
574
  if (url.pathname === base) {
448
575
  if (!indexScripts)
449
576
  indexScripts = await formatScripts(project.config.browser.indexScripts, server);
450
- const html2 = replacer(await runnerHtml, {
577
+ let baseHtml = await orchestratorHtml;
578
+ if (project.config.browser.ui) {
579
+ const manifestContent = await manifest;
580
+ const jsEntry = manifestContent["orchestrator.html"].file;
581
+ baseHtml = baseHtml.replaceAll("./assets/", `${base}__vitest__/assets/`).replace(
582
+ "<!-- !LOAD_METADATA! -->",
583
+ [
584
+ "<script>{__VITEST_INJECTOR__}<\/script>",
585
+ "{__VITEST_SCRIPTS__}",
586
+ `<script type="module" crossorigin src="${jsEntry}"><\/script>`
587
+ ].join("\n")
588
+ );
589
+ }
590
+ const html2 = replacer(baseHtml, {
451
591
  __VITEST_FAVICON__: favicon,
452
592
  __VITEST_TITLE__: "Vitest Browser Runner",
453
593
  __VITEST_SCRIPTS__: indexScripts,
@@ -459,6 +599,7 @@ var index = (project, base = "/") => {
459
599
  }
460
600
  const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length));
461
601
  const tests = decodedTestFile === "__vitest_all__" || !files.includes(decodedTestFile) ? "__vitest_browser_runner__.files" : JSON.stringify([decodedTestFile]);
602
+ const iframeId = decodedTestFile === "__vitest_all__" ? '"__vitest_all__"' : JSON.stringify(decodedTestFile);
462
603
  if (!testerScripts)
463
604
  testerScripts = await formatScripts(project.config.browser.testerScripts, server);
464
605
  const html = replacer(await testerHtml, {
@@ -470,6 +611,7 @@ var index = (project, base = "/") => {
470
611
  // TODO: have only a single global variable to not pollute the global scope
471
612
  `<script type="module">
472
613
  __vitest_browser_runner__.runningFiles = ${tests}
614
+ __vitest_browser_runner__.iframeId = ${iframeId}
473
615
  __vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles)
474
616
  <\/script>`
475
617
  )
@@ -526,6 +668,9 @@ var index = (project, base = "/") => {
526
668
  "vitest/browser",
527
669
  "vitest/runners",
528
670
  "@vitest/utils",
671
+ "std-env",
672
+ "tinybench",
673
+ "tinyspy",
529
674
  // loupe is manually transformed
530
675
  "loupe"
531
676
  ],
@@ -562,16 +707,8 @@ export default globalThis.loupe`;
562
707
  return useId;
563
708
  }
564
709
  },
565
- {
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);
573
- }
574
- }
710
+ BrowserContext(project),
711
+ DynamicImport()
575
712
  ];
576
713
  };
577
714
  function resolveCoverageFolder(project) {
@@ -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)