@rpgjs/testing 5.0.0-beta.2 → 5.0.0-beta.3

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/dist/index.js CHANGED
@@ -1,378 +1,466 @@
1
- import { mergeConfig } from './node_modules/.pnpm/@signe_di@2.8.3/node_modules/@signe/di/dist/index.js';
2
- import { provideLoadMap, LoadMapToken, startGame, provideRpg, provideClientGlobalConfig, provideClientModules, inject, WebSocketToken, RpgClientEngine, clearInject } from '@rpgjs/client';
3
- import { createServer, provideServerModules, clearInject as clearInject$1 } from '@rpgjs/server';
4
- import { h, Container } from 'canvasengine';
5
- import { Subject, combineLatest, filter, take, firstValueFrom, map, timer, switchMap, throwError, race } from 'rxjs';
6
-
1
+ import { mergeConfig } from "./node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js";
2
+ import { LoadMapToken, RpgClientEngine, WebSocketToken, clearInject, inject, provideClientGlobalConfig, provideClientModules, provideLoadMap, provideRpg, startGame } from "@rpgjs/client";
3
+ import { clearInject as clearInject$1, createServer, provideServerModules } from "@rpgjs/server";
4
+ import { Container, h } from "canvasengine";
5
+ import { Subject, combineLatest, filter, firstValueFrom, map, race, switchMap, take, throwError, timer } from "rxjs";
6
+ //#region src/index.ts
7
+ /**
8
+ * Provides a default map loader for testing environments
9
+ *
10
+ * This function returns a `provideLoadMap` provider that creates mock maps
11
+ * with default dimensions (1024x768) and a minimal component. It's automatically
12
+ * used by `testing()` if no custom `provideLoadMap` is provided in `clientConfig.providers`.
13
+ *
14
+ * @returns A provider function that can be used with `provideLoadMap`
15
+ * @example
16
+ * ```ts
17
+ * // Used automatically by testing()
18
+ * const fixture = await testing([myModule])
19
+ *
20
+ * // Or use directly in clientConfig
21
+ * const fixture = await testing([myModule], {
22
+ * providers: [provideTestingLoadMap()]
23
+ * })
24
+ * ```
25
+ */
7
26
  function provideTestingLoadMap() {
8
- return provideLoadMap((id) => {
9
- return {
10
- id,
11
- data: {
12
- width: 1024,
13
- height: 768,
14
- hitboxes: [],
15
- params: {}
16
- },
17
- component: h(Container),
18
- width: 1024,
19
- height: 768
20
- };
21
- });
27
+ return provideLoadMap((id) => {
28
+ return {
29
+ id,
30
+ data: {
31
+ width: 1024,
32
+ height: 768,
33
+ hitboxes: [],
34
+ params: {}
35
+ },
36
+ component: h(Container),
37
+ width: 1024,
38
+ height: 768
39
+ };
40
+ });
22
41
  }
