Skip to content

Commit

Permalink
feat: add action creators as the last argument in model effects
Browse files Browse the repository at this point in the history
  • Loading branch information
x8lucas8x committed Sep 21, 2019
1 parent e1d9bb0 commit d0a21b4
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 77 deletions.
122 changes: 110 additions & 12 deletions __tests__/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,100 @@ import {Model} from '../src';
import {sagaEffects} from '../src/model';

describe('Model', () => {
let modelOptions;
let articleModel;

beforeAll(() => {
articleModel = new Model({
modelOptions = {
namespace: 'articles',
state: {},
};
articleModel = new Model(modelOptions);
});

describe('constructor', () => {
it('throws when namespace is not a string', () => {
expect(() => new Model({
...modelOptions,
namespace: [],
})).toThrow({
name: '',
message: 'Namespace must be a string. The provided namespace type was: object'
});
});

it('throws when namespace is empty', () => {
expect(() => new Model({
...modelOptions,
namespace: '',
})).toThrow({
name: '',
message: 'Namespace must be a non empty string.'
});
});

it('throws when a reducer and effect have the same action type', () => {
expect(() => new Model({
...modelOptions,
reducers: {
whatever: jest.fn(),
},
effects: {
whatever: jest.fn(),
},
})).toThrow({
name: '',
message: 'Reducer and effect action types must be unique in [articles] model. ' +
'The provided reducer/effect action types were: whatever, whatever'
});
});
});

describe('namespace', () => {
it('returns the namespace', () => {
expect(articleModel.namespace).toEqual('articles');
expect(articleModel.namespace).toEqual(modelOptions.namespace);
});
});

describe('state', () => {
it('returns the state', () => {
expect(articleModel.state).toEqual(modelOptions.state);
});
});

describe('selectors', () => {
it('returns an empty object when none is available', () => {
expect(articleModel.selectors).toEqual({});
});

it('returns the selectors', () => {
const modelOptionsWithSelectors = {...modelOptions, selectors: {}};
const model = new Model(modelOptionsWithSelectors);
expect(model.selectors).toEqual(modelOptionsWithSelectors.selectors);
});
});

describe('reducers', () => {
it('returns an empty object when none is available', () => {
expect(articleModel.state).toEqual({});
});

it('returns the reducers', () => {
const modelOptionsWithReducers = {...modelOptions, reducers: {}};
const model = new Model(modelOptionsWithReducers);
expect(model.reducers).toEqual(modelOptionsWithReducers.reducers);
});
});

describe('effects', () => {
it('returns an empty object when none is available', () => {
expect(articleModel.effects).toEqual({});
});

it('returns the effects', () => {
const modelOptionsWithEffects = {...modelOptions, effects: {}};
const model = new Model(modelOptionsWithEffects);
expect(model.effects).toEqual(modelOptionsWithEffects.effects);
});
});

Expand All @@ -36,6 +118,13 @@ describe('Model', () => {
{...actionData, type: articleModel.actionType('ola')}
);
});

it('throws when action data is not a plain obeject', () => {
expect(() => articleModel.actionCreator('ola', [])).toThrow({
name: '',
message: 'Action data must be a plain object, when calling action [ola] in [articles] model.'
});
});
});

describe('actionCreators', () => {
Expand Down Expand Up @@ -109,7 +198,7 @@ describe('Model', () => {
});
});

describe('selectors', () => {
describe('modelSelectors', () => {
it('returns an empty object when no selectors exists', () => {
expect(articleModel.actionCreators()).toEqual({});
});
Expand All @@ -125,7 +214,7 @@ describe('Model', () => {
selectA: selectASpy,
},
});
const selectors = modelX.selectors();
const selectors = modelX.modelSelectors();

it('returns an entry for the provided selector', () => {
expect(selectors).toEqual(
Expand All @@ -144,9 +233,9 @@ describe('Model', () => {
});
});

describe('reducers', () => {
describe('modelReducers', () => {
it('returns a non nil value when no reducers exist', () => {
expect(articleModel.reducers()).toEqual(expect.anything());
expect(articleModel.modelReducers()).toEqual(expect.anything());
});

describe('when reducers are present', () => {
Expand All @@ -161,7 +250,7 @@ describe('Model', () => {
reducerA: reducerASpy,
},
});
const reducers = modelX.reducers();
const reducers = modelX.modelReducers();
const action = modelX.actionCreator('reducerA');

it('calls reducer func when reducer entry is called', () => {
Expand All @@ -175,9 +264,9 @@ describe('Model', () => {
});
});

describe('effects', () => {
describe('modelEffects', () => {
it('returns an empty object when no effects exists', () => {
expect(articleModel.effects()).toEqual({});
expect(articleModel.modelEffects()).toEqual({});
});

describe('when effects are present', () => {
Expand All @@ -191,7 +280,13 @@ describe('Model', () => {
effectA: effectASpy,
},
});
const effects = modelX.effects();
let actionCreatorsSpy;
let effects;

beforeEach(() => {
actionCreatorsSpy = jest.spyOn(modelX, 'actionCreators');
effects = modelX.modelEffects();
});

it('returns an entry for the provided effect', () => {
expect(effects).toEqual(
Expand All @@ -201,7 +296,9 @@ describe('Model', () => {

it('calls effect func when selector entry is called', () => {
effects.effectA({userId: 1});
expect(effectASpy).toHaveBeenCalledWith({userId: 1}, sagaEffects);
expect(effectASpy).toHaveBeenCalledWith(
{userId: 1}, sagaEffects, actionCreatorsSpy.mock.results[0].value,
);
});

it('returns result of effect func', () => {
Expand All @@ -227,6 +324,7 @@ describe('Model', () => {
effectA: effectASpy,
},
});
const actionCreatorsSpy = jest.spyOn(modelX, 'actionCreators');
const actionData = {userId: 1};

beforeEach(() => {
Expand All @@ -248,7 +346,7 @@ describe('Model', () => {
it('calls effectASpy with the right arguments', () => {
gen.next().value.payload.args[1](actionData, sagaEffects);
expect(effectASpy).toHaveBeenCalledWith(
actionData, sagaEffects,
actionData, sagaEffects, actionCreatorsSpy.mock.results[0].value,
);
});
});
Expand Down
26 changes: 13 additions & 13 deletions __tests__/redux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,30 +27,30 @@ describe('resuxRootSaga', () => {

describe('combineModelReducers', () => {
let articleModel;
let reducersSpy;
let modelReducersSpy;
let result;

beforeEach(() => {
articleModel = new Model({
namespace: 'articles',
state: {},
});
reducersSpy = jest.spyOn(articleModel, 'reducers').mockImplementation(
modelReducersSpy = jest.spyOn(articleModel, 'modelReducers').mockImplementation(
// Implements an identity reducer
() => (data) => data
);
result = combineModelReducers([articleModel]);
});

it('calls reducers in article model', () => {
expect(reducersSpy).toHaveBeenCalled();
it('calls modelReducers in article model', () => {
expect(modelReducersSpy).toHaveBeenCalled();
});

it('returns a reducer mapping object with the reducers of the article model', () => {
expect(result).toEqual({[articleModel.namespace]: reducersSpy.mock.results[0].value});
expect(result).toEqual({[articleModel.namespace]: modelReducersSpy.mock.results[0].value});
});

it('throws when multiple modals have the same namespace', () => {
it('throws when multiple models have the same namespace', () => {
expect(() => {
combineModelReducers([articleModel, articleModel])
}).toThrow({
Expand All @@ -63,7 +63,7 @@ describe('combineModelReducers', () => {

describe('connectResuxImpl', () => {
let articleModel;
let selectorsSpy;
let modelSelectorsSpy;
let actionCreatorsSpy;
let mapDispatchToPropsSpy;

Expand All @@ -72,20 +72,20 @@ describe('connectResuxImpl', () => {
namespace: 'articles',
state: {},
});
selectorsSpy = jest.spyOn(articleModel, 'selectors').mockImplementation(
// Implements an identity reducer
modelSelectorsSpy = jest.spyOn(articleModel, 'modelSelectors').mockImplementation(
// Implements an identity selector
() => (data) => data
);
actionCreatorsSpy = jest.spyOn(articleModel, 'actionCreators').mockImplementation(
// Implements an identity reducer
// Implements an identity action creator
() => (data) => data
);
mapDispatchToPropsSpy = jest.fn();
});

it('calls selectors in article model', () => {
it('calls modelSelectors in article model', () => {
connectResuxImpl([articleModel]);
expect(selectorsSpy).toHaveBeenCalled();
expect(modelSelectorsSpy).toHaveBeenCalled();
});

it('returns a list with two items', () => {
Expand All @@ -100,7 +100,7 @@ describe('connectResuxImpl', () => {
beforeEach(() => {
subscriberA = new Subscriber([articleModel]);
subscriberActionCreatorSpy = jest.spyOn(subscriberA, 'actionCreators').mockImplementation(
// Implements an identity reducer
// Implements an identity action creator
() => (data) => data
);
});
Expand Down
17 changes: 14 additions & 3 deletions src/baseTypes.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import {AnyAction, Dispatch} from 'redux';
import {Action, ActionCreatorsMapObject} from "react-redux";

type SelectorFunction = (state: object, ...arguments: any[]) => any;
type ReducerFunction = (state: object, action: AnyAction) => void;
type EffectFunction = (state: object, ...arguments: any[]) => any;
type SelectorFunction = (state: State, ...arguments: any[]) => any;
type ReducerFunction = (state: State, action: AnyAction) => void;
type EffectFunction = (actionData: object, sagaEffects: any, actionCreators: ActionCreatorsMapObject) => any;

type State = any;
type SelectorMap = Record<string, SelectorFunction>;
type ReducerMap = Record<string, ReducerFunction>;
type EffectMap = Record<string, EffectFunction>;

type SelectorModelFunction = (state: State, ...arguments: any[]) => any;
type EffectModelFunction = (actionData: object) => any;

type SelectorModelMap = Record<string, SelectorModelFunction>;
type EffectModelMap = Record<string, EffectModelFunction>;

type MapDispatchToPropsWithActionCreatorsFunction<TDispatchProps, TOwnProps> =
(
Expand Down
Loading

0 comments on commit d0a21b4

Please sign in to comment.