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 +78 -17
- package/dist/index.d.ts +48 -70
- package/dist/index.js +231 -257
- package/package.json +13 -20
package/README.md
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vitest-websocket-mock
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/vitest-websocket-mock)
|
|
4
4
|
[](https://github.com/akiomik/vitest-websocket-mock/actions)
|
|
5
5
|
[](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
|
-
|
|
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-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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://
|
|
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 {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
32
|
+
jsonProtocol?: boolean;
|
|
44
33
|
}
|
|
45
34
|
type DeserializedMessage<TMessage = object> = string | TMessage;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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,
|
|
65
|
+
//#endregion
|
|
66
|
+
export { type ReceiveMessageOptions, WS, deriveToHaveReceivedMessage, deriveToReceiveMessage };
|
package/dist/index.js
CHANGED
|
@@ -1,280 +1,254 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
27
|
-
var
|
|
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
|
-
|
|
20
|
+
act = __require("@testing-library/react").act;
|
|
30
21
|
} catch (_) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
act = (callback) => {
|
|
23
|
+
callback();
|
|
24
|
+
};
|
|
34
25
|
}
|
|
35
26
|
var act_compat_default = act;
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/queue.ts
|
|
29
|
+
/**
|
|
30
|
+
* @copyright Romain Bertrand 2018
|
|
31
|
+
*/
|
|
38
32
|
var Queue = class {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region src/derivers/deriveToHaveReceivedMessage.ts
|
|
158
177
|
function deriveToHaveReceivedMessage(name, fn) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
Expected the WS server to not have received the following messages:
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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.
|
|
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": "
|
|
22
|
-
"
|
|
23
|
-
"
|
|
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
|
-
"@
|
|
38
|
-
"@
|
|
39
|
-
"@vitest/coverage-v8": "
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
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": "
|
|
45
|
+
"vitest": "^5.0.0"
|
|
52
46
|
},
|
|
53
47
|
"peerDependencies": {
|
|
54
|
-
"vitest": ">=
|
|
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": [
|