@mswjs/interceptors 0.26.9 → 0.26.11

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
@@ -292,8 +292,8 @@ interceptor.on(
292
292
 
293
293
  You can intercept a WebSocket communication using the `WebSocketInterceptor` class.
294
294
 
295
- > [!WARNING]
296
- > In practice, WebSocket connections can use different mechanisms to work (called "transports"). At this moment, the WebSocket interceptor only supports connections established using the `globalThis.WebSocket` class. Supporting third-party transports is challenging because they are non-standard and specific to individual WebSocket client libraries.
295
+ > [!IMPORTANT]
296
+ > This library only supports intercepting WebSocket connections created using the global WHATWG `WebSocket` class. Third-party transports, such as HTTP/XHR polling, are not supported by design due to their contrived nature.
297
297
 
298
298
  ```js
299
299
  import { WebSocketInterceptor } from '@mswjs/interceptors/WebSocket'
@@ -303,6 +303,13 @@ const interceptor = new WebSocketInterceptor()
303
303
 
304
304
  Unlike the HTTP-based interceptors that share the same `request`/`response` events, the WebSocket interceptor only emits the `connection` event and let's you handle the incoming/outgoing events in its listener.
305
305
 
306
+ ### Important defaults
307
+
308
+ 1. Intercepted WebSocket connections are _not_ opened. To open the actual WebSocket connection, call [`server.connect()`](#connect) in the interceptor.
309
+ 1. Once connected to the actual server, the outgoing client events are _not_ forwarded to that server. You must add a message listener for the `client` and forward the events manually using [`server.send()`](#senddata-1).
310
+ 1. Once connected to the actual server, the incoming server events _are_ forwarded to the client by default. You can prevent that forwarding by calling `event.preventDefault()` in the message event listener for the `server`.
311
+ 1. Once connected to the actual server, the `close` event received from that server is forwarded to the intercepted client by default. You can prevent that forwarding by calling `event.preventDefault()` in the close event listener for the `server`.
312
+
306
313
  ### WebSocket connection
307
314
 
308
315
  Whenever a WebSocket instance is constructed, the `connection` event is emitted on the WebSocket interceptor.
@@ -315,121 +322,114 @@ intereceptor.on('connection', ({ client }) => {
315
322
 
316
323
  The `connection` event exposes the following arguments:
317
324
 
318
- | Name | Type | Description |
319
- | -------- | -------- | ---------------------------------------------------------------- |
320
- | `client` | `object` | An object representing a connected WebSocket client instance. |
321
- | `server` | `object` | An object representing the original WebSocket server connection. |
325
+ | Name | Type | Description |
326
+ | -------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
327
+ | `client` | [`WebSocketClientConnection`](#websocketclientconnection) | An object representing a connected WebSocket client instance. |
328
+ | `server` | [`WebSocketServerConnection`](#websocketserverconnection) | An object representing the original WebSocket server connection. |
322
329
 
323
- ### Intercepting outgoing client events
330
+ ### `WebSocketClientConnection`
324
331
 
325
- To intercept an event sent by the WebSocket client, add a `message` listener to the `client` object.
332
+ #### `.addEventListener(type, listener)`
326
333
 
327
- ```js
328
- interceptor.on('connection', ({ client }) => {
329
- // Intercept all outgoing events from the client.
330
- client.addEventListener('message', (event) => {
331
- console.log('received:', event.data)
332
- })
333
- })
334
- ```
334
+ - `type`, `"message"`
335
+ - `listener`, `(event: MessageEvent<WebSocketData>) => void`
335
336
 
336
- ### Mocking incoming server events
337
+ Adds an event listener to the given event type of the WebSocket client.
337
338
 
338
- To mock an event sent from the server to the client, call `client.send()` with the event payload.
339
+ | Event name | Description |
340
+ | ---------- | ----------------------------------------------------------------- |
341
+ | `message` | Dispatched when data is sent by the intercepted WebSocket client. |
339
342
 
340
343
  ```js
341
- interceptor.on('connection', ({ client }) => {
342
- // Send a mocked "MessageEvent" to the client
343
- // with the given "hello" string as the data.
344
- client.send('hello')
344
+ client.addEventListener('message', (event) => {
345
+ console.log('outgoing:', event.data)
345
346
  })
346
347
  ```
347
348
 
348
- > The WebSocket interceptor respects the [WebSocket WHATWG standard](https://websockets.spec.whatwg.org/) and supports sending all supported data types (string, Blob, ArrayBuffer, etc).
349
+ #### `.removeEventListener(type, listener)`
349
350
 
350
- ### Bypassing events
351
+ - `type`, `"message"`
352
+ - `listener`, `(event: MessageEvent<WebSocketData>) => void`
351
353
 
352
- By default, the WebSocket interceptor **prevents all the outgoing client events from hitting the production server**. This is a sensible default to support mocking a WebSocket communication when a WebSocket server doesn't exist.
354
+ Removes the listener for the given event type.
353
355
 
354
- To bypass an event, first establish the actual server connection by calling `server.connect()`, and then call `server.send()` with the data you wish to forward to the original server.
356
+ #### `.send(data)`
355
357
 
356
- ```js
357
- interceptor.on('connection', ({ client, server }) => {
358
- // First, connect to the original server.
359
- server.connect()
358
+ - `data`, `string | Blob | ArrayBuffer`
360
359
 
361
- // Forward all outgoing client events to the original server.
362
- client.addEventListener('message', (event) => server.send(event.data))
363
- })
360
+ Sends the data to the intercepted WebSocket client.
361
+
362
+ ```js
363
+ client.send('text')
364
+ client.send(new Blob(['blob']))
365
+ client.send(new TextEncoder().encode('array buffer'))
364
366
  ```
365
367
 
366
- ### Intercepting incoming server events
368
+ #### `.close(code, reason)`
367
369
 
368
- The WebSocket communication is duplex and the WebSocket interceptor allows you to intercept both outgoing (client) events and incoming (original server) events.
370
+ - `code`, close [status code](https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1).
371
+ - `reason`, [close reason](https://www.rfc-editor.org/rfc/rfc6455#section-7.1.6).
369
372
 
370
- To intercept an incoming event from the original server, first establish the original connection by calling `server.connect()`, and then add a `message` listener to the `server` object.
373
+ Closes the client connection. Unlike the regular `WebSocket.prototype.close()`, the `client.close()` method can accept a non-configurable status codes, such as 1001, 1003, etc.
371
374
 
372
375
  ```js
373
- interceptor.on('connection', ({ server }) => {
374
- server.connect()
376
+ // Gracefully close the connection with the
377
+ // intercepted WebSocket client.
378
+ client.close()
379
+ ```
375
380
 
376
- server.addEventListener('message', (event) => {
377
- console.log('original server sent:', event.data)
378
- })
379
- })
381
+ ```js
382
+ // Terminate the connection by emulating
383
+ // the server unable to process the received data.
384
+ client.close(1003)
380
385
  ```
381
386
 
382
- Unlike the outgoing client events, incoming server events **are automatically forwarded to the client as soon as you call `server.connect()`**. This keeps the original server connection authentic if you ever decide to open one.
387
+ ### `WebSocketServerConnection`
383
388
 
384
- If you wish to prevent the automatic forwarding of the server events to the client, call `event.preventDefault()` on the incoming event you wish to prevent. This can be handy for observing as well as modifying incoming events.
389
+ #### `.connect()`
385
390
 
386
- ```js
387
- interceptor.on('connection', ({ client, server }) => {
388
- server.connect()
391
+ Establishes the connection to the original WebSocket server. Connection cannot be awaited. Any data sent via `server.send()` while connecting is buffered and flushed once the connection is open.
389
392
 
390
- server.addEventListener('message', (event) => {
391
- if (event.data === 'hello from server') {
392
- // Never forward this event to the client.
393
- event.preventDefault()
393
+ #### `.addEventListener(type, listener)`
394
394
 
395
- // Instead, send this mock data.
396
- client.send('greetings, client')
397
- return
398
- }
399
- })
400
- })
401
- ```
395
+ - `type`, `"message"`
396
+ - `listener`, `(event: MessageEvent<WebSocketData>) => void`
402
397
 
403
- ### Closing the connection
398
+ Adds an event listener to the given event type of the WebSocket server.
404
399
 
405
- You can terminate the open WebSocket client connection by calling `client.close()`.
400
+ | Event name | Description |
401
+ | ---------- | -------------------------------------------------------------------- |
402
+ | `message` | Dispatched when data is received from the original WebSocket server. |
406
403
 
407
404
  ```js
408
- interceptor.on('connection', ({ client }) => {
409
- client.close()
405
+ server.addEventListener('message', (event) => {
406
+ console.log('incoming:', event.data)
410
407
  })
411
408
  ```
412
409
 
413
- By default, this will close the connection with the `1000` code, meaning a graceful disconnect.
410
+ #### `.removeEventListener(type, listener)`
414
411
 
415
- You can provide a custom close [Status code](https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1) and [Close reason](https://www.rfc-editor.org/rfc/rfc6455#section-7.1.6) to the `client.close()` to mock different connection close scenarios.
412
+ - `type`, `"message"`
413
+ - `listener`, `(event: MessageEvent<WebSocketData>) => void`
416
414
 
417
- ```js
418
- interceptor.on('connection', ({ client }) => {
419
- client.close(3000, 'Close reason')
420
- })
421
- ```
415
+ Removes the listener for the given event type.
416
+
417
+ #### `.send(data)`
422
418
 
423
- You can also close the connection with the termination status code (1001 - 1015), which are not configurable by the user otherwise.
419
+ - `data`, `string | Blob | ArrayBuffer`
420
+
421
+ Sends the data to the original WebSocket server. Useful in a combination with the client-sent events forwarding:
424
422
 
425
423
  ```js
426
- interceptor.on('connection', ({ client }) => {
427
- // Terminate the connection because the "server"
428
- // cannot accept the data sent from the client.
429
- client.close(1003)
424
+ client.addEventListener('message', (event) => {
425
+ server.send(event.data)
430
426
  })
431
427
  ```
432
428
 
429
+ #### `.close()`
430
+
431
+ Closes the connection with the original WebSocket server. Unlike `client.close()`, closing the server connection does not accept any arguments and always asumes a graceful closure. Sending data via `server.send()` after the connection has been closed will have no effect.
432
+
433
433
  ## API
434
434
 
435
435
  ### `Interceptor`
@@ -165,6 +165,7 @@ declare class WebSocketServerConnection {
165
165
  * A WebSocket instance connected to the original server.
166
166
  */
167
167
  private realWebSocket?;
168
+ private mockCloseController;
168
169
  private [kEmitter];
169
170
  constructor(socket: WebSocketOverride, transport: WebSocketClassTransport, createConnection: () => WebSocket);
170
171
  /**
@@ -183,7 +184,7 @@ declare class WebSocketServerConnection {
183
184
  */
184
185
  addEventListener<K extends keyof WebSocketEventMap>(event: K, listener: (this: WebSocket, event: WebSocketEventMap[K]) => void, options?: AddEventListenerOptions | boolean): void;
185
186
  /**
186
- * Removes the listener for the given event.
187
+ * Remove the listener for the given event.
187
188
  */
188
189
  removeEventListener<K extends keyof WebSocketEventMap>(event: K, listener: (this: WebSocket, event: WebSocketEventMap[K]) => void, options?: EventListenerOptions | boolean): void;
189
190
  /**
@@ -194,21 +195,26 @@ declare class WebSocketServerConnection {
194
195
  * server.send(new TextEncoder().encode('hello'))
195
196
  */
196
197
  send(data: WebSocketData): void;
198
+ /**
199
+ * Close the actual server connection.
200
+ */
201
+ close(): void;
202
+ private handleMockClose;
203
+ private handleRealClose;
197
204
  }
198
205
 
199
206
  type WebSocketEventMap$1 = {
200
- connection: [
201
- args: {
202
- /**
203
- * The incoming WebSocket client connection.
204
- */
205
- client: WebSocketClientConnection;
206
- /**
207
- * The original WebSocket server connection.
208
- */
209
- server: WebSocketServerConnection;
210
- }
211
- ];
207
+ connection: [args: WebSocketConnectionData];
208
+ };
209
+ type WebSocketConnectionData = {
210
+ /**
211
+ * The incoming WebSocket client connection.
212
+ */
213
+ client: WebSocketClientConnection;
214
+ /**
215
+ * The original WebSocket server connection.
216
+ */
217
+ server: WebSocketServerConnection;
212
218
  };
213
219
  /**
214
220
  * Intercept the outgoing WebSocket connections created using
@@ -221,4 +227,4 @@ declare class WebSocketInterceptor extends Interceptor<WebSocketEventMap$1> {
221
227
  protected setup(): void;
222
228
  }
223
229
 
224
- export { WebSocketClientConnection, WebSocketClientConnectionProtocol, WebSocketData, WebSocketEventMap$1 as WebSocketEventMap, WebSocketInterceptor, WebSocketServerConnection, WebSocketTransport };
230
+ export { WebSocketClientConnection, WebSocketClientConnectionProtocol, WebSocketConnectionData, WebSocketData, WebSocketEventMap$1 as WebSocketEventMap, WebSocketInterceptor, WebSocketServerConnection, WebSocketTransport };
@@ -101,132 +101,6 @@ kEmitter;
101
101
 
102
102
  // src/interceptors/WebSocket/WebSocketServerConnection.ts
103
103
  var _outvariant = require('outvariant');
104
- var kEmitter2 = Symbol("kEmitter");
105
- var WebSocketServerConnection = class {
106
- constructor(socket, transport, createConnection) {
107
- this.socket = socket;
108
- this.transport = transport;
109
- this.createConnection = createConnection;
110
- this[kEmitter2] = new EventTarget();
111
- this.transport.onIncoming = (event) => {
112
- const messageEvent = bindEvent(
113
- this.realWebSocket,
114
- new CancelableMessageEvent("message", {
115
- data: event.data,
116
- origin: event.origin,
117
- cancelable: true
118
- })
119
- );
120
- this[kEmitter2].dispatchEvent(messageEvent);
121
- if (!messageEvent.defaultPrevented) {
122
- this.socket.dispatchEvent(
123
- bindEvent(
124
- /**
125
- * @note Bind the forwarded original server events
126
- * to the mock WebSocket instance so it would
127
- * dispatch them straight away.
128
- */
129
- this.socket,
130
- // Clone the message event again to prevent
131
- // the "already being dispatched" exception.
132
- new MessageEvent("message", {
133
- data: event.data,
134
- origin: event.origin
135
- })
136
- )
137
- );
138
- }
139
- };
140
- }
141
- /**
142
- * Server ready state.
143
- * Proxies the ready state of the original WebSocket instance,
144
- * if set. If the original connection hasn't been established,
145
- * defaults to `-1`.
146
- */
147
- get readyState() {
148
- if (this.realWebSocket) {
149
- return this.realWebSocket.readyState;
150
- }
151
- return -1;
152
- }
153
- /**
154
- * Open connection to the original WebSocket server.
155
- */
156
- connect() {
157
- _outvariant.invariant.call(void 0,
158
- this.readyState === -1,
159
- 'Failed to call "connect()" on the original WebSocket instance: the connection already open'
160
- );
161
- const ws = this.createConnection();
162
- ws.binaryType = this.socket.binaryType;
163
- this.socket.addEventListener(
164
- "close",
165
- (event) => {
166
- ws.close(event.code, event.reason);
167
- },
168
- { once: true }
169
- );
170
- ws.addEventListener("message", (event) => {
171
- this.transport.onIncoming(event);
172
- });
173
- ws.addEventListener("error", () => {
174
- this.socket.dispatchEvent(bindEvent(this.socket, new Event("error")));
175
- });
176
- this.realWebSocket = ws;
177
- }
178
- /**
179
- * Listen for the incoming events from the original WebSocket server.
180
- */
181
- addEventListener(event, listener, options) {
182
- this[kEmitter2].addEventListener(
183
- event,
184
- listener.bind(this.realWebSocket),
185
- options
186
- );
187
- }
188
- /**
189
- * Removes the listener for the given event.
190
- */
191
- removeEventListener(event, listener, options) {
192
- this[kEmitter2].removeEventListener(
193
- event,
194
- listener,
195
- options
196
- );
197
- }
198
- /**
199
- * Send data to the original WebSocket server.
200
- * @example
201
- * server.send('hello')
202
- * server.send(new Blob(['hello']))
203
- * server.send(new TextEncoder().encode('hello'))
204
- */
205
- send(data) {
206
- const { realWebSocket } = this;
207
- _outvariant.invariant.call(void 0,
208
- realWebSocket,
209
- 'Failed to call "server.send()" for "%s": the connection is not open. Did you forget to call "await server.connect()"?',
210
- this.socket.url
211
- );
212
- if (realWebSocket.readyState === realWebSocket.CONNECTING) {
213
- realWebSocket.addEventListener(
214
- "open",
215
- () => {
216
- realWebSocket.send(data);
217
- },
218
- { once: true }
219
- );
220
- return;
221
- }
222
- realWebSocket.send(data);
223
- }
224
- };
225
- kEmitter2;
226
-
227
- // src/interceptors/WebSocket/WebSocketTransport.ts
228
- var WebSocketTransport = class {
229
- };
230
104
 
