@rpgjs/testing 5.0.0-alpha.26 → 5.0.0-alpha.27
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.d.ts +18 -70
- package/dist/index.js +63 -31
- package/dist/index.js.map +1 -1
- package/dist/setup.js +3 -0
- package/dist/setup.js.map +1 -1
- package/package.json +4 -3
- package/src/index.ts +83 -32
- package/src/setup.ts +5 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Provider } from '@signe/di';
|
|
2
|
-
import { RpgClientEngine, RpgClient } from '@rpgjs/client';
|
|
2
|
+
import { AbstractWebsocket, RpgClientEngine, RpgClient } from '@rpgjs/client';
|
|
3
3
|
import { RpgServer, RpgPlayer } from '@rpgjs/server';
|
|
4
4
|
/**
|
|
5
5
|
* Provides a default map loader for testing environments
|
|
@@ -24,6 +24,22 @@ export declare function provideTestingLoadMap(): {
|
|
|
24
24
|
provide: string;
|
|
25
25
|
useFactory: (context: import('@rpgjs/client').Context) => void;
|
|
26
26
|
}[];
|
|
27
|
+
export interface TestingFixture {
|
|
28
|
+
createClient(): Promise<{
|
|
29
|
+
socket: AbstractWebsocket;
|
|
30
|
+
client: RpgClientEngine;
|
|
31
|
+
playerId: string;
|
|
32
|
+
player: RpgPlayer;
|
|
33
|
+
waitForMapChange(expectedMapId: string, timeout?: number): Promise<RpgPlayer>;
|
|
34
|
+
}>;
|
|
35
|
+
server: RpgServer;
|
|
36
|
+
clear(): Promise<void>;
|
|
37
|
+
applySyncToClient(): Promise<void>;
|
|
38
|
+
nextTick(timestamp?: number): Promise<void>;
|
|
39
|
+
nextTickTimes(times: number, timestamp?: number): Promise<void>;
|
|
40
|
+
wait(ms: number): Promise<void>;
|
|
41
|
+
waitUntil(promise: Promise<any>): Promise<void>;
|
|
42
|
+
}
|
|
27
43
|
/**
|
|
28
44
|
* Testing utility function to set up server and client instances for unit testing
|
|
29
45
|
*
|
|
@@ -55,75 +71,7 @@ export declare function provideTestingLoadMap(): {
|
|
|
55
71
|
export declare function testing(modules?: ({
|
|
56
72
|
server?: RpgServer;
|
|
57
73
|
client?: RpgClient;
|
|
58
|
-
} | Provider)[], clientConfig?: any, serverConfig?: any): Promise<
|
|
59
|
-
createClient(): Promise<{
|
|
60
|
-
socket: any;
|
|
61
|
-
client: RpgClientEngine<any>;
|
|
62
|
-
readonly playerId: string;
|
|
63
|
-
readonly player: RpgPlayer;
|
|
64
|
-
/**
|
|
65
|
-
* Wait for player to be on a specific map
|
|
66
|
-
*
|
|
67
|
-
* This utility function waits for the `onJoinMap` hook to be triggered
|
|
68
|
-
* when the player joins the expected map, or throws an error if the timeout is exceeded.
|
|
69
|
-
*
|
|
70
|
-
* ## Design
|
|
71
|
-
*
|
|
72
|
-
* Uses RxJS to listen for map change events emitted by `onJoinMap`. The function:
|
|
73
|
-
* 1. Checks if the player is already on the expected map
|
|
74
|
-
* 2. Subscribes to the `mapChangeSubject` observable
|
|
75
|
-
* 3. Filters events to match the expected map ID
|
|
76
|
-
* 4. Uses `race` operator with a timer to implement timeout handling
|
|
77
|
-
* 5. Resolves with the player when the map change event is received
|
|
78
|
-
*
|
|
79
|
-
* @param expectedMapId - The expected map ID (without 'map-' prefix, e.g. 'map1')
|
|
80
|
-
* @param timeout - Maximum time to wait in milliseconds (default: 5000)
|
|
81
|
-
* @returns Promise that resolves when player is on the expected map
|
|
82
|
-
* @throws Error if timeout is exceeded
|
|
83
|
-
* @example
|
|
84
|
-
* ```ts
|
|
85
|
-
* const client = await fixture.createClient()
|
|
86
|
-
* await client.waitForMapChange('map1')
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
waitForMapChange(expectedMapId: string, timeout?: number): Promise<RpgPlayer>;
|
|
90
|
-
/**
|
|
91
|
-
* Manually trigger a game tick for processing inputs and physics
|
|
92
|
-
*
|
|
93
|
-
* This method is a convenience wrapper around the exported nextTick() function.
|
|
94
|
-
*
|
|
95
|
-
* @param timestamp - Optional timestamp to use for the tick (default: Date.now())
|
|
96
|
-
* @returns Promise that resolves when the tick is complete
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* ```ts
|
|
100
|
-
* const client = await fixture.createClient()
|
|
101
|
-
*
|
|
102
|
-
* // Manually advance the game by one tick
|
|
103
|
-
* await client.nextTick()
|
|
104
|
-
* ```
|
|
105
|
-
*/
|
|
106
|
-
nextTick(timestamp?: number): Promise<void>;
|
|
107
|
-
}>;
|
|
108
|
-
readonly server: any;
|
|
109
|
-
/**
|
|
110
|
-
* Clear all server, client instances and reset the DOM
|
|
111
|
-
*
|
|
112
|
-
* This method should be called in afterEach to clean up test state.
|
|
113
|
-
* It destroys all created client instances, clears the server, and resets the DOM.
|
|
114
|
-
*
|
|
115
|
-
* @example
|
|
116
|
-
* ```ts
|
|
117
|
-
* const fixture = await testing([myModule])
|
|
118
|
-
*
|
|
119
|
-
* afterEach(() => {
|
|
120
|
-
* fixture.clear()
|
|
121
|
-
* })
|
|
122
|
-
* ```
|
|
123
|
-
*/
|
|
124
|
-
clear(): Promise<void>;
|
|
125
|
-
applySyncToClient(): Promise<void>;
|
|
126
|
-
}>;
|
|
74
|
+
} | Provider)[], clientConfig?: any, serverConfig?: any): Promise<TestingFixture>;
|
|
127
75
|
/**
|
|
128
76
|
* Clear all caches and reset test state
|
|
129
77
|
*
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mergeConfig } from './node_modules/.pnpm/@signe_di@2.6.0/node_modules/@
|
|
|
2
2
|
import { provideLoadMap, LoadMapToken, startGame, provideRpg, provideClientGlobalConfig, provideClientModules, inject, WebSocketToken, RpgClientEngine, clearInject } from '@rpgjs/client';
|
|
3
3
|
import { createServer, provideServerModules, clearInject as clearInject$1 } from '@rpgjs/server';
|
|
4
4
|
import { h, Container } from 'canvasengine';
|
|
5
|
-
import { Subject, filter, take, map, timer, switchMap, throwError,
|
|
5
|
+
import { Subject, combineLatest, filter, take, firstValueFrom, map, timer, switchMap, throwError, race } from 'rxjs';
|
|
6
6
|
|
|
7
7
|
function provideTestingLoadMap() {
|
|
8
8
|
return provideLoadMap((id) => {
|
|
@@ -97,7 +97,13 @@ async function testing(modules = [], clientConfig = {}, serverConfig = {}) {
|
|
|
97
97
|
{
|
|
98
98
|
...clientConfig,
|
|
99
99
|
providers: [
|
|
100
|
-
provideClientGlobalConfig({
|
|
100
|
+
provideClientGlobalConfig({
|
|
101
|
+
// TODO
|
|
102
|
+
// bootstrapCanvasOptions: {
|
|
103
|
+
// components: mockComponents,
|
|
104
|
+
// autoRegister: false,
|
|
105
|
+
// },
|
|
106
|
+
}),
|
|
101
107
|
...hasLoadMap ? [] : [provideTestingLoadMap()],
|
|
102
108
|
// Add only if not already provided
|
|
103
109
|
provideClientModules(clientModules),
|
|
@@ -113,14 +119,14 @@ async function testing(modules = [], clientConfig = {}, serverConfig = {}) {
|
|
|
113
119
|
const clientEngine = inject(RpgClientEngine);
|
|
114
120
|
return {
|
|
115
121
|
async createClient() {
|
|
116
|
-
|
|
122
|
+
const clientObj = {
|
|
117
123
|
socket: websocket.getSocket(),
|
|
118
124
|
client: clientEngine,
|
|
119
125
|
get playerId() {
|
|
120
126
|
return Object.keys(websocket.getServer().subRoom.players())[0];
|
|
121
127
|
},
|
|
122
128
|
get player() {
|
|
123
|
-
return websocket.getServer().subRoom.players()[
|
|
129
|
+
return websocket.getServer().subRoom.players()[clientObj.playerId];
|
|
124
130
|
},
|
|
125
131
|
/**
|
|
126
132
|
* Wait for player to be on a specific map
|
|
@@ -148,9 +154,9 @@ async function testing(modules = [], clientConfig = {}, serverConfig = {}) {
|
|
|
148
154
|
* ```
|
|
149
155
|
*/
|
|
150
156
|
async waitForMapChange(expectedMapId, timeout = 5e3) {
|
|
151
|
-
const currentMap =
|
|
157
|
+
const currentMap = clientObj.player.getCurrentMap();
|
|
152
158
|
if (currentMap?.id === expectedMapId) {
|
|
153
|
-
return
|
|
159
|
+
return clientObj.player;
|
|
154
160
|
}
|
|
155
161
|
const mapChange$ = mapChangeSubject.pipe(
|
|
156
162
|
filter((event) => event.mapId === expectedMapId),
|
|
@@ -160,7 +166,7 @@ async function testing(modules = [], clientConfig = {}, serverConfig = {}) {
|
|
|
160
166
|
const timeout$ = timer(timeout).pipe(
|
|
161
167
|
take(1),
|
|
162
168
|
switchMap(() => {
|
|
163
|
-
const currentMap2 =
|
|
169
|
+
const currentMap2 = clientObj.player.getCurrentMap();
|
|
164
170
|
return throwError(() => new Error(
|
|
165
171
|
`Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. Current map: ${currentMap2?.id || "null"}`
|
|
166
172
|
));
|
|
@@ -173,32 +179,14 @@ async function testing(modules = [], clientConfig = {}, serverConfig = {}) {
|
|
|
173
179
|
if (error instanceof Error) {
|
|
174
180
|
throw error;
|
|
175
181
|
}
|
|
176
|
-
const currentMap2 =
|
|
182
|
+
const currentMap2 = clientObj.player.getCurrentMap();
|
|
177
183
|
throw new Error(
|
|
178
184
|
`Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. Current map: ${currentMap2?.id || "null"}`
|
|
179
185
|
);
|
|
180
186
|
}
|
|
181
|
-
},
|
|
182
|
-
/**
|
|
183
|
-
* Manually trigger a game tick for processing inputs and physics
|
|
184
|
-
*
|
|
185
|
-
* This method is a convenience wrapper around the exported nextTick() function.
|
|
186
|
-
*
|
|
187
|
-
* @param timestamp - Optional timestamp to use for the tick (default: Date.now())
|
|
188
|
-
* @returns Promise that resolves when the tick is complete
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* ```ts
|
|
192
|
-
* const client = await fixture.createClient()
|
|
193
|
-
*
|
|
194
|
-
* // Manually advance the game by one tick
|
|
195
|
-
* await client.nextTick()
|
|
196
|
-
* ```
|
|
197
|
-
*/
|
|
198
|
-
async nextTick(timestamp) {
|
|
199
|
-
return nextTick(this.client);
|
|
200
187
|
}
|
|
201
188
|
};
|
|
189
|
+
return clientObj;
|
|
202
190
|
},
|
|
203
191
|
get server() {
|
|
204
192
|
return websocket.getServer();
|
|
@@ -222,8 +210,54 @@ async function testing(modules = [], clientConfig = {}, serverConfig = {}) {
|
|
|
222
210
|
return clear();
|
|
223
211
|
},
|
|
224
212
|
async applySyncToClient() {
|
|
225
|
-
|
|
213
|
+
websocket.getServer().subRoom.applySyncToClient();
|
|
226
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 finish = false;
|
|
246
|
+
return new Promise((resolve, reject) => {
|
|
247
|
+
promise.then((value) => {
|
|
248
|
+
finish = true;
|
|
249
|
+
resolve(value);
|
|
250
|
+
}).catch(reject);
|
|
251
|
+
const timeout = () => {
|
|
252
|
+
setTimeout(() => {
|
|
253
|
+
if (!finish) {
|
|
254
|
+
nextTick(clientEngine);
|
|
255
|
+
timeout();
|
|
256
|
+
}
|
|
257
|
+
}, 0);
|
|
258
|
+
};
|
|
259
|
+
timeout();
|
|
260
|
+
});
|
|
227
261
|
}
|
|
228
262
|
};
|
|
229
263
|
}
|
|
@@ -279,9 +313,7 @@ async function nextTick(client, timestamp) {
|
|
|
279
313
|
await serverMap.processInput(player.id);
|
|
280
314
|
}
|
|
281
315
|
}
|
|
282
|
-
|
|
283
|
-
serverMap.runFixedTicks(delta);
|
|
284
|
-
}
|
|
316
|
+
serverMap.nextTick(delta);
|
|
285
317
|
for (const player of serverMap.getPlayers()) {
|
|
286
318
|
if (player && typeof player.syncChanges === "function") {
|
|
287
319
|
player.syncChanges();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { mergeConfig, Provider } from \"@signe/di\";\nimport {\n provideRpg,\n startGame,\n provideClientModules,\n provideLoadMap,\n provideClientGlobalConfig,\n inject,\n WebSocketToken,\n AbstractWebsocket,\n RpgClientEngine,\n RpgClient,\n LoadMapToken,\n} from \"@rpgjs/client\";\nimport {\n createServer,\n provideServerModules,\n RpgServer,\n RpgPlayer,\n} from \"@rpgjs/server\";\nimport { h, Container } from \"canvasengine\";\nimport { clearInject as clearClientInject } from \"@rpgjs/client\";\nimport { clearInject as clearServerInject } from \"@rpgjs/server\";\nimport { combineLatest, filter, take, firstValueFrom, Subject, map, throwError, race, timer, switchMap } from \"rxjs\";\n\n/**\n * Provides a default map loader for testing environments\n *\n * This function returns a `provideLoadMap` provider that creates mock maps\n * with default dimensions (1024x768) and a minimal component. It's automatically\n * used by `testing()` if no custom `provideLoadMap` is provided in `clientConfig.providers`.\n *\n * @returns A provider function that can be used with `provideLoadMap`\n * @example\n * ```ts\n * // Used automatically by testing()\n * const fixture = await testing([myModule])\n *\n * // Or use directly in clientConfig\n * const fixture = await testing([myModule], {\n * providers: [provideTestingLoadMap()]\n * })\n * ```\n */\nexport function provideTestingLoadMap() {\n return provideLoadMap((id: string) => {\n return {\n id,\n data: {\n width: 1024,\n height: 768,\n hitboxes: [],\n params: {},\n },\n component: h(Container),\n width: 1024,\n height: 768,\n };\n });\n}\n\n/**\n * Normalizes modules input to extract server/client modules from createModule providers or direct module objects\n *\n * @param modules - Array of modules that can be either:\n * - Direct module objects: { server: RpgServer, client: RpgClient }\n * - Providers returned by createModule(): Provider[] with meta.server/client and useValue\n * @returns Object with separate arrays for server and client modules\n * @example\n * ```ts\n * // Direct modules\n * normalizeModules([{ server: serverModule, client: clientModule }])\n *\n * // createModule providers\n * const providers = createModule('MyModule', [{ server: serverModule, client: clientModule }])\n * normalizeModules(providers)\n * ```\n */\nfunction normalizeModules(modules: any[]): {\n serverModules: RpgServer[];\n clientModules: RpgClient[];\n} {\n if (!modules || modules.length === 0) {\n return { serverModules: [], clientModules: [] };\n }\n\n // Check if first item is a provider (has meta and useValue properties)\n const isProviderArray = modules.some(\n (item: any) =>\n item && typeof item === \"object\" && \"meta\" in item && \"useValue\" in item\n );\n\n const serverModules: RpgServer[] = [];\n const clientModules: RpgClient[] = [];\n\n if (!isProviderArray) {\n // Direct module objects, extract server and client separately\n modules.forEach((module: any) => {\n if (module && typeof module === \"object\") {\n if (module.server) {\n serverModules.push(module.server);\n }\n if (module.client) {\n clientModules.push(module.client);\n }\n }\n });\n return { serverModules, clientModules };\n }\n\n // Extract modules from createModule providers\n // createModule returns providers where useValue contains the original { server, client } object\n // We need to group providers by their useValue to reconstruct the original modules\n const seenUseValues = new Set<any>();\n\n modules.forEach((provider: any) => {\n if (\n !provider ||\n typeof provider !== \"object\" ||\n !(\"meta\" in provider) ||\n !(\"useValue\" in provider)\n ) {\n return;\n }\n\n const { useValue } = provider;\n\n // Skip if we've already processed this useValue (same module, different provider for server/client)\n if (seenUseValues.has(useValue)) {\n return;\n }\n\n // Check if useValue has server or client properties (it's a module object)\n if (\n useValue &&\n typeof useValue === \"object\" &&\n (\"server\" in useValue || \"client\" in useValue)\n ) {\n seenUseValues.add(useValue);\n if (useValue.server) {\n serverModules.push(useValue.server);\n }\n if (useValue.client) {\n clientModules.push(useValue.client);\n }\n }\n });\n\n return { serverModules, clientModules };\n}\n\n// Global storage for all created fixtures and clients (for clear() function)\nconst globalFixtures: Array<{\n context: any;\n clientEngine: RpgClientEngine;\n websocket: AbstractWebsocket;\n server?: any;\n}> = [];\n\n\n/**\n * Testing utility function to set up server and client instances for unit testing\n *\n * This function creates a test environment with both server and client instances,\n * allowing you to test player interactions, server hooks, and game mechanics.\n *\n * @param modules - Array of modules that can be either:\n * - Direct module objects: { server: RpgServer, client: RpgClient }\n * - Providers returned by createModule(): Provider[] with meta.server/client and useValue\n * @param clientConfig - Optional client configuration\n * @param serverConfig - Optional server configuration\n * @returns Testing fixture with createClient method\n * @example\n * ```ts\n * // Using direct modules\n * const fixture = await testing([{\n * server: serverModule,\n * client: clientModule\n * }])\n *\n * // Using createModule\n * const myModule = createModule('MyModule', [{\n * server: serverModule,\n * client: clientModule\n * }])\n * const fixture = await testing(myModule)\n * ```\n */\nexport async function testing(\n modules: ({ server?: RpgServer; client?: RpgClient } | Provider)[] = [],\n clientConfig: any = {},\n serverConfig: any = {}\n) {\n // Normalize modules to extract server/client from providers if needed\n const { serverModules, clientModules } = normalizeModules(modules as any[]);\n\n // Subject to emit map change events when onJoinMap is triggered\n const mapChangeSubject = new Subject<{ mapId: string; player: RpgPlayer }>();\n\n const serverClass = createServer({\n ...serverConfig,\n providers: [\n provideServerModules([\n ...serverModules,\n {\n player: {\n onJoinMap(player: RpgPlayer, map: any) {\n // Emit map change event to RxJS Subject\n const mapId = map?.id;\n if (mapId) {\n mapChangeSubject.next({ mapId, player });\n }\n }\n }\n },\n ]),\n ...(serverConfig.providers || []),\n ],\n });\n\n // Check if LoadMapToken is already provided in clientConfig.providers\n // (provideLoadMap returns an array with LoadMapToken)\n const hasLoadMap =\n clientConfig.providers?.some((provider: any) => {\n if (Array.isArray(provider)) {\n return provider.some((p: any) => p?.provide === LoadMapToken);\n }\n return provider?.provide === LoadMapToken;\n }) || false;\n\n await startGame(\n mergeConfig(\n {\n ...clientConfig,\n providers: [\n provideClientGlobalConfig({}),\n ...(hasLoadMap ? [] : [provideTestingLoadMap()]), // Add only if not already provided\n provideClientModules(clientModules),\n ...(clientConfig.providers || []),\n ],\n },\n {\n providers: [provideRpg(serverClass, { env: { TEST: \"true\" } })],\n }\n )\n );\n const websocket = inject<AbstractWebsocket>(WebSocketToken) as any;\n const clientEngine = inject<RpgClientEngine>(RpgClientEngine);\n\n return {\n async createClient() {\n return {\n socket: websocket.getSocket(),\n client: clientEngine,\n get playerId() {\n return Object.keys(websocket.getServer().subRoom.players())[0];\n },\n get player(): RpgPlayer {\n return websocket.getServer().subRoom.players()[this.playerId] as RpgPlayer;\n },\n /**\n * Wait for player to be on a specific map\n *\n * This utility function waits for the `onJoinMap` hook to be triggered\n * when the player joins the expected map, or throws an error if the timeout is exceeded.\n *\n * ## Design\n *\n * Uses RxJS to listen for map change events emitted by `onJoinMap`. The function:\n * 1. Checks if the player is already on the expected map\n * 2. Subscribes to the `mapChangeSubject` observable\n * 3. Filters events to match the expected map ID\n * 4. Uses `race` operator with a timer to implement timeout handling\n * 5. Resolves with the player when the map change event is received\n *\n * @param expectedMapId - The expected map ID (without 'map-' prefix, e.g. 'map1')\n * @param timeout - Maximum time to wait in milliseconds (default: 5000)\n * @returns Promise that resolves when player is on the expected map\n * @throws Error if timeout is exceeded\n * @example\n * ```ts\n * const client = await fixture.createClient()\n * await client.waitForMapChange('map1')\n * ```\n */\n async waitForMapChange(\n expectedMapId: string,\n timeout = 5000\n ): Promise<RpgPlayer> {\n // Check if already on the expected map\n const currentMap = this.player.getCurrentMap();\n if (currentMap?.id === expectedMapId) {\n return this.player;\n }\n\n // Create observable that filters map changes for the expected map ID\n const mapChange$ = mapChangeSubject.pipe(\n filter((event) => event.mapId === expectedMapId),\n take(1),\n map((event) => event.player)\n );\n\n // Create timeout observable that throws an error\n const timeout$ = timer(timeout).pipe(\n take(1),\n switchMap(() => {\n const currentMap = this.player.getCurrentMap();\n return throwError(() => new Error(\n `Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. ` +\n `Current map: ${currentMap?.id || \"null\"}`\n ));\n })\n );\n\n // Race between map change and timeout\n try {\n const result = await firstValueFrom(race([mapChange$, timeout$]));\n return result as RpgPlayer;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n }\n const currentMap = this.player.getCurrentMap();\n throw new Error(\n `Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. ` +\n `Current map: ${currentMap?.id || \"null\"}`\n );\n }\n },\n /**\n * Manually trigger a game tick for processing inputs and physics\n *\n * This method is a convenience wrapper around the exported nextTick() function.\n *\n * @param timestamp - Optional timestamp to use for the tick (default: Date.now())\n * @returns Promise that resolves when the tick is complete\n *\n * @example\n * ```ts\n * const client = await fixture.createClient()\n *\n * // Manually advance the game by one tick\n * await client.nextTick()\n * ```\n */\n async nextTick(timestamp?: number): Promise<void> {\n return nextTick(this.client, timestamp);\n },\n };\n },\n get server() {\n return websocket.getServer();\n },\n /**\n * Clear all server, client instances and reset the DOM\n *\n * This method should be called in afterEach to clean up test state.\n * It destroys all created client instances, clears the server, and resets the DOM.\n *\n * @example\n * ```ts\n * const fixture = await testing([myModule])\n *\n * afterEach(() => {\n * fixture.clear()\n * })\n * ```\n */\n clear() {\n return clear();\n },\n async applySyncToClient() {\n this.server.subRoom.applySyncToClient();\n await waitForSync(clientEngine);\n },\n };\n}\n\n/**\n * Clear all caches and reset test state\n *\n * This function should be called after the end of each test to clean up\n * all server and client instances, clear caches, and reset the DOM.\n *\n * ## Design\n *\n * Cleans up all created fixtures, client engines, server instances, and resets\n * the DOM to a clean state. This ensures no state leaks between tests.\n *\n * @returns void\n *\n * @example\n * ```ts\n * import { clear } from '@rpgjs/testing'\n *\n * afterEach(() => {\n * clear()\n * })\n * ```\n */\nexport async function clear(): Promise<void> {\n\n // Wait for the next tick to ensure all promises are resolved\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n // Clean up all created client and server instances from all fixtures\n for (const client of globalFixtures) {\n try {\n // Clear client engine\n if (\n client.clientEngine &&\n typeof (client.clientEngine as any).clear === \"function\"\n ) {\n (client.clientEngine as any).clear();\n }\n\n // Clear server map (subRoom)\n const serverMap = client.server?.subRoom as any;\n if (serverMap && typeof serverMap.clear === \"function\") {\n serverMap.clear();\n }\n } catch (error) {\n // Silently ignore cleanup errors\n console.warn(\"Error during cleanup:\", error);\n }\n }\n\n // Clear the global fixtures array\n globalFixtures.length = 0;\n\n // Clear client context injection\n try {\n clearClientInject();\n } catch (error) {\n console.warn(\"Error clearing client inject:\", error);\n }\n\n // Clear server context injection\n try {\n clearServerInject();\n } catch (error) {\n console.warn(\"Error clearing server inject:\", error);\n }\n\n // Reset DOM\n if (typeof window !== \"undefined\" && window.document) {\n window.document.body.innerHTML = `<div id=\"rpg\"></div>`;\n }\n}\n\n/**\n * Manually trigger a game tick for processing inputs and physics\n *\n * This function allows you to manually advance the game by one tick.\n * It performs the following operations:\n * 1. On server: processes pending inputs and advances physics\n * 2. Server sends data to client\n * 3. Client retrieves data and performs inputs (move, etc.) and server reconciliation\n * 4. A tick is performed on the client\n * 5. A tick is performed on VueJS (if Vue is used)\n *\n * @param client - The RpgClientEngine instance\n * @param timestamp - Optional timestamp to use for the tick (default: Date.now())\n * @returns Promise that resolves when the tick is complete\n *\n * @example\n * ```ts\n * import { nextTick } from '@rpgjs/testing'\n *\n * const client = await fixture.createClient()\n *\n * // Manually advance the game by one tick\n * await nextTick(client.client, Date.now())\n * ```\n */\nexport async function nextTick(\n client: RpgClientEngine,\n timestamp?: number\n): Promise<void> {\n if (!client) {\n throw new Error(\"nextTick: client parameter is required\");\n }\n\n const tickTimestamp = timestamp ?? Date.now();\n const delta = 16; // 16ms for 60fps\n\n // Get server instance from client context\n const websocket = (client as any).webSocket;\n if (!websocket) {\n throw new Error(\"nextTick: websocket not found in client\");\n }\n\n const server = websocket.getServer();\n if (!server) {\n throw new Error(\"nextTick: server not found\");\n }\n\n // Get server map (subRoom)\n const serverMap = server.subRoom as any;\n if (!serverMap) {\n return;\n }\n\n // 1. On server: Process inputs for all players\n for (const player of serverMap.getPlayers()) {\n if (player.pendingInputs && player.pendingInputs.length > 0) {\n await serverMap.processInput(player.id);\n }\n }\n\n // 2. Run physics tick on server map\n if (typeof serverMap.runFixedTicks === \"function\") {\n serverMap.runFixedTicks(delta);\n }\n\n // 3. Server sends data to client - trigger sync for all players\n // The sync is triggered by calling syncChanges() on each player\n for (const player of serverMap.getPlayers()) {\n if (player && typeof (player as any).syncChanges === \"function\") {\n (player as any).syncChanges();\n }\n }\n\n // 4. Client retrieves data and performs reconciliation\n // The sync data will be received by the client through the websocket\n // We need to wait a bit for the sync data to be processed\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n // 5. Run physics tick on client map (performs client-side prediction)\n const sceneMap = (client as any).sceneMap;\n if (sceneMap && typeof sceneMap.stepPredictionTick === \"function\") {\n sceneMap.stepPredictionTick();\n }\n\n // 6. Trigger VueJS tick if Vue is used (handled by CanvasEngine internally)\n // CanvasEngine handles this automatically through its tick system\n}\n\n/**\n * Wait for synchronization to complete on the client\n *\n * This function waits for the client to receive and process synchronization data\n * from the server. It monitors the `playersReceived$` and `eventsReceived$` observables\n * in the RpgClientEngine to determine when synchronization is complete.\n *\n * ## Design\n *\n * - Uses `combineLatest` to wait for both `playersReceived$` and `eventsReceived$` to be `true`\n * - Filters to only proceed when both are `true`\n * - Includes a timeout to prevent waiting indefinitely\n * - Resets the observables to `false` before waiting to ensure we catch the next sync\n *\n * @param client - The RpgClientEngine instance\n * @param timeout - Maximum time to wait in milliseconds (default: 1000ms)\n * @returns Promise that resolves when synchronization is complete\n * @throws Error if timeout is exceeded\n *\n * @example\n * ```ts\n * import { waitForSync } from '@rpgjs/testing'\n *\n * const client = await fixture.createClient()\n *\n * // Wait for sync to complete\n * await waitForSync(client.client)\n *\n * // Now you can safely test client-side state\n * expect(client.client.sceneMap.players()).toBeDefined()\n * ```\n */\nexport async function waitForSync(\n client: RpgClientEngine,\n timeout: number = 1000\n): Promise<void> {\n if (!client) {\n throw new Error(\"waitForSync: client parameter is required\");\n }\n\n // Access private observables via type assertion\n const playersReceived$ = (client as any).playersReceived$ as any;\n const eventsReceived$ = (client as any).eventsReceived$ as any;\n\n if (!playersReceived$ || !eventsReceived$) {\n throw new Error(\n \"waitForSync: playersReceived$ or eventsReceived$ not found in client\"\n );\n }\n\n // Check if observables are already true - if so, sync has already arrived, don't reset\n const playersAlreadyTrue = playersReceived$.getValue\n ? playersReceived$.getValue() === true\n : false;\n const eventsAlreadyTrue = eventsReceived$.getValue\n ? eventsReceived$.getValue() === true\n : false;\n\n // If both observables are already true, sync has already completed - return immediately\n if (playersAlreadyTrue && eventsAlreadyTrue) {\n return;\n }\n\n // Reset observables to false to ensure we catch the next sync\n // Note: This is only needed when waitForSync is called standalone.\n // When called from waitForSyncComplete, observables are already reset before nextTick\n playersReceived$.next(false);\n eventsReceived$.next(false);\n\n // Wait for both observables to be true\n const syncComplete$ = combineLatest([\n playersReceived$.pipe(filter((received) => received === true)),\n eventsReceived$.pipe(filter((received) => received === true)),\n ]).pipe(take(1));\n\n // Create a timeout promise\n const timeoutPromise = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(\n new Error(\n `waitForSync: Timeout after ${timeout}ms. Synchronization did not complete.`\n )\n );\n }, timeout);\n });\n\n // Race between sync completion and timeout\n await Promise.race([firstValueFrom(syncComplete$), timeoutPromise]);\n}\n\n/**\n * Wait for complete synchronization cycle (server sync + client receive)\n *\n * This function performs a complete synchronization cycle:\n * 1. Triggers a game tick using `nextTick()` which calls `syncChanges()` on all players\n * 2. Waits for the client to receive and process the synchronization data\n *\n * This is useful when you need to ensure that server-side changes are fully\n * synchronized to the client before testing client-side state.\n *\n * ## Design\n *\n * - Calls `nextTick()` to trigger server-side sync\n * - Waits for client to receive sync data using `waitForSync()`\n * - Ensures complete synchronization cycle is finished\n *\n * @param player - The RpgPlayer instance (optional, will sync all players if not provided)\n * @param client - The RpgClientEngine instance\n * @param timeout - Maximum time to wait in milliseconds (default: 1000ms)\n * @returns Promise that resolves when synchronization is complete\n * @throws Error if timeout is exceeded\n *\n * @example\n * ```ts\n * import { waitForSyncComplete } from '@rpgjs/testing'\n *\n * const client = await fixture.createClient()\n * const player = client.player\n *\n * // Make a server-side change\n * player.addItem('potion', 5)\n *\n * // Wait for sync to complete\n * await waitForSyncComplete(player, client.client)\n *\n * // Now you can safely test client-side state\n * const clientPlayer = client.client.sceneMap.players()[player.id]\n * expect(clientPlayer.items()).toBeDefined()\n * ```\n */\nexport async function waitForSyncComplete(\n player: RpgPlayer | null,\n client: RpgClientEngine,\n timeout: number = 1000\n): Promise<void> {\n if (!client) {\n throw new Error(\"waitForSyncComplete: client parameter is required\");\n }\n\n // Reset observables BEFORE calling nextTick to ensure we catch the sync that will be sent\n // This prevents race condition where sync arrives before we start waiting\n const playersReceived$ = (client as any).playersReceived$ as any;\n const eventsReceived$ = (client as any).eventsReceived$ as any;\n if (playersReceived$ && eventsReceived$) {\n playersReceived$.next(false);\n eventsReceived$.next(false);\n }\n\n // Trigger sync by calling nextTick (which calls syncChanges on all players)\n await nextTick(client);\n\n // Wait for client to receive and process the sync\n await waitForSync(client, timeout);\n}\n"],"names":["map","currentMap","clearClientInject","clearServerInject"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { mergeConfig, Provider } from \"@signe/di\";\nimport {\n provideRpg,\n startGame,\n provideClientModules,\n provideLoadMap,\n provideClientGlobalConfig,\n inject,\n WebSocketToken,\n AbstractWebsocket,\n RpgClientEngine,\n RpgClient,\n LoadMapToken,\n} from \"@rpgjs/client\";\nimport {\n createServer,\n provideServerModules,\n RpgServer,\n RpgPlayer,\n} from \"@rpgjs/server\";\nimport { h, Container } from \"canvasengine\";\nimport { mockComponents } from \"@canvasengine/testing\";\nimport { clearInject as clearClientInject } from \"@rpgjs/client\";\nimport { clearInject as clearServerInject } from \"@rpgjs/server\";\nimport { combineLatest, filter, take, firstValueFrom, Subject, map, throwError, race, timer, switchMap } from \"rxjs\";\n\n/**\n * Provides a default map loader for testing environments\n *\n * This function returns a `provideLoadMap` provider that creates mock maps\n * with default dimensions (1024x768) and a minimal component. It's automatically\n * used by `testing()` if no custom `provideLoadMap` is provided in `clientConfig.providers`.\n *\n * @returns A provider function that can be used with `provideLoadMap`\n * @example\n * ```ts\n * // Used automatically by testing()\n * const fixture = await testing([myModule])\n *\n * // Or use directly in clientConfig\n * const fixture = await testing([myModule], {\n * providers: [provideTestingLoadMap()]\n * })\n * ```\n */\nexport function provideTestingLoadMap() {\n return provideLoadMap((id: string) => {\n return {\n id,\n data: {\n width: 1024,\n height: 768,\n hitboxes: [],\n params: {},\n },\n component: h(Container),\n width: 1024,\n height: 768,\n };\n });\n}\n\n/**\n * Normalizes modules input to extract server/client modules from createModule providers or direct module objects\n *\n * @param modules - Array of modules that can be either:\n * - Direct module objects: { server: RpgServer, client: RpgClient }\n * - Providers returned by createModule(): Provider[] with meta.server/client and useValue\n * @returns Object with separate arrays for server and client modules\n * @example\n * ```ts\n * // Direct modules\n * normalizeModules([{ server: serverModule, client: clientModule }])\n *\n * // createModule providers\n * const providers = createModule('MyModule', [{ server: serverModule, client: clientModule }])\n * normalizeModules(providers)\n * ```\n */\nfunction normalizeModules(modules: any[]): {\n serverModules: RpgServer[];\n clientModules: RpgClient[];\n} {\n if (!modules || modules.length === 0) {\n return { serverModules: [], clientModules: [] };\n }\n\n // Check if first item is a provider (has meta and useValue properties)\n const isProviderArray = modules.some(\n (item: any) =>\n item && typeof item === \"object\" && \"meta\" in item && \"useValue\" in item\n );\n\n const serverModules: RpgServer[] = [];\n const clientModules: RpgClient[] = [];\n\n if (!isProviderArray) {\n // Direct module objects, extract server and client separately\n modules.forEach((module: any) => {\n if (module && typeof module === \"object\") {\n if (module.server) {\n serverModules.push(module.server);\n }\n if (module.client) {\n clientModules.push(module.client);\n }\n }\n });\n return { serverModules, clientModules };\n }\n\n // Extract modules from createModule providers\n // createModule returns providers where useValue contains the original { server, client } object\n // We need to group providers by their useValue to reconstruct the original modules\n const seenUseValues = new Set<any>();\n\n modules.forEach((provider: any) => {\n if (\n !provider ||\n typeof provider !== \"object\" ||\n !(\"meta\" in provider) ||\n !(\"useValue\" in provider)\n ) {\n return;\n }\n\n const { useValue } = provider;\n\n // Skip if we've already processed this useValue (same module, different provider for server/client)\n if (seenUseValues.has(useValue)) {\n return;\n }\n\n // Check if useValue has server or client properties (it's a module object)\n if (\n useValue &&\n typeof useValue === \"object\" &&\n (\"server\" in useValue || \"client\" in useValue)\n ) {\n seenUseValues.add(useValue);\n if (useValue.server) {\n serverModules.push(useValue.server);\n }\n if (useValue.client) {\n clientModules.push(useValue.client);\n }\n }\n });\n\n return { serverModules, clientModules };\n}\n\n// Global storage for all created fixtures and clients (for clear() function)\nconst globalFixtures: Array<{\n context: any;\n clientEngine: RpgClientEngine;\n websocket: AbstractWebsocket;\n server?: any;\n}> = [];\n\nexport interface TestingFixture {\n createClient(): Promise<{\n socket: AbstractWebsocket;\n client: RpgClientEngine;\n playerId: string;\n player: RpgPlayer;\n waitForMapChange(expectedMapId: string, timeout?: number): Promise<RpgPlayer>;\n }>;\n server: RpgServer;\n clear(): Promise<void>;\n applySyncToClient(): Promise<void>;\n nextTick(timestamp?: number): Promise<void>;\n nextTickTimes(times: number, timestamp?: number): Promise<void>;\n wait(ms: number): Promise<void>;\n waitUntil(promise: Promise<any>): Promise<void>;\n}\n\n/**\n * Testing utility function to set up server and client instances for unit testing\n *\n * This function creates a test environment with both server and client instances,\n * allowing you to test player interactions, server hooks, and game mechanics.\n *\n * @param modules - Array of modules that can be either:\n * - Direct module objects: { server: RpgServer, client: RpgClient }\n * - Providers returned by createModule(): Provider[] with meta.server/client and useValue\n * @param clientConfig - Optional client configuration\n * @param serverConfig - Optional server configuration\n * @returns Testing fixture with createClient method\n * @example\n * ```ts\n * // Using direct modules\n * const fixture = await testing([{\n * server: serverModule,\n * client: clientModule\n * }])\n *\n * // Using createModule\n * const myModule = createModule('MyModule', [{\n * server: serverModule,\n * client: clientModule\n * }])\n * const fixture = await testing(myModule)\n * ```\n */\nexport async function testing(\n modules: ({ server?: RpgServer; client?: RpgClient } | Provider)[] = [],\n clientConfig: any = {},\n serverConfig: any = {}\n): Promise<TestingFixture> {\n // Normalize modules to extract server/client from providers if needed\n const { serverModules, clientModules } = normalizeModules(modules as any[]);\n\n // Subject to emit map change events when onJoinMap is triggered\n const mapChangeSubject = new Subject<{ mapId: string; player: RpgPlayer }>();\n\n const serverClass = createServer({\n ...serverConfig,\n providers: [\n provideServerModules([\n ...serverModules,\n {\n player: {\n onJoinMap(player: RpgPlayer, map: any) {\n // Emit map change event to RxJS Subject\n const mapId = map?.id;\n if (mapId) {\n mapChangeSubject.next({ mapId, player });\n }\n }\n }\n },\n ]),\n ...(serverConfig.providers || []),\n ],\n });\n\n // Check if LoadMapToken is already provided in clientConfig.providers\n // (provideLoadMap returns an array with LoadMapToken)\n const hasLoadMap =\n clientConfig.providers?.some((provider: any) => {\n if (Array.isArray(provider)) {\n return provider.some((p: any) => p?.provide === LoadMapToken);\n }\n return provider?.provide === LoadMapToken;\n }) || false;\n\n await startGame(\n mergeConfig(\n {\n ...clientConfig,\n providers: [\n provideClientGlobalConfig({\n // TODO\n // bootstrapCanvasOptions: {\n // components: mockComponents,\n // autoRegister: false,\n // },\n }),\n ...(hasLoadMap ? [] : [provideTestingLoadMap()]), // Add only if not already provided\n provideClientModules(clientModules),\n ...(clientConfig.providers || []),\n ],\n },\n {\n providers: [provideRpg(serverClass, { env: { TEST: \"true\" } })],\n }\n )\n );\n const websocket = inject<AbstractWebsocket>(WebSocketToken) as any;\n const clientEngine = inject<RpgClientEngine>(RpgClientEngine);\n\n return {\n async createClient() {\n const clientObj = {\n socket: websocket.getSocket(),\n client: clientEngine,\n get playerId() {\n return Object.keys(websocket.getServer().subRoom.players())[0];\n },\n get player(): RpgPlayer {\n return websocket.getServer().subRoom.players()[clientObj.playerId] as RpgPlayer;\n },\n /**\n * Wait for player to be on a specific map\n *\n * This utility function waits for the `onJoinMap` hook to be triggered\n * when the player joins the expected map, or throws an error if the timeout is exceeded.\n *\n * ## Design\n *\n * Uses RxJS to listen for map change events emitted by `onJoinMap`. The function:\n * 1. Checks if the player is already on the expected map\n * 2. Subscribes to the `mapChangeSubject` observable\n * 3. Filters events to match the expected map ID\n * 4. Uses `race` operator with a timer to implement timeout handling\n * 5. Resolves with the player when the map change event is received\n *\n * @param expectedMapId - The expected map ID (without 'map-' prefix, e.g. 'map1')\n * @param timeout - Maximum time to wait in milliseconds (default: 5000)\n * @returns Promise that resolves when player is on the expected map\n * @throws Error if timeout is exceeded\n * @example\n * ```ts\n * const client = await fixture.createClient()\n * await client.waitForMapChange('map1')\n * ```\n */\n async waitForMapChange(\n expectedMapId: string,\n timeout = 5000\n ): Promise<RpgPlayer> {\n // Check if already on the expected map\n const currentMap = clientObj.player.getCurrentMap();\n if (currentMap?.id === expectedMapId) {\n return clientObj.player;\n }\n\n // Create observable that filters map changes for the expected map ID\n const mapChange$ = mapChangeSubject.pipe(\n filter((event) => event.mapId === expectedMapId),\n take(1),\n map((event) => event.player)\n );\n\n // Create timeout observable that throws an error\n const timeout$ = timer(timeout).pipe(\n take(1),\n switchMap(() => {\n const currentMap = clientObj.player.getCurrentMap();\n return throwError(() => new Error(\n `Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. ` +\n `Current map: ${currentMap?.id || \"null\"}`\n ));\n })\n );\n\n // Race between map change and timeout\n try {\n const result = await firstValueFrom(race([mapChange$, timeout$]));\n return result as RpgPlayer;\n } catch (error) {\n if (error instanceof Error) {\n throw error;\n }\n const currentMap = clientObj.player.getCurrentMap();\n throw new Error(\n `Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. ` +\n `Current map: ${currentMap?.id || \"null\"}`\n );\n }\n },\n \n };\n return clientObj;\n },\n get server() {\n return websocket.getServer();\n },\n /**\n * Clear all server, client instances and reset the DOM\n *\n * This method should be called in afterEach to clean up test state.\n * It destroys all created client instances, clears the server, and resets the DOM.\n *\n * @example\n * ```ts\n * const fixture = await testing([myModule])\n *\n * afterEach(() => {\n * fixture.clear()\n * })\n * ```\n */\n clear() {\n return clear();\n },\n async applySyncToClient() {\n websocket.getServer().subRoom.applySyncToClient();\n await waitForSync(clientEngine);\n },\n /**\n * Manually trigger a game tick for processing inputs and physics\n *\n * This method is a convenience wrapper around the exported nextTick() function.\n * It uses the clientEngine from the fixture, so no client parameter is needed.\n *\n * @param timestamp - Optional timestamp to use for the tick (default: Date.now())\n * @returns Promise that resolves when the tick is complete\n *\n * @example\n * ```ts\n * const fixture = await testing([myModule])\n *\n * // Manually advance the game by one tick\n * await fixture.nextTick()\n * ```\n */\n async nextTick(timestamp?: number): Promise<void> {\n return nextTick(clientEngine, timestamp);\n },\n async nextTickTimes(times: number, timestamp?: number): Promise<void> {\n for (let i = 0; i < times; i++) {\n await nextTick(clientEngine, timestamp);\n }\n },\n async wait(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n },\n async waitUntil<T = any>(promise: Promise<T>): Promise<T> {\n let tick = 0\n let finish = false\n return new Promise<T>((resolve: (value: T) => void, reject: (reason?: any) => void) => {\n promise.then((value: T) => { \n finish = true\n resolve(value)\n }).catch(reject)\n const timeout = () => {\n setTimeout(() => {\n if (!finish) {\n tick++\n nextTick(clientEngine, Date.now() + tick * 16)\n timeout()\n }\n }, 0)\n }\n timeout()\n })\n },\n };\n}\n\n/**\n * Clear all caches and reset test state\n *\n * This function should be called after the end of each test to clean up\n * all server and client instances, clear caches, and reset the DOM.\n *\n * ## Design\n *\n * Cleans up all created fixtures, client engines, server instances, and resets\n * the DOM to a clean state. This ensures no state leaks between tests.\n *\n * @returns void\n *\n * @example\n * ```ts\n * import { clear } from '@rpgjs/testing'\n *\n * afterEach(() => {\n * clear()\n * })\n * ```\n */\nexport async function clear(): Promise<void> {\n\n // Wait for the next tick to ensure all promises are resolved\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n // Clean up all created client and server instances from all fixtures\n for (const client of globalFixtures) {\n try {\n // Clear client engine\n if (\n client.clientEngine &&\n typeof (client.clientEngine as any).clear === \"function\"\n ) {\n (client.clientEngine as any).clear();\n }\n\n // Clear server map (subRoom)\n const serverMap = client.server?.subRoom as any;\n if (serverMap && typeof serverMap.clear === \"function\") {\n serverMap.clear();\n }\n } catch (error) {\n // Silently ignore cleanup errors\n console.warn(\"Error during cleanup:\", error);\n }\n }\n\n // Clear the global fixtures array\n globalFixtures.length = 0;\n\n // Clear client context injection\n try {\n clearClientInject();\n } catch (error) {\n console.warn(\"Error clearing client inject:\", error);\n }\n\n // Clear server context injection\n try {\n clearServerInject();\n } catch (error) {\n console.warn(\"Error clearing server inject:\", error);\n }\n\n // Reset DOM\n if (typeof window !== \"undefined\" && window.document) {\n window.document.body.innerHTML = `<div id=\"rpg\"></div>`;\n }\n}\n\n/**\n * Manually trigger a game tick for processing inputs and physics\n *\n * This function allows you to manually advance the game by one tick.\n * It performs the following operations:\n * 1. On server: processes pending inputs and advances physics\n * 2. Server sends data to client\n * 3. Client retrieves data and performs inputs (move, etc.) and server reconciliation\n * 4. A tick is performed on the client\n * 5. A tick is performed on VueJS (if Vue is used)\n *\n * @param client - The RpgClientEngine instance\n * @param timestamp - Optional timestamp to use for the tick (default: Date.now())\n * @returns Promise that resolves when the tick is complete\n *\n * @example\n * ```ts\n * import { nextTick } from '@rpgjs/testing'\n *\n * const client = await fixture.createClient()\n *\n * // Manually advance the game by one tick\n * await nextTick(client.client, Date.now())\n * ```\n */\nexport async function nextTick(\n client: RpgClientEngine,\n timestamp?: number\n): Promise<void> {\n if (!client) {\n throw new Error(\"nextTick: client parameter is required\");\n }\n\n const tickTimestamp = timestamp ?? Date.now();\n const delta = 16; // 16ms for 60fps\n\n // Get server instance from client context\n const websocket = (client as any).webSocket;\n if (!websocket) {\n throw new Error(\"nextTick: websocket not found in client\");\n }\n\n const server = websocket.getServer();\n if (!server) {\n throw new Error(\"nextTick: server not found\");\n }\n\n // Get server map (subRoom)\n const serverMap = server.subRoom as any;\n if (!serverMap) {\n return;\n }\n\n // 1. On server: Process inputs for all players\n for (const player of serverMap.getPlayers()) {\n if (player.pendingInputs && player.pendingInputs.length > 0) {\n await serverMap.processInput(player.id);\n }\n }\n\n serverMap.nextTick(delta);\n\n // 3. Server sends data to client - trigger sync for all players\n // The sync is triggered by calling syncChanges() on each player\n for (const player of serverMap.getPlayers()) {\n if (player && typeof (player as any).syncChanges === \"function\") {\n (player as any).syncChanges();\n }\n }\n\n // 4. Client retrieves data and performs reconciliation\n // The sync data will be received by the client through the websocket\n // We need to wait a bit for the sync data to be processed\n await new Promise((resolve) => setTimeout(resolve, 0));\n\n // 5. Run physics tick on client map (performs client-side prediction)\n const sceneMap = (client as any).sceneMap;\n if (sceneMap && typeof sceneMap.stepPredictionTick === \"function\") {\n sceneMap.stepPredictionTick();\n }\n\n // 6. Trigger VueJS tick if Vue is used (handled by CanvasEngine internally)\n // CanvasEngine handles this automatically through its tick system\n}\n\n/**\n * Wait for synchronization to complete on the client\n *\n * This function waits for the client to receive and process synchronization data\n * from the server. It monitors the `playersReceived$` and `eventsReceived$` observables\n * in the RpgClientEngine to determine when synchronization is complete.\n *\n * ## Design\n *\n * - Uses `combineLatest` to wait for both `playersReceived$` and `eventsReceived$` to be `true`\n * - Filters to only proceed when both are `true`\n * - Includes a timeout to prevent waiting indefinitely\n * - Resets the observables to `false` before waiting to ensure we catch the next sync\n *\n * @param client - The RpgClientEngine instance\n * @param timeout - Maximum time to wait in milliseconds (default: 1000ms)\n * @returns Promise that resolves when synchronization is complete\n * @throws Error if timeout is exceeded\n *\n * @example\n * ```ts\n * import { waitForSync } from '@rpgjs/testing'\n *\n * const client = await fixture.createClient()\n *\n * // Wait for sync to complete\n * await waitForSync(client.client)\n *\n * // Now you can safely test client-side state\n * expect(client.client.sceneMap.players()).toBeDefined()\n * ```\n */\nexport async function waitForSync(\n client: RpgClientEngine,\n timeout: number = 1000\n): Promise<void> {\n if (!client) {\n throw new Error(\"waitForSync: client parameter is required\");\n }\n\n // Access private observables via type assertion\n const playersReceived$ = (client as any).playersReceived$ as any;\n const eventsReceived$ = (client as any).eventsReceived$ as any;\n\n if (!playersReceived$ || !eventsReceived$) {\n throw new Error(\n \"waitForSync: playersReceived$ or eventsReceived$ not found in client\"\n );\n }\n\n // Check if observables are already true - if so, sync has already arrived, don't reset\n const playersAlreadyTrue = playersReceived$.getValue\n ? playersReceived$.getValue() === true\n : false;\n const eventsAlreadyTrue = eventsReceived$.getValue\n ? eventsReceived$.getValue() === true\n : false;\n\n // If both observables are already true, sync has already completed - return immediately\n if (playersAlreadyTrue && eventsAlreadyTrue) {\n return;\n }\n\n // Reset observables to false to ensure we catch the next sync\n // Note: This is only needed when waitForSync is called standalone.\n // When called from waitForSyncComplete, observables are already reset before nextTick\n playersReceived$.next(false);\n eventsReceived$.next(false);\n\n // Wait for both observables to be true\n const syncComplete$ = combineLatest([\n playersReceived$.pipe(filter((received) => received === true)),\n eventsReceived$.pipe(filter((received) => received === true)),\n ]).pipe(take(1));\n\n // Create a timeout promise\n const timeoutPromise = new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(\n new Error(\n `waitForSync: Timeout after ${timeout}ms. Synchronization did not complete.`\n )\n );\n }, timeout);\n });\n\n // Race between sync completion and timeout\n await Promise.race([firstValueFrom(syncComplete$), timeoutPromise]);\n}\n\n/**\n * Wait for complete synchronization cycle (server sync + client receive)\n *\n * This function performs a complete synchronization cycle:\n * 1. Triggers a game tick using `nextTick()` which calls `syncChanges()` on all players\n * 2. Waits for the client to receive and process the synchronization data\n *\n * This is useful when you need to ensure that server-side changes are fully\n * synchronized to the client before testing client-side state.\n *\n * ## Design\n *\n * - Calls `nextTick()` to trigger server-side sync\n * - Waits for client to receive sync data using `waitForSync()`\n * - Ensures complete synchronization cycle is finished\n *\n * @param player - The RpgPlayer instance (optional, will sync all players if not provided)\n * @param client - The RpgClientEngine instance\n * @param timeout - Maximum time to wait in milliseconds (default: 1000ms)\n * @returns Promise that resolves when synchronization is complete\n * @throws Error if timeout is exceeded\n *\n * @example\n * ```ts\n * import { waitForSyncComplete } from '@rpgjs/testing'\n *\n * const client = await fixture.createClient()\n * const player = client.player\n *\n * // Make a server-side change\n * player.addItem('potion', 5)\n *\n * // Wait for sync to complete\n * await waitForSyncComplete(player, client.client)\n *\n * // Now you can safely test client-side state\n * const clientPlayer = client.client.sceneMap.players()[player.id]\n * expect(clientPlayer.items()).toBeDefined()\n * ```\n */\nexport async function waitForSyncComplete(\n player: RpgPlayer | null,\n client: RpgClientEngine,\n timeout: number = 1000\n): Promise<void> {\n if (!client) {\n throw new Error(\"waitForSyncComplete: client parameter is required\");\n }\n\n // Reset observables BEFORE calling nextTick to ensure we catch the sync that will be sent\n // This prevents race condition where sync arrives before we start waiting\n const playersReceived$ = (client as any).playersReceived$ as any;\n const eventsReceived$ = (client as any).eventsReceived$ as any;\n if (playersReceived$ && eventsReceived$) {\n playersReceived$.next(false);\n eventsReceived$.next(false);\n }\n\n // Trigger sync by calling nextTick (which calls syncChanges on all players)\n await nextTick(client);\n\n // Wait for client to receive and process the sync\n await waitForSync(client, timeout);\n}\n"],"names":["map","currentMap","clearClientInject","clearServerInject"],"mappings":"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"}
|
package/dist/setup.js
CHANGED
|
@@ -27,6 +27,9 @@ Object.defineProperty(global.window.HTMLMediaElement.prototype, "load", {
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
window.document.body.innerHTML = `<div id="rpg"></div>`;
|
|
30
|
+
if (typeof window !== "undefined") {
|
|
31
|
+
window.__RPGJS_TEST__ = true;
|
|
32
|
+
}
|
|
30
33
|
console.error = () => {
|
|
31
34
|
};
|
|
32
35
|
//# sourceMappingURL=setup.js.map
|
package/dist/setup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sources":["../src/setup.ts"],"sourcesContent":["import 'vitest-webgl-canvas-mock'\n\nconst LOAD_FAILURE_SRC = 'LOAD_FAILURE_SRC';\n\n// mock image loading\nObject.defineProperty(global.Image.prototype, 'src', {\n set(src) {\n if (src === LOAD_FAILURE_SRC) {\n setTimeout(() => this.onerror(new Error('mocked error')));\n } else if (src.startsWith('data')) {\n setTimeout(() => this.dispatchEvent(new Event(\"load\")));\n }\n },\n});\n\nObject.defineProperty(global.window.HTMLMediaElement.prototype, 'play', {\n configurable: true,\n get() {\n setTimeout(() => (this.onloadeddata && this.onloadeddata()))\n return () => { }\n }\n})\n\nObject.defineProperty(global.window.HTMLMediaElement.prototype, 'load', {\n configurable: true,\n get() {\n setTimeout(() => (this.onloadeddata && this.onloadeddata()))\n return () => { }\n }\n})\n\nwindow.document.body.innerHTML = `<div id=\"rpg\"></div>`\n\nconsole.error = () => {}"],"names":[],"mappings":";;AAEA,MAAM,gBAAA,GAAmB,kBAAA;AAGzB,MAAA,CAAO,cAAA,CAAe,MAAA,CAAO,KAAA,CAAM,SAAA,EAAW,KAAA,EAAO;AAAA,EACjD,IAAI,GAAA,EAAK;AACL,IAAA,IAAI,QAAQ,gBAAA,EAAkB;AAC1B,MAAA,UAAA,CAAW,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAI,KAAA,CAAM,cAAc,CAAC,CAAC,CAAA;AAAA,IAC5D,CAAA,MAAA,IAAW,GAAA,CAAI,UAAA,CAAW,MAAM,CAAA,EAAG;AAC/B,MAAA,UAAA,CAAW,MAAM,IAAA,CAAK,aAAA,CAAc,IAAI,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,IAC1D;AAAA,EACJ;AACJ,CAAC,CAAA;AAED,MAAA,CAAO,cAAA,CAAe,MAAA,CAAO,MAAA,CAAO,gBAAA,CAAiB,WAAW,MAAA,EAAQ;AAAA,EACpE,YAAA,EAAc,IAAA;AAAA,EACd,GAAA,GAAM;AACF,IAAA,UAAA,CAAW,MAAO,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,cAAe,CAAA;AAC3D,IAAA,OAAO,MAAM;AAAA,IAAE,CAAA;AAAA,EACnB;AACJ,CAAC,CAAA;AAED,MAAA,CAAO,cAAA,CAAe,MAAA,CAAO,MAAA,CAAO,gBAAA,CAAiB,WAAW,MAAA,EAAQ;AAAA,EACpE,YAAA,EAAc,IAAA;AAAA,EACd,GAAA,GAAM;AACF,IAAA,UAAA,CAAW,MAAO,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,cAAe,CAAA;AAC3D,IAAA,OAAO,MAAM;AAAA,IAAE,CAAA;AAAA,EACnB;AACJ,CAAC,CAAA;AAED,MAAA,CAAO,QAAA,CAAS,KAAK,SAAA,GAAY,CAAA,oBAAA,CAAA;
|
|
1
|
+
{"version":3,"file":"setup.js","sources":["../src/setup.ts"],"sourcesContent":["import 'vitest-webgl-canvas-mock'\n\nconst LOAD_FAILURE_SRC = 'LOAD_FAILURE_SRC';\n\n// mock image loading\nObject.defineProperty(global.Image.prototype, 'src', {\n set(src) {\n if (src === LOAD_FAILURE_SRC) {\n setTimeout(() => this.onerror(new Error('mocked error')));\n } else if (src.startsWith('data')) {\n setTimeout(() => this.dispatchEvent(new Event(\"load\")));\n }\n },\n});\n\nObject.defineProperty(global.window.HTMLMediaElement.prototype, 'play', {\n configurable: true,\n get() {\n setTimeout(() => (this.onloadeddata && this.onloadeddata()))\n return () => { }\n }\n})\n\nObject.defineProperty(global.window.HTMLMediaElement.prototype, 'load', {\n configurable: true,\n get() {\n setTimeout(() => (this.onloadeddata && this.onloadeddata()))\n return () => { }\n }\n})\n\nwindow.document.body.innerHTML = `<div id=\"rpg\"></div>`\n\n// Définir une variable globale pour que le client puisse détecter l'environnement de test\nif (typeof window !== 'undefined') {\n (window as any).__RPGJS_TEST__ = true;\n}\n\nconsole.error = () => {}"],"names":[],"mappings":";;AAEA,MAAM,gBAAA,GAAmB,kBAAA;AAGzB,MAAA,CAAO,cAAA,CAAe,MAAA,CAAO,KAAA,CAAM,SAAA,EAAW,KAAA,EAAO;AAAA,EACjD,IAAI,GAAA,EAAK;AACL,IAAA,IAAI,QAAQ,gBAAA,EAAkB;AAC1B,MAAA,UAAA,CAAW,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAI,KAAA,CAAM,cAAc,CAAC,CAAC,CAAA;AAAA,IAC5D,CAAA,MAAA,IAAW,GAAA,CAAI,UAAA,CAAW,MAAM,CAAA,EAAG;AAC/B,MAAA,UAAA,CAAW,MAAM,IAAA,CAAK,aAAA,CAAc,IAAI,KAAA,CAAM,MAAM,CAAC,CAAC,CAAA;AAAA,IAC1D;AAAA,EACJ;AACJ,CAAC,CAAA;AAED,MAAA,CAAO,cAAA,CAAe,MAAA,CAAO,MAAA,CAAO,gBAAA,CAAiB,WAAW,MAAA,EAAQ;AAAA,EACpE,YAAA,EAAc,IAAA;AAAA,EACd,GAAA,GAAM;AACF,IAAA,UAAA,CAAW,MAAO,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,cAAe,CAAA;AAC3D,IAAA,OAAO,MAAM;AAAA,IAAE,CAAA;AAAA,EACnB;AACJ,CAAC,CAAA;AAED,MAAA,CAAO,cAAA,CAAe,MAAA,CAAO,MAAA,CAAO,gBAAA,CAAiB,WAAW,MAAA,EAAQ;AAAA,EACpE,YAAA,EAAc,IAAA;AAAA,EACd,GAAA,GAAM;AACF,IAAA,UAAA,CAAW,MAAO,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,cAAe,CAAA;AAC3D,IAAA,OAAO,MAAM;AAAA,IAAE,CAAA;AAAA,EACnB;AACJ,CAAC,CAAA;AAED,MAAA,CAAO,QAAA,CAAS,KAAK,SAAA,GAAY,CAAA,oBAAA,CAAA;AAGjC,IAAI,OAAO,WAAW,WAAA,EAAa;AACjC,EAAC,OAAe,cAAA,GAAiB,IAAA;AACnC;AAEA,OAAA,CAAQ,QAAQ,MAAM;AAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/testing",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.27",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"keywords": [],
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"vite-plugin-dts": "^4.5.4"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@
|
|
16
|
-
"@rpgjs/
|
|
15
|
+
"@canvasengine/testing": "2.0.0-beta.42",
|
|
16
|
+
"@rpgjs/client": "5.0.0-alpha.27",
|
|
17
|
+
"@rpgjs/server": "5.0.0-alpha.27",
|
|
17
18
|
"@signe/di": "^2.6.0",
|
|
18
19
|
"canvasengine": "2.0.0-beta.41",
|
|
19
20
|
"rxjs": "^7.8.2",
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
RpgPlayer,
|
|
20
20
|
} from "@rpgjs/server";
|
|
21
21
|
import { h, Container } from "canvasengine";
|
|
22
|
+
import { mockComponents } from "@canvasengine/testing";
|
|
22
23
|
import { clearInject as clearClientInject } from "@rpgjs/client";
|
|
23
24
|
import { clearInject as clearServerInject } from "@rpgjs/server";
|
|
24
25
|
import { combineLatest, filter, take, firstValueFrom, Subject, map, throwError, race, timer, switchMap } from "rxjs";
|
|
@@ -157,6 +158,22 @@ const globalFixtures: Array<{
|
|
|
157
158
|
server?: any;
|
|
158
159
|
}> = [];
|
|
159
160
|
|
|
161
|
+
export interface TestingFixture {
|
|
162
|
+
createClient(): Promise<{
|
|
163
|
+
socket: AbstractWebsocket;
|
|
164
|
+
client: RpgClientEngine;
|
|
165
|
+
playerId: string;
|
|
166
|
+
player: RpgPlayer;
|
|
167
|
+
waitForMapChange(expectedMapId: string, timeout?: number): Promise<RpgPlayer>;
|
|
168
|
+
}>;
|
|
169
|
+
server: RpgServer;
|
|
170
|
+
clear(): Promise<void>;
|
|
171
|
+
applySyncToClient(): Promise<void>;
|
|
172
|
+
nextTick(timestamp?: number): Promise<void>;
|
|
173
|
+
nextTickTimes(times: number, timestamp?: number): Promise<void>;
|
|
174
|
+
wait(ms: number): Promise<void>;
|
|
175
|
+
waitUntil(promise: Promise<any>): Promise<void>;
|
|
176
|
+
}
|
|
160
177
|
|
|
161
178
|
/**
|
|
162
179
|
* Testing utility function to set up server and client instances for unit testing
|
|
@@ -190,7 +207,7 @@ export async function testing(
|
|
|
190
207
|
modules: ({ server?: RpgServer; client?: RpgClient } | Provider)[] = [],
|
|
191
208
|
clientConfig: any = {},
|
|
192
209
|
serverConfig: any = {}
|
|
193
|
-
) {
|
|
210
|
+
): Promise<TestingFixture> {
|
|
194
211
|
// Normalize modules to extract server/client from providers if needed
|
|
195
212
|
const { serverModules, clientModules } = normalizeModules(modules as any[]);
|
|
196
213
|
|
|
@@ -233,7 +250,13 @@ export async function testing(
|
|
|
233
250
|
{
|
|
234
251
|
...clientConfig,
|
|
235
252
|
providers: [
|
|
236
|
-
provideClientGlobalConfig({
|
|
253
|
+
provideClientGlobalConfig({
|
|
254
|
+
// TODO
|
|
255
|
+
// bootstrapCanvasOptions: {
|
|
256
|
+
// components: mockComponents,
|
|
257
|
+
// autoRegister: false,
|
|
258
|
+
// },
|
|
259
|
+
}),
|
|
237
260
|
...(hasLoadMap ? [] : [provideTestingLoadMap()]), // Add only if not already provided
|
|
238
261
|
provideClientModules(clientModules),
|
|
239
262
|
...(clientConfig.providers || []),
|
|
@@ -249,14 +272,14 @@ export async function testing(
|
|
|
249
272
|
|
|
250
273
|
return {
|
|
251
274
|
async createClient() {
|
|
252
|
-
|
|
275
|
+
const clientObj = {
|
|
253
276
|
socket: websocket.getSocket(),
|
|
254
277
|
client: clientEngine,
|
|
255
278
|
get playerId() {
|
|
256
279
|
return Object.keys(websocket.getServer().subRoom.players())[0];
|
|
257
280
|
},
|
|
258
281
|
get player(): RpgPlayer {
|
|
259
|
-
return websocket.getServer().subRoom.players()[
|
|
282
|
+
return websocket.getServer().subRoom.players()[clientObj.playerId] as RpgPlayer;
|
|
260
283
|
},
|
|
261
284
|
/**
|
|
262
285
|
* Wait for player to be on a specific map
|
|
@@ -288,9 +311,9 @@ export async function testing(
|
|
|
288
311
|
timeout = 5000
|
|
289
312
|
): Promise<RpgPlayer> {
|
|
290
313
|
// Check if already on the expected map
|
|
291
|
-
const currentMap =
|
|
314
|
+
const currentMap = clientObj.player.getCurrentMap();
|
|
292
315
|
if (currentMap?.id === expectedMapId) {
|
|
293
|
-
return
|
|
316
|
+
return clientObj.player;
|
|
294
317
|
}
|
|
295
318
|
|
|
296
319
|
// Create observable that filters map changes for the expected map ID
|
|
@@ -304,7 +327,7 @@ export async function testing(
|
|
|
304
327
|
const timeout$ = timer(timeout).pipe(
|
|
305
328
|
take(1),
|
|
306
329
|
switchMap(() => {
|
|
307
|
-
const currentMap =
|
|
330
|
+
const currentMap = clientObj.player.getCurrentMap();
|
|
308
331
|
return throwError(() => new Error(
|
|
309
332
|
`Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. ` +
|
|
310
333
|
`Current map: ${currentMap?.id || "null"}`
|
|
@@ -320,33 +343,16 @@ export async function testing(
|
|
|
320
343
|
if (error instanceof Error) {
|
|
321
344
|
throw error;
|
|
322
345
|
}
|
|
323
|
-
const currentMap =
|
|
346
|
+
const currentMap = clientObj.player.getCurrentMap();
|
|
324
347
|
throw new Error(
|
|
325
348
|
`Timeout: Player did not reach map ${expectedMapId} within ${timeout}ms. ` +
|
|
326
349
|
`Current map: ${currentMap?.id || "null"}`
|
|
327
350
|
);
|
|
328
351
|
}
|
|
329
352
|
},
|
|
330
|
-
|
|
331
|
-
* Manually trigger a game tick for processing inputs and physics
|
|
332
|
-
*
|
|
333
|
-
* This method is a convenience wrapper around the exported nextTick() function.
|
|
334
|
-
*
|
|
335
|
-
* @param timestamp - Optional timestamp to use for the tick (default: Date.now())
|
|
336
|
-
* @returns Promise that resolves when the tick is complete
|
|
337
|
-
*
|
|
338
|
-
* @example
|
|
339
|
-
* ```ts
|
|
340
|
-
* const client = await fixture.createClient()
|
|
341
|
-
*
|
|
342
|
-
* // Manually advance the game by one tick
|
|
343
|
-
* await client.nextTick()
|
|
344
|
-
* ```
|
|
345
|
-
*/
|
|
346
|
-
async nextTick(timestamp?: number): Promise<void> {
|
|
347
|
-
return nextTick(this.client, timestamp);
|
|
348
|
-
},
|
|
353
|
+
|
|
349
354
|
};
|
|
355
|
+
return clientObj;
|
|
350
356
|
},
|
|
351
357
|
get server() {
|
|
352
358
|
return websocket.getServer();
|
|
@@ -370,9 +376,57 @@ export async function testing(
|
|
|
370
376
|
return clear();
|
|
371
377
|
},
|
|
372
378
|
async applySyncToClient() {
|
|
373
|
-
|
|
379
|
+
websocket.getServer().subRoom.applySyncToClient();
|
|
374
380
|
await waitForSync(clientEngine);
|
|
375
381
|
},
|
|
382
|
+
/**
|
|
383
|
+
* Manually trigger a game tick for processing inputs and physics
|
|
384
|
+
*
|
|
385
|
+
* This method is a convenience wrapper around the exported nextTick() function.
|
|
386
|
+
* It uses the clientEngine from the fixture, so no client parameter is needed.
|
|
387
|
+
*
|
|
388
|
+
* @param timestamp - Optional timestamp to use for the tick (default: Date.now())
|
|
389
|
+
* @returns Promise that resolves when the tick is complete
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```ts
|
|
393
|
+
* const fixture = await testing([myModule])
|
|
394
|
+
*
|
|
395
|
+
* // Manually advance the game by one tick
|
|
396
|
+
* await fixture.nextTick()
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
async nextTick(timestamp?: number): Promise<void> {
|
|
400
|
+
return nextTick(clientEngine, timestamp);
|
|
401
|
+
},
|
|
402
|
+
async nextTickTimes(times: number, timestamp?: number): Promise<void> {
|
|
403
|
+
for (let i = 0; i < times; i++) {
|
|
404
|
+
await nextTick(clientEngine, timestamp);
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
async wait(ms: number): Promise<void> {
|
|
408
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
409
|
+
},
|
|
410
|
+
async waitUntil<T = any>(promise: Promise<T>): Promise<T> {
|
|
411
|
+
let tick = 0
|
|
412
|
+
let finish = false
|
|
413
|
+
return new Promise<T>((resolve: (value: T) => void, reject: (reason?: any) => void) => {
|
|
414
|
+
promise.then((value: T) => {
|
|
415
|
+
finish = true
|
|
416
|
+
resolve(value)
|
|
417
|
+
}).catch(reject)
|
|
418
|
+
const timeout = () => {
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
if (!finish) {
|
|
421
|
+
tick++
|
|
422
|
+
nextTick(clientEngine, Date.now() + tick * 16)
|
|
423
|
+
timeout()
|
|
424
|
+
}
|
|
425
|
+
}, 0)
|
|
426
|
+
}
|
|
427
|
+
timeout()
|
|
428
|
+
})
|
|
429
|
+
},
|
|
376
430
|
};
|
|
377
431
|
}
|
|
378
432
|
|
|
@@ -508,10 +562,7 @@ export async function nextTick(
|
|
|
508
562
|
}
|
|
509
563
|
}
|
|
510
564
|
|
|
511
|
-
|
|
512
|
-
if (typeof serverMap.runFixedTicks === "function") {
|
|
513
|
-
serverMap.runFixedTicks(delta);
|
|
514
|
-
}
|
|
565
|
+
serverMap.nextTick(delta);
|
|
515
566
|
|
|
516
567
|
// 3. Server sends data to client - trigger sync for all players
|
|
517
568
|
// The sync is triggered by calling syncChanges() on each player
|
package/src/setup.ts
CHANGED
|
@@ -31,4 +31,9 @@ Object.defineProperty(global.window.HTMLMediaElement.prototype, 'load', {
|
|
|
31
31
|
|
|
32
32
|
window.document.body.innerHTML = `<div id="rpg"></div>`
|
|
33
33
|
|
|
34
|
+
// Définir une variable globale pour que le client puisse détecter l'environnement de test
|
|
35
|
+
if (typeof window !== 'undefined') {
|
|
36
|
+
(window as any).__RPGJS_TEST__ = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
console.error = () => {}
|