@vitest/browser-playwright 5.0.3 → 5.0.5

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.d.ts CHANGED
@@ -2,7 +2,7 @@ import { CustomComparatorsRegistry } from '@vitest/browser';
2
2
  export { defineBrowserCommand } from '@vitest/browser';
3
3
  import { Page, Frame, FrameLocator, BrowserContext, CDPSession, Browser, LaunchOptions, ConnectOptions, BrowserContextOptions } from 'playwright';
4
4
  import { ScreenshotMatcherOptions, ScreenshotComparatorRegistry, Locator } from 'vitest/browser';
5
- import { BrowserProvider, BrowserModuleMocker, TestProject, CDPSession as CDPSession$1, BrowserProviderOption } from 'vitest/node';
5
+ import { BrowserProvider, BrowserModuleMocker, TestProject, CDPSession as CDPSession$1, BrowserProviderCloseOptions, BrowserProviderOption } from 'vitest/node';
6
6
 
7
7
  declare const playwrightBrowsers: readonly ["firefox", "webkit", "chromium"];
8
8
  type PlaywrightBrowser = (typeof playwrightBrowsers)[number];
@@ -57,13 +57,17 @@ declare class PlaywrightBrowserProvider implements BrowserProvider {
57
57
  mocker: BrowserModuleMocker;
58
58
  browserName: PlaywrightBrowser;
59
59
  private browserPromise;
60
+ private launchOptionsJson;
60
61
  private closing;
62
+ private armedContexts;
61
63
  tracingContexts: Set<string>;
62
64
  pendingTraces: Map<string, string>;
63
65
  initScripts: string[];
64
66
  constructor(project: TestProject, options: PlaywrightProviderOptions);
65
67
  private onSIGTERM;
66
68
  private openBrowser;
69
+ private armInterception;
70
+ private probeInterception;
67
71
  private createMocker;
68
72
  private createContext;
69
73
  private getContextOptions;
@@ -80,7 +84,8 @@ declare class PlaywrightBrowserProvider implements BrowserProvider {
80
84
  }): Promise<void>;
81
85
  private _throwIfClosing;
82
86
  getCDPSession(sessionid: string): Promise<CDPSession$1>;
83
- close(): Promise<void>;
87
+ close(options?: BrowserProviderCloseOptions): Promise<void>;
88
+ private closeContexts;
84
89
  }
