@vitest/browser 4.0.0-beta.1 → 4.0.0-beta.11

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.
Files changed (44) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -15
  3. package/context.d.ts +153 -3
  4. package/dist/client/.vite/manifest.json +6 -6
  5. package/dist/client/__vitest__/assets/{index-KbpJLW--.css → index-CCvbyxW7.css} +1 -1
  6. package/dist/client/__vitest__/assets/index-CYIziQD6.js +53 -0
  7. package/dist/client/__vitest__/index.html +2 -2
  8. package/dist/client/__vitest_browser__/orchestrator-DOxlOAkk.js +293 -0
  9. package/dist/client/__vitest_browser__/tester-B7fynsGK.js +2090 -0
  10. package/dist/client/__vitest_browser__/{utils-Owv5OOOf.js → utils-CPmDBIKG.js} +3 -3
  11. package/dist/client/error-catcher.js +7 -3
  12. package/dist/client/esm-client-injector.js +1 -0
  13. package/dist/client/orchestrator.html +2 -2
  14. package/dist/client/tester/tester.html +2 -2
  15. package/dist/client.js +24 -8
  16. package/dist/context.js +29 -22
  17. package/dist/expect-element.js +10 -8
  18. package/dist/index-CwoiDq7G.js +6 -0
  19. package/dist/index-DDlvjJVO.js +1 -0
  20. package/dist/index.d.ts +16 -10
  21. package/dist/index.js +555 -104
  22. package/dist/locators/index.d.ts +8 -7
  23. package/dist/locators/index.js +1 -1
  24. package/dist/locators/playwright.js +1 -1
  25. package/dist/locators/preview.js +1 -1
  26. package/dist/locators/webdriverio.js +1 -1
  27. package/dist/providers/playwright.d.ts +103 -0
  28. package/dist/{webdriver-KA1WiV0q.js → providers/playwright.js} +37 -180
  29. package/dist/providers/preview.d.ts +16 -0
  30. package/dist/{providers.js → providers/preview.js} +17 -21
  31. package/dist/providers/webdriverio.d.ts +50 -0
  32. package/dist/providers/webdriverio.js +171 -0
  33. package/dist/shared/screenshotMatcher/types.d.ts +16 -0
  34. package/dist/state.js +4 -3
  35. package/dist/types.d.ts +5 -7
  36. package/jest-dom.d.ts +95 -1
  37. package/package.json +22 -32
  38. package/utils.d.ts +1 -1
  39. package/dist/client/__vitest__/assets/index-BjtzXzAw.js +0 -58
  40. package/dist/client/__vitest_browser__/orchestrator-CQgVbcQq.js +0 -3213
  41. package/dist/client/__vitest_browser__/tester-BScMoGFI.js +0 -3560
  42. package/dist/index-W1MM53zC.js +0 -1
  43. package/providers/playwright.d.ts +0 -81
  44. package/providers/webdriverio.d.ts +0 -22
package/dist/index.js CHANGED
@@ -1,25 +1,28 @@
1
1
  import { ManualMockedModule, RedirectedModule, AutomockedModule, AutospiedModule, MockerRegistry } from '@vitest/mocker';
2
2
  import { dynamicImportPlugin, ServerMockResolver, interceptorPlugin } from '@vitest/mocker/node';
3
3
  import c from 'tinyrainbow';
4
- import { getFilePoolName, distDir, resolveApiServerConfig, resolveFsAllow, isFileServingAllowed, createDebugger, isValidApiRequest, createViteLogger, createViteServer } from 'vitest/node';
5
- import fs, { readFileSync, lstatSync, promises, existsSync } from 'node:fs';
4
+ import { isValidApiRequest, isFileServingAllowed, distDir, resolveApiServerConfig, resolveFsAllow, rolldownVersion, createDebugger, createViteLogger, createViteServer } from 'vitest/node';
5
+ import fs, { readFileSync, lstatSync, createReadStream, promises, existsSync } from 'node:fs';
6
6
  import { createRequire } from 'node:module';
7
- import { slash as slash$1, toArray, createDefer } from '@vitest/utils';
7
+ import { slash as slash$1, toArray, deepMerge, createDefer } from '@vitest/utils/helpers';
8
8
  import MagicString from 'magic-string';
9
9
  import sirv from 'sirv';
10
- import * as vite from 'vite';
11
10
  import { coverageConfigDefaults } from 'vitest/config';
12
11
  import { fileURLToPath } from 'node:url';
13
12
  import crypto from 'node:crypto';
