@vitest/browser 3.2.0-beta.2 → 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
@@ -13,12 +13,12 @@ import { fileURLToPath } from 'node:url';
13
13
  import crypto from 'node:crypto';
14
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.2";
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";
@@ -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]);
@@ -735,8 +754,10 @@ var BrowserPlugin = (parentServer, base = "/") => {
735
754
  if (svelte) {
736
755
  exclude.push("vitest-browser-svelte");
737
756
  }
757
+ // since we override the resolution in the esbuild plugin, Vite can no longer optimizer it
738
758
  const vue = isPackageExists("vitest-browser-vue", fileRoot);
739
759
  if (vue) {
760
+ // we override them in the esbuild plugin so optimizer can no longer intercept it
740
761
  include.push("vitest-browser-vue", "vitest-browser-vue > @vue/test-utils", "vitest-browser-vue > @vue/test-utils > @vue/compiler-core");
741
762
  }
742
763
  const vueTestUtils = isPackageExists("@vue/test-utils", fileRoot);
@@ -787,6 +808,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
787
808
  },
788
809
  transform(code, id) {
789
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
790
812
  const utilRequire = "nodeUtil = require_util();";
791
813
  return code.replace(utilRequire, " ".repeat(utilRequire.length));
792
814
  }
@@ -829,11 +851,12 @@ var BrowserPlugin = (parentServer, base = "/") => {
829
851
  {
830
852
  name: "vitest:browser:in-source-tests",
831
853
  transform(code, id) {
854
+ const filename = cleanUrl(id);
832
855
  const project = parentServer.vitest.getProjectByName(parentServer.config.name);
833
- if (!project._isCachedTestFile(id) || !code.includes("import.meta.vitest")) {
856
+ if (!project._isCachedTestFile(filename) || !code.includes("import.meta.vitest")) {
834
857
  return;
835
858
  }
836
- const s = new MagicString(code, { filename: cleanUrl(id) });
859
+ const s = new MagicString(code, { filename });
837
860
  s.prepend(`import.meta.vitest = __vitest_index__;\n`);
838
861
  return {
839
862
  code: s.toString(),
@@ -844,6 +867,7 @@ var BrowserPlugin = (parentServer, base = "/") => {
844
867
  {
845
868
  name: "vitest:browser:worker",
846
869
  transform(code, id, _options) {
870
+ // https://github.com/vitejs/vite/blob/ba56cf43b5480f8519349f7d7fe60718e9af5f1a/packages/vite/src/node/plugins/worker.ts#L46
847
871
  if (/(?:\?|&)worker_file&type=\w+(?:&|$)/.test(id)) {
848
872
  const s = new MagicString(code);
849
873
  s.prepend("globalThis.__vitest_browser_runner__ = { wrapDynamicImport: f => f() };\n");
@@ -898,6 +922,8 @@ var BrowserPlugin = (parentServer, base = "/") => {
898
922
  }
899
923
  }
900
924
  } else {
925
+ // inject the reset style only in the default template,
926
+ // allowing users to customize the style in their own template
901
927
  testerTags.push({
902
928
  tag: "style",
903
929
  children: `
@@ -969,6 +995,8 @@ body {
969
995
  const esbuildPlugin = {
970
996
  name: "test-utils-rewrite",
971
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
972
1000
  build.onResolve({ filter: /^@vue\/(test-utils|compiler-core)$/ }, (args) => {
973
1001
  const resolved = getRequire().resolve(args.path, { paths: [args.importer] });
974
1002
  return { path: resolved };
@@ -1006,6 +1034,7 @@ function resolveCoverageFolder(vitest) {
1006
1034
  if (!htmlReporter) {
1007
1035
  return undefined;
1008
1036
  }
1037
+ // reportsDirectory not resolved yet
1009
1038
  const root = resolve(options.root || process.cwd(), options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory);
1010
1039
  const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : undefined;
1011
1040
  if (!subdir || typeof subdir !== "string") {
@@ -1121,6 +1150,7 @@ const dragAndDrop = async (context, source, target, options_) => {
1121
1150
  const $target = context.browser.$(target);
1122
1151
  const options = options_ || {};
1123
1152
  const duration = options.duration ?? 10;
1153
+ // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670
1124
1154
  await context.browser.action("pointer").move({
1125
1155
  duration: 0,
1126
1156
  origin: $source,
@@ -1625,6 +1655,7 @@ function assertFileAccess(path, project) {
1625
1655
  const readFile = async ({ project }, path, options = {}) => {
1626
1656
  const filepath = resolve$1(project.config.root, path);
1627
1657
  assertFileAccess(filepath, project);
1658
+ // never return a Buffer
1628
1659
  if (typeof options === "object" && !options.encoding) {
1629
1660
  options.encoding = "utf-8";
1630
1661
  }
@@ -2015,6 +2046,8 @@ const keyboardCleanup = async (context, state) => {
2015
2046
  throw new TypeError(`Provider "${context.provider.name}" does not support keyboard api`);
2016
2047
  }
2017
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
2018
2051
  const VALID_KEYS = new Set([
2019
2052
  "Escape",
2020
2053
  "F1",
@@ -2230,6 +2263,9 @@ async function keyboardImplementation(pressed, provider, sessionId, text, select
2230
2263
  const actions = parseKeyDef(defaultKeyMap, text);
2231
2264
  for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
2232
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
2233
2269
  if (pressed.has(key)) {
2234
2270
  if (VALID_KEYS.has(key)) {
2235
2271
  await page.keyboard.up(key);
@@ -2296,6 +2332,7 @@ async function keyboardImplementation(pressed, provider, sessionId, text, select
2296
2332
  }
2297
2333
  }
2298
2334
  }
2335
+ // seems like webdriverio doesn't release keys automatically if skipRelease is true and all events are keyUp
2299
2336
  const allRelease = keyboard.toJSON().actions.every((action) => action.type === "keyUp");
2300
2337
  await keyboard.perform(allRelease ? false : skipRelease);
2301
2338
  }
@@ -2454,12 +2491,12 @@ const upload = async (context, selector, files, options) => {
2454
2491
  if (!testPath) {
2455
2492
  throw new Error(`Cannot upload files outside of a test`);
2456
2493
  }
2457
- const testDir = dirname(testPath);
2494
+ const root = context.project.config.root;
2458
2495
  if (context.provider instanceof PlaywrightBrowserProvider) {
2459
2496
  const { iframe } = context;
2460
2497
  const playwrightFiles = files.map((file) => {
2461
2498
  if (typeof file === "string") {
2462
- return resolve(testDir, file);
2499
+ return resolve(root, file);
2463
2500
  }
2464
2501
  return {
2465
2502
  name: file.name,
@@ -2476,7 +2513,7 @@ const upload = async (context, selector, files, options) => {
2476
2513
  }
2477
2514
  const element = context.browser.$(selector);
2478
2515
  for (const file of files) {
2479
- const filepath = resolve(testDir, file);
2516
+ const filepath = resolve(root, file);
2480
2517
  const remoteFilePath = await context.browser.uploadFile(filepath);
2481
2518
  await element.addValue(remoteFilePath);
2482
2519
  }
@@ -2485,6 +2522,14 @@ const upload = async (context, selector, files, options) => {
2485
2522
  }
2486
2523
  };
2487
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
+
2488
2533
  var builtinCommands = {
2489
2534
  readFile,
2490
2535
  removeFile,
@@ -2503,7 +2548,8 @@ var builtinCommands = {
2503
2548
  __vitest_selectOptions: selectOptions,
2504
2549
  __vitest_dragAndDrop: dragAndDrop,
2505
2550
  __vitest_hover: hover,
2506
- __vitest_cleanup: keyboardCleanup
2551
+ __vitest_cleanup: keyboardCleanup,
2552
+ __vitest_viewport: viewport
2507
2553
  };
2508
2554
 
2509
2555
  class BrowserServerState {
@@ -2599,6 +2645,7 @@ class ParentBrowserProject {
2599
2645
  children = new Set();
2600
2646
  vitest;
2601
2647
  config;
2648
+ // cache for non-vite source maps
2602
2649
  sourceMapCache = new Map();
2603
2650
  constructor(project, base) {
2604
2651
  this.project = project;
@@ -2612,6 +2659,7 @@ class ParentBrowserProject {
2612
2659
  return this.sourceMapCache.get(id);
2613
2660
  }
2614
2661
  const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
2662
+ // this can happen for bundled dependencies in node_modules/.vite
2615
2663
  if (result && !result.map) {
2616
2664
  const sourceMapUrl = this.retrieveSourceMapURL(result.code);
2617
2665
  if (!sourceMapUrl) {
@@ -2635,6 +2683,8 @@ class ParentBrowserProject {
2635
2683
  if (modUrl) {
2636
2684
  return resolvedPath;
2637
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
2638
2688
  const files = this.vite.moduleGraph.getModulesByFile(resolvedPath);
2639
2689
  if (files && files.size) {
2640
2690
  return files.values().next().value.id;
@@ -2645,6 +2695,7 @@ class ParentBrowserProject {
2645
2695
  for (const [name, command] of Object.entries(builtinCommands)) {
2646
2696
  this.commands[name] ??= command;
2647
2697
  }
2698
+ // validate names because they can't be used as identifiers
2648
2699
  for (const command in project.config.browser.commands) {
2649
2700
  if (!/^[a-z_$][\w$]*$/i.test(command)) {
2650
2701
  throw new Error(`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`);
@@ -2765,7 +2816,10 @@ class ParentBrowserProject {
2765
2816
  }
2766
2817
  retrieveSourceMapURL(source) {
2767
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.
2768
2821
  let lastMatch, match;
2822
+ // eslint-disable-next-line no-cond-assign
2769
2823
  while (match = re.exec(source)) {
2770
2824
  lastMatch = match;
2771
2825
  }
@@ -2964,6 +3018,7 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2964
3018
  }
2965
3019
  if (type === "orchestrator") {
2966
3020
  const session = vitest._browserSessions.getSession(sessionId);
3021
+ // it's possible the session was already resolved by the preview provider
2967
3022
  session?.connected();
2968
3023
  }
2969
3024
  const project = vitest.getProjectByName(projectName);
@@ -2984,10 +3039,12 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
2984
3039
  if (type === "orchestrator") {
2985
3040
  vitest._browserSessions.destroySession(sessionId);
2986
3041
  }
3042
+ // this will reject any hanging methods if there are any
2987
3043
  rpc.$close(new Error(`[vitest] Browser connection was closed while running tests. Was the page closed unexpectedly?`));
2988
3044
  });
2989
3045
  });
2990
3046
  });
3047
+ // we don't throw an error inside a stream because this can segfault the process
2991
3048
  function error(err) {
2992
3049
  console.error(err);
2993
3050
  vitest.state.catchError(err, "RPC Error");
@@ -3124,6 +3181,8 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3124
3181
  },
3125
3182
  async registerMock(sessionId, module) {
3126
3183
  if (!mocker) {
3184
+ // make sure modules are not processed yet in case they were imported before
3185
+ // and were not mocked
3127
3186
  mockResolver.invalidate([module.id]);
3128
3187
  if (module.type === "manual") {
3129
3188
  const mock = ManualMockedModule.fromJSON(module, async () => {
@@ -3194,7 +3253,10 @@ function setupBrowserRpc(globalServer, defaultMockerRegistry) {
3194
3253
  return rpc;
3195
3254
  }
3196
3255
  }
3256
+ // Serialization support utils.
3197
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.
3198
3260
  return Object.getOwnPropertyNames(value).reduce((clone, prop) => ({
3199
3261
  ...clone,
3200
3262
  [prop]: value[prop]
@@ -3255,7 +3317,7 @@ function createBrowserPool(vitest) {
3255
3317
  vitest.onCancel(() => {
3256
3318
  isCancelled = true;
3257
3319
  });
3258
- await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
3320
+ const initialisedPools = await Promise.all([...groupedFiles.entries()].map(async ([project, files]) => {
3259
3321
  await project._initBrowserProvider();
3260
3322
  if (!project.browser) {
3261
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.`);
@@ -3267,8 +3329,34 @@ function createBrowserPool(vitest) {
3267
3329
  const pool = ensurePool(project);
3268
3330
  vitest.state.clearFiles(project, files);
3269
3331
  providers.add(project.browser.provider);
3270
- await pool.runTests(method, files);
3332
+ return {
3333
+ pool,
3334
+ provider: project.browser.provider,
3335
+ runTests: () => pool.runTests(method, files)
3336
+ };
3271
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
+ }
3272
3360
  };
3273
3361
  function getThreadsCount(project) {
3274
3362
  const config = project.config.browser;
@@ -3339,6 +3427,8 @@ class BrowserPool {
3339
3427
  debug?.("all orchestrators are ready, not creating more");
3340
3428
  return this._promise;
3341
3429
  }
3430
+ // open the minimum amount of tabs
3431
+ // if there is only 1 file running, we don't need 8 tabs running
3342
3432
  const workerCount = Math.min(this.options.maxWorkers - this.orchestrators.size, files.length);
3343
3433
  const promises = [];
3344
3434
  for (let i = 0; i < workerCount; i++) {
@@ -3347,6 +3437,7 @@ class BrowserPool {
3347
3437
  const project = this.project.name;
3348
3438
  debug?.("[%s] creating session for %s", sessionId, project);
3349
3439
  const page = this.openPage(sessionId).then(() => {
3440
+ // start running tests on the page when it's ready
3350
3441
  this.runNextTest(method, sessionId);
3351
3442
  });
3352
3443
  promises.push(page);
@@ -3371,6 +3462,7 @@ class BrowserPool {
3371
3462
  }
3372
3463
  finishSession(sessionId) {
3373
3464
  this.readySessions.add(sessionId);
3465
+ // the last worker finished running tests
3374
3466
  if (this.readySessions.size === this.orchestrators.size) {
3375
3467
  this._promise?.resolve();
3376
3468
  this._promise = undefined;
@@ -3384,10 +3476,14 @@ class BrowserPool {
3384
3476
  if (!file) {
3385
3477
  debug?.("[%s] no more tests to run", sessionId);
3386
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
3387
3481
  if (isolate) {
3388
3482
  this.finishSession(sessionId);
3389
3483
  return;
3390
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
3391
3487
  const orchestrator = this.getOrchestrator(sessionId);
3392
3488
  orchestrator.cleanupTesters().catch((error) => this.reject(error)).finally(() => this.finishSession(sessionId));
3393
3489
  return;
@@ -3398,6 +3494,7 @@ class BrowserPool {
3398
3494
  const orchestrator = this.getOrchestrator(sessionId);
3399
3495
  debug?.("[%s] run test %s", sessionId, file);
3400
3496
  this.setBreakpoint(sessionId, file).then(() => {
3497
+ // this starts running tests inside the orchestrator
3401
3498
  orchestrator.createTesters({
3402
3499
  method,
3403
3500
  files: [file],
@@ -3406,6 +3503,7 @@ class BrowserPool {
3406
3503
  debug?.("[%s] test %s finished running", sessionId, file);
3407
3504
  this.runNextTest(method, sessionId);
3408
3505
  }).catch((error) => {
3506
+ // if user cancells the test run manually, ignore the error and exit gracefully
3409
3507
  if (this.project.vitest.isCancelling && error instanceof Error && error.message.startsWith("Browser connection was closed while running tests")) {
3410
3508
  this.cancel();
3411
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;
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() {
package/dist/state.js CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  /* @__NO_SIDE_EFFECTS__ */
5
5
  function getBrowserState() {
6
+ // @ts-expect-error not typed global
6
7
  return window.__vitest_browser_runner__;
7
8
  }
8
9
 
@@ -40,7 +41,9 @@
40
41
  },
41
42
  providedContext: {}
42
43
  };
44
+ // @ts-expect-error not typed global
43
45
  globalThis.__vitest_browser__ = true;
46
+ // @ts-expect-error not typed global
44
47
  globalThis.__vitest_worker__ = state;
45
48
  getBrowserState().cdp = createCdp();
46
49
  function rpc() {
@@ -59,6 +59,7 @@ class PlaywrightBrowserProvider {
59
59
  headless: options.headless
60
60
  };
61
61
  if (this.project.config.inspector.enabled) {
62
+ // NodeJS equivalent defaults: https://nodejs.org/en/learn/getting-started/debugging#enable-inspector
62
63
  const port = this.project.config.inspector.port || 9229;
63
64
  const host = this.project.config.inspector.host || "127.0.0.1";
64
65
  launchOptions.args ||= [];
@@ -66,6 +67,7 @@ class PlaywrightBrowserProvider {
66
67
  launchOptions.args.push(`--remote-debugging-address=${host}`);
67
68
  this.project.vitest.logger.log(`Debugger listening on ws://${host}:${port}`);
68
69
  }
70
+ // start Vitest UI maximized only on supported browsers
69
71
  if (this.project.config.browser.ui && this.browserName === "chromium") {
70
72
  if (!launchOptions.args) {
71
73
  launchOptions.args = [];
@@ -90,15 +92,18 @@ class PlaywrightBrowserProvider {
90
92
  if (url.searchParams.has("_vitest_original")) {
91
93
  return false;
92
94
  }
95
+ // different modules, ignore request
93
96
  if (url.pathname !== moduleUrl.pathname) {
94
97
  return false;
95
98
  }
96
99
  url.searchParams.delete("t");
97
100
  url.searchParams.delete("v");
98
101
  url.searchParams.delete("import");
102
+ // different search params, ignore request
99
103
  if (url.searchParams.size !== moduleUrl.searchParams.size) {
100
104
  return false;
101
105
  }
106
+ // check that all search params are the same
102
107
  for (const [param, value] of url.searchParams.entries()) {
103
108
  if (moduleUrl.searchParams.get(param) !== value) {
104
109
  return false;
@@ -109,9 +114,12 @@ class PlaywrightBrowserProvider {
109
114
  const ids = sessionIds.get(sessionId) || [];
110
115
  ids.push(moduleUrl.href);
111
116
  sessionIds.set(sessionId, ids);
112
- idPreficates.set(moduleUrl.href, predicate);
117
+ idPreficates.set(predicateKey(sessionId, moduleUrl.href), predicate);
113
118
  return predicate;
114
119
  }
120
+ function predicateKey(sessionId, url) {
121
+ return `${sessionId}:${url}`;
122
+ }
115
123
  return {
116
124
  register: async (sessionId, module) => {
117
125
  const page = this.getPage(sessionId);
@@ -124,16 +132,19 @@ class PlaywrightBrowserProvider {
124
132
  headers: getHeaders(this.project.browser.vite.config)
125
133
  });
126
134
  }
135
+ // webkit doesn't support redirect responses
136
+ // https://github.com/microsoft/playwright/issues/18318
127
137
  const isWebkit = this.browserName === "webkit";
128
138
  if (isWebkit) {
129
- const url = module.type === "redirect" ? (() => {
130
- const url = new URL(module.redirect);
131
- return url.href.slice(url.origin.length);
132
- })() : (() => {
133
- const url = new URL(route.request().url());
134
- url.searchParams.set("mock", module.type);
135
- return url.href.slice(url.origin.length);
136
- })();
139
+ let url;
140
+ if (module.type === "redirect") {
141
+ const redirect = new URL(module.redirect);
142
+ url = redirect.href.slice(redirect.origin.length);
143
+ } else {
144
+ const request = new URL(route.request().url());
145
+ request.searchParams.set("mock", module.type);
146
+ url = request.href.slice(request.origin.length);
147
+ }
137
148
  const result = await this.project.browser.vite.transformRequest(url).catch(() => null);
138
149
  if (!result) {
139
150
  return route.continue();
@@ -165,18 +176,20 @@ class PlaywrightBrowserProvider {
165
176
  },
166
177
  delete: async (sessionId, id) => {
167
178
  const page = this.getPage(sessionId);
168
- const predicate = idPreficates.get(id);
179
+ const key = predicateKey(sessionId, id);
180
+ const predicate = idPreficates.get(key);
169
181
  if (predicate) {
170
- await page.unroute(predicate).finally(() => idPreficates.delete(id));
182
+ await page.unroute(predicate).finally(() => idPreficates.delete(key));
171
183
  }
172
184
  },
173
185
  clear: async (sessionId) => {
174
186
  const page = this.getPage(sessionId);
175
187
  const ids = sessionIds.get(sessionId) || [];
176
188
  const promises = ids.map((id) => {
177
- const predicate = idPreficates.get(id);
189
+ const key = predicateKey(sessionId, id);
190
+ const predicate = idPreficates.get(key);
178
191
  if (predicate) {
179
- return page.unroute(predicate).finally(() => idPreficates.delete(id));
192
+ return page.unroute(predicate).finally(() => idPreficates.delete(key));
180
193
  }
181
194
  return null;
182
195
  });
@@ -359,6 +372,8 @@ class WebdriverBrowserProvider {
359
372
  project;
360
373
  options;
361
374
  closing = false;
375
+ iframeSwitched = false;
376
+ topLevelContext;
362
377
  getSupportedBrowsers() {
363
378
  return webdriverBrowsers;
364
379
  }
@@ -368,14 +383,19 @@ class WebdriverBrowserProvider {
368
383
  this.browserName = browser;
369
384
  this.options = options;
370
385
  }
386
+ isIframeSwitched() {
387
+ return this.iframeSwitched;
388
+ }
371
389
  async switchToTestFrame() {
372
390
  const page = this.browser;
391
+ // support wdio@9
373
392
  if (page.switchFrame) {
374
393
  await page.switchFrame(page.$("iframe[data-vitest]"));
375
394
  } else {
376
395
  const iframe = await page.findElement("css selector", "iframe[data-vitest]");
377
396
  await page.switchToFrame(iframe);
378
397
  }
398
+ this.iframeSwitched = true;
379
399
  }
380
400
  async switchToMainFrame() {
381
401
  const page = this.browser;
@@ -384,6 +404,20 @@ class WebdriverBrowserProvider {
384
404
  } else {
385
405
  await page.switchToParentFrame();
386
406
  }
407
+ this.iframeSwitched = false;
408
+ }
409
+ async setViewport(options) {
410
+ if (this.topLevelContext == null || !this.browser) {
411
+ throw new Error(`The browser has no open pages.`);
412
+ }
413
+ await this.browser.send({
414
+ method: "browsingContext.setViewport",
415
+ params: {
416
+ context: this.topLevelContext,
417
+ devicePixelRatio: 1,
418
+ viewport: options
419
+ }
420
+ });
387
421
  }
388
422
  getCommandsContext() {
389
423
  return { browser: this.browser };
@@ -407,6 +441,7 @@ class WebdriverBrowserProvider {
407
441
  capabilities: this.buildCapabilities()
408
442
  };
409
443
  debug?.("[%s] opening the browser with options: %O", this.browserName, remoteOptions);
444
+ // TODO: close everything, if browser is closed from the outside
410
445
  this.browser = await remote(remoteOptions);
411
446
  await this._throwIfClosing();
412
447
  return this.browser;
@@ -432,6 +467,7 @@ class WebdriverBrowserProvider {
432
467
  args: newArgs
433
468
  };
434
469
  }
470
+ // start Vitest UI maximized only on supported browsers
435
471
  if (options.ui && (browser === "chrome" || browser === "edge")) {
436
472
  const key = browser === "chrome" ? "goog:chromeOptions" : "ms:edgeOptions";
437
473
  const args = capabilities[key]?.args || [];
@@ -449,6 +485,7 @@ class WebdriverBrowserProvider {
449
485
  const browserInstance = await this.openBrowser();
450
486
  debug?.("[%s][%s] browser page is created, opening %s", sessionId, this.browserName, url);
451
487
  await browserInstance.url(url);
488
+ this.topLevelContext = await browserInstance.getWindowHandle();
452
489
  await this._throwIfClosing("opening the url");
453
490
  }
454
491
  async _throwIfClosing(action) {
@@ -462,6 +499,8 @@ class WebdriverBrowserProvider {
462
499
  debug?.("[%s] closing provider", this.browserName);
463
500
  this.closing = true;
464
501
  await Promise.all([this.browser?.sessionId ? this.browser?.deleteSession?.() : null]);
502
+ // TODO: right now process can only exit with timeout, if we use browser
503
+ // needs investigating
465
504
  process.exit();
466
505
  }
467
506
  }