@blac/core 2.0.0-rc-13 → 2.0.0-rc-16

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.
Files changed (54) hide show
  1. package/package.json +10 -15
  2. package/src/Blac.ts +38 -71
  3. package/src/Bloc.ts +8 -9
  4. package/src/BlocBase.ts +246 -540
  5. package/src/Cubit.ts +1 -16
  6. package/src/__tests__/Blac.config.test.ts +10 -17
  7. package/src/__tests__/Bloc.event.test.ts +27 -52
  8. package/src/__tests__/BlocBase.subscription.test.ts +349 -0
  9. package/src/__tests__/Cubit.test.ts +39 -50
  10. package/src/__tests__/memory-leaks.test.ts +383 -0
  11. package/src/__tests__/plugins.test.ts +107 -97
  12. package/src/adapter/BlacAdapter.ts +347 -207
  13. package/src/adapter/ProxyFactory.ts +196 -255
  14. package/src/adapter/__tests__/BlacAdapter.test.ts +71 -77
  15. package/src/adapter/__tests__/ProxyFactory.test.ts +1 -1
  16. package/src/adapter/index.ts +0 -1
  17. package/src/errors/BlacError.ts +69 -0
  18. package/src/errors/ErrorManager.ts +101 -0
  19. package/src/errors/handleError.ts +114 -0
  20. package/src/errors/index.ts +13 -0
  21. package/src/events.ts +7 -1
  22. package/src/index.ts +7 -1
  23. package/src/lifecycle/BlocLifecycle.ts +156 -0
  24. package/src/lifecycle/index.ts +1 -0
  25. package/src/plugins/BlocPluginRegistry.ts +71 -51
  26. package/src/plugins/SystemPluginRegistry.ts +63 -13
  27. package/src/plugins/__tests__/BlocPluginRegistry.test.ts +450 -0
  28. package/src/plugins/__tests__/SystemPluginRegistry.test.ts +404 -0
  29. package/src/plugins/types.ts +32 -3
  30. package/src/subscription/README.md +132 -0
  31. package/src/subscription/SubscriptionManager.ts +395 -0
  32. package/src/subscription/__tests__/SubscriptionManager.test.ts +355 -0
  33. package/src/subscription/index.ts +2 -0
  34. package/src/subscription/types.ts +90 -0
  35. package/src/testing.ts +31 -37
  36. package/src/types.ts +4 -21
  37. package/src/utils/BatchingManager.ts +59 -0
  38. package/src/utils/RerenderLogger.ts +265 -0
  39. package/src/utils/__tests__/generateInstanceId.test.ts +104 -0
  40. package/src/utils/__tests__/shallowEqual.test.ts +171 -0
  41. package/src/utils/__tests__/uuid.test.ts +144 -0
  42. package/src/utils/generateInstanceId.ts +54 -0
  43. package/src/utils/shallowEqual.ts +1 -1
  44. package/README.md +0 -329
  45. package/dist/index.cjs.js +0 -11
  46. package/dist/index.cjs.js.map +0 -1
  47. package/dist/index.d.ts +0 -1078
  48. package/dist/index.es.js +0 -1992
  49. package/dist/index.es.js.map +0 -1
  50. package/src/BlacObserver.ts +0 -189
  51. package/src/__tests__/BlocBase.lifecycle.test.ts +0 -441
  52. package/src/__tests__/props.test.ts +0 -256
  53. package/src/adapter/ConsumerTracker.ts +0 -241
  54. package/src/adapter/__tests__/ConsumerTracker.test.ts +0 -562
