@profullstack/state-manager
Version:
Enhanced state manager with web component integration, persistence, and subscription management
443 lines (329 loc) • 13.5 kB
JavaScript
/**
* Tests for middleware manager
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
createMiddlewareManager,
createLoggerMiddleware,
createPersistenceMiddleware,
createValidationMiddleware,
createThrottleMiddleware,
createDebounceMiddleware
} from '../src/middleware.js';
describe('MiddlewareManager', () => {
let middlewareManager;
beforeEach(() => {
middlewareManager = createMiddlewareManager();
});
describe('use', () => {
it('should add middleware to the specified type', () => {
const middleware = () => {};
middlewareManager.use('beforeUpdate', middleware);
expect(middlewareManager.getMiddlewareCount('beforeUpdate')).toBe(1);
});
it('should throw an error for invalid middleware type', () => {
const middleware = () => {};
expect(() => {
middlewareManager.use('invalidType', middleware);
}).toThrow('Invalid middleware type: invalidType');
});
it('should throw an error if middleware is not a function', () => {
expect(() => {
middlewareManager.use('beforeUpdate', {});
}).toThrow('Middleware must be a function');
});
it('should return a function to remove the middleware', () => {
const middleware = () => {};
const remove = middlewareManager.use('beforeUpdate', middleware);
expect(middlewareManager.getMiddlewareCount('beforeUpdate')).toBe(1);
remove();
expect(middlewareManager.getMiddlewareCount('beforeUpdate')).toBe(0);
});
});
describe('applyMiddleware', () => {
it('should apply middleware to the value', () => {
const middleware1 = vi.fn(value => value + 1);
const middleware2 = vi.fn(value => value * 2);
middlewareManager.use('beforeUpdate', middleware1);
middlewareManager.use('beforeUpdate', middleware2);
const result = middlewareManager.applyMiddleware('beforeUpdate', 1);
expect(middleware1).toHaveBeenCalledWith(1, undefined);
expect(middleware2).toHaveBeenCalledWith(2, undefined);
expect(result).toBe(4);
});
it('should pass extra data to middleware', () => {
const middleware = vi.fn((value, extra) => {
return { ...value, extra };
});
middlewareManager.use('beforeUpdate', middleware);
const result = middlewareManager.applyMiddleware(
'beforeUpdate',
{ value: 1 },
{ data: 'extra' }
);
expect(middleware).toHaveBeenCalledWith({ value: 1 }, { data: 'extra' });
expect(result).toEqual({ value: 1, extra: { data: 'extra' } });
});
it('should handle errors in middleware', () => {
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const middleware1 = vi.fn(value => {
throw new Error('Middleware error');
});
const middleware2 = vi.fn(value => value * 2);
middlewareManager.use('beforeUpdate', middleware1);
middlewareManager.use('beforeUpdate', middleware2);
const result = middlewareManager.applyMiddleware('beforeUpdate', 1);
expect(consoleErrorSpy).toHaveBeenCalled();
expect(middleware2).toHaveBeenCalledWith(1, undefined);
expect(result).toBe(2);
consoleErrorSpy.mockRestore();
});
it('should throw an error for invalid middleware type', () => {
expect(() => {
middlewareManager.applyMiddleware('invalidType', {});
}).toThrow('Invalid middleware type: invalidType');
});
});
describe('getMiddlewareCount', () => {
it('should return the count for a specific type', () => {
const middleware1 = () => {};
const middleware2 = () => {};
middlewareManager.use('beforeUpdate', middleware1);
middlewareManager.use('beforeUpdate', middleware2);
middlewareManager.use('afterUpdate', middleware1);
expect(middlewareManager.getMiddlewareCount('beforeUpdate')).toBe(2);
expect(middlewareManager.getMiddlewareCount('afterUpdate')).toBe(1);
expect(middlewareManager.getMiddlewareCount('beforeReset')).toBe(0);
});
it('should return counts for all types when no type is specified', () => {
const middleware1 = () => {};
const middleware2 = () => {};
middlewareManager.use('beforeUpdate', middleware1);
middlewareManager.use('beforeUpdate', middleware2);
middlewareManager.use('afterUpdate', middleware1);
const counts = middlewareManager.getMiddlewareCount();
expect(counts).toEqual({
beforeUpdate: 2,
afterUpdate: 1,
beforeReset: 0,
afterReset: 0
});
});
});
describe('clearMiddleware', () => {
it('should clear middleware for a specific type', () => {
const middleware1 = () => {};
const middleware2 = () => {};
middlewareManager.use('beforeUpdate', middleware1);
middlewareManager.use('beforeUpdate', middleware2);
middlewareManager.use('afterUpdate', middleware1);
middlewareManager.clearMiddleware('beforeUpdate');
expect(middlewareManager.getMiddlewareCount('beforeUpdate')).toBe(0);
expect(middlewareManager.getMiddlewareCount('afterUpdate')).toBe(1);
});
it('should clear all middleware when no type is specified', () => {
const middleware1 = () => {};
const middleware2 = () => {};
middlewareManager.use('beforeUpdate', middleware1);
middlewareManager.use('beforeUpdate', middleware2);
middlewareManager.use('afterUpdate', middleware1);
middlewareManager.use('beforeReset', middleware2);
middlewareManager.clearMiddleware();
expect(middlewareManager.getMiddlewareCount('beforeUpdate')).toBe(0);
expect(middlewareManager.getMiddlewareCount('afterUpdate')).toBe(0);
expect(middlewareManager.getMiddlewareCount('beforeReset')).toBe(0);
expect(middlewareManager.getMiddlewareCount('afterReset')).toBe(0);
});
it('should throw an error for invalid middleware type', () => {
expect(() => {
middlewareManager.clearMiddleware('invalidType');
}).toThrow('Invalid middleware type: invalidType');
});
});
});
describe('LoggerMiddleware', () => {
let loggerMiddleware;
let loggerSpy;
beforeEach(() => {
loggerSpy = vi.fn();
loggerMiddleware = createLoggerMiddleware({
logger: loggerSpy
});
});
it('should log before updates', () => {
const update = { value: 1 };
const state = { value: 0 };
const result = loggerMiddleware.beforeUpdate(update, state);
expect(loggerSpy).toHaveBeenCalledWith('[StateManager] Before update:', {
currentState: state,
update
});
expect(result).toBe(update);
});
it('should log after updates', () => {
const state = { value: 1 };
const changedPaths = ['value'];
const result = loggerMiddleware.afterUpdate(state, changedPaths);
expect(loggerSpy).toHaveBeenCalledWith('[StateManager] After update:', {
newState: state,
changedPaths
});
expect(result).toBe(state);
});
it('should log before resets', () => {
const initialState = { value: 0 };
const result = loggerMiddleware.beforeReset(initialState);
expect(loggerSpy).toHaveBeenCalledWith('[StateManager] Before reset:', {
initialState
});
expect(result).toBe(initialState);
});
it('should log after resets', () => {
const state = { value: 0 };
const result = loggerMiddleware.afterReset(state);
expect(loggerSpy).toHaveBeenCalledWith('[StateManager] After reset:', {
newState: state
});
expect(result).toBe(state);
});
it('should not log when disabled', () => {
loggerMiddleware = createLoggerMiddleware({
logBefore: false,
logAfter: false,
logger: loggerSpy
});
loggerMiddleware.beforeUpdate({ value: 1 }, { value: 0 });
loggerMiddleware.afterUpdate({ value: 1 }, ['value']);
expect(loggerSpy).not.toHaveBeenCalled();
});
});
describe('PersistenceMiddleware', () => {
it('should save state after updates', () => {
const persistenceManager = {
save: vi.fn()
};
const persistenceMiddleware = createPersistenceMiddleware(persistenceManager);
const state = { value: 1 };
const result = persistenceMiddleware.afterUpdate(state);
expect(persistenceManager.save).toHaveBeenCalledWith(state);
expect(result).toBe(state);
});
it('should save state after resets', () => {
const persistenceManager = {
save: vi.fn()
};
const persistenceMiddleware = createPersistenceMiddleware(persistenceManager);
const state = { value: 0 };
const result = persistenceMiddleware.afterReset(state);
expect(persistenceManager.save).toHaveBeenCalledWith(state);
expect(result).toBe(state);
});
});
describe('ValidationMiddleware', () => {
it('should validate state before updates', () => {
const schema = {
type: 'object',
properties: {
value: { type: 'number' }
}
};
const validator = vi.fn().mockReturnValue(true);
validator.errors = null;
const validationMiddleware = createValidationMiddleware(schema, validator);
const update = { value: 1 };
const state = { value: 0 };
const result = validationMiddleware.beforeUpdate(update, state);
expect(validator).toHaveBeenCalledWith({ value: 1 }, schema);
expect(result).toBe(update);
});
it('should throw an error if validation fails', () => {
const schema = {
type: 'object',
properties: {
value: { type: 'number' }
}
};
const validator = vi.fn().mockReturnValue(false);
validator.errors = [{ message: 'Invalid value' }];
const validationMiddleware = createValidationMiddleware(schema, validator);
const update = { value: 'not a number' };
const state = { value: 0 };
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
validationMiddleware.beforeUpdate(update, state);
}).toThrow('State validation failed');
expect(validator).toHaveBeenCalledWith({ value: 'not a number' }, schema);
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
it('should validate state before resets', () => {
const schema = {
type: 'object',
properties: {
value: { type: 'number' }
}
};
const validator = vi.fn().mockReturnValue(true);
validator.errors = null;
const validationMiddleware = createValidationMiddleware(schema, validator);
const initialState = { value: 0 };
const result = validationMiddleware.beforeReset(initialState);
expect(validator).toHaveBeenCalledWith(initialState, schema);
expect(result).toBe(initialState);
});
it('should throw an error if reset validation fails', () => {
const schema = {
type: 'object',
properties: {
value: { type: 'number' }
}
};
const validator = vi.fn().mockReturnValue(false);
validator.errors = [{ message: 'Invalid value' }];
const validationMiddleware = createValidationMiddleware(schema, validator);
const initialState = { value: 'not a number' };
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
validationMiddleware.beforeReset(initialState);
}).toThrow('State validation failed');
expect(validator).toHaveBeenCalledWith(initialState, schema);
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});
// Note: Throttle and Debounce middleware tests would require timer mocking
// which is more complex. We'll add basic tests for them.
describe('ThrottleMiddleware', () => {
it('should create a throttle middleware', () => {
const throttleMiddleware = createThrottleMiddleware(100);
expect(throttleMiddleware).toHaveProperty('beforeUpdate');
expect(typeof throttleMiddleware.beforeUpdate).toBe('function');
});
it('should allow the first update through', () => {
vi.useFakeTimers();
const throttleMiddleware = createThrottleMiddleware(100);
const update = { value: 1 };
const state = { value: 0 };
const result = throttleMiddleware.beforeUpdate(update, state);
expect(result).toBe(update);
vi.useRealTimers();
});
});
describe('DebounceMiddleware', () => {
it('should create a debounce middleware', () => {
const debounceMiddleware = createDebounceMiddleware(100);
expect(debounceMiddleware).toHaveProperty('beforeUpdate');
expect(typeof debounceMiddleware.beforeUpdate).toBe('function');
});
it('should return an empty object for the first update', () => {
vi.useFakeTimers();
const debounceMiddleware = createDebounceMiddleware(100);
const update = { value: 1 };
const state = {
setState: vi.fn()
};
const result = debounceMiddleware.beforeUpdate(update, state);
expect(result).toEqual({});
vi.useRealTimers();
});
});