@blac/core 2.0.0-rn-12 → 2.0.1

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.
@@ -1,561 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { BlacAdapter } from '../BlacAdapter';
3
- import { Cubit } from '../../Cubit';
4
- import { Blac } from '../../Blac';
5
-
6
- // Test Cubit implementations
7
- class CounterCubit extends Cubit<number> {
8
- static isolated = false;
9
- static keepAlive = false;
10
-
11
- constructor() {
12
- super(0);
13
- }
14
-
15
- increment = () => this.emit(this.state + 1);
16
- decrement = () => this.emit(this.state - 1);
17
- }
18
-
19
- interface UserState {
20
- name: string;
21
- age: number;
22
- profile: {
23
- email: string;
24
- preferences: {
25
- theme: string;
26
- };
27
- };
28
- }
29
-
30
- class UserCubit extends Cubit<UserState> {
31
- constructor() {
32
- super({
33
- name: 'John',
34
- age: 30,
35
- profile: {
36
- email: 'john@example.com',
37
- preferences: {
38
- theme: 'light',
39
- },
40
- },
41
- });
42
- }
43
-
44
- updateName = (name: string) => {
45
- this.patch({ name });
46
- };
47
-
48
- updateTheme = (theme: string) => {
49
- this.emit({
50
- ...this.state,
51
- profile: {
52
- ...this.state.profile,
53
- preferences: { theme },
54
- },
55
- });
56
- };
57
- }
58
-
59
- describe('BlacAdapter', () => {
60
- let blacInstance: Blac;
61
- let componentRef: { current: object };
62
-
63
- beforeEach(() => {
64
- blacInstance = new Blac({ __unsafe_ignore_singleton: true });
65
- componentRef = { current: {} };
66
- vi.clearAllMocks();
67
- });
68
-
69
- afterEach(() => {
70
- vi.restoreAllMocks();
71
- });
72
-
73
- describe('Initialization and Instance Management', () => {
74
- it('should create adapter with proper initialization', () => {
75
- const adapter = new BlacAdapter(
76
- { componentRef, blocConstructor: CounterCubit },
77
- { id: 'test-counter' },
78
- );
79
-
80
- expect(adapter.id).toMatch(/^consumer-/);
81
- expect(adapter.blocConstructor).toBe(CounterCubit);
82
- expect(adapter.componentRef).toBe(componentRef);
83
- expect(adapter.blocInstance).toBeInstanceOf(CounterCubit);
84
- });
85
-
86
- it('should retrieve existing bloc instance when not isolated', () => {
87
- // Create first adapter
88
- const adapter1 = new BlacAdapter(
89
- { componentRef, blocConstructor: CounterCubit },
90
- { id: 'shared-counter' },
91
- );
92
-
93
- // Create second adapter with same id
94
- const componentRef2 = { current: {} };
95
- const adapter2 = new BlacAdapter(
96
- { componentRef: componentRef2, blocConstructor: CounterCubit },
97
- { id: 'shared-counter' },
98
- );
99
-
100
- // Should share the same bloc instance
101
- expect(adapter1.blocInstance).toBe(adapter2.blocInstance);
102
- });
103
-
104
- it('should pass props to bloc constructor', () => {
105
- class PropsCubit extends Cubit<string, { initialValue: string }> {
106
- constructor(
107
- public override props: { initialValue: string } | null = null,
108
- ) {
109
- super(props?.initialValue || 'default');
110
- }
111
- }
112
-
113
- const adapter = new BlacAdapter(
114
- { componentRef, blocConstructor: PropsCubit as any },
115
- { props: { initialValue: 'custom' } },
116
- );
117
-
118
- expect(adapter.blocInstance.state).toBe('custom');
119
- expect(adapter.blocInstance.props).toEqual({ initialValue: 'custom' });
120
- });
121
- });
122
-
123
- describe('Dependency Tracking - Explicit Dependencies', () => {
124
- it('should track dependencies when dependencies function is provided', () => {
125
- const dependencyFn = vi.fn((bloc: UserCubit) => [
126
- bloc.state.name,
127
- bloc.state.age,
128
- ]);
129
-
130
- const adapter = new BlacAdapter(
131
- { componentRef, blocConstructor: UserCubit },
132
- { dependencies: dependencyFn },
133
- );
134
-
135
- expect(dependencyFn).toHaveBeenCalledWith(adapter.blocInstance);
136
- expect(adapter.options?.dependencies).toBe(dependencyFn);
137
- });
138
-
139
- it('should only trigger onChange when dependencies change', () => {
140
- const onChange = vi.fn();
141
- const adapter = new BlacAdapter(
142
- { componentRef, blocConstructor: UserCubit },
143
- { dependencies: (bloc) => [bloc.state.name] },
144
- );
145
-
146
- const unsubscribe = adapter.createSubscription({ onChange });
147
-
148
- // Change name - should trigger
149
- adapter.blocInstance.updateName('Jane');
150
- expect(onChange).toHaveBeenCalledTimes(1);
151
-
152
- // Change theme - should NOT trigger (not in dependencies)
153
- onChange.mockClear();
154
- adapter.blocInstance.updateTheme('dark');
155
- expect(onChange).not.toHaveBeenCalled();
156
-
157
- unsubscribe();
158
- });
159
-
160
- it('should handle dependency array changes correctly', () => {
161
- const onChange = vi.fn();
162
- let depCount = 1;
163
-
164
- const adapter = new BlacAdapter(
165
- { componentRef, blocConstructor: CounterCubit },
166
- {
167
- dependencies: (bloc) => {
168
- // Variable length dependency array
169
- const deps = [];
170
- for (let i = 0; i < depCount; i++) {
171
- deps.push(bloc.state);
172
- }
173
- return deps;
174
- },
175
- },
176
- );
177
-
178
- const unsubscribe = adapter.createSubscription({ onChange });
179
-
180
- // Initial change
181
- adapter.blocInstance.increment();
182
- expect(onChange).toHaveBeenCalledTimes(1);
183
-
184
- // Change dependency array length
185
- depCount = 2;
186
- onChange.mockClear();
187
- adapter.blocInstance.increment();
188
- expect(onChange).toHaveBeenCalledTimes(1); // Length change triggers update
189
-
190
- unsubscribe();
191
- });
192
- });
193
-
194
- describe('Dependency Tracking - Proxy-based (Automatic)', () => {
195
- it('should track state access through proxy when no explicit dependencies', () => {
196
- const adapter = new BlacAdapter({
197
- componentRef,
198
- blocConstructor: UserCubit,
199
- });
200
-
201
- // Access state through proxy
202
- const proxyState = adapter.getProxyState(adapter.blocInstance.state);
203
- const name = proxyState.name;
204
- const email = proxyState.profile.email;
205
-
206
- // Check tracked dependencies
207
- const dependencies = adapter.getConsumerDependencies(
208
- componentRef.current,
209
- );
210
- expect(dependencies?.statePaths).toContain('name');
211
- expect(dependencies?.statePaths).toContain('profile.email');
212
- });
213
-
214
- it('should track class property access through proxy', () => {
215
- class GetterCubit extends Cubit<number> {
216
- constructor() {
217
- super(0);
218
- }
219
-
220
- get doubled() {
221
- return this.state * 2;
222
- }
223
-
224
- get isPositive() {
225
- return this.state > 0;
226
- }
227
- }
228
-
229
- const adapter = new BlacAdapter({
230
- componentRef,
231
- blocConstructor: GetterCubit,
232
- });
233
-
234
- // Access getters through proxy
235
- const proxyBloc = adapter.getProxyBlocInstance();
236
- const doubled = proxyBloc.doubled;
237
- const isPositive = proxyBloc.isPositive;
238
-
239
- // Check tracked dependencies
240
- const dependencies = adapter.getConsumerDependencies(
241
- componentRef.current,
242
- );
243
- expect(dependencies?.classPaths).toContain('doubled');
244
- expect(dependencies?.classPaths).toContain('isPositive');
245
- });
246
-
247
- it('should only notify when tracked values change', () => {
248
- const onChange = vi.fn();
249
- const adapter = new BlacAdapter({
250
- componentRef,
251
- blocConstructor: UserCubit,
252
- });
253
-
254
- // Track specific property access
255
- adapter.resetConsumerTracking();
256
- const proxyState = adapter.getProxyState(adapter.blocInstance.state);
257
- const name = proxyState.name; // Only track name
258
-
259
- // Mark as rendered to enable dependency checking
260
- adapter.updateLastNotified(componentRef.current);
261
-
262
- const unsubscribe = adapter.createSubscription({ onChange });
263
-
264
- // Change tracked property - should notify
265
- adapter.blocInstance.updateName('Jane');
266
- expect(onChange).toHaveBeenCalledTimes(1);
267
-
268
- // Change untracked property - should NOT notify
269
- onChange.mockClear();
270
- adapter.blocInstance.updateTheme('dark');
271
- expect(onChange).not.toHaveBeenCalled();
272
-
273
- unsubscribe();
274
- });
275
-
276
- it('should handle nested property tracking', () => {
277
- const adapter = new BlacAdapter({
278
- componentRef,
279
- blocConstructor: UserCubit,
280
- });
281
-
282
- // Access nested property
283
- const proxyState = adapter.getProxyState(adapter.blocInstance.state);
284
- const theme = proxyState.profile.preferences.theme;
285
-
286
- // Check tracked path
287
- const dependencies = adapter.getConsumerDependencies(
288
- componentRef.current,
289
- );
290
- expect(dependencies?.statePaths).toContain('profile.preferences.theme');
291
- });
292
- });
293
-
294
- describe('Lifecycle Management', () => {
295
- it('should handle mount and unmount correctly', () => {
296
- const onMount = vi.fn();
297
- const onUnmount = vi.fn();
298
-
299
- const adapter = new BlacAdapter(
300
- { componentRef, blocConstructor: CounterCubit },
301
- { onMount, onUnmount },
302
- );
303
-
304
- // Mount
305
- adapter.mount();
306
- expect(onMount).toHaveBeenCalledWith(adapter.blocInstance);
307
- expect(adapter.calledOnMount).toBe(true);
308
- expect(adapter.blocInstance._consumers.has(adapter.id)).toBe(true);
309
-
310
- // Mount again - should not call onMount twice
311
- adapter.mount();
312
- expect(onMount).toHaveBeenCalledTimes(1);
313
-
314
- // Unmount
315
- adapter.unmount();
316
- expect(onUnmount).toHaveBeenCalledWith(adapter.blocInstance);
317
- expect(adapter.blocInstance._consumers.has(adapter.id)).toBe(false);
318
- });
319
-
320
- it('should handle onMount errors', () => {
321
- const onMount = vi.fn(() => {
322
- throw new Error('Mount error');
323
- });
324
-
325
- const adapter = new BlacAdapter(
326
- { componentRef, blocConstructor: CounterCubit },
327
- { onMount },
328
- );
329
-
330
- expect(() => adapter.mount()).toThrow('Mount error');
331
- });
332
-
333
- it('should not throw on unmount errors', () => {
334
- const onUnmount = vi.fn(() => {
335
- throw new Error('Unmount error');
336
- });
337
-
338
- const adapter = new BlacAdapter(
339
- { componentRef, blocConstructor: CounterCubit },
340
- { onUnmount },
341
- );
342
-
343
- adapter.mount();
344
-
345
- // Should not throw
346
- expect(() => adapter.unmount()).not.toThrow();
347
- });
348
-
349
- it('should refresh dependencies on mount', () => {
350
- const dependencies = vi.fn((bloc: CounterCubit) => [bloc.state]);
351
-
352
- const adapter = new BlacAdapter(
353
- { componentRef, blocConstructor: CounterCubit },
354
- { dependencies },
355
- );
356
-
357
- expect(dependencies).toHaveBeenCalledTimes(1); // Initial call
358
-
359
- adapter.mount();
360
- expect(dependencies).toHaveBeenCalledTimes(2); // Called again on mount
361
- });
362
- });
363
-
364
- describe('Subscription Management', () => {
365
- it('should create and cleanup subscriptions', () => {
366
- const adapter = new BlacAdapter({
367
- componentRef,
368
- blocConstructor: CounterCubit,
369
- });
370
-
371
- const onChange = vi.fn();
372
- const unsubscribe = adapter.createSubscription({ onChange });
373
-
374
- expect(adapter.blocInstance._observer.size).toBe(1);
375
-
376
- // Trigger state change
377
- adapter.blocInstance.increment();
378
- expect(onChange).toHaveBeenCalled();
379
-
380
- // Cleanup
381
- unsubscribe();
382
- expect(adapter.blocInstance._observer.size).toBe(0);
383
- });
384
-
385
- it('should handle first render without dependencies check', () => {
386
- const onChange = vi.fn();
387
- const adapter = new BlacAdapter({
388
- componentRef,
389
- blocConstructor: UserCubit,
390
- });
391
-
392
- // Don't mark as rendered yet
393
- const unsubscribe = adapter.createSubscription({ onChange });
394
-
395
- // First state change should always notify
396
- adapter.blocInstance.updateName('Jane');
397
- expect(onChange).toHaveBeenCalledTimes(1);
398
-
399
- unsubscribe();
400
- });
401
- });
402
-
403
- describe('Proxy Creation and Management', () => {
404
- it('should return raw state when using explicit dependencies', () => {
405
- const adapter = new BlacAdapter(
406
- { componentRef, blocConstructor: UserCubit },
407
- { dependencies: (bloc) => [bloc.state.name] },
408
- );
409
-
410
- const state = adapter.getProxyState(adapter.blocInstance.state);
411
-
412
- // Should be raw state, not proxy
413
- expect(state).toBe(adapter.blocInstance.state);
414
- });
415
-
416
- it('should return raw bloc instance when using explicit dependencies', () => {
417
- const adapter = new BlacAdapter(
418
- { componentRef, blocConstructor: CounterCubit },
419
- { dependencies: (bloc) => [bloc.state] },
420
- );
421
-
422
- const blocInstance = adapter.getProxyBlocInstance();
423
-
424
- // Should be raw instance, not proxy
425
- expect(blocInstance).toBe(adapter.blocInstance);
426
- });
427
-
428
- it('should create proxies when not using explicit dependencies', () => {
429
- const adapter = new BlacAdapter({
430
- componentRef,
431
- blocConstructor: UserCubit,
432
- });
433
-
434
- const proxyState = adapter.getProxyState(adapter.blocInstance.state);
435
- const proxyBloc = adapter.getProxyBlocInstance();
436
-
437
- // Should be proxies
438
- expect(proxyState).not.toBe(adapter.blocInstance.state);
439
- expect(proxyBloc).not.toBe(adapter.blocInstance);
440
- });
441
- });
442
-
443
- describe('Consumer Tracking Integration', () => {
444
- it('should properly track and validate consumers', () => {
445
- const adapter = new BlacAdapter({
446
- componentRef,
447
- blocConstructor: CounterCubit,
448
- });
449
-
450
- adapter.mount();
451
-
452
- // Consumer should be tracked
453
- const hasConsumer = adapter.shouldNotifyConsumer(
454
- componentRef.current,
455
- new Set(['state']),
456
- );
457
- expect(hasConsumer).toBe(true); // First render always notifies
458
-
459
- // Update tracking info
460
- adapter.updateLastNotified(componentRef.current);
461
-
462
- // Now should use dependency tracking
463
- const shouldNotify = adapter.shouldNotifyConsumer(
464
- componentRef.current,
465
- new Set(['untracked']),
466
- );
467
- expect(shouldNotify).toBe(false);
468
- });
469
-
470
- it('should reset consumer tracking', () => {
471
- const adapter = new BlacAdapter({
472
- componentRef,
473
- blocConstructor: UserCubit,
474
- });
475
-
476
- // Track some accesses
477
- adapter.trackAccess(componentRef.current, 'state', 'name', 'John');
478
- adapter.trackAccess(componentRef.current, 'state', 'age', 30);
479
-
480
- const depsBefore = adapter.getConsumerDependencies(componentRef.current);
481
- expect(depsBefore?.statePaths.length).toBe(2);
482
-
483
- // Reset tracking
484
- adapter.resetConsumerTracking();
485
-
486
- const depsAfter = adapter.getConsumerDependencies(componentRef.current);
487
- expect(depsAfter?.statePaths.length).toBe(0);
488
- });
489
- });
490
-
491
- describe('Options Updates', () => {
492
- it('should update options after initialization', () => {
493
- const adapter = new BlacAdapter({
494
- componentRef,
495
- blocConstructor: CounterCubit,
496
- });
497
-
498
- expect(adapter.options).toBeUndefined();
499
-
500
- const newOptions = {
501
- dependencies: (bloc: CounterCubit) => [bloc.state],
502
- };
503
-
504
- adapter.options = newOptions;
505
- expect(adapter.options).toBe(newOptions);
506
- });
507
- });
508
-
509
- describe('Edge Cases and Error Handling', () => {
510
- it('should handle missing componentRef gracefully', () => {
511
- // Can't use null as WeakMap key, so test with an empty object instead
512
- const emptyRef = {};
513
- const adapter = new BlacAdapter({
514
- componentRef: { current: emptyRef },
515
- blocConstructor: CounterCubit,
516
- });
517
-
518
- // Should not throw
519
- expect(() => adapter.mount()).not.toThrow();
520
- expect(() =>
521
- adapter.trackAccess(emptyRef, 'state', 'test'),
522
- ).not.toThrow();
523
- });
524
-
525
- it('should handle rapid mount/unmount cycles', () => {
526
- const adapter = new BlacAdapter({
527
- componentRef,
528
- blocConstructor: CounterCubit,
529
- });
530
-
531
- // Rapid mount/unmount
532
- for (let i = 0; i < 10; i++) {
533
- adapter.mount();
534
- adapter.unmount();
535
- }
536
-
537
- expect(adapter.blocInstance._consumers.size).toBe(0);
538
- });
539
-
540
- it('should handle value changes with Object.is semantics', () => {
541
- const onChange = vi.fn();
542
- const adapter = new BlacAdapter(
543
- { componentRef, blocConstructor: CounterCubit },
544
- { dependencies: (bloc) => [bloc.state] },
545
- );
546
-
547
- const unsubscribe = adapter.createSubscription({ onChange });
548
-
549
- // Set to NaN
550
- adapter.blocInstance.emit(NaN);
551
- expect(onChange).toHaveBeenCalledTimes(1);
552
-
553
- // Set to NaN again - should not trigger (Object.is(NaN, NaN) === true)
554
- onChange.mockClear();
555
- adapter.blocInstance.emit(NaN);
556
- expect(onChange).not.toHaveBeenCalled();
557
-
558
- unsubscribe();
559
- });
560
- });
561
- });