@brighte/redux-saga-test-plan
Version:
Test Redux Saga with an easy plan
318 lines (263 loc) • 8.3 kB
JavaScript
// @flow
import isEqual from 'lodash.isequal';
import assign from 'object-assign';
import * as effects from 'redux-saga/effects';
import { eventChannel } from 'redux-saga';
import { ARGUMENT, ERROR, NONE, FINISH, FINISH_ARGUMENT } from './historyTypes';
import {
ALL,
ACTION_CHANNEL,
CALL,
CANCEL,
CANCELLED,
CPS,
FLUSH,
FORK,
GET_CONTEXT,
JOIN,
PUT,
RACE,
SELECT,
SET_CONTEXT,
TAKE,
} from '../shared/keys';
import SagaTestError from '../shared/SagaTestError';
import createErrorMessage from './createErrorMessage';
import assertSameEffect from './assertSameEffect';
export default function testSaga(saga: Function, ...sagaArgs: Array<any>): Api {
const api = {
next,
back,
finish,
restart,
save,
restore,
throw: throwError,
};
const savePoints: SavePoints = {};
let history: Array<HistoryItem> = [];
let finalSagaArgs = sagaArgs;
let iterator = createIterator();
function createEffectTester(
name: string,
key?: string,
effect: Function,
isForkedEffect: boolean = false,
): EffectTesterCreator {
return yieldedValue => (...args) => {
assertSameEffect(
eventChannel,
name,
key,
isForkedEffect,
yieldedValue,
effect(...args),
history.length,
);
return api;
};
}
function createEffectTesterFromEffects(
name: string,
key: string,
): EffectTesterCreator {
return createEffectTester(name, key, effects[name]);
}
function createEffectTesterFromHelperEffect(
name: string,
): EffectTesterCreator {
return createEffectTester(name, undefined, effects[name], true);
}
const effectsTestersCreators: EffectTestersCreator = {
actionChannel: createEffectTesterFromEffects(
'actionChannel',
ACTION_CHANNEL,
),
all: createEffectTesterFromEffects('all', ALL),
apply: createEffectTesterFromEffects('apply', CALL),
call: createEffectTesterFromEffects('call', CALL),
cancel: createEffectTesterFromEffects('cancel', CANCEL),
cancelled: createEffectTesterFromEffects('cancelled', CANCELLED),
cps: createEffectTesterFromEffects('cps', CPS),
delay: createEffectTesterFromEffects('delay', CALL),
flush: createEffectTesterFromEffects('flush', FLUSH),
fork: createEffectTesterFromEffects('fork', FORK),
getContext: createEffectTesterFromEffects('getContext', GET_CONTEXT),
join: createEffectTesterFromEffects('join', JOIN),
put: createEffectTesterFromEffects('put', PUT),
putResolve: createEffectTesterFromEffects('putResolve', PUT),
race: createEffectTesterFromEffects('race', RACE),
select: createEffectTesterFromEffects('select', SELECT),
setContext: createEffectTesterFromEffects('setContext', SET_CONTEXT),
spawn: createEffectTesterFromEffects('spawn', FORK),
take: createEffectTesterFromEffects('take', TAKE),
takeEvery: createEffectTesterFromHelperEffect('takeEvery'),
takeLatest: createEffectTesterFromHelperEffect('takeLatest'),
takeLeading: createEffectTesterFromHelperEffect('takeLeading'),
takeMaybe: createEffectTesterFromEffects('takeMaybe', TAKE),
throttle: createEffectTesterFromHelperEffect('throttle'),
isDone: done => () => {
if (!done) {
throw new SagaTestError('saga not done');
}
return api;
},
is: value => arg => {
if (!isEqual(arg, value)) {
const errorMessage = createErrorMessage(
'yielded values do not match',
history.length,
value,
arg,
);
throw new SagaTestError(errorMessage);
}
return api;
},
inspect: value => fn => {
fn(value);
return api;
},
returns: (value, done) => arg => {
if (!done) {
throw new SagaTestError('saga not done');
}
if (!isEqual(arg, value)) {
const errorMessage = createErrorMessage(
'returned values do not match',
history.length,
value,
arg,
);
throw new SagaTestError(errorMessage);
}
return api;
},
};
function createIterator(): Generator<*, *, *> {
return saga(...finalSagaArgs);
}
function apiWithEffectsTesters({
value,
done,
}: IteratorResult<*, *>): ApiWithEffectsTesters {
const newApi = assign({}, api, {
actionChannel: effectsTestersCreators.actionChannel(value),
all: effectsTestersCreators.all(value),
apply: effectsTestersCreators.apply(value),
call: effectsTestersCreators.call(value),
cancel: effectsTestersCreators.cancel(value),
cancelled: effectsTestersCreators.cancelled(value),
cps: effectsTestersCreators.cps(value),
delay: effectsTestersCreators.delay(value),
flush: effectsTestersCreators.flush(value),
fork: effectsTestersCreators.fork(value),
getContext: effectsTestersCreators.getContext(value),
join: effectsTestersCreators.join(value),
put: effectsTestersCreators.put(value),
putResolve: effectsTestersCreators.putResolve(value),
race: effectsTestersCreators.race(value),
select: effectsTestersCreators.select(value),
setContext: effectsTestersCreators.setContext(value),
spawn: effectsTestersCreators.spawn(value),
take: effectsTestersCreators.take(value),
takeEvery: effectsTestersCreators.takeEvery(value),
takeLatest: effectsTestersCreators.takeLatest(value),
takeLeading: effectsTestersCreators.takeLeading(value),
takeMaybe: effectsTestersCreators.takeMaybe(value),
throttle: effectsTestersCreators.throttle(value),
is: effectsTestersCreators.is(value),
inspect: effectsTestersCreators.inspect(value),
isDone: effectsTestersCreators.isDone(done),
returns: effectsTestersCreators.returns(value, done),
});
return newApi;
}
function restart(...args: Array<any>): Api {
if (args.length > 0) {
finalSagaArgs = args;
}
history = [];
iterator = createIterator();
return api;
}
function next(...args: Array<any>): ApiWithEffectsTesters {
const arg = args[0];
let result;
if (args.length === 0) {
history.push(({ type: NONE }: HistoryItemNone));
result = iterator.next();
} else {
history.push(({ type: ARGUMENT, value: arg }: HistoryItemArgument));
result = iterator.next(arg);
}
return apiWithEffectsTesters(result);
}
function finish(...args: Array<any>): ApiWithEffectsTesters {
const arg = args[0];
let result;
if (args.length === 0) {
history.push(({ type: FINISH }: HistoryItemFinish));
result = iterator.return();
} else {
history.push(
({ type: FINISH_ARGUMENT, value: arg }: HistoryItemFinishArgument),
);
result = iterator.return(arg);
}
return apiWithEffectsTesters(result);
}
function throwError(error: Error): ApiWithEffectsTesters {
history.push(({ type: ERROR, value: error }: HistoryItemError));
const result = iterator.throw(error);
return apiWithEffectsTesters(result);
}
function restore(name: string): Api {
if (!savePoints[name]) {
throw new Error(`No such save point ${name}`);
}
iterator = createIterator();
history = savePoints[name];
return applyHistory();
}
function back(n: number = 1): Api {
if (n > history.length) {
throw new Error('Cannot go back any further');
}
let m = n;
while (m--) {
history.pop();
}
iterator = createIterator();
return applyHistory();
}
function save(name: string): Api {
savePoints[name] = history.slice(0);
return api;
}
function applyHistory(): Api {
for (let i = 0, l = history.length; i < l; i++) {
const arg = history[i];
switch (arg.type) {
case NONE:
iterator.next();
break;
case ARGUMENT:
iterator.next(arg.value);
break;
case ERROR:
iterator.throw(arg.value);
break;
case FINISH:
iterator.return();
break;
case FINISH_ARGUMENT:
iterator.return(arg.value);
break;
// no default
}
}
return api;
}
return api;
}