import { describe, test, expect, vi, beforeEach } from 'vitest'; import { ActionHandlerMiddleware, ActionHandlerMiddlewareCallback } from '../src/action-handler-middleware'; import { ActionHandler } from '../src/action-handler'; import { IActionMapExtended } from '../src/action-map'; import { FlowfulError } from '../src/error'; import { skipDevMessages } from '../src/dev'; import { MODE_DEV, setGlobalMode } from '../src/mode'; import { ChainedActionHandlerMiddlewareCallback } from '../dist/types'; setGlobalMode(MODE_DEV); skipDevMessages(true); describe('ActionHandlerMiddleware', () => { let middleware: ActionHandlerMiddleware; let actionMap: IActionMapExtended; let handler: ActionHandler; beforeEach(() => { middleware = new ActionHandlerMiddleware(); actionMap = {} as IActionMapExtended; handler = { invoke: vi.fn().mockReturnValue('handler-result'), } as unknown as ActionHandler; }); test('constructor with default options', () => { expect(middleware.options).toEqual({ cancellable: true }); }); test('constructor with custom options', () => { const customMiddleware = new ActionHandlerMiddleware({ cancellable: false }); expect(customMiddleware.options).toEqual({ cancellable: false }); }); test('add() should add middleware callback', () => { const callback: ActionHandlerMiddlewareCallback = (next) => (actionMap, handler) => next(actionMap, handler); expect(middleware.add(callback)).toBe(true); // @ts-expect-error - accessing protected property for testing expect(middleware._middlewareCallbacks.has(callback)).toBe(true); // @ts-expect-error - accessing protected property for testing expect(middleware._middlewareCallbacksArray.length).toBe(1); // @ts-expect-error - accessing protected property for testing expect(middleware._composed).toBeUndefined(); // Adding the same callback again should return false expect(middleware.add(callback)).toBe(false); }); test('remove() should remove middleware callback', () => { const callback: ActionHandlerMiddlewareCallback = (next) => (actionMap, handler) => next(actionMap, handler); middleware.add(callback); expect(middleware.remove(callback)).toBe(true); // @ts-expect-error - accessing protected property for testing expect(middleware._middlewareCallbacks.has(callback)).toBe(false); // @ts-expect-error - accessing protected property for testing expect(middleware._middlewareCallbacksArray.length).toBe(0); // @ts-expect-error - accessing protected property for testing expect(middleware._composed).toBeUndefined(); // Removing a non-existent callback should return false expect(middleware.remove(callback)).toBe(false); }); test('compose() should chain middleware callbacks', () => { const mockMiddleware1: ActionHandlerMiddlewareCallback = (next) => (actionMap, handler) => { return next(actionMap, handler); }; const mockMiddleware2: ActionHandlerMiddlewareCallback = (next) => (actionMap, handler) => { return next(actionMap, handler); }; middleware.add(mockMiddleware1); middleware.add(mockMiddleware2); middleware.compose(); // @ts-expect-error - accessing protected property for testing expect(middleware._composed).toBeDefined(); }); test('run() with no middleware callbacks should invoke handler directly', () => { const result = middleware.run(actionMap, handler); expect(handler.invoke).toHaveBeenCalledWith(actionMap); expect(handler.invoke).toHaveBeenCalledTimes(1); expect(result).toBe('handler-result'); }); test('run() should execute middleware chain', () => { const middleware1Called = vi.fn(); const middleware2Called = vi.fn(); middleware.add((next) => (actionMap, handler) => { middleware1Called(); return next(actionMap, handler); }); middleware.add((next) => (actionMap, handler) => { middleware2Called(); return next(actionMap, handler); }); const result = middleware.run(actionMap, handler); expect(middleware1Called).toHaveBeenCalledTimes(1); expect(middleware2Called).toHaveBeenCalledTimes(1); expect(handler.invoke).toHaveBeenCalledWith(actionMap); expect(result).toBe('handler-result'); }); test('run() middleware can modify actionMap when cancellable is true', () => { const modifiedActionMap = { modified: true } as unknown as IActionMapExtended; middleware.add((next) => (_, handler) => { return next(modifiedActionMap, handler); }); middleware.run(actionMap, handler); expect(handler.invoke).toHaveBeenCalledWith(modifiedActionMap); }); test('run() middleware can modify handler when cancellable is true', () => { const modifiedHandler = { invoke: vi.fn().mockReturnValue('modified-result'), } as unknown as ActionHandler; middleware.add((next) => (actionMap, _) => { return next(actionMap, modifiedHandler); }); const result = middleware.run(actionMap, handler); expect(modifiedHandler.invoke).toHaveBeenCalledWith(actionMap); expect(handler.invoke).not.toHaveBeenCalled(); expect(result).toBe('modified-result'); }); test('run() middleware cannot modify actionMap when cancellable is false', () => { const nonCancellableMiddleware = new ActionHandlerMiddleware({ cancellable: false }); const modifiedActionMap = { modified: true } as unknown as IActionMapExtended; nonCancellableMiddleware.add((next) => (_, handler) => { return next(modifiedActionMap, handler); }); expect(() => nonCancellableMiddleware.run(actionMap, handler)).toThrow(FlowfulError); }); test('run() middleware cannot modify handler when cancellable is false', () => { const nonCancellableMiddleware = new ActionHandlerMiddleware({ cancellable: false }); const modifiedHandler = { invoke: vi.fn().mockReturnValue('modified-result'), } as unknown as ActionHandler; nonCancellableMiddleware.add((next) => (actionMap, _) => { return next(actionMap, modifiedHandler); }); expect(() => nonCancellableMiddleware.run(actionMap, handler)).toThrow(FlowfulError); }); test('middleware cannot call next more than once', () => { middleware.add((next) => (actionMap, handler) => { next(actionMap, handler); return next(actionMap, handler); // This should throw }); expect(() => middleware.run(actionMap, handler)).toThrow(FlowfulError); }); test('non-cancellable middleware must call next', () => { const nonCancellableMiddleware = new ActionHandlerMiddleware({ cancellable: false }); nonCancellableMiddleware.add((next) => (_actionMap, _handler) => { // Not calling next return 'cancelled'; }); expect(() => nonCancellableMiddleware.run(actionMap, handler)).toThrow(FlowfulError); }); test('middleware chain can be cancelled when cancellable is true', () => { middleware.add((next) => (_actionMap, _handler) => { // Not calling next return 'cancelled'; }); const result = middleware.run(actionMap, handler); expect(handler.invoke).not.toHaveBeenCalled(); expect(result).toBe('cancelled'); }); test('middleware chain order is preserved', () => { const calls: string[] = []; middleware.add((next) => (actionMap, handler) => { calls.push('middleware1-before'); const result = next(actionMap, handler); calls.push('middleware1-after'); return result; }); middleware.add((next) => (actionMap, handler) => { calls.push('middleware2-before'); const result = next(actionMap, handler); calls.push('middleware2-after'); return result; }); middleware.run(actionMap, handler); expect(calls).toEqual(['middleware1-before', 'middleware2-before', 'middleware2-after', 'middleware1-after']); }); test('middleware can transform return value', () => { middleware.add((next) => (actionMap, handler) => { const result = next(actionMap, handler); return `transformed-${result}`; }); const result = middleware.run(actionMap, handler); expect(result).toBe('transformed-handler-result'); }); test('removing middleware should update the middleware chain', () => { const middleware1 = (next: any) => (actionMap: any, handler: any) => { return 'middleware1-result'; }; const middleware2 = (next: any) => (actionMap: any, handler: any) => { return next(actionMap, handler); }; middleware.add(middleware1); middleware.add(middleware2); // First verify middleware1 is called expect(middleware.run(actionMap, handler)).toBe('middleware1-result'); // Remove middleware1 middleware.remove(middleware1); // Now middleware2 should be called, which passes to handler expect(middleware.run(actionMap, handler)).toBe('handler-result'); }); test('should run handler even when there is no middleware added', () => { const result = middleware.run(actionMap, handler); expect(result).toBe('handler-result'); }); });