vitest-websocket-mock 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,11 +1,15 @@
1
- # Vitest websocket mock
1
+ # vitest-websocket-mock
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/vitest-websocket-mock.svg)](https://badge.fury.io/js/vitest-websocket-mock)
4
4
  [![Build Status](https://github.com/akiomik/vitest-websocket-mock/actions/workflows/ci.yml/badge.svg)](https://github.com/akiomik/vitest-websocket-mock/actions)
5
5
  [![codecov](https://codecov.io/gh/akiomik/vitest-websocket-mock/branch/main/graph/badge.svg?token=40OVYIT90L)](https://codecov.io/gh/akiomik/vitest-websocket-mock)
6
6
 
7
- A set of utilities and Vitest matchers to help testing complex websocket interactions.
8
- A patched fork of [romgain/jest-websocket-mock](https://github.com/romgain/jest-websocket-mock).
7
+ A set of utilities and Vitest matchers to help testing complex websocket interactions:
8
+ mock websocket servers, wait for connections and messages, and assert on them
9
+ with dedicated matchers.
10
+
11
+ Originally forked from [romgain/jest-websocket-mock](https://github.com/romgain/jest-websocket-mock),
12
+ and since developed independently as a Vitest-first library.
9
13
 
10
14
  **Examples:**
11
15
  Several examples are provided in the [examples folder](https://github.com/akiomik/vitest-websocket-mock/blob/main/examples/).
@@ -15,8 +19,31 @@ In particular:
15
19
  - [testing a component using the saga above](https://github.com/akiomik/vitest-websocket-mock/blob/main/examples/redux-saga/src/__tests__/App.test.tsx)
16
20
  - [testing a component that manages a websocket connection using react hooks](https://github.com/akiomik/vitest-websocket-mock/blob/main/examples/hooks/src/App.test.tsx)
17
21
 
22
+ ## When to use this vs. MSW
23
+
24
+ [Vitest recommends](https://vitest.dev/guide/mocking/requests) [Mock Service Worker (MSW)](https://mswjs.io)
25
+ for mocking network requests, and MSW has first-class
26
+ [WebSocket support](https://mswjs.io/docs/websocket/). The two address different
27
+ layers of the problem:
28
+
29
+ - **MSW** provides declarative, network-level mocking (`ws.link()` handlers).
30
+ It shines when you want to share handlers between your app, Storybook, and
31
+ tests, or when you also need to mock HTTP or GraphQL.
32
+ - **`vitest-websocket-mock`** provides imperative test-flow ergonomics that MSW
33
+ does not: a `WS` mock-server object, `await server.connected`, a synchronous
34
+ record of received messages in `server.messages`, and custom matchers such as
35
+ `.toReceiveMessage` and `.toHaveReceivedMessages`.
36
+
37
+ If your test reads best as a step-by-step conversation with a mock server
38
+ ("wait for the connection, assert on the next message, reply, assert again"),
39
+ this library is the better fit. Running this library on top of MSW's
40
+ interceptor, which would make the two complementary rather than alternatives,
41
+ is being explored in [#77](https://github.com/akiomik/vitest-websocket-mock/issues/77).
42
+
18
43
  ## Install
19
44
 
45
+ `vitest-websocket-mock` requires Vitest 5 as a peer dependency.
46
+
20
47
  ```bash
21
48
  npm install -D vitest-websocket-mock
22
49
  ```
@@ -65,7 +92,7 @@ const server = new WS('ws://localhost:1234', { jsonProtocol: true });
65
92
  server.send({ type: 'GREETING', payload: 'hello' });
66
93
  ```
67
94
 
68
- - The `mock-server` options `verifyClient` and `selectProtocol` are directly passed-through to the mock-server's constructor.
95
+ - The [`mock-socket`](https://github.com/thoov/mock-socket) server options `verifyClient` and `selectProtocol` are directly passed through to the underlying mock server's constructor.
69
96
 
70
97
  ### Attributes of a `WS` instance
71
98
 
@@ -80,6 +107,11 @@ A `WS` instance has the following attributes:
80
107
  new message. The resolved value is the received message (deserialized as a
81
108
  JavaScript Object if the `WS` was instantiated with the `{ jsonProtocol: true }`
82
109
  option).
110
+ - `messages`: an array that synchronously and cumulatively records every
111
+ message received by the `WS` instance, in the order they were received.
112
+ Since it's updated synchronously, it can be used to assert that no message
113
+ has been received without waiting on a timeout (see
114
+ [Run assertions on received messages](#run-assertions-on-received-messages)).
83
115
 
84
116
  ### Methods on a `WS` instance
85
117
 
@@ -93,8 +125,9 @@ A `WS` instance has the following attributes:
93
125
 
94
126
  ## Run assertions on received messages
95
127
 
96
- `vitest-websocket-mock` registers custom vitest matchers to make assertions
97
- on received messages easier:
128
+ `vitest-websocket-mock` registers custom Vitest matchers to make assertions
129
+ on received messages easier. They are registered automatically when
130
+ `vitest-websocket-mock` is imported, so no extra setup file is needed:
98
131
 
99
132
  - `.toReceiveMessage`: async matcher that waits for the next message received
100
133
  by the mock websocket server, and asserts its content. It will time out
@@ -102,6 +135,12 @@ on received messages easier:
102
135
  - `.toHaveReceivedMessages`: synchronous matcher that checks that all the
103
136
  expected messages have been received by the mock websocket server.
104
137
 
138
+ **Note**: `.toHaveReceivedMessages([])` always passes, since it only checks
139
+ that every expected message is included in the received messages, and an
140
+ empty list of expected messages is trivially satisfied. To assert that _no_
141
+ message has been received, check `server.messages` directly instead (see
142
+ below).
143
+
105
144
  ### Run assertions on messages as they are received by the mock server
106
145
 
107
146
  ```js
@@ -116,6 +155,21 @@ test('the server keeps track of received messages, and yields them as they come
116
155
  });
117
156
  ```
118
157
 
158
+ ### Assert that a message has not been received
159
+
160
+ `server.messages` is updated synchronously, so it can be checked immediately
161
+ without waiting for a timeout:
162
+
163
+ ```js
164
+ test('asserts that no message has been received', async () => {
165
+ const server = new WS('ws://localhost:1234');
166
+ const client = new WebSocket('ws://localhost:1234');
167
+
168
+ await server.connected;
169
+ expect(server.messages).toEqual([]);
170
+ });
171
+ ```
172
+
119
173
  ### Send messages to the connected clients
120
174
 
121
175
  ```js
@@ -177,7 +231,7 @@ This can be used to test behaviour for a client that connects to a WebSocket ser
177
231
  ```js
178
232
  test('rejects connections that fail the verifyClient option', async () => {
179
233
  new WS('ws://localhost:1234', { verifyClient: () => false });
180
- const errorCallback = vitest.fn();
234
+ const errorCallback = vi.fn();
181
235
 
182
236
  await expect(
183
237
  new Promise((resolve, reject) => {
@@ -200,7 +254,7 @@ This can be used to test behaviour for a client that connects to a WebSocket ser
200
254
  test('rejects connections that fail the selectProtocol option', async () => {
201
255
  const selectProtocol = () => null;
202
256
  new WS('ws://localhost:1234', { selectProtocol });
203
- const errorCallback = vitest.fn();
257
+ const errorCallback = vi.fn();
204
258
 
205
259
  await expect(
206
260
  new Promise((resolve, reject) => {
@@ -256,7 +310,7 @@ it('the server can refuse connections', async () => {
256
310
  });
257
311
 
258
312
  const client = new WebSocket('ws://localhost:1234');
259
- client.onclose = (event: CloseEvent) => {
313
+ client.onclose = (event) => {
260
314
  expect(event.code).toBe(1003);
261
315
  expect(event.wasClean).toBe(false);
262
316
  expect(event.reason).toBe('NOPE');
@@ -292,7 +346,7 @@ afterEach(() => {
292
346
 
293
347
  `mock-socket` has a strong usage of delays (`setTimeout` to be more specific). This means using `vi.useFakeTimers();` will cause issues such as the client appearing to never connect to the server.
294
348
 
295
- While running the websocket server from tests within the vitest-dom environment (as opposed to node)
349
+ While running the websocket server from tests within the jsdom environment (as opposed to node)
296
350
  you may see errors of the nature:
297
351
 
298
352
  ```bash
@@ -303,10 +357,23 @@ You can work around this by installing the setImmediate shim from
303
357
  [https://github.com/YuzuJS/setImmediate](https://github.com/YuzuJS/setImmediate) and
304
358
  adding `require('setimmediate');` to your `setupTests.js`.
305
359
 
360
+ ### The custom matchers are not recognized by TypeScript
361
+
362
+ ```
363
+ Property 'toReceiveMessage' does not exist on type 'Assertion<void, WS>'.
364
+ ```
365
+
366
+ The matchers are contributed with [module augmentation][augmentation], which
367
+ only merges into the copy of `vitest` the augmentation resolves to. Deduplicate
368
+ `vitest` if your project resolves more than one, for example a monorepo where a
369
+ nested package pins its own.
370
+
371
+ [augmentation]: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
372
+
306
373
  ## Testing React applications
307
374
 
308
375
  When testing React applications, `vitest-websocket-mock` will look for
309
- `@testing-library/react`'s implementation of [`act`](https://reactjs.org/docs/test-utils.html#act).
376
+ `@testing-library/react`'s implementation of [`act`](https://react.dev/reference/react/act).
310
377
  If it is available, it will wrap all the necessary calls in `act`, so you don't have to.
311
378
 
312
379
  If `@testing-library/react` is not available, we will assume that you're not testing a React application,
@@ -351,9 +418,3 @@ the `mock-socket` library that `vitest-websocket-mock` uses under the hood only
351
418
  implements the browser API.
352
419
  As a result, `vitest-websocket-mock` will only work with the `ws` library if you
353
420
  restrict yourself to the browser APIs!
354
-
355
- ## Examples
356
-
357
- For a real life example, see the
358
- [examples directory](https://github.com/akiomik/vitest-websocket-mock/tree/main/examples),
359
- and in particular the saga tests.
package/dist/index.d.ts CHANGED
@@ -1,88 +1,66 @@
1
- import { Server, ServerOptions, Client, CloseOptions } from 'mock-socket';
2
- import { RawMatcherFn } from '@vitest/expect';
3
-
4
- /**
5
- * @copyright Romain Bertrand 2018
6
- * @copyright Akiomi Kamakura 2023
7
- */
8
-
9
- declare function deriveToHaveReceivedMessage(name: string, fn: RawMatcherFn): RawMatcherFn;
10
-
11
- /**
12
- * @copyright Romain Bertrand 2018
13
- * @copyright Akiomi Kamakura 2023
14
- */
15
-
16
- declare function deriveToReceiveMessage(name: string, fn: RawMatcherFn): RawMatcherFn;
17
-
1
+ import { Matcher } from "vitest";
2
+ import { Client, CloseOptions, Server, ServerOptions } from "mock-socket";
3
+ //#region src/derivers/deriveToHaveReceivedMessage.d.ts
4
+ declare function deriveToHaveReceivedMessage(name: string, fn: Matcher): Matcher;
5
+ //#endregion
6
+ //#region src/derivers/deriveToReceiveMessage.d.ts
7
+ declare function deriveToReceiveMessage(name: string, fn: Matcher): Matcher;
8
+ //#endregion
9
+ //#region src/derivers/types.d.ts
18
10
  /**
19
11
  * @copyright Romain Bertrand 2018
20
12
  * @copyright Akiomi Kamakura 2023
21
13
  */
22
14
  interface ReceiveMessageOptions {
23
- timeout?: number;
15
+ timeout?: number;
24
16
  }
25
-
17
+ //#endregion
18
+ //#region src/queue.d.ts
26
19
  /**
27
20
  * @copyright Romain Bertrand 2018
28
21
  */
29
22
  declare class Queue<ItemT> {
30
- pendingItems: Array<ItemT>;
31
- nextItemResolver: () => void;
32
- nextItem: Promise<void>;
33
- put(item: ItemT): void;
34
- get(): Promise<ItemT>;
23
+ pendingItems: Array<ItemT>;
24
+ nextItemResolver: () => void;
25
+ nextItem: Promise<void>;
26
+ put(item: ItemT): void;
27
+ get(): Promise<ItemT>;
35
28
  }
36
-
37
- /**
38
- * @copyright Romain Bertrand 2018
39
- * @copyright Akiomi Kamakura 2023
40
- */
41
-
29
+ //#endregion
30
+ //#region src/websocket.d.ts
42
31
  interface WSOptions extends ServerOptions {
43
- jsonProtocol?: boolean;
32
+ jsonProtocol?: boolean;
44
33
  }
45
34
  type DeserializedMessage<TMessage = object> = string | TMessage;
46
- declare class WS {
47
- server: Server;
48
- serializer: (deserializedMessage: DeserializedMessage) => string;
49
- deserializer: (message: string) => DeserializedMessage;
50
- static instances: Array<WS>;
51
- messages: Array<DeserializedMessage>;
52
- messagesToConsume: Queue<unknown>;
53
- private _isConnected;
54
- private _isClosed;
55
- static clean(): void;
56
- constructor(url: string, opts?: WSOptions);
57
- get connected(): Promise<Client>;
58
- get closed(): Promise<void>;
59
- get nextMessage(): Promise<unknown>;
60
- on(eventName: 'connection' | 'message' | 'close', callback: (socket: Client) => void): void;
61
- send(message: DeserializedMessage): void;
62
- close(options?: CloseOptions): void;
63
- error(options?: CloseOptions): void;
35
+ export default class WS {
36
+ server: Server;
37
+ serializer: (deserializedMessage: DeserializedMessage) => string;
38
+ deserializer: (message: string) => DeserializedMessage;
39
+ static instances: Array<WS>;
40
+ messages: Array<DeserializedMessage>;
41
+ messagesToConsume: Queue<unknown>;
42
+ private _isConnected;
43
+ private _isClosed;
44
+ static clean(): void;
45
+ constructor(url: string, opts?: WSOptions);
46
+ get connected(): Promise<Client>;
47
+ get closed(): Promise<void>;
48
+ get nextMessage(): Promise<unknown>;
49
+ on(eventName: 'connection' | 'message' | 'close', callback: (socket: Client) => void): void;
50
+ send(message: DeserializedMessage): void;
51
+ close(options?: CloseOptions): void;
52
+ error(options?: CloseOptions): void;
64
53
  }
65
-
66
- /**
67
- * @copyright Romain Bertrand 2018
68
- * @copyright Akiomi Kamakura 2023
69
- */
70
-
54
+ //#endregion
55
+ //#region src/matchers/index.d.ts
71
56
  interface CustomMatchers<R = unknown> {
72
- toReceiveMessage<TMessage = object>(message: DeserializedMessage<TMessage>, options?: ReceiveMessageOptions): Promise<R>;
73
- toHaveReceivedMessages<TMessage = object>(messages: Array<DeserializedMessage<TMessage>>): R;
57
+ toReceiveMessage<TMessage = object>(message: DeserializedMessage<TMessage>, options?: ReceiveMessageOptions): Promise<void>;
58
+ toHaveReceivedMessages<TMessage = object>(messages: Array<DeserializedMessage<TMessage>>): R;
74
59
  }
75
-
76
- /**
77
- * @copyright Romain Bertrand 2018
78
- * @copyright Akiomi Kamakura 2023
79
- */
80
-
81
- declare module '@vitest/expect' {
82
- interface Assertion<T = any> extends CustomMatchers<T> {
83
- }
84
- interface AsymmetricMatchersContaining extends CustomMatchers {
85
- }
60
+ //#endregion
61
+ //#region src/extend-expect.d.ts
62
+ declare module 'vitest' {
63
+ interface Matchers<R extends void | Promise<void> = void | Promise<void>, T = unknown> extends CustomMatchers<R> {}
86
64
  }
87
-
88
- export { type ReceiveMessageOptions, WS, WS as default, deriveToHaveReceivedMessage, deriveToReceiveMessage };
65
+ //#endregion
66
+ export { type ReceiveMessageOptions, WS, deriveToHaveReceivedMessage, deriveToReceiveMessage };
package/dist/index.js CHANGED
@@ -1,280 +1,254 @@
1
- var __defProp = Object.defineProperty;
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
-
13
- // src/extend-expect.ts
1
+ import { createRequire } from "node:module";
14
2
  import { expect } from "vitest";
15
-
16
- // src/matchers/index.ts
17
- var matchers_exports = {};
18
- __export(matchers_exports, {
19
- toHaveReceivedMessages: () => toHaveReceivedMessages_default,
20
- toReceiveMessage: () => toReceiveMessage_default
21
- });
22
-
23
- // src/websocket.ts
24
3
  import { Server } from "mock-socket";
25
-
26
- // src/act-compat.ts
27
- var act;
4
+ //#region \0rolldown/runtime.js
5
+ var __defProp = Object.defineProperty;
6
+ var __exportAll = (all, no_symbols) => {
7
+ let target = {};
8
+ for (var name in all) __defProp(target, name, {
9
+ get: all[name],
10
+ enumerable: true
11
+ });
12
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
13
+ return target;
14
+ };
15
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
16
+ //#endregion
17
+ //#region src/act-compat.ts
18
+ let act;
28
19
  try {
29
- act = __require("@testing-library/react").act;
20
+ act = __require("@testing-library/react").act;
30
21
  } catch (_) {
31
- act = (callback) => {
32
- callback();
33
- };
22
+ act = (callback) => {
23
+ callback();
24
+ };
34
25
  }
35
26
  var act_compat_default = act;
36
-
37
- // src/queue.ts
27
+ //#endregion
28
+ //#region src/queue.ts
29
+ /**
30
+ * @copyright Romain Bertrand 2018
31
+ */
38
32
  var Queue = class {
39
- pendingItems = [];
40
- nextItemResolver;
41
- nextItem = new Promise((done) => this.nextItemResolver = done);
42
- put(item) {
43
- this.pendingItems.push(item);
44
- this.nextItemResolver();
45
- this.nextItem = new Promise((done) => this.nextItemResolver = done);
46
- }
47
- get() {
48
- const item = this.pendingItems.shift();
49
- if (item) {
50
- return Promise.resolve(item);
51
- }
52
- let resolver;
53
- const nextItemPromise = new Promise((done) => resolver = done);
54
- this.nextItem.then(() => {
55
- resolver(this.pendingItems.shift());
56
- });
57
- return nextItemPromise;
58
- }
33
+ pendingItems = [];
34
+ nextItemResolver;
35
+ nextItem = new Promise((done) => this.nextItemResolver = done);
36
+ put(item) {
37
+ this.pendingItems.push(item);
38
+ this.nextItemResolver();
39
+ this.nextItem = new Promise((done) => this.nextItemResolver = done);
40
+ }
41
+ get() {
42
+ const item = this.pendingItems.shift();
43
+ if (item) return Promise.resolve(item);
44
+ let resolver;
45
+ const nextItemPromise = new Promise((done) => resolver = done);
46
+ this.nextItem.then(() => {
47
+ resolver(this.pendingItems.shift());
48
+ });
49
+ return nextItemPromise;
50
+ }
59
51
  };
60
-
61
- // src/websocket.ts
62
- var identity = (x) => x;
63
- var WS = class _WS {
64
- server;
65
- serializer;
66
- deserializer;
67
- static instances = [];
68
- messages = [];
69
- messagesToConsume = new Queue();
70
- _isConnected;
71
- _isClosed;
72
- static clean() {
73
- _WS.instances.forEach((instance) => {
74
- instance.close();
75
- instance.messages = [];
76
- });
77
- _WS.instances = [];
78
- }
79
- constructor(url, opts = {}) {
80
- _WS.instances.push(this);
81
- const { jsonProtocol = false, ...serverOptions } = opts;
82
- this.serializer = jsonProtocol ? JSON.stringify : identity;
83
- this.deserializer = jsonProtocol ? JSON.parse : identity;
84
- let connectionResolver;
85
- let closedResolver;
86
- this._isConnected = new Promise((done) => connectionResolver = done);
87
- this._isClosed = new Promise((done) => closedResolver = done);
88
- this.server = new Server(url, serverOptions);
89
- this.server.on("close", closedResolver);
90
- this.server.on("connection", (socket) => {
91
- connectionResolver(socket);
92
- socket.on("message", (message) => {
93
- const parsedMessage = this.deserializer(message);
94
- this.messages.push(parsedMessage);
95
- this.messagesToConsume.put(parsedMessage);
96
- });
97
- });
98
- }
99
- get connected() {
100
- let resolve;
101
- const connectedPromise = new Promise((done) => resolve = done);
102
- const waitForConnected = async () => {
103
- await act_compat_default(async () => {
104
- await this._isConnected;
105
- });
106
- resolve(await this._isConnected);
107
- };
108
- waitForConnected();
109
- return connectedPromise;
110
- }
111
- get closed() {
112
- let resolve;
113
- const closedPromise = new Promise((done) => resolve = done);
114
- const waitForclosed = async () => {
115
- await act_compat_default(async () => {
116
- await this._isClosed;
117
- });
118
- await this._isClosed;
119
- resolve();
120
- };
121
- waitForclosed();
122
- return closedPromise;
123
- }
124
- get nextMessage() {
125
- return this.messagesToConsume.get();
126
- }
127
- on(eventName, callback) {
128
- this.server.on(eventName, callback);
129
- }
130
- send(message) {
131
- act_compat_default(() => {
132
- this.server.emit("message", this.serializer(message));
133
- });
134
- }
135
- close(options) {
136
- act_compat_default(() => {
137
- this.server.close(options);
138
- });
139
- }
140
- error(options) {
141
- act_compat_default(() => {
142
- this.server.emit("error", null);
143
- });
144
- this.server.close(options);
145
- }
52
+ //#endregion
53
+ //#region src/websocket.ts
54
+ /**
55
+ * @copyright Romain Bertrand 2018
56
+ * @copyright Akiomi Kamakura 2023
57
+ */
58
+ const identity = (x) => x;
59
+ var WS = class WS {
60
+ server;
61
+ serializer;
62
+ deserializer;
63
+ static instances = [];
64
+ messages = [];
65
+ messagesToConsume = new Queue();
66
+ _isConnected;
67
+ _isClosed;
68
+ static clean() {
69
+ WS.instances.forEach((instance) => {
70
+ instance.close();
71
+ instance.messages = [];
72
+ });
73
+ WS.instances = [];
74
+ }
75
+ constructor(url, opts = {}) {
76
+ WS.instances.push(this);
77
+ const { jsonProtocol = false, ...serverOptions } = opts;
78
+ this.serializer = jsonProtocol ? JSON.stringify : identity;
79
+ this.deserializer = jsonProtocol ? JSON.parse : identity;
80
+ let connectionResolver;
81
+ let closedResolver;
82
+ this._isConnected = new Promise((done) => connectionResolver = done);
83
+ this._isClosed = new Promise((done) => closedResolver = done);
84
+ this.server = new Server(url, serverOptions);
85
+ this.server.on("close", closedResolver);
86
+ this.server.on("connection", (socket) => {
87
+ connectionResolver(socket);
88
+ socket.on("message", (message) => {
89
+ const parsedMessage = this.deserializer(message);
90
+ this.messages.push(parsedMessage);
91
+ this.messagesToConsume.put(parsedMessage);
92
+ });
93
+ });
94
+ }
95
+ get connected() {
96
+ let resolve;
97
+ const connectedPromise = new Promise((done) => resolve = done);
98
+ const waitForConnected = async () => {
99
+ await act_compat_default(async () => {
100
+ await this._isConnected;
101
+ });
102
+ resolve(await this._isConnected);
103
+ };
104
+ waitForConnected();
105
+ return connectedPromise;
106
+ }
107
+ get closed() {
108
+ let resolve;
109
+ const closedPromise = new Promise((done) => resolve = done);
110
+ const waitForclosed = async () => {
111
+ await act_compat_default(async () => {
112
+ await this._isClosed;
113
+ });
114
+ await this._isClosed;
115
+ resolve();
116
+ };
117
+ waitForclosed();
118
+ return closedPromise;
119
+ }
120
+ get nextMessage() {
121
+ return this.messagesToConsume.get();
122
+ }
123
+ on(eventName, callback) {
124
+ this.server.on(eventName, callback);
125
+ }
126
+ send(message) {
127
+ act_compat_default(() => {
128
+ this.server.emit("message", this.serializer(message));
129
+ });
130
+ }
131
+ close(options) {
132
+ act_compat_default(() => {
133
+ this.server.close(options);
134
+ });
135
+ }
136
+ error(options) {
137
+ act_compat_default(() => {
138
+ this.server.emit("error", null);
139
+ });
140
+ this.server.close(options);
141
+ }
146
142
  };
143
+ //#endregion
144
+ //#region src/matcherUtils.ts
145
+ /**
146
+ * Plain-text replacements for `this.utils.matcherHint` / `printExpected` /
147
+ * `printReceived`. Matcher messages must not contain ANSI color codes, so
148
+ * that color detection (e.g. AI-agent environments) cannot make them differ
149
+ * between environments; the reporter renders its own colored diff from the
150
+ * `actual` / `expected` returned by matchers.
151
+ */
152
+ function matcherHint(matcher, isNot = false) {
153
+ return `expect(WS).${isNot ? "not." : ""}${matcher}(expected)`;
154
+ }
155
+ const SPACE_SYMBOL = "·";
156
+ function replaceTrailingSpaces(text) {
157
+ return text.replace(/\s+$/gm, (spaces) => SPACE_SYMBOL.repeat(spaces.length));
158
+ }
159
+ function printValue(value) {
160
+ return replaceTrailingSpaces(this.utils.stringify(value));
161
+ }
162
+ function formatComparison(hint, expectedLabel, expected, receivedLabel, received) {
163
+ return hint + `
147
164
 
148
- // src/derivers/makeInvalidWsMessage.ts
165
+ ${expectedLabel}\n ${printValue.call(this, expected)}\n${receivedLabel}\n ${printValue.call(this, received)}`;
166
+ }
167
+ //#endregion
168
+ //#region src/derivers/makeInvalidWsMessage.ts
149
169
  function makeInvalidWsMessage(ws, matcher) {
150
- return this.utils.matcherHint(this.isNot ? `.not.${matcher}` : `.${matcher}`, "WS", "expected") + `
170
+ return matcherHint(matcher, this.isNot) + `
151
171
 
152
172
  Expected the websocket object to be a valid WS mock.
153
- Received: ${typeof ws}
154
- ${this.utils.printReceived(ws)}`;
173
+ Received: ${typeof ws}\n ${printValue.call(this, ws)}`;
155
174
  }
156
-
157
- // src/derivers/deriveToHaveReceivedMessage.ts
175
+ //#endregion
176
+ //#region src/derivers/deriveToHaveReceivedMessage.ts
158
177
  function deriveToHaveReceivedMessage(name, fn) {
159
- return function(ws, expected, options) {
160
- const isWS = ws instanceof WS;
161
- if (!isWS) {
162
- return {
163
- pass: this.isNot,
164
- // always fail
165
- message: makeInvalidWsMessage.bind(this, ws, name)
166
- };
167
- }
168
- return fn.call(this, ws.messages, expected, options);
169
- };
178
+ return function(ws, expected, options) {
179
+ if (!(ws instanceof WS)) return {
180
+ pass: this.isNot,
181
+ message: makeInvalidWsMessage.bind(this, ws, name)
182
+ };
183
+ return fn.call(this, ws.messages, expected, options);
184
+ };
170
185
  }
171
-
172
- // src/derivers/deriveToReceiveMessage.ts
173
- var WAIT_DELAY = 1e3;
174
- var TIMEOUT = /* @__PURE__ */ Symbol("timeout");
186
+ //#endregion
187
+ //#region src/derivers/deriveToReceiveMessage.ts
188
+ const WAIT_DELAY = 1e3;
189
+ const TIMEOUT = Symbol("timeout");
175
190
  function deriveToReceiveMessage(name, fn) {
176
- return async function(ws, expected, options) {
177
- const isWS = ws instanceof WS;
178
- if (!isWS) {
179
- return {
180
- pass: this.isNot,
181
- // always fail
182
- message: makeInvalidWsMessage.bind(this, ws, name)
183
- };
184
- }
185
- const waitDelay = options?.timeout ?? WAIT_DELAY;
186
- const messageOrTimeout = await Promise.race([
187
- ws.nextMessage,
188
- new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), waitDelay))
189
- ]);
190
- if (messageOrTimeout === TIMEOUT) {
191
- return {
192
- pass: this.isNot,
193
- // always fail
194
- message: () => this.utils.matcherHint(`${this.isNot ? ".not" : ""}.${name}`, "WS", "expected") + `
191
+ return async function(ws, expected, options) {
192
+ if (!(ws instanceof WS)) return {
193
+ pass: this.isNot,
194
+ message: makeInvalidWsMessage.bind(this, ws, name)
195
+ };
196
+ const waitDelay = options?.timeout ?? WAIT_DELAY;
197
+ const messageOrTimeout = await Promise.race([ws.nextMessage, new Promise((resolve) => setTimeout(() => resolve(TIMEOUT), waitDelay))]);
198
+ if (messageOrTimeout === TIMEOUT) return {
199
+ pass: this.isNot,
200
+ message: () => matcherHint(name, this.isNot) + `
195
201
 
196
202
  Expected the websocket server to receive a message,
197
203
  but it didn't receive anything in ${waitDelay}ms.`
198
- };
199
- } else {
200
- const received = messageOrTimeout;
201
- return Promise.resolve(fn.call(this, received, expected, options));
202
- }
203
- };
204
+ };
205
+ else {
206
+ const received = messageOrTimeout;
207
+ return Promise.resolve(fn.call(this, received, expected, options));
208
+ }
209
+ };
204
210
  }
205
-
206
- // src/matchers/toHaveReceivedMessages.ts
207
- var toHaveReceivedMessages = deriveToHaveReceivedMessage(
208
- "toHaveReceivedMessages",
209
- function(received, expected) {
210
- const equalities = expected.map(
211
- (expectedMsg) => (
212
- // object comparison to handle JSON protocols
213
- received.some((receivedMsg) => this.equals(receivedMsg, expectedMsg))
214
- )
215
- );
216
- const pass = this.isNot ? equalities.some(Boolean) : equalities.every(Boolean);
217
- const message = pass ? () => this.utils.matcherHint(".not.toHaveReceivedMessages", "WS", "expected") + `
218
-
219
- Expected the WS server to not have received the following messages:
220
- ${this.utils.printExpected(expected)}
221
- But it received:
222
- ${this.utils.printReceived(received)}` : () => {
223
- return this.utils.matcherHint(".toHaveReceivedMessages", "WS", "expected") + `
224
-
225
- Expected the WS server to have received the following messages:
226
- ${this.utils.printExpected(expected)}
227
- Received:
228
- ${this.utils.printReceived(received)}
229
-
230
- `;
231
- };
232
- return {
233
- actual: received,
234
- expected,
235
- message,
236
- pass
237
- };
238
- }
239
- );
240
- var toHaveReceivedMessages_default = toHaveReceivedMessages;
241
-
242
- // src/matchers/toReceiveMessage.ts
243
- import { diff } from "@vitest/utils/diff";
244
- var toReceiveMessage = deriveToReceiveMessage("toReceiveMessage", function(received, expected) {
245
- const pass = this.equals(received, expected);
246
- const message = pass ? () => this.utils.matcherHint(".not.toReceiveMessage", "WS", "expected") + `
247
-
248
- Expected the next received message to not equal:
249
- ${this.utils.printExpected(expected)}
250
- Received:
251
- ${this.utils.printReceived(received)}` : () => {
252
- const diffString = diff(expected, received, { expand: this.expand });
253
- return this.utils.matcherHint(".toReceiveMessage", "WS", "expected") + `
254
-
255
- Expected the next received message to equal:
256
- ${this.utils.printExpected(expected)}
257
- Received:
258
- ${this.utils.printReceived(received)}
259
-
260
- Difference:
261
-
262
- ${diffString}`;
263
- };
264
- return {
265
- actual: received,
266
- expected,
267
- message,
268
- pass
269
- };
211
+ //#endregion
212
+ //#region src/derivers/index.ts
213
+ /**
214
+ * @copyright Romain Bertrand 2018
215
+ * @copyright Akiomi Kamakura 2023
216
+ */
217
+ //#endregion
218
+ //#region src/matchers/toHaveReceivedMessages.ts
219
+ const toHaveReceivedMessages = deriveToHaveReceivedMessage("toHaveReceivedMessages", function(received, expected) {
220
+ const equalities = expected.map((expectedMsg) => received.some((receivedMsg) => this.equals(receivedMsg, expectedMsg)));
221
+ const pass = this.isNot ? equalities.some(Boolean) : equalities.every(Boolean);
222
+ return {
223
+ actual: received,
224
+ expected,
225
+ message: pass ? () => formatComparison.call(this, matcherHint("toHaveReceivedMessages", true), "Expected the WS server to not have received the following messages:", expected, "But it received:", received) : () => formatComparison.call(this, matcherHint("toHaveReceivedMessages"), "Expected the WS server to have received the following messages:", expected, "Received:", received),
226
+ pass
227
+ };
270
228
  });
271
- var toReceiveMessage_default = toReceiveMessage;
272
-
273
- // src/extend-expect.ts
229
+ //#endregion
230
+ //#region src/matchers/toReceiveMessage.ts
231
+ const toReceiveMessage = deriveToReceiveMessage("toReceiveMessage", function(received, expected) {
232
+ const pass = this.equals(received, expected);
233
+ return {
234
+ actual: received,
235
+ expected,
236
+ message: pass ? () => formatComparison.call(this, matcherHint("toReceiveMessage", true), "Expected the next received message to not equal:", expected, "Received:", received) : () => formatComparison.call(this, matcherHint("toReceiveMessage"), "Expected the next received message to equal:", expected, "Received:", received),
237
+ pass
238
+ };
239
+ });
240
+ //#endregion
241
+ //#region src/matchers/index.ts
242
+ var matchers_exports = /* @__PURE__ */ __exportAll({
243
+ toHaveReceivedMessages: () => toHaveReceivedMessages,
244
+ toReceiveMessage: () => toReceiveMessage
245
+ });
246
+ //#endregion
247
+ //#region src/extend-expect.ts
248
+ /**
249
+ * @copyright Romain Bertrand 2018
250
+ * @copyright Akiomi Kamakura 2023
251
+ */
274
252
  expect.extend(matchers_exports);
275
- export {
276
- WS,
277
- WS as default,
278
- deriveToHaveReceivedMessage,
279
- deriveToReceiveMessage
280
- };
253
+ //#endregion
254
+ export { WS, WS as default, deriveToHaveReceivedMessage, deriveToReceiveMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vitest-websocket-mock",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Mock websockets and assert complex websocket interactions with Vitest",
5
5
  "type": "module",
6
6
  "repository": {
@@ -18,9 +18,10 @@
18
18
  "scripts": {
19
19
  "clean": "rimraf dist",
20
20
  "prebuild": "npm run clean",
21
- "build": "tsup",
22
- "lint": "prettier --check . && eslint .",
23
- "format": "prettier --write .",
21
+ "build": "tsdown",
22
+ "typecheck": "tsc --noEmit",
23
+ "lint": "biome check .",
24
+ "format": "biome check --write .",
24
25
  "prepublishOnly": "npm run build",
25
26
  "test": "vitest --run",
26
27
  "test:watch": "vitest"
@@ -34,27 +35,19 @@
34
35
  "author": "Akiomi Kamakura",
35
36
  "license": "MIT",
36
37
  "devDependencies": {
37
- "@typescript-eslint/eslint-plugin": "^5.59.0",
38
- "@typescript-eslint/parser": "^5.59.0",
39
- "@vitest/coverage-v8": "4.1.1",
40
- "eslint": "^8.38.0",
41
- "eslint-config-prettier": "^8.5.0",
42
- "eslint-config-react-app": "^7.0.1",
43
- "eslint-plugin-react-hooks": "^4.6.0",
44
- "eslint-plugin-react-refresh": "^0.3.4",
45
- "eslint-plugin-simple-import-sort": "^10.0.0",
46
- "prettier": "^2.0.2",
47
- "rimraf": "^4.1.2",
48
- "tsup": "^8.5.1",
49
- "typescript": "^4.0.2",
38
+ "@biomejs/biome": "^2.5.3",
39
+ "@types/node": "^26.1.1",
40
+ "@vitest/coverage-v8": "^5.0.0",
41
+ "rimraf": "^6.1.3",
42
+ "tsdown": "^0.23.0",
43
+ "typescript": "^7.0.2",
50
44
  "vite": "^8.1.4",
51
- "vitest": "4.1.1"
45
+ "vitest": "^5.0.0"
52
46
  },
53
47
  "peerDependencies": {
54
- "vitest": ">=4"
48
+ "vitest": ">=5 <6"
55
49
  },
56
50
  "dependencies": {
57
- "@vitest/utils": "4.1.1",
58
51
  "mock-socket": "^9.2.1"
59
52
  },
60
53
  "files": [