@vitest/browser 3.2.0-beta.1 → 3.2.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,14 +11,14 @@ 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, readFile as readFile$1 } from 'node:fs/promises';
14
+ import { mkdir, rm, readFile as readFile$1 } from 'node:fs/promises';
15
15
  import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
16
- import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-D7k26Na7.js';
16
+ import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-B1QbgqhC.js';
17
17
  import { resolve as resolve$1, basename as basename$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
18
18
  import { WebSocketServer } from 'ws';
19
19
  import * as nodeos from 'node:os';
20
20
 
21
- var version = "3.2.0-beta.1";
21
+ var version = "3.2.0-beta.3";
22
22
 
23
23
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
24
24
  function normalizeWindowsPath(input = "") {
@@ -351,15 +351,19 @@ function slash(path) {
351
351
 
352
352
  async function resolveOrchestrator(globalServer, url, res) {
353
353
  let sessionId = url.searchParams.get("sessionId");
354
+ // it's possible to open the page without a context
354
355
  if (!sessionId) {
355
356
  const contexts = [...globalServer.children].flatMap((p) => [...p.state.orchestrators.keys()]);
356
357
  sessionId = contexts[contexts.length - 1] ?? "none";
357
358
  }
359
+ // it's ok to not have a session here, especially in the preview provider
360
+ // because the user could refresh the page which would remove the session id from the url
358
361
  const session = globalServer.vitest._browserSessions.getSession(sessionId);
359
362
  const browserProject = session?.project.browser || [...globalServer.children][0];
360
363
  if (!browserProject) {
361
364
  return;
362
365
  }
366
+ // ignore uknown pages
363
367
  if (sessionId && sessionId !== "none" && !globalServer.vitest._browserSessions.sessionIds.has(sessionId)) {
364
368
  return;
365
369
  }
@@ -375,6 +379,7 @@ async function resolveOrchestrator(globalServer, url, res) {
375
379
  __VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(browserProject.project.getProvidedContext())),
376
380
  __VITEST_API_TOKEN__: JSON.stringify(globalServer.vitest.config.api.token)
377
381
  });
382
+ // disable CSP for the orchestrator as we are the ones controlling it
378
383
  res.removeHeader("Content-Security-Policy");
379
384
  if (!globalServer.orchestratorScripts) {
380
385
  globalServer.orchestratorScripts = (await globalServer.formatScripts(globalServer.config.browser.orchestratorScripts)).map((script) => {
@@ -387,6 +392,7 @@ async function resolveOrchestrator(globalServer, url, res) {
387
392
  }).join("\n");
388
393
  }
389
394
  let baseHtml = typeof globalServer.orchestratorHtml === "string" ? globalServer.orchestratorHtml : await globalServer.orchestratorHtml;
395
+ // if UI is enabled, use UI HTML and inject the orchestrator script
390
396
  if (globalServer.config.browser.ui) {
391
397
  const manifestContent = globalServer.manifest instanceof Promise ? await globalServer.manifest : globalServer.manifest;
392
398
  const jsEntry = manifestContent["orchestrator.html"].file;
@@ -413,6 +419,7 @@ function disableCache(res) {
413
419
  res.setHeader("Content-Type", "text/html; charset=utf-8");
414
420
  }
415
421
  function allowIframes(res) {
422
+ // remove custom iframe related headers to allow the iframe to load
416
423
  res.removeHeader("X-Frame-Options");
417
424
  }
418
425
 
@@ -438,6 +445,8 @@ function createOrchestratorMiddleware(parentServer) {
438
445
  async function resolveTester(globalServer, url, res, next) {
439
446
  const csp = res.getHeader("Content-Security-Policy");
440
447
  if (typeof csp === "string") {
448
+ // add frame-ancestors to allow the iframe to be loaded by Vitest,
449
+ // but keep the rest of the CSP
441
450
  res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
442
451
  }
443
452
  const sessionId = url.searchParams.get("sessionId") || "none";
@@ -529,7 +538,7 @@ async function generateContextFile(globalServer) {
529
538
  const userEventNonProviderImport = await getUserEventImport(providerName, this.resolve.bind(this));
530
539
  const distContextPath = slash$1(`/@fs/${resolve(__dirname, "context.js")}`);
531
540
  return `
532
- import { page, createUserEvent, cdp } from '${distContextPath}'
541
+ import { page, createUserEvent, cdp, locators } from '${distContextPath}'
533
542
  ${userEventNonProviderImport}
534
543
 
535
544
  export const server = {
@@ -544,7 +553,7 @@ export const server = {
544
553
  }
545
554
  export const commands = server.commands
546
555
  export const userEvent = createUserEvent(_userEventSetup)
547
- export { page, cdp }
556
+ export { page, cdp, locators }
548
557
  `;
549
558
  }
550
559
  async function getUserEventImport(provider, resolve) {
@@ -572,6 +581,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
572
581
  name: "vitest:browser",
573
582
  async configureServer(server) {
574
583
  parentServer.setServer(server);
584
+ // eslint-disable-next-line prefer-arrow-callback
575
585
  server.middlewares.use(function vitestHeaders(_req, res, next) {
576
586
  const headers = server.config.server.headers;
577
587
  if (headers) {
@@ -604,6 +614,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
604
614
  }
605
615
  const uiEnabled = parentServer.config.browser.ui;
606
616
  if (uiEnabled) {
617
+ // eslint-disable-next-line prefer-arrow-callback
607
618
  server.middlewares.use(`${base}__screenshot-error`, function vitestBrowserScreenshotError(req, res) {
608
619
  if (!req.url) {
609
620
  res.statusCode = 404;
@@ -642,6 +653,11 @@ var BrowserPlugin = (parentServer, base = "/") => {
642
653
  });
643
654
  }
644
655
  server.middlewares.use((req, res, next) => {
656
+ // 9000 mega head move
657
+ // Vite always caches optimized dependencies, but users might mock
658
+ // them in _some_ tests, while keeping original modules in others
659
+ // there is no way to configure that in Vite, so we patch it here
660
+ // to always ignore the cache-control set by Vite in the next middleware
645
661
  if (req.url && versionRegexp.test(req.url) && !req.url.includes("chunk-")) {
646
662
  res.setHeader("Cache-Control", "no-cache");
647
663
  const setHeader = res.setHeader.bind(res);
@@ -660,10 +676,13 @@ var BrowserPlugin = (parentServer, base = "/") => {
660
676
  name: "vitest:browser:tests",
661
677
  enforce: "pre",
662
678
  async config() {
679
+ // this plugin can be used in different projects, but all of them
680
+ // have the same `include` pattern, so it doesn't matter which project we use
663
681
  const project = parentServer.project;
664
682
  const { testFiles: allTestFiles } = await project.globTestFiles();
665
683
  const browserTestFiles = allTestFiles.filter((file) => getFilePoolName(project, file) === "browser");
666
684
  const setupFiles = toArray(project.config.setupFiles);
685
+ // replace env values - cannot be reassign at runtime
667
686
  const define = {};
668
687
  for (const env in project.config.env || {}) {
669
688
  const stringValue = JSON.stringify(project.config.env[env]);
@@ -680,8 +699,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
680
699
  ];
681
700
  const exclude = [
682
701
  "vitest",
683
- "vitest/utils",
684
- "vitest/browser",
702
+ "vitest/internal/browser",
685
703
  "vitest/runners",
686
704
  "@vitest/browser",
687
705
  "@vitest/browser/client",
@@ -736,8 +754,10 @@ var BrowserPlugin = (parentServer, base = "/") => {
736
754
  if (svelte) {
737
755
  exclude.push("vitest-browser-svelte");
738
756
  }
757
+ // since we override the resolution in the esbuild plugin, Vite can no longer optimizer it
739
758
  const vue = isPackageExists("vitest-browser-vue", fileRoot);
740
759
  if (vue) {
760
+ // we override them in the esbuild plugin so optimizer can no longer intercept it
741
761
  include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
742
762
  }
743
763
  const vueTestUtils = isPackageExists("@vue/test-utils", fileRoot);
@@ -788,6 +808,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
788
808
  },
789
809
  transform(code, id) {
790
810
  if (id.includes(parentServer.vite.config.cacheDir) && id.includes("loupe.js")) {
811
+ // loupe bundle has a nastry require('util') call that leaves a warning in the console
791
812
  const utilRequire = "nodeUtil = require_util();";
792
813
  return code.replace(utilRequire, " ".repeat(utilRequire.length));
793
814
  }
@@ -830,11 +851,12 @@ var BrowserPlugin = (parentServer, base = "/") => {
830
851
  {
831
852
  name: "vitest:browser:in-source-tests",
832
853
  transform(code, id) {
854
+ const filename = cleanUrl(id);
833
855
  const project = parentServer.vitest.getProjectByName(parentServer.config.name);
834
- if (!project._isCachedTestFile(id) || !code.includes("import.meta.vitest")) {
856
+ if (!project._isCachedTestFile(filename) || !code.includes("import.meta.vitest")) {
835
857
  return;
836
858
  }
837
- const s = new MagicString(code, { filename: cleanUrl(id) });
859
+ const s = new MagicString(code, { filename });
838
860
  s.prepend(`import.meta.vitest = __vitest_index__;\n`);
839
861
  return {
840
862
  code: s.toString(),
@@ -845,6 +867,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
845
867
  {
846
868
  name: "vitest:browser:worker",
847
869
  transform(code, id, _options) {
870
+ // https://github.com/vitejs/vite/blob/ba56cf43b5480f8519349f7d7fe60718e9af5f1a/packages/vite/src/node/plugins/worker.ts#L46
848
871
  if (/(?:\?|&)worker_file&type=\w+(?:&|$)/.test(id)) {
849
872
  const s = new MagicString(code);
850
873
  s.prepend("globalThis.__vitest_browser_runner__ = { wrapDynamicImport: f => f() };\n");
@@ -899,6 +922,8 @@ var BrowserPlugin = (parentServer, base = "/") => {
899
922
  }
900
923
  }
901
924
  } else {
925
+ // inject the reset style only in the default template,
926
+ // allowing users to customize the style in their own template
902
927
  testerTags.push({
903
928
  tag: "style",
904
929
  children: `
@@ -970,6 +995,8 @@ body {
970
995
  const esbuildPlugin = {
971
996
  name: "test-utils-rewrite",
972
997
  setup(build) {
998
+ // test-utils: resolve to CJS instead of the browser because the browser version expects a global Vue object
999
+ // compiler-core: only CJS version allows slots as strings
973
1000
  build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
974
1001
  const resolved = getRequire().resolve(args.path, { paths: [args.importer] });
975
1002
  return { path: resolved };
@@ -1007,6 +1034,7 @@ function resolveCoverageFolder(vitest) {
1007
1034
  if (!htmlReporter) {
1008
1035
  return undefined;
1009
1036
  }
1037
+ // reportsDirectory not resolved yet
1010
1038
  const root = resolve(options.root || process.cwd(), options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory);
1011
1039
  const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : undefined;
1012
1040
  if (!subdir || typeof subdir !== "string") {
@@ -1122,6 +1150,7 @@ const dragAndDrop = async (context, source, target, options_) => {
1122
1150
  const $target = context.browser.$(target);
1123
1151
  const options = options_ || {};
1124
1152
  const duration = options.duration ?? 10;
1153
+ // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670
1125
1154
  await context.browser.action("pointer").move({
1126
1155
  duration: 0,
1127
1156
  origin: $source,
@@ -1626,6 +1655,7 @@ function assertFileAccess(path, project) {
1626
1655
  const readFile = async ({ project }, path, options = {}) => {
1627
1656
  const filepath = resolve$1(project.config.root, path);
1628
1657
  assertFileAccess(filepath, project);
1658
+ // never return a Buffer
1629
1659
  if (typeof options === "object" && !options.encoding) {
1630
1660
  options.encoding = "utf-8";
1631
1661
  }
@@ -2016,6 +2046,8 @@ const keyboardCleanup = async (context, state) => {
2016
2046
  throw new TypeError(`Provider "${context.provider.name}" does not support keyboard api`);
2017
2047
  }
2018
2048
  };
2049
+ // fallback to insertText for non US key
2050
+ // https://github.com/microsoft/playwright/blob/50775698ae13642742f2a1e8983d1d686d7f192d/packages/playwright-core/src/server/input.ts#L95
2019
2051
  const VALID_KEYS = new Set([
2020
2052
  "Escape",
2021
2053
  "F1",
@@ -2231,6 +2263,9 @@ async function keyboardImplementation(pressed, provider, sessionId, text, select
2231
2263
  const actions = parseKeyDef(defaultKeyMap, text);
2232
2264
  for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
2233
2265
  const key = keyDef.key;
2266
+ // TODO: instead of calling down/up for each key, join non special
2267
+ // together, and call `type` once for all non special keys,
2268
+ // and then `press` for special keys
2234
2269
  if (pressed.has(key)) {
2235
2270
  if (VALID_KEYS.has(key)) {
2236
2271
  await page.keyboard.up(key);
@@ -2297,6 +2332,7 @@ async function keyboardImplementation(pressed, provider, sessionId, text, select
2297
2332
  }
2298
2333
  }
2299
2334
  }
2335
+ // seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp
2300
2336
  const allRelease = keyboard.toJSON().actions.every((action) => action.type === "keyUp");
2301
2337
  await keyboard.perform(allRelease ? false : skipRelease);
2302
2338
  }
@@ -2318,6 +2354,10 @@ const screenshot = async (context, name, options = {}) => {
2318
2354
  if (!context.testPath) {
2319
2355
  throw new Error(`Cannot take a screenshot without a test path`);
2320
2356
  }
2357
+ options.save ??= true;
2358
+ if (!options.save) {
2359
+ options.base64 = true;
2360
+ }
2321
2361
  const path = options.path ? resolve(dirname(context.testPath), options.path) : resolveScreenshotPath(context.testPath, name, context.project.config);
2322
2362
  const savePath = normalize$1(path);
2323
2363
  await mkdir(dirname(path), { recursive: true });
@@ -2327,25 +2367,23 @@ const screenshot = async (context, name, options = {}) => {
2327
2367
  const element = context.iframe.locator(`${selector}`);
2328
2368
  const buffer = await element.screenshot({
2329
2369
  ...config,
2330
- path: savePath
2370
+ path: options.save ? savePath : undefined
2331
2371
  });
2332
2372
  return returnResult(options, path, buffer);
2333
2373
  }
2334
2374
  const buffer = await context.iframe.locator("body").screenshot({
2335
2375
  ...options,
2336
- path: savePath
2376
+ path: options.save ? savePath : undefined
2337
2377
  });
2338
2378
  return returnResult(options, path, buffer);
2339
2379
  }
2340
2380
  if (context.provider instanceof WebdriverBrowserProvider) {
2341
2381
  const page = context.provider.browser;
2342
- if (!options.element) {
2343
- const body = await page.$("body");
2344
- const buffer = await body.saveScreenshot(savePath);
2345
- return returnResult(options, path, buffer);
2346
- }
2347
- const element = await page.$(`${options.element}`);
2382
+ const element = !options.element ? await page.$("body") : await page.$(`${options.element}`);
2348
2383
  const buffer = await element.saveScreenshot(savePath);
2384
+ if (!options.save) {
2385
+ await rm(savePath, { force: true });
2386
+ }
2349
2387
  return returnResult(options, path, buffer);
2350
2388
  }
2351
2389
  throw new Error(`Provider "${context.provider.name}" does not support screenshots`);
@@ -2359,6 +2397,9 @@ function resolveScreenshotPath(testPath, name, config) {
2359
2397
  return resolve(dir, "__screenshots__", base, name);
2360
2398
  }
2361
2399
  function returnResult(options, path, buffer) {
2400
+ if (!options.save) {
2401
+ return buffer.toString("base64");
2402
+ }
2362
2403
  if (options.base64) {
2363
2404
  return {
2364
2405
  path,
@@ -2450,12 +2491,12 @@ const upload = async (context, selector, files, options) => {
2450
2491
  if (!testPath) {
2451
2492
  throw new Error(`Cannot upload files outside of a test`);
2452
2493
  }
2453
- const testDir = dirname(testPath);
2494
+ const root = context.project.config.root;
2454
2495
  if (context.provider instanceof PlaywrightBrowserProvider) {
2455
2496
  const { iframe } = context;
2456
2497
  const playwrightFiles = files.map((file) => {
2457
2498
  if (typeof file === "string") {
2458
- return resolve(testDir, file);
2499
+ return resolve(root, file);
2459
2500
  }
2460
2501
  return {
2461
2502
  name: file.name,
@@ -2472,7 +2513,7 @@ const upload = async (context, selector, files, options) => {
2472
2513
  }
2473
2514
  const element = context.browser.$(selector);
2474
2515
  for (const file of files) {
2475
- const filepath = resolve(testDir, file);
2516
+ const filepath = resolve(root, file);
2476
2517
  const remoteFilePath = await context.browser.uploadFile(filepath);
2477
2518
  await element.addValue(remoteFilePath);
2478
2519
  }
@@ -2481,6 +2522,14 @@ const upload = async (context, selector, files, options) => {
2481
2522
  }
2482
2523
  };
2483
2524
 
2525
+ const viewport = async (context, options) => {
2526
+ if (context.provider instanceof WebdriverBrowserProvider) {
2527
+ await context.provider.setViewport(options);
2528
+ } else {
2529
+ throw new TypeError(`Provider ${context.provider.name} doesn't support "viewport" command`);
2530
+ }
2531
+ };
2532
+
2484
2533
  var builtinCommands = {
2485
2534
  readFile,
2486
2535
  removeFile,
@@ -2499,7 +2548,8 @@ var builtinCommands = {
2499
2548
  __vitest_selectOptions: selectOptions,
2500
2549
  __vitest_dragAndDrop: dragAndDrop,
2501
2550
  __vitest_hover: hover,
2502
- __vitest_cleanup: keyboardCleanup
2551
+ __vitest_cleanup: keyboardCleanup,
2552
+ __vitest_viewport: viewport
2503
2553
  };
2504
2554
 
2505
2555
  class BrowserServerState {
@@ -2595,6 +2645,7 @@ class ParentBrowserProject {
2595
2645
  children = new Set();
2596
2646
  vitest;
2597
2647
  config;
2648
+ // cache for non-vite source maps
2598
2649
  sourceMapCache = new Map();
2599
2650
  constructor(project, base) {
2600
2651
  this.project = project;
@@ -2608,6 +2659,7 @@ class ParentBrowserProject {
2608
2659
  return this.sourceMapCache.get(id);
2609
2660
  }
2610
2661
  const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
2662
+ // this can happen for bundled dependencies in node_modules/.vite
2611
2663
  if (result && !result.map) {
2612
2664
  const sourceMapUrl = this.retrieveSourceMapURL(result.code);
2613
2665
  if (!sourceMapUrl) {
@@ -2631,6 +2683,8 @@ class ParentBrowserProject {
2631
2683
  if (modUrl) {
2632
2684
  return resolvedPath;
2633
2685
  }
2686
+ // some browsers (looking at you, safari) don't report queries in stack traces
2687
+ // the next best thing is to try the first id that this file resolves to
2634
2688
  const files = this.vite.moduleGraph.getModulesByFile(resolvedPath);
2635
2689
  if (files && files.size) {
2636
2690
  return files.values().next().value.id;
@@ -2641,6 +2695,7 @@ class ParentBrowserProject {
2641
2695
  for (const [name, command] of Object.entries(builtinCommands)) {
2642
2696
  this.commands[name] ??= command;
2643
2697
  }
2698
+ // validate names because they can't be used as identifiers
2644
2699
  for (const command in project.config.browser.commands) {
2645
2700
  if (!/^[a-z_$][\w$]*$/i.test(command)) {
2646
2701
  throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
@@ -2761,7 +2816,10 @@ class ParentBrowserProject {
2761
2816
  }
2762
2817
  retrieveSourceMapURL(source) {
2763
2818
  const re = /\/\/[@#]\s*sourceMappingURL=([^\s'"]+)\s*$|\/\*[@#]\s*sourceMappingURL=[^\s*'"]+\s*\*\/\s*$/gm;
2819
+ // Keep executing the search to find the *last* sourceMappingURL to avoid
2820
+ // picking up sourceMappingURLs from comments, strings, etc.
2764
2821
  let lastMatch, match;
2822
+ // eslint-disable-next-line no-cond-assign
2765
2823
  while (match = re.exec(source)) {
2766
2824
  lastMatch = match;
2767
2825
  }
@@ -2960,6 +3018,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2960
3018
  }
2961
3019
  if (type === "orchestrator") {
2962
3020
  const session = vitest._browserSessions.getSession(sessionId);
3021
+ // it's possible the session was already resolved by the preview provider
2963
3022
  session?.connected();
2964
3023
  }
2965
3024
  const project = vitest.getProjectByName(projectName);
@@ -2980,10 +3039,12 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2980
3039
  if (type === "orchestrator") {
2981
3040
  vitest._browserSessions.destroySession(sessionId);
2982
3041
  }
3042
+ // this will reject any hanging methods if there are any
2983
3043
  rpc.$close(new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`));
2984
3044
  });
2985
3045
  });
2986
3046
  });
3047
+ // we don't throw an error inside a stream because this can segfault the process
2987
3048
  function error(err) {
2988
3049
  console.error(err);
2989
3050
  vitest.state.catchError(err, "RPC Error");
@@ -3120,6 +3181,8 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3120
3181
  },
3121
3182
  async registerMock(sessionId, module) {
3122
3183
  if (!mocker) {
3184
+ // make sure modules are not processed yet in case they were imported before
3185
+ // and were not mocked
3123
3186
  mockResolver.invalidate([module.id]);
3124
3187
  if (module.type === "manual") {
3125
3188
  const mock = ManualMockedModule.fromJSON(module, async () => {
@@ -3190,7 +3253,10 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3190
3253
  return rpc;
3191
3254
  }
3192
3255
  }
3256
+ // Serialization support utils.
3193
3257
  function cloneByOwnProperties(value) {
3258
+ // Clones the value's properties into a new Object. The simpler approach of
3259
+ // Object.assign() won't work in the case that properties are not enumerable.
3194
3260
  return Object.getOwnPropertyNames(value).reduce((clone, prop) => ({
3195
3261
  ...clone,
3196
3262
  [prop]: value[prop]
@@ -3251,7 +3317,7 @@ function createBrowserPool(vitest) {
3251
3317
  vitest.onCancel(() => {
3252
3318
  isCancelled = true;
3253
3319
  });
3254
- await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
3320
+ const initialisedPools = await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
3255
3321
  await project._initBrowserProvider();
3256
3322
  if (!project.browser) {
3257
3323
  throw new TypeError(`The browser server was not initialized${project.name ? ` for the "${project.name}" project` : ""}. This is a bug in Vitest. Please, open a new issue with reproduction.`);
@@ -3263,8 +3329,34 @@ function createBrowserPool(vitest) {
3263
3329
  const pool = ensurePool(project);
3264
3330
  vitest.state.clearFiles(project, files);
3265
3331
  providers.add(project.browser.provider);
3266
- await pool.runTests(method, files);
3332
+ return {
3333
+ pool,
3334
+ provider: project.browser.provider,
3335
+ runTests: () => pool.runTests(method, files)
3336
+ };
3267
3337
  }));
3338
+ if (isCancelled) {
3339
+ return;
3340
+ }
3341
+ const parallelPools = [];
3342
+ const nonParallelPools = [];
3343
+ for (const result of initialisedPools) {
3344
+ if (!result) {
3345
+ return;
3346
+ }
3347
+ if (result.provider.mocker && result.provider.supportsParallelism) {
3348
+ parallelPools.push(result.runTests);
3349
+ } else {
3350
+ nonParallelPools.push(result.runTests);
3351
+ }
3352
+ }
3353
+ await Promise.all(parallelPools.map((runTests) => runTests()));
3354
+ for (const runTests of nonParallelPools) {
3355
+ if (isCancelled) {
3356
+ return;
3357
+ }
3358
+ await runTests();
3359
+ }
3268
3360
  };
3269
3361
  function getThreadsCount(project) {
3270
3362
  const config = project.config.browser;
@@ -3335,6 +3427,8 @@ class BrowserPool {
3335
3427
  debug?.("all orchestrators are ready, not creating more");
3336
3428
  return this._promise;
3337
3429
  }
3430
+ // open the minimum amount of tabs
3431
+ // if there is only 1 file running, we don't need 8 tabs running
3338
3432
  const workerCount = Math.min(this.options.maxWorkers - this.orchestrators.size, files.length);
3339
3433
  const promises = [];
3340
3434
  for (let i = 0; i < workerCount; i++) {
@@ -3343,6 +3437,7 @@ class BrowserPool {
3343
3437
  const project = this.project.name;
3344
3438
  debug?.("[%s] creating session for %s", sessionId, project);
3345
3439
  const page = this.openPage(sessionId).then(() => {
3440
+ // start running tests on the page when it's ready
3346
3441
  this.runNextTest(method, sessionId);
3347
3442
  });
3348
3443
  promises.push(page);
@@ -3367,6 +3462,7 @@ class BrowserPool {
3367
3462
  }
3368
3463
  finishSession(sessionId) {
3369
3464
  this.readySessions.add(sessionId);
3465
+ // the last worker finished running tests
3370
3466
  if (this.readySessions.size === this.orchestrators.size) {
3371
3467
  this._promise?.resolve();
3372
3468
  this._promise = undefined;
@@ -3380,10 +3476,14 @@ class BrowserPool {
3380
3476
  if (!file) {
3381
3477
  debug?.("[%s] no more tests to run", sessionId);
3382
3478
  const isolate = this.project.config.browser.isolate;
3479
+ // we don't need to cleanup testers if isolation is enabled,
3480
+ // because cleanup is done at the end of every test
3383
3481
  if (isolate) {
3384
3482
  this.finishSession(sessionId);
3385
3483
  return;
3386
3484
  }
3485
+ // we need to cleanup testers first because there is only
3486
+ // one iframe and it does the cleanup only after everything is completed
3387
3487
  const orchestrator = this.getOrchestrator(sessionId);
3388
3488
  orchestrator.cleanupTesters().catch((error) => this.reject(error)).finally(() => this.finishSession(sessionId));
3389
3489
  return;
@@ -3394,6 +3494,7 @@ class BrowserPool {
3394
3494
  const orchestrator = this.getOrchestrator(sessionId);
3395
3495
  debug?.("[%s] run test %s", sessionId, file);
3396
3496
  this.setBreakpoint(sessionId, file).then(() => {
3497
+ // this starts running tests inside the orchestrator
3397
3498
  orchestrator.createTesters({
3398
3499
  method,
3399
3500
  files: [file],
@@ -3402,6 +3503,7 @@ class BrowserPool {
3402
3503
  debug?.("[%s] test %s finished running", sessionId, file);
3403
3504
  this.runNextTest(method, sessionId);
3404
3505
  }).catch((error) => {
3506
+ // if user cancells the test run manually, ignore the error and exit gracefully
3405
3507
  if (this.project.vitest.isCancelling && error instanceof Error && error.message.startsWith("Browser connection was closed while running tests")) {
3406
3508
  this.cancel();
3407
3509
  this._promise?.resolve();
@@ -278,6 +278,7 @@ interface SelectorEngine {
278
278
  queryAll: (root: SelectorRoot, selector: string | any) => Element[];
279
279
  }
280
280
 
281
+ // we prefer using playwright locators because they are more powerful and support Shadow DOM
281
282
  declare const selectorEngine: Ivya;
282
283
  declare abstract class Locator {
283
284
  abstract selector: string;
@@ -1 +1 @@
1
- import"@vitest/browser/context";import"../public-utils-DUr23h1p.js";export{L as Locator,s as selectorEngine}from"../index-C3ICQ6zz.js";import"vitest/utils";
1
+ import"@vitest/browser/context";import"../public-utils-DJ-T0CfF.js";export{L as Locator,s as selectorEngine}from"../index-D0pxULUR.js";import"vitest/internal/browser";
@@ -1 +1 @@
1
- import{page,server}from"@vitest/browser/context";import{g as getByTitleSelector,a as getByTextSelector,b as getByPlaceholderSelector,c as getByAltTextSelector,d as getByTestIdSelector,e as getByRoleSelector,f as getByLabelSelector}from"../public-utils-DUr23h1p.js";import{s as selectorEngine,L as Locator,p as processTimeoutOptions,g as getIframeScale}from"../index-C3ICQ6zz.js";import"vitest/utils";page.extend({getByLabelText(e,_){return new PlaywrightLocator(getByLabelSelector(e,_))},getByRole(e,_){return new PlaywrightLocator(getByRoleSelector(e,_))},getByTestId(e){return new PlaywrightLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute,e))},getByAltText(e,_){return new PlaywrightLocator(getByAltTextSelector(e,_))},getByPlaceholder(e,_){return new PlaywrightLocator(getByPlaceholderSelector(e,_))},getByText(e,_){return new PlaywrightLocator(getByTextSelector(e,_))},getByTitle(e,_){return new PlaywrightLocator(getByTitleSelector(e,_))},elementLocator(e){return new PlaywrightLocator(selectorEngine.generateSelectorSimple(e),e)}});class PlaywrightLocator extends Locator{constructor(e,_){super(),this.selector=e,this._container=_}click(e){return super.click(processTimeoutOptions(processClickOptions(e)))}dblClick(e){return super.dblClick(processTimeoutOptions(processClickOptions(e)))}tripleClick(e){return super.tripleClick(processTimeoutOptions(processClickOptions(e)))}selectOptions(e,_){return super.selectOptions(e,processTimeoutOptions(_))}clear(e){return super.clear(processTimeoutOptions(e))}hover(e){return super.hover(processTimeoutOptions(processHoverOptions(e)))}upload(e,_){return super.upload(e,processTimeoutOptions(_))}fill(e,_){return super.fill(e,processTimeoutOptions(_))}dropTo(e,_){return super.dropTo(e,processTimeoutOptions(processDragAndDropOptions(_)))}locator(e){return new PlaywrightLocator(`${this.selector} >> ${e}`,this._container)}elementLocator(e){return new PlaywrightLocator(selectorEngine.generateSelectorSimple(e),e)}}function processDragAndDropOptions(e){if(!e)return e;let _=e;return _.sourcePosition&&=processPlaywrightPosition(_.sourcePosition),_.targetPosition&&=processPlaywrightPosition(_.targetPosition),e}function processHoverOptions(e){if(!e)return e;let _=e;return _.position&&=processPlaywrightPosition(_.position),e}function processClickOptions(e){if(!e)return e;let _=e;return _.position&&=processPlaywrightPosition(_.position),_}function processPlaywrightPosition(e){let _=getIframeScale();return e.x!=null&&(e.x*=_),e.y!=null&&(e.y*=_),e}
1
+ import{page,server}from"@vitest/browser/context";import{g as getByTitleSelector,a as getByTextSelector,b as getByPlaceholderSelector,c as getByAltTextSelector,d as getByTestIdSelector,e as getByRoleSelector,f as getByLabelSelector}from"../public-utils-DJ-T0CfF.js";import{s as selectorEngine,L as Locator,p as processTimeoutOptions,g as getIframeScale}from"../index-D0pxULUR.js";import"vitest/internal/browser";page.extend({getByLabelText(e,_){return new PlaywrightLocator(getByLabelSelector(e,_))},getByRole(e,_){return new PlaywrightLocator(getByRoleSelector(e,_))},getByTestId(e){return new PlaywrightLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute,e))},getByAltText(e,_){return new PlaywrightLocator(getByAltTextSelector(e,_))},getByPlaceholder(e,_){return new PlaywrightLocator(getByPlaceholderSelector(e,_))},getByText(e,_){return new PlaywrightLocator(getByTextSelector(e,_))},getByTitle(e,_){return new PlaywrightLocator(getByTitleSelector(e,_))},_createLocator(e){return new PlaywrightLocator(e)},elementLocator(e){return new PlaywrightLocator(selectorEngine.generateSelectorSimple(e),e)}});class PlaywrightLocator extends Locator{constructor(e,_){super(),this.selector=e,this._container=_}click(e){return super.click(processTimeoutOptions(processClickOptions(e)))}dblClick(e){return super.dblClick(processTimeoutOptions(processClickOptions(e)))}tripleClick(e){return super.tripleClick(processTimeoutOptions(processClickOptions(e)))}selectOptions(e,_){return super.selectOptions(e,processTimeoutOptions(_))}clear(e){return super.clear(processTimeoutOptions(e))}hover(e){return super.hover(processTimeoutOptions(processHoverOptions(e)))}upload(e,_){return super.upload(e,processTimeoutOptions(_))}fill(e,_){return super.fill(e,processTimeoutOptions(_))}dropTo(e,_){return super.dropTo(e,processTimeoutOptions(processDragAndDropOptions(_)))}locator(e){return new PlaywrightLocator(`${this.selector} >> ${e}`,this._container)}elementLocator(e){return new PlaywrightLocator(selectorEngine.generateSelectorSimple(e),e)}}function processDragAndDropOptions(e){if(!e)return e;let _=e;return _.sourcePosition&&=processPlaywrightPosition(_.sourcePosition),_.targetPosition&&=processPlaywrightPosition(_.targetPosition),e}function processHoverOptions(e){if(!e)return e;let _=e;return _.position&&=processPlaywrightPosition(_.position),e}function processClickOptions(e){if(!e)return e;let _=e;return _.position&&=processPlaywrightPosition(_.position),_}function processPlaywrightPosition(e){let _=getIframeScale();return e.x!=null&&(e.x*=_),e.y!=null&&(e.y*=_),e}
@@ -1 +1 @@
1
- import{page,server,userEvent}from"@vitest/browser/context";import{g as getByTitleSelector,a as getByTextSelector,b as getByPlaceholderSelector,c as getByAltTextSelector,d as getByTestIdSelector,e as getByRoleSelector,f as getByLabelSelector,h as getElementError}from"../public-utils-DUr23h1p.js";import{s as selectorEngine,L as Locator,c as convertElementToCssSelector}from"../index-C3ICQ6zz.js";import"vitest/utils";page.extend({getByLabelText(e,m){return new PreviewLocator(getByLabelSelector(e,m))},getByRole(e,m){return new PreviewLocator(getByRoleSelector(e,m))},getByTestId(e){return new PreviewLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute,e))},getByAltText(e,m){return new PreviewLocator(getByAltTextSelector(e,m))},getByPlaceholder(e,m){return new PreviewLocator(getByPlaceholderSelector(e,m))},getByText(e,m){return new PreviewLocator(getByTextSelector(e,m))},getByTitle(e,m){return new PreviewLocator(getByTitleSelector(e,m))},elementLocator(e){return new PreviewLocator(selectorEngine.generateSelectorSimple(e),e)}});class PreviewLocator extends Locator{constructor(e,m){super(),this._pwSelector=e,this._container=m}get selector(){let e=this.elements().map(e=>convertElementToCssSelector(e));if(!e.length)throw getElementError(this._pwSelector,this._container||document.body);return e.join(`, `)}click(){return userEvent.click(this.element())}dblClick(){return userEvent.dblClick(this.element())}tripleClick(){return userEvent.tripleClick(this.element())}hover(){return userEvent.hover(this.element())}unhover(){return userEvent.unhover(this.element())}async fill(e){return userEvent.fill(this.element(),e)}async upload(e){return userEvent.upload(this.element(),e)}selectOptions(e){return userEvent.selectOptions(this.element(),e)}clear(){return userEvent.clear(this.element())}locator(e){return new PreviewLocator(`${this._pwSelector} >> ${e}`,this._container)}elementLocator(e){return new PreviewLocator(selectorEngine.generateSelectorSimple(e),e)}}
1
+ import{page,server,userEvent}from"@vitest/browser/context";import{g as getByTitleSelector,a as getByTextSelector,b as getByPlaceholderSelector,c as getByAltTextSelector,d as getByTestIdSelector,e as getByRoleSelector,f as getByLabelSelector,h as getElementError}from"../public-utils-DJ-T0CfF.js";import{s as selectorEngine,L as Locator,c as convertElementToCssSelector}from"../index-D0pxULUR.js";import"vitest/internal/browser";page.extend({getByLabelText(e,m){return new PreviewLocator(getByLabelSelector(e,m))},getByRole(e,m){return new PreviewLocator(getByRoleSelector(e,m))},getByTestId(e){return new PreviewLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute,e))},getByAltText(e,m){return new PreviewLocator(getByAltTextSelector(e,m))},getByPlaceholder(e,m){return new PreviewLocator(getByPlaceholderSelector(e,m))},getByText(e,m){return new PreviewLocator(getByTextSelector(e,m))},getByTitle(e,m){return new PreviewLocator(getByTitleSelector(e,m))},_createLocator(e){return new PreviewLocator(e)},elementLocator(e){return new PreviewLocator(selectorEngine.generateSelectorSimple(e),e)}});class PreviewLocator extends Locator{constructor(e,m){super(),this._pwSelector=e,this._container=m}get selector(){let e=this.elements().map(e=>convertElementToCssSelector(e));if(!e.length)throw getElementError(this._pwSelector,this._container||document.body);return e.join(`, `)}click(){return userEvent.click(this.element())}dblClick(){return userEvent.dblClick(this.element())}tripleClick(){return userEvent.tripleClick(this.element())}hover(){return userEvent.hover(this.element())}unhover(){return userEvent.unhover(this.element())}async fill(e){return userEvent.fill(this.element(),e)}async upload(e){return userEvent.upload(this.element(),e)}selectOptions(e){return userEvent.selectOptions(this.element(),e)}clear(){return userEvent.clear(this.element())}locator(e){return new PreviewLocator(`${this._pwSelector} >> ${e}`,this._container)}elementLocator(e){return new PreviewLocator(selectorEngine.generateSelectorSimple(e),e)}}
@@ -1 +1 @@
1
- import{page,server}from"@vitest/browser/context";import{g as getByTitleSelector,a as getByTextSelector,b as getByPlaceholderSelector,c as getByAltTextSelector,d as getByTestIdSelector,e as getByRoleSelector,f as getByLabelSelector,h as getElementError}from"../public-utils-DUr23h1p.js";import{s as selectorEngine,L as Locator,c as convertElementToCssSelector,a as getBrowserState,g as getIframeScale}from"../index-C3ICQ6zz.js";import"vitest/utils";page.extend({getByLabelText(e,g){return new WebdriverIOLocator(getByLabelSelector(e,g))},getByRole(e,g){return new WebdriverIOLocator(getByRoleSelector(e,g))},getByTestId(e){return new WebdriverIOLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute,e))},getByAltText(e,g){return new WebdriverIOLocator(getByAltTextSelector(e,g))},getByPlaceholder(e,g){return new WebdriverIOLocator(getByPlaceholderSelector(e,g))},getByText(e,g){return new WebdriverIOLocator(getByTextSelector(e,g))},getByTitle(e,g){return new WebdriverIOLocator(getByTitleSelector(e,g))},elementLocator(e){return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(e))}});class WebdriverIOLocator extends Locator{constructor(e,g){super(),this._pwSelector=e,this._container=g}get selector(){let e=this.elements().map(e=>convertElementToCssSelector(e));if(!e.length)throw getElementError(this._pwSelector,this._container||document.body);return e.join(`, `)}click(e){return super.click(processClickOptions(e))}dblClick(e){return super.dblClick(processClickOptions(e))}tripleClick(e){return super.tripleClick(processClickOptions(e))}selectOptions(e,g){let _=getWebdriverioSelectOptions(this.element(),e);return this.triggerCommand(`__vitest_selectOptions`,this.selector,_,g)}hover(e){return super.hover(processHoverOptions(e))}dropTo(e,g){return super.dropTo(e,processDragAndDropOptions(g))}locator(e){return new WebdriverIOLocator(`${this._pwSelector} >> ${e}`,this._container)}elementLocator(e){return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(e),e)}}function getWebdriverioSelectOptions(e,g){let _=[...e.querySelectorAll(`option`)],v=Array.isArray(g)?g:[g];if(!v.length)return[];if(v.length>1)throw Error(`Provider "webdriverio" doesn't support selecting multiple values at once`);let y=v[0];if(typeof y!=`string`){let e=`element`in y?y.element():y,g=_.indexOf(e);if(g===-1)throw Error(`The element ${selectorEngine.previewNode(e)} was not found in the "select" options.`);return[{index:g}]}let b=_.findIndex(e=>e.value===y);if(b!==-1)return[{index:b}];let x=_.findIndex(e=>e.textContent?.trim()===y||e.ariaLabel===y);if(x===-1)throw Error(`The option "${y}" was not found in the "select" options.`);return[{index:x}]}function processClickOptions(e){if(!e||!getBrowserState().config.browser.ui)return e;let g=e;if(g.x!=null||g.y!=null){let e={};g.x!=null&&(g.x=scaleCoordinate(g.x,e)),g.y!=null&&(g.y=scaleCoordinate(g.y,e))}return e}function processHoverOptions(e){if(!e||!getBrowserState().config.browser.ui)return e;let g=e,_={};return g.xOffset!=null&&(g.xOffset=scaleCoordinate(g.xOffset,_)),g.yOffset!=null&&(g.yOffset=scaleCoordinate(g.yOffset,_)),e}function processDragAndDropOptions(e){if(!e||!getBrowserState().config.browser.ui)return e;let g={},_=e;return _.sourceX!=null&&(_.sourceX=scaleCoordinate(_.sourceX,g)),_.sourceY!=null&&(_.sourceY=scaleCoordinate(_.sourceY,g)),_.targetX!=null&&(_.targetX=scaleCoordinate(_.targetX,g)),_.targetY!=null&&(_.targetY=scaleCoordinate(_.targetY,g)),e}function scaleCoordinate(e,g){return Math.round(e*getCachedScale(g))}function getCachedScale(e){return e.scale??=getIframeScale()}
1
+ import{page,server}from"@vitest/browser/context";import{g as getByTitleSelector,a as getByTextSelector,b as getByPlaceholderSelector,c as getByAltTextSelector,d as getByTestIdSelector,e as getByRoleSelector,f as getByLabelSelector,h as getElementError}from"../public-utils-DJ-T0CfF.js";import{s as selectorEngine,L as Locator,c as convertElementToCssSelector,a as getBrowserState,g as getIframeScale}from"../index-D0pxULUR.js";import"vitest/internal/browser";page.extend({getByLabelText(e,g){return new WebdriverIOLocator(getByLabelSelector(e,g))},getByRole(e,g){return new WebdriverIOLocator(getByRoleSelector(e,g))},getByTestId(e){return new WebdriverIOLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute,e))},getByAltText(e,g){return new WebdriverIOLocator(getByAltTextSelector(e,g))},getByPlaceholder(e,g){return new WebdriverIOLocator(getByPlaceholderSelector(e,g))},getByText(e,g){return new WebdriverIOLocator(getByTextSelector(e,g))},getByTitle(e,g){return new WebdriverIOLocator(getByTitleSelector(e,g))},_createLocator(e){return new WebdriverIOLocator(e)},elementLocator(e){return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(e))}});class WebdriverIOLocator extends Locator{constructor(e,g){super(),this._pwSelector=e,this._container=g}get selector(){let e=this.elements().map(e=>convertElementToCssSelector(e));if(!e.length)throw getElementError(this._pwSelector,this._container||document.body);return e.join(`, `)}click(e){return super.click(processClickOptions(e))}dblClick(e){return super.dblClick(processClickOptions(e))}tripleClick(e){return super.tripleClick(processClickOptions(e))}selectOptions(e,g){let _=getWebdriverioSelectOptions(this.element(),e);return this.triggerCommand(`__vitest_selectOptions`,this.selector,_,g)}hover(e){return super.hover(processHoverOptions(e))}dropTo(e,g){return super.dropTo(e,processDragAndDropOptions(g))}locator(e){return new WebdriverIOLocator(`${this._pwSelector} >> ${e}`,this._container)}elementLocator(e){return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(e),e)}}function getWebdriverioSelectOptions(e,g){let _=[...e.querySelectorAll(`option`)],v=Array.isArray(g)?g:[g];if(!v.length)return[];if(v.length>1)throw Error(`Provider "webdriverio" doesn't support selecting multiple values at once`);let y=v[0];if(typeof y!=`string`){let e=`element`in y?y.element():y,g=_.indexOf(e);if(g===-1)throw Error(`The element ${selectorEngine.previewNode(e)} was not found in the "select" options.`);return[{index:g}]}let b=_.findIndex(e=>e.value===y);if(b!==-1)return[{index:b}];let x=_.findIndex(e=>e.textContent?.trim()===y||e.ariaLabel===y);if(x===-1)throw Error(`The option "${y}" was not found in the "select" options.`);return[{index:x}]}function processClickOptions(e){if(!e||!getBrowserState().config.browser.ui)return e;let g=e;if(g.x!=null||g.y!=null){let e={};g.x!=null&&(g.x=scaleCoordinate(g.x,e)),g.y!=null&&(g.y=scaleCoordinate(g.y,e))}return e}function processHoverOptions(e){if(!e||!getBrowserState().config.browser.ui)return e;let g=e,_={};return g.xOffset!=null&&(g.xOffset=scaleCoordinate(g.xOffset,_)),g.yOffset!=null&&(g.yOffset=scaleCoordinate(g.yOffset,_)),e}function processDragAndDropOptions(e){if(!e||!getBrowserState().config.browser.ui)return e;let g={},_=e;return _.sourceX!=null&&(_.sourceX=scaleCoordinate(_.sourceX,g)),_.sourceY!=null&&(_.sourceY=scaleCoordinate(_.sourceY,g)),_.targetX!=null&&(_.targetX=scaleCoordinate(_.targetX,g)),_.targetY!=null&&(_.targetY=scaleCoordinate(_.targetY,g)),e}function scaleCoordinate(e,g){return Math.round(e*getCachedScale(g))}function getCachedScale(e){return e.scale??=getIframeScale()}
package/dist/providers.js CHANGED
@@ -1,4 +1,4 @@
1
- import { W as WebdriverBrowserProvider, P as PlaywrightBrowserProvider } from './webdriver-D7k26Na7.js';
1
+ import { W as WebdriverBrowserProvider, P as PlaywrightBrowserProvider } from './webdriver-B1QbgqhC.js';
2
2
  import '@vitest/mocker/node';
3
3
  import 'tinyrainbow';
4
4
  import 'vitest/node';
@@ -9,6 +9,7 @@ class PreviewBrowserProvider {
9
9
  project;
10
10
  open = false;
11
11
  getSupportedBrowsers() {
12
+ // `none` is not restricted to certain browsers.
12
13
  return [];
13
14
  }
14
15
  isOpen() {