UNPKG

@brighte/redux-saga-test-plan

Version:
719 lines (539 loc) 21.3 kB
'use strict'; exports.__esModule = true; var _exposableEffects; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /* eslint-disable no-underscore-dangle */ exports.default = expectSaga; var _reduxSaga = require('redux-saga'); var _is = require('@redux-saga/is'); var is = _interopRequireWildcard(_is); var _effects = require('redux-saga/effects'); var effects = _interopRequireWildcard(_effects); var _objectAssign = require('object-assign'); var _objectAssign2 = _interopRequireDefault(_objectAssign); var _array = require('../utils/array'); var _Map = require('../utils/Map'); var _Map2 = _interopRequireDefault(_Map); var _ArraySet = require('../utils/ArraySet'); var _ArraySet2 = _interopRequireDefault(_ArraySet); var _logging = require('../utils/logging'); var _async = require('../utils/async'); var _identity = require('../utils/identity'); var _identity2 = _interopRequireDefault(_identity); var _parseEffect2 = require('./parseEffect'); var _parseEffect3 = _interopRequireDefault(_parseEffect2); var _provideValue = require('./provideValue'); var _object = require('../utils/object'); var _findDispatchableActionIndex = require('./findDispatchableActionIndex'); var _findDispatchableActionIndex2 = _interopRequireDefault(_findDispatchableActionIndex); var _sagaWrapper = require('./sagaWrapper'); var _sagaWrapper2 = _interopRequireDefault(_sagaWrapper); var _sagaIdFactory = require('./sagaIdFactory'); var _sagaIdFactory2 = _interopRequireDefault(_sagaIdFactory); var _helpers = require('./providers/helpers'); var _asEffect = require('../utils/asEffect'); var _expectations = require('./expectations'); var _keys = require('../shared/keys'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } var all = effects.all, call = effects.call, fork = effects.fork, race = effects.race, spawn = effects.spawn; var INIT_ACTION = { type: '@@redux-saga-test-plan/INIT' }; var defaultSagaWrapper = (0, _sagaWrapper2.default)(); function extractState(reducer, initialState) { return initialState || reducer(undefined, INIT_ACTION); } function _toJSON(object) { if (Array.isArray(object)) { return object.map(_toJSON); } if (typeof object === 'function') { return '@@redux-saga-test-plan/json/function/' + (object.name || '<anonymous>'); } if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) === 'object' && object !== null) { return (0, _object.mapValues)(object, _toJSON); } return object; } function lacksSagaWrapper(value) { var _parseEffect = (0, _parseEffect3.default)(value), type = _parseEffect.type, effect = _parseEffect.effect; return type !== 'FORK' || !(0, _sagaWrapper.isSagaWrapper)(effect.fn); } var exposableEffects = (_exposableEffects = {}, _exposableEffects[_keys.TAKE] = 'take', _exposableEffects[_keys.PUT] = 'put', _exposableEffects[_keys.RACE] = 'race', _exposableEffects[_keys.CALL] = 'call', _exposableEffects[_keys.CPS] = 'cps', _exposableEffects[_keys.FORK] = 'fork', _exposableEffects[_keys.GET_CONTEXT] = 'getContext', _exposableEffects[_keys.SELECT] = 'select', _exposableEffects[_keys.SET_CONTEXT] = 'setContext', _exposableEffects[_keys.ACTION_CHANNEL] = 'actionChannel', _exposableEffects); function expectSaga(generator) { var _effectStores; for (var _len = arguments.length, sagaArgs = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { sagaArgs[_key - 1] = arguments[_key]; } var allEffects = []; var effectStores = (_effectStores = {}, _effectStores[_keys.TAKE] = new _ArraySet2.default(), _effectStores[_keys.PUT] = new _ArraySet2.default(), _effectStores[_keys.RACE] = new _ArraySet2.default(), _effectStores[_keys.CALL] = new _ArraySet2.default(), _effectStores[_keys.CPS] = new _ArraySet2.default(), _effectStores[_keys.FORK] = new _ArraySet2.default(), _effectStores[_keys.GET_CONTEXT] = new _ArraySet2.default(), _effectStores[_keys.SET_CONTEXT] = new _ArraySet2.default(), _effectStores[_keys.SELECT] = new _ArraySet2.default(), _effectStores[_keys.ACTION_CHANNEL] = new _ArraySet2.default(), _effectStores); var expectations = []; var ioChannel = (0, _reduxSaga.stdChannel)(); var queuedActions = []; var forkedTasks = []; var outstandingForkEffects = new _Map2.default(); var outstandingActionChannelEffects = new _Map2.default(); var channelsToPatterns = new _Map2.default(); var dispatchPromise = Promise.resolve(); var nextSagaId = (0, _sagaIdFactory2.default)(); var stopDirty = false; var negateNextAssertion = false; var isRunning = false; var delayTime = null; var iterator = void 0; var mainTask = void 0; var mainTaskPromise = void 0; var providers = void 0; var returnValue = void 0; var storeState = void 0; function setReturnValue(value) { returnValue = value; } function useProvidedValue(value) { function addEffect() { // Because we are providing a return value and not hitting redux-saga, we // need to manually store the effect so assertions on the effect work. processEffect({ effectId: nextSagaId(), effect: value }); } try { var providedValue = (0, _provideValue.provideValue)(providers, value); if (providedValue === _provideValue.NEXT) { return value; } addEffect(); return providedValue; } catch (e) { addEffect(); throw e; } } function refineYieldedValue(value) { var parsedEffect = (0, _parseEffect3.default)(value); var localProviders = providers || {}; var type = parsedEffect.type, effect = parsedEffect.effect; switch (true) { case type === _keys.RACE && !localProviders.race: processEffect({ effectId: nextSagaId(), effect: value }); return race(parsedEffect.mapEffects(refineYieldedValue)); case type === _keys.ALL && !localProviders.all: return all(parsedEffect.mapEffects(refineYieldedValue)); case type === _keys.FORK: { var args = effect.args, detached = effect.detached, context = effect.context, fn = effect.fn; var providedValue = useProvidedValue(value); var isProvided = providedValue !== value; if (!detached && !isProvided) { // Because we wrap the `fork`, we need to manually store the effect, // so assertions on the `fork` work. processEffect({ effectId: nextSagaId(), effect: value }); var finalArgs = args; return fork((0, _sagaWrapper2.default)(fn.name), fn.apply(context, finalArgs), refineYieldedValue); } if (detached && !isProvided) { // Because we wrap the `spawn`, we need to manually store the effect, // so assertions on the `spawn` work. processEffect({ effectId: nextSagaId(), effect: value }); return spawn((0, _sagaWrapper2.default)(fn.name), fn.apply(context, args), refineYieldedValue); } return providedValue; } case type === _keys.CALL: { var _providedValue = useProvidedValue(value); if (_providedValue !== value) { return _providedValue; } // Because we manually consume the `call`, we need to manually store // the effect, so assertions on the `call` work. processEffect({ effectId: nextSagaId(), effect: value }); var _context = effect.context, _fn = effect.fn, _args = effect.args; var result = _fn.apply(_context, _args); if (is.iterator(result)) { return call(defaultSagaWrapper, result, refineYieldedValue); } return result; } // Ensure we wrap yielded iterators (i.e. `yield someInnerSaga()`) for // providers to work. case is.iterator(value): return useProvidedValue(defaultSagaWrapper(value, refineYieldedValue)); default: return useProvidedValue(value); } } function defaultReducer() { var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : storeState; return state; } var reducer = defaultReducer; function getAllPromises() { return new Promise(function (resolve) { Promise.all([].concat(forkedTasks.map(taskPromise), [mainTaskPromise])).then(function () { if (stopDirty) { stopDirty = false; resolve(getAllPromises()); } resolve(); }); }); } function addForkedTask(task) { stopDirty = true; forkedTasks.push(task); } function cancelMainTask(timeout, silenceTimeout, timedOut) { if (!silenceTimeout && timedOut) { (0, _logging.warn)('Saga exceeded async timeout of ' + timeout + 'ms'); } mainTask.cancel(); return mainTaskPromise; } function scheduleStop(timeout) { var promise = (0, _async.schedule)(getAllPromises).then(function () { return false; }); var silenceTimeout = false; var timeoutLength = void 0; if (typeof timeout === 'number') { timeoutLength = timeout; } else if ((typeof timeout === 'undefined' ? 'undefined' : _typeof(timeout)) === 'object') { silenceTimeout = timeout.silenceTimeout === true; if ('timeout' in timeout) { timeoutLength = timeout.timeout; } else { timeoutLength = expectSaga.DEFAULT_TIMEOUT; } } if (typeof timeoutLength === 'number') { promise = Promise.race([promise, (0, _async.delay)(timeoutLength).then(function () { return true; })]); } return promise.then(function (timedOut) { return (0, _async.schedule)(cancelMainTask, [timeoutLength, silenceTimeout, timedOut]); }); } function queueAction(action) { queuedActions.push(action); } function notifyListeners(action) { ioChannel.put(action); } function dispatch(action) { if (typeof action._delayTime === 'number') { var _delayTime = action._delayTime; dispatchPromise.then(function () { return (0, _async.delay)(_delayTime); }).then(function () { storeState = reducer(storeState, action); notifyListeners(action); }); } else { storeState = reducer(storeState, action); dispatchPromise.then(function () { return notifyListeners(action); }); } } function associateChannelWithPattern(channel, pattern) { channelsToPatterns.set(channel, pattern); } function getDispatchableActions(effect) { var pattern = effect.pattern || channelsToPatterns.get(effect.channel); var index = (0, _findDispatchableActionIndex2.default)(queuedActions, pattern); if (index > -1) { var actions = queuedActions.splice(0, index + 1); return actions; } return []; } function processEffect(event) { var parsedEffect = (0, _parseEffect3.default)(event.effect); // Using string literal for flow if (parsedEffect.type === 'NONE') { return; } var effectStore = effectStores[parsedEffect.type]; if (!effectStore) { return; } allEffects.push(event.effect); effectStore.add(event.effect); switch (parsedEffect.type) { case _keys.FORK: { outstandingForkEffects.set(event.effectId, parsedEffect.effect); break; } case _keys.TAKE: { var actions = getDispatchableActions(parsedEffect.effect); var _splitAt = (0, _array.splitAt)(actions, -1), reducerActions = _splitAt[0], _splitAt$ = _splitAt[1], sagaAction = _splitAt$[0]; reducerActions.forEach(function (action) { dispatch(action); }); if (sagaAction) { dispatch(sagaAction); } break; } case _keys.ACTION_CHANNEL: { outstandingActionChannelEffects.set(event.effectId, parsedEffect.effect); break; } // no default } } function addExpectation(expectation) { expectations.push(expectation); } var io = { dispatch: dispatch, channel: ioChannel, getState: function getState() { return storeState; }, sagaMonitor: { effectTriggered: function effectTriggered(event) { processEffect(event); }, effectResolved: function effectResolved(effectId, value) { var forkEffect = outstandingForkEffects.get(effectId); if (forkEffect) { addForkedTask(value); return; } var actionChannelEffect = outstandingActionChannelEffects.get(effectId); if (actionChannelEffect) { associateChannelWithPattern(value, actionChannelEffect.pattern); } }, effectRejected: function effectRejected() {}, effectCancelled: function effectCancelled() {} } }; var api = { run: run, silentRun: silentRun, withState: withState, withReducer: withReducer, provide: provide, returns: returns, hasFinalState: hasFinalState, dispatch: apiDispatch, delay: apiDelay, // $FlowFixMe get not() { negateNextAssertion = true; return api; }, actionChannel: createEffectTesterFromEffects('actionChannel', _keys.ACTION_CHANNEL, _asEffect.asEffect.actionChannel), apply: createEffectTesterFromEffects('apply', _keys.CALL, _asEffect.asEffect.call), call: createEffectTesterFromEffects('call', _keys.CALL, _asEffect.asEffect.call), cps: createEffectTesterFromEffects('cps', _keys.CPS, _asEffect.asEffect.cps), fork: createEffectTesterFromEffects('fork', _keys.FORK, _asEffect.asEffect.fork), getContext: createEffectTesterFromEffects('getContext', _keys.GET_CONTEXT, _asEffect.asEffect.getContext), put: createEffectTesterFromEffects('put', _keys.PUT, _asEffect.asEffect.put), putResolve: createEffectTesterFromEffects('putResolve', _keys.PUT, _asEffect.asEffect.put), race: createEffectTesterFromEffects('race', _keys.RACE, _asEffect.asEffect.race), select: createEffectTesterFromEffects('select', _keys.SELECT, _asEffect.asEffect.select), spawn: createEffectTesterFromEffects('spawn', _keys.FORK, _asEffect.asEffect.fork), setContext: createEffectTesterFromEffects('setContext', _keys.SET_CONTEXT, _asEffect.asEffect.setContext), take: createEffectTesterFromEffects('take', _keys.TAKE, _asEffect.asEffect.take), takeMaybe: createEffectTesterFromEffects('takeMaybe', _keys.TAKE, _asEffect.asEffect.take) }; api.actionChannel.like = createEffectTester('actionChannel', _keys.ACTION_CHANNEL, effects.actionChannel, _asEffect.asEffect.actionChannel, true); api.actionChannel.pattern = function (pattern) { return api.actionChannel.like({ pattern: pattern }); }; api.apply.like = createEffectTester('apply', _keys.CALL, effects.apply, _asEffect.asEffect.call, true); api.apply.fn = function (fn) { return api.apply.like({ fn: fn }); }; api.call.like = createEffectTester('call', _keys.CALL, effects.call, _asEffect.asEffect.call, true); api.call.fn = function (fn) { return api.call.like({ fn: fn }); }; api.cps.like = createEffectTester('cps', _keys.CPS, effects.cps, _asEffect.asEffect.cps, true); api.cps.fn = function (fn) { return api.cps.like({ fn: fn }); }; api.fork.like = createEffectTester('fork', _keys.FORK, effects.fork, _asEffect.asEffect.fork, true); api.fork.fn = function (fn) { return api.fork.like({ fn: fn }); }; api.put.like = createEffectTester('put', _keys.PUT, effects.put, _asEffect.asEffect.put, true); api.put.actionType = function (type) { return api.put.like({ action: { type: type } }); }; api.putResolve.like = createEffectTester('putResolve', _keys.PUT, effects.putResolve, _asEffect.asEffect.put, true); api.putResolve.actionType = function (type) { return api.putResolve.like({ action: { type: type } }); }; api.select.like = createEffectTester('select', _keys.SELECT, effects.select, _asEffect.asEffect.select, true); api.select.selector = function (selector) { return api.select.like({ selector: selector }); }; api.spawn.like = createEffectTester('spawn', _keys.FORK, effects.spawn, _asEffect.asEffect.fork, true); api.spawn.fn = function (fn) { return api.spawn.like({ fn: fn }); }; function checkExpectations() { expectations.forEach(function (expectation) { expectation({ storeState: storeState, returnValue: returnValue }); }); } function apiDispatch(action) { var dispatchableAction = void 0; if (typeof delayTime === 'number') { dispatchableAction = (0, _objectAssign2.default)({}, action, { _delayTime: delayTime }); delayTime = null; } else { dispatchableAction = action; } if (isRunning) { dispatch(dispatchableAction); } else { queueAction(dispatchableAction); } return api; } function taskPromise(task) { return task.toPromise(); } function start() { var sagaWrapper = (0, _sagaWrapper2.default)(generator.name); isRunning = true; iterator = generator.apply(undefined, sagaArgs); mainTask = (0, _reduxSaga.runSaga)(io, sagaWrapper, iterator, refineYieldedValue, setReturnValue); mainTaskPromise = taskPromise(mainTask).then(checkExpectations) // Pass along the error instead of rethrowing or allowing to // bubble up to avoid PromiseRejectionHandledWarning .catch(_identity2.default); return api; } function stop(timeout) { return scheduleStop(timeout).then(function (err) { if (err) { throw err; } }); } function exposeResults() { var finalEffects = Object.keys(exposableEffects).reduce(function (memo, key) { var effectName = exposableEffects[key]; var values = effectStores[key].values().filter(lacksSagaWrapper); if (values.length > 0) { // eslint-disable-next-line no-param-reassign memo[effectName] = effectStores[key].values().filter(lacksSagaWrapper); } return memo; }, {}); return { storeState: storeState, returnValue: returnValue, effects: finalEffects, allEffects: allEffects, toJSON: function toJSON() { return _toJSON(finalEffects); } }; } function run() { var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : expectSaga.DEFAULT_TIMEOUT; start(); return stop(timeout).then(exposeResults); } function silentRun() { var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : expectSaga.DEFAULT_TIMEOUT; return run({ timeout: timeout, silenceTimeout: true }); } function withState(state) { storeState = state; return api; } function withReducer(newReducer, initialState) { reducer = newReducer; storeState = extractState(newReducer, initialState); return api; } function provide(newProviders) { providers = Array.isArray(newProviders) ? (0, _helpers.coalesceProviders)(newProviders) : newProviders; return api; } function returns(value) { addExpectation((0, _expectations.createReturnExpectation)({ value: value, expected: !negateNextAssertion })); return api; } function hasFinalState(state) { addExpectation((0, _expectations.createStoreStateExpectation)({ state: state, expected: !negateNextAssertion })); return api; } function apiDelay(time) { delayTime = time; return api; } function createEffectTester(effectName, storeKey, effectCreator, extractEffect) { var like = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; return function () { var expectedEffect = like ? arguments.length <= 0 ? undefined : arguments[0] : effectCreator.apply(undefined, arguments); addExpectation((0, _expectations.createEffectExpectation)({ effectName: effectName, expectedEffect: expectedEffect, storeKey: storeKey, like: like, extractEffect: extractEffect, store: effectStores[storeKey], expected: !negateNextAssertion })); negateNextAssertion = false; return api; }; } function createEffectTesterFromEffects(effectName, storeKey, extractEffect) { return createEffectTester(effectName, storeKey, effects[effectName], extractEffect); } return api; } expectSaga.DEFAULT_TIMEOUT = 250;