gambit
Version:
A hyper-thin library to help building API driven redux apps
299 lines (252 loc) • 9.51 kB
JavaScript
/* eslint-env mocha */
import { expect } from 'chai';
import Promise from 'bluebird';
import {
hasNotBeenCalled,
hasNotBeenCalledIn,
dependableMatrix,
createStagedAction,
createSimpleAction,
bindCreators,
} from '../../src';
import dispatchCreator from '../lib/dispatch';
import asyncThrow from '../lib/asyncThrow';
const fakeBody = { body: { id: 1 } };
const fakeError = { response: { body: 'You failed' } };
const fakeApi = {
findUser: () => {
return new Promise(resolve => {
resolve(fakeBody);
});
},
brokenFindUser: () => {
return new Promise((_, reject) => {
reject(fakeError);
});
},
};
const { dispatch, last, lastId, clearStack, clearState } = dispatchCreator(fakeApi);
const SOME_ACTION = 'SOME_ACTION';
const SOME_ACTION_DONE = 'SOME_ACTION_DONE';
const SOME_ACTION_FAILED = 'SOME_ACTION_FAILED';
const SOME_ACTION_STARTING = 'SOME_ACTION_STARTING';
beforeEach(() => {
clearStack();
clearState();
});
describe('Creating Actions', () => {
describe('Basic action creation', () => {
it('should let me create an action using createSimpleAction', () => {
const action = createSimpleAction(SOME_ACTION);
dispatch(action({ foo: 'bar' }));
expect(last().type).to.equal(SOME_ACTION_DONE);
});
it('should let me create an action using createStagedAction', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postStart: () => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_STARTING);
resolve();
}),
postDone: () => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_DONE);
resolve();
}),
});
await dispatch(action());
});
});
describe('Validation', () => {
it('should be a dick if you give an invalid constant', () => {
const action = createStagedAction(undefined, api => api.findUser);
expect(
() => dispatch(action())
).to.throw(Error);
});
it('should be a dick if you give an invalid api search', async () => {
const action = createStagedAction(SOME_ACTION, api => api.imNotHere);
await asyncThrow(() => {
return dispatch(action());
});
});
});
describe('Posts', () => {
it('should call post start after start', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postStart: () => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_STARTING);
resolve();
}),
});
await dispatch(action());
});
it('should call post start with named arguments', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postStart: (_, namedArguments) => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_STARTING);
expect(namedArguments.id).to.equal(999);
resolve();
}),
});
await dispatch(action({ id: 999 }));
});
it('should maintain type even when type parameter is passed', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postStart: (_, namedArguments) => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_STARTING);
resolve();
}),
});
return dispatch(action({ type: 'DOG_CHECKER' }));
});
it('should call post done after done', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postDone: () => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_DONE);
resolve();
}),
});
await dispatch(action());
});
it('should call post done with named arguments', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postDone: (x, y, namedArguments) => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_DONE);
expect(namedArguments.id).to.equal(999);
resolve();
}),
});
await dispatch(action({ id: 999 }));
});
it('should call post fail after fail', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postDone: () => new Promise((res, rej) => rej(new Error('bla'))),
postFail: () => new Promise(resolve => {
expect(last().type).to.equal(SOME_ACTION_FAILED);
}),
});
try {
await dispatch(action());
} catch (e) { return; }
});
it('should call fail with named arguments', async () => {
const action = createStagedAction(SOME_ACTION, api => api.findUser, {
postDone: () => new Promise((res, rej) => rej(new Error('bla'))),
postFail: (_, error, args) => new Promise(resolve => {
expect(args.id).to.equal(999);
}),
});
try {
await dispatch(action({ id: 999 }));
} catch (e) { return; }
});
});
describe('Bind Creators', () => {
const constants = { FATHER: 'FATHER_TED' };
it('should let me bind a constant set so I don\'t', async () => {
const { createStagedAction: boundCreateStagedAction } = bindCreators(constants);
const action = boundCreateStagedAction('FATHER', api => api.findUser);
await dispatch(action());
expect(last().type).to.equal('FATHER_TED_DONE');
});
});
describe('Staged Actions', () => {
describe('IDS', () => {
const idAction = createStagedAction(
SOME_ACTION,
api => api.findUser,
{
id: (constant, { userId }) => `${constant}_${userId}`,
}
);
const nonIdAction = createStagedAction(SOME_ACTION, api => api.findUser);
it('should correctly form an id', async () => {
await dispatch(idAction({ userId: 1 }));
expect(lastId()).to.equal(`${SOME_ACTION}_1`);
});
it('should use the constant name as an id where none present', async () => {
await dispatch(nonIdAction());
expect(lastId()).to.equal(SOME_ACTION);
});
it('should chuck an invariant when the id is bad', () => {
expect(() => dispatch(idAction())).to.throw(Error);
});
it('should block an action if previously called', async () => {
await dispatch(nonIdAction());
expect(last().type).to.equal(SOME_ACTION_DONE);
clearStack();
await dispatch(nonIdAction({}, hasNotBeenCalled));
expect(last()).to.equal(undefined);
});
it('should block an action if previously called within X minutes', async () => {
await dispatch(nonIdAction());
expect(last().type).to.equal(SOME_ACTION_DONE);
clearStack();
await dispatch(nonIdAction({}, hasNotBeenCalledIn(5, 'minutes')));
expect(last()).to.equal(undefined);
});
it('should not block if previously called further than the time gap', async function () {
this.timeout(3000);
await dispatch(nonIdAction());
expect(last().type).to.equal(SOME_ACTION_DONE);
clearStack();
await new Promise(r => setTimeout(r, 1001));
await dispatch(nonIdAction({}, hasNotBeenCalledIn(1, 'second')));
expect(last().type).to.equal(SOME_ACTION_DONE);
});
it('should block an id\'d action if previously called', async () => {
await dispatch(idAction({ userId: 2 }));
expect(last().type).to.equal(SOME_ACTION_DONE);
clearStack();
await dispatch(idAction({ userId: 2 }, hasNotBeenCalled));
expect(last()).to.equal(undefined);
});
it('should not block and id action with a differnet id', async () => {
await dispatch(idAction({ userId: 1 }));
expect(last().type).to.equal(SOME_ACTION_DONE);
clearStack();
await dispatch(idAction({ userId: 2 }, hasNotBeenCalled));
expect(last().type).to.equal(SOME_ACTION_DONE);
});
});
});
describe('Dependable Matrix', async () => {
const SOME_OTHER_ACTION = 'SOME_OTHER_ACTION';
const firstAction = createStagedAction(
SOME_ACTION,
() => () => true,
);
const secondAction = createStagedAction(
SOME_OTHER_ACTION,
() => () => true,
);
it('should call the method if the resetting action has been called', async () => {
const matrix = dependableMatrix(
[SOME_OTHER_ACTION]
);
await dispatch(firstAction({}, matrix));
await dispatch(secondAction({}));
await dispatch(firstAction({}, matrix));
expect(last().type).to.equal(SOME_ACTION_DONE);
});
it('should call if the reset action has not been called but no blocker', async () => {
const matrix = dependableMatrix(
[SOME_OTHER_ACTION]
);
await dispatch(firstAction({}, matrix));
expect(last().type).to.equal(SOME_ACTION_DONE);
await dispatch(firstAction({}, matrix));
expect(last().type).to.equal(SOME_ACTION_DONE);
});
it('shouldnt call if the reset action has not been called and otherwise blocker', async () => {
const matrix = dependableMatrix(
[SOME_OTHER_ACTION],
hasNotBeenCalled,
);
await dispatch(firstAction({}, matrix));
expect(last().type).to.equal(SOME_ACTION_DONE);
clearStack();
await dispatch(firstAction({}, matrix));
expect(last()).to.equal(undefined);
});
});
});