@irtio/testing 0.1.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/LICENSE +21 -0
- package/dist/assertions-BP4as6ol.d.ts +38 -0
- package/dist/chunk-O3OZKC3D.js +851 -0
- package/dist/index.d.ts +542 -0
- package/dist/index.js +469 -0
- package/dist/matchers.d.ts +31 -0
- package/dist/matchers.js +18 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InProcessSocket,
|
|
3
|
+
TRACE_TAIL,
|
|
4
|
+
TestHarness,
|
|
5
|
+
clockScheduler,
|
|
6
|
+
divergence,
|
|
7
|
+
matchers,
|
|
8
|
+
materialize,
|
|
9
|
+
toHaveConverged,
|
|
10
|
+
toHaveNoVisibilityLeaks,
|
|
11
|
+
toHaveRejected,
|
|
12
|
+
toStayUnderBandwidth
|
|
13
|
+
} from "./chunk-O3OZKC3D.js";
|
|
14
|
+
|
|
15
|
+
// src/relay-harness.ts
|
|
16
|
+
import { joinRelay } from "@irtio/client";
|
|
17
|
+
import {
|
|
18
|
+
ErrorCode,
|
|
19
|
+
FrameType,
|
|
20
|
+
PRESENCE_COLLECTION,
|
|
21
|
+
decodeFrame,
|
|
22
|
+
decodeHello,
|
|
23
|
+
decodeMsg,
|
|
24
|
+
decodePing,
|
|
25
|
+
encodeErrorPayload,
|
|
26
|
+
encodeFrame,
|
|
27
|
+
encodeMsg,
|
|
28
|
+
encodePong,
|
|
29
|
+
encodeWelcome,
|
|
30
|
+
formatError,
|
|
31
|
+
relaySchema
|
|
32
|
+
} from "@irtio/protocol";
|
|
33
|
+
import { Mulberry32 } from "@irtio/runtime";
|
|
34
|
+
import { FakeClock, frameTypeName } from "@irtio/runtime/test";
|
|
35
|
+
import {
|
|
36
|
+
createDirtySet,
|
|
37
|
+
createState,
|
|
38
|
+
encodeDelta,
|
|
39
|
+
encodeSnapshot,
|
|
40
|
+
markAdd,
|
|
41
|
+
markField,
|
|
42
|
+
markRemove
|
|
43
|
+
} from "@irtio/schema";
|
|
44
|
+
var TEST_URL = "ws://localhost:7070";
|
|
45
|
+
var TEST_KEY = "test-relay";
|
|
46
|
+
var DEFAULT_MAX_CLIENTS = 64;
|
|
47
|
+
var MICROTASK_TURNS = 8;
|
|
48
|
+
var SETTLE_ROUNDS = 64;
|
|
49
|
+
var JOIN_TIMEOUT_MS = 3e4;
|
|
50
|
+
var DROPPABLE = /* @__PURE__ */ new Set([FrameType.MSG]);
|
|
51
|
+
async function microtasks(turns = MICROTASK_TURNS) {
|
|
52
|
+
for (let i = 0; i < turns; i++) await Promise.resolve();
|
|
53
|
+
}
|
|
54
|
+
var CLIENTS = relaySchema.collection(PRESENCE_COLLECTION);
|
|
55
|
+
function fieldIndex(name) {
|
|
56
|
+
const idx = CLIENTS.fieldIndex.get(name);
|
|
57
|
+
if (idx === void 0) throw new Error(`relaySchema presence has no ${name} field`);
|
|
58
|
+
return idx;
|
|
59
|
+
}
|
|
60
|
+
var ROLE_IDX = fieldIndex("role");
|
|
61
|
+
var NAME_IDX = fieldIndex("name");
|
|
62
|
+
var CONNECTED_IDX = fieldIndex("connected");
|
|
63
|
+
var RelayState = class {
|
|
64
|
+
state = createState(relaySchema);
|
|
65
|
+
tick = 0;
|
|
66
|
+
get clients() {
|
|
67
|
+
return this.state[PRESENCE_COLLECTION];
|
|
68
|
+
}
|
|
69
|
+
has(clientId) {
|
|
70
|
+
return this.clients.has(clientId);
|
|
71
|
+
}
|
|
72
|
+
get size() {
|
|
73
|
+
return this.clients.size;
|
|
74
|
+
}
|
|
75
|
+
add(clientId, role, name) {
|
|
76
|
+
const dirty = createDirtySet();
|
|
77
|
+
const existing = this.clients.get(clientId);
|
|
78
|
+
if (existing) {
|
|
79
|
+
existing.role = role;
|
|
80
|
+
existing.name = name;
|
|
81
|
+
existing.connected = true;
|
|
82
|
+
markField(dirty, PRESENCE_COLLECTION, clientId, [ROLE_IDX]);
|
|
83
|
+
markField(dirty, PRESENCE_COLLECTION, clientId, [NAME_IDX]);
|
|
84
|
+
markField(dirty, PRESENCE_COLLECTION, clientId, [CONNECTED_IDX]);
|
|
85
|
+
} else {
|
|
86
|
+
this.clients.add(clientId, { clientId, role, name, connected: true }, { owner: "" });
|
|
87
|
+
markAdd(dirty, PRESENCE_COLLECTION, clientId);
|
|
88
|
+
}
|
|
89
|
+
this.tick++;
|
|
90
|
+
return encodeDelta(relaySchema, this.state, dirty, { tick: this.tick });
|
|
91
|
+
}
|
|
92
|
+
setConnected(clientId, connected) {
|
|
93
|
+
const rec = this.clients.get(clientId);
|
|
94
|
+
if (!rec || rec.connected === connected) return void 0;
|
|
95
|
+
rec.connected = connected;
|
|
96
|
+
const dirty = createDirtySet();
|
|
97
|
+
markField(dirty, PRESENCE_COLLECTION, clientId, [CONNECTED_IDX]);
|
|
98
|
+
this.tick++;
|
|
99
|
+
return encodeDelta(relaySchema, this.state, dirty, { tick: this.tick });
|
|
100
|
+
}
|
|
101
|
+
remove(clientId) {
|
|
102
|
+
if (!this.clients.remove(clientId)) return void 0;
|
|
103
|
+
const dirty = createDirtySet();
|
|
104
|
+
markRemove(dirty, PRESENCE_COLLECTION, clientId);
|
|
105
|
+
this.tick++;
|
|
106
|
+
return encodeDelta(relaySchema, this.state, dirty, { tick: this.tick });
|
|
107
|
+
}
|
|
108
|
+
advance() {
|
|
109
|
+
return ++this.tick;
|
|
110
|
+
}
|
|
111
|
+
snapshot() {
|
|
112
|
+
return encodeSnapshot(relaySchema, this.state, { tick: this.tick });
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var TestRelayHarness = class {
|
|
116
|
+
clock = new FakeClock();
|
|
117
|
+
scheduler;
|
|
118
|
+
rng;
|
|
119
|
+
relay = new RelayState();
|
|
120
|
+
roomId;
|
|
121
|
+
maxClients;
|
|
122
|
+
latency;
|
|
123
|
+
links = [];
|
|
124
|
+
byId = /* @__PURE__ */ new Map();
|
|
125
|
+
resumeTokens = /* @__PURE__ */ new Map();
|
|
126
|
+
clientList = [];
|
|
127
|
+
traceLog = [];
|
|
128
|
+
nextClientId = 1;
|
|
129
|
+
activity = 0;
|
|
130
|
+
stopped = false;
|
|
131
|
+
constructor(options) {
|
|
132
|
+
this.roomId = options.roomId ?? "test-relay";
|
|
133
|
+
this.maxClients = options.maxClients ?? DEFAULT_MAX_CLIENTS;
|
|
134
|
+
this.latency = options.latency;
|
|
135
|
+
this.rng = new Mulberry32(options.seed ?? 1);
|
|
136
|
+
this.scheduler = clockScheduler(this.clock);
|
|
137
|
+
}
|
|
138
|
+
get clients() {
|
|
139
|
+
return this.clientList;
|
|
140
|
+
}
|
|
141
|
+
get trace() {
|
|
142
|
+
return this.traceLog;
|
|
143
|
+
}
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
// The link
|
|
146
|
+
// -------------------------------------------------------------------------
|
|
147
|
+
transportFor() {
|
|
148
|
+
return {
|
|
149
|
+
connect: () => {
|
|
150
|
+
const link = {
|
|
151
|
+
socket: void 0,
|
|
152
|
+
clientId: `c${this.nextClientId++}`,
|
|
153
|
+
role: "",
|
|
154
|
+
name: "",
|
|
155
|
+
joined: false,
|
|
156
|
+
connected: true,
|
|
157
|
+
nextIn: this.clock.time,
|
|
158
|
+
nextOut: this.clock.time
|
|
159
|
+
};
|
|
160
|
+
link.socket = new InProcessSocket(
|
|
161
|
+
(bytes) => this.fromClient(link, bytes),
|
|
162
|
+
() => this.linkClosed(link)
|
|
163
|
+
);
|
|
164
|
+
this.links.push(link);
|
|
165
|
+
this.clock.setTimeout(() => link.socket.open(), 0);
|
|
166
|
+
this.activity++;
|
|
167
|
+
return link.socket;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
linkClosed(link) {
|
|
172
|
+
if (!link.connected) return;
|
|
173
|
+
link.connected = false;
|
|
174
|
+
this.byId.delete(link.clientId);
|
|
175
|
+
if (!link.joined || this.stopped) return;
|
|
176
|
+
link.joined = false;
|
|
177
|
+
const delta = this.relay.remove(link.clientId);
|
|
178
|
+
if (delta) this.broadcastDelta(delta, link.clientId);
|
|
179
|
+
}
|
|
180
|
+
dropClient(clientId, options) {
|
|
181
|
+
const link = this.byId.get(clientId);
|
|
182
|
+
if (!link || !link.connected || link.socket.closed) return;
|
|
183
|
+
link.connected = false;
|
|
184
|
+
this.byId.delete(clientId);
|
|
185
|
+
const delta = this.relay.setConnected(clientId, false);
|
|
186
|
+
if (delta) this.broadcastDelta(delta, clientId);
|
|
187
|
+
link.socket.hangUp(options);
|
|
188
|
+
}
|
|
189
|
+
schedule(link, dir, type, run) {
|
|
190
|
+
const loss = this.latency?.loss ?? 0;
|
|
191
|
+
if (loss > 0 && DROPPABLE.has(type) && this.rng.next() < loss) return false;
|
|
192
|
+
const jitterMs = this.latency?.jitterMs ?? 0;
|
|
193
|
+
const oneWay = (this.latency?.rttMs ?? 0) / 2 + (jitterMs > 0 ? this.rng.next() * jitterMs : 0);
|
|
194
|
+
const floor = dir === "in" ? link.nextIn : link.nextOut;
|
|
195
|
+
const at = Math.max(this.clock.time + oneWay, floor);
|
|
196
|
+
if (dir === "in") link.nextIn = at;
|
|
197
|
+
else link.nextOut = at;
|
|
198
|
+
this.activity++;
|
|
199
|
+
this.clock.setTimeout(() => {
|
|
200
|
+
this.activity++;
|
|
201
|
+
run();
|
|
202
|
+
}, at - this.clock.time);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
// -------------------------------------------------------------------------
|
|
206
|
+
// Client to relay
|
|
207
|
+
// -------------------------------------------------------------------------
|
|
208
|
+
fromClient(link, frame) {
|
|
209
|
+
let type;
|
|
210
|
+
let payload;
|
|
211
|
+
try {
|
|
212
|
+
const decoded = decodeFrame(frame);
|
|
213
|
+
type = decoded.type;
|
|
214
|
+
payload = decoded.payload;
|
|
215
|
+
} catch {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (type === FrameType.HELLO) this.preresolve(link, payload);
|
|
219
|
+
this.record("in", link.clientId, type, frame.length);
|
|
220
|
+
this.schedule(link, "in", type, () => this.toRelay(link, type, payload));
|
|
221
|
+
}
|
|
222
|
+
preresolve(link, payload) {
|
|
223
|
+
try {
|
|
224
|
+
const hello = decodeHello(payload);
|
|
225
|
+
const resumed = hello.resumeToken ? this.resumeTokens.get(hello.resumeToken) : void 0;
|
|
226
|
+
if (resumed) link.clientId = resumed;
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
toRelay(link, type, payload) {
|
|
231
|
+
if (!link.connected || this.stopped) return;
|
|
232
|
+
if (type === FrameType.HELLO) {
|
|
233
|
+
this.handleHello(link, payload);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (type === FrameType.PING) {
|
|
237
|
+
let t = 0;
|
|
238
|
+
try {
|
|
239
|
+
t = decodePing(payload).t;
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
this.toClient(
|
|
243
|
+
link,
|
|
244
|
+
encodeFrame(FrameType.PONG, encodePong({ t, serverTick: this.relay.tick }))
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!link.joined) return;
|
|
249
|
+
if (type !== FrameType.MSG) return;
|
|
250
|
+
this.relayMsg(link, payload);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
handleHello(link, payload) {
|
|
254
|
+
let role;
|
|
255
|
+
let name;
|
|
256
|
+
try {
|
|
257
|
+
const hello = decodeHello(payload);
|
|
258
|
+
role = hello.role;
|
|
259
|
+
name = hello.name;
|
|
260
|
+
} catch {
|
|
261
|
+
this.fail(link, "E_BAD_FRAME", "malformed HELLO");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (!this.relay.has(link.clientId) && this.relay.size >= this.maxClients) {
|
|
265
|
+
this.fail(link, "E_ROOM_FULL", formatError("E_ROOM_FULL", { roomId: this.roomId }));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
link.role = role ?? "";
|
|
269
|
+
link.name = name ?? "";
|
|
270
|
+
const delta = this.relay.add(link.clientId, link.role, link.name);
|
|
271
|
+
link.joined = true;
|
|
272
|
+
this.byId.set(link.clientId, link);
|
|
273
|
+
const resumeToken = `resume:${link.clientId}`;
|
|
274
|
+
this.resumeTokens.set(resumeToken, link.clientId);
|
|
275
|
+
this.toClient(
|
|
276
|
+
link,
|
|
277
|
+
encodeFrame(
|
|
278
|
+
FrameType.WELCOME,
|
|
279
|
+
encodeWelcome({
|
|
280
|
+
clientId: link.clientId,
|
|
281
|
+
role: link.role,
|
|
282
|
+
tick: this.relay.tick,
|
|
283
|
+
snapshot: this.relay.snapshot(),
|
|
284
|
+
resumeToken,
|
|
285
|
+
roomId: this.roomId,
|
|
286
|
+
tickIntervalMs: 0
|
|
287
|
+
// relay rooms have no tick
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
);
|
|
291
|
+
this.broadcastDelta(delta, link.clientId);
|
|
292
|
+
}
|
|
293
|
+
fail(link, code, message) {
|
|
294
|
+
this.toClient(
|
|
295
|
+
link,
|
|
296
|
+
encodeFrame(
|
|
297
|
+
FrameType.ERROR,
|
|
298
|
+
encodeErrorPayload({ code: ErrorCode[code].code, message, fatal: true })
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
relayMsg(link, payload) {
|
|
303
|
+
let target;
|
|
304
|
+
let body;
|
|
305
|
+
try {
|
|
306
|
+
const msg = decodeMsg(payload);
|
|
307
|
+
target = msg.target;
|
|
308
|
+
body = msg.payload;
|
|
309
|
+
} catch {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (target.kind === "server") return;
|
|
313
|
+
const out = encodeFrame(
|
|
314
|
+
FrameType.MSG,
|
|
315
|
+
encodeMsg({ target: { kind: "client", clientId: link.clientId }, payload: body })
|
|
316
|
+
);
|
|
317
|
+
for (const peer of this.links) {
|
|
318
|
+
if (peer === link || !peer.joined || !peer.connected) continue;
|
|
319
|
+
if (target.kind === "client" && peer.clientId !== target.clientId) continue;
|
|
320
|
+
if (target.kind === "role" && peer.role !== target.role) continue;
|
|
321
|
+
this.toClient(peer, out);
|
|
322
|
+
}
|
|
323
|
+
this.relay.advance();
|
|
324
|
+
}
|
|
325
|
+
// -------------------------------------------------------------------------
|
|
326
|
+
// Relay to client
|
|
327
|
+
// -------------------------------------------------------------------------
|
|
328
|
+
toClient(link, frame) {
|
|
329
|
+
const type = frame[0] ?? -1;
|
|
330
|
+
this.record("out", link.clientId, type, frame.length);
|
|
331
|
+
this.schedule(link, "out", type, () => link.socket.deliver(frame));
|
|
332
|
+
}
|
|
333
|
+
/** Sends a presence delta to everyone except (optionally) the client it is about. */
|
|
334
|
+
broadcastDelta(delta, exceptClientId) {
|
|
335
|
+
const out = encodeFrame(FrameType.DELTA, delta);
|
|
336
|
+
for (const peer of this.links) {
|
|
337
|
+
if (!peer.joined || !peer.connected) continue;
|
|
338
|
+
if (exceptClientId !== void 0 && peer.clientId === exceptClientId) continue;
|
|
339
|
+
this.toClient(peer, out);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
record(dir, clientId, type, bytes) {
|
|
343
|
+
let frame;
|
|
344
|
+
try {
|
|
345
|
+
frame = frameTypeName(type);
|
|
346
|
+
} catch {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
this.traceLog.push({ dir, clientId, frame, tick: this.relay.tick, bytes, at: this.clock.time });
|
|
350
|
+
}
|
|
351
|
+
// -------------------------------------------------------------------------
|
|
352
|
+
// Clock
|
|
353
|
+
// -------------------------------------------------------------------------
|
|
354
|
+
async settle() {
|
|
355
|
+
for (let i = 0; i < SETTLE_ROUNDS; i++) {
|
|
356
|
+
const before = this.activity;
|
|
357
|
+
await microtasks();
|
|
358
|
+
this.clock.advance(0);
|
|
359
|
+
if (this.activity === before) return;
|
|
360
|
+
}
|
|
361
|
+
throw new Error("testRelay: the frame pump did not settle");
|
|
362
|
+
}
|
|
363
|
+
async tick(n = 1) {
|
|
364
|
+
for (let i = 0; i < n; i++) {
|
|
365
|
+
this.clock.advance(0);
|
|
366
|
+
await this.settle();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async run(ms) {
|
|
370
|
+
this.clock.advance(ms);
|
|
371
|
+
await this.settle();
|
|
372
|
+
}
|
|
373
|
+
async until(pred, options = {}) {
|
|
374
|
+
const maxTicks = options.maxTicks ?? 1e3;
|
|
375
|
+
await this.settle();
|
|
376
|
+
if (pred()) return;
|
|
377
|
+
for (let i = 0; i < maxTicks; i++) {
|
|
378
|
+
await this.run(1);
|
|
379
|
+
if (pred()) return;
|
|
380
|
+
}
|
|
381
|
+
throw new Error(`t.until: predicate still false after ${maxTicks} ticks`);
|
|
382
|
+
}
|
|
383
|
+
async join(a, b) {
|
|
384
|
+
if (typeof a === "number") {
|
|
385
|
+
const out = [];
|
|
386
|
+
for (let i = 0; i < a; i++) out.push(await this.joinOne(b ?? {}));
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
return this.joinOne(a ?? {});
|
|
390
|
+
}
|
|
391
|
+
async joinOne(spec) {
|
|
392
|
+
const received = [];
|
|
393
|
+
const room = await this.awaitJoin(
|
|
394
|
+
joinRelay({
|
|
395
|
+
url: TEST_URL,
|
|
396
|
+
key: TEST_KEY,
|
|
397
|
+
room: this.roomId,
|
|
398
|
+
transport: this.transportFor(),
|
|
399
|
+
scheduler: this.scheduler,
|
|
400
|
+
...spec.role !== void 0 ? { role: spec.role } : {},
|
|
401
|
+
...spec.name !== void 0 ? { name: spec.name } : {}
|
|
402
|
+
})
|
|
403
|
+
);
|
|
404
|
+
room.onMessage((from, bytes) => received.push({ from, bytes }));
|
|
405
|
+
const client = Object.create(room);
|
|
406
|
+
Object.defineProperties(client, {
|
|
407
|
+
received: { get: () => received, enumerable: true },
|
|
408
|
+
drop: {
|
|
409
|
+
value: (options) => this.dropClient(room.me, options),
|
|
410
|
+
enumerable: true
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
this.clientList.push(client);
|
|
414
|
+
return client;
|
|
415
|
+
}
|
|
416
|
+
async awaitJoin(promise) {
|
|
417
|
+
let settled = false;
|
|
418
|
+
let value;
|
|
419
|
+
let error;
|
|
420
|
+
promise.then(
|
|
421
|
+
(v) => {
|
|
422
|
+
settled = true;
|
|
423
|
+
value = v;
|
|
424
|
+
},
|
|
425
|
+
(e) => {
|
|
426
|
+
settled = true;
|
|
427
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
this.clock.advance(0);
|
|
431
|
+
await microtasks();
|
|
432
|
+
for (let ms = 0; ms < JOIN_TIMEOUT_MS && !settled; ms++) {
|
|
433
|
+
this.clock.advance(1);
|
|
434
|
+
await microtasks();
|
|
435
|
+
}
|
|
436
|
+
if (error !== void 0) throw error;
|
|
437
|
+
if (!settled) throw new Error("testRelay: join never resolved");
|
|
438
|
+
await this.settle();
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
stop() {
|
|
442
|
+
this.stopped = true;
|
|
443
|
+
for (const client of this.clientList) client.leave();
|
|
444
|
+
for (const link of this.links) link.socket.hangUp();
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/index.ts
|
|
449
|
+
async function testRoom(definition, options = {}) {
|
|
450
|
+
return new TestHarness(definition, options);
|
|
451
|
+
}
|
|
452
|
+
async function testRelay(options = {}) {
|
|
453
|
+
return new TestRelayHarness(options);
|
|
454
|
+
}
|
|
455
|
+
export {
|
|
456
|
+
TRACE_TAIL,
|
|
457
|
+
TestHarness,
|
|
458
|
+
TestRelayHarness,
|
|
459
|
+
clockScheduler,
|
|
460
|
+
divergence,
|
|
461
|
+
matchers,
|
|
462
|
+
materialize,
|
|
463
|
+
testRelay,
|
|
464
|
+
testRoom,
|
|
465
|
+
toHaveConverged,
|
|
466
|
+
toHaveNoVisibilityLeaks,
|
|
467
|
+
toHaveRejected,
|
|
468
|
+
toStayUnderBandwidth
|
|
469
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { m as matchers, t as toHaveConverged, a as toHaveNoVisibilityLeaks, b as toHaveRejected, c as toStayUnderBandwidth } from './assertions-BP4as6ol.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `@irtio/testing/matchers` — the Vitest entry.
|
|
5
|
+
*
|
|
6
|
+
* Importing this module registers the four matchers and augments `expect`'s types. It is a
|
|
7
|
+
* separate entry point on purpose: Vitest is an **optional** peer of `@irtio/testing`, so the main
|
|
8
|
+
* entry (and anything that only wants `testRoom`) never touches it.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import '@irtio/testing/matchers';
|
|
12
|
+
* expect(t).toHaveNoVisibilityLeaks();
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
/** The matchers this package adds to `expect`. */
|
|
16
|
+
interface IrtioMatchers<R = unknown> {
|
|
17
|
+
/** No client's view or received frame named a collection its role must not see. */
|
|
18
|
+
toHaveNoVisibilityLeaks(): R;
|
|
19
|
+
/** Every client's view equals the authority projected to that client's role. */
|
|
20
|
+
toHaveConverged(): R;
|
|
21
|
+
/** Some call to `rpcName` came back as an error `REPLY`. */
|
|
22
|
+
toHaveRejected(rpcName: string): R;
|
|
23
|
+
/** No client averaged more than `bytesPerTick` outbound bytes per room tick. */
|
|
24
|
+
toStayUnderBandwidth(bytesPerTick: number): R;
|
|
25
|
+
}
|
|
26
|
+
declare module 'vitest' {
|
|
27
|
+
interface Matchers<T = any> extends IrtioMatchers<T> {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type { IrtioMatchers };
|
package/dist/matchers.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
matchers,
|
|
3
|
+
toHaveConverged,
|
|
4
|
+
toHaveNoVisibilityLeaks,
|
|
5
|
+
toHaveRejected,
|
|
6
|
+
toStayUnderBandwidth
|
|
7
|
+
} from "./chunk-O3OZKC3D.js";
|
|
8
|
+
|
|
9
|
+
// src/matchers.ts
|
|
10
|
+
import { expect } from "vitest";
|
|
11
|
+
expect.extend(matchers);
|
|
12
|
+
export {
|
|
13
|
+
matchers,
|
|
14
|
+
toHaveConverged,
|
|
15
|
+
toHaveNoVisibilityLeaks,
|
|
16
|
+
toHaveRejected,
|
|
17
|
+
toStayUnderBandwidth
|
|
18
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@irtio/testing",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "irtio's blessed test API: testRoom() in-process rooms with real client semantics, plus Vitest matchers",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./matchers": {
|
|
18
|
+
"types": "./dist/matchers.d.ts",
|
|
19
|
+
"import": "./dist/matchers.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@irtio/client": "0.1.0",
|
|
27
|
+
"@irtio/protocol": "0.1.0",
|
|
28
|
+
"@irtio/runtime": "0.1.0",
|
|
29
|
+
"@irtio/server": "0.1.0",
|
|
30
|
+
"@irtio/schema": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"vitest": ">=3"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"vitest": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"test": "vitest run"
|
|
43
|
+
}
|
|
44
|
+
}
|