@blac/react 2.0.0-rc-16 → 2.0.0-rc.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.
@@ -1,200 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react';
2
- import { describe, expect, it, vi } from 'vitest';
3
- import { BlocConstructorParams, Cubit } from '@blac/core';
4
- import useBloc from '../useBloc';
5
-
6
- interface CounterProps {
7
- initialValue: number;
8
- step?: number;
9
- }
10
-
11
- class CounterCubit extends Cubit<number> {
12
- props: BlocConstructorParams<typeof CounterCubit>;
13
-
14
- constructor(props?: CounterProps) {
15
- super(props?.initialValue ?? 0);
16
- this.props = props;
17
- }
18
-
19
- increment = () => {
20
- const step = this.props?.step ?? 1;
21
- this.emit(this.state + step);
22
- };
23
- }
24
-
25
- describe('useBloc instance changes', () => {
26
- describe('when instanceId changes', () => {
27
- it('should use a different bloc instance', async () => {
28
- const { result, rerender } = renderHook(
29
- ({ instanceId }) =>
30
- useBloc(CounterCubit, {
31
- instanceId,
32
- staticProps: { initialValue: 0 },
33
- }),
34
- { initialProps: { instanceId: 'counter-1' } },
35
- );
36
-
37
- // Wait for initial render
38
- await act(async () => {
39
- await new Promise((resolve) => setTimeout(resolve, 0));
40
- });
41
-
42
- const firstInstance = result.current[1];
43
- expect(firstInstance._id).toBe('counter-1');
44
- expect(firstInstance.props).toEqual({ initialValue: 0 });
45
-
46
- // Change instanceId
47
- rerender({ instanceId: 'counter-2' });
48
-
49
- const secondInstance = result.current[1];
50
- expect(secondInstance._id).toBe('counter-2');
51
- expect(secondInstance).not.toBe(firstInstance);
52
- expect(result.current[0]).toBe(0); // New instance starts at 0
53
- });
54
- });
55
-
56
- describe('when staticProps change', () => {
57
- it('should use a different bloc instance when staticProps change', async () => {
58
- const { result, rerender } = renderHook(
59
- ({ initialValue }) =>
60
- useBloc(CounterCubit, {
61
- staticProps: { initialValue },
62
- }),
63
- { initialProps: { initialValue: 10 } },
64
- );
65
-
66
- // Wait for initial render
67
- await act(async () => {
68
- await new Promise((resolve) => setTimeout(resolve, 0));
69
- });
70
-
71
- const firstInstance = result.current[1];
72
- expect(result.current[0]).toBe(10);
73
- expect(firstInstance._id).toBe('initialValue:10');
74
-
75
- // Change staticProps
76
- rerender({ initialValue: 20 });
77
-
78
- const secondInstance = result.current[1];
79
- expect(secondInstance).not.toBe(firstInstance);
80
- expect(result.current[0]).toBe(20); // New instance with new initial value
81
- expect(secondInstance._id).toBe('initialValue:20');
82
- });
83
-
84
- it('should use same instance when non-primitive staticProps values change', () => {
85
- const { result, rerender } = renderHook(
86
- ({ config }) =>
87
- useBloc(CounterCubit, {
88
- staticProps: {
89
- initialValue: 5,
90
- config, // This will be ignored for instance ID
91
- } as any,
92
- }),
93
- { initialProps: { config: { debug: true } } },
94
- );
95
-
96
- const firstInstance = result.current[1];
97
- expect(firstInstance._id).toBe('initialValue:5');
98
-
99
- // Change non-primitive prop
100
- rerender({ config: { debug: false } });
101
-
102
- const secondInstance = result.current[1];
103
- expect(secondInstance).toBe(firstInstance); // Same instance
104
- expect(secondInstance._id).toBe('initialValue:5');
105
- });
106
-
107
- it('should change instance when primitive staticProps change', () => {
108
- const { result, rerender } = renderHook(
109
- ({ step }) =>
110
- useBloc(CounterCubit, {
111
- staticProps: { initialValue: 0, step },
112
- }),
113
- { initialProps: { step: 1 } },
114
- );
115
-
116
- const firstInstance = result.current[1];
117
- expect(firstInstance._id).toBe('initialValue:0|step:1');
118
-
119
- // Change primitive prop
120
- rerender({ step: 2 });
121
-
122
- const secondInstance = result.current[1];
123
- expect(secondInstance).not.toBe(firstInstance); // Different instance
124
- expect(secondInstance._id).toBe('initialValue:0|step:2');
125
- });
126
- });
127
-
128
- describe('when using both instanceId and staticProps', () => {
129
- it('should use explicit instanceId over generated one', () => {
130
- const { result } = renderHook(() =>
131
- useBloc(CounterCubit, {
132
- instanceId: 'my-counter',
133
- staticProps: { initialValue: 100 },
134
- }),
135
- );
136
-
137
- expect(result.current[1]._id).toBe('my-counter');
138
- expect(result.current[0]).toBe(100);
139
- });
140
-
141
- it('should change instance when instanceId changes even if staticProps are same', () => {
142
- const { result, rerender } = renderHook(
143
- ({ instanceId }) =>
144
- useBloc(CounterCubit, {
145
- instanceId,
146
- staticProps: { initialValue: 50 },
147
- }),
148
- { initialProps: { instanceId: 'counter-a' } },
149
- );
150
-
151
- const firstInstance = result.current[1];
152
-
153
- // Change instanceId
154
- rerender({ instanceId: 'counter-b' });
155
-
156
- const secondInstance = result.current[1];
157
- expect(secondInstance).not.toBe(firstInstance);
158
- expect(secondInstance._id).toBe('counter-b');
159
- expect(result.current[0]).toBe(50); // Same initial value, different instance
160
- });
161
- });
162
-
163
- describe('lifecycle hooks', () => {
164
- it('should call onMount/onUnmount when instance changes', () => {
165
- const onMount1 = vi.fn();
166
- const onUnmount1 = vi.fn();
167
- const onMount2 = vi.fn();
168
- const onUnmount2 = vi.fn();
169
-
170
- const { rerender, unmount } = renderHook(
171
- ({ instanceId, onMount, onUnmount }) =>
172
- useBloc(CounterCubit, { instanceId, onMount, onUnmount }),
173
- {
174
- initialProps: {
175
- instanceId: 'counter-1',
176
- onMount: onMount1,
177
- onUnmount: onUnmount1,
178
- },
179
- },
180
- );
181
-
182
- expect(onMount1).toHaveBeenCalledTimes(1);
183
- expect(onUnmount1).not.toHaveBeenCalled();
184
-
185
- // Change instance
186
- rerender({
187
- instanceId: 'counter-2',
188
- onMount: onMount2,
189
- onUnmount: onUnmount2,
190
- });
191
-
192
- expect(onUnmount1).toHaveBeenCalledTimes(1);
193
- expect(onMount2).toHaveBeenCalledTimes(1);
194
- expect(onUnmount2).not.toHaveBeenCalled();
195
-
196
- unmount();
197
- expect(onUnmount2).toHaveBeenCalledTimes(1);
198
- });
199
- });
200
- });
@@ -1,435 +0,0 @@
1
- import {
2
- Blac,
3
- Bloc,
4
- BlocConstructorParams,
5
- Cubit,
6
- PropsUpdated,
7
- } from '@blac/core';
8
- import { act, renderHook, waitFor } from '@testing-library/react';
9
- import { describe, expect, it, vi, beforeEach } from 'vitest';
10
- import useBloc from '../useBloc';
11
-
12
- // Test components
13
- interface SearchProps {
14
- query: string;
15
- limit?: number;
16
- }
17
-
18
- interface SearchState {
19
- results: string[];
20
- loading: boolean;
21
- }
22
-
23
- class SearchBloc extends Bloc<SearchState, PropsUpdated<SearchProps>> {
24
- props: BlocConstructorParams<typeof SearchBloc>;
25
-
26
- constructor(props?: SearchProps) {
27
- // Initialize with props
28
- super({
29
- results: props ? [`Search: ${props.query}`] : [],
30
- loading: false,
31
- });
32
- this.props = props;
33
-
34
- this.on(PropsUpdated<SearchProps>, (event, emit) => {
35
- emit({ results: [`Search: ${event.props.query}`], loading: false });
36
- });
37
- }
38
- }
39
-
40
- interface CounterProps {
41
- step: number;
42
- }
43
-
44
- interface CounterState {
45
- count: number;
46
- stepSize: number;
47
- }
48
-
49
- class CounterCubit extends Cubit<CounterState> {
50
- constructor(props?: CounterProps) {
51
- super({ count: 0, stepSize: props?.step ?? 1 });
52
- this.props = props || null;
53
- }
54
-
55
- props: CounterProps | null = null;
56
-
57
- protected onPropsChanged(
58
- oldProps: CounterProps | undefined,
59
- newProps: CounterProps,
60
- ): void {
61
- if (oldProps?.step !== newProps.step) {
62
- this.emit({ ...this.state, stepSize: newProps.step });
63
- }
64
- }
65
-
66
- increment = () => {
67
- const step = this.props?.step ?? 1;
68
- this.emit({
69
- ...this.state,
70
- count: this.state.count + step,
71
- stepSize: step,
72
- });
73
- };
74
- }
75
-
76
- describe('useBloc props integration', () => {
77
- beforeEach(() => {
78
- Blac.resetInstance();
79
- });
80
-
81
- describe('Basic props functionality', () => {
82
- it('should pass props to Bloc via adapter', async () => {
83
- const { result } = renderHook(
84
- ({ query }) => useBloc(SearchBloc, { staticProps: { query } }),
85
- { initialProps: { query: 'initial' } },
86
- );
87
-
88
- await act(async () => {
89
- await new Promise((resolve) => setTimeout(resolve, 0));
90
- });
91
-
92
- const [state] = result.current;
93
- expect(state.results).toEqual(['Search: initial']);
94
- });
95
-
96
- it('should create new instance when staticProps change', async () => {
97
- const { result, rerender } = renderHook(
98
- ({ query }) => useBloc(SearchBloc, { staticProps: { query } }),
99
- { initialProps: { query: 'initial' } },
100
- );
101
-
102
- await act(async () => {
103
- await new Promise((resolve) => setTimeout(resolve, 0));
104
- });
105
-
106
- const firstInstance = result.current[1];
107
- expect(result.current[0].results).toEqual(['Search: initial']);
108
-
109
- rerender({ query: 'updated' });
110
-
111
- await act(async () => {
112
- await new Promise((resolve) => setTimeout(resolve, 0));
113
- });
114
-
115
- const secondInstance = result.current[1];
116
- expect(secondInstance).not.toBe(firstInstance); // New instance created
117
- expect(result.current[0].results).toEqual(['Search: updated']);
118
- });
119
-
120
- it('should work with Cubit props', async () => {
121
- const { result } = renderHook(
122
- ({ step }) => {
123
- const [state, cubit] = useBloc(CounterCubit, {
124
- staticProps: { step },
125
- });
126
- // Access count to ensure it's tracked
127
- void state.count;
128
- return [state, cubit];
129
- },
130
- { initialProps: { step: 1 } },
131
- );
132
-
133
- // Wait for props to be set
134
- await act(async () => {
135
- await new Promise((resolve) => setTimeout(resolve, 0));
136
- });
137
-
138
- const [state, cubit] = result.current as [CounterState, CounterCubit];
139
- expect(state.stepSize).toBe(1);
140
-
141
- act(() => {
142
- cubit.increment();
143
- });
144
-
145
- // Wait for React to re-render with new state
146
- await waitFor(() => {
147
- expect((result.current[0] as CounterState).count).toBe(1);
148
- });
149
- });
150
-
151
- it('should create new Cubit instance when staticProps change', async () => {
152
- const { result, rerender } = renderHook(
153
- ({ step }) => {
154
- const [state, cubit] = useBloc(CounterCubit, {
155
- staticProps: { step },
156
- });
157
- // Access count to ensure it's tracked
158
- void state.count;
159
- return [state, cubit];
160
- },
161
- { initialProps: { step: 1 } },
162
- );
163
-
164
- // Wait for initial props to be set
165
- await act(async () => {
166
- await new Promise((resolve) => setTimeout(resolve, 0));
167
- });
168
-
169
- const firstInstance = result.current[1];
170
- expect((result.current[0] as CounterState).stepSize).toBe(1);
171
-
172
- rerender({ step: 5 });
173
-
174
- // Wait for new instance to be created
175
- await act(async () => {
176
- await new Promise((resolve) => setTimeout(resolve, 0));
177
- });
178
-
179
- const secondInstance = result.current[1];
180
- expect(secondInstance).not.toBe(firstInstance); // New instance
181
- expect((result.current[0] as CounterState).stepSize).toBe(5);
182
- expect((result.current[0] as CounterState).count).toBe(0); // New instance starts at 0
183
-
184
- act(() => {
185
- (result.current[1] as CounterCubit).increment();
186
- });
187
-
188
- // Wait for React to re-render with new state
189
- await waitFor(() => {
190
- expect((result.current[0] as CounterState).count).toBe(5);
191
- });
192
- });
193
- });
194
-
195
- describe('Instance sharing', () => {
196
- it('should share instance when using same instanceId', async () => {
197
- // First hook creates instance with explicit id
198
- const { result: result1 } = renderHook(() =>
199
- useBloc(SearchBloc, {
200
- staticProps: { query: 'first' },
201
- instanceId: 'shared-search',
202
- }),
203
- );
204
-
205
- // Second hook uses same instanceId - gets same instance
206
- const { result: result2 } = renderHook(() =>
207
- useBloc(SearchBloc, {
208
- staticProps: { query: 'second' }, // Different props, but same instanceId
209
- instanceId: 'shared-search',
210
- }),
211
- );
212
-
213
- await act(async () => {
214
- await new Promise((resolve) => setTimeout(resolve, 0));
215
- });
216
-
217
- // Both should share the same instance (check by ID)
218
- expect(result1.current[1]._id).toBe('shared-search');
219
- expect(result2.current[1]._id).toBe('shared-search');
220
-
221
- // Both see the state from the first instance (query: 'first')
222
- expect(result1.current[0].results).toEqual(['Search: first']);
223
- expect(result2.current[0].results).toEqual(['Search: first']);
224
- });
225
-
226
- it('should allow consumers without staticProps to share instance', async () => {
227
- // First consumer with props and explicit id
228
- const { result: firstResult } = renderHook(() =>
229
- useBloc(SearchBloc, {
230
- staticProps: { query: 'test' },
231
- instanceId: 'search-instance',
232
- }),
233
- );
234
-
235
- // Second consumer without props but same instanceId
236
- const { result: secondResult } = renderHook(() =>
237
- useBloc(SearchBloc, { instanceId: 'search-instance' }),
238
- );
239
-
240
- await act(async () => {
241
- await new Promise((resolve) => setTimeout(resolve, 0));
242
- });
243
-
244
- // Same instance (check by ID)
245
- expect(firstResult.current[1]._id).toBe('search-instance');
246
- expect(secondResult.current[1]._id).toBe('search-instance');
247
-
248
- // Both see the same state
249
- expect(secondResult.current[0].results).toEqual(['Search: test']);
250
- });
251
-
252
- it('should create independent instances with different staticProps', async () => {
253
- const { result: result1, unmount: unmount1 } = renderHook(() =>
254
- useBloc(SearchBloc, { staticProps: { query: 'first' } }),
255
- );
256
-
257
- await act(async () => {
258
- await new Promise((resolve) => setTimeout(resolve, 0));
259
- });
260
-
261
- expect(result1.current[0].results).toEqual(['Search: first']);
262
- const instance1Id = result1.current[1]._id;
263
-
264
- // Create second instance with different props
265
- const { result: result2 } = renderHook(() =>
266
- useBloc(SearchBloc, { staticProps: { query: 'second' } }),
267
- );
268
-
269
- await act(async () => {
270
- await new Promise((resolve) => setTimeout(resolve, 0));
271
- });
272
-
273
- expect(result2.current[0].results).toEqual(['Search: second']);
274
- const instance2Id = result2.current[1]._id;
275
-
276
- // Different instances with different auto-generated IDs
277
- expect(instance1Id).not.toBe(instance2Id);
278
- expect(result1.current[1]).not.toBe(result2.current[1]);
279
-
280
- // Unmount first - second is unaffected
281
- unmount1();
282
-
283
- expect(result2.current[0].results).toEqual(['Search: second']);
284
- });
285
- });
286
-
287
- describe('Props with other options', () => {
288
- it('should work with key option', async () => {
289
- const { result } = renderHook(() =>
290
- useBloc(SearchBloc, {
291
- instanceId: 'custom-search',
292
- staticProps: { query: 'test' },
293
- }),
294
- );
295
-
296
- await act(async () => {
297
- await new Promise((resolve) => setTimeout(resolve, 0));
298
- });
299
-
300
- expect(result.current[0].results).toEqual(['Search: test']);
301
- expect(result.current[1]._id).toBe('custom-search');
302
- });
303
-
304
- it('should work with lifecycle hooks', async () => {
305
- const onMount = vi.fn();
306
- const onUnmount = vi.fn();
307
-
308
- const { result, unmount } = renderHook(() =>
309
- useBloc(SearchBloc, {
310
- staticProps: { query: 'test' },
311
- onMount,
312
- onUnmount,
313
- }),
314
- );
315
-
316
- expect(onMount).toHaveBeenCalledWith(result.current[1]);
317
-
318
- unmount();
319
-
320
- expect(onUnmount).toHaveBeenCalledWith(result.current[1]);
321
- });
322
-
323
- it('should work with manual dependencies', () => {
324
- const { result, rerender } = renderHook(
325
- ({ step }) =>
326
- useBloc(CounterCubit, {
327
- staticProps: { step },
328
- dependencies: (cubit) => [cubit.state.count],
329
- }),
330
- { initialProps: { step: 1 } },
331
- );
332
-
333
- const [, cubit] = result.current as [CounterState, CounterCubit];
334
-
335
- // Increment should trigger re-render (count is a dependency)
336
- act(() => {
337
- cubit.increment();
338
- });
339
-
340
- expect((result.current[0] as CounterState).count).toBe(1);
341
-
342
- // Changing props creates new instance
343
- rerender({ step: 2 });
344
-
345
- // New instance has different state
346
- const [newState, newCubit] = result.current as [
347
- CounterState,
348
- CounterCubit,
349
- ];
350
- expect(newCubit).not.toBe(cubit); // New instance
351
- expect(newState.stepSize).toBe(2);
352
- expect(newState.count).toBe(0); // New instance starts at 0
353
- });
354
- });
355
-
356
- describe('Edge cases', () => {
357
- it('should handle undefined props', async () => {
358
- const { result } = renderHook(() => {
359
- const [state, cubit] = useBloc(CounterCubit, {
360
- staticProps: undefined,
361
- });
362
- // Access count to ensure it's tracked
363
- void state.count;
364
- return [state, cubit];
365
- });
366
-
367
- // Wait for any effects
368
- await act(async () => {
369
- await new Promise((resolve) => setTimeout(resolve, 0));
370
- });
371
-
372
- const [state, cubit] = result.current as [CounterState, CounterCubit];
373
- expect(state.stepSize).toBe(1);
374
-
375
- act(() => {
376
- cubit.increment();
377
- });
378
-
379
- // Wait for React to re-render with new state
380
- await waitFor(() => {
381
- expect((result.current[0] as CounterState).count).toBe(1); // Uses default step
382
- });
383
- });
384
-
385
- it('should not re-render for unchanged props', () => {
386
- let renderCount = 0;
387
-
388
- const { rerender } = renderHook(
389
- ({ query }) => {
390
- renderCount++;
391
- return useBloc(SearchBloc, { staticProps: { query } });
392
- },
393
- { initialProps: { query: 'test' } },
394
- );
395
-
396
- const initialRenderCount = renderCount;
397
-
398
- // Rerender with same props
399
- rerender({ query: 'test' });
400
-
401
- // Should not cause additional renders beyond React's normal behavior
402
- expect(renderCount).toBe(initialRenderCount + 1);
403
- });
404
-
405
- it('should handle rapid props updates', async () => {
406
- const { result, rerender } = renderHook(
407
- ({ query }) => useBloc(SearchBloc, { staticProps: { query } }),
408
- { initialProps: { query: 'initial' } },
409
- );
410
-
411
- // Wait for initial render
412
- await act(async () => {
413
- await new Promise((resolve) => setTimeout(resolve, 0));
414
- });
415
-
416
- const firstInstance = result.current[1];
417
-
418
- // Rapid updates - each creates a new instance
419
- await act(async () => {
420
- rerender({ query: 'update1' });
421
- rerender({ query: 'update2' });
422
- rerender({ query: 'update3' });
423
- await new Promise((resolve) => setTimeout(resolve, 50));
424
- });
425
-
426
- const lastInstance = result.current[1];
427
-
428
- // Different instance after updates
429
- expect(lastInstance).not.toBe(firstInstance);
430
-
431
- // Should have the latest value
432
- expect(result.current[0].results).toEqual(['Search: update3']);
433
- });
434
- });
435
- });