42
+ /**
43
+ * Normalizes modules input to extract server/client modules from createModule providers or direct module objects
44
+ *
45
+ * @param modules - Array of modules that can be either:
46
+ * - Direct module objects: { server: RpgServer, client: RpgClient }
47
+ * - Providers returned by createModule(): Provider[] with meta.server/client and useValue
48
+ * @returns Object with separate arrays for server and client modules
49
+ * @example
50
+ * ```ts
51
+ * // Direct modules
52
+ * normalizeModules([{ server: serverModule, client: clientModule }])
53
+ *
54
+ * // createModule providers
55
+ * const providers = createModule('MyModule', [{ server: serverModule, client: clientModule }])
56
+ * normalizeModules(providers)
57
+ * ```
58
+ */
23
59
  function normalizeModules(modules) {
24
- if (!modules || modules.length === 0) {
25
- return { serverModules: [], clientModules: [] };
26
- }
27
- const isProviderArray = modules.some(
28
- (item) => item && typeof item === "object" && "meta" in item && "useValue" in item
29
- );
30
- const serverModules = [];
31
- const clientModules = [];
32
- if (!isProviderArray) {
33
- modules.forEach((module) => {
34
- if (module && typeof module === "object") {
35
- if (module.server) {
36
- serverModules.push(module.server);
37
- }
38
- if (module.client) {
39
- clientModules.push(module.client);
40
- }
41
- }
42
- });
43
- return { serverModules, clientModules };
44
- }
45
- const seenUseValues = /* @__PURE__ */ new Set();
46
- modules.forEach((provider) => {
47
- if (!provider || typeof provider !== "object" || !("meta" in provider) || !("useValue" in provider)) {
48
- return;
49
- }
50
- const { useValue } = provider;
51
- if (seenUseValues.has(useValue)) {
52
- return;
53
- }
54
- if (useValue && typeof useValue === "object" && ("server" in useValue || "client" in useValue)) {
55
- seenUseValues.add(useValue);
56
- if (useValue.server) {
57
- serverModules.push(useValue.server);
58
- }
59
- if (useValue.client) {
60
- clientModules.push(useValue.client);
61
- }
62
- }
63
- });
64
- return { serverModules, clientModules };
60
+ if (!modules || modules.length === 0) return {
61
+ serverModules: [],
62
+ clientModules: []
63
+ };
64
+ const isProviderArray = modules.some((item) => item && typeof item === "object" && "meta" in item && "useValue" in item);
65
+ const serverModules = [];
66
+ const clientModules = [];
67
+ if (!isProviderArray) {
68
+ modules.forEach((module) => {
69
+ if (module && typeof module === "object") {
70
+ if (module.server) serverModules.push(module.server);
71
+ if (module.client) clientModules.push(module.client);
72
+ }
73
+ });
74
+ return {
75
+ serverModules,
76
+ clientModules
77
+ };
78
+ }
79
+ const seenUseValues = /* @__PURE__ */ new Set();
80
+ modules.forEach((provider) => {
81
+ if (!provider || typeof provider !== "object" || !("meta" in provider) || !("useValue" in provider)) return;
82
+ const { useValue } = provider;
83
+ if (seenUseValues.has(useValue)) return;
84
+ if (useValue && typeof useValue === "object" && ("server" in useValue || "client" in useValue)) {
85
+ seenUseValues.add(useValue);
86
+ if (useValue.server) serverModules.push(useValue.server);
87
+ if (useValue.client) clientModules.push(useValue.client);
88
+ }
89
+ });
90
+ return {
91
+ serverModules,
92
+ clientModules
93
+ };
65
94
  }
