use-saga-reducer
Version:
Use redux-saga without redux
279 lines (278 loc) • 11.1 kB
JavaScript
import React from 'react';
import { takeEvery, put, select, delay, getContext, setContext, spawn } from 'redux-saga/effects';
import { render, fireEvent, act } from '@testing-library/react';
import useSagaReducer, { SagaProvider, makeCustomEffect } from '.';
function flushPromiseQueue() {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 0);
});
}
describe('useSagaReducer()', () => {
it('yields actions taken by `takeEvery`', async () => {
const testCaller = jest.fn();
const testAction = {
type: 'TEST'
};
const testPutAction = {
type: 'TEST_PUT'
};
const testReducer = jest.fn((state = {}, action) => {
return state;
});
function* testSaga() {
yield put(testPutAction);
yield takeEvery(testAction.type, testCaller);
}
function TestUseSagaReducer() {
const [, dispatch] = useSagaReducer(testSaga, testReducer, {});
return (React.createElement("div", null,
React.createElement("button", { "data-testid": 'button', onClick: () => {
dispatch(testAction);
} }, "TEST")));
}
const { getByTestId } = render(React.createElement(TestUseSagaReducer, null));
const button = getByTestId('button');
expect(testReducer).toHaveBeenCalledWith({}, testPutAction);
fireEvent.click(button);
await flushPromiseQueue();
expect(testCaller.mock.calls.length).toBe(1);
fireEvent.click(button);
await flushPromiseQueue();
expect(testCaller.mock.calls.length).toBe(2);
});
it('saga can update the state using put actions', async () => {
const testReducer = jest.fn((state = {}, action) => {
if (action.payload) {
return action.payload;
}
return state;
});
function* testSaga() {
yield delay(0);
const state = yield select();
yield put({
type: 'UPDATE',
payload: {
count: state.count + 1
}
});
}
function TestUseSagaReducer() {
const [state] = useSagaReducer(testSaga, testReducer, { count: 1 });
return React.createElement("div", { "data-testid": 'test' }, state.count);
}
const { getByTestId } = render(React.createElement(TestUseSagaReducer, null));
const el = getByTestId('test');
expect(el.textContent).toBe('1');
await act(flushPromiseQueue);
expect(el.textContent).toBe('2');
});
it('saga updates the state available to yield select()', async () => {
let testState;
const testReducer = jest.fn((state = {}, action) => {
if (action.type === 'INCREMENT') {
return {
count: state.count + 1
};
}
if (action.type === 'SET') {
return {
count: action.payload
};
}
return state;
});
function* increment() {
const { count } = yield select();
testState = count;
}
function* incrementAsync() {
const state = yield select();
yield put({
type: 'SET',
payload: state.count + 1
});
yield delay(0);
const { count } = yield select();
testState = count;
}
function* testSaga() {
yield takeEvery('INCREMENT', increment);
yield takeEvery('INCREMENT_ASYNC', incrementAsync);
}
function TestUseSagaReducer() {
const [, dispatch] = useSagaReducer(testSaga, testReducer, { count: 1 });
return (React.createElement("div", null,
React.createElement("button", { "data-testid": 'button', onClick: () => {
dispatch({
type: 'INCREMENT'
});
} }, "TEST"),
React.createElement("button", { "data-testid": 'button-async', onClick: () => {
dispatch({
type: 'INCREMENT_ASYNC'
});
} }, "TEST")));
}
const { getByTestId } = render(React.createElement(TestUseSagaReducer, null));
const button = getByTestId('button');
const buttonAsync = getByTestId('button-async');
fireEvent.click(button);
await act(flushPromiseQueue);
expect(testState).toEqual(2);
fireEvent.click(button);
await act(flushPromiseQueue);
expect(testState).toEqual(3);
fireEvent.click(buttonAsync);
await act(flushPromiseQueue);
// Add a second flush here, once to wait for microtasks queue from put,
// second one to wait for timer queue from delay task
await act(flushPromiseQueue);
expect(testState).toEqual(4);
});
it('provides context values in sagas passed to provider', async () => {
const testReducer = jest.fn((state = {}, action) => {
return state;
});
function* updateContextValue({ payload }) {
yield setContext({ foo: payload });
const contextValue = yield getContext('foo');
yield put({
type: 'CONTEXT_VALUE',
payload: contextValue
});
}
function* testSaga() {
const contextValue = yield getContext('foo');
yield put({
type: 'CONTEXT_VALUE',
payload: contextValue
});
yield takeEvery('UPDATE_CONTEXT', updateContextValue);
}
const globalState = { foo: 'bar' };
function TestApp() {
return (React.createElement(SagaProvider, { context: globalState },
React.createElement(TestUseSagaReducer, null)));
}
function TestUseSagaReducer() {
const [, dispatch] = useSagaReducer(testSaga, testReducer, {});
return (React.createElement("div", null,
React.createElement("button", { "data-testid": 'button', onClick: () => {
dispatch({
type: 'UPDATE_CONTEXT',
payload: 'baz'
});
} }, "TEST")));
}
const { getByTestId } = render(React.createElement(TestApp, null));
const button = getByTestId('button');
expect(testReducer).toHaveBeenCalledWith({}, { type: 'CONTEXT_VALUE', payload: 'bar' });
fireEvent.click(button);
await flushPromiseQueue();
expect(testReducer).toHaveBeenNthCalledWith(2, {}, { type: 'UPDATE_CONTEXT', payload: 'baz' });
expect(testReducer).toHaveBeenNthCalledWith(3, {}, { type: 'CONTEXT_VALUE', payload: 'baz' });
});
it('calls sagaMonitor, onError, and effectMiddlewares from context', async () => {
const testReducer = jest.fn((state = {}, action) => {
return state;
});
function* errorThrower() {
throw new Error('Happy Test');
}
function* testErrorSaga() {
yield takeEvery('TEST_ERROR', errorThrower);
}
function* testSaga() {
yield put({
type: 'TEST_INIT',
payload: 'TEST'
});
yield spawn(testErrorSaga);
}
const testMonior = {
effectTriggered: jest.fn()
};
const testOnError = jest.fn();
const testIntercept = jest.fn();
const testEffectMiddleware = jest.fn((next) => (effect) => {
testIntercept(effect);
return next(effect);
});
function TestApp() {
return (React.createElement(SagaProvider, { sagaMonitor: testMonior, onError: testOnError, effectMiddlewares: [testEffectMiddleware] },
React.createElement(TestUseSagaReducer, null)));
}
function TestUseSagaReducer() {
const [, dispatch] = useSagaReducer(testSaga, testReducer, {});
return (React.createElement("div", null,
React.createElement("button", { "data-testid": 'button', onClick: () => {
dispatch({
type: 'TEST_ERROR'
});
} }, "TEST")));
}
const { getByTestId } = render(React.createElement(TestApp, null));
const button = getByTestId('button');
// Distpaches
// - init
// - spawn
// - takeEvery (take effects inside a spawned process dispatch twice)
expect(testMonior.effectTriggered).toHaveBeenCalledTimes(4);
expect(testOnError).not.toHaveBeenCalled();
expect(testIntercept).toHaveBeenCalledTimes(4);
fireEvent.click(button);
await flushPromiseQueue();
expect(testMonior.effectTriggered).toHaveBeenCalledTimes(5);
expect(testOnError).toHaveBeenCalledTimes(1);
expect(testIntercept).toHaveBeenCalledTimes(5);
});
it('supports yield-ing to custom effects', async () => {
const testReducer = jest.fn((state = {}, action) => {
return state;
});
function testPut(action) {
return makeCustomEffect('TEST_PUT', { action });
}
function* testSaga() {
yield put({
type: 'TEST_INIT',
payload: 'TEST'
});
yield testPut({
type: 'TEST_TEST_PUT'
});
}
let testPutCount = 0;
const testMonior = {
effectTriggered: jest.fn((options) => {
if (options.effect.type === 'TEST_PUT') {
testPutCount += 1;
}
})
};
function TestApp() {
return (React.createElement(SagaProvider, { sagaMonitor: testMonior },
React.createElement(TestUseSagaReducer, null)));
}
function TestUseSagaReducer() {
const [, dispatch] = useSagaReducer(testSaga, testReducer, {});
return (React.createElement("div", null,
React.createElement("button", { "data-testid": 'button', onClick: () => {
dispatch({
type: 'TEST_TEST_PUT'
});
} }, "TEST")));
}
const { getByTestId } = render(React.createElement(TestApp, null));
const button = getByTestId('button');
expect(testMonior.effectTriggered).toHaveBeenCalledTimes(2);
expect(testPutCount).toBe(1);
fireEvent.click(button);
await flushPromiseQueue();
expect(testMonior.effectTriggered).toHaveBeenCalledTimes(2);
expect(testPutCount).toBe(1);
});
});