@joystyk/testing 0.0.0-stage → 0.2.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +191 -2
  5. package/dist/builders.d.ts +59 -0
  6. package/dist/builders.js +129 -0
  7. package/dist/builders.js.map +1 -0
  8. package/dist/clock.d.ts +30 -0
  9. package/dist/clock.js +58 -0
  10. package/dist/clock.js.map +1 -0
  11. package/dist/deep.d.ts +25 -0
  12. package/dist/deep.js +105 -0
  13. package/dist/deep.js.map +1 -0
  14. package/dist/engine-conformance.d.ts +166 -0
  15. package/dist/engine-conformance.js +995 -0
  16. package/dist/engine-conformance.js.map +1 -0
  17. package/dist/fixtures/dice-race.d.ts +41 -0
  18. package/dist/fixtures/dice-race.js +153 -0
  19. package/dist/fixtures/dice-race.js.map +1 -0
  20. package/dist/fixtures/draw-play.d.ts +54 -0
  21. package/dist/fixtures/draw-play.js +188 -0
  22. package/dist/fixtures/draw-play.js.map +1 -0
  23. package/dist/fixtures/drift.d.ts +34 -0
  24. package/dist/fixtures/drift.js +109 -0
  25. package/dist/fixtures/drift.js.map +1 -0
  26. package/dist/fixtures/guards.d.ts +11 -0
  27. package/dist/fixtures/guards.js +16 -0
  28. package/dist/fixtures/guards.js.map +1 -0
  29. package/dist/fixtures/hint-puzzle.d.ts +47 -0
  30. package/dist/fixtures/hint-puzzle.js +129 -0
  31. package/dist/fixtures/hint-puzzle.js.map +1 -0
  32. package/dist/fixtures/index.d.ts +6 -0
  33. package/dist/fixtures/index.js +7 -0
  34. package/dist/fixtures/nim.d.ts +34 -0
  35. package/dist/fixtures/nim.js +141 -0
  36. package/dist/fixtures/nim.js.map +1 -0
  37. package/dist/fixtures/sealed-bids.d.ts +46 -0
  38. package/dist/fixtures/sealed-bids.js +141 -0
  39. package/dist/fixtures/sealed-bids.js.map +1 -0
  40. package/dist/index.d.ts +13 -0
  41. package/dist/index.js +14 -0
  42. package/dist/replay.d.ts +58 -0
  43. package/dist/replay.js +110 -0
  44. package/dist/replay.js.map +1 -0
  45. package/dist/report.d.ts +68 -0
  46. package/dist/report.js +104 -0
  47. package/dist/report.js.map +1 -0
  48. package/dist/rng.d.ts +18 -0
  49. package/dist/rng.js +25 -0
  50. package/dist/rng.js.map +1 -0
  51. package/dist/schema.d.ts +13 -0
  52. package/dist/schema.js +21 -0
  53. package/dist/schema.js.map +1 -0
  54. package/dist/session-ports.d.ts +16 -0
  55. package/dist/session-ports.js +32 -0
  56. package/dist/session-ports.js.map +1 -0
  57. package/dist/simulation-conformance.d.ts +71 -0
  58. package/dist/simulation-conformance.js +312 -0
  59. package/dist/simulation-conformance.js.map +1 -0
  60. package/dist/storage.d.ts +29 -0
  61. package/dist/storage.js +63 -0
  62. package/dist/storage.js.map +1 -0
  63. package/dist/vitest.d.ts +32 -0
  64. package/dist/vitest.js +45 -0
  65. package/dist/vitest.js.map +1 -0
  66. package/dist/waiting.d.ts +58 -0
  67. package/dist/waiting.js +97 -0
  68. package/dist/waiting.js.map +1 -0
  69. package/package.json +55 -3
