redux-mock-store-await-actions
Advanced tools
Comparing version 1.0.0 to 2.0.0
@@ -5,2 +5,21 @@ # Change Log | ||
<a name="2.0.0"></a> | ||
# [2.0.0](https://github.com/moxystudio/redux-mock-store-await-actions/compare/v1.0.0...v2.0.0) (2018-02-01) | ||
### Features | ||
* add option to supply custom matchers ([#6](https://github.com/moxystudio/redux-mock-store-await-actions/issues/6)) ([907dbc0](https://github.com/moxystudio/redux-mock-store-await-actions/commit/907dbc0)) | ||
### BREAKING CHANGES | ||
* convert timeout argument to an options object. This | ||
commit introduces the matcher concept, which allows to supply a function | ||
via options implementing custom comparison of actions. The predicate | ||
function, which could be passed until now in the actions argument to | ||
perform comparisons, is now no longer supported. | ||
<a name="1.0.0"></a> | ||
@@ -7,0 +26,0 @@ # 1.0.0 (2017-10-20) |
86
index.js
@@ -6,25 +6,49 @@ 'use strict'; | ||
const isPlainObject = require('lodash/isPlainObject'); | ||
const isEqualWith = require('lodash/isEqualWith'); | ||
function actionsMatch(actions, storeActions) { | ||
return differenceWith(actions, storeActions, (action, storeAction) => isMatch(storeAction, action)).length === 0; | ||
function actionsContaining(expectedActions, storeActions) { | ||
return differenceWith(expectedActions, storeActions, (expectedAction, storeAction) => isMatch(storeAction, expectedAction)).length === 0; | ||
} | ||
function waitForActions(store, actions, timeout) { | ||
timeout = timeout || 50; | ||
function actionsMatchOrder(expectedActions, storeActions) { | ||
const isEqual = isEqualWith(expectedActions, storeActions, (expectedAction, storeAction) => isMatch(storeAction, expectedAction)); | ||
if (typeof actions === 'string' || isPlainObject(actions)) { | ||
actions = [actions]; | ||
if (!isEqual && storeActions.length >= expectedActions.length) { | ||
throw new MismatchError(); | ||
} | ||
if (Array.isArray(actions)) { | ||
actions = actions.map((value) => typeof value === 'string' ? { type: value } : value); | ||
return isEqual; | ||
} | ||
function settledPromise(matcher, expectedActions, storeActions) { | ||
try { | ||
if (matcher(expectedActions, storeActions)) { | ||
return Promise.resolve(); | ||
} | ||
} catch (err) { | ||
if (err instanceof MismatchError) { | ||
return Promise.reject(err); | ||
} | ||
} | ||
} | ||
const shouldPromiseResolve = typeof actions === 'function' ? | ||
actions : | ||
(storeActions) => actionsMatch(actions, storeActions); | ||
module.exports = (store, expectedActions, options) => { | ||
const { timeout, matcher } = { | ||
timeout: 50, | ||
matcher: actionsMatchOrder, | ||
...options, | ||
}; | ||
// If the store already contains the expected actions, resolve the Promise immediately | ||
if (shouldPromiseResolve(store.getActions())) { | ||
const promise = Promise.resolve(); | ||
if (typeof expectedActions === 'string' || isPlainObject(expectedActions)) { | ||
expectedActions = [expectedActions]; | ||
} | ||
if (Array.isArray(expectedActions)) { | ||
expectedActions = expectedActions.map((value) => typeof value === 'string' ? { type: value } : value); | ||
} | ||
const matchPromise = settledPromise.bind(null, matcher, expectedActions); | ||
let promise = matchPromise(store.getActions()); | ||
if (promise) { | ||
promise.cancel = () => {}; | ||
@@ -37,3 +61,3 @@ | ||
const promise = new Promise((resolve, reject) => { | ||
promise = new Promise((resolve, reject) => { | ||
const teardown = () => { | ||
@@ -45,9 +69,8 @@ clearTimeout(timeoutId); | ||
teardown(); | ||
reject(new waitForActions.TimeoutError()); | ||
reject(new TimeoutError()); | ||
}, timeout); | ||
const unsubscribe = store.subscribe(() => { | ||
if (shouldPromiseResolve(store.getActions())) { | ||
teardown(); | ||
resolve(); | ||
} | ||
const promise = matchPromise(store.getActions()); | ||
promise && resolve(promise); | ||
}); | ||
@@ -57,3 +80,3 @@ | ||
teardown(); | ||
reject(new waitForActions.CancelledError()); | ||
reject(new CancelledError()); | ||
}; | ||
@@ -65,5 +88,5 @@ }); | ||
return promise; | ||
} | ||
}; | ||
waitForActions.TimeoutError = class extends Error { | ||
class TimeoutError extends Error { | ||
constructor() { | ||
@@ -74,5 +97,5 @@ super('Timeout reached while waiting for actions'); | ||
} | ||
}; | ||
} | ||
waitForActions.CancelledError = class extends Error { | ||
class CancelledError extends Error { | ||
constructor() { | ||
@@ -83,4 +106,13 @@ super('Cancel was called by user'); | ||
} | ||
}; | ||
} | ||
module.exports = waitForActions; | ||
class MismatchError extends Error { | ||
constructor() { | ||
super('Found mismatch between the order of the array of expected and dispatched actions'); | ||
this.code = 'EMISMATCH'; | ||
this.name = 'MismatchError'; | ||
} | ||
} | ||
module.exports.MismatchError = MismatchError; | ||
module.exports.matchers = { containing: actionsContaining, order: actionsMatchOrder }; |
{ | ||
"name": "redux-mock-store-await-actions", | ||
"description": "Waits for specific actions to be dispatched or a timeout expires.", | ||
"version": "1.0.0", | ||
"version": "2.0.0", | ||
"keywords": [ | ||
@@ -20,18 +20,27 @@ "redux", | ||
"license": "MIT", | ||
"main": "index.js", | ||
"scripts": { | ||
"lint": "eslint .", | ||
"test": "jest --env node --coverage", | ||
"posttest": "npm run lint", | ||
"prerelease": "npm t && npm run lint", | ||
"release": "standard-version", | ||
"precommit": "lint-staged", | ||
"prerelease": "npm t", | ||
"release": "standard-version" | ||
"commitmsg": "commitlint -e $GIT_PARAMS" | ||
}, | ||
"standard-version": { | ||
"scripts": { | ||
"posttag": "git push --follow-tags origin master" | ||
"posttag": "git push --follow-tags origin master && npm publish" | ||
} | ||
}, | ||
"lint-staged": { | ||
"*.js": "eslint" | ||
"*.js": [ | ||
"eslint --fix", | ||
"git add" | ||
] | ||
}, | ||
"commitlint": { | ||
"extends": [ | ||
"@commitlint/config-conventional" | ||
] | ||
}, | ||
"dependencies": { | ||
@@ -41,10 +50,9 @@ "lodash": "^4.17.4" | ||
"devDependencies": { | ||
"babel-plugin-transform-async-to-generator": "^6.24.1", | ||
"babel-plugin-transform-runtime": "^6.23.0", | ||
"babel-preset-es2015": "^6.22.0", | ||
"@commitlint/cli": "^6.0.1", | ||
"@commitlint/config-conventional": "^6.0.2", | ||
"eslint": "^4.3.0", | ||
"eslint-config-moxy": "^3.0.0", | ||
"eslint-config-moxy": "^4.1.0", | ||
"husky": "^0.14.3", | ||
"jest": "^21.2.1", | ||
"lint-staged": "^4.0.2", | ||
"jest": "^22.0.0", | ||
"lint-staged": "^6.0.0", | ||
"redux": "^3.7.2", | ||
@@ -51,0 +59,0 @@ "redux-mock-store": "^1.2.3", |
105
README.md
@@ -71,3 +71,3 @@ # redux-mock-store-await-actions | ||
store.dispatch(login('my-username', '12345')); | ||
store.dispatch(login('my-username', 'my-password')); | ||
@@ -84,3 +84,3 @@ expect(store.getActions()).toContain([ | ||
```js | ||
store.dispatch(login('my-username', '12345')); | ||
store.dispatch(login('my-username', 'my-password')); | ||
@@ -109,6 +109,8 @@ setTimeout(() => expect(store.getActions()).toContain([ | ||
store.dispatch(login('my-username', '12345')); | ||
store.dispatch(login('my-username', 'my-password')); | ||
await waitForActions(store, ['LOGIN_START', 'FETCH_ORDERS_SUCCESS']); | ||
``` | ||
### Example #2: action objects | ||
@@ -125,6 +127,6 @@ | ||
store.dispatch(login('my-username', '123')); | ||
store.dispatch(login('my-username', 'my-password')); | ||
// { type: 'LOGIN_START', payload: { username: 'my-username' } } | ||
// matches | ||
// { type: 'LOGIN_START', payload: { username, password } } | ||
// { type: 'LOGIN_START', payload: { username: 'my-username', password } } | ||
// | ||
@@ -134,2 +136,3 @@ // { type: 'FETCH_ORDERS_SUCCESS', } | ||
// { type: 'FETCH_ORDERS_SUCCESS', payload: orders } | ||
await waitForActions(store, [ | ||
@@ -147,35 +150,17 @@ { | ||
### Example #3: function (advanced use cases) | ||
Supply a predicate which will be called for every action dispatched. | ||
```js | ||
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', '123')); | ||
await waitForActions(store, (storeActions) => { | ||
const hasLoginStart = storeActions.some((item) => item.type === 'LOGIN_START' && item.payload.username === 'my-username'); | ||
const hasFetchOrdersSuccess = storeActions.some((item) => item.type === 'FETCH_ORDERS_SUCCESS'); | ||
return hasLoginStart && hasFetchOrdersSuccess; | ||
}); | ||
``` | ||
**NOTE:** Subsequent calls to `waitForActions` should be preceded by a call to `store.clearActions()`, otherwise the returned `Promise` will resolve immediately. | ||
## API | ||
### waitForActions(store, actions, [timeout]) | ||
### waitForActions(store, actions, [options]) | ||
Returns a `Promise` which fulfills if all `actions` are dispatched before the timeout expires, otherwise it is rejected. The `Promise` has a `.cancel()` function which, if called, will reject the `Promise`. | ||
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 throwing `CancelledError` | ||
* as a result of timeout expiration, throwing `TimeoutError` | ||
* as a result of `.cancel()` invocation, throwing `CancelledError` | ||
* 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 | ||
@@ -199,6 +184,7 @@ | ||
* action objects mixed with action type strings. | ||
* `Function`: a predicate which receives the array returned by `store.getActions()` of `redux-mock-store`. | ||
#### timeout | ||
#### options | ||
##### timeout | ||
Type: `Number` | ||
@@ -209,3 +195,58 @@ Default: 50 | ||
##### 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 the `Promise` | ||
* return `false` to indicate a match is yet to occur and the `Promise` remains in pending state | ||
* throw `MismatchError` to indicate a match will not occur anymore and reject the `Promise` | ||
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 rejection | ||
* `containing` 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()](https://lodash.com/docs/4.17.4#isMatch). | ||
Example of a custom matcher implementation: | ||
```js | ||
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 | ||
@@ -212,0 +253,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
24242
11
11
266
253
1