14
- import { mkdir, rm, readFile as readFile$1 } from 'node:fs/promises';
13
+ import { mkdir, rm, readFile as readFile$1, writeFile as writeFile$1 } from 'node:fs/promises';
15
14
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
16
- import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-KA1WiV0q.js';
15
+ import { PlaywrightBrowserProvider } from './providers/playwright.js';
16
+ import { WebdriverBrowserProvider } from './providers/webdriverio.js';
17
17
  import { resolve as resolve$1, basename as basename$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
18
- import { WebSocketServer } from 'ws';
19
18
  import * as nodeos from 'node:os';
19
+ import { platform } from 'node:os';
20
+ import { PNG } from 'pngjs';
21
+ import pm from 'pixelmatch';
22
+ import { WebSocketServer } from 'ws';
20
23
  import { performance } from 'node:perf_hooks';
21
24
 
22
- var version = "4.0.0-beta.1";
25
+ var version = "4.0.0-beta.11";
23
26
 
24
27
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
25
28
  function normalizeWindowsPath(input = "") {
@@ -324,27 +327,28 @@ const stringify = (value, replacer, space) => {
324
327
  function replacer(code, values) {
325
328
  return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? _);
326
329
  }
327
- const builtinProviders = [
328
- "webdriverio",
329
- "playwright",
330
- "preview"
331
- ];
332
330
  async function getBrowserProvider(options, project) {
333
- if (options.provider == null || builtinProviders.includes(options.provider)) {
334
- const providers = await import('./providers.js');
335
- const provider = options.provider || "preview";
336
- return providers[provider];
331
+ const browser = project.config.browser.name;
332
+ const name = project.name ? `[${project.name}] ` : "";
333
+ if (!browser) {
334
+ throw new Error(`${name}Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`);
337
335
  }
338
- let customProviderModule;
339
- try {
340
- customProviderModule = await project.import(options.provider);
341
- } catch (error) {
342
- throw new Error(`Failed to load custom BrowserProvider from ${options.provider}`, { cause: error });
336
+ if (options.provider == null || "_cli" in options.provider && typeof options.provider.factory !== "function") {
337
+ const providers = await import('./index-CwoiDq7G.js');
338
+ const name = options.provider?.name || "preview";
339
+ if (!(name in providers)) {
340
+ throw new Error(`Unknown browser provider "${name}". Available providers: ${Object.keys(providers).join(", ")}.`);
341
+ }
342
+ return providers[name]().factory(project);
343
+ }
344
+ const supportedBrowsers = options.provider.supportedBrowser || [];
345
+ if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
346
+ throw new Error(`${name}Browser "${browser}" is not supported by the browser provider "${options.provider.name}". Supported browsers: ${supportedBrowsers.join(", ")}.`);
343
347
  }
344
- if (customProviderModule.default == null) {
345
- throw new Error(`Custom BrowserProvider loaded from ${options.provider} was not the default export`);
348
+ if (typeof options.provider.factory !== "function") {
349
+ throw new TypeError(`The "${name}" browser provider does not provide a "factory" function. Received ${typeof options.provider.factory}.`);
346
350
  }
347
- return customProviderModule.default;
351
+ return options.provider.factory(project);
348
352
  }
349
353
  function slash(path) {
350
354
  return path.replace(/\\/g, "/").replace(/\/+/g, "/");
@@ -355,7 +359,7 @@ async function resolveOrchestrator(globalServer, url, res) {
355
359
  // it's possible to open the page without a context
356
360
  if (!sessionId) {
357
361
  const contexts = [...globalServer.children].flatMap((p) => [...p.state.orchestrators.keys()]);
358
- sessionId = contexts[contexts.length - 1] ?? "none";
362
+ sessionId = contexts.at(-1) ?? "none";
359
363
  }
360
364
  // it's ok to not have a session here, especially in the preview provider
361
365
  // because the user could refresh the page which would remove the session id from the url
@@ -364,13 +368,13 @@ async function resolveOrchestrator(globalServer, url, res) {
364
368
  if (!browserProject) {
365
369
  return;
366
370
  }
367
- // ignore uknown pages
371
+ // ignore unknown pages
368
372
  if (sessionId && sessionId !== "none" && !globalServer.vitest._browserSessions.sessionIds.has(sessionId)) {
369
373
  return;
370
374
  }
371
375
  const injectorJs = typeof globalServer.injectorJs === "string" ? globalServer.injectorJs : await globalServer.injectorJs;
372
376
  const injector = replacer(injectorJs, {
373
- __VITEST_PROVIDER__: JSON.stringify(browserProject.config.browser.provider || "preview"),
377
+ __VITEST_PROVIDER__: JSON.stringify(browserProject.config.browser.provider?.name || "preview"),
374
378
  __VITEST_CONFIG__: JSON.stringify(browserProject.wrapSerializedConfig()),
375
379
  __VITEST_VITE_CONFIG__: JSON.stringify({ root: browserProject.vite.config.root }),
376
380
  __VITEST_METHOD__: JSON.stringify("orchestrate"),
@@ -388,7 +392,7 @@ async function resolveOrchestrator(globalServer, url, res) {
388
392
  for (const attr in script.attrs || {}) {
389
393
  html += `${attr}="${script.attrs[attr]}" `;
390
394
  }
391
- html += `>${script.children}</script>`;
395
+ html += `>${script.children}<\/script>`;
392
396
  return html;
393
397
  }).join("\n");
394
398
  }
@@ -402,15 +406,15 @@ async function resolveOrchestrator(globalServer, url, res) {
402
406
  "{__VITEST_INJECTOR__}",
403
407
  "{__VITEST_ERROR_CATCHER__}",
404
408
  "{__VITEST_SCRIPTS__}",
405
- `<script type="module" crossorigin src="${base}${jsEntry}"></script>`
409
+ `<script type="module" crossorigin src="${base}${jsEntry}"><\/script>`
406
410
  ].join("\n"));
407
411
  }
408
412
  return replacer(baseHtml, {
409
413
  __VITEST_FAVICON__: globalServer.faviconUrl,
410
414
  __VITEST_TITLE__: "Vitest Browser Runner",
411
415
  __VITEST_SCRIPTS__: globalServer.orchestratorScripts,
412
- __VITEST_INJECTOR__: `<script type="module">${injector}</script>`,
413
- __VITEST_ERROR_CATCHER__: `<script type="module" src="${globalServer.errorCatcherUrl}"></script>`,
416
+ __VITEST_INJECTOR__: `<script type="module">${injector}<\/script>`,
417
+ __VITEST_ERROR_CATCHER__: `<script type="module" src="${globalServer.errorCatcherUrl}"><\/script>`,
414
418
  __VITEST_SESSION_ID__: JSON.stringify(sessionId)
415
419
  });
416
420
  }
@@ -671,6 +675,31 @@ var BrowserPlugin = (parentServer, base = "/") => {
671
675
  }
672
676
  next();
673
677
  });
678
+ // handle attachments the same way as in packages/ui/node/index.ts
679
+ server.middlewares.use((req, res, next) => {
680
+ if (!req.url) {
681
+ return next();
682
+ }
683
+ const url = new URL(req.url, "http://localhost");
684
+ if (url.pathname !== "/__vitest_attachment__") {
685
+ return next();
686
+ }
687
+ const path = url.searchParams.get("path");
688
+ const contentType = url.searchParams.get("contentType");
689
+ if (!isValidApiRequest(parentServer.config, req) || !contentType || !path) {
690
+ return next();
691
+ }
692
+ const fsPath = decodeURIComponent(path);
693
+ if (!isFileServingAllowed(parentServer.vite.config, fsPath)) {
694
+ return next();
695
+ }
696
+ try {
697
+ res.setHeader("content-type", contentType);
698
+ return createReadStream(fsPath).pipe(res).on("close", () => res.end());
699
+ } catch (err) {
700
+ return next(err);
701
+ }
702
+ });
674
703
  }
675
704
  },
676
705
  {
@@ -680,8 +709,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
680
709
  // this plugin can be used in different projects, but all of them
681
710
  // have the same `include` pattern, so it doesn't matter which project we use
682
711
  const project = parentServer.project;
683
- const { testFiles: allTestFiles } = await project.globTestFiles();
684
- const browserTestFiles = allTestFiles.filter((file) => getFilePoolName(project, file) === "browser");
712
+ const { testFiles: browserTestFiles } = await project.globTestFiles();
685
713
  const setupFiles = toArray(project.config.setupFiles);
686
714
  // replace env values - cannot be reassign at runtime
687
715
  const define = {};
@@ -723,7 +751,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
723
751
  entries.push(project.config.diff);
724
752
  }