@@ -1,441 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { BlocBase, BlocLifecycleState } from '../BlocBase';
3
- import { Blac } from '../Blac';
4
-
5
- // Test implementation of BlocBase
6
- class TestBloc extends BlocBase<number> {
7
- constructor(initialState: number = 0) {
8
- super(initialState);
9
- }
10
-
11
- increment() {
12
- this._pushState(this.state + 1, this.state);
13
- }
14
- }
15
-
16
- // Test bloc with static properties
17
- class KeepAliveBloc extends BlocBase<string> {
18
- static keepAlive = true;
19
-
20
- constructor() {
21
- super('initial');
22
- }
23
- }
24
-
25
- class IsolatedBloc extends BlocBase<string> {
26
- static isolated = true;
27
-
28
- constructor() {
29
- super('isolated');
30
- }
31
- }
32
-
33
- describe('BlocBase Lifecycle Management', () => {
34
- let blac: Blac;
35
-
36
- beforeEach(() => {
37
- blac = new Blac({ __unsafe_ignore_singleton: true });
38
- vi.useFakeTimers();
39
- });
40
-
41
- afterEach(() => {
42
- vi.useRealTimers();
43
- vi.clearAllMocks();
44
- });
45
-
46
- describe('Atomic State Transitions', () => {
47
- it('should transition through lifecycle states atomically', () => {
48
- const bloc = new TestBloc();
49
-
50
- // Initial state should be ACTIVE
51
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.ACTIVE);
52
-
53
- // Transition to DISPOSAL_REQUESTED
54
- const result1 = (bloc as any)._atomicStateTransition(
55
- BlocLifecycleState.ACTIVE,
56
- BlocLifecycleState.DISPOSAL_REQUESTED,
57
- );
58
- expect(result1.success).toBe(true);
59
- expect((bloc as any)._disposalState).toBe(
60
- BlocLifecycleState.DISPOSAL_REQUESTED,
61
- );
62
-
63
- // Transition to DISPOSING
64
- const result2 = (bloc as any)._atomicStateTransition(
65
- BlocLifecycleState.DISPOSAL_REQUESTED,
66
- BlocLifecycleState.DISPOSING,
67
- );
68
- expect(result2.success).toBe(true);
69
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSING);
70
-
71
- // Transition to DISPOSED
72
- const result3 = (bloc as any)._atomicStateTransition(
73
- BlocLifecycleState.DISPOSING,
74
- BlocLifecycleState.DISPOSED,
75
- );
76
- expect(result3.success).toBe(true);
77
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
78
- });
79
-
80
- it('should reject invalid state transitions', () => {
81
- const bloc = new TestBloc();
82
-
83
- // Try to transition from ACTIVE to DISPOSED directly (should work in _dispose but not here)
84
- const result = (bloc as any)._atomicStateTransition(
85
- BlocLifecycleState.DISPOSAL_REQUESTED,
86
- BlocLifecycleState.DISPOSED,
87
- );
88
- expect(result.success).toBe(false);
89
- expect(result.currentState).toBe(BlocLifecycleState.ACTIVE); // Still in ACTIVE because expectedState didn't match
90
- });
91
-
92
- it('should handle concurrent transition attempts', () => {
93
- const bloc = new TestBloc();
94
-
95
- // First transition succeeds
96
- const result1 = (bloc as any)._atomicStateTransition(
97
- BlocLifecycleState.ACTIVE,
98
- BlocLifecycleState.DISPOSAL_REQUESTED,
99
- );
100
- expect(result1.success).toBe(true);
101
-
102
- // Second attempt with same expected state fails
103
- const result2 = (bloc as any)._atomicStateTransition(
104
- BlocLifecycleState.ACTIVE,
105
- BlocLifecycleState.DISPOSAL_REQUESTED,
106
- );
107
- expect(result2.success).toBe(false);
108
- expect(result2.currentState).toBe(BlocLifecycleState.DISPOSAL_REQUESTED);
109
- });
110
- });
111
-
112
- describe('Consumer Management', () => {
113
- it('should register and unregister consumers', () => {
114
- const bloc = new TestBloc();
115
- const consumerId = 'test-consumer-1';
116
- const consumerRef = {};
117
-
118
- // Add consumer
119
- const added = bloc._addConsumer(consumerId, consumerRef);
120
- expect(added).toBe(true);
121
- expect(bloc._consumers.has(consumerId)).toBe(true);
122
- expect(bloc._consumers.size).toBe(1);
123
-
124
- // Remove consumer
125
- bloc._removeConsumer(consumerId);
126
- expect(bloc._consumers.has(consumerId)).toBe(false);
127
- expect(bloc._consumers.size).toBe(0);
128
- });
129
-
130
- it('should prevent duplicate consumer registration', () => {
131
- const bloc = new TestBloc();
132
- const consumerId = 'test-consumer-1';
133
-
134
- bloc._addConsumer(consumerId);
135
- expect(bloc._consumers.size).toBe(1);
136
-
137
- // Try to add same consumer again
138
- bloc._addConsumer(consumerId);
139
- expect(bloc._consumers.size).toBe(1); // Should still be 1
140
- });
141
-
142
- it('should schedule disposal when last consumer is removed', async () => {
143
- const bloc = new TestBloc();
144
- const consumerId = 'test-consumer-1';
145
-
146
- // Register bloc with Blac instance
147
- blac.registerBlocInstance(bloc as BlocBase<unknown>);
148
-
149
- // Add and remove consumer
150
- bloc._addConsumer(consumerId);
151
- bloc._removeConsumer(consumerId);
152
-
153
- // Should transition to DISPOSAL_REQUESTED immediately
154
- expect((bloc as any)._disposalState).toBe(
155
- BlocLifecycleState.DISPOSAL_REQUESTED,
156
- );
157
-
158
- // After microtask, should be disposed
159
- await vi.runAllTimersAsync();
160
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
161
- });
162
-
163
- it('should cancel disposal if consumer is re-added during grace period', () => {
164
- const bloc = new TestBloc();
165
- const consumerId1 = 'test-consumer-1';
166
- const consumerId2 = 'test-consumer-2';
167
-
168
- // Register bloc
169
- blac.registerBlocInstance(bloc as BlocBase<unknown>);
170
-
171
- // Add and remove consumer to trigger disposal
172
- bloc._addConsumer(consumerId1);
173
- bloc._removeConsumer(consumerId1);
174
-
175
- // State should transition to DISPOSAL_REQUESTED immediately
176
- expect((bloc as any)._disposalState).toBe(
177
- BlocLifecycleState.DISPOSAL_REQUESTED,
178
- );
179
-
180
- // Add new consumer before microtask runs - should fail
181
- const added = bloc._addConsumer(consumerId2);
182
- expect(added).toBe(false); // Should fail because bloc is in disposal process
183
- });
184
-
185
- it('should clean up dead WeakRef consumers', () => {
186
- const bloc = new TestBloc();
187
- const consumerId1 = 'consumer-1';
188
- const consumerId2 = 'consumer-2';
189
- let consumerRef1: any = { name: 'consumer1' };
190
- const consumerRef2 = { name: 'consumer2' };
191
-
192
- // Add consumers with refs first
193
- bloc._addConsumer(consumerId1, consumerRef1);
194
- bloc._addConsumer(consumerId2, consumerRef2);
195
- expect(bloc._consumers.size).toBe(2);
196
-
197
- // Now mock the WeakRef's deref method to simulate garbage collection
198
- const consumerRefsMap = (bloc as any)._consumerRefs as Map<
199
- string,
200
- WeakRef<object>
201
- >;
202
- const weakRef1 = consumerRefsMap.get(consumerId1);
203
- const weakRef2 = consumerRefsMap.get(consumerId2);
204
-
205
- if (weakRef1) {
206
- // Mock the deref method to return undefined (simulating GC)
207
- vi.spyOn(weakRef1, 'deref').mockReturnValue(undefined);
208
- }
209
-
210
- // Validate consumers
211
- bloc._validateConsumers();
212
-
213
- // First consumer should be removed
214
- expect(bloc._consumers.size).toBe(1);
215
- expect(bloc._consumers.has(consumerId1)).toBe(false);
216
- expect(bloc._consumers.has(consumerId2)).toBe(true);
217
- });
218
-
219
- it('should reject consumer additions when bloc is disposed', () => {
220
- const bloc = new TestBloc();
221
-
222
- // Force dispose
223
- bloc._dispose();
224
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
225
-
226
- // Try to add consumer
227
- const added = bloc._addConsumer('new-consumer');
228
- expect(added).toBe(false);
229
- expect(bloc._consumers.size).toBe(0);
230
- });
231
- });
232
-
233
- describe('Disposal Behavior', () => {
234
- it('should properly dispose bloc and clean up resources', () => {
235
- const bloc = new TestBloc();
236
- const onDisposeSpy = vi.fn();
237
- bloc.onDispose = onDisposeSpy;
238
-
239
- // Add some consumers and observers
240
- bloc._addConsumer('consumer-1');
241
- const unsubscribe = bloc._observer.subscribe({
242
- id: 'observer-1',
243
- fn: vi.fn(),
244
- });
245
-
246
- // Dispose
247
- const disposed = bloc._dispose();
248
- expect(disposed).toBe(true);
249
- expect(onDisposeSpy).toHaveBeenCalled();
250
- expect(bloc._consumers.size).toBe(0);
251
- expect(bloc._observer.size).toBe(0);
252
- expect(bloc.isDisposed).toBe(true);
253
- });
254
-
255
- it('should handle disposal failures gracefully', () => {
256
- const bloc = new TestBloc();
257
- bloc.onDispose = () => {
258
- throw new Error('Disposal error');
259
- };
260
-
261
- // Disposal should reset state on error
262
- expect(() => bloc._dispose()).toThrow('Disposal error');
263
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.ACTIVE);
264
- });
265
-
266
- it('should schedule disposal with microtask deferral', () => {
267
- const bloc = new TestBloc();
268
- blac.registerBlocInstance(bloc as BlocBase<unknown>);
269
-
270
- // Remove last consumer
271
- bloc._addConsumer('consumer-1');
272
- bloc._removeConsumer('consumer-1');
273
-
274
- // Disposal should be scheduled immediately
275
- expect((bloc as any)._disposalState).toBe(
276
- BlocLifecycleState.DISPOSAL_REQUESTED,
277
- );
278
- });
279
-
280
- it('should be idempotent - multiple dispose calls should be safe', () => {
281
- const bloc = new TestBloc();
282
-
283
- const result1 = bloc._dispose();
284
- expect(result1).toBe(true);
285
-
286
- const result2 = bloc._dispose();
287
- expect(result2).toBe(false); // Already disposed
288
- });
289
- });
290
-
291
- describe('keepAlive and isolated flags', () => {
292
- it('should respect keepAlive flag and not dispose when consumers are removed', async () => {
293
- const bloc = new KeepAliveBloc();
294
- blac.registerBlocInstance(bloc as BlocBase<unknown>);
295
-
296
- expect(bloc.isKeepAlive).toBe(true);
297
-
298
- // Add and remove consumer
299
- bloc._addConsumer('consumer-1');
300
- bloc._removeConsumer('consumer-1');
301
-
302
- // Should not schedule disposal
303
- await vi.runAllTimersAsync();
304
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.ACTIVE);
305
- });
306
-
307
- it('should properly inherit isolated flag from static property', () => {
308
- const bloc = new IsolatedBloc();
309
- expect(bloc.isIsolated).toBe(true);
310
- });
311
-
312
- it('should handle missing static properties gracefully', () => {
313
- class BlocWithoutStatics extends BlocBase<number> {
314
- constructor() {
315
- super(0);
316
- }
317
- }
318
-
319
- const bloc = new BlocWithoutStatics();
320
- expect(bloc.isKeepAlive).toBe(false);
321
- expect(bloc.isIsolated).toBe(false);
322
- });
323
- });
324
-
325
- describe('State Access During Lifecycle', () => {
326
- it('should allow state access during all non-disposed states', () => {
327
- const bloc = new TestBloc(42);
328
-
329
- // ACTIVE state
330
- expect(bloc.state).toBe(42);
331
-
332
- // DISPOSAL_REQUESTED state
333
- (bloc as any)._atomicStateTransition(
334
- BlocLifecycleState.ACTIVE,
335
- BlocLifecycleState.DISPOSAL_REQUESTED,
336
- );
337
- expect(bloc.state).toBe(42);
338
-
339
- // DISPOSING state
340
- (bloc as any)._atomicStateTransition(
341
- BlocLifecycleState.DISPOSAL_REQUESTED,
342
- BlocLifecycleState.DISPOSING,
343
- );
344
- expect(bloc.state).toBe(42);
345
-
346
- // DISPOSED state - should return last known state
347
- (bloc as any)._atomicStateTransition(
348
- BlocLifecycleState.DISPOSING,
349
- BlocLifecycleState.DISPOSED,
350
- );
351
- expect(bloc.state).toBe(42);
352
- });
353
-
354
- it('should correctly report isDisposed status', () => {
355
- const bloc = new TestBloc();
356
-
357
- expect(bloc.isDisposed).toBe(false);
358
-
359
- bloc._dispose();
360
- expect(bloc.isDisposed).toBe(true);
361
- });
362
- });
363
-
364
- describe('Edge Cases', () => {
365
- it('should handle rapid consumer additions and removals', async () => {
366
- const bloc = new TestBloc();
367
- blac.registerBlocInstance(bloc as BlocBase<unknown>);
368
-
369
- // Rapidly add and remove consumers
370
- for (let i = 0; i < 10; i++) {
371
- bloc._addConsumer(`consumer-${i}`);
372
- bloc._removeConsumer(`consumer-${i}`);
373
- }
374
-
375
- // Should be in DISPOSAL_REQUESTED state
376
- expect((bloc as any)._disposalState).toBe(
377
- BlocLifecycleState.DISPOSAL_REQUESTED,
378
- );
379
-
380
- // After microtask, should be disposed
381
- await vi.runAllTimersAsync();
382
- expect((bloc as any)._disposalState).toBe(BlocLifecycleState.DISPOSED);
383
- });
384
-
385
- it('should handle disposal during active state mutations', () => {
386
- const bloc = new TestBloc();
387
- const observer = vi.fn();
388
-
389
- bloc._observer.subscribe({
390
- id: 'observer-1',
391
- fn: observer,
392
- });
393
-
394
- // Start a state mutation
395
- bloc.increment();
396
- expect(observer).toHaveBeenCalledWith(1, 0, undefined);
397
-
398
- // Dispose during active usage
399
- bloc._dispose();
400
-
401
- // Further mutations should not notify (bloc is disposed)
402
- observer.mockClear();
403
- bloc.increment();
404
- expect(observer).not.toHaveBeenCalled();
405
- });
406
-
407
- it('should prevent memory leaks via WeakRef cleanup', () => {
408
- const bloc = new TestBloc();
409
- const consumerCount = 100;
410
- const refs: any[] = [];
411
-
412
- // Add many consumers
413
- for (let i = 0; i < consumerCount; i++) {
414
- const ref = { id: i };
415
- refs.push(ref);
416
- bloc._addConsumer(`consumer-${i}`, ref);
417
- }
418
-
419
- expect(bloc._consumers.size).toBe(consumerCount);
420
-
421
- // Mock half of the WeakRefs to return undefined (simulating GC)
422
- const consumerRefsMap = (bloc as any)._consumerRefs as Map<
423
- string,
424
- WeakRef<object>
425
- >;
426
- let mockCount = 0;
427
-
428
- for (const [consumerId, weakRef] of consumerRefsMap) {
429
- if (mockCount < consumerCount / 2) {
430
- vi.spyOn(weakRef, 'deref').mockReturnValue(undefined);
431
- mockCount++;
432
- }
433
- }
434
-
435
- // Validate should clean up dead refs
436
- bloc._validateConsumers();
437
-
438
- expect(bloc._consumers.size).toBe(consumerCount / 2);
439
- });
440
- });
441
- });
@@ -1,256 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { Bloc } from '../Bloc';
3
- import { Cubit } from '../Cubit';
4
- import { PropsUpdated } from '../events';
5
- import { BlacAdapter } from '../adapter/BlacAdapter';
6
- import { Blac } from '../Blac';
7
-
8
- // Test types
9
- interface TestProps {
10
- query: string;
11
- filters?: string[];
12
- }
13
-
14
- interface TestState {
15
- data: string;
16
- loading: boolean;
17
- }
18
-
19
- // Test Cubit with props
20
- class TestCubit extends Cubit<TestState, TestProps> {
21
- onPropsChangedMock = vi.fn();
22
-
23
- constructor() {
24
- super({ data: '', loading: false });
25
- }
26
-
27
- protected onPropsChanged(oldProps: TestProps | undefined, newProps: TestProps): void {
28
- this.onPropsChangedMock(oldProps, newProps);
29
- if (oldProps?.query !== newProps.query) {
30
- this.emit({ ...this.state, data: `Query: ${newProps.query}` });
31
- }
32
- }
33
-
34
- loadData = () => {
35
- const query = this.props?.query ?? 'default';
36
- this.emit({ data: `Loaded: ${query}`, loading: false });
37
- };
38
- }
39
-
40
- // Test Bloc with props
41
- class TestBloc extends Bloc<TestState, PropsUpdated<TestProps>> {
42
- constructor() {
43
- super({ data: '', loading: false });
44
-
45
- this.on(PropsUpdated<TestProps>, (event, emit) => {
46
- emit({ ...this.state, data: `Props: ${event.props.query}` });
47
- });
48
- }
49
- }
50
-
51
- describe('Props functionality', () => {
52
- beforeEach(() => {
53
- // Clear Blac instance between tests
54
- Blac.resetInstance();
55
- });
56
-
57
- describe('PropsUpdated event', () => {
58
- it('should create PropsUpdated event with correct props', () => {
59
- const props = { query: 'test', filters: ['a', 'b'] };
60
- const event = new PropsUpdated(props);
61
-
62
- expect(event.props).toEqual(props);
63
- expect(event.props).toBe(props); // Should be the same reference
64
- });
65
- });
66
-
67
- describe('Cubit props support', () => {
68
- it('should support props getter', () => {
69
- const cubit = new TestCubit();
70
- expect(cubit.props).toBeNull();
71
- });
72
-
73
- it('should update props via _updateProps', () => {
74
- const cubit = new TestCubit();
75
- const props = { query: 'test' };
76
-
77
- (cubit as any)._updateProps(props);
78
-
79
- expect(cubit.props).toEqual(props);
80
- expect(cubit.onPropsChangedMock).toHaveBeenCalledWith(null, props);
81
- });
82
-
83
- it('should emit state when props change', () => {
84
- const cubit = new TestCubit();
85
- const props1 = { query: 'test1' };
86
- const props2 = { query: 'test2' };
87
-
88
- (cubit as any)._updateProps(props1);
89
- expect(cubit.state.data).toBe('Query: test1');
90
-
91
- (cubit as any)._updateProps(props2);
92
- expect(cubit.state.data).toBe('Query: test2');
93
- });
94
-
95
- it('should access props in methods', () => {
96
- const cubit = new TestCubit();
97
-
98
- cubit.loadData();
99
- expect(cubit.state.data).toBe('Loaded: default');
100
-
101
- (cubit as any)._updateProps({ query: 'custom' });
102
- cubit.loadData();
103
- expect(cubit.state.data).toBe('Loaded: custom');
104
- });
105
- });
106
-
107
- describe('Bloc props support', () => {
108
- it('should handle PropsUpdated events', async () => {
109
- const bloc = new TestBloc();
110
- const props = { query: 'search' };
111
-
112
- await bloc.add(new PropsUpdated(props));
113
-
114
- expect(bloc.state.data).toBe('Props: search');
115
- });
116
-
117
- it('should queue PropsUpdated events like any other event', async () => {
118
- const bloc = new TestBloc();
119
-
120
- await bloc.add(new PropsUpdated({ query: 'first' }));
121
- await bloc.add(new PropsUpdated({ query: 'second' }));
122
-
123
- expect(bloc.state.data).toBe('Props: second');
124
- });
125
- });
126
-
127
- describe('BlacAdapter props ownership', () => {
128
- it('should allow first adapter to own props', () => {
129
- const Constructor = TestCubit as any;
130
- const adapter1 = new BlacAdapter(
131
- { componentRef: { current: {} }, blocConstructor: Constructor },
132
- { props: { query: 'test1' } }
133
- );
134
-
135
- expect(() => {
136
- adapter1.updateProps({ query: 'test2' });
137
- }).not.toThrow();
138
- });
139
-
140
- it('should prevent non-owner adapters from updating props', () => {
141
- const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
142
- const Constructor = TestCubit as any;
143
-
144
- // First adapter becomes owner
145
- const adapter1 = new BlacAdapter(
146
- { componentRef: { current: {} }, blocConstructor: Constructor },
147
- { props: { query: 'test1' } }
148
- );
149
-
150
- // Second adapter tries to update props
151
- const adapter2 = new BlacAdapter(
152
- { componentRef: { current: {} }, blocConstructor: Constructor },
153
- {}
154
- );
155
-
156
- adapter2.updateProps({ query: 'hijack' });
157
-
158
- expect(warnSpy).toHaveBeenCalledWith(
159
- expect.stringContaining('non-owner adapter')
160
- );
161
-
162
- warnSpy.mockRestore();
163
- });
164
-
165
- it('should clear ownership on adapter unmount', () => {
166
- const Constructor = TestCubit as any;
167
- const adapter1 = new BlacAdapter(
168
- { componentRef: { current: {} }, blocConstructor: Constructor },
169
- { props: { query: 'test1' } }
170
- );
171
-
172
- adapter1.mount();
173
- adapter1.unmount();
174
-
175
- // New adapter should be able to take ownership
176
- const adapter2 = new BlacAdapter(
177
- { componentRef: { current: {} }, blocConstructor: Constructor },
178
- { props: { query: 'test2' } }
179
- );
180
-
181
- expect(() => {
182
- adapter2.updateProps({ query: 'test3' });
183
- }).not.toThrow();
184
- });
185
-
186
- it('should not update props if they are shallowly equal', () => {
187
- const cubit = new TestCubit();
188
- const Constructor = TestCubit as any;
189
- const adapter = new BlacAdapter(
190
- { componentRef: { current: {} }, blocConstructor: Constructor },
191
- {}
192
- );
193
-
194
- const props = { query: 'test' };
195
- adapter.updateProps(props);
196
-
197
- // Reset mock
198
- cubit.onPropsChangedMock.mockClear();
199
-
200
- // Update with same props
201
- adapter.updateProps({ query: 'test' });
202
-
203
- // onPropsChanged should not have been called
204
- expect(cubit.onPropsChangedMock).not.toHaveBeenCalled();
205
- });
206
-
207
- it('should ignore props updates during disposal', () => {
208
- const Constructor = TestCubit as any;
209
- const adapter = new BlacAdapter(
210
- { componentRef: { current: {} }, blocConstructor: Constructor },
211
- { props: { query: 'test1' } }
212
- );
213
-
214
- // Force disposal state
215
- (adapter.blocInstance as any)._disposalState = 'disposing';
216
-
217
- expect(() => {
218
- adapter.updateProps({ query: 'test2' });
219
- }).not.toThrow();
220
-
221
- // Props should not have been updated
222
- expect((adapter.blocInstance as TestCubit).props?.query).toBe('test1');
223
- });
224
- });
225
-
226
- describe('Props integration', () => {
227
- it('should dispatch PropsUpdated for Bloc instances', async () => {
228
- const Constructor = TestBloc as any;
229
- const adapter = new BlacAdapter(
230
- { componentRef: { current: {} }, blocConstructor: Constructor },
231
- {}
232
- );
233
-
234
- const bloc = adapter.blocInstance as TestBloc;
235
- adapter.updateProps({ query: 'adapter-test' });
236
-
237
- // Wait for event processing
238
- await new Promise(resolve => setTimeout(resolve, 50));
239
-
240
- expect(bloc.state.data).toBe('Props: adapter-test');
241
- });
242
-
243
- it('should call _updateProps for Cubit instances', () => {
244
- const Constructor = TestCubit as any;
245
- const adapter = new BlacAdapter(
246
- { componentRef: { current: {} }, blocConstructor: Constructor },
247
- {}
248
- );
249
-
250
- adapter.updateProps({ query: 'adapter-test' });
251
-
252
- expect((adapter.blocInstance as TestCubit).props?.query).toBe('adapter-test');
253
- expect((adapter.blocInstance as TestCubit).state.data).toBe('Query: adapter-test');
254
- });
255
- });
256
- });