UNPKG

@sigi/core

Version:
434 lines 17.7 kB
import { __decorate, __metadata } from "tslib"; import '@abraham/reflection'; import { rootInjector } from '@sigi/di'; import { of, Observable, noop } from 'rxjs'; import { delay, map, withLatestFrom, takeUntil, tap, switchMap, exhaustMap, startWith, share } from 'rxjs/operators'; import { Effect, Reducer, ImmerReducer, DefineAction } from '../decorators'; import { EffectModule } from '../module'; import { Module } from '../module.decorator'; const TIME_TO_DELAY = 300; let Counter = class Counter extends EffectModule { constructor() { super(...arguments); this.defaultState = { count: 1, }; } setCountImmer(state, payload) { state.count = payload.valueOf(); } addCountOneImmer(state) { state.count = state.count + 1; } addOne(state) { return Object.assign(Object.assign({}, state), { count: state.count + 1 }); } setCount(state, payload) { return Object.assign(Object.assign({}, state), { count: payload }); } asyncAddCount(payload$) { return payload$.pipe(delay(TIME_TO_DELAY), map((payload) => this.getActions().asyncAddCountString(`${payload}`)), takeUntil(this.dispose$)); } asyncAddCountString(payload$) { return payload$.pipe(delay(TIME_TO_DELAY), map((payload) => this.getActions().setCount(parseInt(payload))), takeUntil(this.getAction$().asyncAddCount)); } asyncAddCountOne(payload$) { return payload$.pipe(delay(TIME_TO_DELAY), withLatestFrom(this.state$), map(([, state]) => this.getActions().setCount(state.count + 1))); } effectWithPureSideEffect(payload$) { return payload$.pipe(tap(noop), map(() => this.noop())); } effectToResetState(payload$) { return payload$.pipe(map(() => this.reset())); } cancellableEffect(payload$) { return payload$.pipe(switchMap(() => of(this.getActions().addOne()).pipe(delay(TIME_TO_DELAY), startWith(this.getActions().setCount(1))))); } exhaustEffect(payload$) { return payload$.pipe(exhaustMap(() => of(this.getActions().addOne()).pipe(delay(TIME_TO_DELAY), startWith(this.getActions().setCount(1))))); } }; __decorate([ DefineAction(), __metadata("design:type", Observable) ], Counter.prototype, "dispose$", void 0); __decorate([ ImmerReducer(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Date]), __metadata("design:returntype", void 0) ], Counter.prototype, "setCountImmer", null); __decorate([ ImmerReducer(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", void 0) ], Counter.prototype, "addCountOneImmer", null); __decorate([ Reducer(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object]), __metadata("design:returntype", void 0) ], Counter.prototype, "addOne", null); __decorate([ Reducer(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Number]), __metadata("design:returntype", void 0) ], Counter.prototype, "setCount", null); __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", Observable) ], Counter.prototype, "asyncAddCount", null); __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", Observable) ], Counter.prototype, "asyncAddCountString", null); __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", Observable) ], Counter.prototype, "asyncAddCountOne", null); __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", void 0) ], Counter.prototype, "effectWithPureSideEffect", null); __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", void 0) ], Counter.prototype, "effectToResetState", null); __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", void 0) ], Counter.prototype, "cancellableEffect", null); __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", void 0) ], Counter.prototype, "exhaustEffect", null); Counter = __decorate([ Module('counter') ], Counter); describe('EffectModule Class', () => { let counter; let store; beforeEach(() => { counter = rootInjector.resolveAndInstantiate(Counter); store = counter.store; }); afterEach(() => { store.dispose(); }); describe('basic shape specs', () => { it('should dispatch reducer action', () => { expect(counter.getActions().setCount(1)).toStrictEqual({ payload: 1, type: 'setCount', store, }); }); it('should dispatch effect action', () => { expect(counter.getActions().asyncAddCount(1)).toStrictEqual({ payload: 1, type: 'asyncAddCount', store, }); }); it('should dispatch immer reducer action', () => { const payload = new Date(); expect(counter.getActions().setCountImmer(payload)).toStrictEqual({ payload, type: 'setCountImmer', store, }); }); it('should dispatch reducer action with void payload', () => { expect(counter.getActions().addOne()).toStrictEqual({ payload: undefined, type: 'addOne', store, }); }); it('should dispatch effect action with void payload', () => { expect(counter.getActions().asyncAddCountOne()).toStrictEqual({ payload: undefined, type: 'asyncAddCountOne', store, }); }); it('should dispatch immer reducer action with void payload', () => { expect(counter.getActions().addCountOneImmer()).toStrictEqual({ payload: undefined, type: 'addCountOneImmer', store, }); }); it('should dispatch action created by DefineAction', () => { expect(counter.getActions().dispose$()).toStrictEqual({ payload: undefined, type: 'dispose$', store, }); }); }); describe('complex shape specs', () => { it('should be able to create module with without epic', () => { let WithoutEpic = class WithoutEpic extends EffectModule { constructor() { super(...arguments); this.defaultState = { count: 0, }; } set(state, payload) { return Object.assign(Object.assign({}, state), { count: payload }); } }; __decorate([ Reducer(), __metadata("design:type", Function), __metadata("design:paramtypes", [Object, Number]), __metadata("design:returntype", void 0) ], WithoutEpic.prototype, "set", null); WithoutEpic = __decorate([ Module('WithoutEpic') ], WithoutEpic); const onlyReducer = new WithoutEpic(); const store = onlyReducer.store; const actionCreator = onlyReducer.getActions(); const beforeSpy = jest.fn(); const afterSpy = jest.fn(); store.addEpic((prev) => (action$) => { return prev(action$.pipe(tap(beforeSpy))); }); store.addEpic((prev) => (action$) => { return prev(action$).pipe(tap(afterSpy)); }); store.dispatch(actionCreator.set(1)); expect(store.state.count).toBe(1); expect(beforeSpy).toHaveBeenCalledTimes(1); expect(afterSpy).toHaveBeenCalledTimes(0); }); it('should be able to create module without reducer', () => { let WithoutReducer = class WithoutReducer extends EffectModule { constructor() { super(...arguments); this.defaultState = { count: 0, }; } set(payload$) { return payload$.pipe(tap(noop), map(() => this.noop())); } }; __decorate([ Effect(), __metadata("design:type", Function), __metadata("design:paramtypes", [Observable]), __metadata("design:returntype", void 0) ], WithoutReducer.prototype, "set", null); WithoutReducer = __decorate([ Module('WithoutReducer') ], WithoutReducer); const withoutReducer = new WithoutReducer(); const store = withoutReducer.store; const actions = withoutReducer.getActions(); store.dispatch(actions.set(1)); expect(withoutReducer.state.count).toBe(withoutReducer.defaultState.count); }); it('should throw if module name conflict#1', () => { const fn = () => { let Module1 = class Module1 extends EffectModule { constructor() { super(...arguments); this.defaultState = {}; } }; Module1 = __decorate([ Module('Module') ], Module1); let Module2 = class Module2 extends EffectModule { constructor() { super(...arguments); this.defaultState = {}; } }; Module2 = __decorate([ Module('Module') ], Module2); return { Module1, Module2 }; }; expect(fn).toThrow(); }); it('should throw if module name conflict#2', () => { const fn = () => { let Module1 = class Module1 extends EffectModule { constructor() { super(...arguments); this.defaultState = {}; } }; Module1 = __decorate([ Module('Module1') ], Module1); let Module2 = class Module2 extends EffectModule { constructor() { super(...arguments); this.defaultState = {}; } }; Module2 = __decorate([ Module('Module1') ], Module2); return { Module1, Module2 }; }; expect(fn).toThrow(); }); it('should throw if config in module invalid', () => { const fn = () => { let Module1 = class Module1 extends EffectModule { constructor() { super(...arguments); this.defaultState = {}; } }; Module1 = __decorate([ Module({}) ], Module1); return Module1; }; expect(fn).toThrow(); }); }); describe('dispatcher', () => { let actionsDispatcher; let spy = jest.fn(); let timer = jest.useFakeTimers(); beforeEach(() => { const actions = counter.getActions(); actionsDispatcher = Object.keys(actions).reduce((acc, key) => { acc[key] = (p) => { const action = actions[key](p); store.dispatch(action); return action; }; return acc; }, {}); spy = jest.fn(); store.addEpic((prev) => (action$) => prev(action$.pipe(tap(spy), share()))); timer = jest.useFakeTimers(); }); afterEach(() => { spy.mockReset(); jest.useRealTimers(); }); it('should be able to dispatch reducer action by actions dispatcher #void', () => { const action = actionsDispatcher.addOne(); expect(spy).toHaveBeenCalledWith(action); expect(spy).toHaveBeenCalledTimes(1); expect(store.state.count).toBe(counter.defaultState.count + 1); }); it('should be able to dispatch reducer action by actions dispatcher #param', () => { const newCount = 100; const action = actionsDispatcher.setCount(newCount); const [[arg]] = spy.mock.calls; expect(arg).toStrictEqual(action); expect(spy).toHaveBeenCalledTimes(1); expect(store.state.count).toBe(newCount); }); it('should be able to dispatch immer reducer action by actions dispatcher #void', () => { const action = actionsDispatcher.addCountOneImmer(); const [[arg]] = spy.mock.calls; expect(arg).toStrictEqual(action); expect(spy).toHaveBeenCalledTimes(1); expect(store.state.count).toBe(counter.defaultState.count + 1); }); it('should be able to dispatch immer reducer action by actions dispatcher #param', () => { const newCount = new Date(); const action = actionsDispatcher.setCountImmer(newCount); const [[arg]] = spy.mock.calls; expect(arg).toStrictEqual(action); expect(spy).toHaveBeenCalledTimes(1); expect(store.state.count).toBe(newCount.valueOf()); }); it('should be able to dispatch epic action by actions dispatcher #void', () => { const action = actionsDispatcher.asyncAddCountOne(); const [[arg]] = spy.mock.calls; expect(arg).toStrictEqual(action); expect(spy).toHaveBeenCalledTimes(1); expect(store.state.count).toBe(counter.defaultState.count); timer.advanceTimersByTime(TIME_TO_DELAY); const [, [arg1]] = spy.mock.calls; expect(arg1).toStrictEqual(counter.getActions().setCount(counter.defaultState.count + 1)); expect(spy).toHaveBeenCalledTimes(2); }); it('should be able to dispatch epic action by actions dispatcher #param', () => { const newCount = 100; const action = actionsDispatcher.asyncAddCount(newCount); const [[arg]] = spy.mock.calls; expect(arg).toStrictEqual(action); expect(spy).toHaveBeenCalledTimes(1); expect(store.state.count).toBe(counter.defaultState.count); timer.advanceTimersByTime(TIME_TO_DELAY); const [, [arg1]] = spy.mock.calls; expect(arg1).toStrictEqual(counter.getActions().asyncAddCountString(`${newCount}`)); expect(spy).toHaveBeenCalledTimes(2); }); it('should be able to dispose by other actions', () => { const newCount = 100; actionsDispatcher.asyncAddCount(newCount); expect(spy).toHaveBeenCalledTimes(1); actionsDispatcher.dispose$(); timer.advanceTimersByTime(TIME_TO_DELAY); expect(spy).toHaveBeenCalledTimes(2); expect(store.state).toStrictEqual(counter.defaultState); }); it('should be able to dispatch noop action', () => { const action = actionsDispatcher.effectWithPureSideEffect(); expect(spy).toHaveBeenCalledTimes(1); const [[arg1]] = spy.mock.calls; expect(arg1).toStrictEqual(action); expect(store.state).toStrictEqual(counter.defaultState); }); it('should be able to dispatch reset action', () => { actionsDispatcher.addOne(); expect(store.state.count).toBe(counter.defaultState.count + 1); actionsDispatcher.effectToResetState(); expect(spy).toHaveBeenCalledTimes(3); expect(store.state.count).toBe(counter.defaultState.count); }); it('should follow rxjs flow control #switchMap', () => { actionsDispatcher.cancellableEffect(); actionsDispatcher.cancellableEffect(); timer.advanceTimersByTime(TIME_TO_DELAY); expect(spy).toHaveBeenCalledTimes(5); expect(spy.mock.calls.map(([{ type }]) => type)).toEqual([ 'cancellableEffect', 'setCount', 'cancellableEffect', 'setCount', 'addOne', ]); }); it('should follow rxjs flow control #exhaustMap', () => { actionsDispatcher.exhaustEffect(); actionsDispatcher.exhaustEffect(); timer.advanceTimersByTime(TIME_TO_DELAY); expect(spy).toHaveBeenCalledTimes(4); expect(spy.mock.calls.map(([{ type }]) => type)).toEqual(['exhaustEffect', 'setCount', 'exhaustEffect', 'addOne']); }); }); }); //# sourceMappingURL=effect-module.spec.js.map