@vitest/browser 3.1.1 → 3.1.2

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
@@ -4,7 +4,7 @@ import c from 'tinyrainbow';
4
4
  import { getFilePoolName, distDir, resolveApiServerConfig, resolveFsAllow, isFileServingAllowed, createDebugger, isValidApiRequest, createViteLogger, createViteServer } from 'vitest/node';
5
5
  import fs, { readFileSync, lstatSync, promises, existsSync } from 'node:fs';
6
6
  import { createRequire } from 'node:module';
7
- import { slash as slash$1, toArray } from '@vitest/utils';
7
+ import { slash as slash$1, toArray, createDefer } from '@vitest/utils';
8
8
  import MagicString from 'magic-string';
9
9
  import sirv from 'sirv';
10
10
  import * as vite from 'vite';
@@ -13,12 +13,12 @@ import { fileURLToPath } from 'node:url';
13
13
  import crypto from 'node:crypto';
14
14
  import { mkdir, readFile as readFile$1 } from 'node:fs/promises';
15
15
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
16
- import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-2iYWIzBv.js';
16
+ import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-BzjiKqT8.js';
17
17
  import { resolve as resolve$1, dirname as dirname$1, basename as basename$1, normalize as normalize$1 } from 'node:path';
18
18
  import { WebSocketServer } from 'ws';
19
19
  import * as nodeos from 'node:os';
20
20
 
21
- var version = "3.1.1";
21
+ var version = "3.1.2";
22
22
 
23
23
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
24
24
  function normalizeWindowsPath(input = "") {
@@ -213,6 +213,113 @@ const basename = function(p, extension) {
213
213
  const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
214
214
  const distRoot = resolve(pkgRoot, "dist");
215
215
 
216
+ /// <reference types="../types/index.d.ts" />
217
+
218
+ // (c) 2020-present Andrea Giammarchi
219
+
220
+ const {parse: $parse, stringify: $stringify} = JSON;
221
+ const {keys} = Object;
222
+
223
+ const Primitive = String; // it could be Number
224
+ const primitive = 'string'; // it could be 'number'
225
+
226
+ const ignore = {};
227
+ const object = 'object';
228
+
229
+ const noop = (_, value) => value;
230
+
231
+ const primitives = value => (
232
+ value instanceof Primitive ? Primitive(value) : value
233
+ );
234
+
235
+ const Primitives = (_, value) => (
236
+ typeof value === primitive ? new Primitive(value) : value
237
+ );
238
+
239
+ const revive = (input, parsed, output, $) => {
240
+ const lazy = [];
241
+ for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
242
+ const k = ke[y];
243
+ const value = output[k];
244
+ if (value instanceof Primitive) {
245
+ const tmp = input[value];
246
+ if (typeof tmp === object && !parsed.has(tmp)) {
247
+ parsed.add(tmp);
248
+ output[k] = ignore;
249
+ lazy.push({k, a: [input, parsed, tmp, $]});
250
+ }
251
+ else
252
+ output[k] = $.call(output, k, tmp);
253
+ }
254
+ else if (output[k] !== ignore)
255
+ output[k] = $.call(output, k, value);
256
+ }
257
+ for (let {length} = lazy, i = 0; i < length; i++) {
258
+ const {k, a} = lazy[i];
259
+ output[k] = $.call(output, k, revive.apply(null, a));
260
+ }
261
+ return output;
262
+ };
263
+
264
+ const set = (known, input, value) => {
265
+ const index = Primitive(input.push(value) - 1);
266
+ known.set(value, index);
267
+ return index;
268
+ };
269
+
270
+ /**
271
+ * Converts a specialized flatted string into a JS value.
272
+ * @param {string} text
273
+ * @param {(this: any, key: string, value: any) => any} [reviver]
274
+ * @returns {any}
275
+ */
276
+ const parse = (text, reviver) => {
277
+ const input = $parse(text, Primitives).map(primitives);
278
+ const value = input[0];
279
+ const $ = reviver || noop;
280
+ const tmp = typeof value === object && value ?
281
+ revive(input, new Set, value, $) :
282
+ value;
283
+ return $.call({'': tmp}, '', tmp);
284
+ };
285
+
286
+ /**
287
+ * Converts a JS value into a specialized flatted string.
288
+ * @param {any} value
289
+ * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
290
+ * @param {string | number | undefined} [space]
291
+ * @returns {string}
292
+ */
293
+ const stringify = (value, replacer, space) => {
294
+ const $ = replacer && typeof replacer === object ?
295
+ (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
296
+ (replacer || noop);
297
+ const known = new Map;
298
+ const input = [];
299
+ const output = [];
300
+ let i = +set(known, input, $.call({'': value}, '', value));
301
+ let firstRun = !i;
302
+ while (i < input.length) {
303
+ firstRun = true;
304
+ output[i] = $stringify(input[i++], replace, space);
305
+ }
306
+ return '[' + output.join(',') + ']';
307
+ function replace(key, value) {
308
+ if (firstRun) {
309
+ firstRun = !firstRun;
310
+ return value;
311
+ }
312
+ const after = $.call(this, key, value);
313
+ switch (typeof after) {
314
+ case object:
315
+ if (after === null) return after;
316
+ case primitive:
317
+ return known.get(after) || set(known, input, after);
318
+ }
319
+ return after;
320
+ }
321
+ };
322
+
216
323
  function replacer(code, values) {
217
324
  return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _);
218
325
  }
@@ -249,7 +356,6 @@ async function resolveOrchestrator(globalServer, url, res) {
249
356
  sessionId = contexts[contexts.length - 1] ?? "none";
250
357
  }
251
358
  const session = globalServer.vitest._browserSessions.getSession(sessionId);
252
- const files = session?.files ?? [];
253
359
  const browserProject = session?.project.browser || [...globalServer.children][0];
254
360
  if (!browserProject) {
255
361
  return;
@@ -259,12 +365,11 @@ async function resolveOrchestrator(globalServer, url, res) {
259
365
  __VITEST_PROVIDER__: JSON.stringify(browserProject.config.browser.provider || "preview"),
260
366
  __VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()),
261
367
  __VITEST_VITE_CONFIG__: JSON.stringify({ root: browserProject.vite.config.root }),
262
- __VITEST_METHOD__: JSON.stringify(session?.method || "run"),
263
- __VITEST_FILES__: JSON.stringify(files),
368
+ __VITEST_METHOD__: JSON.stringify("orchestrate"),
264
369
  __VITEST_TYPE__: "\"orchestrator\"",
265
370
  __VITEST_SESSION_ID__: JSON.stringify(sessionId),
266
371
  __VITEST_TESTER_ID__: "\"none\"",
267
- __VITEST_PROVIDED_CONTEXT__: "{}",
372
+ __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(browserProject.project.getProvidedContext())),
268
373
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
269
374
  });
