@msw/playwright 0.4.5 → 0.5.0

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/README.md CHANGED
@@ -66,6 +66,10 @@ test('displays the user dashboard', async ({ network, page }) => {
66
66
  })
67
67
  ```
68
68
 
69
+ ## Limitations
70
+
71
+ - Since `context.routeWebSocket()` provides no means of knowing which page triggered a WebSocket connection, relative WebSocket URLs in `ws.link(url)` will be resolved against the _latest_ created page in the browser context.
72
+
69
73
  ## Comparison
70
74
 
71
75
  ### `playwright-msw`
package/build/index.d.ts CHANGED
@@ -1,10 +1,18 @@
1
1
  import { LifeCycleEventsMap, RequestHandler, SetupApi, UnhandledRequestStrategy, WebSocketHandler } from "msw";
2
- import { Page, PlaywrightTestArgs, PlaywrightWorkerArgs, TestFixture } from "@playwright/test";
2
+ import { BrowserContext, PlaywrightTestArgs, PlaywrightWorkerArgs, TestFixture } from "@playwright/test";
3
3
 
4
4
  //#region src/fixture.d.ts
5
5
  interface CreateNetworkFixtureArgs {
6
6
  initialHandlers?: Array<RequestHandler | WebSocketHandler>;
7
7
  onUnhandledRequest?: UnhandledRequestStrategy;
8
+ /**
9
+ * Skip common asset requests (e.g. `*.html`, `*.css`, `*.js`, etc).
10
+ * This improves performance for certian projects.
11
+ * @default true
12
+ *
13
+ * @see https://mswjs.io/docs/api/is-common-asset-request
14
+ */
15
+ skipAssetRequests?: boolean;
8
16
  }
9
17
  /**
10
18
  * Creates a fixture that controls the network in your tests.
@@ -37,12 +45,14 @@ declare function createNetworkFixture(args?: CreateNetworkFixtureArgs): [TestFix
37
45
 
38
46
  declare class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
39
47
  protected args: {
40
- page: Page;
48
+ context: BrowserContext;
49
+ skipAssetRequests: boolean;
41
50
  initialHandlers: Array<RequestHandler | WebSocketHandler>;
42
51
  onUnhandledRequest?: UnhandledRequestStrategy;
43
52
  };
44
53
  constructor(args: {
45
- page: Page;
54
+ context: BrowserContext;
55
+ skipAssetRequests: boolean;
46
56
  initialHandlers: Array<RequestHandler | WebSocketHandler>;
47
57
  onUnhandledRequest?: UnhandledRequestStrategy;
48
58
  });
package/build/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { invariant } from "outvariant";
2
- import { RequestHandler, SetupApi, WebSocketHandler, handleRequest } from "msw";
2
+ import { RequestHandler, SetupApi, WebSocketHandler, handleRequest, isCommonAssetRequest } from "msw";
3
3
  import { CancelableCloseEvent, CancelableMessageEvent } from "@mswjs/interceptors/WebSocket";
4
4
 
5
5
  //#region src/fixture.ts
@@ -23,9 +23,10 @@ import { CancelableCloseEvent, CancelableMessageEvent } from "@mswjs/interceptor
23
23
  * ```
24
24
  */
