@rpgjs/testing 5.0.0-beta.1 → 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/LICENSE +19 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +451 -363
- package/dist/index.js.map +1 -1
- package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js +277 -0
- package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +1 -0
- package/dist/setup.js +35 -46
- package/dist/setup.js.map +1 -1
- package/package.json +7 -7
- package/dist/node_modules/.pnpm/@signe_di@2.8.3/node_modules/@signe/di/dist/index.js +0 -366
- package/dist/node_modules/.pnpm/@signe_di@2.8.3/node_modules/@signe/di/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,378 +1,466 @@
|
|
|
1
|
-
import { mergeConfig } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { Subject, combineLatest, filter,
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
465
|
+
|
|
466
|
+
//# sourceMappingURL=index.js.map
|