@blac/core 2.0.0-rc-8 → 2.0.0-rc-11

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.
@@ -0,0 +1,473 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Cubit } from '../Cubit';
3
+ import { Blac } from '../Blac';
4
+
5
+ // Test Cubit implementations
6
+ class CounterCubit extends Cubit<number> {
7
+ constructor(initialValue = 0) {
8
+ super(initialValue);
9
+ }
10
+
11
+ increment = () => this.emit(this.state + 1);
12
+ decrement = () => this.emit(this.state - 1);
13
+ set = (value: number) => this.emit(value);
14
+ }
15
+
16
+ interface UserState {
17
+ name: string;
18
+ age: number;
19
+ email: string;
20
+ preferences?: {
21
+ theme: string;
22
+ notifications: boolean;
23
+ };
24
+ }
25
+
26
+ class UserCubit extends Cubit<UserState> {
27
+ constructor() {
28
+ super({
29
+ name: 'John Doe',
30
+ age: 30,
31
+ email: 'john@example.com',
32
+ });
33
+ }
34
+
35
+ updateName = (name: string) => {
36
+ this.patch({ name });
37
+ };
38
+
39
+ updateMultiple = (updates: Partial<UserState>) => {
40
+ this.patch(updates);
41
+ };
42
+
43
+ setPreferences = (theme: string, notifications: boolean) => {
44
+ this.patch({
45
+ preferences: { theme, notifications },
46
+ });
47
+ };
48
+ }
49
+
50
+ class PrimitiveCubit extends Cubit<string> {
51
+ constructor() {
52
+ super('initial');
53
+ }
54
+
55
+ update = (value: string) => this.emit(value);
56
+ }
57
+
58
+ describe('Cubit State Emissions', () => {
59
+ let blacInstance: Blac;
60
+
61
+ beforeEach(() => {
62
+ blacInstance = new Blac({ __unsafe_ignore_singleton: true });
63
+ Blac.enableLog = false;
64
+ vi.clearAllMocks();
65
+ });
66
+
67
+ describe('Basic emit() functionality', () => {
68
+ it('should emit new state and notify observers', () => {
69
+ const cubit = new CounterCubit();
70
+ const observer = vi.fn();
71
+
72
+ cubit._observer.subscribe({ id: 'test', fn: observer });
73
+
74
+ cubit.increment();
75
+
76
+ expect(observer).toHaveBeenCalledWith(1, 0, undefined);
77
+ expect(cubit.state).toBe(1);
78
+ });
79
+
80
+ it('should not emit when new state is identical (Object.is comparison)', () => {
81
+ const cubit = new CounterCubit(5);
82
+ const observer = vi.fn();
83
+
84
+ cubit._observer.subscribe({ id: 'test', fn: observer });
85
+
86
+ // Try to emit same value
87
+ cubit.set(5);
88
+
89
+ expect(observer).not.toHaveBeenCalled();
90
+ expect(cubit.state).toBe(5);
91
+ });
92
+
93
+ it('should handle NaN correctly with Object.is', () => {
94
+ const cubit = new CounterCubit(NaN);
95
+ const observer = vi.fn();
96
+
97
+ cubit._observer.subscribe({ id: 'test', fn: observer });
98
+
99
+ // NaN === NaN is false, but Object.is(NaN, NaN) is true
100
+ cubit.set(NaN);
101
+
102
+ expect(observer).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it('should distinguish between +0 and -0', () => {
106
+ const cubit = new CounterCubit(0);
107
+ const observer = vi.fn();
108
+
109
+ cubit._observer.subscribe({ id: 'test', fn: observer });
110
+
111
+ // Object.is can distinguish +0 and -0
112
+ cubit.set(-0);
113
+
114
+ expect(observer).toHaveBeenCalledWith(-0, 0, undefined);
115
+ });
116
+
117
+ it('should emit multiple state changes sequentially', () => {
118
+ const cubit = new CounterCubit();
119
+ const observer = vi.fn();
120
+
121
+ cubit._observer.subscribe({ id: 'test', fn: observer });
122
+
123
+ cubit.increment();
124
+ cubit.increment();
125
+ cubit.decrement();
126
+
127
+ expect(observer).toHaveBeenCalledTimes(3);
128
+ expect(observer).toHaveBeenNthCalledWith(1, 1, 0, undefined);
129
+ expect(observer).toHaveBeenNthCalledWith(2, 2, 1, undefined);
130
+ expect(observer).toHaveBeenNthCalledWith(3, 1, 2, undefined);
131
+ expect(cubit.state).toBe(1);
132
+ });
133
+ });
134
+
135
+ describe('patch() functionality for object states', () => {
136
+ it('should partially update object state', () => {
137
+ const cubit = new UserCubit();
138
+ const observer = vi.fn();
139
+
140
+ cubit._observer.subscribe({ id: 'test', fn: observer });
141
+
142
+ cubit.updateName('Jane Doe');
143
+
144
+ expect(observer).toHaveBeenCalledWith(
145
+ {
146
+ name: 'Jane Doe',
147
+ age: 30,
148
+ email: 'john@example.com',
149
+ },
150
+ {
151
+ name: 'John Doe',
152
+ age: 30,
153
+ email: 'john@example.com',
154
+ },
155
+ undefined,
156
+ );
157
+ expect(cubit.state.name).toBe('Jane Doe');
158
+ expect(cubit.state.age).toBe(30); // Unchanged
159
+ });
160
+
161
+ it('should update multiple properties at once', () => {
162
+ const cubit = new UserCubit();
163
+ const observer = vi.fn();
164
+
165
+ cubit._observer.subscribe({ id: 'test', fn: observer });
166
+
167
+ cubit.updateMultiple({ name: 'Jane Smith', age: 25 });
168
+
169
+ expect(observer).toHaveBeenCalledTimes(1);
170
+ expect(cubit.state).toEqual({
171
+ name: 'Jane Smith',
172
+ age: 25,
173
+ email: 'john@example.com',
174
+ });
175
+ });
176
+
177
+ it('should not emit when patched values are identical', () => {
178
+ const cubit = new UserCubit();
179
+ const observer = vi.fn();
180
+
181
+ cubit._observer.subscribe({ id: 'test', fn: observer });
182
+
183
+ // Patch with same values
184
+ cubit.patch({ name: 'John Doe', age: 30 });
185
+
186
+ expect(observer).not.toHaveBeenCalled();
187
+ });
188
+
189
+ it('should emit when at least one patched value differs', () => {
190
+ const cubit = new UserCubit();
191
+ const observer = vi.fn();
192
+
193
+ cubit._observer.subscribe({ id: 'test', fn: observer });
194
+
195
+ // One value different
196
+ cubit.patch({ name: 'John Doe', age: 31 });
197
+
198
+ expect(observer).toHaveBeenCalledTimes(1);
199
+ expect(cubit.state.age).toBe(31);
200
+ });
201
+
202
+ it('should handle nested object updates', () => {
203
+ const cubit = new UserCubit();
204
+ const observer = vi.fn();
205
+
206
+ cubit._observer.subscribe({ id: 'test', fn: observer });
207
+
208
+ cubit.setPreferences('dark', true);
209
+
210
+ expect(observer).toHaveBeenCalledTimes(1);
211
+ expect(cubit.state.preferences).toEqual({
212
+ theme: 'dark',
213
+ notifications: true,
214
+ });
215
+ });
216
+
217
+ it('should handle patch with ignoreChangeCheck flag', () => {
218
+ const cubit = new UserCubit();
219
+ const observer = vi.fn();
220
+
221
+ cubit._observer.subscribe({ id: 'test', fn: observer });
222
+
223
+ // Force emit even with same values
224
+ cubit.patch({ name: 'John Doe' }, true);
225
+
226
+ expect(observer).toHaveBeenCalledTimes(1);
227
+ });
228
+
229
+ it('should warn when patch is called on non-object state', () => {
230
+ const cubit = new PrimitiveCubit();
231
+ const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
232
+
233
+ cubit.patch('new value' as any);
234
+
235
+ expect(warnSpy).toHaveBeenCalledWith(
236
+ expect.stringContaining(
237
+ 'Cubit.patch: was called on a cubit where the state is not an object',
238
+ ),
239
+ );
240
+ expect(cubit.state).toBe('initial'); // State unchanged
241
+ });
242
+
243
+ it('should handle null state gracefully', () => {
244
+ class NullStateCubit extends Cubit<null> {
245
+ constructor() {
246
+ super(null);
247
+ }
248
+ }
249
+
250
+ const cubit = new NullStateCubit();
251
+ const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
252
+
253
+ cubit.patch({} as any);
254
+
255
+ expect(warnSpy).toHaveBeenCalled();
256
+ expect(cubit.state).toBe(null);
257
+ });
258
+ });
259
+
260
+ describe('State change detection edge cases', () => {
261
+ it('should detect changes in array states', () => {
262
+ class ArrayCubit extends Cubit<number[]> {
263
+ constructor() {
264
+ super([1, 2, 3]);
265
+ }
266
+
267
+ add = (num: number) => {
268
+ this.emit([...this.state, num]);
269
+ };
270
+
271
+ updateIndex = (index: number, value: number) => {
272
+ const newArray = [...this.state];
273
+ newArray[index] = value;
274
+ this.emit(newArray);
275
+ };
276
+ }
277
+
278
+ const cubit = new ArrayCubit();
279
+ const observer = vi.fn();
280
+
281
+ cubit._observer.subscribe({ id: 'test', fn: observer });
282
+
283
+ // New array reference should trigger update
284
+ cubit.add(4);
285
+ expect(observer).toHaveBeenCalledTimes(1);
286
+ expect(cubit.state).toEqual([1, 2, 3, 4]);
287
+
288
+ // Same array reference should not trigger
289
+ observer.mockClear();
290
+ const currentState = cubit.state;
291
+ cubit.emit(currentState);
292
+ expect(observer).not.toHaveBeenCalled();
293
+ });
294
+
295
+ it('should handle undefined and null transitions', () => {
296
+ class NullableCubit extends Cubit<string | null | undefined> {
297
+ constructor() {
298
+ super('initial');
299
+ }
300
+
301
+ setNull = () => this.emit(null);
302
+ setUndefined = () => this.emit(undefined);
303
+ setString = (s: string) => this.emit(s);
304
+ }
305
+
306
+ const cubit = new NullableCubit();
307
+ const observer = vi.fn();
308
+
309
+ cubit._observer.subscribe({ id: 'test', fn: observer });
310
+
311
+ // null and undefined are different according to Object.is
312
+ expect(Object.is(null, undefined)).toBe(false);
313
+
314
+ cubit.setNull();
315
+ expect(observer).toHaveBeenCalledWith(null, 'initial', undefined);
316
+ expect(cubit.state).toBe(null);
317
+
318
+ observer.mockClear();
319
+
320
+ // NOTE: There's a limitation in BlocBase._pushState that prevents undefined states
321
+ // It has a validation: if (newState === undefined) return;
322
+ // So undefined cannot be emitted as a state
323
+ cubit.setUndefined();
324
+
325
+ // State should remain null since undefined is rejected
326
+ expect(observer).not.toHaveBeenCalled();
327
+ expect(cubit.state).toBe(null);
328
+
329
+ observer.mockClear();
330
+ cubit.setString('value');
331
+ expect(observer).toHaveBeenCalledWith('value', null, undefined);
332
+ expect(cubit.state).toBe('value');
333
+ });
334
+
335
+ it('should handle complex object comparisons', () => {
336
+ interface ComplexState {
337
+ data: { id: number; values: number[] };
338
+ metadata: Map<string, any>;
339
+ }
340
+
341
+ class ComplexCubit extends Cubit<ComplexState> {
342
+ constructor() {
343
+ super({
344
+ data: { id: 1, values: [1, 2, 3] },
345
+ metadata: new Map([['key', 'value']]),
346
+ });
347
+ }
348
+
349
+ updateData = (data: ComplexState['data']) => {
350
+ this.patch({ data });
351
+ };
352
+ }
353
+
354
+ const cubit = new ComplexCubit();
355
+ const observer = vi.fn();
356
+
357
+ cubit._observer.subscribe({ id: 'test', fn: observer });
358
+
359
+ // Different object reference with same values
360
+ cubit.updateData({ id: 1, values: [1, 2, 3] });
361
+
362
+ // Should emit because object reference is different
363
+ expect(observer).toHaveBeenCalledTimes(1);
364
+ });
365
+ });
366
+
367
+ describe('Integration with BlocBase', () => {
368
+ it('should properly inherit from BlocBase', () => {
369
+ const cubit = new CounterCubit();
370
+
371
+ // Should have BlocBase properties and methods
372
+ expect(cubit._state).toBe(0);
373
+ expect(cubit._observer).toBeDefined();
374
+ expect(cubit._consumers).toBeDefined();
375
+ expect(typeof cubit._addConsumer).toBe('function');
376
+ });
377
+
378
+ it('should work with state batching', () => {
379
+ const cubit = new CounterCubit();
380
+ const observer = vi.fn();
381
+
382
+ cubit._observer.subscribe({ id: 'test', fn: observer });
383
+
384
+ cubit.batch(() => {
385
+ cubit.increment(); // 0 -> 1
386
+ cubit.increment(); // 1 -> 2
387
+ cubit.increment(); // 2 -> 3
388
+ });
389
+
390
+ // Should only notify once with final state
391
+ // The oldState in the batch notification is from the last update (2 -> 3)
392
+ expect(observer).toHaveBeenCalledTimes(1);
393
+ expect(observer).toHaveBeenCalledWith(3, 2, undefined);
394
+ expect(cubit.state).toBe(3);
395
+ });
396
+
397
+ it('should maintain state history correctly', () => {
398
+ const cubit = new CounterCubit();
399
+
400
+ cubit.increment();
401
+ expect(cubit._oldState).toBe(0);
402
+ expect(cubit._state).toBe(1);
403
+
404
+ cubit.increment();
405
+ expect(cubit._oldState).toBe(1);
406
+ expect(cubit._state).toBe(2);
407
+ });
408
+ });
409
+
410
+ describe('Memory and Performance', () => {
411
+ it('should not leak memory with rapid emissions', () => {
412
+ const cubit = new CounterCubit();
413
+ const observer = vi.fn();
414
+
415
+ cubit._observer.subscribe({ id: 'test', fn: observer });
416
+
417
+ // Rapid emissions
418
+ for (let i = 0; i < 1000; i++) {
419
+ cubit.increment();
420
+ }
421
+
422
+ expect(cubit.state).toBe(1000);
423
+ expect(observer).toHaveBeenCalledTimes(1000);
424
+
425
+ // Check no accumulation of state
426
+ expect(cubit._state).toBe(1000);
427
+ expect(cubit._oldState).toBe(999);
428
+ });
429
+
430
+ it('should handle concurrent patch operations', () => {
431
+ const cubit = new UserCubit();
432
+ const observer = vi.fn();
433
+
434
+ cubit._observer.subscribe({ id: 'test', fn: observer });
435
+
436
+ // Multiple patches in quick succession
437
+ cubit.patch({ name: 'Name1' });
438
+ cubit.patch({ age: 25 });
439
+ cubit.patch({ email: 'new@example.com' });
440
+
441
+ expect(observer).toHaveBeenCalledTimes(3);
442
+ expect(cubit.state).toEqual({
443
+ name: 'Name1',
444
+ age: 25,
445
+ email: 'new@example.com',
446
+ });
447
+ });
448
+ });
449
+
450
+ describe('Type Safety', () => {
451
+ it('should maintain type safety with emit', () => {
452
+ const cubit = new CounterCubit();
453
+
454
+ // TypeScript should enforce number type
455
+ cubit.emit(42);
456
+ expect(cubit.state).toBe(42);
457
+
458
+ // Test that emit only accepts numbers
459
+ // cubit.emit('not a number'); // This would be a type error
460
+ });
461
+
462
+ it('should maintain type safety with patch', () => {
463
+ const cubit = new UserCubit();
464
+
465
+ // Valid patch
466
+ cubit.patch({ name: 'New Name' });
467
+
468
+ // Test that patch only accepts valid properties and types
469
+ // cubit.patch({ invalidProp: 'value' }); // This would be a type error
470
+ // cubit.patch({ age: 'not a number' }); // This would be a type error
471
+ });
472
+ });
473
+ });