diff --git a/README.md b/README.md
index e2083c6d..6af9b1bc 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,7 @@ See [Recipes](./docs/recipes.md) for more examples.
- `mount` is the most important function, allows to mount a given React component as a mini web application and interact with it using Cypress commands
- `createMount` factory function that creates new `mount` function with default options
- `unmount` removes previously mounted component, mostly useful to test how the component cleans up after itself
-- `mountHook` mounts a given React Hook in a test component for full testing, see `hooks` example
+- `mountHook` mounts a given React Hook in a test component for full testing, see the [`hooks` example](cypress/component/advanced/hooks)
## Examples
@@ -191,7 +191,7 @@ Spec | Description
[context](cypress/component/advanced/context) | Confirms components that use React context feature work
[custom-command](cypress/component/advanced/custom-command) | Wraps `mount` in a custom command for convenience
[forward-ref](cypress/component/advanced/forward-ref) | Tests a component that uses a forward ref feature
-[hooks](cypress/component/advanced/hooks) | Tests several components that use React Hooks like `useState`, `useCallback`
+[hooks](cypress/component/advanced/hooks) | Tests several components that use React Hooks like `useState`, `useCallback` by using `mountHook` function
[lazy-loaded](cypress/component/advanced/lazy-loaded) | Confirms components that use `React.lazy` and dynamic imports work
[material-ui-example](cypress/component/advanced/material-ui-example) | Large components demos from [Material UI](https://material-ui.com/)
[mobx-v6](cypress/component/advanced/mobx-v6) | Test components with MobX v6 observable
diff --git a/cypress/component/advanced/hooks/README.md b/cypress/component/advanced/hooks/README.md
index 053f08b3..716b61a1 100644
--- a/cypress/component/advanced/hooks/README.md
+++ b/cypress/component/advanced/hooks/README.md
@@ -2,7 +2,19 @@
- [counter-with-hooks.spec.js](counter-with-hooks.spec.js) and [counter2-with-hooks.spec.js](counter2-with-hooks.spec.js) test React components that uses hooks
- [use-counter.spec.js](use-counter.spec.js) shows how to test a React hook using `mountHook` function
+- [custom-hook.mount-spec.js](custom-hook.mount-spec.js) manually creates a wrapper component around a custom hook that uses Redux provider
+- [custom-hook.mount-hook-spec.js](custom-hook.mount-hook-spec.js) shows how `mountHook` can be surrounded with `wrapper` element
![Hook test](images/hook.png)
Note: hooks are mounted inside a test component following the approach shown in [react-hooks-testing-library](https://github.com/testing-library/react-hooks-testing-library/blob/master/src/pure.js)
+
+Example:
+
+```js
+import { mountHook } from 'cypress-react-unit-test'
+// wrapper is optional, only if your hook requires
+// something like a context provider
+const wrapper = ({ children }) => {children}
+mountHook(() => useCustomHook(), { wrapper })
+```
diff --git a/cypress/component/advanced/hooks/count-reducer.js b/cypress/component/advanced/hooks/count-reducer.js
new file mode 100644
index 00000000..e988ad75
--- /dev/null
+++ b/cypress/component/advanced/hooks/count-reducer.js
@@ -0,0 +1,10 @@
+export const countReducer = function(state = 0, action) {
+ switch (action.type) {
+ case 'INCREMENT':
+ return state + 1
+ case 'DECREMENT':
+ return state - 1
+ default:
+ return state
+ }
+}
diff --git a/cypress/component/advanced/hooks/custom-hook.js b/cypress/component/advanced/hooks/custom-hook.js
new file mode 100644
index 00000000..2e752937
--- /dev/null
+++ b/cypress/component/advanced/hooks/custom-hook.js
@@ -0,0 +1,10 @@
+import { useEffect } from 'react'
+import { useDispatch } from 'react-redux'
+
+export function useCustomHook() {
+ useDispatch()
+
+ useEffect(() => {
+ console.log('hello world!')
+ }, [])
+}
diff --git a/cypress/component/advanced/hooks/custom-hook.mount-hook-spec.js b/cypress/component/advanced/hooks/custom-hook.mount-hook-spec.js
new file mode 100644
index 00000000..d66bc0eb
--- /dev/null
+++ b/cypress/component/advanced/hooks/custom-hook.mount-hook-spec.js
@@ -0,0 +1,20 @@
+///
+import React from 'react'
+import { mountHook } from 'cypress-react-unit-test'
+const { useCustomHook } = require('./custom-hook')
+import { Provider } from 'react-redux'
+import store from './store'
+
+describe('custom hook that needs redux provider', () => {
+ it('mounted with wrapper', () => {
+ const wrapper = ({ children }) => (
+ {children}
+ )
+
+ cy.spy(console, 'log').as('log')
+ mountHook(() => useCustomHook(), { wrapper })
+
+ // make sure the custom hook calls "useEffect"
+ cy.get('@log').should('have.been.calledWith', 'hello world!')
+ })
+})
diff --git a/cypress/component/advanced/hooks/custom-hook.mount-spec.js b/cypress/component/advanced/hooks/custom-hook.mount-spec.js
new file mode 100644
index 00000000..fb0ac306
--- /dev/null
+++ b/cypress/component/advanced/hooks/custom-hook.mount-spec.js
@@ -0,0 +1,23 @@
+///
+import React from 'react'
+import { mount } from 'cypress-react-unit-test'
+const { useCustomHook } = require('./custom-hook')
+import { Provider } from 'react-redux'
+import store from './store'
+
+describe('custom hook that needs redux provider', () => {
+ it('mounts if we make a test component around it', () => {
+ const App = () => {
+ useCustomHook()
+
+ return <>>
+ }
+ cy.spy(console, 'log').as('log')
+ mount(
+
+
+ ,
+ )
+ cy.get('@log').should('have.been.calledWith', 'hello world!')
+ })
+})
diff --git a/cypress/component/advanced/hooks/store.js b/cypress/component/advanced/hooks/store.js
new file mode 100644
index 00000000..f75d0b10
--- /dev/null
+++ b/cypress/component/advanced/hooks/store.js
@@ -0,0 +1,6 @@
+import { createStore } from 'redux'
+import { countReducer } from './count-reducer'
+
+const store = createStore(countReducer)
+
+export default store
diff --git a/lib/mountHook.ts b/lib/mountHook.ts
index 97d23c87..e8bc7d8d 100644
--- a/lib/mountHook.ts
+++ b/lib/mountHook.ts
@@ -60,21 +60,39 @@ function TestHook({ callback, onError, children }: TestHookProps) {
return null
}
+type MountHookOptions = {
+ wrapper?: React.ReactElement
+}
+
/**
* Mounts a React hook function in a test component for testing.
*
* @see https://github.com/bahmutov/cypress-react-unit-test#advanced-examples
*/
-export const mountHook = (hookFn: (...args: any[]) => any) => {
+export const mountHook = (
+ hookFn: (...args: any[]) => any,
+ options: MountHookOptions = {},
+) => {
const { result, setValue, setError } = resultContainer()
- return mount(
- React.createElement(TestHook, {
- callback: hookFn,
- onError: setError,
- children: setValue,
- }),
- ).then(() => {
+ const testElement = React.createElement(TestHook, {
+ callback: hookFn,
+ onError: setError,
+ children: setValue,
+ key: Math.random().toString(),
+ })
+
+ let mountElement: any = testElement
+ if (options.wrapper) {
+ // what's the proper type? I don't even care anymore
+ // because types for React seem to be a mess
+ // @ts-ignore
+ mountElement = React.createElement(options.wrapper, {
+ children: [testElement],
+ })
+ }
+
+ return mount(mountElement).then(() => {
cy.wrap(result)
})
}
diff --git a/package-lock.json b/package-lock.json
index e6a6d34d..3d14b331 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -35383,6 +35383,42 @@
}
}
},
+ "react-redux": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",
+ "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.12.1",
+ "hoist-non-react-statics": "^3.3.2",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.13.1"
+ },
+ "dependencies": {
+ "@babel/runtime": {
+ "version": "7.12.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
+ "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "dev": true
+ },
+ "regenerator-runtime": {
+ "version": "0.13.7",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
+ "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
+ "dev": true
+ }
+ }
+ },
"react-refresh": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
@@ -37093,6 +37129,16 @@
"esprima": "~4.0.0"
}
},
+ "redux": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
+ "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
+ "dev": true,
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "symbol-observable": "^1.2.0"
+ }
+ },
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
diff --git a/package.json b/package.json
index d286e6ea..df948dc8 100644
--- a/package.json
+++ b/package.json
@@ -114,9 +114,11 @@
"react-google-maps": "9.4.5",
"react-i18next": "11.7.2",
"react-loading-skeleton": "2.0.1",
+ "react-redux": "7.2.2",
"react-router": "6.0.0-alpha.1",
"react-router-dom": "6.0.0-alpha.1",
"react-scripts": "3.4.1",
+ "redux": "4.0.5",
"rollup-plugin-istanbul": "2.0.1",
"semantic-release": "17.2.2",
"standard": "14.3.3",