@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 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.visibility === "spatial-grid" ? memberships.get(name) : void 0;
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.visibility === "spatial-grid" && !seen.has(op.id);
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.visibility !== "spatial-grid") return false;
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.visibility !== "spatial-grid" || policy.maySeeEntity(desc, op.id)) {
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.advance(ms);
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.visibility === "spatial-grid") {
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
@@ -12,7 +12,7 @@ import {
12
12
  toHaveRejected,
13
13
  toStayUnderBandwidth,
14
14
  toStayWithinPrediction
15
- } from "./chunk-47O3HZBE.js";
15
+ } from "./chunk-7GS3V7QO.js";
16
16
 
17
17
  // src/relay-harness.ts
18
18
  import { joinRelay } from "@irtio/client";
package/dist/matchers.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  toHaveRejected,
6
6
  toStayUnderBandwidth,
7
7
  toStayWithinPrediction
8
- } from "./chunk-47O3HZBE.js";
8
+ } from "./chunk-7GS3V7QO.js";
9
9
 
10
10
  // src/matchers.ts
11
11
  import { expect } from "vitest";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irtio/testing",
3
- "version": "3.1.1",
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": "3.1.1",
32
- "@irtio/client": "3.1.1",
33
- "@irtio/protocol": "3.1.1",
34
- "@irtio/runtime": "3.1.1",
35
- "@irtio/schema": "3.1.1",
36
- "@irtio/server": "3.1.1"
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"