From e13ab4c81d1117b9b61507b4febc49baef190dc3 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 21 Feb 2024 10:45:08 +0100 Subject: [PATCH 01/16] add describing websocket api page --- src/components/react/action.tsx | 6 +- src/components/react/success.tsx | 2 +- .../docs/network-behavior/websocket.mdx | 278 ++++++++++++++++++ src/styles/global.css | 7 + 4 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 src/content/docs/network-behavior/websocket.mdx diff --git a/src/components/react/action.tsx b/src/components/react/action.tsx index 0d6e82f5..0e0cc89d 100644 --- a/src/components/react/action.tsx +++ b/src/components/react/action.tsx @@ -3,9 +3,9 @@ import { ArrowRightCircleIcon } from '@heroicons/react/24/solid' export function Action({ children }: { children: ReactNode }) { return ( -

- +

+ {children} -

+
) } diff --git a/src/components/react/success.tsx b/src/components/react/success.tsx index 72643aae..6e8d784e 100644 --- a/src/components/react/success.tsx +++ b/src/components/react/success.tsx @@ -4,7 +4,7 @@ export function Success({ children }: { children: ReactNode }): JSX.Element { return (
{children}
diff --git a/src/content/docs/network-behavior/websocket.mdx b/src/content/docs/network-behavior/websocket.mdx new file mode 100644 index 00000000..e7b6d3e6 --- /dev/null +++ b/src/content/docs/network-behavior/websocket.mdx @@ -0,0 +1,278 @@ +--- +order: 3 +title: Describing WebSocket API +description: Learn how to describe WebSocket API with Mock Service Worker. +keywords: + - websocket + - socket + - events + - mock + - describe +--- + +## Import + +MSW provides a designated [`ws`](/docs/api/ws) namespace for describing WebSocket events. We will use that namespace to describe what connections and events to intercept and how to handle them. + +import { Action } from '../../../components/react/action' + +Import the `ws` namespace from the `msw` package: + +```js {2} +// src/mocks/handlers.js +import { ws } from 'msw' + +export const handlers = [] +``` + +## Event handler + +WebSocket communications are _event-based_ so we will be using an event handler to intercept and describe them. + +In this tutorial, we will describe a chat application that uses WebSocket to send and receive messages. You can imagine that application like this: + +```js +// src/app.js +const ws = new WebSocket('wss://chat.example.com') + +// Handle receiving messages. +ws.addEventListener('message', (event) => { + renderMessage(event.data) +}) + +// Handle sending messages. +const handleFormSubmit = (event) => { + const data = new FormData(event.target) + const message = data.get('message') + ws.send(message) +} +``` + +Let's start by creating an event handler for a WebSocket endpoint using the `ws.link()` method. + +Call `ws.link()` to declare your first event handler: + +```js {4} +// src/mocks/handlers.js +import { ws } from 'msw' + +const chat = ws.link('wss://chat.example.com') + +export const handlers = [ + chat.on('connection', ({ client }) => { + console.log('Intercepted a WebSocket connection:', client.url) + }), +] +``` + +import { Info } from '../../../components/react/info' + + + You can use a plain string, a `URL` instance, a `RegExp`, and a path with + special tokens (like `*` wildcards) to describe the WebSocket endpoint. + + +The `chat` object returned from the `ws.link()` method gives us the server-like API to interact with the intercepted WebSocket connection. We can add the `"connection"` event listener to know when a client in our application tries to connect to the specified WebSocket server. + +Next, let's describe the incoming and outgoing events for the WebSocket connection. + +## Event flow + +WebSocket communications are _duplex_, which means that the client may receive events it hasn't explicitly requested. With that in mind, the WebSocket event handlers you create sit in-between the client and the server, allowing you to intercept and mock both client-to-server and server-to-client events. + +``` +client ⇄ MSW ⇄ server +``` + +This means that the event handler may both act as a replacement for a WebSocket Server (e.g. when developing mock-first) as well as middleware layer that proxies, observes, or modifies the actual client-to-server communication. We will take a look at both scenarios in this tutorial. + +## Client events + +### Intercepting client events + +Any event sent by the WebSocket client is considered an _outgoing event_. To intercept an outgoing client event, add a `"message"` event listener on the `client` object provided to you by the event handler. + + + Add a `"message"` listener to the `client` to intercept client events: + + +```js {2-4} /client/ +chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + console.log('Intercepted an outgoing message:', event.data) + }) +}) +``` + +> The event handler is compliant with the [WHATWG WebSocket specification](https://websockets.spec.whatwg.org/), which means it exposes messages as [`MessageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent) instances. + +### Mocking client events + +To send a server-to-client event, the `client` object provides a `send()` method that can send text, `Blob`, and `ArrayBuffer` data to the client. + +Use `client.send()` to mock an incoming client event: + +```js {3} /client.send/ +chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + client.send('hello from server') + }) +}) +``` + +With this event listener, every outgoing client event (us sending a message to the chat) will receive a `"hello from server"` message from the "server". + +Note that you can call `client.send()` anywhere in the connection listener. That way, you can send server-to-client data outside of the client message handling logic. + +```js {4} +chat.on('connection', ({ client }) => { + // Immediately send this message to every + // connected WebSocket client. + client.send('hello from server') +}) +``` + +### Broadcasting client events + +The `client.send()` method sends data to the individual connected WebSocket client. In order to broadcast data to multiple clients, MSW provides a `broadcast()` and `broadcastExcept()` methods on the event handler. + +For example, we can broadcast a message to everyone whenever a new client joins the chat, including that client: + +```js +chat.on('connection', ({ client }) => { + // Broadcast this message to all connected clients. + chat.broadcast('all say hi to a new client') +}) +``` + +We will use the `broadcastExcept()` method to broadcast a client-sent message to all other clients so they can see it in the chat too. + +```js {5} +chat.on('connection', ({ client }) => { + // Whenever a client sends a message... + client.addEventListener('message', (event) => { + // ...broadcast it to all other clients. + chat.broadcastExcept(client, event.data) + }) +}) +``` + +## Server events + +### Connecting to server + +By default, MSW does not establish the actual WebSocket server connection. This is handy when prototyping and developing mock-first. + +In order to affect the server-to-client communication, you must establish the actual server connection by calling `server.connect()` within the connection listener. + + + Call `server.connect()` to establish the actual WebSocket server connection: + + +```js {2} /server/ +chat.on('connection', ({ client, server }) => { + server.connect() +}) +``` + +This will connect the WebSocket client to the actual server and establish the server-to-client communication. + +### Forwarding client events + +Even with the server connection established, no client events will be forwarded to that server by default. This gives the client-to-server messaging an opt-in nature: no outgoing events are forwarded and you can decide which are. + +To enable client-to-server event forwarding, listen to the client messages you wish to forward and use `server.send()` method to send events to the actual server: + +```js {7} /server.send/ +chat.on('connection', ({ client, server }) => { + server.connect() + + // Listen to all messages the client sends... + client.addEventListener('message', (event) => { + // ...and send (forward) them to the server. + server.send(event.data) + }) +}) +``` + +import { Success } from '../../../components/react/success' + + + Forwarding client events to the server is required if you wish for the actual + server to receive those events. Without this forwarding, the client-sent + events will stop on the MSW layer. + + +### Intercepting server events + +You can intercept the events sent from the actual server by adding a `"message"` listener to the `server` object. + +```js {2-4} /server/ +chat.on('connection', ({ client, server }) => { + server.addEventListener('message', (event) => { + console.log('Intercepted an incoming message:', event.data) + }) +}) +``` + +import { Warning } from '../../../components/react/warning' + + + Unlike client-to-server events, **all server-sent events are automatically + forwarded to the client** the moment you establish the actual server + connection. + + +This means you don't have to call `client.send()` to forward an intercepted server-sent event to the client—it will be forwarded automatically. This allows MSW to keep a transparent server-to-client communication, giving you the means to modify it when needed. + +### Modifying server events + +You can modify the server-sent event before it reaches the WebSocket client by preventing it first, and then using `client.send()` to send whichever modified data you wish. + + + Use `event.preventDefault()` to prevent server-to-client forwarding, and + `client.send()` to send a mock data: + + +```js {6,9} +chat.on('connection', ({ client, server }) => { + server.addEventListener('message', (event) => { + if (event.data === 'hello from server') { + // Prevent this particular event from being + // forwarded to the client. + event.preventDefault() + + // Send a mocked data to the client instead. + client.send(event.data.replace('server', 'mock')) + } + }) +}) +``` + +> Since the default server-sent message behavior is to forward that message to the client, by calling `event.preventDefault()`, you opt-out from that behavior. + +In the scenario above, whenever the actual server sends a `"hello from server"` event, it will be intercepted, prevented, and a mocked `"hello from mock"` event will be sent to the client instead. + +### Mocking server events + +To mock a client-to-server event, the `server` object provides a `send()` method similar to that of the `client` object. + +Use `server.send()` to mock an outgoing client event: + +```js {10} /server.send/2 +chat.on('connection', ({ client, server }) => { + server.connect() + + client.addEventListener('message', (event) => { + server.send(event.data) + }) + + server.addEventListener('message', (event) => { + if (event.data === 'ping') { + server.send('pong') + } + }) +}) +``` + +Here, whenever the actual server sends a `"ping"` message, we immediately send a mocked `"pong"` message _from_ the client to the server. Since the server event is not prevented, it will be forwarded to the client as well. diff --git a/src/styles/global.css b/src/styles/global.css index 62525e0f..27c289a7 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -242,3 +242,10 @@ code[data-line-numbers-max-digits='3'] > [data-line]::before { .docs-group-item[open] .icon { @apply text-white rotate-90; } + +.action { + @apply my-5; +} +.action p { + @apply m-0; +} From 938a7ffd39f9f70737a355bb4a17ccf7cf5a7057 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 21 Feb 2024 11:14:58 +0100 Subject: [PATCH 02/16] add ws api page --- src/content/docs/api/ws.mdx | 202 ++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 src/content/docs/api/ws.mdx diff --git a/src/content/docs/api/ws.mdx b/src/content/docs/api/ws.mdx new file mode 100644 index 00000000..fc16de8e --- /dev/null +++ b/src/content/docs/api/ws.mdx @@ -0,0 +1,202 @@ +--- +order: 4 +title: ws +description: Intercept WebSocket connections. +keywords: + - websocket + - socket + - event + - handler + - namespace +--- + +The `ws` namespace helps you create event handlers to intercept WebSocket connections. + +## Call signature + +The `ws` namespace only exposes a single method called `link()`. The `link()` method creates an event handler that intercepts the WebSocket connection to the specified URL. + +```ts +ws.link(url: string | URL | RegExp) +``` + +import { PageCard } from '../../../components/react/pageCard' +import { CodeBracketSquareIcon } from '@heroicons/react/24/outline' + + + +## Event handler + +The object returned from the `ws.link()` call is referred to as _event handler_. The event handler object has the following properties and methods: + +### `.on(event, listener)` + +Adds a [connection listener](#connection-listener) for the outgoing WebSocket client connections. + +### `.clients` + +- `Set` + +The set of all active WebSocket clients. + +### `.broadcast(data)` + +- `data: string | Blob | ArrayBuffer` + +Sends the given data to all active WebSocket clients. + +```js {2} +const api = ws.link('wss://*') +api.broadcast('hello, everyone') +``` + +### `.broadcastExcept(clients, data)` + +- `clients: WebSocketClientConnection | Array` +- `data: string | Blob | ArrayBuffer` + +Sends the given data to all active WebSocket clients except the given `clients`. + +```js {4} +const api = ws.link('wss://*') + +api.on('connection', ({ client }) => { + api.broadcastExcept(client, 'all except this') +}) +``` + +You can also provide an array of WebSocket client connections as the argument to `clients`: + +```js +const ignoredClients = Array.from(api.clients).filter((client) => { + return client.url.includes('abc') +}) + +api.broadcastExcept(ignoredClients, 'hello') +``` + +## Connection listener + +| Argument | Type | Description | +| -------- | -------- | ---------------------------------------------------- | +| `client` | `object` | Outgoing WebSocket client connection object. | +| `server` | `object` | Actual WebSocket server connection object. | +| `params` | `object` | Path parameters extracted from the connection `url`. | + +The connection listener is called on every outgoing WebSocket client connection. + +```js {7-9} +import { ws } from 'msw' +import { setupWorker } from 'msw/browser' + +const api = ws.link('wss://chat.example.com') + +const worker = setupWorker( + api.on('connection', () => { + console.log('client connected!') + }) +) + +await worker.start() + +const socket = new WebSocket('wss://chat.example.com') +socket.onopen = () => { + console.log('connection established!') +} +``` + +In this example, the WebSocket connection to `wss://chat.example.com` emits the `"connection"` event on the `api` event handler because the endpoint matches the one provided to the `ws.link()` call. Since the connection is successful, the `"open"` event is also dispatched on the `socket` instance. + +## `WebSocketClientConnection` + +The `WebSocketClientConnection` object represents an intercepted WebSocket client connection. + +### `.addEventListener(event, listener, options)` + +### `.removeEventListener(event, listener, options)` + +### `.send(data)` + +- `data: string | Blob | ArrayBuffer` + +Sends data to the WebSocket client. This is equivalent to the client receiving that data from the server. + +```js {2-4} +api.on('connection', ({ client }) => { + client.send('hello') + client.send(new Blob(['hello'])) + client.send(new TextEncoder().encode('hello')) +}) +``` + +### `.close(code, reason)` + +- `code: number | undefined`, default: `1000` +- `reason: string | undefined` + +Closes the active WebSocket client connection. + +```js {2} +api.on('connection', ({ client }) => { + client.close() +}) +``` + +Unlike the `WebSocket.prototype.close()` method, the `client.close()` method accepts non-configurable close codes. This allows you to emulate client close scenarios based on server-side errors. + +```js {3} +api.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + client.close(1003, 'Invalid data') + }) +}) +``` + +You can also implement custom close code and reason: + +```js {2} +api.on('connection', ({ client }) => { + client.close(4000, 'Custom close reason') +}) +``` + +## `WebSocketServerConnection` + +The `WebSocketServerConnection` object represents the actual WebSocket server connection. + +### `.connect()` + +Establishes connection to the actual WebSocket server. + +```js {2} +api.on('connection', ({ server }) => { + server.connect() +}) +``` + +### `.addEventListener(event, listener, options)` + +### `.removeEventListener(event, listener, options)` + +### `.send(data)` + +- `data: string | Blob | ArrayBuffer` + +Sends data to the actual WebSocket server. This is equivalent to the client sending this data to the server. + +```js {6} +api.on('connection', ({ server }) => { + server.connect() + + server.addEventListener('message', (event) => { + if (event.data === 'hello from server') { + server.send('hello from client') + } + }) +}) +``` From 300ea5fee2d712365feddb636458516e6477fef7 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Fri, 1 Mar 2024 14:43:22 +0100 Subject: [PATCH 03/16] add handling-websocket-events page --- .../docs/basics/handling-websocket-events.mdx | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 src/content/docs/basics/handling-websocket-events.mdx diff --git a/src/content/docs/basics/handling-websocket-events.mdx b/src/content/docs/basics/handling-websocket-events.mdx new file mode 100644 index 00000000..4397343d --- /dev/null +++ b/src/content/docs/basics/handling-websocket-events.mdx @@ -0,0 +1,239 @@ +--- +order: 3 +title: Handling WebSocket events +description: Learn how to intercept and mock WebSocket events. +keywords: + - websocket + - event + - ws +--- + +MSW supports intercepting and mocking WebSocket connections using its designated [`ws` API](/docs/api/ws). This page will guide you through the basics of handling WebSocket events, explain the mental model behind MSW when intercepting duplex connections, and elaborate on the defaults the library ships to promote good developer experience. + +## Respecting standards + +Mock Service Worker is dedicated to respecting, promoting, and teaching you about the web standards. The way you intercept and mock WebSocket communications will be according to the [WHATWG WebSocket Standard](https://websockets.spec.whatwg.org/), which means treating clients as `EventTarget`, listening to events like `"message"` and `"close"`, and reading the sent and received data from the `MessageEvent` objects. + +**We do not plan to support custom WebSocket protocols**, such as those using HTTP polling. Those are proprietary to the third-party tooling that implements them, and there is no reliable way for MSW to intercept such protocols without introducing non-standard, library-specific logic. + +That being said, we acknowledge that the standard `WebSocket` interface is rarely used in production systems as-is. Often, it's used as the underlying implementation detail for more convenient third-party abstractions like SocketIO or PartyKit. We firmly believe in mock-as-you-use philosophy and want to provide you with the mocking experience that resembles the actual usage of the third-party libraries you may be relying on through the concept of [Bindings](#bindings). + +## Event types + +Unlike HTTP, a WebSocket communication is _duplex_, which means that the client and the server may send events independently. There are two types of events you can handle with MSW: + +- **Outgoing client events**. These are the events the client sends via `.send()`; +- **Incoming server events**. These are the events the server sends and the client receives via its `"message"` event listener. + +## Intercepting connections + +To support the duplex nature of the WebSocket communication and allow you to intercept both client-sent and server-sent events, MSW effectively acts as a middleware layer that sits between your client and a WebSocket server. + +``` +client ⇄ MSW ⇄ server +``` + +You are in control of how you want to utilize MSW. It can become a full substitute for a WebSocket server in a mock-first development, act as a proxy to observe and modify the events coming from the production server, or emulate client-sent events to test various server behaviors. + +Handling WebSocket events starts by defining the server URL that the client connects to. This is done using the `ws.link()` method. + +```js /ws/1,2 +import { ws } from 'msw' + +const chat = ws.link('wss://chat.example.com') +``` + +> You can use the same URL predicate for WebSocket as you use for the `http` handlers: relative and absolute URLs, regular expressions, and paths with parameters and wildcards. + +Next, add an event handler to the list of your handlers: + +```js {2-4} +export const handlers = [ + chat.on('connection', () => { + console.log('outgoing WebSocket connection') + }), +] +``` + +You will be handling both client-sent and server-sent events within the `"connection"` event listener. + +## Important defaults + +MSW implements a set of default behaviors to ensure good developer experience in different testing and development scenarios concerning WebSockets. You can opt-out from all of those, and fine-tune the interception behavior to suit your needs. + +### Client-to-server event forwarding + +By default, **outgoing client events _are not_ forwarded to the original server**. In fact, no connection to the original server is ever established. This allows for mock-first development, where the original server may not yet exist. + +> Learn how to opt-out from this behavior and [enable client-to-server forwarding](#client-to-server-forwarding). + +### Server-to-client event forwarding + +By default, once you [establish the actual server connection](#connecting-to-the-server), **all incoming server events _are_ forwarded to the client.** This combines nicely with the explicit intention to connect to the actual server, as there's little reason to do so without also wishing to receive the server events. + +> Learn how to opt-out from this behavior and [modify or prevent server events](#preventing-server-events). + +## Client events + +### Intercepting client events + +To intercept an outgoing client event, grab the `client` object from the `"connection"` event listener argument and add a `"message"` listener on that object. + +```js /client/1,2 {2-4} +chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + console.log('from client:', event.data) + }) +}) +``` + +Now, whenever a WebSocket client sends data via the `.send()` method, the `"message"` listener in this handler will be called. The listener exposes a single `event` argument, which is a [`MessageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent) received from the client, with the sent data available as `event.data`. + +### Sending data to the client + +To send data to the connected client, grab the `client` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send. + +```js /client/1,2 {2} +chat.on('connection', ({ client }) => { + client.send('Hello from the server!') +}) +``` + +> MSW supports sending strings, `Blob`, and `ArrayBuffer`. + +### Broadcasting data to clients + +To broadcast data to all connected clients, use the `.broadcast()` method on the event handler object (the one returned from the `ws.link()` call) and provide it with the data you wish to broadcast. + +```js /chat/2 {2} +chat.on('connection', () => { + chat.broadcast('Hello everyone!') +}) +``` + +You can also broadcast data to all clients except a subset of clients by using the `.boardcastExcept()` method on the event handler object. + +```js {3,6} +chat.on('connection', ({ client }) => { + // Broadcast data to all clients except the current one. + chat.broadcastExcept(client, 'Hello everyone except you!') + + // Broadcast data to all the clients matching a predicate. + chat.boardcastExcept(chat.clients.filter((client) => { + return client + }, "Hello to some of you!") +}) +``` + +## Server events + +### Connecting to the server + +import { Warning } from '../../../components/react/warning' + +To handle any events from the actual WebSocket server, you must _connect_ to that server first. + +To establish the connection to the actual WebSocket server, grab the `server` object from the `"connection"` event listener argument and call its `.connect()` method. + +```js /server/ {2} +chat.on('connection', ({ server }) => { + server.connect() +}) +``` + +### Intercepting server events + +To intercept an incoming event from the actual sever, grab the `server` object from the `"connection"` event listener argument and add a `"message"` event listener on that object. + +```js /server/1,2 {2-4} +chat.on('connection', ({ server }) => { + server.addEventListener('message', (event) => { + console.log('from server:', event.data) + }) +}) +``` + +Now, whenever the actual server sends data, the `"message"` listener in this handler will be called. The listener exposes a single `event` argument, which is a [`MessageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent) received from the client, with the sent data available as `event.data`. + +### Preventing server events + +By default, all server events are forwarded to the connected client. You can opt-out from this behavior by preventing the received server `"message"` event. This is handy if you wish to modify the server-sent data before it reaches the client or prevent some server events from arriving at the client completely. + +```js {4} +chat.on('connection', ({ client, server }) => { + server.addEventListener('message', (event) => { + // Prevent the default server-to-client forwarding. + event.preventDefault() + + // Modify the original server-sent data and send + // it to the client instead. + client.send(event.data + 'mocked') + }) +}) +``` + +### Sending data to the server + +To send data to the actual server, grab the `server` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send to the server. + +```js /server/ {2} +chat.on('connection', ({ server }) => { + server.send('hello from client!') +} +``` + +This is equivalent to a client sending that data to the server. + +### Client-to-server forwarding + +By default, the actual server will not receive any outgoing client events—they will short-circuit on your event handler's level. If you wish to forward client-to-server events, establish the actual server connection by calling `server.connect()`, listen to the outgoing events via the `"message"` event listener on the `client` object, and use the `server.send()` method to forward the data. + +```js {2-3,5-9} +chat.on('connection', ({ client, server }) => { + // Establish the actual server connection. + server.connect() + + // Listen to all outgoing client events. + client.addEventListener('message', (event) => { + // And send them to the actual server as-is. + server.send(event.data) + }) +}) +``` + +> You can control what messages to forward to the actual server in the `"message'` event listener on the `client` object. Feel free to introduce conditions, analyze the message, or modify the data to forward. + +## Bindings + +To provide a more familiar experience when mocking third-party WebSocket clients, MSW uses _bindings_. A binding is a wrapper over the standard `WebSocket` class that encapsulates the third-party-specific behaviors, such as message parsing, and gives you a public API similar to that of the bound third-party library. + +For example, here's how to handle SocketIO communication using MSW and a designated SocketIO binding: + +```js /bind/1,3 {2,8} +import { ws } from 'msw' +import { bind } from '@mswjs/socket.io-binding' + +const chat = ws.link('wss://chat.example.com') + +export const handlers = [ + chat.on('connection', (connection) => { + const io = bind(connection) + + io.client.on('hello', (username) => { + io.client.emit('message', `hello, ${username}!`) + }) + }), +] +``` + +import { PageCard } from '../../../components/react/pageCard' +import { CodeBracketSquareIcon } from '@heroicons/react/24/outline' + + + +> Note that binding is not meant to cover all the public APIs of the respective third-party library. Unless the binding is shipped by that library, maintaining full compatibility is not feasible. From c7455c236722f02759e024c29d69fb938d64c6a2 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 2 Mar 2024 13:32:14 +0100 Subject: [PATCH 04/16] rewrite the describing websocket apis page --- .../docs/network-behavior/websocket.mdx | 268 +++++++----------- 1 file changed, 102 insertions(+), 166 deletions(-) diff --git a/src/content/docs/network-behavior/websocket.mdx b/src/content/docs/network-behavior/websocket.mdx index e7b6d3e6..e76988ea 100644 --- a/src/content/docs/network-behavior/websocket.mdx +++ b/src/content/docs/network-behavior/websocket.mdx @@ -29,7 +29,9 @@ export const handlers = [] WebSocket communications are _event-based_ so we will be using an event handler to intercept and describe them. -In this tutorial, we will describe a chat application that uses WebSocket to send and receive messages. You can imagine that application like this: +In this tutorial, we will describe a chat application that uses WebSocket to send and receive messages. We will use MSW as a substitute for an actual WebSocket server, developing mock-first. + +You can imagine the chat application like this: ```js // src/app.js @@ -50,12 +52,15 @@ const handleFormSubmit = (event) => { Let's start by creating an event handler for a WebSocket endpoint using the `ws.link()` method. -Call `ws.link()` to declare your first event handler: +Call `ws.link()` to declare an event handler: -```js {4} +```js {7,10-12} // src/mocks/handlers.js import { ws } from 'msw' +// The "chat" object is an event handler responsible +// for intercepting and mocking any WebSocket events +// to the provided endpoint. const chat = ws.link('wss://chat.example.com') export const handlers = [ @@ -65,214 +70,145 @@ export const handlers = [ ] ``` -import { Info } from '../../../components/react/info' - - - You can use a plain string, a `URL` instance, a `RegExp`, and a path with - special tokens (like `*` wildcards) to describe the WebSocket endpoint. - - The `chat` object returned from the `ws.link()` method gives us the server-like API to interact with the intercepted WebSocket connection. We can add the `"connection"` event listener to know when a client in our application tries to connect to the specified WebSocket server. -Next, let's describe the incoming and outgoing events for the WebSocket connection. - -## Event flow - -WebSocket communications are _duplex_, which means that the client may receive events it hasn't explicitly requested. With that in mind, the WebSocket event handlers you create sit in-between the client and the server, allowing you to intercept and mock both client-to-server and server-to-client events. - -``` -client ⇄ MSW ⇄ server -``` - -This means that the event handler may both act as a replacement for a WebSocket Server (e.g. when developing mock-first) as well as middleware layer that proxies, observes, or modifies the actual client-to-server communication. We will take a look at both scenarios in this tutorial. - -## Client events +Next, let's describe how to handle the chat messages that the client sends and mock the server responding to them. -### Intercepting client events +## Responding to client messages -Any event sent by the WebSocket client is considered an _outgoing event_. To intercept an outgoing client event, add a `"message"` event listener on the `client` object provided to you by the event handler. +Whenever the WebSocket client sends data to the server, the `client` object in the `"connection"` event listener argument will emit the `"message"` event. We can attach a listener to that event to listen and react to outgoing client messages. Add a `"message"` listener to the `client` to intercept client events: -```js {2-4} /client/ -chat.on('connection', ({ client }) => { - client.addEventListener('message', (event) => { - console.log('Intercepted an outgoing message:', event.data) - }) -}) -``` - -> The event handler is compliant with the [WHATWG WebSocket specification](https://websockets.spec.whatwg.org/), which means it exposes messages as [`MessageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent) instances. - -### Mocking client events - -To send a server-to-client event, the `client` object provides a `send()` method that can send text, `Blob`, and `ArrayBuffer` data to the client. - -Use `client.send()` to mock an incoming client event: - -```js {3} /client.send/ -chat.on('connection', ({ client }) => { - client.addEventListener('message', (event) => { - client.send('hello from server') - }) -}) -``` - -With this event listener, every outgoing client event (us sending a message to the chat) will receive a `"hello from server"` message from the "server". +```js /client.addEventListener/ {8-10} +// src/mocks/handlers.js +import { ws } from 'msw' -Note that you can call `client.send()` anywhere in the connection listener. That way, you can send server-to-client data outside of the client message handling logic. +const chat = ws.link('wss://chat.example.com') -```js {4} -chat.on('connection', ({ client }) => { - // Immediately send this message to every - // connected WebSocket client. - client.send('hello from server') -}) +export const handlers = [ + chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + console.log('client sent:', event.data) + }) + }), +] ``` -### Broadcasting client events +Now that we know when the client sends a message in the chat, we can send data back from the "server", which is our event handler. -The `client.send()` method sends data to the individual connected WebSocket client. In order to broadcast data to multiple clients, MSW provides a `broadcast()` and `broadcastExcept()` methods on the event handler. +To send data from the server to the client, we can use the `client.send()` method provided by the `client` object. -For example, we can broadcast a message to everyone whenever a new client joins the chat, including that client: + + Call `client.send()` to send data to the client: + -```js -chat.on('connection', ({ client }) => { - // Broadcast this message to all connected clients. - chat.broadcast('all say hi to a new client') -}) -``` +```js /client.send/ {9} +// src/mocks/handlers.js +import { ws } from 'msw' -We will use the `broadcastExcept()` method to broadcast a client-sent message to all other clients so they can see it in the chat too. +const chat = ws.link('wss://chat.example.com') -```js {5} -chat.on('connection', ({ client }) => { - // Whenever a client sends a message... - client.addEventListener('message', (event) => { - // ...broadcast it to all other clients. - chat.broadcastExcept(client, event.data) - }) -}) +export const handlers = [ + chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + client.send('hello from server!') + }) + }), +] ``` -## Server events +Now, whenever the client sends a message, our `chat` event handler intercepts it and sends back a `"hello from server!"` string. You can think of this interaction as a mock chat message arriving in response to any message you send from the application. -### Connecting to server +Our event handler has been interacting with a single `client` so far. Let's take a look how to broadcast data across all clients to implement the realtime chat functionality. -By default, MSW does not establish the actual WebSocket server connection. This is handy when prototyping and developing mock-first. +## Broadcasting data -In order to affect the server-to-client communication, you must establish the actual server connection by calling `server.connect()` within the connection listener. +When a single client sends a message, we want to broadcast that message to all connected clients so they would see it in their applications. To do so, our `chat` event handler object provides a `broadcast()` method that we can use. - Call `server.connect()` to establish the actual WebSocket server connection: + Call `chat.broadcast()` to broadcast the message to all clients: -```js {2} /server/ -chat.on('connection', ({ client, server }) => { - server.connect() -}) -``` - -This will connect the WebSocket client to the actual server and establish the server-to-client communication. - -### Forwarding client events - -Even with the server connection established, no client events will be forwarded to that server by default. This gives the client-to-server messaging an opt-in nature: no outgoing events are forwarded and you can decide which are. - -To enable client-to-server event forwarding, listen to the client messages you wish to forward and use `server.send()` method to send events to the actual server: +```js /chat.broadcast/ {9} +// src/mocks/handlers.js +import { ws } from 'msw' -```js {7} /server.send/ -chat.on('connection', ({ client, server }) => { - server.connect() +const chat = ws.link('wss://chat.example.com') - // Listen to all messages the client sends... - client.addEventListener('message', (event) => { - // ...and send (forward) them to the server. - server.send(event.data) - }) -}) +export const handlers = [ + chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + chat.broadcast(event.data) + }) + }), +] ``` -import { Success } from '../../../components/react/success' - - - Forwarding client events to the server is required if you wish for the actual - server to receive those events. Without this forwarding, the client-sent - events will stop on the MSW layer. - - -### Intercepting server events +When using the `.broadcast()` method of the event handler, _all the connected clients will receive the sent data_. That includes the `client` that has sent the message we are broadcasting! Depending on how you implement your chat, you may want to omit the initial client from this broadcasting (e.g. if you display the sent message for the client optimistically). -You can intercept the events sent from the actual server by adding a `"message"` listener to the `server` object. +To broadcast data to all clients except a subset of clients, use the `.broacastExcept()` method on the event handler object. -```js {2-4} /server/ -chat.on('connection', ({ client, server }) => { - server.addEventListener('message', (event) => { - console.log('Intercepted an incoming message:', event.data) - }) -}) -``` - -import { Warning } from '../../../components/react/warning' + + Call `chat.broadcastExcept()` to broadcast the message to all clients except the initial sender: + - - Unlike client-to-server events, **all server-sent events are automatically - forwarded to the client** the moment you establish the actual server - connection. - +```js /chat.broadcastExcept/ {9} +// src/mocks/handlers.js +import { ws } from 'msw' -This means you don't have to call `client.send()` to forward an intercepted server-sent event to the client—it will be forwarded automatically. This allows MSW to keep a transparent server-to-client communication, giving you the means to modify it when needed. +const chat = ws.link('wss://chat.example.com') -### Modifying server events +export const handlers = [ + chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + chat.broadcastExcept(client, event.data) + }) + }), +] +``` -You can modify the server-sent event before it reaches the WebSocket client by preventing it first, and then using `client.send()` to send whichever modified data you wish. +Now, whenever a client sends a message to the chat, it will be broadcasted to _all the other clients_ so they would see it in the UI too. - - Use `event.preventDefault()` to prevent server-to-client forwarding, and - `client.send()` to send a mock data: - +## Next steps -```js {6,9} -chat.on('connection', ({ client, server }) => { - server.addEventListener('message', (event) => { - if (event.data === 'hello from server') { - // Prevent this particular event from being - // forwarded to the client. - event.preventDefault() - - // Send a mocked data to the client instead. - client.send(event.data.replace('server', 'mock')) - } - }) -}) -``` +### Integrations -> Since the default server-sent message behavior is to forward that message to the client, by calling `event.preventDefault()`, you opt-out from that behavior. +Once you have described the network behavior you want, integrate it into any environment in your application. -In the scenario above, whenever the actual server sends a `"hello from server"` event, it will be intercepted, prevented, and a mocked `"hello from mock"` event will be sent to the client instead. +import { PageCard } from '../../../components/react/pageCard' +import { WindowIcon, CommandLineIcon } from '@heroicons/react/24/outline' -### Mocking server events +
+ + +
-To mock a client-to-server event, the `server` object provides a `send()` method similar to that of the `client` object. +> Note that some envirionments, like Node.js, do not ship the global WebSocket API yet. You may want to configure your environment to polyfill the `WebSocket` class in those cases. -Use `server.send()` to mock an outgoing client event: +### Learn about handling WebSocket events -```js {10} /server.send/2 -chat.on('connection', ({ client, server }) => { - server.connect() +This tutorial includes a minimal functionality to describe the WebSocket communication for a chat application. There's much more you can do with WebSosckets in MSW, like connecting to the actual server, modifying server-sent events, mocking errors and connection closures. - client.addEventListener('message', (event) => { - server.send(event.data) - }) +Learn more about what you can do with WebSocket connections on this page: - server.addEventListener('message', (event) => { - if (event.data === 'ping') { - server.send('pong') - } - }) -}) -``` +import { NewspaperIcon } from '@heroicons/react/24/outline' -Here, whenever the actual server sends a `"ping"` message, we immediately send a mocked `"pong"` message _from_ the client to the server. Since the server event is not prevented, it will be forwarded to the client as well. + From 43f6c6b58fd9f3bc7cb64d47546fdc937b0cda44 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 2 Mar 2024 13:43:10 +0100 Subject: [PATCH 05/16] add api links to handling websocket events --- .../docs/basics/handling-websocket-events.mdx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/content/docs/basics/handling-websocket-events.mdx b/src/content/docs/basics/handling-websocket-events.mdx index 4397343d..2bbcaac9 100644 --- a/src/content/docs/basics/handling-websocket-events.mdx +++ b/src/content/docs/basics/handling-websocket-events.mdx @@ -101,6 +101,13 @@ chat.on('connection', ({ client }) => { > MSW supports sending strings, `Blob`, and `ArrayBuffer`. + + ### Broadcasting data to clients To broadcast data to all connected clients, use the `.broadcast()` method on the event handler object (the one returned from the `ws.link()` call) and provide it with the data you wish to broadcast. @@ -125,6 +132,55 @@ chat.on('connection', ({ client }) => { }) ``` + + + + +### Closing client connections + +You can close an existing client connection at any time by calling `client.close()`. + +```js {2} +chat.on('connection', ({ client }) => { + client.close() +}) +``` + +By default, the `.close()` method will result in a graceful closure of the connection (1000 code). You can control the nature of the connection closure by providing the custom `code` and `reason` arguments to the `.close()` method. + +```js /1003/ {4} +chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + if (event.data === 'hello') { + client.close(1003) + } + }) +}) +``` + +For example, in this handler, once the client sends a `"hello"` message, its connection will be terminated with the `1003` code (received data the server cannot accept). + +Unlike the `WebSocket.prototype.close()` method, the `.close()` method on the `client` connection can accept even non user-configurable closure codes like 1001, 1002, 1003, etc, which gives you more flexibility in describing the WebSocket communication. + +import { CubeTransparentIcon } from '@heroicons/react/24/outline' + + + ## Server events ### Connecting to the server @@ -141,6 +197,13 @@ chat.on('connection', ({ server }) => { }) ``` + + ### Intercepting server events To intercept an incoming event from the actual sever, grab the `server` object from the `"connection"` event listener argument and add a `"message"` event listener on that object. @@ -184,6 +247,13 @@ chat.on('connection', ({ server }) => { This is equivalent to a client sending that data to the server. + + ### Client-to-server forwarding By default, the actual server will not receive any outgoing client events—they will short-circuit on your event handler's level. If you wish to forward client-to-server events, establish the actual server connection by calling `server.connect()`, listen to the outgoing events via the `"message"` event listener on the `client` object, and use the `server.send()` method to forward the data. From f94126b8d13800cdb78b2743a5007660841fa104 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 2 Apr 2024 15:32:48 +0200 Subject: [PATCH 06/16] document websocket logging --- .../docs/basics/handling-websocket-events.mdx | 113 +++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/src/content/docs/basics/handling-websocket-events.mdx b/src/content/docs/basics/handling-websocket-events.mdx index 2bbcaac9..1174e3e5 100644 --- a/src/content/docs/basics/handling-websocket-events.mdx +++ b/src/content/docs/basics/handling-websocket-events.mdx @@ -187,7 +187,10 @@ import { CubeTransparentIcon } from '@heroicons/react/24/outline' import { Warning } from '../../../components/react/warning' -To handle any events from the actual WebSocket server, you must _connect_ to that server first. + + To handle any events from the actual WebSocket server, you must _connect_ to + that server first. + To establish the connection to the actual WebSocket server, grab the `server` object from the `"connection"` event listener argument and call its `.connect()` method. @@ -273,6 +276,114 @@ chat.on('connection', ({ client, server }) => { > You can control what messages to forward to the actual server in the `"message'` event listener on the `client` object. Feel free to introduce conditions, analyze the message, or modify the data to forward. +## Logging + +Since MSW implements the WebSocket interception mock-first, no actual connections will be established until you explicitly say so. This means that the mocked scenarios won't appear as network entries in your browser's DevTools and you won't be able to observe them. + +MSW enables custom logging for both mocked and original WebSocket connections **in the browser** to allow you to: + +- Observe any WebSocket connection if you have an event handler for it; +- See previews for binary messages (Blob/ArrayBuffer); +- Tell apart the messages sent by your application/original server from those initiated by the event handler (i.e. mocked messages). + +### Message colors and styles + +All the printed messages are color-graded by the following criteria: system events, outgoing +messages, incoming +messages, and mocked +events. In addition to the colors, some messages are represented by solid (↑↓) or dotted icons (⇡⇣), indicating whether the event occurred in your application or in the event handler, respectively. + +### Connection events + +#### Connection opened + +``` +[MSW] 12:34:56 ▶ wss://example.com +``` + +Dispatched when the connection is open (i.e. the WebSocket client emits the `open` event). + +#### × Connection errored + +``` +[MSW] 12:34:56 × wss://example.com +``` + +Dispatched when the client receives an error (i.e. the WebSocket client emits the `error` event). + +#### Connection closed + +``` +[MSW] 12:34:56 ■ wss://example.com +``` + +Dispatched when the connection is closed (i.e. the WebSocket client emits the `close` event). + +### Message events + +Any message, be it outgoing or incoming message, follows the same structure: + +```txt /▼ timestamp/ /▲ icon/ /▼ sent data/ /▼ data length/ + ▼ timestamp ▼ sent data ▼ data length (bytes) +[MSW] 00:00:00.000 ↑ hello from client 17 + ▲ icon +``` + +Binary messages print a text preview of the sent binary alongside its full byte length: + +``` +[MSW] 12:34:56.789 ↑ Blob(hello world) 11 +[MSW] 12:34:56.789 ↑ ArrayBuffer(preview) 7 +``` + +Long text messages and text previews are truncated: + +``` +[MSW] 12:34:56.789 ↑ this is a very long stri… 17 +``` + +> You can access the full message by clicking on its console group and inspecting the original `MessageEvent` reference. + +#### Outgoing client message + +``` +[MSW] 12:34:56.789 ↑ hello from client 17 +``` + +A message sent by the client in your application. + +#### Outgoing mocked client message + +``` +[MSW] 12:34:56.789 ⇡ hello from mock 15 +``` + +A message sent from the client by the event handler (via `server.send()`). Requires the actual server connection to be opened via `server.connect()`. The client itself never sent this, thus the icon is dotted. + +#### Incoming client message + +``` +[MSW] 12:34:56.789 ↓ hello from server 17 +``` + +The end message the client received (i.e. the message that triggered the "message" event on the WebSocket client). The message can be either from the event handler or from the actual WebSocket server. + +#### Incoming mocked client message + +``` +[MSW] 12:34:56.789 ⇣ hello from mock 15 +``` + +A mocked message sent to the client from the event handler via `client.send()`. The actual server has never sent this, thus the icon is dotted. + +#### Incoming server message + +``` +[MSW] 12:34:56.789 ⇣ hello from server 17 +``` + +An incoming message from the actual server. Requires the actual server connection to be opened via `server.connect()`. The incoming server messages can be modified or skipped by the event handler, thus the icon is dotted. + ## Bindings To provide a more familiar experience when mocking third-party WebSocket clients, MSW uses _bindings_. A binding is a wrapper over the standard `WebSocket` class that encapsulates the third-party-specific behaviors, such as message parsing, and gives you a public API similar to that of the bound third-party library. From 29a5fe37f10dd3c686443ad1e9429a221ebd38ea Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sat, 6 Apr 2024 13:30:00 +0200 Subject: [PATCH 07/16] update client-to-server forwarding default --- .../docs/basics/handling-websocket-events.mdx | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/content/docs/basics/handling-websocket-events.mdx b/src/content/docs/basics/handling-websocket-events.mdx index 1174e3e5..5695223f 100644 --- a/src/content/docs/basics/handling-websocket-events.mdx +++ b/src/content/docs/basics/handling-websocket-events.mdx @@ -61,17 +61,21 @@ You will be handling both client-sent and server-sent events within the `"connec MSW implements a set of default behaviors to ensure good developer experience in different testing and development scenarios concerning WebSockets. You can opt-out from all of those, and fine-tune the interception behavior to suit your needs. +### Client connections + +By default, no intercepted WebSocket connections are opened. This encourages mock-first development and makes it easier to manage connections to non-existing servers. You can [establish the actual server connection](#establishing-server-connection) by calling `server.connect()`. + ### Client-to-server event forwarding -By default, **outgoing client events _are not_ forwarded to the original server**. In fact, no connection to the original server is ever established. This allows for mock-first development, where the original server may not yet exist. +By default, once you [establish the actual server connection](#connecting-to-the-server), **outgoing client events are _forwarded to the original server_**. If the server connection hasn't been established, no forwarding occurs (nowhere to forward). You can opt-out from this behavior by calling `event.preventDefault()` on the client message event. -> Learn how to opt-out from this behavior and [enable client-to-server forwarding](#client-to-server-forwarding). +> Learn more about [client-to-server forwarding](#client-to-server-forwarding). ### Server-to-client event forwarding -By default, once you [establish the actual server connection](#connecting-to-the-server), **all incoming server events _are_ forwarded to the client.** This combines nicely with the explicit intention to connect to the actual server, as there's little reason to do so without also wishing to receive the server events. +By default, once you [establish the actual server connection](#connecting-to-the-server), **all incoming server events are _forwarded to the client_.** You can opt-out from this behavior by calling `event.preventDefault()` on the server message event. -> Learn how to opt-out from this behavior and [modify or prevent server events](#preventing-server-events). +> Learn more about [server-to-client forwarding](#server-to-client-forwarding). ## Client events @@ -183,7 +187,7 @@ import { CubeTransparentIcon } from '@heroicons/react/24/outline' ## Server events -### Connecting to the server +### Establishing server connection import { Warning } from '../../../components/react/warning' @@ -207,6 +211,23 @@ chat.on('connection', ({ server }) => { description="The `server.connect()` API." /> +### Client-to-server forwarding + +Once the server connection has been established, all outgoing client message events are forwarded to the server. To prevent this behavior, call `event.preventDefault()` on the client message event. You can use this to modify the client-sent data before it reaches the server or ignore it completely. + +```js +chat.on('connection', ({ client }) => { + client.addEventListener('message', (event) => { + // Prevent the default client-to-server forwarding. + event.preventDefault() + + // Modify the original client-sent data and send + // it to the server instead. + server.send(event.data + 'mocked') + }) +}) +``` + ### Intercepting server events To intercept an incoming event from the actual sever, grab the `server` object from the `"connection"` event listener argument and add a `"message"` event listener on that object. @@ -221,9 +242,9 @@ chat.on('connection', ({ server }) => { Now, whenever the actual server sends data, the `"message"` listener in this handler will be called. The listener exposes a single `event` argument, which is a [`MessageEvent`](https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent) received from the client, with the sent data available as `event.data`. -### Preventing server events +### Server-to-client forwarding -By default, all server events are forwarded to the connected client. You can opt-out from this behavior by preventing the received server `"message"` event. This is handy if you wish to modify the server-sent data before it reaches the client or prevent some server events from arriving at the client completely. +By default, all server events are forwarded to the connected client. You can opt-out from this behavior by calling `event.preventDefault()` on the server message event. This is handy if you wish to modify the server-sent data before it reaches the client or prevent some server events from arriving at the client completely. ```js {4} chat.on('connection', ({ client, server }) => { From fe9890b08e356b197bbf4ac58f143303bf37462f Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 31 Jul 2024 18:50:54 +0200 Subject: [PATCH 08/16] fix websocket docs --- websites/mswjs.io/src/content/docs/api/ws.mdx | 23 +++++++++- .../docs/basics/handling-websocket-events.mdx | 45 +++++++------------ 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/api/ws.mdx b/websites/mswjs.io/src/content/docs/api/ws.mdx index fc16de8e..9e50d58b 100644 --- a/websites/mswjs.io/src/content/docs/api/ws.mdx +++ b/websites/mswjs.io/src/content/docs/api/ws.mdx @@ -114,12 +114,22 @@ In this example, the WebSocket connection to `wss://chat.example.com` emits the ## `WebSocketClientConnection` -The `WebSocketClientConnection` object represents an intercepted WebSocket client connection. +The `WebSocketClientConnection` object represents an intercepted WebSocket client connection from the _server's_ perspective. This means that the `message` event on the client stands for a message _sent_ by the client and received by the "server". ### `.addEventListener(event, listener, options)` +Adds a listener to the given client event. These are the supported client events: + +| Event name | Description | +| ---------- | --------------------------------------------------------------------- | +| `message` | Dispatched when this client _sends_ a message. | +| `error` | Dispatched when this client connection has been closed with an error. | +| `close` | Dispatched when this client is closed (e.g. by your application). | + ### `.removeEventListener(event, listener, options)` +Removes the listener for the given client event. + ### `.send(data)` - `data: string | Blob | ArrayBuffer` @@ -181,8 +191,19 @@ api.on('connection', ({ server }) => { ### `.addEventListener(event, listener, options)` +Adds a listener to the original server WebSocket connection. The supported events are: + +| Event name | Description | +| ---------- | ----------------------------------------------------------------------------- | +| `open` | Dispatched when the connection to the original server has been opened. | +| `message` | Dispatched when the original server _sends_ a message. | +| `error` | Dispatched when the original server connection has been closed with an error. | +| `close` | Dispatched when the original server connection has been closed. | + ### `.removeEventListener(event, listener, options)` +Removes the listener for the given server event. + ### `.send(data)` - `data: string | Blob | ArrayBuffer` diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index b41629a5..c6b14a10 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -8,22 +8,26 @@ keywords: - ws --- -MSW supports intercepting and mocking WebSocket connections using its designated [`ws` API](/docs/api/ws). This page will guide you through the basics of handling WebSocket events, explain the mental model behind MSW when intercepting duplex connections, and elaborate on the defaults the library ships to promote good developer experience. +MSW supports intercepting and mocking WebSocket connections using its designated [`ws` API](/docs/api/ws). This page will guide you through the basics of handling WebSocket events, explain the mental model behind MSW when working with duplex connections, and elaborate on the defaults the library ships to ensure great developer experience. ## Respecting standards Mock Service Worker is dedicated to respecting, promoting, and teaching you about the web standards. The way you intercept and mock WebSocket communications will be according to the [WHATWG WebSocket Standard](https://websockets.spec.whatwg.org/), which means treating clients as `EventTarget`, listening to events like `"message"` and `"close"`, and reading the sent and received data from the `MessageEvent` objects. -**We do not plan to support custom WebSocket protocols**, such as those using HTTP polling. Those are proprietary to the third-party tooling that implements them, and there is no reliable way for MSW to intercept such protocols without introducing non-standard, library-specific logic. +**We have no plans of supporting custom WebSocket protocols**, such as those using HTTP polling or XMLHttpRequest. Those are proprietary to the third-party tooling that implements them, and there is no reliable way for MSW to intercept such protocols without introducing non-standard, library-specific logic. -That being said, we acknowledge that the standard `WebSocket` interface is rarely used in production systems as-is. Often, it's used as the underlying implementation detail for more convenient third-party abstractions like SocketIO or PartyKit. We firmly believe in mock-as-you-use philosophy and want to provide you with the mocking experience that resembles the actual usage of the third-party libraries you may be relying on through the concept of [Bindings](#bindings). +That being said, we acknowledge that the standard `WebSocket` interface is rarely used in production systems as-is. Often, it's used as the underlying implementation detail for more convenient third-party abstractions, like SocketIO or PartyKit. We aim to address that experience gap via [Bindings](#bindings). + +import { Info } from '@mswjs/shared/components/react/info' + +Foo ## Event types -Unlike HTTP, a WebSocket communication is _duplex_, which means that the client and the server may send events independently. There are two types of events you can handle with MSW: +A WebSocket communication is _duplex_, which means that both the client and the server may send and receive events independently and simultaneously. There are two types of events you can handle with MSW: -- **Outgoing client events**. These are the events the client sends via `.send()`; -- **Incoming server events**. These are the events the server sends and the client receives via its `"message"` event listener. +- **Outgoing client events**. These are the events your application sends to the WebSocket server; +- **Incoming server events**. These are the events the original server sends and the client receives via its `"message"` event listener. ## Intercepting connections @@ -81,7 +85,7 @@ By default, once you [establish the actual server connection](#connecting-to-the ### Intercepting client events -To intercept an outgoing client event, grab the `client` object from the `"connection"` event listener argument and add a `"message"` listener on that object. +To intercept an outgoing client event, get the `client` object from the `"connection"` event listener argument and add a `"message"` listener on that object. ```js /client/1,2 {2-4} chat.on('connection', ({ client }) => { @@ -95,7 +99,7 @@ Now, whenever a WebSocket client sends data via the `.send()` method, the `"mess ### Sending data to the client -To send data to the connected client, grab the `client` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send. +To send data to the connected client, get the `client` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send. ```js /client/1,2 {2} chat.on('connection', ({ client }) => { @@ -196,7 +200,7 @@ import { Warning } from '@mswjs/shared/components/react/warning' that server first. -To establish the connection to the actual WebSocket server, grab the `server` object from the `"connection"` event listener argument and call its `.connect()` method. +To establish the connection to the actual WebSocket server, get the `server` object from the `"connection"` event listener argument and call its `.connect()` method. ```js /server/ {2} chat.on('connection', ({ server }) => { @@ -230,7 +234,7 @@ chat.on('connection', ({ client }) => { ### Intercepting server events -To intercept an incoming event from the actual sever, grab the `server` object from the `"connection"` event listener argument and add a `"message"` event listener on that object. +To intercept an incoming event from the actual sever, get the `server` object from the `"connection"` event listener argument and add a `"message"` event listener on that object. ```js /server/1,2 {2-4} chat.on('connection', ({ server }) => { @@ -261,7 +265,7 @@ chat.on('connection', ({ client, server }) => { ### Sending data to the server -To send data to the actual server, grab the `server` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send to the server. +To send data to the actual server, get the `server` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send to the server. ```js /server/ {2} chat.on('connection', ({ server }) => { @@ -278,25 +282,6 @@ This is equivalent to a client sending that data to the server. description="The `server.send()` API." /> -### Client-to-server forwarding - -By default, the actual server will not receive any outgoing client events—they will short-circuit on your event handler's level. If you wish to forward client-to-server events, establish the actual server connection by calling `server.connect()`, listen to the outgoing events via the `"message"` event listener on the `client` object, and use the `server.send()` method to forward the data. - -```js {2-3,5-9} -chat.on('connection', ({ client, server }) => { - // Establish the actual server connection. - server.connect() - - // Listen to all outgoing client events. - client.addEventListener('message', (event) => { - // And send them to the actual server as-is. - server.send(event.data) - }) -}) -``` - -> You can control what messages to forward to the actual server in the `"message'` event listener on the `client` object. Feel free to introduce conditions, analyze the message, or modify the data to forward. - ## Logging Since MSW implements the WebSocket interception mock-first, no actual connections will be established until you explicitly say so. This means that the mocked scenarios won't appear as network entries in your browser's DevTools and you won't be able to observe them. From c17237df45c62e84ef5e7ea62731e25a5f23950a Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Fri, 20 Sep 2024 18:45:59 +0200 Subject: [PATCH 09/16] update ws api, include event flow section --- websites/mswjs.io/src/content/docs/api/ws.mdx | 50 +++++--- .../docs/basics/handling-websocket-events.mdx | 112 ++++++++++++++++-- 2 files changed, 132 insertions(+), 30 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/api/ws.mdx b/websites/mswjs.io/src/content/docs/api/ws.mdx index 9e50d58b..0cdac4b7 100644 --- a/websites/mswjs.io/src/content/docs/api/ws.mdx +++ b/websites/mswjs.io/src/content/docs/api/ws.mdx @@ -14,7 +14,7 @@ The `ws` namespace helps you create event handlers to intercept WebSocket connec ## Call signature -The `ws` namespace only exposes a single method called `link()`. The `link()` method creates an event handler that intercepts the WebSocket connection to the specified URL. +The `ws` namespace exposes a method called `link()`. The `link()` method creates a WebSocket link preconfigured to handle WebSocket connections matching the specified URL. ```ts ws.link(url: string | URL | RegExp) @@ -32,9 +32,9 @@ import { CodeBracketSquareIcon } from '@heroicons/react/24/outline' ## Event handler -The object returned from the `ws.link()` call is referred to as _event handler_. The event handler object has the following properties and methods: +The object returned from the `ws.link()` call is referred to as a _WebSocket link_. The link has the following properties and methods: -### `.on(event, listener)` +### `.addEventListener(event, listener)` Adds a [connection listener](#connection-listener) for the outgoing WebSocket client connections. @@ -65,7 +65,7 @@ Sends the given data to all active WebSocket clients except the given `clients`. ```js {4} const api = ws.link('wss://*') -api.on('connection', ({ client }) => { +api.addEventListener('connection', ({ client }) => { api.broadcastExcept(client, 'all except this') }) ``` @@ -82,11 +82,12 @@ api.broadcastExcept(ignoredClients, 'hello') ## Connection listener -| Argument | Type | Description | -| -------- | -------- | ---------------------------------------------------- | -| `client` | `object` | Outgoing WebSocket client connection object. | -| `server` | `object` | Actual WebSocket server connection object. | -| `params` | `object` | Path parameters extracted from the connection `url`. | +| Argument | Type | Description | +| -------- | --------------------------------------------------------- | ---------------------------------------------------- | +| `client` | [`WebSocketClientConnection`](#websocketclientconnection) | Outgoing WebSocket client connection object. | +| `server` | [`WebSocketServerConnection`](#websocketserverconnection) | Actual WebSocket server connection object. | +| `params` | `Record` | Path parameters extracted from the connection `url`. | +| `info` | [`WebSocketConnectionInfo`](#websocketconnectioninfo) | Extra information about this WebSocket connection. | The connection listener is called on every outgoing WebSocket client connection. @@ -97,9 +98,9 @@ import { setupWorker } from 'msw/browser' const api = ws.link('wss://chat.example.com') const worker = setupWorker( - api.on('connection', () => { + api.addEventListener('connection', () => { console.log('client connected!') - }) + }), ) await worker.start() @@ -137,7 +138,7 @@ Removes the listener for the given client event. Sends data to the WebSocket client. This is equivalent to the client receiving that data from the server. ```js {2-4} -api.on('connection', ({ client }) => { +api.addEventListener('connection', ({ client }) => { client.send('hello') client.send(new Blob(['hello'])) client.send(new TextEncoder().encode('hello')) @@ -152,7 +153,7 @@ api.on('connection', ({ client }) => { Closes the active WebSocket client connection. ```js {2} -api.on('connection', ({ client }) => { +api.addEventListener('connection', ({ client }) => { client.close() }) ``` @@ -160,7 +161,7 @@ api.on('connection', ({ client }) => { Unlike the `WebSocket.prototype.close()` method, the `client.close()` method accepts non-configurable close codes. This allows you to emulate client close scenarios based on server-side errors. ```js {3} -api.on('connection', ({ client }) => { +api.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { client.close(1003, 'Invalid data') }) @@ -170,7 +171,7 @@ api.on('connection', ({ client }) => { You can also implement custom close code and reason: ```js {2} -api.on('connection', ({ client }) => { +api.addEventListener('connection', ({ client }) => { client.close(4000, 'Custom close reason') }) ``` @@ -184,7 +185,7 @@ The `WebSocketServerConnection` object represents the actual WebSocket server co Establishes connection to the actual WebSocket server. ```js {2} -api.on('connection', ({ server }) => { +api.addEventListener('connection', ({ server }) => { server.connect() }) ``` @@ -211,7 +212,7 @@ Removes the listener for the given server event. Sends data to the actual WebSocket server. This is equivalent to the client sending this data to the server. ```js {6} -api.on('connection', ({ server }) => { +api.addEventListener('connection', ({ server }) => { server.connect() server.addEventListener('message', (event) => { @@ -221,3 +222,18 @@ api.on('connection', ({ server }) => { }) }) ``` + +## `WebSocketConnectionInfo` + +The `info` argument on the `connection` event listener contains additional WebSocket connection infromation. + +| Property name | Type | Description | +| `protocols` | `string | string[] | undefined` | The list of protocols used when establishing this WebSocket connection. | + +```js +api.addEventListener('connection', ({ info }) => { + if (info.protocols?.includes('chat')) { + // ... + } +}) +``` diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index c6b14a10..7de32250 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -53,7 +53,7 @@ Next, add an event handler to the list of your handlers: ```js {2-4} export const handlers = [ - chat.on('connection', () => { + chat.addEventListener('connection', () => { console.log('outgoing WebSocket connection') }), ] @@ -88,7 +88,7 @@ By default, once you [establish the actual server connection](#connecting-to-the To intercept an outgoing client event, get the `client` object from the `"connection"` event listener argument and add a `"message"` listener on that object. ```js /client/1,2 {2-4} -chat.on('connection', ({ client }) => { +chat.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { console.log('from client:', event.data) }) @@ -102,7 +102,7 @@ Now, whenever a WebSocket client sends data via the `.send()` method, the `"mess To send data to the connected client, get the `client` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send. ```js /client/1,2 {2} -chat.on('connection', ({ client }) => { +chat.addEventListener('connection', ({ client }) => { client.send('Hello from the server!') }) ``` @@ -121,7 +121,7 @@ chat.on('connection', ({ client }) => { To broadcast data to all connected clients, use the `.broadcast()` method on the event handler object (the one returned from the `ws.link()` call) and provide it with the data you wish to broadcast. ```js /chat/2 {2} -chat.on('connection', () => { +chat.addEventListener('connection', () => { chat.broadcast('Hello everyone!') }) ``` @@ -129,7 +129,7 @@ chat.on('connection', () => { You can also broadcast data to all clients except a subset of clients by using the `.boardcastExcept()` method on the event handler object. ```js {3,6} -chat.on('connection', ({ client }) => { +chat.addEventListener('connection', ({ client }) => { // Broadcast data to all clients except the current one. chat.broadcastExcept(client, 'Hello everyone except you!') @@ -159,7 +159,7 @@ chat.on('connection', ({ client }) => { You can close an existing client connection at any time by calling `client.close()`. ```js {2} -chat.on('connection', ({ client }) => { +chat.addEventListener('connection', ({ client }) => { client.close() }) ``` @@ -167,7 +167,7 @@ chat.on('connection', ({ client }) => { By default, the `.close()` method will result in a graceful closure of the connection (1000 code). You can control the nature of the connection closure by providing the custom `code` and `reason` arguments to the `.close()` method. ```js /1003/ {4} -chat.on('connection', ({ client }) => { +chat.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { if (event.data === 'hello') { client.close(1003) @@ -203,7 +203,7 @@ import { Warning } from '@mswjs/shared/components/react/warning' To establish the connection to the actual WebSocket server, get the `server` object from the `"connection"` event listener argument and call its `.connect()` method. ```js /server/ {2} -chat.on('connection', ({ server }) => { +chat.addEventListener('connection', ({ server }) => { server.connect() }) ``` @@ -220,7 +220,7 @@ chat.on('connection', ({ server }) => { Once the server connection has been established, all outgoing client message events are forwarded to the server. To prevent this behavior, call `event.preventDefault()` on the client message event. You can use this to modify the client-sent data before it reaches the server or ignore it completely. ```js -chat.on('connection', ({ client }) => { +chat.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { // Prevent the default client-to-server forwarding. event.preventDefault() @@ -237,7 +237,7 @@ chat.on('connection', ({ client }) => { To intercept an incoming event from the actual sever, get the `server` object from the `"connection"` event listener argument and add a `"message"` event listener on that object. ```js /server/1,2 {2-4} -chat.on('connection', ({ server }) => { +chat.addEventListener('connection', ({ server }) => { server.addEventListener('message', (event) => { console.log('from server:', event.data) }) @@ -251,7 +251,7 @@ Now, whenever the actual server sends data, the `"message"` listener in this han By default, all server events are forwarded to the connected client. You can opt-out from this behavior by calling `event.preventDefault()` on the server message event. This is handy if you wish to modify the server-sent data before it reaches the client or prevent some server events from arriving at the client completely. ```js {4} -chat.on('connection', ({ client, server }) => { +chat.addEventListener('connection', ({ client, server }) => { server.addEventListener('message', (event) => { // Prevent the default server-to-client forwarding. event.preventDefault() @@ -268,7 +268,7 @@ chat.on('connection', ({ client, server }) => { To send data to the actual server, get the `server` object from the `"connection"` event listener argument and call its `.send()` method with the data you wish to send to the server. ```js /server/ {2} -chat.on('connection', ({ server }) => { +chat.addEventListener('connection', ({ server }) => { server.send('hello from client!') } ``` @@ -390,6 +390,92 @@ A mocked message sent to the client from the event handler via `client.send()`. An incoming message from the actual server. Requires the actual server connection to be opened via `server.connect()`. The incoming server messages can be modified or skipped by the event handler, thus the icon is dotted. +## Event flow + +Much like the WebSocket communication, handling it with MSW is event-based. Your experience mocking WebSockets will involve the understanding of `EventTarget` and how events work in JavaScript. Let's have a quick reminder. + +When your application establishes a WebSocket connection, the `connection` event will be emitted on _all matching WebSocket links_. + +```js {4,5} +const chat = ws.link('wss://example.com') + +export const handlers = [ + chat.addEventListener('connection', () => console.log('This is called')), + chat.addEventListener('connection', () => console.log('This is also called')), +] +``` + +This way, both the happy path and the runtime handlers can react to the same connection. + +The client/server events are dispatched on _all_ `client` and `server` objects of the same connection as well. Meanwhile, you can attach multiple listeners to the same object, or to different objects across different handlers. + +```js {3-5,8-11} +export const handlers = [ + chat.addEventListener('connection', ({ client }) => { + // Attaching multiple message listeners to the same `client` object. + client.addEventListener('message', () => console.log('This is called')) + client.addEventListener('message', () => console.log('This is also called')) + }), + chat.addEventListener('connection', ({ client }) => { + // Attaching another message listener to a different `client` object. + client.addEventListener('message', () => + console.log('Hey, this gets called too!'), + ) + }), +] +``` + +Since these events are dispatched against the same event target, you can utilize that to _prevent_ them. That comes in handy when creating runtime handlers (i.e. network behavior overrides), as you can control whether your override _augments_ or _completely overrides_ particular event handling. + +```js {4-6,15-19} +const server = setupServer( + chat.addEventListener('connection', ({ client }) => { + client.addEventListener('message', (event) => { + // In a happy path handler, send back the event data + // received from the WebSocket client. + client.send(event.data) + }) + }), +) + +it('handles error payload', async () => { + server.use( + chat.addEventListener('connection', ({ client }) => { + client.addEventListener('message', (event) => { + // In this runtime handler, prevent the "message" client from + // propagating to another event target (the happy path handler). + // Then, send a completely different message to the client. + event.preventPropagation() + client.send('error-payload') + }) + }), + ) +}) +``` + +> Omitting `event.preventPropagation()` will result in _two_ messages being sent to the client upon receiving the same event—the `'error-payload'` first, then the original `event.data` second. + +Just like with a regular `EventTarget`, you can utilize `event.preventImmediatePropagation()` to stop an event from propagating across sibling listeners. For example, when handling a particular WebSocket event, you can use that to short-circuit any other event listeners that would otherwise be called. + +```js {4-6,12} +chat.addEventListener('connection', ({ client }) => { + client.addEventListener('message', (event) => { + if (event.data === 'special-scenario') { + // This prevents this "message" event from propagating + // to the "message" event listener below. + event.stopImmediatePropagation() + client.close() + } + }) + + client.addEventListener('message', (event) => { + client.send(event.data) + }) +}) +``` + +> If the client sends a `'special-scenario'` payload in the message, it will be closed, and the `client.send(event.data)` logic from the second event listener will never be called. + ## Bindings To provide a more familiar experience when mocking third-party WebSocket clients, MSW uses _bindings_. A binding is a wrapper over the standard `WebSocket` class that encapsulates the third-party-specific behaviors, such as message parsing, and gives you a public API similar to that of the bound third-party library. @@ -403,7 +489,7 @@ import { bind } from '@mswjs/socket.io-binding' const chat = ws.link('wss://chat.example.com') export const handlers = [ - chat.on('connection', (connection) => { + chat.addEventListener('connection', (connection) => { const io = bind(connection) io.client.on('hello', (username) => { From ba270bfa04d96dbdcde9b329dcfc45be54380db3 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Fri, 20 Sep 2024 19:05:37 +0200 Subject: [PATCH 10/16] add type safety section --- .../docs/basics/handling-websocket-events.mdx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index 7de32250..472f0799 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -476,6 +476,61 @@ chat.addEventListener('connection', ({ client }) => { > If the client sends a `'special-scenario'` payload in the message, it will be closed, and the `client.send(event.data)` logic from the second event listener will never be called. +## Type safety + +Bringing type safety to the WebSocket communication is essential when using TypeScript, and that includes your handlers too! That being said, MSW intentionally doesn't support any type arguments to annotate the outgoing/incoming events. The reasoning behing that is twofold: + +1. Just because you narrow down the data type doesn't mean a different data type cannot be transferred over the network (the classic types vs runtime debate); +1. It is common to send stringified payload over the WebSocket protocol, which implies the need of parsing it anyway. + +You can achieve a proper type and runtime safety in WebSockets by introducing parsing utilities. Libraries like [Zod](https://github.com/colinhacks/zod) can help you greatly in achieving type and runtime safety. + +```js +import { z } from 'zod' + +// Define a Zod schema for the incoming events. +// Here, our WebSocket communication supports two +// events: "chat/join" and "chat/message". +const incomingSchema = z.union([ + z.object({ + type: z.literal('chat/join'), + user: userSchema, + }), + z.object({ + type: z.literal('chat/message'), + message: z.object({ + text: z.string(), + sentAt: z.string().datetime(), + }), + }), +]) + +chat.addEventListener('connection', ({ client, server }) => { + client.addEventListener('message', (event) => { + const result = incomingSchema.safeParse(event.data) + const message = result.data + + // Ignore non-matching events. + if (!message) { + return + } + + // Handle incoming events in type-safe way. + switch (result.success.type) { + case 'chat/join': { + // ... + break + } + + case 'chat/message': { + // ... + break + } + } + }) +}) +``` + ## Bindings To provide a more familiar experience when mocking third-party WebSocket clients, MSW uses _bindings_. A binding is a wrapper over the standard `WebSocket` class that encapsulates the third-party-specific behaviors, such as message parsing, and gives you a public API similar to that of the bound third-party library. From 8877766e6a3d7a188080855a9be7d0e9116a08d3 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 24 Sep 2024 10:55:46 +0200 Subject: [PATCH 11/16] remove "Foo" leftover --- .../src/content/docs/basics/handling-websocket-events.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index 472f0799..ecaed50f 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -18,10 +18,6 @@ Mock Service Worker is dedicated to respecting, promoting, and teaching you abou That being said, we acknowledge that the standard `WebSocket` interface is rarely used in production systems as-is. Often, it's used as the underlying implementation detail for more convenient third-party abstractions, like SocketIO or PartyKit. We aim to address that experience gap via [Bindings](#bindings). -import { Info } from '@mswjs/shared/components/react/info' - -Foo - ## Event types A WebSocket communication is _duplex_, which means that both the client and the server may send and receive events independently and simultaneously. There are two types of events you can handle with MSW: From 95ed37993887ba2345c6ceccd0ae99f15502e7d0 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 24 Sep 2024 11:33:26 +0200 Subject: [PATCH 12/16] simplify ws logging, use text-primary vs text-orange --- .../docs/basics/handling-websocket-events.mdx | 67 +++++++++++++++---- websites/shared/styles/shared.css | 6 ++ 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index ecaed50f..a347a5b2 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -282,18 +282,59 @@ This is equivalent to a client sending that data to the server. Since MSW implements the WebSocket interception mock-first, no actual connections will be established until you explicitly say so. This means that the mocked scenarios won't appear as network entries in your browser's DevTools and you won't be able to observe them. -MSW enables custom logging for both mocked and original WebSocket connections **in the browser** to allow you to: +MSW enables custom logging for both mocked and original WebSocket connections **in the browser** that allows you to: - Observe any WebSocket connection if you have an event handler for it; -- See previews for binary messages (Blob/ArrayBuffer); +- See previews for binary messages (`Blob`/`ArrayBuffer`); - Tell apart the messages sent by your application/original server from those initiated by the event handler (i.e. mocked messages). -### Message colors and styles - -All the printed messages are color-graded by the following criteria: system events, outgoing -messages, incoming -messages, and mocked -events. In addition to the colors, some messages are represented by solid (↑↓) or dotted icons (⇡⇣), indicating whether the event occurred in your application or in the event handler, respectively. +### Message grading + +All themessages printed by MSW are graded by three criteria: _icon_, _stroke_, and _color_. + +#### Icon + +- System events: + - ▶ Connection open; + - ■ Connection closed; + - × Connection error. +- Message events: + - ↑ Outgoing message (i.e. sent by the client); + - ↓ Incoming message (i.e. received by the client). + +#### Stroke + +Message icons can have a different stroke (solid or dashed) representing the origin of those messages. Solid-stroke icons stand for the end events sent or received by your WebSocket client. Dashed icons stand for mocked events sent or received from the event handler. + +- ↑ Outgoing message sent by the actual client; +- ⇡ Outgoing message sent by the event handler (i.e. mock); +- ↓ Incoming message from the original server; +- ⇣ Incoming message from the event handler (i.e. mock). + +#### Color + +To help read the log output, all the icons are also color-graded based on their type: + +-

+ System + events (,{' '} + ,{' '} + ×); +

+-

+ Outgoing + client messages (,{' '} + ); +

+-

+ Incoming + client messages (,{' '} + ); +

+-

+ Mock + events (either outgoing or incoming). +

### Connection events @@ -325,10 +366,10 @@ Dispatched when the connection is closed (i.e. the WebSocket client emits the `c Any message, be it outgoing or incoming message, follows the same structure: -```txt /▼ timestamp/ /▲ icon/ /▼ sent data/ /▼ data length/ - ▼ timestamp ▼ sent data ▼ data length (bytes) +```txt /00:00:00.000/#g /↑/#v /hello from client/ /17/#b + timestamp sent data [MSW] 00:00:00.000 ↑ hello from client 17 - ▲ icon + icon byte length ``` Binary messages print a text preview of the sent binary alongside its full byte length: @@ -354,7 +395,7 @@ Long text messages and text previews are truncated: A message sent by the client in your application. -#### Outgoing mocked client message +#### Outgoing mocked client message ``` [MSW] 12:34:56.789 ⇡ hello from mock 15 @@ -370,7 +411,7 @@ A message sent from the client by the event handler (via `server.send()`). Requi The end message the client received (i.e. the message that triggered the "message" event on the WebSocket client). The message can be either from the event handler or from the actual WebSocket server. -#### Incoming mocked client message +#### Incoming mocked client message ``` [MSW] 12:34:56.789 ⇣ hello from mock 15 diff --git a/websites/shared/styles/shared.css b/websites/shared/styles/shared.css index b9b9ae75..4a46cc53 100644 --- a/websites/shared/styles/shared.css +++ b/websites/shared/styles/shared.css @@ -148,6 +148,12 @@ code[data-line-numbers-max-digits='3'] > [data-line]::before { [data-highlighted-chars][data-chars-id='v'] { @apply bg-violet-700 border-violet-400 !text-violet-100; } +[data-highlighted-chars][data-chars-id='g'] { + @apply bg-green-700 border-green-400 !text-green-100; +} +[data-highlighted-chars][data-chars-id='b'] { + @apply bg-blue-700 border-blue-400 !text-blue-100; +} /* Sections */ #mobile-menu-container { From 0e4c0d870852efdd888c00ceb7f4d81a26339b13 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 24 Sep 2024 11:53:27 +0200 Subject: [PATCH 13/16] improve ws type-safety section --- .../docs/basics/handling-websocket-events.mdx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index a347a5b2..f4b89f32 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -515,10 +515,21 @@ chat.addEventListener('connection', ({ client }) => { ## Type safety -Bringing type safety to the WebSocket communication is essential when using TypeScript, and that includes your handlers too! That being said, MSW intentionally doesn't support any type arguments to annotate the outgoing/incoming events. The reasoning behing that is twofold: +Bringing type safety to the WebSocket communication is essential when using TypeScript, and that includes your handlers too! That being said, MSW intentionally doesn't support any type arguments to annotate the outgoing/incoming events: -1. Just because you narrow down the data type doesn't mean a different data type cannot be transferred over the network (the classic types vs runtime debate); -1. It is common to send stringified payload over the WebSocket protocol, which implies the need of parsing it anyway. +```ts +import { ws } from 'msw' + +ws.link(url) +// ^^^^^^^^^^^^ Type error! +``` + +The reasoning behing this decision is twofold: + +1. Narrowing down the data type doesn't guarantee that a different data type wouldn't be sent over the network (the classic types vs runtime debate); +1. The `event.data` value you receive in the message event listener will always be of type `string | Blob | ArrayBuffer` because MSW provides no message parsing. + +If you are using objects to communicate with a WebSocket server, those objects have to be stringified and parsed when sending and receiving them, respectively, which already implies a parsing layer being present in your application. You can achieve a proper type and runtime safety in WebSockets by introducing parsing utilities. Libraries like [Zod](https://github.com/colinhacks/zod) can help you greatly in achieving type and runtime safety. @@ -568,6 +579,8 @@ chat.addEventListener('connection', ({ client, server }) => { }) ``` +> Feel free to introduce a higher-order listener for the message event that abstracts that parsing, helping you reuse it across your handlers. + ## Bindings To provide a more familiar experience when mocking third-party WebSocket clients, MSW uses _bindings_. A binding is a wrapper over the standard `WebSocket` class that encapsulates the third-party-specific behaviors, such as message parsing, and gives you a public API similar to that of the bound third-party library. From 0d76426e8696edf2c4286e68ffde9b1966ff4e6c Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 24 Sep 2024 17:07:38 +0200 Subject: [PATCH 14/16] fix incorrect zod parse result usage --- .../src/content/docs/basics/handling-websocket-events.mdx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index f4b89f32..9819d56f 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -556,15 +556,16 @@ const incomingSchema = z.union([ chat.addEventListener('connection', ({ client, server }) => { client.addEventListener('message', (event) => { const result = incomingSchema.safeParse(event.data) - const message = result.data // Ignore non-matching events. - if (!message) { + if (!result.success) { return } + const message = result.data + // Handle incoming events in type-safe way. - switch (result.success.type) { + switch (message.type) { case 'chat/join': { // ... break From d557db47508e0bf1c9feb432804011cf61f36e35 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Wed, 2 Oct 2024 15:11:34 +0200 Subject: [PATCH 15/16] improve websocket logging docs --- .../docs/basics/handling-websocket-events.mdx | 117 ++++++------------ websites/mswjs.io/src/styles/global.css | 6 + 2 files changed, 46 insertions(+), 77 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx index 9819d56f..c61194ab 100644 --- a/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx +++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx @@ -280,63 +280,34 @@ This is equivalent to a client sending that data to the server. ## Logging -Since MSW implements the WebSocket interception mock-first, no actual connections will be established until you explicitly say so. This means that the mocked scenarios won't appear as network entries in your browser's DevTools and you won't be able to observe them. +Since MSW implements the WebSocket interception mock-first, no actual connections will be established until you explicitly say so. This means that the mocked scenarios won't appear as network entries in your browser's DevTools and you won't be able to observe them there. -MSW enables custom logging for both mocked and original WebSocket connections **in the browser** that allows you to: +MSW provides custom logging for both mocked and original WebSocket connections **in the browser**. -- Observe any WebSocket connection if you have an event handler for it; -- See previews for binary messages (`Blob`/`ArrayBuffer`); -- Tell apart the messages sent by your application/original server from those initiated by the event handler (i.e. mocked messages). +### Reading the log output -### Message grading +The logger will print out various events occurring during the WebSocket communication as collapsed console groups in your browser's console. -All themessages printed by MSW are graded by three criteria: _icon_, _stroke_, and _color_. +**There are four types of logs you can observe:** -#### Icon +1.

+ ▶■× + System events; +

+1.

+ ⬆⇡ + Client events; +

+1.

+ ⬇⇣ + Server events. +

+1.

+ ⬆⬇ + Mocked events. +

-- System events: - - ▶ Connection open; - - ■ Connection closed; - - × Connection error. -- Message events: - - ↑ Outgoing message (i.e. sent by the client); - - ↓ Incoming message (i.e. received by the client). - -#### Stroke - -Message icons can have a different stroke (solid or dashed) representing the origin of those messages. Solid-stroke icons stand for the end events sent or received by your WebSocket client. Dashed icons stand for mocked events sent or received from the event handler. - -- ↑ Outgoing message sent by the actual client; -- ⇡ Outgoing message sent by the event handler (i.e. mock); -- ↓ Incoming message from the original server; -- ⇣ Incoming message from the event handler (i.e. mock). - -#### Color - -To help read the log output, all the icons are also color-graded based on their type: - --

- System - events (,{' '} - ,{' '} - ×); -

--

- Outgoing - client messages (,{' '} - ); -

--

- Incoming - client messages (,{' '} - ); -

--

- Mock - events (either outgoing or incoming). -

- -### Connection events +### System events #### Connection opened @@ -366,66 +337,58 @@ Dispatched when the connection is closed (i.e. the WebSocket client emits the `c Any message, be it outgoing or incoming message, follows the same structure: -```txt /00:00:00.000/#g /↑/#v /hello from client/ /17/#b +```txt /00:00:00.000/#v /⬆/#g /hello from client/ /17/#b timestamp sent data -[MSW] 00:00:00.000 ↑ hello from client 17 +[MSW] 00:00:00.000 ⬆ hello from client 17 icon byte length ``` Binary messages print a text preview of the sent binary alongside its full byte length: ``` -[MSW] 12:34:56.789 ↑ Blob(hello world) 11 -[MSW] 12:34:56.789 ↑ ArrayBuffer(preview) 7 +[MSW] 12:34:56.789 ⬆ Blob(hello world) 11 +[MSW] 12:34:56.789 ⬆ ArrayBuffer(preview) 7 ``` Long text messages and text previews are truncated: ``` -[MSW] 12:34:56.789 ↑ this is a very long stri… 17 -``` - -> You can access the full message by clicking on its console group and inspecting the original `MessageEvent` reference. - -#### Outgoing client message - -``` -[MSW] 12:34:56.789 ↑ hello from client 17 +[MSW] 12:34:56.789 ⬆ this is a very long stri… 17 ``` -A message sent by the client in your application. +> You can access the full message by clicking on its console group and inspecting the original `MessageEvent` instance. -#### Outgoing mocked client message +#### ⬆⇡ Outgoing client message ``` -[MSW] 12:34:56.789 ⇡ hello from mock 15 +[MSW] 12:34:56.789 ⬆ hello from client 17 ``` -A message sent from the client by the event handler (via `server.send()`). Requires the actual server connection to be opened via `server.connect()`. The client itself never sent this, thus the icon is dotted. +A raw message sent by the WebSocket client in your application. If the arrow is dashed, the forwarding of this message event has been prevented in the event handler. -#### Incoming client message +#### Outgoing mocked client message ``` -[MSW] 12:34:56.789 ↓ hello from server 17 +[MSW] 12:34:56.789 ⬆ hello from mock 15 ``` -The end message the client received (i.e. the message that triggered the "message" event on the WebSocket client). The message can be either from the event handler or from the actual WebSocket server. +A message sent from the client by the event handler via `server.send()`. Requires an [open server connection](#establishing-server-connection). -#### Incoming mocked client message +#### ⬇⇣ Incoming server message ``` -[MSW] 12:34:56.789 ⇣ hello from mock 15 +[MSW] 12:34:56.789 ⬇ hello from server 17 ``` -A mocked message sent to the client from the event handler via `client.send()`. The actual server has never sent this, thus the icon is dotted. +An incoming message sent from the original server. Requires an [open server connection](#establishing-server-connection). If the arrow is dashed, the forwarding of this message event has been prevented in the event handler. -#### Incoming server message +#### Incoming mocked server message ``` -[MSW] 12:34:56.789 ⇣ hello from server 17 +[MSW] 12:34:56.789 ⬇ hello from mock 15 ``` -An incoming message from the actual server. Requires the actual server connection to be opened via `server.connect()`. The incoming server messages can be modified or skipped by the event handler, thus the icon is dotted. +A mocked message sent to the client from the event handler via `client.send()`. ## Event flow diff --git a/websites/mswjs.io/src/styles/global.css b/websites/mswjs.io/src/styles/global.css index 0f2bb4bc..e8efce7a 100644 --- a/websites/mswjs.io/src/styles/global.css +++ b/websites/mswjs.io/src/styles/global.css @@ -2,6 +2,12 @@ @tailwind components; @tailwind utilities; +@layer utilities { + .no-emoji { + font-family: sans-serif; + } +} + :root { --primary: #ff6a33; } From d494dd366134497aa743858feb3e012621e0e48e Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Tue, 29 Oct 2024 12:22:30 +0100 Subject: [PATCH 16/16] use `.addEventListener` vs `.on` in "websocket.mdx" --- .../content/docs/network-behavior/websocket.mdx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/websites/mswjs.io/src/content/docs/network-behavior/websocket.mdx b/websites/mswjs.io/src/content/docs/network-behavior/websocket.mdx index e76988ea..f0059301 100644 --- a/websites/mswjs.io/src/content/docs/network-behavior/websocket.mdx +++ b/websites/mswjs.io/src/content/docs/network-behavior/websocket.mdx @@ -64,7 +64,7 @@ import { ws } from 'msw' const chat = ws.link('wss://chat.example.com') export const handlers = [ - chat.on('connection', ({ client }) => { + chat.addEventListener('connection', ({ client }) => { console.log('Intercepted a WebSocket connection:', client.url) }), ] @@ -89,7 +89,7 @@ import { ws } from 'msw' const chat = ws.link('wss://chat.example.com') export const handlers = [ - chat.on('connection', ({ client }) => { + chat.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { console.log('client sent:', event.data) }) @@ -101,9 +101,7 @@ Now that we know when the client sends a message in the chat, we can send data b To send data from the server to the client, we can use the `client.send()` method provided by the `client` object. - - Call `client.send()` to send data to the client: - +Call `client.send()` to send data to the client: ```js /client.send/ {9} // src/mocks/handlers.js @@ -112,7 +110,7 @@ import { ws } from 'msw' const chat = ws.link('wss://chat.example.com') export const handlers = [ - chat.on('connection', ({ client }) => { + chat.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { client.send('hello from server!') }) @@ -139,7 +137,7 @@ import { ws } from 'msw' const chat = ws.link('wss://chat.example.com') export const handlers = [ - chat.on('connection', ({ client }) => { + chat.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { chat.broadcast(event.data) }) @@ -152,7 +150,8 @@ When using the `.broadcast()` method of the event handler, _all the connected cl To broadcast data to all clients except a subset of clients, use the `.broacastExcept()` method on the event handler object. - Call `chat.broadcastExcept()` to broadcast the message to all clients except the initial sender: + Call `chat.broadcastExcept()` to broadcast the message to all clients except + the initial sender: ```js /chat.broadcastExcept/ {9} @@ -162,7 +161,7 @@ import { ws } from 'msw' const chat = ws.link('wss://chat.example.com') export const handlers = [ - chat.on('connection', ({ client }) => { + chat.addEventListener('connection', ({ client }) => { client.addEventListener('message', (event) => { chat.broadcastExcept(client, event.data) })