85
90
  declare module "vitest/node" {
86
91
  interface BrowserCommandContext {
package/dist/index.js CHANGED
@@ -875,18 +875,11 @@ function prewarmBrowser(project, options) {
875
875
  if (warmBrowsers.has(project.config)) {
876
876
  return;
877
877
  }
878
- let pending = pendingWarmBrowsers.get(project.vitest);
879
- if (!pending) {
880
- const pendingBrowsers = new Set();
881
- pending = pendingBrowsers;
882
- pendingWarmBrowsers.set(project.vitest, pendingBrowsers);
883
- // Browsers whose projects never initialize a provider (they have no test
884
- // files to run) are cleaned up when Vitest closes.
885
- project.vitest.onClose(() => closeWarmBrowsers(pendingBrowsers));
886
- }
878
+ const pending = getPendingWarmBrowsers(project.vitest);
887
879
  const launchOptions = resolveLaunchOptions(project.config.browser, project.vitest.config.inspector, options, browserName);
888
880
  const entry = {
889
- launchOptionsJson: JSON.stringify(launchOptions),
881
+ config: project.config,
882
+ launchOptionsJson: getWarmBrowserKey(browserName, launchOptions),
890
883
  pending,
891
884
  promise: (async () => {
892
885
  debug?.("[%s] prewarming the browser", browserName);
@@ -905,6 +898,34 @@ function prewarmBrowser(project, options) {
905
898
  pending.add(entry);
906
899
  warmBrowsers.set(project.config, entry);
907
900
  }
901
+ function getWarmBrowserKey(browserName, launchOptions) {
902
+ return JSON.stringify([browserName, launchOptions]);
903
+ }
904
+ function getPendingWarmBrowsers(vitest) {
905
+ let pending = pendingWarmBrowsers.get(vitest);
906
+ if (!pending) {
907
+ const pendingBrowsers = new Set();
908
+ pending = pendingBrowsers;
909
+ pendingWarmBrowsers.set(vitest, pendingBrowsers);
910
+ // Browsers whose projects never initialize a provider (they have no test
911
+ // files to run) are cleaned up when Vitest closes.
912
+ vitest.onClose(() => closeWarmBrowsers(pendingBrowsers));
913
+ }
914
+ return pending;
915
+ }
916
+ // the pool closes an idle instance to start another project; the browser
917
+ // process stays warm so the next project adopts it instead of launching one
918
+ function keepWarmBrowser(project, browser, launchOptionsJson) {
919
+ const pending = getPendingWarmBrowsers(project.vitest);
920
+ const entry = {
921
+ config: project.config,
922
+ launchOptionsJson,
923
+ pending,
924
+ promise: browser
925
+ };
926
+ pending.add(entry);
927
+ warmBrowsers.set(project.config, entry);
928
+ }
908
929
  function takeWarmBrowser(config) {
909
930
  const warm = warmBrowsers.get(config);
910
931
  if (warm) {
@@ -913,6 +934,22 @@ function takeWarmBrowser(config) {
913
934
  }
914
935
  return warm;
915
936
  }
937
+ // fewer browsers are prewarmed than there are projects and the pool starts
938
+ // projects in test order, so a browser prepared for another project is
939
+ // adopted when it is the same browser launched with the same options
940
+ function takeAnyWarmBrowser(vitest, launchOptionsJson) {
941
+ const pending = pendingWarmBrowsers.get(vitest);
942
+ if (!pending) {
943
+ return;
944
+ }
945
+ for (const warm of pending) {
946
+ if (warm.launchOptionsJson === launchOptionsJson) {
947
+ pending.delete(warm);
948
+ warmBrowsers.delete(warm.config);
949
+ return warm;
950
+ }
951
+ }
952
+ }
916
953
  async function closeWarmBrowsers(pending) {
917
954
  const closing = Array.from(pending, (warm) => warm.promise.then((browser) => browser.close()).catch(() => {}));
918
955
  pending.clear();
@@ -955,7 +992,10 @@ class PlaywrightBrowserProvider {
955
992
  mocker;
956
993
  browserName;
957
994
  browserPromise = null;
995
+ // set for a locally launched browser, which can be kept warm for another project
996
+ launchOptionsJson = null;
958
997
  closing = false;
998
+ armedContexts = new WeakMap();
959
999
  tracingContexts = new Set();
960
1000
  pendingTraces = new Map();
961
1001
  initScripts = [resolve(distRoot, "locators.js")];
@@ -1037,26 +1077,66 @@ class PlaywrightBrowserProvider {
1037
1077
  });
1038
1078
  this.browser = this.persistentContext.browser();
1039
1079
  } else {
1040
- const warm = takeWarmBrowser(this.project.config);
1041
- if (warm && warm.launchOptionsJson === JSON.stringify(launchOptions)) {
1080
+ const launchOptionsJson = getWarmBrowserKey(this.browserName, launchOptions);
1081
+ let warm = takeWarmBrowser(this.project.config);
1082
+ if (warm && warm.launchOptionsJson !== launchOptionsJson) {
1083
+ debug?.("[%s] discarding the prewarmed browser, launch options changed", this.browserName);
1084
+ void warm.promise.then((browser) => browser.close()).catch(() => {});
1085
+ warm = undefined;
1086
+ }
1087
+ warm ??= takeAnyWarmBrowser(this.project.vitest, launchOptionsJson);
1088
+ if (warm) {
1042
1089
  const browser = await warm.promise.catch(() => null);
1043
1090
  if (browser?.isConnected()) {
1044
1091
  debug?.("[%s] adopting the prewarmed browser", this.browserName);
1045
1092
  this.browser = browser;
1046
1093
  this.browserPromise = null;
1094
+ this.launchOptionsJson = launchOptionsJson;
1047
1095
  return this.browser;
1048
1096
  }
1049
- } else if (warm) {
1050
- debug?.("[%s] discarding the prewarmed browser, launch options changed", this.browserName);
1051
- void warm.promise.then((browser) => browser.close()).catch(() => {});
1052
1097
  }
1053
1098
  this.browser = await playwright[this.browserName].launch(launchOptions);
1099
+ this.launchOptionsJson = launchOptionsJson;
1054
1100
  }
1055
1101
  this.browserPromise = null;
1056
1102
  return this.browser;
1057
1103
  })();
1058
1104
  return this.browserPromise;
1059
1105
  }
1106
+ armInterception(page) {
1107
+ if (this.browserName !== "chromium") {
1108
+ return Promise.resolve();
1109
+ }
1110
+ const context = page.context();
1111
+ const armed = this.armedContexts.get(context) ?? this.probeInterception(page).catch((error) => {
1112
+ this.armedContexts.delete(context);
1113
+ throw error;
1114
+ });
1115
+ this.armedContexts.set(context, armed);
1116
+ return armed;
1117
+ }
1118
+ async probeInterception(page) {
1119
+ const probeUrl = "/__vitest_interception_probe__";
1120
+ const probePattern = `**${probeUrl}`;
1121
+ const context = page.context();
1122
+ await context.route(probePattern, (route) => route.fulfill({
1123
+ status: 204,
1124
+ headers: { "x-vitest-probe": "1" }
1125
+ }));
1126
+ const deadline = Date.now() + 5e3;
1127
+ while (Date.now() < deadline) {
1128
+ const intercepted = await page.evaluate((url) => fetch(url, {
1129
+ cache: "no-store",
1130
+ signal: AbortSignal.timeout(1e3)
1131
+ }).then((r) => r.headers.has("x-vitest-probe"), () => false), probeUrl).catch(() => false);
1132
+ if (intercepted) {
1133
+ return;
1134
+ }
1135
+ await new Promise((resolve) => setTimeout(resolve, 10));
1136
+ }
1137
+ await context.unroute(probePattern);
1138
+ throw new Error(`Cannot verify that ${this.browserName} request interception is active after 5s; module mocks would not apply reliably (#8339)`);
1139
+ }
1060
1140
  createMocker() {
1061
1141
  const idPredicates = new Map();
1062
1142
  const sessionIds = new Map();
@@ -1090,12 +1170,14 @@ class PlaywrightBrowserProvider {
1090
1170
  predicate
1091
1171
  };
1092
1172
  }
1173
+ // the tester unmocks by path, but the route is registered by the full url
1093
1174
  function predicateKey(sessionId, url) {
1094
- return `${sessionId}:${url}`;
1175
+ return `${sessionId}:${new URL(url, "http://localhost").href}`;
1095
1176
  }
1096
1177
  return {
1097
1178
  register: async (sessionId, module) => {
1098
1179
  const page = this.getPage(sessionId);
1180
+ await this.armInterception(page);
1099
1181
  const { url: moduleUrl, predicate } = createPredicate(module.url);
1100
1182
  const key = predicateKey(sessionId, moduleUrl);
1101
1183
  const existingPredicate = idPredicates.get(key);
@@ -1128,7 +1210,7 @@ class PlaywrightBrowserProvider {
1128
1210
  request.searchParams.set("mock", module.type);
1129
1211
  url = request.href.slice(request.origin.length);
1130
1212
  }
1131
- const result = await this.project.browser.vite.transformRequest(url).catch(() => null);
1213
+ const result = await this.project.browser.vite.transformRequest(decodeURI(url)).catch(() => null);
1132
1214
  if (!result) {
1133
1215
  return route.continue();
1134
1216
  }
@@ -1290,10 +1372,6 @@ class PlaywrightBrowserProvider {
1290
1372
  if (this.closing) {
1291
1373
  debug?.("[%s] provider was closed, cannot perform the action on %s", this.browserName, String(disposable));
1292
1374
  await disposable?.close();
1293
- this.pages.clear();
1294
- this.contexts.clear();
1295
- this.browser = null;
1296
- this.browserPromise = null;
1297
1375
  throw new Error(`[vitest] The provider was closed.`);
1298
1376
  }
1299
1377
  }
@@ -1309,7 +1387,7 @@ class PlaywrightBrowserProvider {
1309
1387
  detach: cdp.detach.bind(cdp)
1310
1388
  };
1311
1389
  }
1312
- async close() {
1390
+ async close(options) {
1313
1391
  process.off("SIGTERM", this.onSIGTERM);
1314
1392
  debug?.("[%s] closing provider", this.browserName);
1315
1393
  this.closing = true;
@@ -1324,6 +1402,24 @@ class PlaywrightBrowserProvider {
1324
1402
  }
1325
1403
  const browser = this.browser;
1326
1404
  this.browser = null;
1405
+ const closed = this.closeContexts();
1406
+ if (options?.keepWarm && browser && this.launchOptionsJson && browser.isConnected()) {
1407
+ // registered before the contexts are closed, so the project that takes
1408
+ // over the pool slot finds the browser right away and waits for it
1409
+ const kept = closed.then(() => browser, async (error) => {
1410
+ await browser.close().catch(() => {});
1411
+ throw error;
1412
+ });
1413
+ keepWarmBrowser(this.project, kept, this.launchOptionsJson);
1414
+ await kept;
1415
+ debug?.("[%s] provider is closed, the browser is kept warm", this.browserName);
1416
+ return;
1417
+ }
1418
+ await closed;
1419
+ await browser?.close();
1420
+ debug?.("[%s] provider is closed", this.browserName);
1421
+ }
1422
+ async closeContexts() {
1327
1423
  await Promise.all(Array.from(this.pages.values(), (p) => p.close()));
1328
1424
  this.pages.clear();
1329
1425
  if (this.persistentContext) {
@@ -1332,8 +1428,6 @@ class PlaywrightBrowserProvider {
1332
1428
  await Promise.all(Array.from(this.contexts.values(), (c) => c.close()));
1333
1429
  }
1334
1430
  this.contexts.clear();
1335
- await browser?.close();
1336
- debug?.("[%s] provider is closed", this.browserName);
1337
1431
  }
1338
1432
  }
1339
1433
  function getHeaders(config) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/browser-playwright",
3
3
  "type": "module",
4
- "version": "5.0.3",
4
+ "version": "5.0.5",
5
5
  "description": "Browser running for Vitest using playwright",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -43,7 +43,7 @@
43
43
  ],
44
44
  "peerDependencies": {
45
45
  "playwright": "*",
46
- "vitest": "5.0.3"
46
+ "vitest": "5.0.5"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "playwright": {
@@ -51,13 +51,15 @@
51
51
  }
52
52
  },
53
53
  "dependencies": {
54
- "@vitest/browser": "5.0.3",
55
- "@vitest/mocker": "5.0.3",
56
- "tinyrainbow": "^3.1.1"
54
+ "@vitest/browser": "5.0.5",
55
+ "@vitest/mocker": "5.0.5",
56
+ "tinyrainbow": "^3.2.0"
57
57
  },
58
58
  "devDependencies": {
59
- "playwright": "^1.62.1",
60
- "vitest": "5.0.3"
59
+ "pathe": "^2.0.3",
60
+ "playwright": "^1.64.0",
61
+ "vite": "8.0.11",
62
+ "vitest": "5.0.5"
61
63
  },
62
64
  "scripts": {
63
65
  "build": "premove dist && pnpm rollup -c",