66
- const globalFixtures = [];
95
+ var globalFixtures = [];
96
+ /**
97
+ * Testing utility function to set up server and client instances for unit testing
98
+ *
99
+ * This function creates a test environment with both server and client instances,
100
+ * allowing you to test player interactions, server hooks, and game mechanics.
101
+ *
102
+ * @param modules - Array of modules that can be either:
103
+ * - Direct module objects: { server: RpgServer, client: RpgClient }
104
+ * - Providers returned by createModule(): Provider[] with meta.server/client and useValue
105
+ * @param clientConfig - Optional client configuration
106
+ * @param serverConfig - Optional server configuration
107
+ * @returns Testing fixture with createClient method
108
+ * @example
109
+ * ```ts
110
+ * // Using direct modules
111
+ * const fixture = await testing([{
112
+ * server: serverModule,
113
+ * client: clientModule
114
+ * }])
115
+ *
116
+ * // Using createModule
117
+ * const myModule = createModule('MyModule', [{
118
+ * server: serverModule,
119
+ * client: clientModule
120
+ * }])
121
+ * const fixture = await testing(myModule)
122
+ * ```
123
+ */
67
124
  async function testing(modules = [], clientConfig = {}, serverConfig = {}) {
68
- const { serverModules, clientModules } = normalizeModules(modules);
69
- const mapChangeSubject = new Subject();
70
- const serverClass = createServer({
71
- ...serverConfig,
72
- providers: [
73
- provideServerModules([
74
- ...serverModules,
75
- {
76
- player: {
77
- onJoinMap(player, map2) {
78
- const mapId = map2?.id;
79
- if (mapId) {
80
- mapChangeSubject.next({ mapId, player });
81
- }
82
- }
83
- }
84
- }
85
- ]),
86
- ...serverConfig.providers || []
87
- ]
88
- });
89
- const hasLoadMap = clientConfig.providers?.some((provider) => {
90
- if (Array.isArray(provider)) {
91
- return provider.some((p) => p?.provide === LoadMapToken);
92
- }
93
- return provider?.provide === LoadMapToken;
94
- }) || false;
95
- await startGame(
96
- mergeConfig(
97
- {
98
- ...clientConfig,
99
- providers: [
100
- provideClientGlobalConfig({
101
- // TODO
102
- // bootstrapCanvasOptions: {
103
- // components: mockComponents,
104
- // autoRegister: false,
105
- // },
106
- }),
107
- ...hasLoadMap ? [] : [provideTestingLoadMap()],
108
- // Add only if not already provided
109
- provideClientModules(clientModules),
110
- ...clientConfig.providers || []
111
- ]
112
- },
113
- {
114
- providers: [provideRpg(serverClass, { env: { TEST: "true" } })]
115
- }
116
- )
117
- );
118
- const websocket = inject(WebSocketToken);
119
- const clientEngine = inject(RpgClientEngine);
120
- return {
121
- async createClient() {
122
- const clientObj = {
123
- socket: websocket.getSocket(),
124
- client: clientEngine,
125
- get playerId() {
126
- return Object.keys(websocket.getServer().subRoom.players())[0];
127
- },
128
- get player() {
129
- return websocket.getServer().subRoom.players()[clientObj.playerId];
130
- },
131
- /**
132
- * Wait for player to be on a specific map
133
- *
134
- * This utility function waits for the `onJoinMap` hook to be triggered
135
- * when the player joins the expected map, or throws an error if the timeout is exceeded.
136
- *
137
- * ## Design
138
- *
139
- * Uses RxJS to listen for map change events emitted by `onJoinMap`. The function:
140
- * 1. Checks if the player is already on the expected map
141
- * 2. Subscribes to the `mapChangeSubject` observable
142
- * 3. Filters events to match the expected map ID
143
- * 4. Uses `race` operator with a timer to implement timeout handling
144
- * 5. Resolves with the player when the map change event is received
145
- *
146
- * @param expectedMapId - The expected map ID (without 'map-' prefix, e.g. 'map1')
147
- * @param timeout - Maximum time to wait in milliseconds (default: 5000)
148
- * @returns Promise that resolves when player is on the expected map
149
- * @throws Error if timeout is exceeded
150
- * @example
151
- * ```ts
152
- * const client = await fixture.createClient()
153
- * await client.waitForMapChange('map1')
154
- * ```
155
- */
156
- async waitForMapChange(expectedMapId, timeout = 5e3) {
157
- const currentMap = clientObj.player.getCurrentMap();
158
- if (currentMap?.id === expectedMapId) {
159
- return clientObj.player;
160
- }
161
- const mapChange$ = mapChangeSubject.pipe(
162
- filter((event) => event.mapId === expectedMapId),
163
- take(1),
164
- map((event) => event.player)
165
- );
166
- const timeout$ = timer(timeout).pipe(
167
- take(1),
168
- switchMap(() => {
169
- const currentMap2 = clientObj.player.getCurrentMap();
170
- return throwError(() => new Error(
171
- `Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. Current map: ${currentMap2?.id || "null"}`
172
- ));
173
- })
174
- );
175
- try {
176
- const result = await firstValueFrom(race([mapChange$, timeout$]));
177
- return result;
178
- } catch (error) {
179
- if (error instanceof Error) {
180
- throw error;
181
- }
182
- const currentMap2 = clientObj.player.getCurrentMap();
183
- throw new Error(
184
- `Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. Current map: ${currentMap2?.id || "null"}`
185
- );
186
- }
187
- }
188
- };
189
- return clientObj;
190
- },
191
- get server() {
192
- return websocket.getServer();
193
- },
194
- /**
195
- * Clear all server, client instances and reset the DOM
196
- *
197
- * This method should be called in afterEach to clean up test state.
198
- * It destroys all created client instances, clears the server, and resets the DOM.
199
- *
200
- * @example
201
- * ```ts
202
- * const fixture = await testing([myModule])
203
- *
204
- * afterEach(() => {
205
- * fixture.clear()
206
- * })
207
- * ```
208
- */
209
- clear() {
210
- return clear();
211
- },
212
- async applySyncToClient() {
213
- websocket.getServer().subRoom.applySyncToClient();
214
- await waitForSync(clientEngine);
215
- },
216
- /**
217
- * Manually trigger a game tick for processing inputs and physics
218
- *
219
- * This method is a convenience wrapper around the exported nextTick() function.
220
- * It uses the clientEngine from the fixture, so no client parameter is needed.
221
- *
222
- * @param timestamp - Optional timestamp to use for the tick (default: Date.now())
223
- * @returns Promise that resolves when the tick is complete
224
- *
225
- * @example
226
- * ```ts
227
- * const fixture = await testing([myModule])
228
- *
229
- * // Manually advance the game by one tick
230
- * await fixture.nextTick()
231
- * ```
232
- */
233
- async nextTick(timestamp) {
234
- return nextTick(clientEngine);
235
- },
236
- async nextTickTimes(times, timestamp) {
237
- for (let i = 0; i < times; i++) {
238
- await nextTick(clientEngine);
239
- }
240
- },
241
- async wait(ms) {
242
- return new Promise((resolve) => setTimeout(resolve, ms));
243
- },
244
- async waitUntil(promise) {
245
- let settled = false;
246
- let resolvedValue;
247
- let rejectedError;
248
- promise.then((value) => {
249
- settled = true;
250
- resolvedValue = value;
251
- }).catch((error) => {
252
- settled = true;
253
- rejectedError = error;
254
- });
255
- while (!settled) {
256
- await nextTick(clientEngine);
257
- await new Promise((resolve) => setTimeout(resolve, 0));
258
- }
259
- if (typeof rejectedError !== "undefined") {
260
- throw rejectedError;
261
- }
262
- return resolvedValue;
263
- }
264
- };
125
+ const { serverModules, clientModules } = normalizeModules(modules);
126
+ const mapChangeSubject = new Subject();
127
+ const serverClass = createServer({
128
+ ...serverConfig,
129
+ providers: [provideServerModules([...serverModules, { player: { onJoinMap(player, map) {
130
+ const mapId = map?.id;
131
+ if (mapId) mapChangeSubject.next({
132
+ mapId,
133
+ player
134
+ });
135
+ } } }]), ...serverConfig.providers || []]
136
+ });
137
+ const hasLoadMap = clientConfig.providers?.some((provider) => {
138
+ if (Array.isArray(provider)) return provider.some((p) => p?.provide === LoadMapToken);
139
+ return provider?.provide === LoadMapToken;
140
+ }) || false;
141
+ await startGame(mergeConfig({
142
+ ...clientConfig,
143
+ providers: [
144
+ provideClientGlobalConfig({}),
145
+ ...hasLoadMap ? [] : [provideTestingLoadMap()],
146
+ provideClientModules(clientModules),
147
+ ...clientConfig.providers || []
148
+ ]
149
+ }, { providers: [provideRpg(serverClass, { env: { TEST: "true" } })] }));
150
+ const websocket = inject(WebSocketToken);
151
+ const clientEngine = inject(RpgClientEngine);
152
+ return {
153
+ async createClient() {
154
+ const clientObj = {
155
+ socket: websocket.getSocket(),
156
+ client: clientEngine,
157
+ get playerId() {
158
+ return Object.keys(websocket.getServer().subRoom.players())[0];
159
+ },
160
+ get player() {
161
+ return websocket.getServer().subRoom.players()[clientObj.playerId];
162
+ },
163
+ /**
164
+ * Wait for player to be on a specific map
165
+ *
166
+ * This utility function waits for the `onJoinMap` hook to be triggered
167
+ * when the player joins the expected map, or throws an error if the timeout is exceeded.
168
+ *
169
+ * ## Design
170
+ *
171
+ * Uses RxJS to listen for map change events emitted by `onJoinMap`. The function:
172
+ * 1. Checks if the player is already on the expected map
173
+ * 2. Subscribes to the `mapChangeSubject` observable
174
+ * 3. Filters events to match the expected map ID
175
+ * 4. Uses `race` operator with a timer to implement timeout handling
176
+ * 5. Resolves with the player when the map change event is received
177
+ *
178
+ * @param expectedMapId - The expected map ID (without 'map-' prefix, e.g. 'map1')
179
+ * @param timeout - Maximum time to wait in milliseconds (default: 5000)
180
+ * @returns Promise that resolves when player is on the expected map
181
+ * @throws Error if timeout is exceeded
182
+ * @example
183
+ * ```ts
184
+ * const client = await fixture.createClient()
185
+ * await client.waitForMapChange('map1')
186
+ * ```
187
+ */
188
+ async waitForMapChange(expectedMapId, timeout = 5e3) {
189
+ if (clientObj.player.getCurrentMap()?.id === expectedMapId) return clientObj.player;
190
+ const mapChange$ = mapChangeSubject.pipe(filter((event) => event.mapId === expectedMapId), take(1), map((event) => event.player));
191
+ const timeout$ = timer(timeout).pipe(take(1), switchMap(() => {
192
+ const currentMap = clientObj.player.getCurrentMap();
193
+ return throwError(() => /* @__PURE__ */ new Error(`Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. Current map: ${currentMap?.id || "null"}`));
194
+ }));
195
+ try {
196
+ return await firstValueFrom(race([mapChange$, timeout$]));
197
+ } catch (error) {
198
+ if (error instanceof Error) throw error;
199
+ const currentMap = clientObj.player.getCurrentMap();
200
+ throw new Error(`Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. Current map: ${currentMap?.id || "null"}`);
201
+ }
202
+ }
203
+ };
204
+ return clientObj;
205
+ },
206
+ get server() {
207
+ return websocket.getServer();
208
+ },
209
+ /**
210
+ * Clear all server, client instances and reset the DOM
211
+ *
212
+ * This method should be called in afterEach to clean up test state.
213
+ * It destroys all created client instances, clears the server, and resets the DOM.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * const fixture = await testing([myModule])
218
+ *
219
+ * afterEach(() => {
220
+ * fixture.clear()
221
+ * })
222
+ * ```
223
+ */
224
+ clear() {
225
+ return clear();
226
+ },
227
+ async applySyncToClient() {
228
+ websocket.getServer().subRoom.applySyncToClient();
229
+ await waitForSync(clientEngine);
230
+ },
231
+ /**
232
+ * Manually trigger a game tick for processing inputs and physics
233
+ *
234
+ * This method is a convenience wrapper around the exported nextTick() function.
235
+ * It uses the clientEngine from the fixture, so no client parameter is needed.
236
+ *
237
+ * @param timestamp - Optional timestamp to use for the tick (default: Date.now())
238
+ * @returns Promise that resolves when the tick is complete
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * const fixture = await testing([myModule])
243
+ *
244
+ * // Manually advance the game by one tick
245
+ * await fixture.nextTick()
246
+ * ```
247
+ */
248
+ async nextTick(timestamp) {
249
+ return nextTick(clientEngine, timestamp);
250
+ },
251
+ async nextTickTimes(times, timestamp) {
252
+ for (let i = 0; i < times; i++) await nextTick(clientEngine, timestamp);
253
+ },
254
+ async wait(ms) {
255
+ return new Promise((resolve) => setTimeout(resolve, ms));
256
+ },
257
+ async waitUntil(promise) {
258
+ let settled = false;
259
+ let resolvedValue;
260
+ let rejectedError;
261
+ promise.then((value) => {
262
+ settled = true;
263
+ resolvedValue = value;
264
+ }).catch((error) => {
265
+ settled = true;
266
+ rejectedError = error;
267
+ });
268
+ while (!settled) {
269
+ await nextTick(clientEngine, Date.now());
270
+ await new Promise((resolve) => setTimeout(resolve, 0));
271
+ }
272
+ if (typeof rejectedError !== "undefined") throw rejectedError;
273
+ return resolvedValue;
274
+ }
275
+ };
265
276
  }
