@sigi/core
Version:
Sigi core library
434 lines • 17.7 kB
JavaScript
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