231
105
  // src/interceptors/WebSocket/WebSocketOverride.ts
232
106
 
@@ -377,6 +251,167 @@ function getDataSize(data) {
377
251
  return data.byteLength;
378
252
  }
379
253
 
254
+ // src/interceptors/WebSocket/WebSocketServerConnection.ts
255
+ var kEmitter2 = Symbol("kEmitter");
256
+ var WebSocketServerConnection = class {
257
+ constructor(socket, transport, createConnection) {
258
+ this.socket = socket;
259
+ this.transport = transport;
260
+ this.createConnection = createConnection;
261
+ this[kEmitter2] = new EventTarget();
262
+ this.mockCloseController = new AbortController();
263
+ this.transport.onIncoming = (event) => {
264
+ const messageEvent = bindEvent(
265
+ this.realWebSocket,
266
+ new CancelableMessageEvent("message", {
267
+ data: event.data,
268
+ origin: event.origin,
269
+ cancelable: true
270
+ })
271
+ );
272
+ this[kEmitter2].dispatchEvent(messageEvent);
273
+ if (!messageEvent.defaultPrevented) {
274
+ this.socket.dispatchEvent(
275
+ bindEvent(
276
+ /**
277
+ * @note Bind the forwarded original server events
278
+ * to the mock WebSocket instance so it would
279
+ * dispatch them straight away.
280
+ */
281
+ this.socket,
282
+ // Clone the message event again to prevent
283
+ // the "already being dispatched" exception.
284
+ new MessageEvent("message", {
285
+ data: event.data,
286
+ origin: event.origin
287
+ })
288
+ )
289
+ );
290
+ }
291
+ };
292
+ }
293
+ /**
294
+ * Server ready state.
295
+ * Proxies the ready state of the original WebSocket instance,
296
+ * if set. If the original connection hasn't been established,
297
+ * defaults to `-1`.
298
+ */
299
+ get readyState() {
300
+ if (this.realWebSocket) {
301
+ return this.realWebSocket.readyState;
302
+ }
303
+ return -1;
304
+ }
305
+ /**
306
+ * Open connection to the original WebSocket server.
307
+ */
308
+ connect() {
309
+ _outvariant.invariant.call(void 0,
310
+ !this.realWebSocket || this.realWebSocket.readyState !== WebSocket.OPEN,
311
+ 'Failed to call "connect()" on the original WebSocket instance: the connection already open'
312
+ );
313
+ const realWebSocket = this.createConnection();
314
+ realWebSocket.binaryType = this.socket.binaryType;
315
+ realWebSocket.addEventListener("message", (event) => {
316
+ this.transport.onIncoming(event);
317
+ });
318
+ this.socket.addEventListener("close", this.handleMockClose.bind(this), {
319
+ signal: this.mockCloseController.signal
320
+ });
321
+ realWebSocket.addEventListener("close", this.handleRealClose.bind(this));
322
+ realWebSocket.addEventListener("error", () => {
323
+ this.socket.dispatchEvent(bindEvent(this.socket, new Event("error")));
324
+ });
325
+ this.realWebSocket = realWebSocket;
326
+ }
327
+ /**
328
+ * Listen for the incoming events from the original WebSocket server.
329
+ */
330
+ addEventListener(event, listener, options) {
331
+ this[kEmitter2].addEventListener(
332
+ event,
333
+ listener.bind(this.realWebSocket),
334
+ options
335
+ );
336
+ }
337
+ /**
338
+ * Remove the listener for the given event.
339
+ */
340
+ removeEventListener(event, listener, options) {
341
+ this[kEmitter2].removeEventListener(
342
+ event,
343
+ listener,
344
+ options
345
+ );
346
+ }
347
+ /**
348
+ * Send data to the original WebSocket server.
349
+ * @example
350
+ * server.send('hello')
351
+ * server.send(new Blob(['hello']))
352
+ * server.send(new TextEncoder().encode('hello'))
353
+ */
354
+ send(data) {
355
+ const { realWebSocket } = this;
356
+ _outvariant.invariant.call(void 0,
357
+ realWebSocket,
358
+ 'Failed to call "server.send()" for "%s": the connection is not open. Did you forget to call "server.connect()"?',
359
+ this.socket.url
360
+ );
361
+ if (realWebSocket.readyState === WebSocket.CLOSING || realWebSocket.readyState === WebSocket.CLOSED) {
362
+ return;
363
+ }
364
+ if (realWebSocket.readyState === WebSocket.CONNECTING) {
365
+ realWebSocket.addEventListener(
366
+ "open",
367
+ () => {
368
+ realWebSocket.send(data);
369
+ },
370
+ { once: true }
371
+ );
372
+ return;
373
+ }
374
+ realWebSocket.send(data);
375
+ }
376
+ /**
377
+ * Close the actual server connection.
378
+ */
379
+ close() {
380
+ const { realWebSocket } = this;
381
+ _outvariant.invariant.call(void 0,
382
+ realWebSocket,
383
+ 'Failed to close server connection for "%s": the connection is not open. Did you forget to call "server.connect()"?',
384
+ this.socket.url
385
+ );
386
+ realWebSocket.removeEventListener("close", this.handleRealClose);
387
+ if (realWebSocket.readyState === WebSocket.CLOSING || realWebSocket.readyState === WebSocket.CLOSED) {
388
+ return;
389
+ }
390
+ realWebSocket.close();
391
+ }
392
+ handleMockClose(_event) {
393
+ if (this.realWebSocket) {
394
+ this.realWebSocket.close();
395
+ }
396
+ }
397
+ handleRealClose(event) {
398
+ this.mockCloseController.abort();
399
+ const closeEvent = bindEvent(
400
+ this.realWebSocket,
401
+ new CloseEvent("close", event)
402
+ );
403
+ this[kEmitter2].dispatchEvent(closeEvent);
404
+ if (!closeEvent.defaultPrevented) {
405
+ this.socket[kClose](event.code, event.reason);
406
+ }
407
+ }
408
+ };
409
+ kEmitter2;
410
+
411
+ // src/interceptors/WebSocket/WebSocketTransport.ts
412
+ var WebSocketTransport = class {
413
+ };
414
+
380
415
  // src/interceptors/WebSocket/WebSocketClassTransport.ts
381
416
  var WebSocketClassTransport = class extends WebSocketTransport {
382
417
  constructor(socket) {