277
+ /**
278
+ * Clear all caches and reset test state
279
+ *
280
+ * This function should be called after the end of each test to clean up
281
+ * all server and client instances, clear caches, and reset the DOM.
282
+ *
283
+ * ## Design
284
+ *
285
+ * Cleans up all created fixtures, client engines, server instances, and resets
286
+ * the DOM to a clean state. This ensures no state leaks between tests.
287
+ *
288
+ * @returns void
289
+ *
290
+ * @example
291
+ * ```ts
292
+ * import { clear } from '@rpgjs/testing'
293
+ *
294
+ * afterEach(() => {
295
+ * clear()
296
+ * })
297
+ * ```
298
+ */
266
299
  async function clear() {
267
- await new Promise((resolve) => setTimeout(resolve, 0));
268
- for (const client of globalFixtures) {
269
- try {
270
- if (client.clientEngine && typeof client.clientEngine.clear === "function") {
271
- client.clientEngine.clear();
272
- }
273
- const serverMap = client.server?.subRoom;
274
- if (serverMap && typeof serverMap.clear === "function") {
275
- serverMap.clear();
276
- }
277
- } catch (error) {
278
- console.warn("Error during cleanup:", error);
279
- }
280
- }
281
- globalFixtures.length = 0;
282
- try {
283
- clearInject();
284
- } catch (error) {
285
- console.warn("Error clearing client inject:", error);
286
- }
287
- try {
288
- clearInject$1();
289
- } catch (error) {
290
- console.warn("Error clearing server inject:", error);
291
- }
292
- if (typeof window !== "undefined" && window.document) {
293
- window.document.body.innerHTML = `<div id="rpg"></div>`;
294
- }
300
+ await new Promise((resolve) => setTimeout(resolve, 0));
301
+ for (const client of globalFixtures) try {
302
+ if (client.clientEngine && typeof client.clientEngine.clear === "function") client.clientEngine.clear();
303
+ const serverMap = client.server?.subRoom;
304
+ if (serverMap && typeof serverMap.clear === "function") serverMap.clear();
305
+ } catch (error) {
306
+ console.warn("Error during cleanup:", error);
307
+ }
308
+ globalFixtures.length = 0;
309
+ try {
310
+ clearInject();
311
+ } catch (error) {
312
+ console.warn("Error clearing client inject:", error);
313
+ }
314
+ try {
315
+ clearInject$1();
316
+ } catch (error) {
317
+ console.warn("Error clearing server inject:", error);
318
+ }
319
+ if (typeof window !== "undefined" && window.document) window.document.body.innerHTML = `<div id="rpg"></div>`;
295
320
  }
