titanpl-superls 1.0.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/EXPLAIN.md +612 -0
- package/README.md +486 -0
- package/TEST_DOCUMENTATION.md +255 -0
- package/index.js +615 -0
- package/jsconfig.json +13 -0
- package/mkctx.config.json +7 -0
- package/package.json +31 -0
- package/tests/super-ls.edge-cases.spec.js +911 -0
- package/tests/super-ls.normal-cases.spec.js +794 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { SuperLocalStorage } from "../index.js"
|
|
3
|
+
import { parse } from 'devalue';
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// MOCK for Titan Planet's t.ls API
|
|
7
|
+
// ============================================
|
|
8
|
+
const mockStorage = new Map();
|
|
9
|
+
|
|
10
|
+
globalThis.t = {
|
|
11
|
+
ls: {
|
|
12
|
+
set(key, value) {
|
|
13
|
+
mockStorage.set(key, value);
|
|
14
|
+
},
|
|
15
|
+
get(key) {
|
|
16
|
+
return mockStorage.get(key) || null;
|
|
17
|
+
},
|
|
18
|
+
remove(key) {
|
|
19
|
+
mockStorage.delete(key);
|
|
20
|
+
},
|
|
21
|
+
clear() {
|
|
22
|
+
mockStorage.clear();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ============================================
|
|
28
|
+
// Test Classes
|
|
29
|
+
// ============================================
|
|
30
|
+
class Player {
|
|
31
|
+
constructor(name = '', score = 0) {
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.score = score;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
greet() {
|
|
37
|
+
return `Hello, I am ${this.name}!`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
addScore(points) {
|
|
41
|
+
this.score += points;
|
|
42
|
+
return this.score;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class GameState {
|
|
47
|
+
constructor() {
|
|
48
|
+
this.level = 1;
|
|
49
|
+
this.players = [];
|
|
50
|
+
this.hydrated = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static hydrate(data) {
|
|
54
|
+
const instance = new GameState();
|
|
55
|
+
instance.level = data.level;
|
|
56
|
+
instance.players = data.players || [];
|
|
57
|
+
instance.hydrated = true;
|
|
58
|
+
return instance;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================
|
|
63
|
+
// Dependency Injection Test Classes
|
|
64
|
+
// ============================================
|
|
65
|
+
|
|
66
|
+
// Simple dependency: Weapon class
|
|
67
|
+
class Weapon {
|
|
68
|
+
constructor(name = '', damage = 0) {
|
|
69
|
+
this.name = name;
|
|
70
|
+
this.damage = damage;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
attack() {
|
|
74
|
+
return `Attacks with ${this.name} for ${this.damage} damage!`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Class with single dependency
|
|
79
|
+
class Warrior {
|
|
80
|
+
constructor(name = '', weapon = null) {
|
|
81
|
+
this.name = name;
|
|
82
|
+
this.weapon = weapon; // Dependency: Weapon instance
|
|
83
|
+
this.health = 100;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fight() {
|
|
87
|
+
if (!this.weapon) return `${this.name} has no weapon!`;
|
|
88
|
+
return `${this.name} ${this.weapon.attack()}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getWeaponDamage() {
|
|
92
|
+
return this.weapon?.damage || 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Nested dependency: Armor depends on Material
|
|
97
|
+
class Material {
|
|
98
|
+
constructor(name = '', resistance = 0) {
|
|
99
|
+
this.name = name;
|
|
100
|
+
this.resistance = resistance;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
getProtection() {
|
|
104
|
+
return this.resistance * 2;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class Armor {
|
|
109
|
+
constructor(type = '', material = null) {
|
|
110
|
+
this.type = type;
|
|
111
|
+
this.material = material; // Dependency: Material instance
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getDefense() {
|
|
115
|
+
const baseDefense = this.type === 'heavy' ? 50 : 25;
|
|
116
|
+
return baseDefense + (this.material?.getProtection() || 0);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Class with multiple dependencies
|
|
121
|
+
class Knight {
|
|
122
|
+
constructor(name = '') {
|
|
123
|
+
this.name = name;
|
|
124
|
+
this.weapon = null; // Dependency: Weapon
|
|
125
|
+
this.armor = null; // Dependency: Armor (which has Material)
|
|
126
|
+
this.level = 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
equip(weapon, armor) {
|
|
130
|
+
this.weapon = weapon;
|
|
131
|
+
this.armor = armor;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getStats() {
|
|
135
|
+
return {
|
|
136
|
+
name: this.name,
|
|
137
|
+
attack: this.weapon?.damage || 0,
|
|
138
|
+
defense: this.armor?.getDefense() || 0,
|
|
139
|
+
level: this.level
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
battleCry() {
|
|
144
|
+
return `${this.name} charges with ${this.weapon?.name || 'bare fists'}!`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Service-like classes (IoC pattern)
|
|
149
|
+
class Logger {
|
|
150
|
+
constructor(prefix = '[LOG]') {
|
|
151
|
+
this.prefix = prefix;
|
|
152
|
+
this.logs = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
log(message) {
|
|
156
|
+
const entry = `${this.prefix} ${message}`;
|
|
157
|
+
this.logs.push(entry);
|
|
158
|
+
return entry;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
getLogs() {
|
|
162
|
+
return [...this.logs];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class Database {
|
|
167
|
+
constructor() {
|
|
168
|
+
this.data = new Map();
|
|
169
|
+
this.connected = false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
connect() {
|
|
173
|
+
this.connected = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
save(key, value) {
|
|
177
|
+
this.data.set(key, value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get(key) {
|
|
181
|
+
return this.data.get(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
class UserService {
|
|
186
|
+
constructor(database = null, logger = null) {
|
|
187
|
+
this.database = database; // Dependency: Database
|
|
188
|
+
this.logger = logger; // Dependency: Logger
|
|
189
|
+
this.serviceName = 'UserService';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
createUser(id, name) {
|
|
193
|
+
this.logger?.log(`Creating user: ${name}`);
|
|
194
|
+
this.database?.save(id, { id, name });
|
|
195
|
+
return { id, name };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getUser(id) {
|
|
199
|
+
return this.database?.get(id) || null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static hydrate(data) {
|
|
203
|
+
const instance = new UserService();
|
|
204
|
+
instance.serviceName = data.serviceName;
|
|
205
|
+
|
|
206
|
+
// Manually reconstruct dependencies if they exist
|
|
207
|
+
if (data.database) {
|
|
208
|
+
instance.database = new Database();
|
|
209
|
+
instance.database.connected = data.database.connected;
|
|
210
|
+
if (data.database.data) {
|
|
211
|
+
instance.database.data = new Map(Object.entries(data.database.data));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (data.logger) {
|
|
216
|
+
instance.logger = new Logger(data.logger.prefix);
|
|
217
|
+
instance.logger.logs = data.logger.logs || [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return instance;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Circular dependency classes
|
|
225
|
+
class Parent {
|
|
226
|
+
constructor(name = '') {
|
|
227
|
+
this.name = name;
|
|
228
|
+
this.children = []; // Will contain Child instances
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
addChild(child) {
|
|
232
|
+
this.children.push(child);
|
|
233
|
+
child.parent = this;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getChildrenNames() {
|
|
237
|
+
return this.children.map(c => c.name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class Child {
|
|
242
|
+
constructor(name = '') {
|
|
243
|
+
this.name = name;
|
|
244
|
+
this.parent = null; // Will reference Parent instance
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
getParentName() {
|
|
248
|
+
return this.parent?.name || 'orphan';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================
|
|
253
|
+
// TESTS
|
|
254
|
+
// ============================================
|
|
255
|
+
describe('SuperLocalStorage', () => {
|
|
256
|
+
let superLs = new SuperLocalStorage();
|
|
257
|
+
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
mockStorage.clear();
|
|
260
|
+
superLs = new SuperLocalStorage();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('Basic JavaScript Types', () => {
|
|
264
|
+
it('should store and retrieve simple objects', () => {
|
|
265
|
+
const obj = { name: 'Test', value: 123, active: true };
|
|
266
|
+
superLs.set('obj', obj);
|
|
267
|
+
|
|
268
|
+
const recovered = superLs.get('obj');
|
|
269
|
+
|
|
270
|
+
expect(recovered.name).toBe('Test');
|
|
271
|
+
expect(recovered.value).toBe(123);
|
|
272
|
+
expect(recovered.active).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should store and retrieve arrays', () => {
|
|
276
|
+
const arr = [1, 2, 3, 'four', null];
|
|
277
|
+
superLs.set('arr', arr);
|
|
278
|
+
|
|
279
|
+
const recovered = superLs.get('arr');
|
|
280
|
+
|
|
281
|
+
expect(recovered).toHaveLength(5);
|
|
282
|
+
expect(recovered[3]).toBe('four');
|
|
283
|
+
expect(recovered[4]).toBeNull();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should store and retrieve Map', () => {
|
|
287
|
+
const map = new Map([['a', 1], ['b', 2]]);
|
|
288
|
+
superLs.set('map', map);
|
|
289
|
+
|
|
290
|
+
const recovered = superLs.get('map');
|
|
291
|
+
|
|
292
|
+
expect(recovered).toBeInstanceOf(Map);
|
|
293
|
+
expect(recovered.get('a')).toBe(1);
|
|
294
|
+
expect(recovered.size).toBe(2);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should store and retrieve Set', () => {
|
|
298
|
+
const set = new Set([1, 2, 3, 3, 3]);
|
|
299
|
+
superLs.set('set', set);
|
|
300
|
+
|
|
301
|
+
const recovered = superLs.get('set');
|
|
302
|
+
|
|
303
|
+
expect(recovered).toBeInstanceOf(Set);
|
|
304
|
+
expect(recovered.size).toBe(3);
|
|
305
|
+
expect(recovered.has(2)).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('should store and retrieve Date', () => {
|
|
309
|
+
const date = new Date('2024-06-15T12:00:00Z');
|
|
310
|
+
superLs.set('date', date);
|
|
311
|
+
|
|
312
|
+
const recovered = superLs.get('date');
|
|
313
|
+
|
|
314
|
+
expect(recovered).toBeInstanceOf(Date);
|
|
315
|
+
expect(recovered.toISOString()).toBe(date.toISOString());
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should store and retrieve undefined', () => {
|
|
319
|
+
superLs.set('undef', undefined);
|
|
320
|
+
|
|
321
|
+
const recovered = superLs.get('undef');
|
|
322
|
+
|
|
323
|
+
expect(recovered).toBeUndefined();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should store and retrieve null', () => {
|
|
327
|
+
superLs.set('null', null);
|
|
328
|
+
|
|
329
|
+
const recovered = superLs.get('null');
|
|
330
|
+
|
|
331
|
+
expect(recovered).toBeNull();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should store and retrieve special numbers (NaN, Infinity)', () => {
|
|
335
|
+
superLs.set('special', { nan: NaN, inf: Infinity, negInf: -Infinity });
|
|
336
|
+
|
|
337
|
+
const recovered = superLs.get('special');
|
|
338
|
+
|
|
339
|
+
expect(recovered.nan).toBeNaN();
|
|
340
|
+
expect(recovered.inf).toBe(Infinity);
|
|
341
|
+
expect(recovered.negInf).toBe(-Infinity);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should store and retrieve circular references', () => {
|
|
345
|
+
const obj = { name: 'circular' };
|
|
346
|
+
obj.self = obj;
|
|
347
|
+
superLs.set('circular', obj);
|
|
348
|
+
|
|
349
|
+
const recovered = superLs.get('circular');
|
|
350
|
+
|
|
351
|
+
expect(recovered.name).toBe('circular');
|
|
352
|
+
expect(recovered.self).toBe(recovered);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('Registered Classes', () => {
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
superLs.register(Player);
|
|
359
|
+
superLs.register(GameState);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should store and retrieve class instances', () => {
|
|
363
|
+
const player = new Player('Alice', 100);
|
|
364
|
+
superLs.set('player', player);
|
|
365
|
+
|
|
366
|
+
const recovered = superLs.get('player');
|
|
367
|
+
|
|
368
|
+
expect(recovered).toBeInstanceOf(Player);
|
|
369
|
+
expect(recovered.name).toBe('Alice');
|
|
370
|
+
expect(recovered.score).toBe(100);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should preserve class methods', () => {
|
|
374
|
+
const player = new Player('Bob', 50);
|
|
375
|
+
superLs.set('player', player);
|
|
376
|
+
|
|
377
|
+
const recovered = superLs.get('player');
|
|
378
|
+
|
|
379
|
+
expect(recovered.greet()).toBe('Hello, I am Bob!');
|
|
380
|
+
expect(recovered.addScore(25)).toBe(75);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('should use static hydrate() when defined', () => {
|
|
384
|
+
const game = new GameState();
|
|
385
|
+
game.level = 5;
|
|
386
|
+
game.players = ['Alice', 'Bob'];
|
|
387
|
+
superLs.set('game', game);
|
|
388
|
+
|
|
389
|
+
const recovered = superLs.get('game');
|
|
390
|
+
|
|
391
|
+
expect(recovered).toBeInstanceOf(GameState);
|
|
392
|
+
expect(recovered.hydrated).toBe(true);
|
|
393
|
+
expect(recovered.level).toBe(5);
|
|
394
|
+
expect(recovered.players).toEqual(['Alice', 'Bob']);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should handle multiple instances of the same class', () => {
|
|
398
|
+
superLs.set('p1', new Player('P1', 10));
|
|
399
|
+
superLs.set('p2', new Player('P2', 20));
|
|
400
|
+
superLs.set('p3', new Player('P3', 30));
|
|
401
|
+
|
|
402
|
+
const r1 = superLs.get('p1');
|
|
403
|
+
const r2 = superLs.get('p2');
|
|
404
|
+
const r3 = superLs.get('p3');
|
|
405
|
+
|
|
406
|
+
expect(r1.name).toBe('P1');
|
|
407
|
+
expect(r2.name).toBe('P2');
|
|
408
|
+
expect(r3.name).toBe('P3');
|
|
409
|
+
expect(r1).not.toBe(r2);
|
|
410
|
+
expect(r2).not.toBe(r3);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should allow registration with custom typeName', () => {
|
|
414
|
+
class CustomClass {
|
|
415
|
+
constructor() { this.val = 42; }
|
|
416
|
+
getValue() { return this.val * 2; }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
superLs.register(CustomClass, 'MyCustomClass');
|
|
420
|
+
const instance = new CustomClass();
|
|
421
|
+
superLs.set('custom', instance);
|
|
422
|
+
|
|
423
|
+
const recovered = superLs.get('custom');
|
|
424
|
+
|
|
425
|
+
expect(recovered).toBeInstanceOf(CustomClass);
|
|
426
|
+
expect(recovered.val).toBe(42);
|
|
427
|
+
expect(recovered.getValue()).toBe(84);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// ============================================
|
|
432
|
+
// DEPENDENCY INJECTION TESTS
|
|
433
|
+
// ============================================
|
|
434
|
+
describe('Dependency Injection - Single Dependency', () => {
|
|
435
|
+
beforeEach(() => {
|
|
436
|
+
superLs.register(Weapon);
|
|
437
|
+
superLs.register(Warrior);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should store and retrieve class with single dependency', () => {
|
|
441
|
+
const sword = new Weapon('Excalibur', 50);
|
|
442
|
+
const warrior = new Warrior('Arthur', sword);
|
|
443
|
+
|
|
444
|
+
superLs.set('warrior', warrior);
|
|
445
|
+
const recovered = superLs.get('warrior');
|
|
446
|
+
|
|
447
|
+
expect(recovered).toBeInstanceOf(Warrior);
|
|
448
|
+
expect(recovered.name).toBe('Arthur');
|
|
449
|
+
expect(recovered.weapon).toBeDefined();
|
|
450
|
+
expect(recovered.weapon.name).toBe('Excalibur');
|
|
451
|
+
expect(recovered.weapon.damage).toBe(50);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('should preserve dependency methods', () => {
|
|
455
|
+
const axe = new Weapon('Battle Axe', 75);
|
|
456
|
+
const warrior = new Warrior('Ragnar', axe);
|
|
457
|
+
|
|
458
|
+
superLs.set('viking', warrior);
|
|
459
|
+
const recovered = superLs.get('viking');
|
|
460
|
+
|
|
461
|
+
expect(recovered.fight()).toBe('Ragnar Attacks with Battle Axe for 75 damage!');
|
|
462
|
+
expect(recovered.getWeaponDamage()).toBe(75);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should handle null dependency gracefully', () => {
|
|
466
|
+
const unarmedWarrior = new Warrior('Peasant', null);
|
|
467
|
+
|
|
468
|
+
superLs.set('peasant', unarmedWarrior);
|
|
469
|
+
const recovered = superLs.get('peasant');
|
|
470
|
+
|
|
471
|
+
expect(recovered.weapon).toBeNull();
|
|
472
|
+
expect(recovered.fight()).toBe('Peasant has no weapon!');
|
|
473
|
+
expect(recovered.getWeaponDamage()).toBe(0);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should preserve dependency as proper class instance', () => {
|
|
477
|
+
const dagger = new Weapon('Shadow Dagger', 30);
|
|
478
|
+
const assassin = new Warrior('Shadow', dagger);
|
|
479
|
+
|
|
480
|
+
superLs.set('assassin', assassin);
|
|
481
|
+
const recovered = superLs.get('assassin');
|
|
482
|
+
|
|
483
|
+
// Verify the dependency is a proper Weapon instance
|
|
484
|
+
expect(recovered.weapon).toBeInstanceOf(Weapon);
|
|
485
|
+
expect(recovered.weapon.attack()).toBe('Attacks with Shadow Dagger for 30 damage!');
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
describe('Dependency Injection - Nested Dependencies', () => {
|
|
490
|
+
beforeEach(() => {
|
|
491
|
+
superLs.register(Material);
|
|
492
|
+
superLs.register(Armor);
|
|
493
|
+
superLs.register(Weapon);
|
|
494
|
+
superLs.register(Knight);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should store and retrieve class with nested dependencies', () => {
|
|
498
|
+
const steel = new Material('Steel', 25);
|
|
499
|
+
const plateArmor = new Armor('heavy', steel);
|
|
500
|
+
const sword = new Weapon('Longsword', 40);
|
|
501
|
+
|
|
502
|
+
const knight = new Knight('Lancelot');
|
|
503
|
+
knight.equip(sword, plateArmor);
|
|
504
|
+
|
|
505
|
+
superLs.set('knight', knight);
|
|
506
|
+
const recovered = superLs.get('knight');
|
|
507
|
+
|
|
508
|
+
expect(recovered).toBeInstanceOf(Knight);
|
|
509
|
+
expect(recovered.name).toBe('Lancelot');
|
|
510
|
+
|
|
511
|
+
// Check weapon dependency
|
|
512
|
+
expect(recovered.weapon).toBeDefined();
|
|
513
|
+
expect(recovered.weapon.name).toBe('Longsword');
|
|
514
|
+
|
|
515
|
+
// Check armor dependency
|
|
516
|
+
expect(recovered.armor).toBeDefined();
|
|
517
|
+
expect(recovered.armor.type).toBe('heavy');
|
|
518
|
+
|
|
519
|
+
// Check nested material dependency
|
|
520
|
+
expect(recovered.armor.material).toBeDefined();
|
|
521
|
+
expect(recovered.armor.material.name).toBe('Steel');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should preserve nested dependency methods', () => {
|
|
525
|
+
const mithril = new Material('Mithril', 50);
|
|
526
|
+
const elvenArmor = new Armor('light', mithril);
|
|
527
|
+
const bow = new Weapon('Elven Bow', 35);
|
|
528
|
+
|
|
529
|
+
const knight = new Knight('Legolas');
|
|
530
|
+
knight.equip(bow, elvenArmor);
|
|
531
|
+
|
|
532
|
+
superLs.set('elf', knight);
|
|
533
|
+
const recovered = superLs.get('elf');
|
|
534
|
+
|
|
535
|
+
// Test nested method chain
|
|
536
|
+
expect(recovered.armor.material.getProtection()).toBe(100); // 50 * 2
|
|
537
|
+
expect(recovered.armor.getDefense()).toBe(125); // 25 (light) + 100
|
|
538
|
+
|
|
539
|
+
const stats = recovered.getStats();
|
|
540
|
+
expect(stats.attack).toBe(35);
|
|
541
|
+
expect(stats.defense).toBe(125);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should preserve nested dependencies as proper class instances', () => {
|
|
545
|
+
const iron = new Material('Iron', 15);
|
|
546
|
+
const chainmail = new Armor('light', iron);
|
|
547
|
+
const mace = new Weapon('War Mace', 45);
|
|
548
|
+
|
|
549
|
+
const knight = new Knight('Gawain');
|
|
550
|
+
knight.equip(mace, chainmail);
|
|
551
|
+
|
|
552
|
+
superLs.set('knight2', knight);
|
|
553
|
+
const recovered = superLs.get('knight2');
|
|
554
|
+
|
|
555
|
+
expect(recovered.weapon).toBeInstanceOf(Weapon);
|
|
556
|
+
expect(recovered.armor).toBeInstanceOf(Armor);
|
|
557
|
+
expect(recovered.armor.material).toBeInstanceOf(Material);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
describe('Dependency Injection - Multiple Dependencies (IoC Pattern)', () => {
|
|
562
|
+
beforeEach(() => {
|
|
563
|
+
superLs.register(Logger);
|
|
564
|
+
superLs.register(Database);
|
|
565
|
+
superLs.register(UserService);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should store and retrieve service with multiple dependencies', () => {
|
|
569
|
+
const logger = new Logger('[UserService]');
|
|
570
|
+
const database = new Database();
|
|
571
|
+
database.connect();
|
|
572
|
+
|
|
573
|
+
const userService = new UserService(database, logger);
|
|
574
|
+
|
|
575
|
+
superLs.set('userService', userService);
|
|
576
|
+
const recovered = superLs.get('userService');
|
|
577
|
+
|
|
578
|
+
expect(recovered).toBeInstanceOf(UserService);
|
|
579
|
+
expect(recovered.serviceName).toBe('UserService');
|
|
580
|
+
expect(recovered.database).toBeDefined();
|
|
581
|
+
expect(recovered.logger).toBeDefined();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('should handle service with state in dependencies', () => {
|
|
585
|
+
const logger = new Logger('[APP]');
|
|
586
|
+
logger.log('Application started');
|
|
587
|
+
logger.log('Loading config');
|
|
588
|
+
|
|
589
|
+
const database = new Database();
|
|
590
|
+
database.connect();
|
|
591
|
+
database.save('user1', { id: 'user1', name: 'John' });
|
|
592
|
+
|
|
593
|
+
const userService = new UserService(database, logger);
|
|
594
|
+
userService.createUser('user2', 'Jane');
|
|
595
|
+
|
|
596
|
+
superLs.set('service', userService);
|
|
597
|
+
const recovered = superLs.get('service');
|
|
598
|
+
|
|
599
|
+
// Check logger state preserved
|
|
600
|
+
expect(recovered.logger.prefix).toBe('[APP]');
|
|
601
|
+
expect(recovered.logger.logs).toContain('[APP] Application started');
|
|
602
|
+
expect(recovered.logger.logs).toContain('[APP] Creating user: Jane');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('should work with services using static hydrate', () => {
|
|
606
|
+
const logger = new Logger('[HYDRATE]');
|
|
607
|
+
logger.log('Test message');
|
|
608
|
+
|
|
609
|
+
const database = new Database();
|
|
610
|
+
database.save('key1', { data: 'value1' });
|
|
611
|
+
|
|
612
|
+
const service = new UserService(database, logger);
|
|
613
|
+
|
|
614
|
+
superLs.set('hydratedService', service);
|
|
615
|
+
const recovered = superLs.get('hydratedService');
|
|
616
|
+
|
|
617
|
+
expect(recovered).toBeInstanceOf(UserService);
|
|
618
|
+
// Note: Due to hydrate, logger and database are reconstructed
|
|
619
|
+
expect(recovered.logger).toBeDefined();
|
|
620
|
+
expect(recovered.database).toBeDefined();
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
describe('Dependency Injection - Circular Dependencies', () => {
|
|
625
|
+
beforeEach(() => {
|
|
626
|
+
superLs.register(Parent);
|
|
627
|
+
superLs.register(Child);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('should handle parent-child circular references', () => {
|
|
631
|
+
const parent = new Parent('John');
|
|
632
|
+
const child1 = new Child('Alice');
|
|
633
|
+
const child2 = new Child('Bob');
|
|
634
|
+
|
|
635
|
+
parent.addChild(child1);
|
|
636
|
+
parent.addChild(child2);
|
|
637
|
+
|
|
638
|
+
superLs.set('family', parent);
|
|
639
|
+
const recovered = superLs.get('family');
|
|
640
|
+
|
|
641
|
+
expect(recovered.name).toBe('John');
|
|
642
|
+
expect(recovered.children).toHaveLength(2);
|
|
643
|
+
expect(recovered.getChildrenNames()).toEqual(['Alice', 'Bob']);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should preserve bidirectional references', () => {
|
|
647
|
+
const parent = new Parent('Mary');
|
|
648
|
+
const child = new Child('Tom');
|
|
649
|
+
|
|
650
|
+
parent.addChild(child);
|
|
651
|
+
|
|
652
|
+
superLs.set('parentChild', parent);
|
|
653
|
+
const recovered = superLs.get('parentChild');
|
|
654
|
+
|
|
655
|
+
// Child should reference back to parent
|
|
656
|
+
const recoveredChild = recovered.children[0];
|
|
657
|
+
expect(recoveredChild.getParentName()).toBe('Mary');
|
|
658
|
+
expect(recoveredChild.parent).toBe(recovered);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
describe('Dependency Injection - Array of Dependencies', () => {
|
|
663
|
+
beforeEach(() => {
|
|
664
|
+
superLs.register(Weapon);
|
|
665
|
+
superLs.register(Warrior);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('should handle array of class instances as dependency', () => {
|
|
669
|
+
// Create multiple weapons
|
|
670
|
+
const weapons = [
|
|
671
|
+
new Weapon('Sword', 30),
|
|
672
|
+
new Weapon('Axe', 40),
|
|
673
|
+
new Weapon('Bow', 25)
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
// Store as plain object with array of registered classes
|
|
677
|
+
const armory = { weapons, owner: 'Kingdom' };
|
|
678
|
+
|
|
679
|
+
superLs.set('armory', armory);
|
|
680
|
+
const recovered = superLs.get('armory');
|
|
681
|
+
|
|
682
|
+
expect(recovered.weapons).toHaveLength(3);
|
|
683
|
+
expect(recovered.weapons[0]).toBeInstanceOf(Weapon);
|
|
684
|
+
expect(recovered.weapons[1].attack()).toBe('Attacks with Axe for 40 damage!');
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it('should handle Map with class instances as values', () => {
|
|
688
|
+
const inventory = new Map();
|
|
689
|
+
inventory.set('primary', new Weapon('Main Sword', 50));
|
|
690
|
+
inventory.set('secondary', new Weapon('Dagger', 20));
|
|
691
|
+
|
|
692
|
+
superLs.set('inventory', inventory);
|
|
693
|
+
const recovered = superLs.get('inventory');
|
|
694
|
+
|
|
695
|
+
expect(recovered).toBeInstanceOf(Map);
|
|
696
|
+
expect(recovered.get('primary')).toBeInstanceOf(Weapon);
|
|
697
|
+
expect(recovered.get('secondary').damage).toBe(20);
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
describe('Complex Cases', () => {
|
|
702
|
+
it('should handle nested objects with Map', () => {
|
|
703
|
+
const nested = {
|
|
704
|
+
level1: {
|
|
705
|
+
level2: {
|
|
706
|
+
config: new Map([['theme', 'dark'], ['lang', 'en']])
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
superLs.set('nested', nested);
|
|
711
|
+
|
|
712
|
+
const recovered = superLs.get('nested');
|
|
713
|
+
|
|
714
|
+
expect(recovered.level1.level2.config).toBeInstanceOf(Map);
|
|
715
|
+
expect(recovered.level1.level2.config.get('theme')).toBe('dark');
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should handle arrays with mixed types', () => {
|
|
719
|
+
const arr = [1, 'two', new Date('2024-01-01'), new Set([1, 2, 3])];
|
|
720
|
+
superLs.set('mixed', arr);
|
|
721
|
+
|
|
722
|
+
const recovered = superLs.get('mixed');
|
|
723
|
+
|
|
724
|
+
expect(recovered[0]).toBe(1);
|
|
725
|
+
expect(recovered[1]).toBe('two');
|
|
726
|
+
expect(recovered[2]).toBeInstanceOf(Date);
|
|
727
|
+
expect(recovered[3]).toBeInstanceOf(Set);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it('should return null for non-existent keys', () => {
|
|
731
|
+
const result = superLs.get('does_not_exist');
|
|
732
|
+
|
|
733
|
+
expect(result).toBeNull();
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
describe('README Validation', () => {
|
|
738
|
+
it('Basic Usage: Map with user_settings', () => {
|
|
739
|
+
const myMap = new Map();
|
|
740
|
+
myMap.set("user_id", 12345);
|
|
741
|
+
myMap.set("roles", ["admin", "editor"]);
|
|
742
|
+
|
|
743
|
+
superLs.set("user_settings", myMap);
|
|
744
|
+
const recoveredSettings = superLs.get("user_settings");
|
|
745
|
+
|
|
746
|
+
expect(recoveredSettings).toBeInstanceOf(Map);
|
|
747
|
+
expect(recoveredSettings.get("user_id")).toBe(12345);
|
|
748
|
+
expect(recoveredSettings.get("roles")).toEqual(["admin", "editor"]);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it('Class Hydration: Player with methods', () => {
|
|
752
|
+
superLs.register(Player);
|
|
753
|
+
|
|
754
|
+
const p1 = new Player("Alice", 100);
|
|
755
|
+
superLs.set("player_1", p1);
|
|
756
|
+
const restoredPlayer = superLs.get("player_1");
|
|
757
|
+
|
|
758
|
+
expect(restoredPlayer.name).toBe("Alice");
|
|
759
|
+
expect(restoredPlayer.greet()).toBe("Hello, I am Alice!");
|
|
760
|
+
expect(restoredPlayer).toBeInstanceOf(Player);
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe('Internals (Under the Hood)', () => {
|
|
765
|
+
it('should use devalue for serialization', () => {
|
|
766
|
+
superLs.set('internal', { a: 1 });
|
|
767
|
+
|
|
768
|
+
const raw = mockStorage.get('sls_internal');
|
|
769
|
+
|
|
770
|
+
expect(typeof raw).toBe('string');
|
|
771
|
+
expect(() => parse(raw)).not.toThrow();
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('should add __super_type__ metadata for registered classes', () => {
|
|
775
|
+
superLs.register(Player);
|
|
776
|
+
superLs.set('meta', new Player('Test', 50));
|
|
777
|
+
|
|
778
|
+
const raw = mockStorage.get('sls_meta');
|
|
779
|
+
const parsed = parse(raw);
|
|
780
|
+
|
|
781
|
+
expect(parsed.__super_type__).toBe('Player');
|
|
782
|
+
expect(parsed.__data__).toBeDefined();
|
|
783
|
+
expect(parsed.__data__.name).toBe('Test');
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
describe('Error Handling', () => {
|
|
788
|
+
it('should throw error when registering non-class values', () => {
|
|
789
|
+
expect(() => superLs.register('not a class')).toThrow();
|
|
790
|
+
expect(() => superLs.register(123)).toThrow();
|
|
791
|
+
expect(() => superLs.register(null)).toThrow();
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
});
|