@blac/core 2.0.0-rc-7 → 2.0.0-rc-10

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,419 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Bloc } from '../Bloc';
3
+ import { Blac } from '../Blac';
4
+
5
+ // Define test events
6
+ class IncrementEvent {
7
+ constructor(public amount: number = 1) {}
8
+ }
9
+
10
+ class AsyncIncrementEvent {
11
+ constructor(
12
+ public amount: number,
13
+ public delay: number,
14
+ ) {}
15
+ }
16
+
17
+ class DecrementEvent {
18
+ constructor(public amount: number = 1) {}
19
+ }
20
+
21
+ class ErrorEvent {
22
+ constructor(public message: string) {}
23
+ }
24
+
25
+ class CriticalErrorEvent extends Error {
26
+ name = 'CriticalError';
27
+ constructor(message: string) {
28
+ super(message);
29
+ }
30
+ }
31
+
32
+ // Test Bloc implementation
33
+ class CounterBloc extends Bloc<
34
+ number,
35
+ IncrementEvent | AsyncIncrementEvent | DecrementEvent | ErrorEvent
36
+ > {
37
+ constructor(initialValue = 0) {
38
+ super(initialValue);
39
+
40
+ // Register event handlers
41
+ this.on(IncrementEvent, (event, emit) => {
42
+ emit(this.state + event.amount);
43
+ });
44
+
45
+ this.on(AsyncIncrementEvent, async (event, emit) => {
46
+ await new Promise((resolve) => setTimeout(resolve, event.delay));
47
+ emit(this.state + event.amount);
48
+ });
49
+
50
+ this.on(DecrementEvent, (event, emit) => {
51
+ emit(this.state - event.amount);
52
+ });
53
+
54
+ this.on(ErrorEvent, (event, emit) => {
55
+ throw new Error(event.message);
56
+ });
57
+ }
58
+ }
59
+
60
+ // Bloc with critical error handling
61
+ class CriticalErrorBloc extends Bloc<string> {
62
+ constructor() {
63
+ super('initial');
64
+
65
+ this.on(CriticalErrorEvent, (event) => {
66
+ throw event;
67
+ });
68
+ }
69
+ }
70
+
71
+ describe('Bloc Event Handling', () => {
72
+ let bloc: CounterBloc;
73
+ let blacInstance: Blac;
74
+
75
+ beforeEach(() => {
76
+ blacInstance = new Blac({ __unsafe_ignore_singleton: true });
77
+ Blac.enableLog = false; // Disable logging for tests
78
+ bloc = blacInstance.getBloc(CounterBloc);
79
+ });
80
+
81
+ afterEach(() => {
82
+ vi.clearAllMocks();
83
+ });
84
+
85
+ describe('Event Handler Registration', () => {
86
+ it('should register event handlers with on() method', () => {
87
+ expect(bloc.eventHandlers.size).toBe(4); // 4 handlers registered
88
+ expect(bloc.eventHandlers.has(IncrementEvent)).toBe(true);
89
+ expect(bloc.eventHandlers.has(AsyncIncrementEvent)).toBe(true);
90
+ expect(bloc.eventHandlers.has(DecrementEvent)).toBe(true);
91
+ expect(bloc.eventHandlers.has(ErrorEvent)).toBe(true);
92
+ });
93
+
94
+ it('should overwrite existing handler when registering same event type', () => {
95
+ const originalHandler = bloc.eventHandlers.get(IncrementEvent);
96
+
97
+ // Register new handler for same event
98
+ const newHandler = vi.fn();
99
+ (bloc as any).on(IncrementEvent, newHandler);
100
+
101
+ expect(bloc.eventHandlers.get(IncrementEvent)).toBe(newHandler);
102
+ expect(bloc.eventHandlers.get(IncrementEvent)).not.toBe(originalHandler);
103
+ });
104
+
105
+ it('should handle events with proper type inference', async () => {
106
+ const observer = vi.fn();
107
+ bloc._observer.subscribe({ id: 'test', fn: observer });
108
+
109
+ await bloc.add(new IncrementEvent(5));
110
+
111
+ expect(observer).toHaveBeenCalledWith(5, 0, expect.any(IncrementEvent));
112
+ expect(bloc.state).toBe(5);
113
+ });
114
+ });
115
+
116
+ describe('Event Queue and Sequential Processing', () => {
117
+ it('should queue events and process them sequentially', async () => {
118
+ const observer = vi.fn();
119
+ bloc._observer.subscribe({ id: 'test', fn: observer });
120
+
121
+ // Add multiple events rapidly
122
+ const promises = [
123
+ bloc.add(new IncrementEvent(1)),
124
+ bloc.add(new IncrementEvent(2)),
125
+ bloc.add(new IncrementEvent(3)),
126
+ ];
127
+
128
+ await Promise.all(promises);
129
+
130
+ // Should be called 3 times with sequential state updates
131
+ expect(observer).toHaveBeenCalledTimes(3);
132
+ expect(observer).toHaveBeenNthCalledWith(
133
+ 1,
134
+ 1,
135
+ 0,
136
+ expect.any(IncrementEvent),
137
+ );
138
+ expect(observer).toHaveBeenNthCalledWith(
139
+ 2,
140
+ 3,
141
+ 1,
142
+ expect.any(IncrementEvent),
143
+ );
144
+ expect(observer).toHaveBeenNthCalledWith(
145
+ 3,
146
+ 6,
147
+ 3,
148
+ expect.any(IncrementEvent),
149
+ );
150
+ expect(bloc.state).toBe(6);
151
+ });
152
+
153
+ it('should process async events in order', async () => {
154
+ const observer = vi.fn();
155
+ bloc._observer.subscribe({ id: 'test', fn: observer });
156
+
157
+ // Add async events with different delays
158
+ const promises = [
159
+ bloc.add(new AsyncIncrementEvent(1, 50)),
160
+ bloc.add(new AsyncIncrementEvent(2, 10)),
161
+ bloc.add(new AsyncIncrementEvent(3, 30)),
162
+ ];
163
+
164
+ await Promise.all(promises);
165
+
166
+ // Despite different delays, events should process in order
167
+ expect(observer).toHaveBeenCalledTimes(3);
168
+ expect(observer).toHaveBeenNthCalledWith(
169
+ 1,
170
+ 1,
171
+ 0,
172
+ expect.any(AsyncIncrementEvent),
173
+ );
174
+ expect(observer).toHaveBeenNthCalledWith(
175
+ 2,
176
+ 3,
177
+ 1,
178
+ expect.any(AsyncIncrementEvent),
179
+ );
180
+ expect(observer).toHaveBeenNthCalledWith(
181
+ 3,
182
+ 6,
183
+ 3,
184
+ expect.any(AsyncIncrementEvent),
185
+ );
186
+ });
187
+
188
+ it('should handle mixed sync and async events', async () => {
189
+ const observer = vi.fn();
190
+ bloc._observer.subscribe({ id: 'test', fn: observer });
191
+
192
+ const promises = [
193
+ bloc.add(new IncrementEvent(1)),
194
+ bloc.add(new AsyncIncrementEvent(2, 20)),
195
+ bloc.add(new DecrementEvent(1)),
196
+ bloc.add(new AsyncIncrementEvent(3, 10)),
197
+ ];
198
+
199
+ await Promise.all(promises);
200
+
201
+ expect(observer).toHaveBeenCalledTimes(4);
202
+ expect(bloc.state).toBe(5); // 0 + 1 + 2 - 1 + 3 = 5
203
+ });
204
+ });
205
+
206
+ describe('Error Handling', () => {
207
+ it('should handle errors in event handlers gracefully', async () => {
208
+ const errorSpy = vi.spyOn(Blac, 'error').mockImplementation(() => {});
209
+ const observer = vi.fn();
210
+ bloc._observer.subscribe({ id: 'test', fn: observer });
211
+
212
+ await bloc.add(new ErrorEvent('Test error'));
213
+
214
+ // Error should be logged but not crash the bloc
215
+ expect(errorSpy).toHaveBeenCalled();
216
+ expect(observer).not.toHaveBeenCalled(); // No state change
217
+ expect(bloc.state).toBe(0); // State unchanged
218
+
219
+ // Bloc should still be functional
220
+ await bloc.add(new IncrementEvent(1));
221
+ expect(bloc.state).toBe(1);
222
+ });
223
+
224
+ it('should re-throw critical errors', async () => {
225
+ const criticalBloc = new CriticalErrorBloc();
226
+
227
+ await expect(
228
+ criticalBloc.add(new CriticalErrorEvent('Critical failure')),
229
+ ).rejects.toThrow('Critical failure');
230
+ });
231
+
232
+ it('should log warning for unhandled events', async () => {
233
+ const warnSpy = vi.spyOn(Blac, 'warn').mockImplementation(() => {});
234
+
235
+ // Create event without handler
236
+ class UnhandledEvent {}
237
+
238
+ await bloc.add(new UnhandledEvent() as any);
239
+
240
+ expect(warnSpy).toHaveBeenCalledWith(
241
+ expect.stringContaining('No handler registered'),
242
+ expect.any(String),
243
+ expect.any(Array),
244
+ expect.any(String),
245
+ expect.any(UnhandledEvent),
246
+ );
247
+ });
248
+ });
249
+
250
+ describe('Event Context and Metadata', () => {
251
+ it('should pass event instance to state change notifications', async () => {
252
+ const observer = vi.fn();
253
+ bloc._observer.subscribe({ id: 'test', fn: observer });
254
+
255
+ const event = new IncrementEvent(10);
256
+ await bloc.add(event);
257
+
258
+ expect(observer).toHaveBeenCalledWith(10, 0, event);
259
+ });
260
+
261
+ it('should maintain correct state context during handler execution', async () => {
262
+ let capturedStates: number[] = [];
263
+
264
+ // Custom bloc that captures state during handler
265
+ class StateCapturingBloc extends Bloc<number> {
266
+ constructor() {
267
+ super(0);
268
+
269
+ this.on(IncrementEvent, (event, emit) => {
270
+ capturedStates.push(this.state); // Capture current state
271
+ emit(this.state + event.amount);
272
+ });
273
+ }
274
+ }
275
+
276
+ const capturingBloc = new StateCapturingBloc();
277
+
278
+ await capturingBloc.add(new IncrementEvent(1));
279
+ await capturingBloc.add(new IncrementEvent(2));
280
+ await capturingBloc.add(new IncrementEvent(3));
281
+
282
+ expect(capturedStates).toEqual([0, 1, 3]); // State at time of each handler
283
+ expect(capturingBloc.state).toBe(6);
284
+ });
285
+ });
286
+
287
+ describe('Edge Cases', () => {
288
+ it('should handle empty event queue gracefully', async () => {
289
+ // Process empty queue should not throw
290
+ await (bloc as any)._processEventQueue();
291
+ expect(bloc.state).toBe(0);
292
+ });
293
+
294
+ it('should handle rapid event additions during processing', async () => {
295
+ const observer = vi.fn();
296
+ bloc._observer.subscribe({ id: 'test', fn: observer });
297
+
298
+ // Custom bloc that adds more events during processing
299
+ class ChainReactionBloc extends Bloc<number> {
300
+ constructor() {
301
+ super(0);
302
+
303
+ this.on(IncrementEvent, async (event, emit) => {
304
+ emit(this.state + event.amount);
305
+
306
+ // Add another event during processing
307
+ if (this.state < 5) {
308
+ await this.add(new IncrementEvent(1));
309
+ }
310
+ });
311
+ }
312
+ }
313
+
314
+ const chainBloc = new ChainReactionBloc();
315
+ const chainObserver = vi.fn();
316
+ chainBloc._observer.subscribe({ id: 'test', fn: chainObserver });
317
+
318
+ await chainBloc.add(new IncrementEvent(1));
319
+
320
+ // Should process all chained events
321
+ expect(chainBloc.state).toBe(5);
322
+ expect(chainObserver).toHaveBeenCalledTimes(5);
323
+ });
324
+
325
+ it('should handle event handler that emits multiple times', async () => {
326
+ class MultiEmitBloc extends Bloc<number[]> {
327
+ constructor() {
328
+ super([]);
329
+
330
+ this.on(IncrementEvent, (event, emit) => {
331
+ // Emit multiple state updates
332
+ for (let i = 1; i <= event.amount; i++) {
333
+ emit([...this.state, i]);
334
+ }
335
+ });
336
+ }
337
+ }
338
+
339
+ const multiBloc = new MultiEmitBloc();
340
+ const observer = vi.fn();
341
+ multiBloc._observer.subscribe({ id: 'test', fn: observer });
342
+
343
+ await multiBloc.add(new IncrementEvent(3));
344
+
345
+ expect(observer).toHaveBeenCalledTimes(3);
346
+ expect(multiBloc.state).toEqual([1, 2, 3]);
347
+ });
348
+
349
+ it('should handle events with constructor-less classes', async () => {
350
+ // Event without explicit constructor
351
+ class SimpleEvent {}
352
+
353
+ class SimpleBloc extends Bloc<string> {
354
+ constructor() {
355
+ super('initial');
356
+ this.on(SimpleEvent, (event, emit) => {
357
+ emit('handled');
358
+ });
359
+ }
360
+ }
361
+
362
+ const simpleBloc = new SimpleBloc();
363
+ await simpleBloc.add(new SimpleEvent() as any);
364
+
365
+ expect(simpleBloc.state).toBe('handled');
366
+ });
367
+
368
+ it('should maintain event processing integrity during disposal', async () => {
369
+ const observer = vi.fn();
370
+ bloc._observer.subscribe({ id: 'test', fn: observer });
371
+
372
+ // Start async event processing
373
+ const promise = bloc.add(new AsyncIncrementEvent(5, 50));
374
+
375
+ // Dispose bloc while event is processing
376
+ setTimeout(() => bloc._dispose(), 25);
377
+
378
+ await promise;
379
+
380
+ // Event should complete processing despite disposal attempt
381
+ expect(bloc.state).toBe(5);
382
+ });
383
+ });
384
+
385
+ describe('Performance Considerations', () => {
386
+ it('should handle large event queues efficiently', async () => {
387
+ const eventCount = 1000;
388
+ const promises: Promise<void>[] = [];
389
+
390
+ for (let i = 0; i < eventCount; i++) {
391
+ promises.push(bloc.add(new IncrementEvent(1)));
392
+ }
393
+
394
+ const startTime = performance.now();
395
+ await Promise.all(promises);
396
+ const endTime = performance.now();
397
+
398
+ expect(bloc.state).toBe(eventCount);
399
+ expect(endTime - startTime).toBeLessThan(1000); // Should process quickly
400
+ });
401
+
402
+ it('should not leak memory with event references', async () => {
403
+ const events: any[] = [];
404
+
405
+ // Create many events
406
+ for (let i = 0; i < 100; i++) {
407
+ const event = new IncrementEvent(1);
408
+ events.push(event);
409
+ await bloc.add(event);
410
+ }
411
+
412
+ // Clear event array
413
+ events.length = 0;
414
+
415
+ // Events should be garbage collectible after processing
416
+ expect((bloc as any)._eventQueue.length).toBe(0);
417
+ });
418
+ });
419
+ });