321
+ /**
322
+ * Manually trigger a game tick for processing inputs and physics
323
+ *
324
+ * This function allows you to manually advance the game by one tick.
325
+ * It performs the following operations:
326
+ * 1. On server: processes pending inputs and advances physics
327
+ * 2. Server sends data to client
328
+ * 3. Client retrieves data and performs inputs (move, etc.) and server reconciliation
329
+ * 4. A tick is performed on the client
330
+ * 5. A tick is performed on VueJS (if Vue is used)
331
+ *
332
+ * @param client - The RpgClientEngine instance
333
+ * @param timestamp - Optional timestamp to use for the tick (default: Date.now())
334
+ * @returns Promise that resolves when the tick is complete
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * import { nextTick } from '@rpgjs/testing'
339
+ *
340
+ * const client = await fixture.createClient()
341
+ *
342
+ * // Manually advance the game by one tick
343
+ * await nextTick(client.client, Date.now())
344
+ * ```
345
+ */
296
346
  async function nextTick(client, timestamp) {
297
- if (!client) {
298
- throw new Error("nextTick: client parameter is required");
299
- }
300
- const delta = 16;
301
- const websocket = client.webSocket;
302
- if (!websocket) {
303
- throw new Error("nextTick: websocket not found in client");
304
- }
305
- const server = websocket.getServer();
306
- if (!server) {
307
- throw new Error("nextTick: server not found");
308
- }
309
- const serverMap = server.subRoom;
310
- if (!serverMap) {
311
- return;
312
- }
313
- for (const player of serverMap.getPlayers()) {
314
- if (player.pendingInputs && player.pendingInputs.length > 0) {
315
- await serverMap.processInput(player.id);
316
- }
317
- }
318
- serverMap.nextTick(delta);
319
- for (const player of serverMap.getPlayers()) {
320
- if (player && typeof player.syncChanges === "function") {
321
- player.syncChanges();
322
- }
323
- }
324
- await new Promise((resolve) => setTimeout(resolve, 0));
325
- const sceneMap = client.sceneMap;
326
- if (sceneMap && typeof sceneMap.stepPredictionTick === "function") {
327
- sceneMap.stepPredictionTick();
328
- }
347
+ if (!client) throw new Error("nextTick: client parameter is required");
348
+ const delta = 16;
349
+ const websocket = client.webSocket;
350
+ if (!websocket) throw new Error("nextTick: websocket not found in client");
351
+ const server = websocket.getServer();
352
+ if (!server) throw new Error("nextTick: server not found");
353
+ const serverMap = server.subRoom;
354
+ if (!serverMap) return;
355
+ for (const player of serverMap.getPlayers()) if (player.pendingInputs && player.pendingInputs.length > 0) await serverMap.processInput(player.id);
356
+ serverMap.nextTick(delta);
357
+ for (const player of serverMap.getPlayers()) if (player && typeof player.syncChanges === "function") player.syncChanges();
358
+ await new Promise((resolve) => setTimeout(resolve, 0));
359
+ const sceneMap = client.sceneMap;
360
+ if (sceneMap && typeof sceneMap.stepPredictionTick === "function") sceneMap.stepPredictionTick();
329
361
  }
