redux-mock-store-await-actions v2.1.0
redux-mock-store-await-actions
Waits for specific actions to be dispatched or a timeout expires.
NOTE: This module only works with redux-mock-store and shall only be used for testing purposes. Support for real Redux store is not provided.
Installation
$ npm install redux-mock-store-await-actions --save-dev
Motivation
Consider the following example:
function login(username, password) {
return async (dispatch) => {
dispatch({ type: 'LOGIN_START', payload: { username, password } });
try {
const user = await fetch('/login', {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
body: JSON.stringify({ username, password })
});
dispatch({ type: 'LOGIN_SUCCESS', payload: user });
} catch (err) {
dispatch({ type: 'LOGIN_FAIL', payload: err });
throw err;
}
// Fetch orders asynchronously, not waiting for them to be retrieved
dispatch(fetchOrders());
}
}
function fetchOrders() {
return async (dispatch) => {
dispatch({ type: 'FETCH_ORDERS_START' });
try {
const orders = await fetch('/account/orders');
dispatch({ type: 'FETCH_ORDERS_SUCCESS', payload: orders });
} catch (err) {
dispatch({ type: 'FETCH_ORDERS_FAIL', payload: err });
throw err;
}
}
}
store.dispatch(login('my-username', 'my-password'));
expect(store.getActions()).toContain([
'LOGIN_START',
'FETCH_ORDERS_SUCCESS'
]);
The assertion above will fail because FETCH_ORDERS_SUCCESS
will not yet exist in the stack of actions.
To solve this, one can use setTimeout
explicitly in each test:
store.dispatch(login('my-username', 'my-password'));
setTimeout(() => expect(store.getActions()).toContain([
'LOGIN_START',
'FETCH_ORDERS_SUCCESS'
]), 50);
However, this is not pretty and is error-prone. redux-mock-store-await-actions
makes this easier for you.
Usage
Example #1: action types
Supply the action types to await for.
import waitForActions from 'redux-mock-store-await-actions';
import configureStore from 'redux-mock-store';
import thunkMiddleware from 'redux-thunk';
const store = configureStore([thunkMiddleware])();
store.dispatch(login('my-username', 'my-password'));
await waitForActions(store, ['LOGIN_START', 'FETCH_ORDERS_SUCCESS']);
Example #2: action objects
Supply the action objects to await for, matching a subset of the properties of the dispatched actions. It performs a deep comparison between property values of dispatched and expected actions to determine whether the expected actions are partially contained in the stack of dispatched actions.
import waitForActions from 'redux-mock-store-await-actions';
import configureStore from 'redux-mock-store';
import thunkMiddleware from 'redux-thunk';
const store = configureStore([thunkMiddleware])();
store.dispatch(login('my-username', 'my-password'));
// { type: 'LOGIN_START', payload: { username: 'my-username' } }
// matches
// { type: 'LOGIN_START', payload: { username: 'my-username', password } }
//
// { type: 'FETCH_ORDERS_SUCCESS', }
// matches
// { type: 'FETCH_ORDERS_SUCCESS', payload: orders }
await waitForActions(store, [
{
type: 'LOGIN_START',
payload: { username: 'my-username' },
},
{
type: 'FETCH_ORDERS_SUCCESS',
},
]);
API
waitForActions(store, actions, options)
Returns a Promise
which fulfills if all actions
are dispatched before the timeout expires. The Promise
has a .cancel()
function which, if called, will reject the Promise
.
The Promise
might be rejected:
- as a result of timeout expiration, throwing
TimeoutError
- as a result of
.cancel()
invocation, throwingCancelledError
- when the action's matcher throws
MismatchError
NOTE: Subsequent calls to waitForActions
with the same actions should be preceded by a call to store.clearActions()
, otherwise the returned Promise
will resolve immediately.
store
Type: Object
The redux-mock-store
.
actions
Type: Object
String
Array
Function
The actions to wait for. It can be either:
String
: an action type string.Object
: an action object.Array
of either- action objects;
- action type strings;
- action objects mixed with action type strings.
options
timeout
Type: Number
Default: 2000
The timeout given in milliseconds.
throttleWait
Type: Number
Default: 0
Specifies the time in milliseconds that every invocation to the action's matcher take place at since the last invocation. When set to zero, throttling is disabled.
When throttling is enabled, the matcher
will be called at most once per throttleWait
milliseconds receiving the array of actions dispatched until that time. If the matcher
does not resolve the Promise
until timeout
milliseconds have elapsed, the Promise
is rejected throwing TimeoutError
.
This feature is useful when one needs to wait for several actions or a burst of actions to be dispatched, effectively skip invocations to the action's matcher until the Redux store "settles" to avoid running complex action comparison logic in the meantime and improve performance.
matcher
Type: Function
Default: .matchers.order
Supplies custom behavior to specify how expected and dispatched actions should be compared. The function accepts two arguments: the array of expected actions and dispatched actions.
The matcher must either:
- return
true
to indicate a match has occurred and fulfill thePromise
- return
false
to indicate a match is yet to occur and thePromise
remains in pending state - throw
MismatchError
to indicate a match will not occur anymore and reject thePromise
Two built-in matchers are already shipped and available under .matchers
property:
order
matcher performs a comparison between the specified order of expected actions against the order of arrival of dispatched actions. On the first mismatch detected,MismatchError
is thrown for early rejectioncontaining
matcher is a less strict matcher which checks whether expected actions are contained within dispatched actions
Both matchers perform a partial deep comparison between dispatched and expected actions, as per Lodash's isMatch().
Example of a custom matcher implementation:
import waitForActions from 'redux-mock-store-await-actions';
import configureStore from 'redux-mock-store';
import thunkMiddleware from 'redux-thunk';
const store = configureStore([thunkMiddleware])();
const expectedActions = [
{ type: 'LOGIN_START', payload: { username: 'my-username' } },
{ type: 'FETCH_ORDERS_SUCCESS' }
];
store.dispatch(login('my-username', 'my-password'));
// Throws if LOGIN_FAIL is dispatched or
// Matches when LOGIN_START and FETCH_ORDERS_SUCCESS are dispatched
waitForActions(store, expectedActions, { matcher: (expectedActions, storeActions) => {
const hasLoginFail = storeActions.some((action) => action.type === 'LOGIN_FAIL');
if (hasLoginFail) {
throw new waitForActions.MismatchError();
}
const hasLoginStart = storeActions.some((action) => action.type === 'LOGIN_START' && action.payload.username === 'my-username');
const hasFetchOrdersSuccess = storeActions.some((action) => action.type === 'FETCH_ORDERS_SUCCESS');
return hasLoginStart && hasFetchOrdersSuccess;
}})
.then(() => {
// Expected actions dispatched
})
.catch((err) => {
// MismatchError
});
Tests
$ npm test
$ npm test -- --watch
during development