@irtio/testing 3.1.1 → 4.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/README.md +66 -0
- package/dist/{chunk-47O3HZBE.js → chunk-7GS3V7QO.js} +31 -6
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -1
- package/dist/matchers.js +1 -1
- package/package.json +7 -7
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @irtio/testing
|
|
2
|
+
|
|
3
|
+
Test room logic with real irtio clients and a controllable clock. Check synchronization, RPCs,
|
|
4
|
+
reconnection, and visibility without opening sockets.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install --save-dev @irtio/testing vitest
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Test a player update
|
|
11
|
+
|
|
12
|
+
This test uses the room generated by `npx irtio init --tick --yes`.
|
|
13
|
+
Create or replace `irtio/room.test.ts`:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { expect, test } from 'vitest';
|
|
17
|
+
import { testRoom } from '@irtio/testing';
|
|
18
|
+
import '@irtio/testing/matchers';
|
|
19
|
+
import room from './room.js';
|
|
20
|
+
|
|
21
|
+
test('an owned write reaches another player', async () => {
|
|
22
|
+
const t = await testRoom(room);
|
|
23
|
+
try {
|
|
24
|
+
const [a, b] = await t.join(2, { role: 'player' });
|
|
25
|
+
const player = a.state.players.get(a.id);
|
|
26
|
+
expect(player).toBeDefined();
|
|
27
|
+
player!.x = 10;
|
|
28
|
+
a.flush();
|
|
29
|
+
await t.tick(2);
|
|
30
|
+
|
|
31
|
+
expect(t.state.players.get(a.id)?.x).toBe(10);
|
|
32
|
+
expect(b.state.players.get(a.id)?.x).toBe(10);
|
|
33
|
+
expect(t).toHaveConverged();
|
|
34
|
+
expect(t).toHaveNoVisibilityLeaks();
|
|
35
|
+
} finally {
|
|
36
|
+
t.stop();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx vitest run irtio/room.test.ts
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Drive the clock
|
|
46
|
+
|
|
47
|
+
| Method | Behavior |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `await t.join(spec?)` | Join one client |
|
|
50
|
+
| `await t.join(n, spec?)` | Join multiple clients |
|
|
51
|
+
| `await t.tick(n?)` | Advance server ticks; defaults to one |
|
|
52
|
+
| `await t.run(ms)` | Advance fake time in milliseconds |
|
|
53
|
+
| `await t.until(predicate, options?)` | Advance until a condition holds, or reject at the limit |
|
|
54
|
+
| `t.stop()` | Stop the room and its clients |
|
|
55
|
+
|
|
56
|
+
Await all clock operations. By default, awaiting client RPCs also advances the clock until they
|
|
57
|
+
settle, and a room handler error fails the test. Configure `autoPump` and `failOnHandlerError`
|
|
58
|
+
when testing those behaviors explicitly.
|
|
59
|
+
|
|
60
|
+
`testRelay()` tests schema-less relay messaging. Import `@irtio/testing/matchers` to register
|
|
61
|
+
Vitest matchers; the main entry point also exports assertion functions for other runners.
|
|
62
|
+
For physics rooms, await the corresponding `initPhysics`, `initRapier2d`, or `initMatter`
|
|
63
|
+
initializer before creating the test room.
|
|
64
|
+
|
|
65
|
+
See the [testing reference](https://irt.io/docs/reference/testing) for all options, matchers,
|
|
66
|
+
network conditions, and physics helpers.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/compare.ts
|
|
2
|
+
import { isPerClient } from "@irtio/runtime";
|
|
2
3
|
import {
|
|
3
4
|
cloneValue,
|
|
4
5
|
computeDirty,
|
|
@@ -46,7 +47,7 @@ function divergence(ext, authority, client, visible, memberships = /* @__PURE__
|
|
|
46
47
|
for (const [name, cd] of computeDirty(ext, authority, client)) {
|
|
47
48
|
if (!visible.has(name)) continue;
|
|
48
49
|
const desc = ext.collection(name);
|
|
49
|
-
const membership = desc
|
|
50
|
+
const membership = isPerClient(desc) ? memberships.get(name) : void 0;
|
|
50
51
|
for (const id of cd.added) {
|
|
51
52
|
if (!membership || !membership.has(id)) out.push(`${name}[${id}]: only in the client's view`);
|
|
52
53
|
}
|
|
@@ -108,6 +109,7 @@ import {
|
|
|
108
109
|
RoomCore,
|
|
109
110
|
RoomFullError,
|
|
110
111
|
createVisibilityPolicy,
|
|
112
|
+
isPerClient as isPerClient2,
|
|
111
113
|
visibleNames
|
|
112
114
|
} from "@irtio/runtime";
|
|
113
115
|
import { FakeClock, HarnessHost, frameTypeName } from "@irtio/runtime/test";
|
|
@@ -247,6 +249,7 @@ var DROPPABLE = /* @__PURE__ */ new Set([
|
|
|
247
249
|
]);
|
|
248
250
|
var MICROTASK_TURNS = 8;
|
|
249
251
|
var SETTLE_ROUNDS = 64;
|
|
252
|
+
var RUN_HOPS_MAX = 1e4;
|
|
250
253
|
var JOIN_TIMEOUT_MS = 3e4;
|
|
251
254
|
var HELD_FRAMES_MAX = 16;
|
|
252
255
|
var DROPPED_HELD = new Uint8Array(0);
|
|
@@ -873,16 +876,16 @@ var TestHarness = class {
|
|
|
873
876
|
const seen = this.deliveredIds(link.clientId, dc.name);
|
|
874
877
|
const leaked = dc.ops.filter((op) => {
|
|
875
878
|
if (op.op === "remove") {
|
|
876
|
-
if (keep.has(dc.name)) return desc
|
|
879
|
+
if (keep.has(dc.name)) return isPerClient2(desc) && !seen.has(op.id);
|
|
877
880
|
return !seen.has(op.id);
|
|
878
881
|
}
|
|
879
882
|
if (!keep.has(dc.name)) return true;
|
|
880
|
-
if (desc
|
|
883
|
+
if (!isPerClient2(desc)) return false;
|
|
881
884
|
return !policy.maySeeEntity(desc, op.id);
|
|
882
885
|
});
|
|
883
886
|
for (const op of dc.ops) {
|
|
884
887
|
if (op.op === "remove") seen.delete(op.id);
|
|
885
|
-
else if (desc
|
|
888
|
+
else if (!isPerClient2(desc) || policy.maySeeEntity(desc, op.id)) {
|
|
886
889
|
seen.add(op.id);
|
|
887
890
|
}
|
|
888
891
|
}
|
|
@@ -976,8 +979,30 @@ var TestHarness = class {
|
|
|
976
979
|
await this.settle();
|
|
977
980
|
}
|
|
978
981
|
}
|
|
982
|
+
/**
|
|
983
|
+
* Bug #127: one `clock.advance(ms)` would fire every timer in the window, but a script parked on
|
|
984
|
+
* `await npc.wait(50)` only resumes in a **microtask**, which cannot run until `advance` has
|
|
985
|
+
* returned — so the script's next timer was armed after the window had already gone by and a
|
|
986
|
+
* 1000 ms run took the script exactly one step. So hop instead of leaping: advance to the next
|
|
987
|
+
* due timer, let microtasks drain (continuations run, new timers get armed), and repeat until
|
|
988
|
+
* the target. With nothing armed it is still a single leap.
|
|
989
|
+
*/
|
|
979
990
|
async run(ms) {
|
|
980
|
-
this.clock.
|
|
991
|
+
const target = this.clock.now() + Math.max(0, ms);
|
|
992
|
+
for (let hops = 0; ; hops++) {
|
|
993
|
+
if (hops > RUN_HOPS_MAX) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`testRoom: t.run(${ms}) did not settle \u2014 more than ${RUN_HOPS_MAX} timers came due, which usually means a timer re-arms itself at the same fake time`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const due = this.clock.nextDueAt();
|
|
999
|
+
if (due === void 0 || due > target) {
|
|
1000
|
+
this.clock.advance(target - this.clock.now());
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
this.clock.advance(Math.max(0, due - this.clock.now()));
|
|
1004
|
+
await microtasks();
|
|
1005
|
+
}
|
|
981
1006
|
await this.settle();
|
|
982
1007
|
}
|
|
983
1008
|
/**
|
|
@@ -1182,7 +1207,7 @@ var TestHarness = class {
|
|
|
1182
1207
|
const policy = createVisibilityPolicy(this.ext, this.plain, client.id, role);
|
|
1183
1208
|
const view = client.view;
|
|
1184
1209
|
for (const c of this.ext.collections) {
|
|
1185
|
-
if (c
|
|
1210
|
+
if (isPerClient2(c)) {
|
|
1186
1211
|
const coll = view[c.name];
|
|
1187
1212
|
const ids = coll ? [...coll.ids()].filter((id) => !policy.maySeeEntity(c, id)) : [];
|
|
1188
1213
|
if (ids.length > 0) {
|
package/dist/index.d.ts
CHANGED
|
@@ -566,6 +566,14 @@ declare class TestHarness<S extends AnySchema> implements TestRoom<S> {
|
|
|
566
566
|
*/
|
|
567
567
|
private reportHandlerErrors;
|
|
568
568
|
tick(n?: number): Promise<void>;
|
|
569
|
+
/**
|
|
570
|
+
* Bug #127: one `clock.advance(ms)` would fire every timer in the window, but a script parked on
|
|
571
|
+
* `await npc.wait(50)` only resumes in a **microtask**, which cannot run until `advance` has
|
|
572
|
+
* returned — so the script's next timer was armed after the window had already gone by and a
|
|
573
|
+
* 1000 ms run took the script exactly one step. So hop instead of leaping: advance to the next
|
|
574
|
+
* due timer, let microtasks drain (continuations run, new timers get armed), and repeat until
|
|
575
|
+
* the target. With nothing armed it is still a single leap.
|
|
576
|
+
*/
|
|
569
577
|
run(ms: number): Promise<void>;
|
|
570
578
|
/**
|
|
571
579
|
* Advances until `pred()` holds. In **event mode** a tick is a zero-length clock step, so this
|
package/dist/index.js
CHANGED
package/dist/matchers.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/testing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "irtio's blessed test API: testRoom() in-process rooms with real client semantics, plus Vitest matchers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@irtio/bots": "
|
|
32
|
-
"@irtio/client": "
|
|
33
|
-
"@irtio/protocol": "
|
|
34
|
-
"@irtio/runtime": "
|
|
35
|
-
"@irtio/schema": "
|
|
36
|
-
"@irtio/server": "
|
|
31
|
+
"@irtio/bots": "^4.1.0",
|
|
32
|
+
"@irtio/client": "^4.1.0",
|
|
33
|
+
"@irtio/protocol": "^4.1.0",
|
|
34
|
+
"@irtio/runtime": "^4.1.0",
|
|
35
|
+
"@irtio/schema": "^4.1.0",
|
|
36
|
+
"@irtio/server": "^4.1.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"vitest": ">=3"
|