362
+ /**
363
+ * Wait for synchronization to complete on the client
364
+ *
365
+ * This function waits for the client to receive and process synchronization data
366
+ * from the server. It monitors the `playersReceived$` and `eventsReceived$` observables
367
+ * in the RpgClientEngine to determine when synchronization is complete.
368
+ *
369
+ * ## Design
370
+ *
371
+ * - Uses `combineLatest` to wait for both `playersReceived$` and `eventsReceived$` to be `true`
372
+ * - Filters to only proceed when both are `true`
373
+ * - Includes a timeout to prevent waiting indefinitely
374
+ * - Resets the observables to `false` before waiting to ensure we catch the next sync
375
+ *
376
+ * @param client - The RpgClientEngine instance
377
+ * @param timeout - Maximum time to wait in milliseconds (default: 1000ms)
378
+ * @returns Promise that resolves when synchronization is complete
379
+ * @throws Error if timeout is exceeded
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * import { waitForSync } from '@rpgjs/testing'
384
+ *
385
+ * const client = await fixture.createClient()
386
+ *
387
+ * // Wait for sync to complete
388
+ * await waitForSync(client.client)
389
+ *
390
+ * // Now you can safely test client-side state
391
+ * expect(client.client.sceneMap.players()).toBeDefined()
392
+ * ```
393
+ */
330
394
  async function waitForSync(client, timeout = 1e3) {
331
- if (!client) {
332
- throw new Error("waitForSync: client parameter is required");
333
- }
334
- const playersReceived$ = client.playersReceived$;
335
- const eventsReceived$ = client.eventsReceived$;
336
- if (!playersReceived$ || !eventsReceived$) {
337
- throw new Error(
338
- "waitForSync: playersReceived$ or eventsReceived$ not found in client"
339
- );
340
- }
341
- const playersAlreadyTrue = playersReceived$.getValue ? playersReceived$.getValue() === true : false;
342
- const eventsAlreadyTrue = eventsReceived$.getValue ? eventsReceived$.getValue() === true : false;
343
- if (playersAlreadyTrue && eventsAlreadyTrue) {
344
- return;
345
- }
346
- playersReceived$.next(false);
347
- eventsReceived$.next(false);
348
- const syncComplete$ = combineLatest([
349
- playersReceived$.pipe(filter((received) => received === true)),
350
- eventsReceived$.pipe(filter((received) => received === true))
351
- ]).pipe(take(1));
352
- const timeoutPromise = new Promise((_, reject) => {
353
- setTimeout(() => {
354
- reject(
355
- new Error(
356
- `waitForSync: Timeout after ${timeout}ms. Synchronization did not complete.`
357
- )
358
- );
359
- }, timeout);
360
- });
361
- await Promise.race([firstValueFrom(syncComplete$), timeoutPromise]);
395
+ if (!client) throw new Error("waitForSync: client parameter is required");
396
+ const playersReceived$ = client.playersReceived$;
397
+ const eventsReceived$ = client.eventsReceived$;
398
+ if (!playersReceived$ || !eventsReceived$) throw new Error("waitForSync: playersReceived$ or eventsReceived$ not found in client");
399
+ const playersAlreadyTrue = playersReceived$.getValue ? playersReceived$.getValue() === true : false;
400
+ const eventsAlreadyTrue = eventsReceived$.getValue ? eventsReceived$.getValue() === true : false;
401
+ if (playersAlreadyTrue && eventsAlreadyTrue) return;
402
+ playersReceived$.next(false);
403
+ eventsReceived$.next(false);
404
+ const syncComplete$ = combineLatest([playersReceived$.pipe(filter((received) => received === true)), eventsReceived$.pipe(filter((received) => received === true))]).pipe(take(1));
405
+ const timeoutPromise = new Promise((_, reject) => {
406
+ setTimeout(() => {
407
+ reject(/* @__PURE__ */ new Error(`waitForSync: Timeout after ${timeout}ms. Synchronization did not complete.`));
408
+ }, timeout);
409
+ });
410
+ await Promise.race([firstValueFrom(syncComplete$), timeoutPromise]);
362
411
  }
