From 94cdad5e733f165d6f8dd1c384496c1c44eb61ec Mon Sep 17 00:00:00 2001 From: msynk Date: Sat, 1 Nov 2025 11:07:16 +0330 Subject: [PATCH] improve observe tests --- src/flare.ts | 19 +--- test/observe.test.ts | 225 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 213 insertions(+), 31 deletions(-) diff --git a/src/flare.ts b/src/flare.ts index 3732eb9..4100da8 100644 --- a/src/flare.ts +++ b/src/flare.ts @@ -226,6 +226,7 @@ export class Flare> { }; observer.fn(context, arg); } catch (error) { + // The throwing observer should not break others } } } @@ -242,30 +243,16 @@ export class Flare> { if (!shouldRun) return; try { - if (!timeout) return this.call(handlerOptions.handler, payload); + if (!timeout) return handlerOptions.handler(payload); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('FlareHandler timeout')), timeout)); - return Promise.race([this.call(handlerOptions.handler, payload), timeoutPromise]); + return Promise.race([handlerOptions.handler(payload), timeoutPromise]); } finally { if (handlerOptions.options.once) { this.handlerOptionsStore[event]?.delete(handlerOptions); } } }; - - private call( - handler: FlareHandler, - payload: E[K], - ) { - try { - return Promise.resolve(handler(payload)).catch((e) => { - // TODO: handle async exception - }); - } catch (error) { - // TODO: handle sync exception - throw error; - } - } } interface HandlerOptionsPair { diff --git a/test/observe.test.ts b/test/observe.test.ts index 2d02f0a..3ba80cd 100644 --- a/test/observe.test.ts +++ b/test/observe.test.ts @@ -1,18 +1,213 @@ -import { flare } from '../src'; - -test('observe', () => { - const PAYLOAD = 'PAYLOAD'; - const EVENT_NAME = 'EVENT_NAME'; - - flare.observe({ - id: 'ID', - fn: (context, arg) => { - console.log('context:', context); - console.log('arg:', arg); - expect(context.event).toBe(EVENT_NAME); - expect(context.payload).toBe(PAYLOAD); - } +import Flare, { + FlareObservationSource, + FlareObservationType, +} from '../src'; + +type Events = { + save: { data: string }; + delete: { id: number }; +}; + +describe('Observation system', () => { + let flare: Flare; + let observerFn: jest.Mock; + const payload = { data: 'abc' }; + + beforeEach(() => { + flare = new Flare(); + observerFn = jest.fn(); + flare.observe({ fn: observerFn }); }); - flare.fire(EVENT_NAME, PAYLOAD); + describe('basic observation behavior', () => { + it('should call observer on normal event fire with Info type', async () => { + const handler = jest.fn(); + flare.catch('save', handler); + + await flare.fire('save', payload); + + expect(observerFn).toHaveBeenCalledWith( + expect.objectContaining({ + event: 'save', + payload, + type: FlareObservationType.Info, + source: FlareObservationSource.Flare, + }), + undefined + ); + }); + + it('should include timestamp and source info', async () => { + flare.catch('save', jest.fn()); + await flare.fire('save', payload); + + const [ctx] = observerFn.mock.calls[0]; + expect(typeof ctx.timestamp).toBe('number'); + expect(ctx.source).toBe(FlareObservationSource.Flare); + }); + + it('should not block event flow if observer throws error', async () => { + const badObserver = jest.fn(() => { throw new Error('bad'); }); + flare.observe({ fn: badObserver }); + + const handler = jest.fn(); + flare.catch('save', handler); + + await expect(flare.fire('save', payload)).resolves.toBeUndefined(); + expect(handler).toHaveBeenCalled(); + // The throwing observer should not break others + expect(observerFn).toHaveBeenCalled(); + }); + }); + + describe('observation from other sources', () => { + it('should record Warning observation when event stopped by middleware', async () => { + flare.use({ + id: 'blocker', + fn: (ctx) => ctx.stop(), + }); + flare.catch('save', jest.fn()); + + await flare.fire('save', payload); + + expect(observerFn).toHaveBeenCalledWith( + expect.objectContaining({ + type: FlareObservationType.Warning, + source: FlareObservationSource.Middleware, + sourceId: 'blocker', + }), + expect.stringContaining('stopped') + ); + }); + + it('should record Warning observation when event cancelled by interceptor', async () => { + flare.in({ + id: 'breaker', + before: () => false, + }); + flare.catch('save', jest.fn()); + + await flare.fire('save', payload); + + expect(observerFn).toHaveBeenCalledWith( + expect.objectContaining({ + type: FlareObservationType.Warning, + source: FlareObservationSource.Interceptor, + sourceId: 'breaker', + }), + expect.stringContaining('cancelled') + ); + }); + + it('should record Error observation when interceptor throws', async () => { + flare.in({ + id: 'faulty', + before: () => { throw new Error('boom'); }, + }); + flare.catch('save', jest.fn()); + + await flare.fire('save', payload); + + expect(observerFn).toHaveBeenCalledWith( + expect.objectContaining({ + type: FlareObservationType.Error, + source: FlareObservationSource.Interceptor, + sourceId: 'faulty', + }), + expect.any(Error) + ); + }); + + it('should record Error observation when middleware throws', async () => { + flare.use({ + id: 'm1', + fn: async () => { + throw new Error('mw fail'); + }, + }); + flare.catch('save', jest.fn()); + + await expect(flare.fire('save', payload)).rejects.toThrow('mw fail'); + + expect(observerFn).toHaveBeenCalledWith( + expect.objectContaining({ + type: FlareObservationType.Error, + source: FlareObservationSource.Middleware, + sourceId: 'm1', + }), + expect.any(Error) + ); + }); + }); + + describe('observation during handler execution', () => { + it('should record Error observation when handler throws synchronously', async () => { + const errorHandler = jest.fn(() => { throw new Error('handler fail'); }); + flare.catch('save', errorHandler); + + await flare.fire('save', payload); + + expect(observerFn).toHaveBeenCalledWith( + expect.objectContaining({ + type: FlareObservationType.Error, + source: FlareObservationSource.Handler, + }), + expect.any(Error) + ); + }); + + it('should record Error observation when handler rejects asynchronously', async () => { + const asyncFailHandler = jest.fn(async () => { + return Promise.reject(new Error('async fail')); + }); + flare.catch('save', asyncFailHandler); + + await flare.fire('save', payload); + + expect(observerFn).toHaveBeenCalledWith( + expect.objectContaining({ + type: FlareObservationType.Error, + source: FlareObservationSource.Handler, + }), + expect.any(Error) + ); + }); + }); + + describe('observer management', () => { + it('should allow multiple observers to receive the same events', async () => { + const obs2 = jest.fn(); + flare.observe({ fn: obs2 }); + + flare.catch('save', jest.fn()); + await flare.fire('save', payload); + + expect(observerFn).toHaveBeenCalled(); + expect(obs2).toHaveBeenCalled(); + }); + + it('should allow dynamic observation of different event types', async () => { + const obsA = jest.fn(); + const obsB = jest.fn(); + + flare.observe({ fn: obsA }); + flare.observe({ fn: obsB }); + + flare.catch('save', jest.fn()); + flare.catch('delete', jest.fn()); + + await flare.fire('save', { data: 'ok' }); + await flare.fire('delete', { id: 7 }); + + expect(obsA).toHaveBeenCalledWith( + expect.objectContaining({ event: 'save' }), + undefined + ); + expect(obsB).toHaveBeenCalledWith( + expect.objectContaining({ event: 'delete' }), + undefined + ); + }); + }); }); +