@vitest/browser 3.2.4 → 4.0.0-beta.10

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