412
+ /**
413
+ * Wait for complete synchronization cycle (server sync + client receive)
414
+ *
415
+ * This function performs a complete synchronization cycle:
416
+ * 1. Triggers a game tick using `nextTick()` which calls `syncChanges()` on all players
417
+ * 2. Waits for the client to receive and process the synchronization data
418
+ *
419
+ * This is useful when you need to ensure that server-side changes are fully
420
+ * synchronized to the client before testing client-side state.
421
+ *
422
+ * ## Design
423
+ *
424
+ * - Calls `nextTick()` to trigger server-side sync
425
+ * - Waits for client to receive sync data using `waitForSync()`
426
+ * - Ensures complete synchronization cycle is finished
427
+ *
428
+ * @param player - The RpgPlayer instance (optional, will sync all players if not provided)
429
+ * @param client - The RpgClientEngine instance
430
+ * @param timeout - Maximum time to wait in milliseconds (default: 1000ms)
431
+ * @returns Promise that resolves when synchronization is complete
432
+ * @throws Error if timeout is exceeded
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * import { waitForSyncComplete } from '@rpgjs/testing'
437
+ *
438
+ * const client = await fixture.createClient()
439
+ * const player = client.player
440
+ *
441
+ * // Make a server-side change
442
+ * player.addItem('potion', 5)
443
+ *
444
+ * // Wait for sync to complete
445
+ * await waitForSyncComplete(player, client.client)
446
+ *
447
+ * // Now you can safely test client-side state
448
+ * const clientPlayer = client.client.sceneMap.players()[player.id]
449
+ * expect(clientPlayer.items()).toBeDefined()
450
+ * ```
451
+ */
363
452
  async function waitForSyncComplete(player, client, timeout = 1e3) {
364
- if (!client) {
365
- throw new Error("waitForSyncComplete: client parameter is required");
366
- }
367
- const playersReceived$ = client.playersReceived$;
368
- const eventsReceived$ = client.eventsReceived$;
369
- if (playersReceived$ && eventsReceived$) {
370
- playersReceived$.next(false);
371
- eventsReceived$.next(false);
372
- }
373
- await nextTick(client);
374
- await waitForSync(client, timeout);
453
+ if (!client) throw new Error("waitForSyncComplete: client parameter is required");
454
+ const playersReceived$ = client.playersReceived$;
455
+ const eventsReceived$ = client.eventsReceived$;
456
+ if (playersReceived$ && eventsReceived$) {
457
+ playersReceived$.next(false);
458
+ eventsReceived$.next(false);
459
+ }
460
+ await nextTick(client);
461
+ await waitForSync(client, timeout);
375
462
  }
376
-
463
+ //#endregion
377
464
  export { clear, nextTick, provideTestingLoadMap, testing, waitForSync, waitForSyncComplete };
378
- //# sourceMappingURL=index.js.map
465
+
466
+ //# sourceMappingURL=index.js.map