phaser-hooks 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +628 -30
- package/dist/hooks/type.d.ts +24 -2
- package/dist/hooks/type.d.ts.map +1 -1
- package/dist/hooks/with-computed-state.d.ts.map +1 -1
- package/dist/hooks/with-computed-state.js +1 -5
- package/dist/hooks/with-computed-state.js.map +1 -1
- package/dist/hooks/with-debounced-state.d.ts.map +1 -1
- package/dist/hooks/with-debounced-state.js +9 -6
- package/dist/hooks/with-debounced-state.js.map +1 -1
- package/dist/hooks/with-global-state.spec.js +131 -0
- package/dist/hooks/with-global-state.spec.js.map +1 -1
- package/dist/hooks/with-local-state.spec.js +895 -6
- package/dist/hooks/with-local-state.spec.js.map +1 -1
- package/dist/hooks/with-state-def.d.ts +8 -0
- package/dist/hooks/with-state-def.d.ts.map +1 -1
- package/dist/hooks/with-state-def.js +89 -48
- package/dist/hooks/with-state-def.js.map +1 -1
- package/dist/hooks/with-undoable-state.d.ts.map +1 -1
- package/dist/hooks/with-undoable-state.js +13 -8
- package/dist/hooks/with-undoable-state.js.map +1 -1
- package/dist/utils/logger.d.ts +54 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +172 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logger.spec.d.ts +2 -0
- package/dist/utils/logger.spec.d.ts.map +1 -0
- package/dist/utils/logger.spec.js +198 -0
- package/dist/utils/logger.spec.js.map +1 -0
- package/package.json +2 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
/* eslint-disable max-lines-per-function */
|
|
2
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
-
import { buildSceneMock } from
|
|
4
|
-
import { withLocalState } from
|
|
2
|
+
import { buildSceneMock } from '../test/scene-mock';
|
|
3
|
+
import { withLocalState } from './with-local-state';
|
|
5
4
|
describe('withLocalState', () => {
|
|
6
5
|
const baseState = {
|
|
7
6
|
life: 100,
|
|
@@ -11,8 +10,8 @@ describe('withLocalState', () => {
|
|
|
11
10
|
});
|
|
12
11
|
describe('validation', () => {
|
|
13
12
|
it('should throw an error if the scene is not provided', () => {
|
|
14
|
-
|
|
15
|
-
expect(() => withLocalState(
|
|
13
|
+
const scene = null;
|
|
14
|
+
expect(() => withLocalState(scene, 'test-state', baseState)).toThrow('[withLocalState] Scene parameter is required');
|
|
16
15
|
});
|
|
17
16
|
it('should throw an error if the data manager is not provided', () => {
|
|
18
17
|
const scene = buildSceneMock();
|
|
@@ -43,7 +42,10 @@ describe('withLocalState', () => {
|
|
|
43
42
|
withLocalState(scene, key, initialState);
|
|
44
43
|
expect(setSpy).toHaveBeenCalledWith(`phaser-hooks:local:test-scene:${key}`, initialState);
|
|
45
44
|
// second call should not set state
|
|
46
|
-
const hook = withLocalState(scene, key, {
|
|
45
|
+
const hook = withLocalState(scene, key, {
|
|
46
|
+
...baseState,
|
|
47
|
+
life: 100,
|
|
48
|
+
});
|
|
47
49
|
expect(setSpy).toHaveBeenCalledTimes(1);
|
|
48
50
|
expect(hook.get()).toEqual(initialState);
|
|
49
51
|
setSpy.mockRestore();
|
|
@@ -66,6 +68,606 @@ describe('withLocalState', () => {
|
|
|
66
68
|
expect(hook.get()).toEqual({ ...baseState, life: 90 });
|
|
67
69
|
});
|
|
68
70
|
});
|
|
71
|
+
describe('set with updater function', () => {
|
|
72
|
+
describe('object state', () => {
|
|
73
|
+
it('should update state using updater function', () => {
|
|
74
|
+
const scene = buildSceneMock();
|
|
75
|
+
const key = `test-state-${Date.now()}`;
|
|
76
|
+
const initialState = { ...baseState, life: 100 };
|
|
77
|
+
const hook = withLocalState(scene, key, initialState);
|
|
78
|
+
hook.set((currentState) => ({
|
|
79
|
+
...currentState,
|
|
80
|
+
life: currentState.life - 10,
|
|
81
|
+
}));
|
|
82
|
+
expect(hook.get()).toEqual({ ...baseState, life: 90 });
|
|
83
|
+
});
|
|
84
|
+
it('should update state using updater function multiple times', () => {
|
|
85
|
+
const scene = buildSceneMock();
|
|
86
|
+
const key = `test-state-${Date.now()}`;
|
|
87
|
+
const initialState = { ...baseState, life: 100 };
|
|
88
|
+
const hook = withLocalState(scene, key, initialState);
|
|
89
|
+
// First update
|
|
90
|
+
hook.set((currentState) => ({
|
|
91
|
+
...currentState,
|
|
92
|
+
life: currentState.life - 20,
|
|
93
|
+
}));
|
|
94
|
+
expect(hook.get()).toEqual({ ...baseState, life: 80 });
|
|
95
|
+
// Second update
|
|
96
|
+
hook.set((currentState) => ({
|
|
97
|
+
...currentState,
|
|
98
|
+
life: currentState.life + 5,
|
|
99
|
+
}));
|
|
100
|
+
expect(hook.get()).toEqual({ ...baseState, life: 85 });
|
|
101
|
+
});
|
|
102
|
+
it('should work with complex updater logic', () => {
|
|
103
|
+
const scene = buildSceneMock();
|
|
104
|
+
const key = `test-state-${Date.now()}`;
|
|
105
|
+
const initialState = { ...baseState, life: 100 };
|
|
106
|
+
const hook = withLocalState(scene, key, initialState);
|
|
107
|
+
hook.set((currentState) => ({
|
|
108
|
+
...currentState,
|
|
109
|
+
life: Math.min(currentState.life + 30, 100),
|
|
110
|
+
}));
|
|
111
|
+
expect(hook.get()).toEqual({ ...baseState, life: 100 });
|
|
112
|
+
});
|
|
113
|
+
it('should trigger change events when using updater function', () => {
|
|
114
|
+
const scene = buildSceneMock();
|
|
115
|
+
const key = `test-state-${Date.now()}`;
|
|
116
|
+
const initialState = { ...baseState, life: 100 };
|
|
117
|
+
const hook = withLocalState(scene, key, initialState);
|
|
118
|
+
const callback = vi.fn();
|
|
119
|
+
hook.on('change', callback);
|
|
120
|
+
hook.set((currentState) => ({
|
|
121
|
+
...currentState,
|
|
122
|
+
life: currentState.life - 15,
|
|
123
|
+
}));
|
|
124
|
+
expect(callback).toHaveBeenCalledWith(expect.anything(), // Phaser adds an extra parameter
|
|
125
|
+
{ ...baseState, life: 85 }, { ...baseState, life: 100 });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('primitive state', () => {
|
|
129
|
+
it('should work with number state', () => {
|
|
130
|
+
const scene = buildSceneMock();
|
|
131
|
+
const key = `test-state-${Date.now()}`;
|
|
132
|
+
const initialState = 100;
|
|
133
|
+
const hook = withLocalState(scene, key, initialState);
|
|
134
|
+
hook.set((currentValue) => currentValue + 10);
|
|
135
|
+
expect(hook.get()).toEqual(110);
|
|
136
|
+
hook.set((currentValue) => currentValue * 2);
|
|
137
|
+
expect(hook.get()).toEqual(220);
|
|
138
|
+
});
|
|
139
|
+
it('should work with string state', () => {
|
|
140
|
+
const scene = buildSceneMock();
|
|
141
|
+
const key = `test-state-${Date.now()}`;
|
|
142
|
+
const initialState = 'hello';
|
|
143
|
+
const hook = withLocalState(scene, key, initialState);
|
|
144
|
+
hook.set((currentValue) => `${currentValue} world`);
|
|
145
|
+
expect(hook.get()).toEqual('hello world');
|
|
146
|
+
hook.set((currentValue) => currentValue.toUpperCase());
|
|
147
|
+
expect(hook.get()).toEqual('HELLO WORLD');
|
|
148
|
+
});
|
|
149
|
+
it('should work with boolean state', () => {
|
|
150
|
+
const scene = buildSceneMock();
|
|
151
|
+
const key = `test-state-${Date.now()}`;
|
|
152
|
+
const initialState = false;
|
|
153
|
+
const hook = withLocalState(scene, key, initialState);
|
|
154
|
+
hook.set((currentValue) => !currentValue);
|
|
155
|
+
expect(hook.get()).toEqual(true);
|
|
156
|
+
hook.set((currentValue) => !currentValue);
|
|
157
|
+
expect(hook.get()).toEqual(false);
|
|
158
|
+
});
|
|
159
|
+
it('should work with array state', () => {
|
|
160
|
+
const scene = buildSceneMock();
|
|
161
|
+
const key = `test-state-${Date.now()}`;
|
|
162
|
+
const initialState = [1, 2, 3];
|
|
163
|
+
const hook = withLocalState(scene, key, initialState);
|
|
164
|
+
hook.set((currentValue) => [...currentValue, 4]);
|
|
165
|
+
expect(hook.get()).toEqual([1, 2, 3, 4]);
|
|
166
|
+
hook.set((currentValue) => currentValue.map(x => x * 2));
|
|
167
|
+
expect(hook.get()).toEqual([2, 4, 6, 8]);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe('validator with updater function', () => {
|
|
171
|
+
it('should validate the result of updater function', () => {
|
|
172
|
+
const scene = buildSceneMock();
|
|
173
|
+
const key = `test-state-${Date.now()}`;
|
|
174
|
+
const initialState = { ...baseState, life: 50 };
|
|
175
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
176
|
+
return value.life >= 0 && value.life <= 100 ? true : 'Invalid life value';
|
|
177
|
+
});
|
|
178
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
179
|
+
validator,
|
|
180
|
+
});
|
|
181
|
+
// Valid updater function
|
|
182
|
+
hook.set((currentState) => ({
|
|
183
|
+
...currentState,
|
|
184
|
+
life: currentState.life + 20,
|
|
185
|
+
}));
|
|
186
|
+
expect(hook.get()).toEqual({ ...baseState, life: 70 });
|
|
187
|
+
expect(validator).toHaveBeenCalledWith({ ...baseState, life: 70 });
|
|
188
|
+
// Invalid updater function result
|
|
189
|
+
expect(() => hook.set((currentState) => ({
|
|
190
|
+
...currentState,
|
|
191
|
+
life: currentState.life + 50, // This would make life = 120, which is invalid
|
|
192
|
+
}))).toThrow('[withStateDef] Invalid life value');
|
|
193
|
+
});
|
|
194
|
+
it('should not update state when updater function result is invalid', () => {
|
|
195
|
+
const scene = buildSceneMock();
|
|
196
|
+
const key = `test-state-${Date.now()}`;
|
|
197
|
+
const initialState = { ...baseState, life: 50 };
|
|
198
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
199
|
+
return value.life >= 0 && value.life <= 100 ? true : 'Invalid life value';
|
|
200
|
+
});
|
|
201
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
202
|
+
validator,
|
|
203
|
+
});
|
|
204
|
+
// Try to set invalid value via updater function
|
|
205
|
+
expect(() => hook.set((currentState) => ({
|
|
206
|
+
...currentState,
|
|
207
|
+
life: 150, // Invalid value
|
|
208
|
+
}))).toThrow('[withStateDef] Invalid life value');
|
|
209
|
+
// State should remain unchanged
|
|
210
|
+
expect(hook.get()).toEqual(initialState);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('edge cases', () => {
|
|
214
|
+
it('should handle updater function that returns the same reference', () => {
|
|
215
|
+
const scene = buildSceneMock();
|
|
216
|
+
const key = `test-state-${Date.now()}`;
|
|
217
|
+
const initialState = { ...baseState, life: 100 };
|
|
218
|
+
const hook = withLocalState(scene, key, initialState);
|
|
219
|
+
const callback = vi.fn();
|
|
220
|
+
hook.on('change', callback);
|
|
221
|
+
// Updater function that returns the same object reference
|
|
222
|
+
hook.set((currentState) => currentState);
|
|
223
|
+
// Should still trigger change event (Phaser's registry will detect the change)
|
|
224
|
+
expect(callback).toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
it('should handle updater function with complex logic', () => {
|
|
227
|
+
const scene = buildSceneMock();
|
|
228
|
+
const key = `test-state-${Date.now()}`;
|
|
229
|
+
const initialState = { ...baseState, life: 100 };
|
|
230
|
+
const hook = withLocalState(scene, key, initialState);
|
|
231
|
+
hook.set((currentState) => {
|
|
232
|
+
const newLife = currentState.life - 10;
|
|
233
|
+
if (newLife < 0) {
|
|
234
|
+
return { ...currentState, life: 0 };
|
|
235
|
+
}
|
|
236
|
+
return { ...currentState, life: newLife };
|
|
237
|
+
});
|
|
238
|
+
expect(hook.get()).toEqual({ ...baseState, life: 90 });
|
|
239
|
+
});
|
|
240
|
+
it('should work with nested object updates', () => {
|
|
241
|
+
const scene = buildSceneMock();
|
|
242
|
+
const key = `test-state-${Date.now()}`;
|
|
243
|
+
const initialState = {
|
|
244
|
+
player: {
|
|
245
|
+
stats: { hp: 100, mp: 50 },
|
|
246
|
+
level: 1,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const hook = withLocalState(scene, key, initialState);
|
|
250
|
+
hook.set((currentState) => ({
|
|
251
|
+
...currentState,
|
|
252
|
+
player: {
|
|
253
|
+
...currentState.player,
|
|
254
|
+
stats: {
|
|
255
|
+
...currentState.player.stats,
|
|
256
|
+
hp: currentState.player.stats.hp - 20,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
}));
|
|
260
|
+
expect(hook.get()).toEqual({
|
|
261
|
+
player: {
|
|
262
|
+
stats: { hp: 80, mp: 50 },
|
|
263
|
+
level: 1,
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe('patch method', () => {
|
|
270
|
+
describe('object state patching', () => {
|
|
271
|
+
it('should patch object state with partial updates', () => {
|
|
272
|
+
const scene = buildSceneMock();
|
|
273
|
+
const key = `test-state-${Date.now()}`;
|
|
274
|
+
const initialState = {
|
|
275
|
+
life: 100,
|
|
276
|
+
mana: 50,
|
|
277
|
+
level: 1,
|
|
278
|
+
stats: {
|
|
279
|
+
strength: 10,
|
|
280
|
+
agility: 8,
|
|
281
|
+
intelligence: 12
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const hook = withLocalState(scene, key, initialState);
|
|
285
|
+
// Patch only specific properties
|
|
286
|
+
hook.patch({ life: 90 });
|
|
287
|
+
expect(hook.get()).toEqual({
|
|
288
|
+
life: 90,
|
|
289
|
+
mana: 50,
|
|
290
|
+
level: 1,
|
|
291
|
+
stats: {
|
|
292
|
+
strength: 10,
|
|
293
|
+
agility: 8,
|
|
294
|
+
intelligence: 12
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
// Patch multiple properties
|
|
298
|
+
hook.patch({ mana: 75, level: 2 });
|
|
299
|
+
expect(hook.get()).toEqual({
|
|
300
|
+
life: 90,
|
|
301
|
+
mana: 75,
|
|
302
|
+
level: 2,
|
|
303
|
+
stats: {
|
|
304
|
+
strength: 10,
|
|
305
|
+
agility: 8,
|
|
306
|
+
intelligence: 12
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
it('should patch nested object properties', () => {
|
|
311
|
+
const scene = buildSceneMock();
|
|
312
|
+
const key = `test-state-${Date.now()}`;
|
|
313
|
+
const initialState = {
|
|
314
|
+
player: {
|
|
315
|
+
name: 'Hero',
|
|
316
|
+
stats: {
|
|
317
|
+
hp: 100,
|
|
318
|
+
mp: 50,
|
|
319
|
+
level: 1
|
|
320
|
+
},
|
|
321
|
+
inventory: ['sword', 'potion']
|
|
322
|
+
},
|
|
323
|
+
game: {
|
|
324
|
+
score: 0,
|
|
325
|
+
difficulty: 'normal'
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const hook = withLocalState(scene, key, initialState);
|
|
329
|
+
// Patch nested stats
|
|
330
|
+
hook.patch({
|
|
331
|
+
player: {
|
|
332
|
+
stats: {
|
|
333
|
+
hp: 80
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
expect(hook.get()).toEqual({
|
|
338
|
+
player: {
|
|
339
|
+
name: 'Hero',
|
|
340
|
+
stats: {
|
|
341
|
+
hp: 80,
|
|
342
|
+
mp: 50,
|
|
343
|
+
level: 1
|
|
344
|
+
},
|
|
345
|
+
inventory: ['sword', 'potion']
|
|
346
|
+
},
|
|
347
|
+
game: {
|
|
348
|
+
score: 0,
|
|
349
|
+
difficulty: 'normal'
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
it('should work with updater function for patching', () => {
|
|
354
|
+
const scene = buildSceneMock();
|
|
355
|
+
const key = `test-state-${Date.now()}`;
|
|
356
|
+
const initialState = {
|
|
357
|
+
life: 100,
|
|
358
|
+
mana: 50,
|
|
359
|
+
level: 1
|
|
360
|
+
};
|
|
361
|
+
const hook = withLocalState(scene, key, initialState);
|
|
362
|
+
// Use updater function to patch
|
|
363
|
+
hook.patch((currentState) => ({
|
|
364
|
+
life: currentState.life - 10,
|
|
365
|
+
level: currentState.level + 1
|
|
366
|
+
}));
|
|
367
|
+
expect(hook.get()).toEqual({
|
|
368
|
+
life: 90,
|
|
369
|
+
mana: 50,
|
|
370
|
+
level: 2
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
it('should trigger change events when patching', () => {
|
|
374
|
+
const scene = buildSceneMock();
|
|
375
|
+
const key = `test-state-${Date.now()}`;
|
|
376
|
+
const initialState = {
|
|
377
|
+
life: 100,
|
|
378
|
+
mana: 50,
|
|
379
|
+
level: 1
|
|
380
|
+
};
|
|
381
|
+
const hook = withLocalState(scene, key, initialState);
|
|
382
|
+
const callback = vi.fn();
|
|
383
|
+
hook.on('change', callback);
|
|
384
|
+
hook.patch({ life: 90 });
|
|
385
|
+
expect(callback).toHaveBeenCalledWith(expect.anything(), // Phaser adds an extra parameter
|
|
386
|
+
{ life: 90, mana: 50, level: 1 }, { life: 100, mana: 50, level: 1 });
|
|
387
|
+
});
|
|
388
|
+
it('should preserve other properties when patching', () => {
|
|
389
|
+
const scene = buildSceneMock();
|
|
390
|
+
const key = `test-state-${Date.now()}`;
|
|
391
|
+
const initialState = {
|
|
392
|
+
a: 1,
|
|
393
|
+
b: 2,
|
|
394
|
+
c: 3,
|
|
395
|
+
d: 4,
|
|
396
|
+
e: 5
|
|
397
|
+
};
|
|
398
|
+
const hook = withLocalState(scene, key, initialState);
|
|
399
|
+
// Patch only one property
|
|
400
|
+
hook.patch({ c: 30 });
|
|
401
|
+
expect(hook.get()).toEqual({
|
|
402
|
+
a: 1,
|
|
403
|
+
b: 2,
|
|
404
|
+
c: 30,
|
|
405
|
+
d: 4,
|
|
406
|
+
e: 5
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
describe('complex object patching', () => {
|
|
411
|
+
it('should patch deeply nested objects', () => {
|
|
412
|
+
const scene = buildSceneMock();
|
|
413
|
+
const key = `test-state-${Date.now()}`;
|
|
414
|
+
const initialState = {
|
|
415
|
+
game: {
|
|
416
|
+
player: {
|
|
417
|
+
character: {
|
|
418
|
+
stats: {
|
|
419
|
+
primary: {
|
|
420
|
+
strength: 10,
|
|
421
|
+
dexterity: 8
|
|
422
|
+
},
|
|
423
|
+
secondary: {
|
|
424
|
+
charisma: 5,
|
|
425
|
+
wisdom: 7
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
equipment: {
|
|
429
|
+
weapon: 'sword',
|
|
430
|
+
armor: 'leather'
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
position: { x: 0, y: 0 }
|
|
434
|
+
},
|
|
435
|
+
world: {
|
|
436
|
+
level: 1,
|
|
437
|
+
difficulty: 'normal'
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const hook = withLocalState(scene, key, initialState);
|
|
442
|
+
// Patch deeply nested property
|
|
443
|
+
hook.patch({
|
|
444
|
+
game: {
|
|
445
|
+
player: {
|
|
446
|
+
character: {
|
|
447
|
+
stats: {
|
|
448
|
+
primary: {
|
|
449
|
+
strength: 15
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
expect(hook.get()).toEqual({
|
|
457
|
+
game: {
|
|
458
|
+
player: {
|
|
459
|
+
character: {
|
|
460
|
+
stats: {
|
|
461
|
+
primary: {
|
|
462
|
+
strength: 15,
|
|
463
|
+
dexterity: 8
|
|
464
|
+
},
|
|
465
|
+
secondary: {
|
|
466
|
+
charisma: 5,
|
|
467
|
+
wisdom: 7
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
equipment: {
|
|
471
|
+
weapon: 'sword',
|
|
472
|
+
armor: 'leather'
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
position: { x: 0, y: 0 }
|
|
476
|
+
},
|
|
477
|
+
world: {
|
|
478
|
+
level: 1,
|
|
479
|
+
difficulty: 'normal'
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
it('should handle array properties in objects', () => {
|
|
485
|
+
const scene = buildSceneMock();
|
|
486
|
+
const key = `test-state-${Date.now()}`;
|
|
487
|
+
const initialState = {
|
|
488
|
+
player: {
|
|
489
|
+
name: 'Hero',
|
|
490
|
+
inventory: ['sword', 'potion'],
|
|
491
|
+
skills: ['attack', 'defend'],
|
|
492
|
+
stats: {
|
|
493
|
+
hp: 100,
|
|
494
|
+
mp: 50
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const hook = withLocalState(scene, key, initialState);
|
|
499
|
+
// Patch array property
|
|
500
|
+
hook.patch({
|
|
501
|
+
player: {
|
|
502
|
+
inventory: ['sword', 'potion', 'shield']
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
expect(hook.get()).toEqual({
|
|
506
|
+
player: {
|
|
507
|
+
name: 'Hero',
|
|
508
|
+
inventory: ['sword', 'potion', 'shield'],
|
|
509
|
+
skills: ['attack', 'defend'],
|
|
510
|
+
stats: {
|
|
511
|
+
hp: 100,
|
|
512
|
+
mp: 50
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
describe('error handling', () => {
|
|
519
|
+
it('should throw error when trying to patch non-object state', () => {
|
|
520
|
+
const scene = buildSceneMock();
|
|
521
|
+
const key = `test-state-${Date.now()}`;
|
|
522
|
+
const initialState = 100; // Primitive value
|
|
523
|
+
const hook = withLocalState(scene, key, initialState);
|
|
524
|
+
expect(() => {
|
|
525
|
+
// @ts-expect-error - we want to test the error case
|
|
526
|
+
hook.patch({ value: 200 });
|
|
527
|
+
}).toThrow('[withStateDef] Current value is not an object');
|
|
528
|
+
});
|
|
529
|
+
it('should throw error when trying to patch null state', () => {
|
|
530
|
+
const scene = buildSceneMock();
|
|
531
|
+
const key = `test-state-${Date.now()}`;
|
|
532
|
+
const initialState = null;
|
|
533
|
+
const hook = withLocalState(scene, key, initialState);
|
|
534
|
+
expect(() => {
|
|
535
|
+
// @ts-expect-error - we want to test the error case
|
|
536
|
+
hook.patch({ value: 'something' });
|
|
537
|
+
}).toThrow('[withStateDef] Current value is not an object');
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
describe('validator with patch', () => {
|
|
541
|
+
it('should validate patched values', () => {
|
|
542
|
+
const scene = buildSceneMock();
|
|
543
|
+
const key = `test-state-${Date.now()}`;
|
|
544
|
+
const initialState = {
|
|
545
|
+
life: 50,
|
|
546
|
+
mana: 30
|
|
547
|
+
};
|
|
548
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
549
|
+
return value.life >= 0 && value.life <= 100 ? true : 'Invalid life value';
|
|
550
|
+
});
|
|
551
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
552
|
+
validator,
|
|
553
|
+
});
|
|
554
|
+
// Valid patch
|
|
555
|
+
hook.patch({ life: 75 });
|
|
556
|
+
expect(hook.get()).toEqual({ life: 75, mana: 30 });
|
|
557
|
+
expect(validator).toHaveBeenCalledWith({ life: 75, mana: 30 });
|
|
558
|
+
// Invalid patch
|
|
559
|
+
expect(() => hook.patch({ life: 150 })).toThrow('[withStateDef] Invalid life value');
|
|
560
|
+
expect(hook.get()).toEqual({ life: 75, mana: 30 }); // Should remain unchanged
|
|
561
|
+
});
|
|
562
|
+
it('should not update state when patch result is invalid', () => {
|
|
563
|
+
const scene = buildSceneMock();
|
|
564
|
+
const key = `test-state-${Date.now()}`;
|
|
565
|
+
const initialState = {
|
|
566
|
+
score: 100,
|
|
567
|
+
level: 1
|
|
568
|
+
};
|
|
569
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
570
|
+
return value.score >= 0 && value.score <= 1000 ? true : 'Score must be 0-1000';
|
|
571
|
+
});
|
|
572
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
573
|
+
validator,
|
|
574
|
+
});
|
|
575
|
+
// Try to patch with invalid value
|
|
576
|
+
expect(() => hook.patch({ score: 2000 })).toThrow('[withStateDef] Score must be 0-1000');
|
|
577
|
+
expect(hook.get()).toEqual(initialState); // Should remain unchanged
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
describe('edge cases', () => {
|
|
581
|
+
it('should handle patching with empty object', () => {
|
|
582
|
+
const scene = buildSceneMock();
|
|
583
|
+
const key = `test-state-${Date.now()}`;
|
|
584
|
+
const initialState = {
|
|
585
|
+
life: 100,
|
|
586
|
+
mana: 50
|
|
587
|
+
};
|
|
588
|
+
const hook = withLocalState(scene, key, initialState);
|
|
589
|
+
// Patch with empty object should not change anything
|
|
590
|
+
hook.patch({});
|
|
591
|
+
expect(hook.get()).toEqual(initialState);
|
|
592
|
+
});
|
|
593
|
+
it('should handle patching with undefined values', () => {
|
|
594
|
+
const scene = buildSceneMock();
|
|
595
|
+
const key = `test-state-${Date.now()}`;
|
|
596
|
+
const initialState = {
|
|
597
|
+
life: 100,
|
|
598
|
+
mana: 50
|
|
599
|
+
};
|
|
600
|
+
const hook = withLocalState(scene, key, initialState);
|
|
601
|
+
// Patch with undefined should not change anything
|
|
602
|
+
hook.patch({ life: undefined });
|
|
603
|
+
expect(hook.get()).toEqual(initialState);
|
|
604
|
+
});
|
|
605
|
+
it('should handle patching with null values', () => {
|
|
606
|
+
const scene = buildSceneMock();
|
|
607
|
+
const key = `test-state-${Date.now()}`;
|
|
608
|
+
const initialState = {
|
|
609
|
+
life: 100,
|
|
610
|
+
mana: 50
|
|
611
|
+
};
|
|
612
|
+
const hook = withLocalState(scene, key, initialState);
|
|
613
|
+
// Patch with null
|
|
614
|
+
hook.patch({ life: null });
|
|
615
|
+
expect(hook.get()).toEqual({ life: null, mana: 50 });
|
|
616
|
+
});
|
|
617
|
+
it('should handle patching with function values', () => {
|
|
618
|
+
const scene = buildSceneMock();
|
|
619
|
+
const key = `test-state-${Date.now()}`;
|
|
620
|
+
const initialState = {
|
|
621
|
+
life: 100,
|
|
622
|
+
mana: 50
|
|
623
|
+
};
|
|
624
|
+
const hook = withLocalState(scene, key, initialState);
|
|
625
|
+
const testFunction = () => 'test';
|
|
626
|
+
// Patch with function
|
|
627
|
+
hook.patch({ life: testFunction });
|
|
628
|
+
expect(hook.get()).toEqual({ life: testFunction, mana: 50 });
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
describe('performance and immutability', () => {
|
|
632
|
+
it('should create new object references when patching', () => {
|
|
633
|
+
const scene = buildSceneMock();
|
|
634
|
+
const key = `test-state-${Date.now()}`;
|
|
635
|
+
const initialState = {
|
|
636
|
+
life: 100,
|
|
637
|
+
mana: 50,
|
|
638
|
+
nested: {
|
|
639
|
+
value: 10
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
const hook = withLocalState(scene, key, initialState);
|
|
643
|
+
const originalState = hook.get();
|
|
644
|
+
hook.patch({ life: 90 });
|
|
645
|
+
const newState = hook.get();
|
|
646
|
+
// Should be different object references
|
|
647
|
+
expect(newState).not.toBe(originalState);
|
|
648
|
+
expect(newState.nested).not.toBe(originalState.nested);
|
|
649
|
+
// But nested objects should be preserved if not patched
|
|
650
|
+
expect(newState.nested).toEqual(originalState.nested);
|
|
651
|
+
});
|
|
652
|
+
it('should handle multiple sequential patches', () => {
|
|
653
|
+
const scene = buildSceneMock();
|
|
654
|
+
const key = `test-state-${Date.now()}`;
|
|
655
|
+
const initialState = {
|
|
656
|
+
a: 1,
|
|
657
|
+
b: 2,
|
|
658
|
+
c: 3,
|
|
659
|
+
d: 4
|
|
660
|
+
};
|
|
661
|
+
const hook = withLocalState(scene, key, initialState);
|
|
662
|
+
// Multiple sequential patches
|
|
663
|
+
hook.patch({ a: 10 });
|
|
664
|
+
hook.patch({ b: 20 });
|
|
665
|
+
hook.patch({ c: 30 });
|
|
666
|
+
hook.patch({ d: 40 });
|
|
667
|
+
expect(hook.get()).toEqual({ a: 10, b: 20, c: 30, d: 40 });
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
});
|
|
69
671
|
describe('events', () => {
|
|
70
672
|
describe('on', () => {
|
|
71
673
|
it('should register a change listener', () => {
|
|
@@ -151,5 +753,292 @@ describe('withLocalState', () => {
|
|
|
151
753
|
});
|
|
152
754
|
});
|
|
153
755
|
});
|
|
756
|
+
describe('validators', () => {
|
|
757
|
+
describe('valid values', () => {
|
|
758
|
+
it('should accept valid values when validator returns true', () => {
|
|
759
|
+
const scene = buildSceneMock();
|
|
760
|
+
const key = `test-state-${Date.now()}`;
|
|
761
|
+
const initialState = { ...baseState, life: 50 };
|
|
762
|
+
const validator = vi.fn().mockReturnValue(true);
|
|
763
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
764
|
+
validator,
|
|
765
|
+
});
|
|
766
|
+
expect(validator).toHaveBeenCalledWith(initialState);
|
|
767
|
+
expect(hook.get()).toEqual(initialState);
|
|
768
|
+
// Test setting a new valid value
|
|
769
|
+
const newValue = { ...baseState, life: 75 };
|
|
770
|
+
hook.set(newValue);
|
|
771
|
+
expect(validator).toHaveBeenCalledWith(newValue);
|
|
772
|
+
expect(hook.get()).toEqual(newValue);
|
|
773
|
+
expect(validator).toHaveBeenCalledTimes(2);
|
|
774
|
+
});
|
|
775
|
+
it('should accept valid values when validator returns string true', () => {
|
|
776
|
+
const scene = buildSceneMock();
|
|
777
|
+
const key = `test-state-${Date.now()}`;
|
|
778
|
+
const initialState = { ...baseState, life: 50 };
|
|
779
|
+
const validator = vi.fn().mockReturnValue(true);
|
|
780
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
781
|
+
validator,
|
|
782
|
+
});
|
|
783
|
+
expect(validator).toHaveBeenCalledWith(initialState);
|
|
784
|
+
expect(hook.get()).toEqual(initialState);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
describe('invalid values', () => {
|
|
788
|
+
it('should reject invalid values when validator returns false', () => {
|
|
789
|
+
const scene = buildSceneMock();
|
|
790
|
+
const key = `test-state-${Date.now()}`;
|
|
791
|
+
const initialState = { ...baseState, life: 50 };
|
|
792
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
793
|
+
return value.life >= 0 ? true : false;
|
|
794
|
+
});
|
|
795
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
796
|
+
validator,
|
|
797
|
+
});
|
|
798
|
+
expect(validator).toHaveBeenCalledWith(initialState);
|
|
799
|
+
expect(hook.get()).toEqual(initialState);
|
|
800
|
+
// Test setting an invalid value
|
|
801
|
+
const invalidValue = { ...baseState, life: -10 };
|
|
802
|
+
expect(() => hook.set(invalidValue)).toThrow('[withStateDef] Invalid value for key');
|
|
803
|
+
expect(validator).toHaveBeenCalledWith(invalidValue);
|
|
804
|
+
expect(hook.get()).toEqual(initialState); // Should remain unchanged
|
|
805
|
+
});
|
|
806
|
+
it('should reject invalid values when validator returns error message', () => {
|
|
807
|
+
const scene = buildSceneMock();
|
|
808
|
+
const key = `test-state-${Date.now()}`;
|
|
809
|
+
const initialState = { ...baseState, life: 50 };
|
|
810
|
+
const errorMessage = 'Life must be between 0 and 100';
|
|
811
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
812
|
+
return value.life >= 0 && value.life <= 100 ? true : errorMessage;
|
|
813
|
+
});
|
|
814
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
815
|
+
validator,
|
|
816
|
+
});
|
|
817
|
+
expect(validator).toHaveBeenCalledWith(initialState);
|
|
818
|
+
expect(hook.get()).toEqual(initialState);
|
|
819
|
+
// Test setting an invalid value
|
|
820
|
+
const invalidValue = { ...baseState, life: 150 };
|
|
821
|
+
expect(() => hook.set(invalidValue)).toThrow(`[withStateDef] ${errorMessage}`);
|
|
822
|
+
expect(validator).toHaveBeenCalledWith(invalidValue);
|
|
823
|
+
expect(hook.get()).toEqual(initialState); // Should remain unchanged
|
|
824
|
+
});
|
|
825
|
+
it('should reject invalid initial value when validator returns false', () => {
|
|
826
|
+
const scene = buildSceneMock();
|
|
827
|
+
const key = `test-state-${Date.now()}`;
|
|
828
|
+
const invalidInitialValue = { ...baseState, life: -50 };
|
|
829
|
+
const validator = vi.fn().mockReturnValue(false);
|
|
830
|
+
expect(() => withLocalState(scene, key, invalidInitialValue, {
|
|
831
|
+
validator,
|
|
832
|
+
})).toThrow('[withStateDef] Invalid initial value for key');
|
|
833
|
+
expect(validator).toHaveBeenCalledWith(invalidInitialValue);
|
|
834
|
+
});
|
|
835
|
+
it('should reject invalid initial value when validator returns error message', () => {
|
|
836
|
+
const scene = buildSceneMock();
|
|
837
|
+
const key = `test-state-${Date.now()}`;
|
|
838
|
+
const invalidInitialValue = { ...baseState, life: 200 };
|
|
839
|
+
const errorMessage = 'Life cannot exceed 100';
|
|
840
|
+
const validator = vi.fn().mockReturnValue(errorMessage);
|
|
841
|
+
expect(() => withLocalState(scene, key, invalidInitialValue, {
|
|
842
|
+
validator,
|
|
843
|
+
})).toThrow(`[withStateDef] ${errorMessage}`);
|
|
844
|
+
expect(validator).toHaveBeenCalledWith(invalidInitialValue);
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
describe('validator behavior', () => {
|
|
848
|
+
it('should not call validator on get operations', () => {
|
|
849
|
+
const scene = buildSceneMock();
|
|
850
|
+
const key = `test-state-${Date.now()}`;
|
|
851
|
+
const initialState = { ...baseState, life: 50 };
|
|
852
|
+
const validator = vi.fn().mockReturnValue(true);
|
|
853
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
854
|
+
validator,
|
|
855
|
+
});
|
|
856
|
+
// Clear the validator calls from initialization
|
|
857
|
+
validator.mockClear();
|
|
858
|
+
// Get the value multiple times
|
|
859
|
+
hook.get();
|
|
860
|
+
hook.get();
|
|
861
|
+
hook.get();
|
|
862
|
+
expect(validator).not.toHaveBeenCalled();
|
|
863
|
+
});
|
|
864
|
+
it('should call validator on every set operation', () => {
|
|
865
|
+
const scene = buildSceneMock();
|
|
866
|
+
const key = `test-state-${Date.now()}`;
|
|
867
|
+
const initialState = { ...baseState, life: 50 };
|
|
868
|
+
const validator = vi.fn().mockReturnValue(true);
|
|
869
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
870
|
+
validator,
|
|
871
|
+
});
|
|
872
|
+
// Clear the validator calls from initialization
|
|
873
|
+
validator.mockClear();
|
|
874
|
+
// Set multiple values
|
|
875
|
+
hook.set({ ...baseState, life: 60 });
|
|
876
|
+
hook.set({ ...baseState, life: 70 });
|
|
877
|
+
hook.set({ ...baseState, life: 80 });
|
|
878
|
+
expect(validator).toHaveBeenCalledTimes(3);
|
|
879
|
+
expect(validator).toHaveBeenCalledWith({ ...baseState, life: 60 });
|
|
880
|
+
expect(validator).toHaveBeenCalledWith({ ...baseState, life: 70 });
|
|
881
|
+
expect(validator).toHaveBeenCalledWith({ ...baseState, life: 80 });
|
|
882
|
+
});
|
|
883
|
+
it('should work with complex validator logic', () => {
|
|
884
|
+
const scene = buildSceneMock();
|
|
885
|
+
const key = `test-state-${Date.now()}`;
|
|
886
|
+
const initialState = { ...baseState, life: 50 };
|
|
887
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
888
|
+
if (value.life < 0)
|
|
889
|
+
return 'Life cannot be negative';
|
|
890
|
+
if (value.life > 100)
|
|
891
|
+
return 'Life cannot exceed 100';
|
|
892
|
+
if (value.life % 10 !== 0)
|
|
893
|
+
return 'Life must be a multiple of 10';
|
|
894
|
+
return true;
|
|
895
|
+
});
|
|
896
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
897
|
+
validator,
|
|
898
|
+
});
|
|
899
|
+
// Test valid value
|
|
900
|
+
hook.set({ ...baseState, life: 90 });
|
|
901
|
+
expect(hook.get()).toEqual({ ...baseState, life: 90 });
|
|
902
|
+
// Test invalid values
|
|
903
|
+
expect(() => hook.set({ ...baseState, life: -10 })).toThrow('[withStateDef] Life cannot be negative');
|
|
904
|
+
expect(() => hook.set({ ...baseState, life: 150 })).toThrow('[withStateDef] Life cannot exceed 100');
|
|
905
|
+
expect(() => hook.set({ ...baseState, life: 55 })).toThrow('[withStateDef] Life must be a multiple of 10');
|
|
906
|
+
// State should remain unchanged after invalid attempts
|
|
907
|
+
expect(hook.get()).toEqual({ ...baseState, life: 90 });
|
|
908
|
+
});
|
|
909
|
+
it('should work with async-like validator (synchronous)', () => {
|
|
910
|
+
const scene = buildSceneMock();
|
|
911
|
+
const key = `test-state-${Date.now()}`;
|
|
912
|
+
const initialState = { ...baseState, life: 50 };
|
|
913
|
+
const validator = vi.fn().mockImplementation((value) => {
|
|
914
|
+
// Simulate some validation logic
|
|
915
|
+
const isValid = value.life >= 0 && value.life <= 100;
|
|
916
|
+
return isValid ? true : 'Invalid life value';
|
|
917
|
+
});
|
|
918
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
919
|
+
validator,
|
|
920
|
+
});
|
|
921
|
+
// Test valid value
|
|
922
|
+
hook.set({ ...baseState, life: 75 });
|
|
923
|
+
expect(hook.get()).toEqual({ ...baseState, life: 75 });
|
|
924
|
+
// Test invalid value
|
|
925
|
+
expect(() => hook.set({ ...baseState, life: 150 })).toThrow('[withStateDef] Invalid life value');
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
describe('clearListeners', () => {
|
|
929
|
+
it('should clear all event listeners', () => {
|
|
930
|
+
const scene = buildSceneMock();
|
|
931
|
+
const key = `test-state-${Date.now()}`;
|
|
932
|
+
const initialState = { ...baseState, life: 100 };
|
|
933
|
+
const callback1 = vi.fn();
|
|
934
|
+
const callback2 = vi.fn();
|
|
935
|
+
const callback3 = vi.fn();
|
|
936
|
+
const hook = withLocalState(scene, key, initialState);
|
|
937
|
+
// Add multiple listeners
|
|
938
|
+
hook.on('change', callback1);
|
|
939
|
+
hook.on('change', callback2);
|
|
940
|
+
hook.once('change', callback3);
|
|
941
|
+
// Verify listeners are working
|
|
942
|
+
hook.set({ ...baseState, life: 90 });
|
|
943
|
+
expect(callback1).toHaveBeenCalled();
|
|
944
|
+
expect(callback2).toHaveBeenCalled();
|
|
945
|
+
expect(callback3).toHaveBeenCalled();
|
|
946
|
+
// Clear all listeners
|
|
947
|
+
hook.clearListeners();
|
|
948
|
+
// Verify listeners are cleared
|
|
949
|
+
callback1.mockClear();
|
|
950
|
+
callback2.mockClear();
|
|
951
|
+
callback3.mockClear();
|
|
952
|
+
hook.set({ ...baseState, life: 80 });
|
|
953
|
+
expect(callback1).not.toHaveBeenCalled();
|
|
954
|
+
expect(callback2).not.toHaveBeenCalled();
|
|
955
|
+
expect(callback3).not.toHaveBeenCalled();
|
|
956
|
+
});
|
|
957
|
+
it('should clear listeners added via onChange (deprecated)', () => {
|
|
958
|
+
const scene = buildSceneMock();
|
|
959
|
+
const key = `test-state-${Date.now()}`;
|
|
960
|
+
const initialState = { ...baseState, life: 100 };
|
|
961
|
+
const callback = vi.fn();
|
|
962
|
+
const hook = withLocalState(scene, key, initialState);
|
|
963
|
+
// Add listener via deprecated onChange
|
|
964
|
+
hook.onChange(callback);
|
|
965
|
+
// Verify listener is working
|
|
966
|
+
hook.set({ ...baseState, life: 90 });
|
|
967
|
+
expect(callback).toHaveBeenCalled();
|
|
968
|
+
// Clear all listeners
|
|
969
|
+
hook.clearListeners();
|
|
970
|
+
// Verify listener is cleared
|
|
971
|
+
callback.mockClear();
|
|
972
|
+
hook.set({ ...baseState, life: 80 });
|
|
973
|
+
expect(callback).not.toHaveBeenCalled();
|
|
974
|
+
});
|
|
975
|
+
it('should work with debug mode enabled', () => {
|
|
976
|
+
const scene = buildSceneMock();
|
|
977
|
+
const key = `test-state-${Date.now()}`;
|
|
978
|
+
const initialState = { ...baseState, life: 100 };
|
|
979
|
+
// Test that debug mode doesn't throw errors
|
|
980
|
+
const hook = withLocalState(scene, key, initialState, {
|
|
981
|
+
debug: true,
|
|
982
|
+
});
|
|
983
|
+
const callback = vi.fn();
|
|
984
|
+
hook.on('change', callback);
|
|
985
|
+
// Clear listeners with debug enabled - should not throw
|
|
986
|
+
expect(() => hook.clearListeners()).not.toThrow();
|
|
987
|
+
});
|
|
988
|
+
it('should not affect other state instances', () => {
|
|
989
|
+
const scene = buildSceneMock();
|
|
990
|
+
const key1 = `test-state-1-${Date.now()}`;
|
|
991
|
+
const key2 = `test-state-2-${Date.now()}`;
|
|
992
|
+
const initialState = { ...baseState, life: 100 };
|
|
993
|
+
const callback1 = vi.fn();
|
|
994
|
+
const callback2 = vi.fn();
|
|
995
|
+
const hook1 = withLocalState(scene, key1, initialState);
|
|
996
|
+
const hook2 = withLocalState(scene, key2, initialState);
|
|
997
|
+
// Add listeners to both hooks
|
|
998
|
+
hook1.on('change', callback1);
|
|
999
|
+
hook2.on('change', callback2);
|
|
1000
|
+
// Clear listeners from hook1 only
|
|
1001
|
+
hook1.clearListeners();
|
|
1002
|
+
// Verify hook1 listeners are cleared but hook2 listeners still work
|
|
1003
|
+
hook1.set({ ...baseState, life: 90 });
|
|
1004
|
+
hook2.set({ ...baseState, life: 90 });
|
|
1005
|
+
expect(callback1).not.toHaveBeenCalled();
|
|
1006
|
+
expect(callback2).toHaveBeenCalled();
|
|
1007
|
+
});
|
|
1008
|
+
it('should work when no listeners are present', () => {
|
|
1009
|
+
const scene = buildSceneMock();
|
|
1010
|
+
const key = `test-state-${Date.now()}`;
|
|
1011
|
+
const initialState = { ...baseState, life: 100 };
|
|
1012
|
+
const hook = withLocalState(scene, key, initialState);
|
|
1013
|
+
// Should not throw when clearing listeners that don't exist
|
|
1014
|
+
expect(() => hook.clearListeners()).not.toThrow();
|
|
1015
|
+
});
|
|
1016
|
+
it('should clear listeners after partial removal', () => {
|
|
1017
|
+
const scene = buildSceneMock();
|
|
1018
|
+
const key = `test-state-${Date.now()}`;
|
|
1019
|
+
const initialState = { ...baseState, life: 100 };
|
|
1020
|
+
const callback1 = vi.fn();
|
|
1021
|
+
const callback2 = vi.fn();
|
|
1022
|
+
const callback3 = vi.fn();
|
|
1023
|
+
const hook = withLocalState(scene, key, initialState);
|
|
1024
|
+
// Add multiple listeners
|
|
1025
|
+
hook.on('change', callback1);
|
|
1026
|
+
hook.on('change', callback2);
|
|
1027
|
+
hook.on('change', callback3);
|
|
1028
|
+
// Remove one listener manually
|
|
1029
|
+
hook.off('change', callback2);
|
|
1030
|
+
// Clear all remaining listeners
|
|
1031
|
+
hook.clearListeners();
|
|
1032
|
+
// Verify all listeners are cleared
|
|
1033
|
+
callback1.mockClear();
|
|
1034
|
+
callback2.mockClear();
|
|
1035
|
+
callback3.mockClear();
|
|
1036
|
+
hook.set({ ...baseState, life: 90 });
|
|
1037
|
+
expect(callback1).not.toHaveBeenCalled();
|
|
1038
|
+
expect(callback2).not.toHaveBeenCalled();
|
|
1039
|
+
expect(callback3).not.toHaveBeenCalled();
|
|
1040
|
+
});
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
154
1043
|
});
|
|
155
1044
|
//# sourceMappingURL=with-local-state.spec.js.map
|