270
375
  res.removeHeader("Content-Security-Policy");
@@ -327,119 +432,12 @@ function createOrchestratorMiddleware(parentServer) {
327
432
  };
328
433
  }
329
434
 
330
- /// <reference types="../types/index.d.ts" />
331
-
332
- // (c) 2020-present Andrea Giammarchi
333
-
334
- const {parse: $parse, stringify: $stringify} = JSON;
335
- const {keys} = Object;
336
-
337
- const Primitive = String; // it could be Number
338
- const primitive = 'string'; // it could be 'number'
339
-
340
- const ignore = {};
341
- const object = 'object';
342
-
343
- const noop = (_, value) => value;
344
-
345
- const primitives = value => (
346
- value instanceof Primitive ? Primitive(value) : value
347
- );
348
-
349
- const Primitives = (_, value) => (
350
- typeof value === primitive ? new Primitive(value) : value
351
- );
352
-
353
- const revive = (input, parsed, output, $) => {
354
- const lazy = [];
355
- for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
356
- const k = ke[y];
357
- const value = output[k];
358
- if (value instanceof Primitive) {
359
- const tmp = input[value];
360
- if (typeof tmp === object && !parsed.has(tmp)) {
361
- parsed.add(tmp);
362
- output[k] = ignore;
363
- lazy.push({k, a: [input, parsed, tmp, $]});
364
- }
365
- else
366
- output[k] = $.call(output, k, tmp);
367
- }
368
- else if (output[k] !== ignore)
369
- output[k] = $.call(output, k, value);
370
- }
371
- for (let {length} = lazy, i = 0; i < length; i++) {
372
- const {k, a} = lazy[i];
373
- output[k] = $.call(output, k, revive.apply(null, a));
374
- }
375
- return output;
376
- };
377
-
378
- const set = (known, input, value) => {
379
- const index = Primitive(input.push(value) - 1);
380
- known.set(value, index);
381
- return index;
382
- };
383
-
384
- /**
385
- * Converts a specialized flatted string into a JS value.
386
- * @param {string} text
387
- * @param {(this: any, key: string, value: any) => any} [reviver]
388
- * @returns {any}
389
- */
390
- const parse = (text, reviver) => {
391
- const input = $parse(text, Primitives).map(primitives);
392
- const value = input[0];
393
- const $ = reviver || noop;
394
- const tmp = typeof value === object && value ?
395
- revive(input, new Set, value, $) :
396
- value;
397
- return $.call({'': tmp}, '', tmp);
398
- };
399
-
400
- /**
401
- * Converts a JS value into a specialized flatted string.
402
- * @param {any} value
403
- * @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
404
- * @param {string | number | undefined} [space]
405
- * @returns {string}
406
- */
407
- const stringify = (value, replacer, space) => {
408
- const $ = replacer && typeof replacer === object ?
409
- (k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
410
- (replacer || noop);
411
- const known = new Map;
412
- const input = [];
413
- const output = [];
414
- let i = +set(known, input, $.call({'': value}, '', value));
415
- let firstRun = !i;
416
- while (i < input.length) {
417
- firstRun = true;
418
- output[i] = $stringify(input[i++], replace, space);
419
- }
420
- return '[' + output.join(',') + ']';
421
- function replace(key, value) {
422
- if (firstRun) {
423
- firstRun = !firstRun;
424
- return value;
425
- }
426
- const after = $.call(this, key, value);
427
- switch (typeof after) {
428
- case object:
429
- if (after === null) return after;
430
- case primitive:
431
- return known.get(after) || set(known, input, after);
432
- }
433
- return after;
434
- }
435
- };
436
-
437
435
  async function resolveTester(globalServer, url, res, next) {
438
436
  const csp = res.getHeader("Content-Security-Policy");
439
437
  if (typeof csp === "string") {
440
438
  res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
441
439
  }
442
- const { sessionId, testFile } = globalServer.resolveTesterUrl(url.pathname);
440
+ const sessionId = url.searchParams.get("sessionId") || "none";
443
441
  const session = globalServer.vitest._browserSessions.getSession(sessionId);
444
442
  if (!session) {
445
443
  res.statusCode = 400;
@@ -447,11 +445,6 @@ async function resolveTester(globalServer, url, res, next) {
447
445
  return;
448
446
  }
449
447
  const project = globalServer.vitest.getProjectByName(session.project.name || "");
450
- const { testFiles } = await project.globTestFiles();
451
- const tests = testFile === "__vitest_all__" || !testFiles.includes(testFile) ? "__vitest_browser_runner__.files" : JSON.stringify([testFile]);
452
- const iframeId = JSON.stringify(testFile);
453
- const files = session.files ?? [];
454
- const method = session.method ?? "run";
455
448
  const browserProject = project.browser || [...globalServer.children][0];
456
449
  if (!browserProject) {
457
450
  res.statusCode = 400;
@@ -462,13 +455,12 @@ async function resolveTester(globalServer, url, res, next) {
462
455
  const injector = replacer(injectorJs, {
463
456
  __VITEST_PROVIDER__: JSON.stringify(project.browser.provider.name),
464
457
  __VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()),
465
- __VITEST_FILES__: JSON.stringify(files),
466
458
  __VITEST_VITE_CONFIG__: JSON.stringify({ root: browserProject.vite.config.root }),
467
459
  __VITEST_TYPE__: "\"tester\"",
468
- __VITEST_METHOD__: JSON.stringify(method),
460
+ __VITEST_METHOD__: JSON.stringify("none"),
469
461
  __VITEST_SESSION_ID__: JSON.stringify(sessionId),
470
462
  __VITEST_TESTER_ID__: JSON.stringify(crypto.randomUUID()),
471
- __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext())),
463
+ __VITEST_PROVIDED_CONTEXT__: "{}",
472
464
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
473
465
  });
474
466
  const testerHtml = typeof browserProject.testerHtml === "string" ? browserProject.testerHtml : await browserProject.testerHtml;
@@ -477,17 +469,11 @@ async function resolveTester(globalServer, url, res, next) {
477
469
  const indexhtml = await browserProject.vite.transformIndexHtml(url, testerHtml);
478
470
  const html = replacer(indexhtml, {
479
471
  __VITEST_FAVICON__: globalServer.faviconUrl,
480
- __VITEST_INJECTOR__: injector,
481
- __VITEST_APPEND__: `
482
- __vitest_browser_runner__.runningFiles = ${tests}
483
- __vitest_browser_runner__.iframeId = ${iframeId}
484
- __vitest_browser_runner__.${method === "run" ? "runTests" : "collectTests"}(__vitest_browser_runner__.runningFiles)
485
- document.querySelector('script[data-vitest-append]').remove()
486
- `
472
+ __VITEST_INJECTOR__: injector
487
473
  });
488
474
  return html;
489
475
  } catch (err) {
490
- session.reject(err);
476
+ session.fail(err);
491
477
  next(err);
492
478
  }
493
479
  }
@@ -498,7 +484,7 @@ function createTesterMiddleware(browserServer) {
498
484
  return next();
499
485
  }
500
486
  const url = new URL(req.url, "http://localhost");
501
- if (!url.pathname.startsWith(browserServer.prefixTesterUrl)) {
487
+ if (!url.pathname.startsWith(browserServer.prefixTesterUrl) || !url.searchParams.has("sessionId")) {
502
488
  return next();
503
489
  }
504
490
  const html = await resolveTester(browserServer, url, res, next);
@@ -961,16 +947,7 @@ body {
961
947
  injectTo: "head"
962
948
  } : null,
963
949
  ...parentServer.testerScripts,
964
- ...testerTags,
965
- {
966
- tag: "script",
967
- attrs: {
968
- "type": "module",
969
- "data-vitest-append": ""
970
- },
971
- children: "{__VITEST_APPEND__}",
972
- injectTo: "body"
973
- }
950
+ ...testerTags
974
951
  ].filter((s) => s != null);
975
952
  }
