Skip to content
This repository has been archived by the owner on Mar 5, 2022. It is now read-only.

Commit

Permalink
feat: add wrapper option to mountHook function (#536)
Browse files Browse the repository at this point in the history
  • Loading branch information
bahmutov authored Nov 14, 2020
2 parents 6481d07 + 593954c commit 22fd85f
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 10 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions cypress/component/advanced/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => <Provider store={store}>{children}</Provider>
mountHook(() => useCustomHook(), { wrapper })
```
10 changes: 10 additions & 0 deletions cypress/component/advanced/hooks/count-reducer.js
Original file line number Diff line number Diff line change
@@ -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
}
}
10 changes: 10 additions & 0 deletions cypress/component/advanced/hooks/custom-hook.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useEffect } from 'react'
import { useDispatch } from 'react-redux'

export function useCustomHook() {
useDispatch()

useEffect(() => {
console.log('hello world!')
}, [])
}
20 changes: 20 additions & 0 deletions cypress/component/advanced/hooks/custom-hook.mount-hook-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference types="cypress" />
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 }) => (
<Provider store={store}>{children}</Provider>
)

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!')
})
})
23 changes: 23 additions & 0 deletions cypress/component/advanced/hooks/custom-hook.mount-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// <reference types="cypress" />
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(
<Provider store={store}>
<App />
</Provider>,
)
cy.get('@log').should('have.been.calledWith', 'hello world!')
})
})
6 changes: 6 additions & 0 deletions cypress/component/advanced/hooks/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { createStore } from 'redux'
import { countReducer } from './count-reducer'

const store = createStore(countReducer)

export default store
34 changes: 26 additions & 8 deletions lib/mountHook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
46 changes: 46 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 22fd85f

Please sign in to comment.