725
753
  if (parentServer.vitest.coverageProvider) {
726
- const coverage = parentServer.vitest.config.coverage;
754
+ const coverage = parentServer.vitest._coverageOptions;
727
755
  const provider = coverage.provider;
728
756
  if (provider === "v8") {
729
757
  const path = tryResolve("@vitest/coverage-v8", [parentServer.config.root]);
@@ -744,9 +772,8 @@ var BrowserPlugin = (parentServer, base = "/") => {
744
772
  const include = [
745
773
  "vitest > expect-type",
746
774
  "vitest > @vitest/snapshot > magic-string",
747
- "vitest > chai",
748
- "vitest > chai > loupe",
749
- "vitest > @vitest/runner > strip-literal",
775
+ "vitest > @vitest/expect > chai",
776
+ "vitest > @vitest/expect > chai > loupe",
750
777
  "vitest > @vitest/utils > loupe",
751
778
  "@vitest/browser > @testing-library/user-event",
752
779
  "@vitest/browser > @testing-library/dom"
@@ -835,7 +862,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
835
862
  viteConfig.esbuild ||= {};
836
863
  viteConfig.esbuild.legalComments = "inline";
837
864
  }
838
- const defaultPort = parentServer.vitest._browserLastPort++;
865
+ const defaultPort = parentServer.vitest.state._data.browserLastPort++;
839
866
  const api = resolveApiServerConfig(viteConfig.test?.browser || {}, defaultPort);
840
867
  viteConfig.server = {
841
868
  ...viteConfig.server,
@@ -859,7 +886,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
859
886
  return;
860
887
  }
861
888
  const s = new MagicString(code, { filename });
862
- s.prepend(`import.meta.vitest = __vitest_index__;\n`);
889
+ s.prepend(`Object.defineProperty(import.meta, 'vitest', { get() { return typeof __vitest_worker__ !== 'undefined' && __vitest_worker__.filepath === "${filename.replace(/"/g, "\\\"")}" ? __vitest_index__ : undefined } });\n`);
863
890
  return {
864
891
  code: s.toString(),
865
892
  map: s.generateMap({ hires: true })
@@ -890,10 +917,6 @@ var BrowserPlugin = (parentServer, base = "/") => {
890
917
  if (!projectBrowser) {
891
918
  return;
892
919
  }
893
- if (!parentServer.testerScripts) {
894
- const testerScripts = await parentServer.formatScripts(parentServer.config.browser.testerScripts);
895
- parentServer.testerScripts = testerScripts;
896
- }
897
920
  const stateJs = typeof parentServer.stateJs === "string" ? parentServer.stateJs : await parentServer.stateJs;
898
921
  const testerTags = [];
899
922
  const isDefaultTemplate = resolve(distRoot, "client/tester/tester.html") === projectBrowser.testerFilepath;
@@ -976,7 +999,6 @@ body {
976
999
  },
977
1000
  injectTo: "head"
978
1001
  } : null,
979
- ...parentServer.testerScripts,
980
1002
  ...testerTags
981
1003
  ].filter((s) => s != null);
982
1004
  }
@@ -1005,7 +1027,7 @@ body {
1005
1027
  });
1006
1028
  }
1007
1029
  };
1008
- return { optimizeDeps: "rolldownVersion" in vite ? { rollupOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } } };
1030
+ return { optimizeDeps: rolldownVersion ? { rollupOptions: { plugins: [rolldownPlugin] } } : { esbuildOptions: { plugins: [esbuildPlugin] } } };
1009
1031
  }
1010
1032
  }
1011
1033
  ];
@@ -1027,7 +1049,8 @@ function getRequire() {
1027
1049
  }