@@ -0,0 +1,97 @@
1
+ //#region src/waiting.ts
2
+ var host = globalThis;
3
+ /**
4
+ * The default wall-clock budget for a wait: generous, but under the browser and e2e
5
+ * projects' 60-second test timeout, so a wait that fails names what never happened instead of
6
+ * ending in a bare timeout.
7
+ */
8
+ var WAIT_BUDGET_MS = 45e3;
9
+ /** Resolves after `ms` milliseconds: for yielding to the page, never for waiting on a result. */
10
+ var pause = (ms) => new Promise((resolve) => host.setTimeout(resolve, ms));
11
+ /**
12
+ * Resolves on a fresh task, without the 4 ms clamp nested timers get, so the events,
13
+ * commands and due timers queued before it can run.
14
+ */
15
+ var nextTask = () => {
16
+ const Channel = host.MessageChannel;
17
+ if (Channel === void 0) return pause(0);
18
+ return new Promise((resolve) => {
19
+ const channel = new Channel();
20
+ channel.port1.onmessage = () => {
21
+ channel.port1.close();
22
+ resolve();
23
+ };
24
+ channel.port2.postMessage(null);
25
+ });
26
+ };
27
+ /**
28
+ * Resolves once `condition` holds, checking every few milliseconds. Fails with "Timed out
29
+ * waiting for <what>" after the budget, which by default is most of the test's own timeout.
30
+ */
31
+ var waitUntil = async (condition, what, options = {}) => {
32
+ const budget = options.timeoutMs ?? 45e3;
33
+ const start = host.performance.now();
34
+ while (!condition()) {
35
+ if (host.performance.now() - start > budget) throw new Error(`Timed out after ${String(budget)} ms waiting for ${what}.`);
36
+ await pause(options.intervalMs ?? 10);
37
+ }
38
+ };
39
+ /** A manual frame scheduler for a fixed-step runner, at `frameMs` per frame (default 60 Hz). */
40
+ var manualFrames = (frameMs = 1e3 / 60) => {
41
+ let pending = null;
42
+ let now = 0;
43
+ const port = { request: (callback) => {
44
+ pending = callback;
45
+ return () => {
46
+ if (pending === callback) pending = null;
47
+ };
48
+ } };
49
+ const run = (count) => {
50
+ let ran = 0;
51
+ for (let i = 0; i < count; i += 1) {
52
+ const callback = pending;
53
+ pending = null;
54
+ if (callback !== null) {
55
+ ran += 1;
56
+ callback(now);
57
+ }
58
+ now += frameMs;
59
+ }
60
+ return ran;
61
+ };
62
+ const until = async (condition, what, options = {}) => {
63
+ const maxFrames = options.maxFrames ?? 3600;
64
+ const budget = options.timeoutMs ?? 45e3;
65
+ const start = host.performance.now();
66
+ let frames = 0;
67
+ while (!condition()) {
68
+ if (frames >= maxFrames) throw new Error(`Gave up waiting for ${what} after ${String(maxFrames)} frames.`);
69
+ if (host.performance.now() - start > budget) throw new Error(`Timed out after ${String(budget)} ms waiting for ${what} (${String(frames)} frames run).`);
70
+ if (pending === null) await pause(10);
71
+ else {
72
+ frames += run(1);
73
+ await nextTask();
74
+ }
75
+ }
76
+ };
77
+ const advance = async (count) => {
78
+ let ran = 0;
79
+ for (let i = 0; i < count; i += 1) {
80
+ await nextTask();
81
+ ran += run(1);
82
+ }
83
+ await nextTask();
84
+ return ran;
85
+ };
86
+ return {
87
+ port,
88
+ run,
89
+ pending: () => pending !== null,
90
+ until,
91
+ advance
92
+ };
93
+ };
94
+ //#endregion
95
+ export { WAIT_BUDGET_MS, manualFrames, nextTask, pause, waitUntil };
96
+
97
+ //# sourceMappingURL=waiting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"waiting.js","names":[],"sources":["../src/waiting.ts"],"sourcesContent":["// Waiting in tests on a busy machine. The suite shares its machine with other work, so a\n// test never sleeps for a fixed time and hopes, and never gives a condition a budget measured\n// in seconds of wall-clock time it might not get. It waits for the condition itself, with a\n// budget that only a real failure exhausts. A real-time game runs on `manualFrames`, which\n// counts game time in frames the test runs, so load slows the test down but can't change\n// what happens in it. See docs/design-docs/testing-under-load.md.\nimport type { FrameScheduler } from '@joystyk/session';\n\n// This package names neither DOM nor Node types; these exist in both.\nconst host = globalThis as unknown as {\n setTimeout(callback: () => void, ms: number): unknown;\n performance: { now(): number };\n MessageChannel?: new () => {\n port1: { onmessage: (() => void) | null; close(): void };\n port2: { postMessage(message: unknown): void };\n };\n};\n\n/**\n * The default wall-clock budget for a wait: generous, but under the browser and e2e\n * projects' 60-second test timeout, so a wait that fails names what never happened instead of\n * ending in a bare timeout.\n */\nexport const WAIT_BUDGET_MS = 45_000;\n\n/** Resolves after `ms` milliseconds: for yielding to the page, never for waiting on a result. */\nexport const pause = (ms: number): Promise<void> => new Promise((resolve) => host.setTimeout(resolve, ms));\n\n/**\n * Resolves on a fresh task, without the 4 ms clamp nested timers get, so the events,\n * commands and due timers queued before it can run.\n */\nexport const nextTask = (): Promise<void> => {\n const Channel = host.MessageChannel;\n if (Channel === undefined) return pause(0);\n return new Promise((resolve) => {\n const channel = new Channel();\n channel.port1.onmessage = () => {\n channel.port1.close();\n resolve();\n };\n channel.port2.postMessage(null);\n });\n};\n\nexport type WaitOptions = {\n /** The wall-clock budget. Default `WAIT_BUDGET_MS`. */\n readonly timeoutMs?: number;\n /** How often to check. Default 10 ms. */\n readonly intervalMs?: number;\n};\n\n/**\n * Resolves once `condition` holds, checking every few milliseconds. Fails with \"Timed out\n * waiting for <what>\" after the budget, which by default is most of the test's own timeout.\n */\nexport const waitUntil = async (condition: () => boolean, what: string, options: WaitOptions = {}): Promise<void> => {\n const budget = options.timeoutMs ?? WAIT_BUDGET_MS;\n const start = host.performance.now();\n while (!condition()) {\n if (host.performance.now() - start > budget)\n throw new Error(`Timed out after ${String(budget)} ms waiting for ${what}.`);\n await pause(options.intervalMs ?? 10);\n }\n};\n\nexport type FrameWaitOptions = {\n /** The most frames that may run a callback before the wait fails: game time. Default 3600 (a minute at 60 Hz). */\n readonly maxFrames?: number;\n /** The wall-clock budget, for waits on the UI while the runner is paused. Default `WAIT_BUDGET_MS`. */\n readonly timeoutMs?: number;\n};\n\n/** A frame scheduler that runs only when the test says so. */\nexport interface ManualFrames {\n /** Pass this as the runner's `frames` port. */\n readonly port: FrameScheduler;\n /**\n * Runs `count` frames, each `frameMs` after the last, and returns how many of them had a\n * callback waiting. A paused or finished runner requests none, so it returns 0.\n */\n run(count: number): number;\n /** True when a callback waits for the next frame. */\n pending(): boolean;\n /**\n * Runs `count` frames, yielding to the page between them as `until` does, so input that\n * arrives meanwhile is handled. For checking that nothing happens over some game time.\n * Returns how many frames had a callback waiting.\n */\n advance(count: number): Promise<number>;\n /**\n * Runs one frame at a time until `condition` holds, yielding to the page between frames\n * so events, commands and timers run. Game time can't outrun the check, so a phase that\n * lasts a few ticks is never skipped, and a busy machine only makes the wait take longer.\n * Fails after `maxFrames` frames that ran a callback, or after the wall-clock budget.\n */\n until(condition: () => boolean, what: string, options?: FrameWaitOptions): Promise<void>;\n}\n\n/** A manual frame scheduler for a fixed-step runner, at `frameMs` per frame (default 60 Hz). */\nexport const manualFrames = (frameMs = 1000 / 60): ManualFrames => {\n let pending: ((now: number) => void) | null = null;\n let now = 0;\n const port: FrameScheduler = {\n request: (callback) => {\n pending = callback;\n return () => {\n if (pending === callback) pending = null;\n };\n },\n };\n const run = (count: number): number => {\n let ran = 0;\n for (let i = 0; i < count; i += 1) {\n const callback = pending;\n pending = null;\n if (callback !== null) {\n ran += 1;\n callback(now);\n }\n now += frameMs;\n }\n return ran;\n };\n const until = async (condition: () => boolean, what: string, options: FrameWaitOptions = {}): Promise<void> => {\n const maxFrames = options.maxFrames ?? 3600;\n const budget = options.timeoutMs ?? WAIT_BUDGET_MS;\n const start = host.performance.now();\n let frames = 0;\n while (!condition()) {\n if (frames >= maxFrames) throw new Error(`Gave up waiting for ${what} after ${String(maxFrames)} frames.`);\n if (host.performance.now() - start > budget)\n throw new Error(`Timed out after ${String(budget)} ms waiting for ${what} (${String(frames)} frames run).`);\n if (pending === null) {\n // Nothing to run: the runner is paused or waiting on the UI. Let the page catch up.\n await pause(10);\n } else {\n frames += run(1);\n await nextTask();\n }\n }\n };\n const advance = async (count: number): Promise<number> => {\n let ran = 0;\n for (let i = 0; i < count; i += 1) {\n await nextTask();\n ran += run(1);\n }\n await nextTask();\n return ran;\n };\n return { port, run, pending: () => pending !== null, until, advance };\n};\n"],"mappings":";AASA,IAAM,OAAO;;;;;;AAcb,IAAa,iBAAiB;;AAG9B,IAAa,SAAS,OAA8B,IAAI,SAAS,YAAY,KAAK,WAAW,SAAS,EAAE,CAAC;;;;;AAMzG,IAAa,iBAAgC;CAC3C,MAAM,UAAU,KAAK;CACrB,IAAI,YAAY,KAAA,GAAW,OAAO,MAAM,CAAC;CACzC,OAAO,IAAI,SAAS,YAAY;EAC9B,MAAM,UAAU,IAAI,QAAQ;EAC5B,QAAQ,MAAM,kBAAkB;GAC9B,QAAQ,MAAM,MAAM;GACpB,QAAQ;EACV;EACA,QAAQ,MAAM,YAAY,IAAI;CAChC,CAAC;AACH;;;;;AAaA,IAAa,YAAY,OAAO,WAA0B,MAAc,UAAuB,CAAC,MAAqB;CACnH,MAAM,SAAS,QAAQ,aAAA;CACvB,MAAM,QAAQ,KAAK,YAAY,IAAI;CACnC,OAAO,CAAC,UAAU,GAAG;EACnB,IAAI,KAAK,YAAY,IAAI,IAAI,QAAQ,QACnC,MAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,EAAE,kBAAkB,KAAK,EAAE;EAC7E,MAAM,MAAM,QAAQ,cAAc,EAAE;CACtC;AACF;;AAoCA,IAAa,gBAAgB,UAAU,MAAO,OAAqB;CACjE,IAAI,UAA0C;CAC9C,IAAI,MAAM;CACV,MAAM,OAAuB,EAC3B,UAAU,aAAa;EACrB,UAAU;EACV,aAAa;GACX,IAAI,YAAY,UAAU,UAAU;EACtC;CACF,EACF;CACA,MAAM,OAAO,UAA0B;EACrC,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;GACjC,MAAM,WAAW;GACjB,UAAU;GACV,IAAI,aAAa,MAAM;IACrB,OAAO;IACP,SAAS,GAAG;GACd;GACA,OAAO;EACT;EACA,OAAO;CACT;CACA,MAAM,QAAQ,OAAO,WAA0B,MAAc,UAA4B,CAAC,MAAqB;EAC7G,MAAM,YAAY,QAAQ,aAAa;EACvC,MAAM,SAAS,QAAQ,aAAA;EACvB,MAAM,QAAQ,KAAK,YAAY,IAAI;EACnC,IAAI,SAAS;EACb,OAAO,CAAC,UAAU,GAAG;GACnB,IAAI,UAAU,WAAW,MAAM,IAAI,MAAM,uBAAuB,KAAK,SAAS,OAAO,SAAS,EAAE,SAAS;GACzG,IAAI,KAAK,YAAY,IAAI,IAAI,QAAQ,QACnC,MAAM,IAAI,MAAM,mBAAmB,OAAO,MAAM,EAAE,kBAAkB,KAAK,IAAI,OAAO,MAAM,EAAE,cAAc;GAC5G,IAAI,YAAY,MAEd,MAAM,MAAM,EAAE;QACT;IACL,UAAU,IAAI,CAAC;IACf,MAAM,SAAS;GACjB;EACF;CACF;CACA,MAAM,UAAU,OAAO,UAAmC;EACxD,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;GACjC,MAAM,SAAS;GACf,OAAO,IAAI,CAAC;EACd;EACA,MAAM,SAAS;EACf,OAAO;CACT;CACA,OAAO;EAAE;EAAM;EAAK,eAAe,YAAY;EAAM;EAAO;CAAQ;AACtE"}
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "@joystyk/testing",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Dev-time conformance suites for engines and simulations, plus test RNGs, a fake clock, fake storage and fixture builders.",
5
+ "private": false,
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./fixtures": {
14
+ "types": "./dist/fixtures/index.d.ts",
15
+ "default": "./dist/fixtures/index.js"
16
+ }
17
+ },
18
+ "dependencies": {
19
+ "@joystyk/contracts": "0.2.0",
20
+ "@joystyk/kit": "0.2.0",
21
+ "@joystyk/session": "0.2.0"
22
+ },
23
+ "peerDependencies": {
24
+ "vitest": "4.1.11"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "5.9.3",
28
+ "vite": "8.3.3",
29
+ "vitest": "4.1.11"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE",
38
+ "NOTICE",
39
+ "CHANGELOG.md"
40
+ ],
41
+ "engines": {
42
+ "node": ">=24.15.0"
43
+ },
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/joystyk-games/game-libraries.git",
48
+ "directory": "packages/testing"
49
+ },
50
+ "homepage": "https://github.com/joystyk-games/game-libraries/tree/main/packages/testing#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/joystyk-games/game-libraries/issues"
53
+ },
54
+ "scripts": {
55
+ "typecheck": "tsc --noEmit",
56
+ "build": "node ../../scripts/build-package.ts"
57
+ }
6
58
  }