976
953
  },
@@ -1216,6 +1193,7 @@ const types = {
1216
1193
  'application/dash+xml': ['mpd'],
1217
1194
  'application/dash-patch+xml': ['mpp'],
1218
1195
  'application/davmount+xml': ['davmount'],
1196
+ 'application/dicom': ['dcm'],
1219
1197
  'application/docbook+xml': ['dbk'],
1220
1198
  'application/dssc+der': ['dssc'],
1221
1199
  'application/dssc+xml': ['xdssc'],
@@ -1302,7 +1280,14 @@ const types = {
1302
1280
  'application/oebps-package+xml': ['opf'],
1303
1281
  'application/ogg': ['ogx'],
1304
1282
  'application/omdoc+xml': ['omdoc'],
1305
- 'application/onenote': ['onetoc', 'onetoc2', 'onetmp', 'onepkg'],
1283
+ 'application/onenote': [
1284
+ 'onetoc',
1285
+ 'onetoc2',
1286
+ 'onetmp',
1287
+ 'onepkg',
1288
+ 'one',
1289
+ 'onea',
1290
+ ],
1306
1291
  'application/oxps': ['oxps'],
1307
1292
  'application/p2p-overlay+xml': ['relo'],
1308
1293
  'application/patch-ops-error+xml': ['xer'],
@@ -1398,6 +1383,7 @@ const types = {
1398
1383
  'application/yang': ['yang'],
1399
1384
  'application/yin+xml': ['yin'],
1400
1385
  'application/zip': ['zip'],
1386
+ 'application/zip+dotlottie': ['lottie'],
1401
1387
  'audio/3gpp': ['*3gpp'],
1402
1388
  'audio/aac': ['adts', 'aac'],
1403
1389
  'audio/adpcm': ['adp'],
@@ -1406,7 +1392,7 @@ const types = {
1406
1392
  'audio/midi': ['mid', 'midi', 'kar', 'rmi'],
1407
1393
  'audio/mobile-xmf': ['mxmf'],
1408
1394
  'audio/mp3': ['*mp3'],
1409
- 'audio/mp4': ['m4a', 'mp4a'],
1395
+ 'audio/mp4': ['m4a', 'mp4a', 'm4b'],
1410
1396
  'audio/mpeg': ['mpga', 'mp2', 'mp2a', 'mp3', 'm2a', 'm3a'],
1411
1397
  'audio/ogg': ['oga', 'ogg', 'spx', 'opus'],
1412
1398
  'audio/s3m': ['s3m'],
@@ -1438,11 +1424,12 @@ const types = {
1438
1424
  'image/heif': ['heif'],
1439
1425
  'image/heif-sequence': ['heifs'],
1440
1426
  'image/hej2k': ['hej2'],
1441
- 'image/hsj2': ['hsj2'],
1442
1427
  'image/ief': ['ief'],
1428
+ 'image/jaii': ['jaii'],
1429
+ 'image/jais': ['jais'],
1443
1430
  'image/jls': ['jls'],
1444
1431
  'image/jp2': ['jp2', 'jpg2'],
1445
- 'image/jpeg': ['jpeg', 'jpg', 'jpe'],
1432
+ 'image/jpeg': ['jpg', 'jpeg', 'jpe'],
1446
1433
  'image/jph': ['jph'],
1447
1434
  'image/jphc': ['jhc'],
1448
1435
  'image/jpm': ['jpm', 'jpgm'],
@@ -1457,6 +1444,7 @@ const types = {
1457
1444
  'image/jxss': ['jxss'],
1458
1445
  'image/ktx': ['ktx'],
1459
1446
  'image/ktx2': ['ktx2'],
1447
+ 'image/pjpeg': ['jfif'],
1460
1448
  'image/png': ['png'],
1461
1449
  'image/sgi': ['sgi'],
1462
1450
  'image/svg+xml': ['svg', 'svgz'],
@@ -1470,7 +1458,7 @@ const types = {
1470
1458
  'message/global-delivery-status': ['u8dsn'],
1471
1459
  'message/global-disposition-notification': ['u8mdn'],
1472
1460
  'message/global-headers': ['u8hdr'],
1473
- 'message/rfc822': ['eml', 'mime'],
1461
+ 'message/rfc822': ['eml', 'mime', 'mht', 'mhtml'],
1474
1462
  'model/3mf': ['3mf'],
1475
1463
  'model/gltf+json': ['gltf'],
1476
1464
  'model/gltf-binary': ['glb'],
@@ -1480,6 +1468,7 @@ const types = {
1480
1468
  'model/mtl': ['mtl'],
1481
1469
  'model/obj': ['obj'],
1482
1470
  'model/prc': ['prc'],
1471
+ 'model/step': ['step', 'stp', 'stpnc', 'p21', '210'],
1483
1472
  'model/step+xml': ['stpx'],
1484
1473
  'model/step+zip': ['stpz'],
1485
1474
  'model/step-xml+zip': ['stpxz'],
@@ -1586,8 +1575,8 @@ class Mime {
1586
1575
  getType(path) {
1587
1576
  if (typeof path !== 'string')
1588
1577
  return null;
1589
- const last = path.replace(/^.*[/\\]/, '').toLowerCase();
1590
- const ext = last.replace(/^.*\./, '').toLowerCase();
1578
+ const last = path.replace(/^.*[/\\]/s, '').toLowerCase();
1579
+ const ext = last.replace(/^.*\./s, '').toLowerCase();
1591
1580
  const hasPath = last.length < path.length;
1592
1581
  const hasDot = ext.length < last.length - 1;
1593
1582
  if (!hasDot && hasPath)
@@ -2718,14 +2707,13 @@ class ParentBrowserProject {
2718
2707
  if (!provider.getCDPSession) {
2719
2708
  throw new Error(`CDP is not supported by the provider "${provider.name}".`);
2720
2709
  }
2721
- const promise = this.cdpSessionsPromises.get(rpcId) ?? await (async () => {
2710
+ const session = await this.cdpSessionsPromises.get(rpcId) ?? await (async () => {
2722
2711
  const promise = provider.getCDPSession(sessionId).finally(() => {
2723
2712
  this.cdpSessionsPromises.delete(rpcId);
2724
2713
  });
2725
2714
  this.cdpSessionsPromises.set(rpcId, promise);
2726
2715
  return promise;
2727
2716
  })();
2728
- const session = await promise;
2729
2717
  const rpc = browser.state.testers.get(rpcId);
2730
2718
  if (!rpc) {
2731
2719
  throw new Error(`Tester RPC "${rpcId}" was not established.`);
@@ -2781,6 +2769,8 @@ class ParentBrowserProject {
2781
2769
  }
2782
2770
  }
2783
2771
 
2772
+ const TYPE_REQUEST = "q";
2773
+ const TYPE_RESPONSE = "s";
2784
2774
  const DEFAULT_TIMEOUT = 6e4;
2785
2775
  function defaultSerialize(i) {
2786
2776
  return i;
@@ -2813,7 +2803,7 @@ function createBirpc(functions, options) {
2813
2803
  if (method === "then" && !eventNames.includes("then") && !("then" in functions))
2814
2804
  return void 0;
2815
2805
  const sendEvent = (...args) => {
2816
- post(serialize({ m: method, a: args, t: "q" }));
2806
+ post(serialize({ m: method, a: args, t: TYPE_REQUEST }));
2817
2807
  };
2818
2808
  if (eventNames.includes(method)) {
2819
2809
  sendEvent.asEvent = sendEvent;
@@ -2835,8 +2825,9 @@ function createBirpc(functions, options) {
2835
2825
  if (timeout >= 0) {
2836
2826
  timeoutId = setTimeout(() => {
2837
2827
  try {
2838
- options.onTimeoutError?.(method, args);
2839
- throw new Error(`[birpc] timeout on calling "${method}"`);
2828
+ const handleResult = options.onTimeoutError?.(method, args);
2829
+ if (handleResult !== true)
2830
+ throw new Error(`[birpc] timeout on calling "${method}"`);
2840
2831
  } catch (e) {
2841
2832
  reject(e);
2842
2833
  }
@@ -2853,17 +2844,24 @@ function createBirpc(functions, options) {
2853
2844
  return sendCall;
2854
2845
  }
2855
2846
  });
2856
- function close() {
2847
+ function close(error) {
2857
2848
  closed = true;
2858
2849
  rpcPromiseMap.forEach(({ reject, method }) => {
2859
- reject(new Error(`[birpc] rpc is closed, cannot call "${method}"`));
2850
+ reject(error || new Error(`[birpc] rpc is closed, cannot call "${method}"`));
2860
2851
  });
2861
2852
  rpcPromiseMap.clear();
2862
2853
  off(onMessage);
2863
2854
  }
2864
2855
  async function onMessage(data, ...extra) {
2865
- const msg = deserialize(data);
2866
- if (msg.t === "q") {
2856
+ let msg;
2857
+ try {
2858
+ msg = deserialize(data);
2859
+ } catch (e) {
2860
+ if (options.onGeneralError?.(e) !== true)
2861
+ throw e;
2862
+ return;
2863
+ }
2864
+ if (msg.t === TYPE_REQUEST) {
2867
2865
  const { m: method, a: args } = msg;
2868
2866
  let result, error;
2869
2867
  const fn = resolver ? resolver(method, functions[method]) : functions[method];
@@ -2879,7 +2877,26 @@ function createBirpc(functions, options) {
2879
2877
  if (msg.i) {
2880
2878
  if (error && options.onError)
2881
2879
  options.onError(error, method, args);
2882
- post(serialize({ t: "s", i: msg.i, r: result, e: error }), ...extra);
2880
+ if (error && options.onFunctionError) {
2881
+ if (options.onFunctionError(error, method, args) === true)
2882
+ return;
2883
+ }
2884
+ if (!error) {
2885
+ try {
2886
+ post(serialize({ t: TYPE_RESPONSE, i: msg.i, r: result }), ...extra);
2887
+ return;
2888
+ } catch (e) {
2889
+ error = e;
2890
+ if (options.onGeneralError?.(e, method, args) !== true)
2891
+ throw e;
2892
+ }
2893
+ }
2894
+ try {
2895
+ post(serialize({ t: TYPE_RESPONSE, i: msg.i, e: error }), ...extra);
2896
+ } catch (e) {
2897
+ if (options.onGeneralError?.(e, method, args) !== true)
2898
+ throw e;
2899
+ }
2883
2900
  }
2884
2901
  } else {
2885
2902
  const { i: ack, r: result, e: error } = msg;
@@ -2934,10 +2951,6 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2934
2951
  if (!sessionId || !rpcId || projectName == null) {
2935
2952
  return error(new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" queries are required.`));
2936
2953
  }
2937
- const method = searchParams.get("method");
2938
- if (method !== "run" && method !== "collect") {
2939
- return error(new Error(`[vitest] Method query in ${request.url} is invalid. Method should be either "run" or "collect".`));
2940
- }
2941
2954
  if (type === "orchestrator") {
2942
2955
  const session = vitest._browserSessions.getSession(sessionId);
2943
2956
  session?.connected();
@@ -2948,7 +2961,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2948
2961
  }
2949
2962
  wss.handleUpgrade(request, socket, head, (ws) => {
2950
2963
  wss.emit("connection", ws, request);
2951
- const rpc = setupClient(project, rpcId, ws, method);
2964
+ const rpc = setupClient(project, rpcId, ws);
2952
2965
  const state = project.browser.state;
2953
2966
  const clients = type === "tester" ? state.testers : state.orchestrators;
2954
2967
  clients.set(rpcId, rpc);
@@ -2957,6 +2970,10 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2957
2970
  debug$1?.("[%s] Browser API disconnected from %s", rpcId, type);
2958
2971
  clients.delete(rpcId);
2959
2972
  globalServer.removeCDPHandler(rpcId);
2973
+ if (type === "orchestrator") {
2974
+ vitest._browserSessions.destroySession(sessionId);
2975
+ }
2976
+ rpc.$close(new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`));
2960
2977
  });
2961
2978
  });
2962
2979
  });
@@ -2969,7 +2986,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2969
2986
  throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
2970
2987
  }
2971
2988
  }
2972
- function setupClient(project, rpcId, ws, method) {
2989
+ function setupClient(project, rpcId, ws) {
2973
2990
  const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config.server?.deps?.moduleDirectories });
2974
2991
  const mocker = project.browser?.provider.mocker;
2975
2992
  const rpc = createBirpc({
@@ -2980,21 +2997,21 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2980
2997
  }
2981
2998
  vitest.state.catchError(error, type);
2982
2999
  },
2983
- async onQueued(file) {
3000
+ async onQueued(method, file) {
2984
3001
  if (method === "collect") {
2985
3002
  vitest.state.collectFiles(project, [file]);
2986
3003
  } else {
2987
3004
  await vitest._testRun.enqueued(project, file);
2988
3005
  }
2989
3006
  },
2990
- async onCollected(files) {
3007
+ async onCollected(method, files) {
2991
3008
  if (method === "collect") {
2992
3009
  vitest.state.collectFiles(project, files);
2993
3010
  } else {
2994
3011
  await vitest._testRun.collected(project, files);
2995
3012
  }
2996
3013
  },
2997
- async onTaskUpdate(packs, events) {
3014
+ async onTaskUpdate(method, packs, events) {
2998
3015
  if (method === "collect") {
2999
3016
  vitest.state.updateTasks(packs);
3000
3017
  } else {
@@ -3004,7 +3021,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3004
3021
  onAfterSuiteRun(meta) {
3005
3022
  vitest.coverageProvider?.onAfterSuiteRun(meta);
3006
3023
  },
3007
- async sendLog(log) {
3024
+ async sendLog(method, log) {
3008
3025
  if (method === "collect") {
3009
3026
  vitest.state.updateUserLog(log);
3010
3027
  } else {
@@ -3088,10 +3105,6 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3088
3105
  }, provider.getCommandsContext(sessionId));
3089
3106
  return await commands[command](context, ...payload);
3090
3107
  },
3091
- finishBrowserTests(sessionId) {
3092
- debug$1?.("[%s] Finishing browser tests for session", sessionId);
3093
- return vitest._browserSessions.getSession(sessionId)?.resolve();
3094
- },
3095
3108
  resolveMock(rawId, importer, options) {
3096
3109
  return mockResolver.resolveMock(rawId, importer, options);
3097
3110
  },
@@ -3160,6 +3173,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3160
3173
  on: (fn) => ws.on("message", fn),
3161
3174
  eventNames: ["onCancel", "cdpEvent"],
3162
3175
  serialize: (data) => stringify(data, stringifyReplace),
3176
+ timeout: -1,
3163
3177
  deserialize: parse,
3164
3178
  onTimeoutError(functionName) {
3165
3179
  throw new Error(`[vitest-api]: Timeout calling "${functionName}"`);
@@ -3194,69 +3208,30 @@ function stringifyReplace(key, value) {
3194
3208
  }
3195
3209
 
3196
3210
  const debug = createDebugger("vitest:browser:pool");
3197
- async function waitForTests(method, sessionId, project, files) {
3198
- const context = project.vitest._browserSessions.createAsyncSession(method, sessionId, files, project);
3199
- return await context;
3200
- }
3201
3211
  function createBrowserPool(vitest) {
3202
3212
  const providers = new Set();
3203
- const executeTests = async (method, project, files) => {
3204
- vitest.state.clearFiles(project, files);
3205
- const browser = project.browser;
3206
- const threadsCount = getThreadsCount(project);
3207
- const provider = browser.provider;
3208
- providers.add(provider);
3209
- const resolvedUrls = browser.vite.resolvedUrls;
3213
+ const numCpus = typeof nodeos.availableParallelism === "function" ? nodeos.availableParallelism() : nodeos.cpus().length;
3214
+ const threadsCount = vitest.config.watch ? Math.max(Math.floor(numCpus / 2), 1) : Math.max(numCpus - 1, 1);
3215
+ const projectPools = new WeakMap();
3216
+ const ensurePool = (project) => {
3217
+ if (projectPools.has(project)) {
3218
+ return projectPools.get(project);
3219
+ }
3220
+ debug?.("creating pool for project %s", project.name);
3221
+ const resolvedUrls = project.browser.vite.resolvedUrls;
3210
3222
  const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
3211
3223
  if (!origin) {
3212
- throw new Error(`Can't find browser origin URL for project "${project.name}" when running tests for files "${files.join("\", \"")}"`);
3224
+ throw new Error(`Can't find browser origin URL for project "${project.name}"`);
3213
3225
  }
3214
- async function setBreakpoint(sessionId, file) {
3215
- if (!project.config.inspector.waitForDebugger) {
3216
- return;
3217
- }
3218
- if (!provider.getCDPSession) {
3219
- throw new Error("Unable to set breakpoint, CDP not supported");
3220
- }
3221
- const session = await provider.getCDPSession(sessionId);
3222
- await session.send("Debugger.enable", {});
3223
- await session.send("Debugger.setBreakpointByUrl", {
3224
- lineNumber: 0,
3225
- urlRegex: escapePathToRegexp(file)
3226
- });
3227
- }
3228
- const filesPerThread = Math.ceil(files.length / threadsCount);
3229
- const chunks = [];
3230
- for (let i = 0; i < files.length; i += filesPerThread) {
3231
- const chunk = files.slice(i, i + filesPerThread);
3232
- chunks.push(chunk);
3233
- }
3234
- debug?.(`[%s] Running %s tests in %s chunks (%s threads)`, project.name || "core", files.length, chunks.length, threadsCount);
3235
- const orchestrators = [...browser.state.orchestrators.entries()];
3236
- const promises = [];
3237
- chunks.forEach((files, index) => {
3238
- if (orchestrators[index]) {
3239
- const [sessionId, orchestrator] = orchestrators[index];
3240
- debug?.("Reusing orchestrator (session %s) for files: %s", sessionId, [...files.map((f) => relative(project.config.root, f))].join(", "));
3241
- const promise = waitForTests(method, sessionId, project, files);
3242
- const tester = orchestrator.createTesters(files).catch((error) => {
3243
- if (error instanceof Error && error.message.startsWith("[birpc] rpc is closed")) {
3244
- return;
3245
- }
3246
- return Promise.reject(error);
3247
- });
3248
- promises.push(promise, tester);
3249
- } else {
3250
- const sessionId = crypto.randomUUID();
3251
- const waitPromise = waitForTests(method, sessionId, project, files);
3252
- debug?.("Opening a new session %s for files: %s", sessionId, [...files.map((f) => relative(project.config.root, f))].join(", "));
3253
- const url = new URL("/", origin);
3254
- url.searchParams.set("sessionId", sessionId);
3255
- const page = provider.openPage(sessionId, url.toString(), () => setBreakpoint(sessionId, files[0]));
3256
- promises.push(page, waitPromise);
3257
- }
3226
+ const pool = new BrowserPool(project, {
3227
+ maxWorkers: getThreadsCount(project),
3228
+ origin
3258
3229
  });
3259
- await Promise.all(promises);
3230
+ projectPools.set(project, pool);
3231
+ vitest.onCancel(() => {
3232
+ pool.cancel();
3233
+ });
3234
+ return pool;
3260
3235
  };
3261
3236
  const runWorkspaceTests = async (method, specs) => {
3262
3237
  const groupedFiles = new Map();
@@ -3269,30 +3244,29 @@ function createBrowserPool(vitest) {
3269
3244
  vitest.onCancel(() => {
3270
3245
  isCancelled = true;
3271
3246
  });
3272
- for (const [project, files] of groupedFiles.entries()) {
3273
- if (isCancelled) {
3274
- break;
3275
- }
3247
+ await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
3276
3248
  await project._initBrowserProvider();
3277
3249
  if (!project.browser) {
3278
3250
  throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ""}. This is a bug in Vitest. Please, open a new issue with reproduction.`);
3279
3251
  }
3280
- await executeTests(method, project, files);
3281
- }
3252
+ if (isCancelled) {
3253
+ return;
3254
+ }
3255
+ const pool = ensurePool(project);
3256
+ vitest.state.clearFiles(project, files);
3257
+ providers.add(project.browser.provider);
3258
+ await pool.runTests(method, files);
3259
+ }));
3282
3260
  };
3283
- const numCpus = typeof nodeos.availableParallelism === "function" ? nodeos.availableParallelism() : nodeos.cpus().length;
3284
3261
  function getThreadsCount(project) {
3285
3262
  const config = project.config.browser;
3286
- if (!config.headless || !project.browser.provider.supportsParallelism) {
3287
- return 1;
3288
- }
3289
- if (!config.fileParallelism) {
3263
+ if (!config.headless || !config.fileParallelism || !project.browser.provider.supportsParallelism) {
3290
3264
  return 1;
3291
3265
  }
3292
3266
  if (project.config.maxWorkers) {
3293
3267
  return project.config.maxWorkers;
3294
3268
  }
3295
- return vitest.config.watch ? Math.max(Math.floor(numCpus / 2), 1) : Math.max(numCpus - 1, 1);
3269
+ return threadsCount;
3296
3270
  }
3297
3271
  return {
3298
3272
  name: "browser",
@@ -3312,6 +3286,132 @@ function createBrowserPool(vitest) {
3312
3286
  function escapePathToRegexp(path) {
3313
3287
  return path.replace(/[/\\.?*()^${}|[\]+]/g, "\\$&");
3314
3288
  }
3289
+ class BrowserPool {
3290
+ _queue = [];
3291
+ _promise;
3292
+ _providedContext;
3293
+ readySessions = new Set();
3294
+ constructor(project, options) {
3295
+ this.project = project;
3296
+ this.options = options;
3297
+ }
3298
+ cancel() {
3299
+ this._queue = [];
3300
+ }
3301
+ reject(error) {
3302
+ this._promise?.reject(error);
3303
+ this._promise = undefined;
3304
+ this.cancel();
3305
+ }
3306
+ get orchestrators() {
3307
+ return this.project.browser.state.orchestrators;
3308
+ }
3309
+ async runTests(method, files) {
3310
+ this._promise ??= createDefer();
3311
+ if (!files.length) {
3312
+ this._promise.resolve();
3313
+ return this._promise;
3314
+ }
3315
+ this._providedContext = stringify(this.project.getProvidedContext());
3316
+ this._queue.push(...files);
3317
+ this.readySessions.forEach((sessionId) => {
3318
+ if (this._queue.length) {
3319
+ this.readySessions.delete(sessionId);
3320
+ this.runNextTest(method, sessionId);
3321
+ }
3322
+ });
3323
+ if (this.orchestrators.size >= this.options.maxWorkers) {
3324
+ return this._promise;
3325
+ }
3326
+ const workerCount = Math.min(this.options.maxWorkers - this.orchestrators.size, files.length);
3327
+ const promises = [];
3328
+ for (let i = 0; i < workerCount; i++) {
3329
+ const sessionId = crypto.randomUUID();
3330
+ const page = this.openPage(sessionId).then(() => {
3331
+ this.runNextTest(method, sessionId);
3332
+ });
3333
+ promises.push(page);
3334
+ }
3335
+ await Promise.all(promises);
3336
+ return this._promise;
3337
+ }
3338
+ async openPage(sessionId) {
3339
+ const sessionPromise = this.project.vitest._browserSessions.createSession(sessionId, this.project, this);
3340
+ const url = new URL("/", this.options.origin);
3341
+ url.searchParams.set("sessionId", sessionId);
3342
+ const pagePromise = this.project.browser.provider.openPage(sessionId, url.toString());
3343
+ await Promise.all([sessionPromise, pagePromise]);
3344
+ }
3345
+ getOrchestrator(sessionId) {
3346
+ const orchestrator = this.orchestrators.get(sessionId);
3347
+ if (!orchestrator) {
3348
+ throw new Error(`Orchestrator not found for session ${sessionId}. This is a bug in Vitest. Please, open a new issue with reproduction.`);
3349
+ }
3350
+ return orchestrator;
3351
+ }
3352
+ finishSession(sessionId) {
3353
+ this.readySessions.add(sessionId);
3354
+ if (this.readySessions.size === this.orchestrators.size) {
3355
+ this._promise?.resolve();
3356
+ this._promise = undefined;
3357
+ debug?.("all tests finished running");
3358
+ }
3359
+ }
3360
+ runNextTest(method, sessionId) {
3361
+ const file = this._queue.shift();
3362
+ if (!file) {
3363
+ debug?.("[%s] no more tests to run", sessionId);
3364
+ const isolate = this.project.config.browser.isolate;
3365
+ if (isolate) {
3366
+ this.finishSession(sessionId);
3367
+ return;
3368
+ }
3369
+ const orchestrator = this.getOrchestrator(sessionId);
3370
+ orchestrator.cleanupTesters().catch((error) => this.reject(error)).finally(() => this.finishSession(sessionId));
3371
+ return;
3372
+ }
3373
+ if (!this._promise) {
3374
+ throw new Error(`Unexpected empty queue`);
3375
+ }
3376
+ const orchestrator = this.getOrchestrator(sessionId);
3377
+ debug?.("[%s] run test %s", sessionId, file);
3378
+ this.setBreakpoint(sessionId, file).then(() => {
3379
+ orchestrator.createTesters({
3380
+ method,
3381
+ files: [file],
3382
+ providedContext: this._providedContext || "[{}]"
3383
+ }).then(() => {
3384
+ debug?.("[%s] test %s finished running", sessionId, file);
3385
+ this.runNextTest(method, sessionId);
3386
+ }).catch((error) => {
3387
+ if (this.project.vitest.isCancelling && error instanceof Error && error.message.startsWith("Browser connection was closed while running tests")) {
3388
+ this.cancel();
3389
+ this._promise?.resolve();
3390
+ this._promise = undefined;
3391
+ return;
3392
+ }
3393
+ debug?.("[%s] error during %s test run: %s", sessionId, file, error);
3394
+ this.reject(error);
3395
+ });
3396
+ }).catch((err) => this.reject(err));
3397
+ }
3398
+ async setBreakpoint(sessionId, file) {
3399
+ if (!this.project.config.inspector.waitForDebugger) {
3400
+ return;
3401
+ }
3402
+ const provider = this.project.browser.provider;
3403
+ if (!provider.getCDPSession) {
3404
+ throw new Error("Unable to set breakpoint, CDP not supported");
3405
+ }
3406
+ debug?.("[%s] set breakpoint for %s", sessionId, file);
3407
+ const session = await provider.getCDPSession(sessionId);
3408
+ await session.send("Debugger.enable", {});
3409
+ await session.send("Debugger.setBreakpointByUrl", {
3410
+ lineNumber: 0,
3411
+ urlRegex: escapePathToRegexp(file)
3412
+ });
3413
+ }
3414
+ }
3315
3415
 
3316
3416
  async function createBrowserServer(project, configFile, prePlugins = [], postPlugins = []) {
3317
3417
  if (project.vitest.version !== version) {