204 lines
5.3 KiB
JavaScript
204 lines
5.3 KiB
JavaScript
import { Observable, Subject } from 'rx';
|
|
import test from 'tape';
|
|
import { spy } from 'sinon';
|
|
import { applyMiddleware, createStore } from 'redux';
|
|
import createSaga from './redux-epic';
|
|
|
|
const setup = (saga, spy) => {
|
|
const reducer = (state = 0) => state;
|
|
const sagaMiddleware = createSaga(
|
|
action$ => action$
|
|
.filter(({ type }) => type === 'foo')
|
|
.map(() => ({ type: 'bar' })),
|
|
action$ => action$
|
|
.filter(({ type }) => type === 'bar')
|
|
.map(({ type: 'baz' })),
|
|
saga ? saga : () => Observable.empty()
|
|
);
|
|
const store = applyMiddleware(sagaMiddleware)(createStore)(spy || reducer);
|
|
return {
|
|
reducer,
|
|
sagaMiddleware,
|
|
store
|
|
};
|
|
};
|
|
|
|
test('createSaga', t => {
|
|
const sagaMiddleware = createSaga(
|
|
action$ => action$.map({ type: 'foo' })
|
|
);
|
|
t.equal(
|
|
typeof sagaMiddleware,
|
|
'function',
|
|
'sagaMiddleware is not a function'
|
|
);
|
|
t.equal(
|
|
typeof sagaMiddleware.subscribe,
|
|
'function',
|
|
'sagaMiddleware does not have a subscription method'
|
|
);
|
|
t.equal(
|
|
typeof sagaMiddleware.subscribeOnCompleted,
|
|
'function',
|
|
'sagaMiddleware does not have a subscribeOnCompleted method'
|
|
);
|
|
t.equal(
|
|
typeof sagaMiddleware.end,
|
|
'function',
|
|
'sagaMiddleware does not have an end method'
|
|
);
|
|
t.equal(
|
|
typeof sagaMiddleware.restart,
|
|
'function',
|
|
'sagaMiddleware does not have an restart method'
|
|
);
|
|
t.equal(
|
|
typeof sagaMiddleware.dispose,
|
|
'function',
|
|
'sagaMiddleware does not have a dispose method'
|
|
);
|
|
t.end();
|
|
});
|
|
|
|
test('dispatching actions', t => {
|
|
const reducer = spy((state = 0) => state);
|
|
const { store } = setup(null, reducer);
|
|
store.dispatch({ type: 'foo' });
|
|
t.equal(reducer.callCount, 4, 'reducer is called four times');
|
|
t.assert(
|
|
reducer.getCall(1).calledWith(0, { type: 'foo' }),
|
|
'reducer called with initial action'
|
|
);
|
|
t.assert(
|
|
reducer.getCall(2).calledWith(0, { type: 'bar' }),
|
|
'reducer was not called with saga action'
|
|
);
|
|
t.assert(
|
|
reducer.getCall(3).calledWith(0, { type: 'baz' }),
|
|
'second saga responded to action from first saga'
|
|
);
|
|
t.end();
|
|
});
|
|
|
|
test('lifecycle', t => {
|
|
t.test('subscribe', t => {
|
|
const { sagaMiddleware } = setup();
|
|
const subscription = sagaMiddleware.subscribeOnCompleted(() => {});
|
|
t.assert(
|
|
subscription,
|
|
'subscribe did not return a disposable'
|
|
);
|
|
t.isEqual(
|
|
typeof subscription.dispose,
|
|
'function',
|
|
'disposable does not have a dispose method'
|
|
);
|
|
t.doesNotThrow(
|
|
() => subscription.dispose(),
|
|
'disposable is not disposable'
|
|
);
|
|
t.end();
|
|
});
|
|
|
|
t.test('end', t => {
|
|
const result$ = new Subject();
|
|
const { sagaMiddleware } = setup(() => result$);
|
|
sagaMiddleware.subscribeOnCompleted(() => {
|
|
t.pass('all sagas completed');
|
|
t.end();
|
|
});
|
|
sagaMiddleware.end();
|
|
t.pass('saga still active');
|
|
result$.onCompleted();
|
|
});
|
|
|
|
t.test('disposable', t => {
|
|
const result$ = new Subject();
|
|
const { sagaMiddleware } = setup(() => result$);
|
|
t.plan(2);
|
|
sagaMiddleware.subscribeOnCompleted(() => {
|
|
t.fail('all sagas completed');
|
|
});
|
|
t.assert(
|
|
result$.hasObservers(),
|
|
'saga is observed by sagaMiddleware'
|
|
);
|
|
sagaMiddleware.dispose();
|
|
t.false(
|
|
result$.hasObservers(),
|
|
'watcher has no observers after sagaMiddleware is disposed'
|
|
);
|
|
});
|
|
});
|
|
|
|
test('restart', t => {
|
|
const reducer = spy((state = 0) => state);
|
|
const { sagaMiddleware, store } = setup(null, reducer);
|
|
store.dispatch({ type: 'foo' });
|
|
t.assert(
|
|
reducer.getCall(1).calledWith(0, { type: 'foo' }),
|
|
'reducer called with initial dispatch'
|
|
);
|
|
t.assert(
|
|
reducer.getCall(2).calledWith(0, { type: 'bar' }),
|
|
'reducer called with saga action'
|
|
);
|
|
t.assert(
|
|
reducer.getCall(3).calledWith(0, { type: 'baz' }),
|
|
'second saga responded to action from first saga'
|
|
);
|
|
sagaMiddleware.end();
|
|
t.equal(reducer.callCount, 4, 'saga produced correct amount of actions');
|
|
sagaMiddleware.restart();
|
|
store.dispatch({ type: 'foo' });
|
|
t.equal(
|
|
reducer.callCount,
|
|
7,
|
|
'saga restart and produced correct amount of actions'
|
|
);
|
|
t.assert(
|
|
reducer.getCall(4).calledWith(0, { type: 'foo' }),
|
|
'reducer called with second dispatch'
|
|
);
|
|
t.assert(
|
|
reducer.getCall(5).calledWith(0, { type: 'bar' }),
|
|
'reducer called with saga reaction'
|
|
);
|
|
t.assert(
|
|
reducer.getCall(6).calledWith(0, { type: 'baz' }),
|
|
'second saga responded to action from first saga'
|
|
);
|
|
t.end();
|
|
});
|
|
|
|
test('long lived saga', t => {
|
|
let count = 0;
|
|
const tickSaga = action$ => action$
|
|
.filter(({ type }) => type === 'start-tick')
|
|
.flatMap(() => Observable.interval(500))
|
|
// make sure long lived saga's do not persist after
|
|
// action$ has completed
|
|
.takeUntil(action$.last())
|
|
.map(({ type: 'tick' }));
|
|
|
|
const reducerSpy = spy((state = 0) => state);
|
|
const { store, sagaMiddleware } = setup(tickSaga, reducerSpy);
|
|
const unlisten = store.subscribe(() => {
|
|
count += 1;
|
|
if (count >= 5) {
|
|
sagaMiddleware.end();
|
|
}
|
|
});
|
|
sagaMiddleware.subscribeOnCompleted(() => {
|
|
t.equal(
|
|
count,
|
|
5,
|
|
'saga dispatched correct amount of ticks'
|
|
);
|
|
unlisten();
|
|
t.pass('long lived saga completed');
|
|
t.end();
|
|
});
|
|
store.dispatch({ type: 'start-tick' });
|
|
});
|