diff --git a/websites/mswjs.io/src/components/react/action.tsx b/websites/mswjs.io/src/components/react/action.tsx
index 6d3753d7..1cdf8f2f 100644
--- a/websites/mswjs.io/src/components/react/action.tsx
+++ b/websites/mswjs.io/src/components/react/action.tsx
@@ -3,8 +3,8 @@ import { ArrowRightCircleIcon } from '@heroicons/react/24/solid'
export function Action({ children }: { children: ReactNode }) {
return (
-
-
+
+
{children}
)
diff --git a/websites/mswjs.io/src/content/docs/api/ws.mdx b/websites/mswjs.io/src/content/docs/api/ws.mdx
new file mode 100644
index 00000000..0cdac4b7
--- /dev/null
+++ b/websites/mswjs.io/src/content/docs/api/ws.mdx
@@ -0,0 +1,239 @@
+---
+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 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)
+```
+
+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 a _WebSocket link_. The link has the following properties and methods:
+
+### `.addEventListener(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.addEventListener('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` | [`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.
+
+```js {7-9}
+import { ws } from 'msw'
+import { setupWorker } from 'msw/browser'
+
+const api = ws.link('wss://chat.example.com')
+
+const worker = setupWorker(
+ api.addEventListener('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 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`
+
+Sends data to the WebSocket client. This is equivalent to the client receiving that data from the server.
+
+```js {2-4}
+api.addEventListener('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.addEventListener('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.addEventListener('connection', ({ client }) => {
+ client.addEventListener('message', (event) => {
+ client.close(1003, 'Invalid data')
+ })
+})
+```
+
+You can also implement custom close code and reason:
+
+```js {2}
+api.addEventListener('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.addEventListener('connection', ({ server }) => {
+ server.connect()
+})
+```
+
+### `.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`
+
+Sends data to the actual WebSocket server. This is equivalent to the client sending this data to the server.
+
+```js {6}
+api.addEventListener('connection', ({ server }) => {
+ server.connect()
+
+ server.addEventListener('message', (event) => {
+ if (event.data === 'hello from server') {
+ server.send('hello from client')
+ }
+ })
+})
+```
+
+## `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
new file mode 100644
index 00000000..c61194ab
--- /dev/null
+++ b/websites/mswjs.io/src/content/docs/basics/handling-websocket-events.mdx
@@ -0,0 +1,581 @@
+---
+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 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 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 aim to address that experience gap via [Bindings](#bindings).
+
+## 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:
+
+- **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
+
+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.addEventListener('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 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, 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 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_.** You can opt-out from this behavior by calling `event.preventDefault()` on the server message event.
+
+> Learn more about [server-to-client forwarding](#server-to-client-forwarding).
+
+## Client events
+
+### Intercepting client events
+
+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.addEventListener('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, 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.addEventListener('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.addEventListener('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.addEventListener('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!")
+})
+```
+
+
+
+
+
+### Closing client connections
+
+You can close an existing client connection at any time by calling `client.close()`.
+
+```js {2}
+chat.addEventListener('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.addEventListener('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
+
+### Establishing server connection
+
+import { Warning } from '@mswjs/shared/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, get the `server` object from the `"connection"` event listener argument and call its `.connect()` method.
+
+```js /server/ {2}
+chat.addEventListener('connection', ({ server }) => {
+ server.connect()
+})
+```
+
+
+
+### 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.addEventListener('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, 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.addEventListener('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`.
+
+### Server-to-client forwarding
+
+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.addEventListener('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, 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.addEventListener('connection', ({ server }) => {
+ server.send('hello from client!')
+}
+```
+
+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 there.
+
+MSW provides custom logging for both mocked and original WebSocket connections **in the browser**.
+
+### Reading the log output
+
+The logger will print out various events occurring during the WebSocket communication as collapsed console groups in your browser's console.
+
+**There are four types of logs you can observe:**
+
+1.
+ ▶■×
+ System events;
+
+1.
+ ⬆⇡
+ Client events;
+
+1.
+ ⬇⇣
+ Server events.
+
+1.
+ ⬆⬇
+ Mocked events.
+
+
+### System 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 /00:00:00.000/#v /⬆/#g /hello from client/ /17/#b
+ timestamp sent data
+[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
+```
+
+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` instance.
+
+#### ⬆⇡ Outgoing client message
+
+```
+[MSW] 12:34:56.789 ⬆ hello from client 17
+```
+
+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.
+
+#### ⬆ 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 an [open server connection](#establishing-server-connection).
+
+#### ⬇⇣ Incoming server message
+
+```
+[MSW] 12:34:56.789 ⬇ hello from server 17
+```
+
+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 mocked server message
+
+```
+[MSW] 12:34:56.789 ⬇ hello from mock 15
+```
+
+A mocked message sent to the client from the event handler via `client.send()`.
+
+## 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.
+
+## 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:
+
+```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.
+
+```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)
+
+ // Ignore non-matching events.
+ if (!result.success) {
+ return
+ }
+
+ const message = result.data
+
+ // Handle incoming events in type-safe way.
+ switch (message.type) {
+ case 'chat/join': {
+ // ...
+ break
+ }
+
+ case 'chat/message': {
+ // ...
+ break
+ }
+ }
+ })
+})
+```
+
+> 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.
+
+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.addEventListener('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.
diff --git a/websites/mswjs.io/src/content/docs/network-behavior/websocket.mdx b/websites/mswjs.io/src/content/docs/network-behavior/websocket.mdx
new file mode 100644
index 00000000..f0059301
--- /dev/null
+++ b/websites/mswjs.io/src/content/docs/network-behavior/websocket.mdx
@@ -0,0 +1,213 @@
+---
+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. 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
+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 an event handler:
+
+```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 = [
+ chat.addEventListener('connection', ({ client }) => {
+ console.log('Intercepted a WebSocket connection:', client.url)
+ }),
+]
+```
+
+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 how to handle the chat messages that the client sends and mock the server responding to them.
+
+## Responding to client messages
+
+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 /client.addEventListener/ {8-10}
+// src/mocks/handlers.js
+import { ws } from 'msw'
+
+const chat = ws.link('wss://chat.example.com')
+
+export const handlers = [
+ chat.addEventListener('connection', ({ client }) => {
+ client.addEventListener('message', (event) => {
+ console.log('client sent:', event.data)
+ })
+ }),
+]
+```
+
+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.
+
+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:
+
+```js /client.send/ {9}
+// src/mocks/handlers.js
+import { ws } from 'msw'
+
+const chat = ws.link('wss://chat.example.com')
+
+export const handlers = [
+ chat.addEventListener('connection', ({ client }) => {
+ client.addEventListener('message', (event) => {
+ client.send('hello from server!')
+ })
+ }),
+]
+```
+
+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.
+
+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.
+
+## Broadcasting data
+
+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 `chat.broadcast()` to broadcast the message to all clients:
+
+
+```js /chat.broadcast/ {9}
+// src/mocks/handlers.js
+import { ws } from 'msw'
+
+const chat = ws.link('wss://chat.example.com')
+
+export const handlers = [
+ chat.addEventListener('connection', ({ client }) => {
+ client.addEventListener('message', (event) => {
+ chat.broadcast(event.data)
+ })
+ }),
+]
+```
+
+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).
+
+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:
+
+
+```js /chat.broadcastExcept/ {9}
+// src/mocks/handlers.js
+import { ws } from 'msw'
+
+const chat = ws.link('wss://chat.example.com')
+
+export const handlers = [
+ chat.addEventListener('connection', ({ client }) => {
+ client.addEventListener('message', (event) => {
+ chat.broadcastExcept(client, event.data)
+ })
+ }),
+]
+```
+
+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.
+
+## Next steps
+
+### Integrations
+
+Once you have described the network behavior you want, integrate it into any environment in your application.
+
+import { PageCard } from '../../../components/react/pageCard'
+import { WindowIcon, CommandLineIcon } from '@heroicons/react/24/outline'
+
+
+
+> 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.
+
+### Learn about handling WebSocket events
+
+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.
+
+Learn more about what you can do with WebSocket connections on this page:
+
+import { NewspaperIcon } from '@heroicons/react/24/outline'
+
+
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;
}
diff --git a/websites/shared/components/react/success.tsx b/websites/shared/components/react/success.tsx
index 72643aae..6e8d784e 100644
--- a/websites/shared/components/react/success.tsx
+++ b/websites/shared/components/react/success.tsx
@@ -4,7 +4,7 @@ export function Success({ children }: { children: ReactNode }): JSX.Element {
return (
{children}
diff --git a/websites/shared/styles/shared.css b/websites/shared/styles/shared.css
index 2f74e7ad..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 {
@@ -196,3 +202,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;
+}