1028
1050
  function resolveCoverageFolder(vitest) {
1029
1051
  const options = vitest.config;
1030
- const htmlReporter = options.coverage?.enabled ? toArray(options.coverage.reporter).find((reporter) => {
1052
+ const coverageOptions = vitest._coverageOptions;
1053
+ const htmlReporter = coverageOptions?.enabled ? toArray(options.coverage.reporter).find((reporter) => {
1031
1054
  if (typeof reporter === "string") {
1032
1055
  return reporter === "html";
1033
1056
  }
@@ -1037,7 +1060,7 @@ function resolveCoverageFolder(vitest) {
1037
1060
  return undefined;
1038
1061
  }
1039
1062
  // reportsDirectory not resolved yet
1040
- const root = resolve(options.root || process.cwd(), options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory);
1063
+ const root = resolve(options.root || process.cwd(), coverageOptions.reportsDirectory || coverageConfigDefaults.reportsDirectory);
1041
1064
  const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : undefined;
1042
1065
  if (!subdir || typeof subdir !== "string") {
1043
1066
  return [root, `/${basename(root)}/`];
@@ -1650,7 +1673,7 @@ _Mime_extensionToType = new WeakMap(), _Mime_typeToExtension = new WeakMap(), _M
1650
1673
  var mime = new Mime(types)._freeze();
1651
1674
 
1652
1675
  function assertFileAccess(path, project) {
1653
- if (!isFileServingAllowed(path, project.vite) && !isFileServingAllowed(path, project.vitest.server)) {
1676
+ if (!isFileServingAllowed(path, project.vite) && !isFileServingAllowed(path, project.vitest.vite)) {
1654
1677
  throw new Error(`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`);
1655
1678
  }
1656
1679
  }
@@ -2353,44 +2376,73 @@ function selectAll() {
2353
2376
  }
2354
2377
 
2355
2378
  const screenshot = async (context, name, options = {}) => {
2356
- if (!context.testPath) {
2357
- throw new Error(`Cannot take a screenshot without a test path`);
2358
- }
2359
2379
  options.save ??= true;
2360
2380
  if (!options.save) {
2361
2381
  options.base64 = true;
2362
2382
  }
2363
- const path = options.path ? resolve(dirname(context.testPath), options.path) : resolveScreenshotPath(context.testPath, name, context.project.config);
2383
+ const { buffer, path } = await takeScreenshot(context, name, options);
2384
+ return returnResult(options, path, buffer);
2385
+ };
2386
+ /**
2387
+ * Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
2388
+ *
2389
+ * **Note**: the returned `path` indicates where the screenshot *might* be found.
2390
+ * It is not guaranteed to exist, especially if `options.save` is `false`.
2391
+ *
2392
+ * @throws {Error} If the function is not called within a test or if the browser provider does not support screenshots.
2393
+ */
2394
+ async function takeScreenshot(context, name, options) {
2395
+ if (!context.testPath) {
2396
+ throw new Error(`Cannot take a screenshot without a test path`);
2397
+ }
2398
+ const path = resolveScreenshotPath(context.testPath, name, context.project.config, options.path);
2364
2399
  const savePath = normalize$1(path);
2365
2400
  await mkdir(dirname(path), { recursive: true });
2366
2401
  if (context.provider instanceof PlaywrightBrowserProvider) {
2402
+ const mask = options.mask?.map((selector) => context.iframe.locator(selector));
2367
2403
  if (options.element) {
2368
2404
  const { element: selector,...config } = options;
2369
- const element = context.iframe.locator(`${selector}`);
2405
+ const element = context.iframe.locator(selector);
2370
2406
  const buffer = await element.screenshot({
2371
2407
  ...config,
2408
+ mask,
2372
2409
  path: options.save ? savePath : undefined
2373
2410
  });
2374
- return returnResult(options, path, buffer);
2411
+ return {
2412
+ buffer,
2413
+ path
2414
+ };
2375
2415
  }
2376
2416
  const buffer = await context.iframe.locator("body").screenshot({
2377
2417
  ...options,
2418
+ mask,
2378
2419
  path: options.save ? savePath : undefined
2379
2420
  });
2380
- return returnResult(options, path, buffer);
2421
+ return {
2422
+ buffer,
2423
+ path
2424
+ };
2381
2425
  }
2382
2426
  if (context.provider instanceof WebdriverBrowserProvider) {
2383
2427
  const page = context.provider.browser;
2384
2428
  const element = !options.element ? await page.$("body") : await page.$(`${options.element}`);
2385
- const buffer = await element.saveScreenshot(savePath);
2429
+ // webdriverio expects the path to contain the extension and only works with PNG files
2430
+ const savePathWithExtension = savePath.endsWith(".png") ? savePath : `${savePath}.png`;
2431
+ const buffer = await element.saveScreenshot(savePathWithExtension);
2386
2432
  if (!options.save) {
2387
- await rm(savePath, { force: true });
2433
+ await rm(savePathWithExtension, { force: true });
2388
2434
  }
2389
- return returnResult(options, path, buffer);
2435
+ return {
2436
+ buffer,
2437
+ path
2438
+ };
2390
2439
  }
2391
2440
  throw new Error(`Provider "${context.provider.name}" does not support screenshots`);
2392
- };
2393
- function resolveScreenshotPath(testPath, name, config) {
2441
+ }
2442
+ function resolveScreenshotPath(testPath, name, config, customPath) {
2443
+ if (customPath) {
2444
+ return resolve(dirname(testPath), customPath);
2445
+ }
2394
2446
  const dir = dirname(testPath);
2395
2447
  const base = basename(testPath);
2396
2448
  if (config.browser.screenshotDirectory) {
@@ -2411,6 +2463,401 @@ function returnResult(options, path, buffer) {
2411
2463
  return path;
2412
2464
  }
2413
2465
 
2466
+ const codec = {
2467
+ decode: (buffer, options) => {
2468
+ const { data, alpha, bpp, color, colorType, depth, height, interlace, palette, width } = PNG.sync.read(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer), options);
2469
+ return {
2470
+ metadata: {
2471
+ alpha,
2472
+ bpp,
2473
+ color,
2474
+ colorType,
2475
+ depth,
2476
+ height,
2477
+ interlace,
2478
+ palette,
2479
+ width
2480
+ },
2481
+ data
2482
+ };
2483
+ },
2484
+ encode: ({ data, metadata: { height, width } }, options) => {
2485
+ const png = new PNG({
2486
+ height,
2487
+ width
2488
+ });
2489
+ png.data = Buffer.isBuffer(data) ? data : Buffer.from(data);
2490
+ return PNG.sync.write(png, options);
2491
+ }
2492
+ };
2493
+
2494
+ function getCodec(type) {
2495
+ switch (type) {
2496
+ case "png": return codec;
2497
+ default: throw new Error(`No codec found for type ${type}`);
2498
+ }
2499
+ }
2500
+
2501
+ const defaultOptions$1 = {
2502
+ allowedMismatchedPixelRatio: undefined,
2503
+ allowedMismatchedPixels: undefined,
2504
+ threshold: .1,
2505
+ includeAA: false,
2506
+ alpha: .1,
2507
+ aaColor: [
2508
+ 255,
2509
+ 255,
2510
+ 0
2511
+ ],
2512
+ diffColor: [
2513
+ 255,
2514
+ 0,
2515
+ 0
2516
+ ],
2517
+ diffColorAlt: undefined,
2518
+ diffMask: false
2519
+ };
2520
+ const pixelmatch = (reference, actual, { createDiff,...options }) => {
2521
+ if (reference.metadata.height !== actual.metadata.height || reference.metadata.width !== actual.metadata.width) {
2522
+ return {
2523
+ pass: false,
2524
+ diff: null,
2525
+ message: `Expected image dimensions to be ${reference.metadata.width}×${reference.metadata.height}px, but received ${actual.metadata.width}×${actual.metadata.height}px.`
2526
+ };
2527
+ }
2528
+ const optionsWithDefaults = {
2529
+ ...defaultOptions$1,
2530
+ ...options
2531
+ };
2532
+ const diffBuffer = createDiff ? new Uint8Array(reference.data.length) : undefined;
2533
+ const mismatchedPixels = pm(reference.data, actual.data, diffBuffer, reference.metadata.width, reference.metadata.height, optionsWithDefaults);
2534
+ const imageArea = reference.metadata.width * reference.metadata.height;
2535
+ let allowedMismatchedPixels = Math.min(optionsWithDefaults.allowedMismatchedPixels ?? Number.POSITIVE_INFINITY, (optionsWithDefaults.allowedMismatchedPixelRatio ?? Number.POSITIVE_INFINITY) * imageArea);
2536
+ if (allowedMismatchedPixels === Number.POSITIVE_INFINITY) {
2537
+ allowedMismatchedPixels = 0;
2538
+ }
2539
+ const pass = mismatchedPixels <= allowedMismatchedPixels;
2540
+ return {
2541
+ pass,
2542
+ diff: diffBuffer ?? null,
2543
+ message: pass ? null : `${mismatchedPixels} pixels (ratio ${(Math.ceil(mismatchedPixels / imageArea * 100) / 100).toFixed(2)}) differ.`
2544
+ };
2545
+ };
2546
+
2547
+ const comparators = new Map(Object.entries({ pixelmatch }));
2548
+ function getComparator(comparator) {
2549
+ if (comparators.has(comparator)) {
2550
+ return comparators.get(comparator);
2551
+ }
2552
+ throw new Error(`Unrecognized comparator ${comparator}`);
2553
+ }
2554
+
2555
+ const defaultOptions = {
2556
+ comparatorName: "pixelmatch",
2557
+ comparatorOptions: {},
2558
+ screenshotOptions: {
2559
+ animations: "disabled",
2560
+ caret: "hide",
2561
+ fullPage: false,
2562
+ maskColor: "#ff00ff",
2563
+ omitBackground: false,
2564
+ scale: "device"
2565
+ },
2566
+ timeout: 5e3,
2567
+ resolveDiffPath: ({ arg, ext, root, attachmentsDir, browserName, platform, testFileDirectory, testFileName }) => resolve(root, attachmentsDir, testFileDirectory, testFileName, `${arg}-${browserName}-${platform}${ext}`),
2568
+ resolveScreenshotPath: ({ arg, ext, root, screenshotDirectory, testFileDirectory, testFileName, browserName }) => resolve(root, testFileDirectory, screenshotDirectory, testFileName, `${arg}-${browserName}-${platform}${ext}`)
2569
+ };
2570
+ const supportedExtensions = ["png"];
2571
+ function resolveOptions({ context, name, options, testName }) {
2572
+ if (context.testPath === undefined) {
2573
+ throw new Error("`resolveOptions` has to be used in a test file");
2574
+ }
2575
+ const resolvedOptions = deepMerge(Object.create(null), defaultOptions, context.project.config.browser.expect?.toMatchScreenshot ?? {}, options);
2576
+ const extensionFromName = extname(name);
2577
+ // technically the type is a lie, but we check beneath and reassign otherwise
2578
+ let extension = extensionFromName.replace(/^\./, "");
2579
+ // when `type` will be supported in `screenshotOptions`:
2580
+ // - `'png'` should end up in `defaultOptions.screenshotOptions.type`
2581
+ // - this condition should be switched around
2582
+ // - the assignment should be `resolvedOptions.screenshotOptions.type = extension`
2583
+ // - everything using `extension` should use `resolvedOptions.screenshotOptions.type`
2584
+ if (supportedExtensions.includes(extension) === false) {
2585
+ extension = "png";
2586
+ }
2587
+ const { root } = context.project.serializedConfig;
2588
+ const resolvePathData = {
2589
+ arg: sanitizeArg(
2590
+ // remove the extension only if it ends up being used
2591
+ extensionFromName.endsWith(extension) ? basename(name, extensionFromName) : name
2592
+ ),
2593
+ ext: `.${extension}`,
2594
+ platform: platform(),
2595
+ root,
2596
+ screenshotDirectory: relative(root, join(root, context.project.config.browser.screenshotDirectory ?? "__screenshots__")),
2597
+ attachmentsDir: relative(root, context.project.config.attachmentsDir),
2598
+ testFileDirectory: relative(root, dirname(context.testPath)),
2599
+ testFileName: basename(context.testPath),
2600
+ testName: sanitize(testName, false),
2601
+ browserName: context.project.config.browser.name
2602
+ };
2603
+ return {
2604
+ codec: getCodec(extension),
2605
+ comparator: getComparator(resolvedOptions.comparatorName),
2606
+ resolvedOptions,
2607
+ paths: {
2608
+ reference: resolvedOptions.resolveScreenshotPath(resolvePathData),
2609
+ get diffs() {
2610
+ const diffs = {
2611
+ reference: resolvedOptions.resolveDiffPath({
2612
+ ...resolvePathData,
2613
+ arg: `${resolvePathData.arg}-reference`
2614
+ }),
2615
+ actual: resolvedOptions.resolveDiffPath({
2616
+ ...resolvePathData,
2617
+ arg: `${resolvePathData.arg}-actual`
2618
+ }),
2619
+ diff: resolvedOptions.resolveDiffPath({
2620
+ ...resolvePathData,
2621
+ arg: `${resolvePathData.arg}-diff`
2622
+ })
2623
+ };
2624
+ Object.defineProperty(this, "diffs", { value: diffs });
2625
+ return diffs;
2626
+ }
2627
+ }
2628
+ };
2629
+ }
2630
+ /**
2631
+ * Sanitizes a string by removing or transforming characters to ensure it is
2632
+ * safe for use as a filename or path segment. It supports two modes:
2633
+ *
2634
+ * 1. Non-path mode (`keepPaths === false`):
2635
+ * - Replaces one or more whitespace characters (`\s+`) with a single hyphen (`-`).
2636
+ * - Removes any character that is not a word character (`\w`) or a hyphen (`-`).
2637
+ * - Collapses multiple consecutive hyphens (`-{2,}`) into a single hyphen.
2638
+ *
2639
+ * 2. Path-preserving mode (`keepPaths === true`):
2640
+ * - Splits the input string on the path separator.
2641
+ * - Sanitizes each path segment individually in non-path mode.
2642
+ * - Joins the sanitized segments back together.
2643
+ *
2644
+ * @param input - The raw string to sanitize.
2645
+ * @param keepPaths - If `false`, performs a flat sanitization (drops path segments).
2646
+ * If `true`, treats `input` as a path: each segment is sanitized independently,
2647
+ * preserving separators.
2648
+ */
2649
+ function sanitize(input, keepPaths) {
2650
+ if (keepPaths === false) {
2651
+ return input.replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/-{2,}/g, "-");
2652
+ }
2653
+ return input.split("/").map((path) => sanitize(path, false)).join("/");
2654
+ }
2655
+ /**
2656
+ * Takes a string, treats it as a potential path or filename, and ensures it cannot
2657
+ * escape the root directory or contain invalid characters. Internally, it:
2658
+ *
2659
+ * 1. Prepends the path separator to the raw input to form a path-like string.
2660
+ * 2. Uses {@linkcode relative|relative('/', <that-path>)} to compute a relative
2661
+ * path from the root, which effectively strips any leading separators and prevents
2662
+ * traversal above the root.
2663
+ * 3. Passes the resulting relative path into {@linkcode sanitize|sanitize(..., true)},
2664
+ * preserving any path separators but sanitizing each segment.
2665
+ *
2666
+ * @param input - The raw string to clean.
2667
+ */
2668
+ function sanitizeArg(input) {
2669
+ return sanitize(relative("/", join("/", input)), true);
2670
+ }
2671
+ /**
2672
+ * Takes a screenshot and decodes it using the provided codec.
2673
+ *
2674
+ * The screenshot is taken as a base64 string and then decoded into the format
2675
+ * expected by the comparator.
2676
+ *
2677
+ * @returns `Promise` resolving to the decoded screenshot data
2678
+ */
2679
+ function takeDecodedScreenshot({ codec, context, element, name, screenshotOptions }) {
2680
+ return takeScreenshot(context, name, {
2681
+ ...screenshotOptions,
2682
+ save: false,
2683
+ element
2684
+ }).then(({ buffer }) => codec.decode(buffer, {}));
2685
+ }
2686
+ /**
2687
+ * Creates a promise that resolves to `null` after the specified timeout.
2688
+ * If the timeout is `0`, the promise resolves immediately.
2689
+ *
2690
+ * @param timeout - The delay in milliseconds before the promise resolves
2691
+ * @returns `Promise` that resolves to `null` after the timeout
2692
+ */
2693
+ function asyncTimeout(timeout) {
2694
+ return new Promise((resolve) => {
2695
+ if (timeout === 0) {
2696
+ resolve(null);
2697
+ } else {
2698
+ setTimeout(() => resolve(null), timeout);
2699
+ }
2700
+ });
2701
+ }
2702
+
2703
+ const screenshotMatcher = async (context, name, testName, options) => {
2704
+ if (!context.testPath) {
2705
+ throw new Error(`Cannot compare screenshots without a test path`);
2706
+ }
2707
+ const { element } = options;
2708
+ const { codec, comparator, paths, resolvedOptions: { comparatorOptions, screenshotOptions, timeout } } = resolveOptions({
2709
+ context,
2710
+ name,
2711
+ testName,
2712
+ options
2713
+ });
2714
+ const referenceFile = await readFile$1(paths.reference).catch(() => null);
2715
+ const reference = referenceFile && await codec.decode(await readFile$1(paths.reference), {});
2716
+ const abortController = new AbortController();
2717
+ const stableScreenshot = getStableScreenshots({
2718
+ codec,
2719
+ comparator,
2720
+ comparatorOptions,
2721
+ context,
2722
+ element,
2723
+ name: `${Date.now()}-${basename(paths.reference)}`,
2724
+ reference,
2725
+ screenshotOptions,
2726
+ signal: abortController.signal
2727
+ });
2728
+ const value = await (timeout === 0 ? stableScreenshot : Promise.race([stableScreenshot, asyncTimeout(timeout).finally(() => {
2729
+ abortController.abort();
2730
+ })]));
2731
+ // case #01
2732
+ // - impossible to get a stable screenshot to compare against
2733
+ // - fail
2734
+ if (value === null || value.actual === null) {
2735
+ return {
2736
+ pass: false,
2737
+ reference: referenceFile && paths.reference,
2738
+ actual: null,
2739
+ diff: null,
2740
+ message: `Could not capture a stable screenshot within ${timeout}ms.`
2741
+ };
2742
+ }
2743
+ const { updateSnapshot } = context.project.serializedConfig.snapshotOptions;
2744
+ // if there's no reference or if we want to update snapshots, we have to finish the comparison early
2745
+ if (reference === null || updateSnapshot === "all") {
2746
+ const shouldCreateReference = updateSnapshot !== "none";
2747
+ const referencePath = shouldCreateReference ? paths.reference : paths.diffs.reference;
2748
+ await writeScreenshot(referencePath, await codec.encode(value.actual, {}));
2749
+ // case #02
2750
+ // - got a stable screenshot, but there is no reference and we don't want to update screenshots
2751
+ // - fail
2752
+ if (updateSnapshot !== "all") {
2753
+ return {
2754
+ pass: false,
2755
+ reference: referencePath,
2756
+ actual: null,
2757
+ diff: null,
2758
+ message: `No existing reference screenshot found${shouldCreateReference ? "; a new one was created. Review it before running tests again." : "."}`
2759
+ };
2760
+ }
2761
+ // case #03
2762
+ // - got a stable screenshot, there is no reference, but we want to update screenshots
2763
+ // - pass
2764
+ return { pass: true };
2765
+ }
2766
+ // case #04
2767
+ // - got a stable screenshot with no retries and there's a reference
2768
+ // - pass
2769
+ if (referenceFile && value.retries === 0) {
2770
+ return { pass: true };
2771
+ }
2772
+ const finalResult = await comparator(reference, value.actual, {
2773
+ createDiff: true,
2774
+ ...comparatorOptions
2775
+ });
2776
+ if (finalResult.pass === false && finalResult.diff !== null) {
2777
+ const diff = await codec.encode({
2778
+ data: finalResult.diff,
2779
+ metadata: {
2780
+ height: reference.metadata.height,
2781
+ width: reference.metadata.width
2782
+ }
2783
+ }, {});
2784
+ await writeScreenshot(paths.diffs.diff, diff);
2785
+ }
2786
+ // case #05
2787
+ // - reference matches stable screenshot
2788
+ // - pass
2789
+ if (finalResult.pass === true) {
2790
+ return { pass: true };
2791
+ }
2792
+ const actual = await codec.encode(value.actual, {});
2793
+ await writeScreenshot(paths.diffs.actual, actual);
2794
+ // case #06
2795
+ // - fallback, reference does NOT match stable screenshot
2796
+ // - fail
2797
+ return {
2798
+ pass: false,
2799
+ reference: paths.reference,
2800
+ actual: paths.diffs.actual,
2801
+ diff: finalResult.diff && paths.diffs.diff,
2802
+ message: `Screenshot does not match the stored reference.${finalResult.message === null ? "" : `\n${finalResult.message}`}`
2803
+ };
2804
+ };
2805
+ async function writeScreenshot(path, image) {
2806
+ try {
2807
+ await mkdir(dirname(path), { recursive: true });
2808
+ await writeFile$1(path, image);
2809
+ } catch {
2810
+ throw new Error("Couldn't write file to fs");
2811
+ }
2812
+ }
2813
+ /**
2814
+ * Takes screenshots repeatedly until the page reaches a visually stable state.
2815
+ *
2816
+ * This function compares consecutive screenshots and continues taking new ones
2817
+ * until two consecutive screenshots match according to the provided comparator.
2818
+ *
2819
+ * The process works as follows:
2820
+ *
2821
+ * 1. Uses as baseline an optional reference screenshot or takes a new screenshot
2822
+ * 2. Takes a screenshot and compares with baseline
2823
+ * 3. If they match, the page is considered stable and the function returns
2824
+ * 4. If they don't match, it continues with the newer screenshot as the baseline
2825
+ * 5. Repeats until stability is achieved or the operation is aborted
2826
+ *
2827
+ * @returns `Promise` resolving to an object containing the retry count and
2828
+ * final screenshot
2829
+ */
2830
+ async function getStableScreenshots({ codec, context, comparator, comparatorOptions, element, name, reference, screenshotOptions, signal }) {
2831
+ const screenshotArgument = {
2832
+ codec,
2833
+ context,
2834
+ element,
2835
+ name,
2836
+ screenshotOptions
2837
+ };
2838
+ let retries = 0;
2839
+ let decodedBaseline = reference;
2840
+ while (signal.aborted === false) {
2841
+ if (decodedBaseline === null) {
2842
+ decodedBaseline = takeDecodedScreenshot(screenshotArgument);
2843
+ }
2844
+ const [image1, image2] = await Promise.all([decodedBaseline, takeDecodedScreenshot(screenshotArgument)]);
2845
+ const comparatorResult = (await comparator(image1, image2, {
2846
+ ...comparatorOptions,
2847
+ createDiff: false
2848
+ })).pass;
2849
+ decodedBaseline = image2;
2850
+ if (comparatorResult) {
2851
+ break;
2852
+ }
2853
+ retries += 1;
2854
+ }
2855
+ return {
2856
+ retries,
2857
+ actual: await decodedBaseline
2858
+ };
2859
+ }
2860
+
2414
2861
  const selectOptions = async (context, selector, userValues, options = {}) => {
2415
2862
  if (context.provider instanceof PlaywrightBrowserProvider) {
2416
2863
  const value = userValues;
@@ -2551,7 +2998,8 @@ var builtinCommands = {
2551
2998
  __vitest_dragAndDrop: dragAndDrop,
2552
2999
  __vitest_hover: hover,
2553
3000
  __vitest_cleanup: keyboardCleanup,
2554
- __vitest_viewport: viewport
3001
+ __vitest_viewport: viewport,
3002
+ __vitest_screenshotMatcher: screenshotMatcher
2555
3003
  };
2556
3004
 
2557
3005
  class BrowserServerState {
@@ -2595,22 +3043,7 @@ class ProjectBrowser {
2595
3043
  if (this.provider) {
2596
3044
  return;
2597
3045
  }
2598
- const Provider = await getBrowserProvider(project.config.browser, project);
2599
- this.provider = new Provider();
2600
- const browser = project.config.browser.name;
2601
- const name = project.name ? `[${project.name}] ` : "";
2602
- if (!browser) {
2603
- throw new Error(`${name}Browser name is required. Please, set \`test.browser.instances[].browser\` option manually.`);
2604
- }
2605
- const supportedBrowsers = this.provider.getSupportedBrowsers();
2606
- if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
2607
- throw new Error(`${name}Browser "${browser}" is not supported by the browser provider "${this.provider.name}". Supported browsers: ${supportedBrowsers.join(", ")}.`);
2608
- }
2609
- const providerOptions = project.config.browser.providerOptions;
2610
- await this.provider.initialize(project, {
2611
- browser,
2612
- options: providerOptions
2613
- });
3046
+ this.provider = await getBrowserProvider(project.config.browser, project);
2614
3047
  }
2615
3048
  parseErrorStacktrace(e, options = {}) {
2616
3049
  return this.parent.parseErrorStacktrace(e, options);
@@ -2631,7 +3064,6 @@ function wrapConfig(config) {
2631
3064
 
2632
3065
  class ParentBrowserProject {
2633
3066
  orchestratorScripts;
2634
- testerScripts;
2635
3067
  faviconUrl;
2636
3068
  prefixOrchestratorUrl;
2637
3069
  prefixTesterUrl;
@@ -2677,18 +3109,19 @@ class ParentBrowserProject {
2677
3109
  return result?.map;
2678
3110
  },
2679
3111
  getUrlId: (id) => {
2680
- const mod = this.vite.moduleGraph.getModuleById(id);
3112
+ const moduleGraph = this.vite.environments.client.moduleGraph;
3113
+ const mod = moduleGraph.getModuleById(id);
2681
3114
  if (mod) {
2682
3115
  return id;
2683
3116
  }
2684
3117
  const resolvedPath = resolve(this.vite.config.root, id.slice(1));
2685
- const modUrl = this.vite.moduleGraph.getModuleById(resolvedPath);
3118
+ const modUrl = moduleGraph.getModuleById(resolvedPath);
2686
3119
  if (modUrl) {
2687
3120
  return resolvedPath;
2688
3121
  }
2689
3122
  // some browsers (looking at you, safari) don't report queries in stack traces
2690
3123
  // the next best thing is to try the first id that this file resolves to
2691
- const files = this.vite.moduleGraph.getModulesByFile(resolvedPath);
3124
+ const files = moduleGraph.getModulesByFile(resolvedPath);
2692
3125
  if (files && files.size) {
2693
3126
  return files.values().next().value.id;
2694
3127
  }
@@ -2719,7 +3152,7 @@ class ParentBrowserProject {
2719
3152
  "webdriverio",
2720
3153
  "preview"
2721
3154
  ];
2722
- const providerName = project.config.browser.provider || "preview";
3155
+ const providerName = project.config.browser.provider?.name || "preview";
2723
3156
  if (builtinProviders.includes(providerName)) {
2724
3157
  this.locatorsUrl = join("/@fs/", distRoot, "locators", `${providerName}.js`);
2725
3158
  }
@@ -2841,7 +3274,7 @@ function defaultSerialize(i) {
2841
3274
  return i;
2842
3275
  }
2843
3276
  const defaultDeserialize = defaultSerialize;
2844
- const { clearTimeout, setTimeout } = globalThis;
3277
+ const { clearTimeout, setTimeout: setTimeout$1 } = globalThis;
2845
3278
  const random = Math.random.bind(Math);
2846
3279
  function createBirpc(functions, options) {
2847
3280
  const {
@@ -2865,10 +3298,13 @@ function createBirpc(functions, options) {
2865
3298
  return functions;
2866
3299
  if (method === "$close")
2867
3300
  return close;
3301
+ if (method === "$rejectPendingCalls") {
3302
+ return rejectPendingCalls;
3303
+ }
2868
3304
  if (method === "$closed")
2869
3305
  return closed;
2870
3306
  if (method === "then" && !eventNames.includes("then") && !("then" in functions))
2871
- return undefined;
3307
+ return void 0;
2872
3308
  const sendEvent = (...args) => {
2873
3309
  post(serialize({ m: method, a: args, t: TYPE_REQUEST }));
2874
3310
  };
@@ -2883,14 +3319,14 @@ function createBirpc(functions, options) {
2883
3319
  try {
2884
3320
  await _promise;
2885
3321
  } finally {
2886
- _promise = undefined;
3322
+ _promise = void 0;
2887
3323
  }
2888
3324
  }
2889
3325
  return new Promise((resolve, reject) => {
2890
3326
  const id = nanoid();
2891
3327
  let timeoutId;
2892
3328
  if (timeout >= 0) {
2893
- timeoutId = setTimeout(() => {
3329
+ timeoutId = setTimeout$1(() => {
2894
3330
  try {
2895
3331
  const handleResult = options.onTimeoutError?.(method, args);
2896
3332
  if (handleResult !== true)
@@ -2911,14 +3347,30 @@ function createBirpc(functions, options) {
2911
3347
  return sendCall;
2912
3348
  }
2913
3349
  });
2914
- function close(error) {
3350
+ function close(customError) {
2915
3351
  closed = true;
2916
3352
  rpcPromiseMap.forEach(({ reject, method }) => {
2917
- reject(error || new Error(`[birpc] rpc is closed, cannot call "${method}"`));
3353
+ const error = new Error(`[birpc] rpc is closed, cannot call "${method}"`);
3354
+ if (customError) {
3355
+ customError.cause ??= error;
3356
+ return reject(customError);
3357
+ }
3358
+ reject(error);
2918
3359
  });
2919
3360
  rpcPromiseMap.clear();
2920
3361
  off(onMessage);
2921
3362
  }
3363
+ function rejectPendingCalls(handler) {
3364
+ const entries = Array.from(rpcPromiseMap.values());
3365
+ const handlerResults = entries.map(({ method, reject }) => {
3366
+ if (!handler) {
3367
+ return reject(new Error(`[birpc]: rejected pending call "${method}".`));
3368
+ }
3369
+ return handler({ method, reject });
3370
+ });
3371
+ rpcPromiseMap.clear();
3372
+ return handlerResults;
3373
+ }
2922
3374
  async function onMessage(data, ...extra) {
2923
3375
  let msg;
2924
3376
  try {
@@ -3018,12 +3470,13 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3018
3470
  if (!sessionId || !rpcId || projectName == null) {
3019
3471
  return error(new Error(`[vitest] Invalid URL ${request.url}. "projectName", "sessionId" and "rpcId" queries are required.`));
3020
3472
  }
3021
- if (!vitest._browserSessions.sessionIds.has(sessionId)) {
3022
- const ids = [...vitest._browserSessions.sessionIds].join(", ");
3473
+ const sessions = vitest._browserSessions;
3474
+ if (!sessions.sessionIds.has(sessionId)) {
3475
+ const ids = [...sessions.sessionIds].join(", ");
3023
3476
  return error(new Error(`[vitest] Unknown session id "${sessionId}". Expected one of ${ids}.`));
3024
3477
  }
3025
3478
  if (type === "orchestrator") {
3026
- const session = vitest._browserSessions.getSession(sessionId);
3479
+ const session = sessions.getSession(sessionId);
3027
3480
  // it's possible the session was already resolved by the preview provider
3028
3481
  session?.connected();
3029
3482
  }
@@ -3043,7 +3496,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3043
3496
  clients.delete(rpcId);
3044
3497
  globalServer.removeCDPHandler(rpcId);
3045
3498
  if (type === "orchestrator") {
3046
- vitest._browserSessions.destroySession(sessionId);
3499
+ sessions.destroySession(sessionId);
3047
3500
  }
3048
3501
  // this will reject any hanging methods if there are any
3049
3502
  rpc.$close(new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`));
@@ -3061,7 +3514,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3061
3514
  }
3062
3515
  }
3063
3516
  function setupClient(project, rpcId, ws) {
3064
- const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config.server?.deps?.moduleDirectories });
3517
+ const mockResolver = new ServerMockResolver(globalServer.vite, { moduleDirectories: project.config?.deps?.moduleDirectories });
3065
3518
  const mocker = project.browser?.provider.mocker;
3066
3519
  const rpc = createBirpc({
3067
3520
  async onUnhandledError(error, type) {
@@ -3252,11 +3705,8 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3252
3705
  on: (fn) => ws.on("message", fn),
3253
3706
  eventNames: ["onCancel", "cdpEvent"],
3254
3707
  serialize: (data) => stringify(data, stringifyReplace),
3255
- timeout: -1,
3256
3708
  deserialize: parse,
3257
- onTimeoutError(functionName) {
3258
- throw new Error(`[vitest-api]: Timeout calling "${functionName}"`);
3259
- }
3709
+ timeout: -1
3260
3710
  });
3261
3711
  vitest.onCancel((reason) => rpc.onCancel(reason));
3262
3712
  return rpc;
@@ -3349,14 +3799,15 @@ function createBrowserPool(vitest) {
3349
3799
  }
3350
3800
  const parallelPools = [];
3351
3801
  const nonParallelPools = [];
3352
- for (const result of initialisedPools) {
3353
- if (!result) {
3802
+ for (const pool of initialisedPools) {
3803
+ if (!pool) {
3804
+ // this means it was cancelled
3354
3805
  return;
3355
3806
  }
3356
- if (result.provider.mocker && result.provider.supportsParallelism) {
3357
- parallelPools.push(result.runTests);
3807
+ if (pool.provider.mocker && pool.provider.supportsParallelism) {
3808
+ parallelPools.push(pool.runTests);
3358
3809
  } else {
3359
- nonParallelPools.push(result.runTests);
3810
+ nonParallelPools.push(pool.runTests);
3360
3811
  }
3361
3812
  }
3362
3813
  await Promise.all(parallelPools.map((runTests) => runTests()));
@@ -3515,7 +3966,7 @@ class BrowserPool {
3515
3966
  debug?.("[%s] test %s finished running", sessionId, file);
3516
3967
  this.runNextTest(method, sessionId);
3517
3968
  }).catch((error) => {
3518
- // if user cancells the test run manually, ignore the error and exit gracefully
3969
+ // if user cancels the test run manually, ignore the error and exit gracefully
3519
3970
  if (this.project.vitest.isCancelling && error instanceof Error && error.message.startsWith("Browser connection was closed while running tests")) {
3520
3971
  this.cancel();
3521
3972
  this._promise?.resolve();