25
25
  function createNetworkFixture(args) {
26
- return [async ({ page }, use) => {
26
+ return [async ({ context }, use) => {
27
27
  const worker = new NetworkFixture({
28
- page,
28
+ context,
29
+ skipAssetRequests: args?.skipAssetRequests ?? true,
29
30
  initialHandlers: args?.initialHandlers || [],
30
31
  onUnhandledRequest: args?.onUnhandledRequest
31
32
  });
@@ -47,62 +48,70 @@ var NetworkFixture = class extends SetupApi {
47
48
  this.args = args;
48
49
  }
49
50
  async start() {
50
- await this.args.page.route(INTERNAL_MATCH_ALL_REG_EXP, async (route, request) => {
51
+ await this.args.context.route(INTERNAL_MATCH_ALL_REG_EXP, async (route, request) => {
51
52
  const fetchRequest = new Request(request.url(), {
52
53
  method: request.method(),
53
54
  headers: new Headers(await request.allHeaders()),
54
55
  body: request.postDataBuffer()
55
56
  });
57
+ /**
58
+ * @note Skip common asset requests (default).
59
+ * Playwright seems to experience performance degradation when routing all
60
+ * requests through the matching logic below.
61
+ * @see https://github.com/mswjs/playwright/issues/13
62
+ */
63
+ if (this.args.skipAssetRequests && isCommonAssetRequest(fetchRequest)) return route.continue();
56
64
  const handlers = this.handlersController.currentHandlers().filter((handler) => {
57
65
  return handler instanceof RequestHandler;
58
66
  });
67
+ const baseUrl = request.headers().referer ? new URL(request.headers().referer).origin : void 0;
59
68
  /**
60
69
  * @note Use `handleRequest` instead of `getResponse` so we can pass
61
70
  * the `onUnhandledRequest` option as-is and benefit from MSW's default behaviors.
62
71
  */
63
72
  const response = await handleRequest(fetchRequest, crypto.randomUUID(), handlers, { onUnhandledRequest: this.args.onUnhandledRequest || "bypass" }, this.emitter, { resolutionContext: {
64
73
  quiet: true,
65
- baseUrl: this.getPageUrl()
74
+ baseUrl
66
75
  } });
67
76
  if (response) {
68
- if (response.status === 0) {
69
- route.abort();
70
- return;
71
- }
72
- route.fulfill({
77
+ if (response.status === 0) return route.abort();
78
+ return route.fulfill({
73
79
  status: response.status,
74
80
  headers: Object.fromEntries(response.headers),
75
81
  body: response.body ? Buffer.from(await response.arrayBuffer()) : void 0
76
82
  });
77
- return;
78
83
  }
79
- route.continue();
84
+ return route.continue();
80
85
  });
81
- await this.args.page.routeWebSocket(INTERNAL_MATCH_ALL_REG_EXP, async (ws) => {
86
+ await this.args.context.routeWebSocket(INTERNAL_MATCH_ALL_REG_EXP, async (route) => {
82
87
  const allWebSocketHandlers = this.handlersController.currentHandlers().filter((handler) => {
83
88
  return handler instanceof WebSocketHandler;
84
89
  });
85
90
  if (allWebSocketHandlers.length === 0) {
86
- ws.connectToServer();
91
+ route.connectToServer();
87
92
  return;
88
93
  }
89
- const client = new PlaywrightWebSocketClientConnection(ws);
90
- const server = new PlaywrightWebSocketServerConnection(ws);
94
+ const client = new PlaywrightWebSocketClientConnection(route);
95
+ const server = new PlaywrightWebSocketServerConnection(route);
96
+ const pages = this.args.context.pages();
97
+ const lastPage = pages[pages.length - 1];
98
+ const baseUrl = lastPage ? this.getPageUrl(lastPage) : void 0;
91
99
  for (const handler of allWebSocketHandlers) await handler.run({
92
100
  client,
93
101
  server,
94
102
  info: { protocols: [] }
95
- }, { baseUrl: this.getPageUrl() });
103
+ }, { baseUrl });
96
104
  });
97
105
  }
98
106
  async stop() {
99
107
  super.dispose();
100
- await this.args.page.unroute(INTERNAL_MATCH_ALL_REG_EXP);
101
- await unrouteWebSocket(this.args.page, INTERNAL_MATCH_ALL_REG_EXP);
108
+ await this.args.context.unroute(INTERNAL_MATCH_ALL_REG_EXP);
109
+ await unrouteWebSocket(this.args.context, INTERNAL_MATCH_ALL_REG_EXP);
102
110
  }
103
- getPageUrl() {
104
- const url = this.args.page.url();
105
- return url !== "about:blank" ? url : void 0;
111
+ getPageUrl(page) {
112
+ const url = page.url();
113
+ if (url === "about:blank") return;
114
+ return decodeURI(new URL(encodeURI(url)).origin);
106
115
  }
107
116
  };
108
117
  var PlaywrightWebSocketClientConnection = class {
@@ -248,11 +257,11 @@ var PlaywrightWebSocketServerConnection = class {
248
257
  * Custom implementation of the missing `page.unrouteWebSocket()` to remove
249
258
  * WebSocket route handlers from the page. Loosely inspired by `page.unroute()`.
250
259
  */
251
- async function unrouteWebSocket(page, url, handler) {
252
- if (!("_webSocketRoutes" in page && Array.isArray(page._webSocketRoutes))) return;
253
- for (let i = page._webSocketRoutes.length - 1; i >= 0; i--) {
254
- const route = page._webSocketRoutes[i];
255
- if (route.url === url && (handler != null ? route.handler === handler : true)) page._webSocketRoutes.splice(i, 1);
260
+ async function unrouteWebSocket(target, url, handler) {
261
+ if (!("_webSocketRoutes" in target && Array.isArray(target._webSocketRoutes))) return;
262
+ for (let i = target._webSocketRoutes.length - 1; i >= 0; i--) {
263
+ const route = target._webSocketRoutes[i];
264
+ if (route.url === url && (handler != null ? route.handler === handler : true)) target._webSocketRoutes.splice(i, 1);
256
265
  }
257
266
  }
258
267
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@msw/playwright",
4
- "version": "0.4.5",
4
+ "version": "0.5.0",
5
5
  "description": "Mock Service Worker binding for Playwright",
6
6
  "main": "./build/index.js",
7
7
  "types": "./build/index.d.ts",
@@ -33,22 +33,22 @@
33
33
  "node": ">=20.0.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "msw": "^2.10.3"
36
+ "msw": "^2.12.9"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@epic-web/test-server": "^0.1.6",
40
40
  "@ossjs/release": "^0.10.1",
41
- "@playwright/test": "^1.57.0",
41
+ "@playwright/test": "^1.58.1",
42
42
  "@types/node": "^22.15.29",
43
43
  "@types/sinon": "^21.0.0",
44
- "msw": "^2.12.7",
44
+ "msw": "^2.12.9",
45
45
  "sinon": "^21.0.1",
46
46
  "tsdown": "^0.12.7",
47
47
  "typescript": "^5.9.3",
48
48
  "vite": "^7.3.1"
49
49
  },
50
50
  "dependencies": {
51
- "@mswjs/interceptors": "^0.40.0",
51
+ "@mswjs/interceptors": "^0.41.2",
52
52
  "outvariant": "^1.4.3"
53
53
  },
54
54
  "scripts": {
package/src/fixture.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import type {
3
+ BrowserContext,
3
4
  Page,
4
5
  PlaywrightTestArgs,
5
6
  PlaywrightWorkerArgs,
6
- Request,
7
+ Request as PlaywrightRequest,
7
8
  Route,
8
9
  TestFixture,
9
10
  WebSocketRoute,
@@ -15,6 +16,7 @@ import {
15
16
  RequestHandler,
16
17
  WebSocketHandler,
17
18
  handleRequest,
19
+ isCommonAssetRequest,
18
20
  } from 'msw'
19
21
  import {
20
22
  type WebSocketClientEventMap,
@@ -29,6 +31,14 @@ import {
29
31
  export interface CreateNetworkFixtureArgs {
30
32
  initialHandlers?: Array<RequestHandler | WebSocketHandler>
31
33
  onUnhandledRequest?: UnhandledRequestStrategy
34
+ /**
35
+ * Skip common asset requests (e.g. `*.html`, `*.css`, `*.js`, etc).
36
+ * This improves performance for certian projects.
37
+ * @default true
38
+ *
39
+ * @see https://mswjs.io/docs/api/is-common-asset-request
40
+ */
41
+ skipAssetRequests?: boolean
32
42
  }
33
43
 
34
44
  /**
@@ -57,9 +67,10 @@ export function createNetworkFixture(
57
67
  { auto: boolean },
58
68
  ] {
59
69
  return [
60
- async ({ page }, use) => {
70
+ async ({ context }, use) => {
61
71
  const worker = new NetworkFixture({
62
- page,
72
+ context,
73
+ skipAssetRequests: args?.skipAssetRequests ?? true,
63
74
  initialHandlers: args?.initialHandlers || [],
64
75
  onUnhandledRequest: args?.onUnhandledRequest,
65
76
  })
@@ -83,7 +94,8 @@ export const INTERNAL_MATCH_ALL_REG_EXP = /.+(__MSW_PLAYWRIGHT_PREDICATE__)?/
83
94
  export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
84
95
  constructor(
85
96
  protected args: {
86
- page: Page
97
+ context: BrowserContext
98
+ skipAssetRequests: boolean
87
99
  initialHandlers: Array<RequestHandler | WebSocketHandler>
88
100
  onUnhandledRequest?: UnhandledRequestStrategy
89
101
  },
@@ -93,21 +105,35 @@ export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
93
105
 
94
106
  public async start(): Promise<void> {
95
107
  // Handle HTTP requests.
96
- await this.args.page.route(
108
+ await this.args.context.route(
97
109
  INTERNAL_MATCH_ALL_REG_EXP,
98
- async (route: Route, request: Request) => {
110
+ async (route: Route, request: PlaywrightRequest) => {
99
111
  const fetchRequest = new Request(request.url(), {
100
112
  method: request.method(),
101
113
  headers: new Headers(await request.allHeaders()),
102
- body: request.postDataBuffer(),
114
+ body: request.postDataBuffer() as ArrayBuffer | null,
103
115
  })
104
116
 
117
+ /**
118
+ * @note Skip common asset requests (default).
119
+ * Playwright seems to experience performance degradation when routing all
120
+ * requests through the matching logic below.
121
+ * @see https://github.com/mswjs/playwright/issues/13
122
+ */
123
+ if (this.args.skipAssetRequests && isCommonAssetRequest(fetchRequest)) {
124
+ return route.continue()
125
+ }
126
+
105
127
  const handlers = this.handlersController
106
128
  .currentHandlers()
107
129
  .filter((handler) => {
108
130
  return handler instanceof RequestHandler
109
131
  })
110
132
 
133
+ const baseUrl = request.headers().referer
134
+ ? new URL(request.headers().referer).origin
135
+ : undefined
136
+
111
137
  /**
112
138
  * @note Use `handleRequest` instead of `getResponse` so we can pass
113
139
  * the `onUnhandledRequest` option as-is and benefit from MSW's default behaviors.
@@ -123,35 +149,33 @@ export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
123
149
  {
124
150
  resolutionContext: {
125
151
  quiet: true,
126
- baseUrl: this.getPageUrl(),
152
+ baseUrl,
127
153
  },
128
154
  },
129
155
  )
130
156
 
131
157
  if (response) {
132
158
  if (response.status === 0) {
133
- route.abort()
134
- return
159
+ return route.abort()
135
160
  }
136
161
 
137
- route.fulfill({
162
+ return route.fulfill({
138
163
  status: response.status,
139
164
  headers: Object.fromEntries(response.headers),
140
165
  body: response.body
141
166
  ? Buffer.from(await response.arrayBuffer())
142
167
  : undefined,
143
168
  })
144
- return
145
169
  }
146
170
 
147
- route.continue()
171
+ return route.continue()
148
172
  },
149
173
  )
150
174
 
151
175
  // Handle WebSocket connections.
152
- await this.args.page.routeWebSocket(
176
+ await this.args.context.routeWebSocket(
153
177
  INTERNAL_MATCH_ALL_REG_EXP,
154
- async (ws) => {
178
+ async (route) => {
155
179
  const allWebSocketHandlers = this.handlersController
156
180
  .currentHandlers()
157
181
  .filter((handler) => {
@@ -159,12 +183,16 @@ export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
159
183
  })
160
184
 
161
185
  if (allWebSocketHandlers.length === 0) {
162
- ws.connectToServer()
186
+ route.connectToServer()
163
187
  return
164
188
  }
165
189
 
166
- const client = new PlaywrightWebSocketClientConnection(ws)
167
- const server = new PlaywrightWebSocketServerConnection(ws)
190
+ const client = new PlaywrightWebSocketClientConnection(route)
191
+ const server = new PlaywrightWebSocketServerConnection(route)
192
+
193
+ const pages = this.args.context.pages()
194
+ const lastPage = pages[pages.length - 1]
195
+ const baseUrl = lastPage ? this.getPageUrl(lastPage) : undefined
168
196
 
169
197
  for (const handler of allWebSocketHandlers) {
170
198
  await handler.run(
@@ -174,7 +202,7 @@ export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
174
202
  info: { protocols: [] },
175
203
  },
176
204
  {
177
- baseUrl: this.getPageUrl(),
205
+ baseUrl,
178
206
  },
179
207
  )
180
208
  }
@@ -184,19 +212,23 @@ export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
184
212
 
185
213
  public async stop(): Promise<void> {
186
214
  super.dispose()
187
- await this.args.page.unroute(INTERNAL_MATCH_ALL_REG_EXP)
188
- await unrouteWebSocket(this.args.page, INTERNAL_MATCH_ALL_REG_EXP)
215
+ await this.args.context.unroute(INTERNAL_MATCH_ALL_REG_EXP)
216
+ await unrouteWebSocket(this.args.context, INTERNAL_MATCH_ALL_REG_EXP)
189
217
  }
190
218
 
191
- private getPageUrl(): string | undefined {
192
- const url = this.args.page.url()
193
- return url !== 'about:blank' ? url : undefined
219
+ private getPageUrl(page: Page): string | undefined {
220
+ const url = page.url()
221
+
222
+ if (url === 'about:blank') {
223
+ return
224
+ }
225
+
226
+ // Encode/decode to preserve escape characters.
227
+ return decodeURI(new URL(encodeURI(url)).origin)
194
228
  }
195
229
  }
196
230
 
197
- class PlaywrightWebSocketClientConnection
198
- implements WebSocketClientConnectionProtocol
199
- {
231
+ class PlaywrightWebSocketClientConnection implements WebSocketClientConnectionProtocol {
200
232
  public id: string
201
233
  public url: URL
202
234
 
@@ -298,9 +330,7 @@ class PlaywrightWebSocketClientConnection
298
330
  }
299
331
  }
300
332
 
301
- class PlaywrightWebSocketServerConnection
302
- implements WebSocketServerConnectionProtocol
303
- {
333
+ class PlaywrightWebSocketServerConnection implements WebSocketServerConnectionProtocol {
304
334
  #server?: WebSocketRoute
305
335
  #bufferedEvents: Array<
306
336
  Parameters<WebSocketServerConnectionProtocol['addEventListener']>
@@ -411,22 +441,24 @@ interface InternalWebSocketRoute {
411
441
  * WebSocket route handlers from the page. Loosely inspired by `page.unroute()`.
412
442
  */
413
443
  async function unrouteWebSocket(
414
- page: Page,
444
+ target: BrowserContext,
415
445
  url: InternalWebSocketRoute['url'],
416
446
  handler?: InternalWebSocketRoute['handler'],
417
447
  ): Promise<void> {
418
- if (!('_webSocketRoutes' in page && Array.isArray(page._webSocketRoutes))) {
448
+ if (
449
+ !('_webSocketRoutes' in target && Array.isArray(target._webSocketRoutes))
450
+ ) {
419
451
  return
420
452
  }
421
453
 
422
- for (let i = page._webSocketRoutes.length - 1; i >= 0; i--) {
423
- const route = page._webSocketRoutes[i] as InternalWebSocketRoute
454
+ for (let i = target._webSocketRoutes.length - 1; i >= 0; i--) {
455
+ const route = target._webSocketRoutes[i] as InternalWebSocketRoute
424
456
 
425
457
  if (
426
458
  route.url === url &&
427
459
  (handler != null ? route.handler === handler : true)
428
460
  ) {
429
- page._webSocketRoutes.splice(i, 1)
461
+ target._webSocketRoutes.splice(i, 1)
430
462
  }
431
463
  }
432
464
  }