@msw/playwright 0.1.0 → 0.3.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
@@ -24,7 +24,7 @@ This package aims to provide a better developer experience when mocking APIs in
24
24
  ## Usage
25
25
 
26
26
  ```sh
27
- npm i @msw/playwright
27
+ npm i msw @msw/playwright
28
28
  ```
29
29
 
30
30
  ```ts
package/build/index.d.ts CHANGED
@@ -1,16 +1,19 @@
1
- import { LifeCycleEventsMap, RequestHandler, SetupApi } from "msw";
1
+ import { LifeCycleEventsMap, RequestHandler, SetupApi, WebSocketHandler } from "msw";
2
2
  import { Page, TestFixture } from "@playwright/test";
3
3
 
4
4
  //#region src/index.d.ts
5
- declare function createWorkerFixture(initialHandlers?: Array<RequestHandler>): TestFixture<WorkerFixture, any>;
5
+ interface CreateWorkerFixtureArgs {
6
+ initialHandlers: Array<RequestHandler | WebSocketHandler>;
7
+ }
8
+ declare function createWorkerFixture(args?: CreateWorkerFixtureArgs): TestFixture<WorkerFixture, any>;
6
9
  declare class WorkerFixture extends SetupApi<LifeCycleEventsMap> {
7
10
  #private;
8
11
  constructor(args: {
9
12
  page: Page;
10
- initialHandlers: Array<RequestHandler>;
13
+ initialHandlers: Array<RequestHandler | WebSocketHandler>;
11
14
  });
12
15
  start(): Promise<void>;
13
16
  stop(): Promise<void>;
14
17
  }
15
18
  //#endregion
16
- export { WorkerFixture, createWorkerFixture };
19
+ export { CreateWorkerFixtureArgs, WorkerFixture, createWorkerFixture };
package/build/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { RequestHandler, SetupApi, getResponse } from "msw";
1
+ import { invariant } from "outvariant";
2
+ import { RequestHandler, SetupApi, WebSocketHandler, getResponse } from "msw";
3
+ import { CancelableCloseEvent, CancelableMessageEvent } from "@mswjs/interceptors/WebSocket";
2
4
 
3
5
  //#region src/index.ts
4
- function createWorkerFixture(initialHandlers = []) {
6
+ function createWorkerFixture(args) {
5
7
  return async ({ page }, use) => {
6
8
  const worker = new WorkerFixture({
7
9
  page,
8
- initialHandlers
10
+ initialHandlers: args?.initialHandlers || []
9
11
  });
10
12
  await worker.start();
11
13
  await use(worker);
@@ -38,12 +40,167 @@ var WorkerFixture = class extends SetupApi {
38
40
  }
39
41
  route.continue();
40
42
  });
43
+ await this.#page.routeWebSocket(/.+/, async (ws) => {
44
+ const allWebSocketHandlers = this.handlersController.currentHandlers().filter((handler) => {
45
+ return handler instanceof WebSocketHandler;
46
+ });
47
+ if (allWebSocketHandlers.length === 0) {
48
+ ws.connectToServer();
49
+ return;
50
+ }
51
+ const client = new PlaywrightWebSocketClientConnection(ws);
52
+ const server = new PlaywrightWebSocketServerConnection(ws);
53
+ for (const handler of allWebSocketHandlers) await handler.run({
54
+ client,
55
+ server,
56
+ info: { protocols: [] }
57
+ });
58
+ });
41
59
  }
42
60
  async stop() {
43
61
  super.dispose();
44
62
  await this.#page.unroute(/.+/);
45
63
  }
46
64
  };
65
+ var PlaywrightWebSocketClientConnection = class {
66
+ id;
67
+ url;
68
+ constructor(ws) {
69
+ this.ws = ws;
70
+ this.id = crypto.randomUUID();
71
+ this.url = new URL(ws.url());
72
+ }
73
+ send(data) {
74
+ if (data instanceof Blob) {
75
+ /**
76
+ * @note Playwright does not support sending Blob data.
77
+ * Read the blob as buffer, then send the buffer instead.
78
+ */
79
+ data.bytes().then((bytes) => {
80
+ this.ws.send(Buffer.from(bytes));
81
+ });
82
+ return;
83
+ }
84
+ if (typeof data === "string") {
85
+ this.ws.send(data);
86
+ return;
87
+ }
88
+ this.ws.send(
89
+ /**
90
+ * @note Forcefully cast all data to Buffer because Playwright
91
+ * has trouble digesting ArrayBuffer and Blob directly.
92
+ */
93
+ Buffer.from(
94
+ /**
95
+ * @note Playwright type definitions are tailored to Node.js
96
+ * while MSW describes all data types that can be sent over
97
+ * the WebSocket protocol, like ArrayBuffer and Blob.
98
+ */
99
+ data
100
+ )
101
+ );
102
+ }
103
+ close(code, reason) {
104
+ const resolvedCode = code ?? 1e3;
105
+ this.ws.close({
106
+ code: resolvedCode,
107
+ reason
108
+ });
109
+ }
110
+ addEventListener(type, listener, options) {
111
+ /**
112
+ * @note Playwright does not expose the actual WebSocket reference.
113
+ */
114
+ const target = {};
115
+ switch (type) {
116
+ case "message": {
117
+ this.ws.onMessage((data) => {
118
+ listener.call(target, new CancelableMessageEvent("message", { data }));
119
+ });
120
+ break;
121
+ }
122
+ case "close": {
123
+ this.ws.onClose((code, reason) => {
124
+ listener.call(target, new CancelableCloseEvent("close", {
125
+ code,
126
+ reason
127
+ }));
128
+ });
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ removeEventListener(event, listener, options) {
134
+ console.warn("@msw/playwright: WebSocketRoute does not support removing event listeners");
135
+ }
136
+ };
137
+ var PlaywrightWebSocketServerConnection = class {
138
+ #server;
139
+ #bufferedEvents;
140
+ #bufferedData;
141
+ constructor(ws) {
142
+ this.ws = ws;
143
+ this.#bufferedEvents = [];
144
+ this.#bufferedData = [];
145
+ }
146
+ connect() {
147
+ this.#server = this.ws.connectToServer();
148
+ /**
149
+ * @note Playwright does not support event buffering.
150
+ * Manually add event listeners that might have been registered
151
+ * before `connect()` was called.
152
+ */
153
+ for (const [type, listener, options] of this.#bufferedEvents) this.addEventListener(type, listener, options);
154
+ this.#bufferedEvents.length = 0;
155
+ for (const data of this.#bufferedData) this.send(data);
156
+ this.#bufferedData.length = 0;
157
+ }
158
+ send(data) {
159
+ if (this.#server == null) {
160
+ this.#bufferedData.push(data);
161
+ return;
162
+ }
163
+ this.#server.send(data);
164
+ }
165
+ close(code, reason) {
166
+ invariant(this.#server, "Failed to close connection to the actual WebSocket server: connection not established. Did you forget to call `connect()`?");
167
+ this.#server.close({
168
+ code,
169
+ reason
170
+ });
171
+ }
172
+ addEventListener(type, listener, options) {
173
+ if (this.#server == null) {
174
+ this.#bufferedEvents.push([
175
+ type,
176
+ listener,
177
+ options
178
+ ]);
179
+ return;
180
+ }
181
+ const target = {};
182
+ switch (type) {
183
+ case "message": {
184
+ this.#server.onMessage((data) => {
185
+ listener.call(target, new CancelableMessageEvent("message", { data }));
186
+ });
187
+ break;
188
+ }
189
+ case "close": {
190
+ this.#server.onClose((code, reason) => {
191
+ listener.call(target, new CancelableCloseEvent("close", {
192
+ code,
193
+ reason
194
+ }));
195
+ });
196
+ break;
197
+ }
198
+ }
199
+ }
200
+ removeEventListener(type, listener, options) {
201
+ console.warn("@msw/playwright: WebSocketRoute does not support removing event listeners");
202
+ }
203
+ };
47
204
 
48
205
  //#endregion
49
206
  export { WorkerFixture, createWorkerFixture };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@msw/playwright",
4
- "version": "0.1.0",
4
+ "version": "0.3.0",
5
5
  "description": "Mock Service Worker binding for Playwright",
6
6
  "main": "./build/index.js",
7
7
  "types": "./build/index.d.ts",
@@ -34,16 +34,21 @@
34
34
  "node": ">=20.0.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "msw": "^2.9.0"
37
+ "msw": "^2.10.1"
38
38
  },
39
39
  "devDependencies": {
40
+ "@epic-web/test-server": "^0.1.6",
40
41
  "@ossjs/release": "^0.8.1",
41
42
  "@playwright/test": "^1.52.0",
42
43
  "@types/node": "^22.15.29",
43
- "msw": "^2.9.0",
44
+ "msw": "^2.10.1",
44
45
  "tsdown": "^0.12.7",
45
46
  "typescript": "^5.8.3"
46
47
  },
48
+ "dependencies": {
49
+ "@mswjs/interceptors": "^0.39.2",
50
+ "outvariant": "^1.4.3"
51
+ },
47
52
  "scripts": {
48
53
  "dev": "tsdown --watch",
49
54
  "test": "playwright test",
package/src/index.ts CHANGED
@@ -1,19 +1,34 @@
1
- import type { Page, TestFixture } from '@playwright/test'
1
+ import { invariant } from 'outvariant'
2
+ import type { Page, TestFixture, WebSocketRoute } from '@playwright/test'
2
3
  import {
3
4
  type LifeCycleEventsMap,
4
5
  SetupApi,
5
6
  RequestHandler,
7
+ WebSocketHandler,
6
8
  getResponse,
7
9
  } from 'msw'
10
+ import {
11
+ type WebSocketClientEventMap,
12
+ type WebSocketData,
13
+ type WebSocketServerEventMap,
14
+ CancelableMessageEvent,
15
+ CancelableCloseEvent,
16
+ WebSocketClientConnectionProtocol,
17
+ WebSocketServerConnectionProtocol,
18
+ } from '@mswjs/interceptors/WebSocket'
19
+
20
+ export interface CreateWorkerFixtureArgs {
21
+ initialHandlers: Array<RequestHandler | WebSocketHandler>
22
+ }
8
23
 
9
24
  export function createWorkerFixture(
10
- initialHandlers: Array<RequestHandler> = [],
25
+ args?: CreateWorkerFixtureArgs,
11
26
  /** @todo `onUnhandledRequest`? */
12
27
  ): TestFixture<WorkerFixture, any> {
13
28
  return async ({ page }, use) => {
14
29
  const worker = new WorkerFixture({
15
30
  page,
16
- initialHandlers,
31
+ initialHandlers: args?.initialHandlers || [],
17
32
  })
18
33
 
19
34
  await worker.start()
@@ -25,12 +40,16 @@ export function createWorkerFixture(
25
40
  export class WorkerFixture extends SetupApi<LifeCycleEventsMap> {
26
41
  #page: Page
27
42
 
28
- constructor(args: { page: Page; initialHandlers: Array<RequestHandler> }) {
43
+ constructor(args: {
44
+ page: Page
45
+ initialHandlers: Array<RequestHandler | WebSocketHandler>
46
+ }) {
29
47
  super(...args.initialHandlers)
30
48
  this.#page = args.page
31
49
  }
32
50
 
33
51
  public async start() {
52
+ // Handle HTTP requests.
34
53
  await this.#page.route(/.+/, async (route, request) => {
35
54
  const fetchRequest = new Request(request.url(), {
36
55
  method: request.method(),
@@ -58,6 +77,31 @@ export class WorkerFixture extends SetupApi<LifeCycleEventsMap> {
58
77
 
59
78
  route.continue()
60
79
  })
80
+
81
+ // Handle WebSocket connections.
82
+ await this.#page.routeWebSocket(/.+/, async (ws) => {
83
+ const allWebSocketHandlers = this.handlersController
84
+ .currentHandlers()
85
+ .filter((handler) => {
86
+ return handler instanceof WebSocketHandler
87
+ })
88
+
89
+ if (allWebSocketHandlers.length === 0) {
90
+ ws.connectToServer()
91
+ return
92
+ }
93
+
94
+ const client = new PlaywrightWebSocketClientConnection(ws)
95
+ const server = new PlaywrightWebSocketServerConnection(ws)
96
+
97
+ for (const handler of allWebSocketHandlers) {
98
+ await handler.run({
99
+ client,
100
+ server,
101
+ info: { protocols: [] },
102
+ })
103
+ }
104
+ })
61
105
  }
62
106
 
63
107
  public async stop() {
@@ -65,3 +109,210 @@ export class WorkerFixture extends SetupApi<LifeCycleEventsMap> {
65
109
  await this.#page.unroute(/.+/)
66
110
  }
67
111
  }
112
+
113
+ class PlaywrightWebSocketClientConnection
114
+ implements WebSocketClientConnectionProtocol
115
+ {
116
+ public id: string
117
+ public url: URL
118
+
119
+ constructor(protected readonly ws: WebSocketRoute) {
120
+ this.id = crypto.randomUUID()
121
+ this.url = new URL(ws.url())
122
+ }
123
+
124
+ public send(data: WebSocketData): void {
125
+ if (data instanceof Blob) {
126
+ /**
127
+ * @note Playwright does not support sending Blob data.
128
+ * Read the blob as buffer, then send the buffer instead.
129
+ */
130
+ data.bytes().then((bytes) => {
131
+ this.ws.send(Buffer.from(bytes))
132
+ })
133
+ return
134
+ }
135
+
136
+ if (typeof data === 'string') {
137
+ this.ws.send(data)
138
+ return
139
+ }
140
+
141
+ this.ws.send(
142
+ /**
143
+ * @note Forcefully cast all data to Buffer because Playwright
144
+ * has trouble digesting ArrayBuffer and Blob directly.
145
+ */
146
+ Buffer.from(
147
+ /**
148
+ * @note Playwright type definitions are tailored to Node.js
149
+ * while MSW describes all data types that can be sent over
150
+ * the WebSocket protocol, like ArrayBuffer and Blob.
151
+ */
152
+ data as any,
153
+ ),
154
+ )
155
+ }
156
+
157
+ public close(code?: number, reason?: string): void {
158
+ const resolvedCode = code ?? 1000
159
+ this.ws.close({ code: resolvedCode, reason })
160
+ }
161
+
162
+ public addEventListener<EventType extends keyof WebSocketClientEventMap>(
163
+ type: EventType,
164
+ listener: (
165
+ this: WebSocket,
166
+ event: WebSocketClientEventMap[EventType],
167
+ ) => void,
168
+ options?: AddEventListenerOptions | boolean,
169
+ ): void {
170
+ /**
171
+ * @note Playwright does not expose the actual WebSocket reference.
172
+ */
173
+ const target = {} as WebSocket
174
+
175
+ switch (type) {
176
+ case 'message': {
177
+ this.ws.onMessage((data) => {
178
+ listener.call(
179
+ target,
180
+ new CancelableMessageEvent('message', {
181
+ data,
182
+ }) as any,
183
+ )
184
+ })
185
+ break
186
+ }
187
+
188
+ case 'close': {
189
+ this.ws.onClose((code, reason) => {
190
+ listener.call(
191
+ target,
192
+ new CancelableCloseEvent('close', {
193
+ code,
194
+ reason,
195
+ }) as any,
196
+ )
197
+ })
198
+ break
199
+ }
200
+ }
201
+ }
202
+
203
+ public removeEventListener<EventType extends keyof WebSocketClientEventMap>(
204
+ event: EventType,
205
+ listener: (
206
+ this: WebSocket,
207
+ event: WebSocketClientEventMap[EventType],
208
+ ) => void,
209
+ options?: EventListenerOptions | boolean,
210
+ ): void {
211
+ console.warn(
212
+ '@msw/playwright: WebSocketRoute does not support removing event listeners',
213
+ )
214
+ }
215
+ }
216
+
217
+ class PlaywrightWebSocketServerConnection
218
+ implements WebSocketServerConnectionProtocol
219
+ {
220
+ #server?: WebSocketRoute
221
+ #bufferedEvents: Array<
222
+ Parameters<WebSocketServerConnectionProtocol['addEventListener']>
223
+ >
224
+ #bufferedData: Array<WebSocketData>
225
+
226
+ constructor(protected readonly ws: WebSocketRoute) {
227
+ this.#bufferedEvents = []
228
+ this.#bufferedData = []
229
+ }
230
+
231
+ public connect(): void {
232
+ this.#server = this.ws.connectToServer()
233
+
234
+ /**
235
+ * @note Playwright does not support event buffering.
236
+ * Manually add event listeners that might have been registered
237
+ * before `connect()` was called.
238
+ */
239
+ for (const [type, listener, options] of this.#bufferedEvents) {
240
+ this.addEventListener(type, listener, options)
241
+ }
242
+ this.#bufferedEvents.length = 0
243
+
244
+ // Same for the buffered data.
245
+ for (const data of this.#bufferedData) {
246
+ this.send(data)
247
+ }
248
+ this.#bufferedData.length = 0
249
+ }
250
+
251
+ public send(data: WebSocketData): void {
252
+ if (this.#server == null) {
253
+ this.#bufferedData.push(data)
254
+ return
255
+ }
256
+
257
+ this.#server.send(data as any)
258
+ }
259
+
260
+ public close(code?: number, reason?: string): void {
261
+ invariant(
262
+ this.#server,
263
+ 'Failed to close connection to the actual WebSocket server: connection not established. Did you forget to call `connect()`?',
264
+ )
265
+
266
+ this.#server.close({ code, reason })
267
+ }
268
+
269
+ public addEventListener<EventType extends keyof WebSocketServerEventMap>(
270
+ type: EventType,
271
+ listener: (
272
+ this: WebSocket,
273
+ event: WebSocketServerEventMap[EventType],
274
+ ) => void,
275
+ options?: AddEventListenerOptions | boolean,
276
+ ): void {
277
+ if (this.#server == null) {
278
+ this.#bufferedEvents.push([type, listener as any, options])
279
+ return
280
+ }
281
+
282
+ const target = {} as WebSocket
283
+ switch (type) {
284
+ case 'message': {
285
+ this.#server.onMessage((data) => {
286
+ listener.call(
287
+ target,
288
+ new CancelableMessageEvent('message', { data }) as any,
289
+ )
290
+ })
291
+ break
292
+ }
293
+
294
+ case 'close': {
295
+ this.#server.onClose((code, reason) => {
296
+ listener.call(
297
+ target,
298
+ new CancelableCloseEvent('close', { code, reason }) as any,
299
+ )
300
+ })
301
+ break
302
+ }
303
+ }
304
+ }
305
+
306
+ public removeEventListener<EventType extends keyof WebSocketServerEventMap>(
307
+ type: EventType,
308
+ listener: (
309
+ this: WebSocket,
310
+ event: WebSocketServerEventMap[EventType],
311
+ ) => void,
312
+ options?: EventListenerOptions | boolean,
313
+ ): void {
314
+ console.warn(
315
+ '@msw/playwright: WebSocketRoute does not support removing event listeners',
316
+ )
317
+ }
318
+ }