├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── samples-and-e2e-tests ├── cra-with-mocking-by-default │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── setupTests.js │ │ └── use-effect-and-use-layout-effect │ │ │ ├── callback.js │ │ │ ├── cleanup.js │ │ │ ├── components.test.jsx │ │ │ ├── use-effect-component.jsx │ │ │ └── use-layout-effect-component.jsx │ └── yarn.lock ├── cra-without-mocking-by-default │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── setupTests.js │ │ └── use-effect-and-use-layout-effect │ │ │ ├── callback.js │ │ │ ├── cleanup.js │ │ │ ├── components.test.jsx │ │ │ ├── use-effect-component.jsx │ │ │ └── use-layout-effect-component.jsx │ └── yarn.lock ├── with-mocking-by-default-reset-mocks-true │ ├── babel.config.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── setupTests.js │ ├── src │ │ ├── index.html │ │ ├── index.jsx │ │ ├── material-ui │ │ │ ├── material-ui-component.jsx │ │ │ └── material-ui-component.test.jsx │ │ └── use-effect-and-use-layout-effect │ │ │ ├── callback.js │ │ │ ├── cleanup.js │ │ │ ├── components.test.jsx │ │ │ ├── use-effect-component.jsx │ │ │ └── use-layout-effect-component.jsx │ └── webpack.config.js ├── with-mocking-by-default │ ├── babel.config.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── setupTests.js │ ├── src │ │ ├── index.html │ │ ├── index.jsx │ │ ├── material-ui │ │ │ ├── material-ui-component.jsx │ │ │ └── material-ui-component.test.jsx │ │ └── use-effect-and-use-layout-effect │ │ │ ├── callback.js │ │ │ ├── cleanup.js │ │ │ ├── components.test.jsx │ │ │ ├── use-effect-component.jsx │ │ │ └── use-layout-effect-component.jsx │ └── webpack.config.js └── without-mocking-by-default │ ├── babel.config.js │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── setupTests.js │ ├── src │ ├── index.html │ ├── index.jsx │ ├── material-ui │ │ ├── material-ui-component.jsx │ │ └── material-ui-component.test.jsx │ └── use-effect-and-use-layout-effect │ │ ├── callback.js │ │ ├── cleanup.js │ │ ├── components.test.jsx │ │ ├── use-effect-component.jsx │ │ └── use-layout-effect-component.jsx │ └── webpack.config.js ├── src ├── enable-hooks.ts └── mock-use-effect │ ├── mock-use-effect.test.ts │ └── mock-use-effect.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | ], 12 | ignorePatterns: 'sample-tests-and-e2e-tests' 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | samples-and-e2e-tests 2 | src 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Mike Borozdin { 39 | const [text, setText] = useState<>(); 40 | const [buttonClicked, setButtonClicked] = useState(false); 41 | 42 | useEffect(() => setText( 43 | `Button clicked: ${buttonClicked.toString()}`), 44 | [buttonClicked] 45 | ); 46 | 47 | return ( 48 |
49 |
{text}
50 | 51 |
52 | ); 53 | }; 54 | ``` 55 | 56 | You can easily test it with code like this: 57 | 58 | ```js 59 | test('Renders default message and updates it on clicking a button', () => { 60 | const component = shallow(); 61 | 62 | expect(component.text()).toContain('Button clicked: false'); 63 | 64 | component.find('button').simulate('click'); 65 | 66 | expect(component.text()).toContain('Button clicked: true'); 67 | }); 68 | ``` 69 | 70 | Please note, that those tests didn't have to import anything else. They simply don't know that a component calls `useEffect()`. Yet, it's being called when you invoke `shallow()`. 71 | 72 | That said, often you want to test that a specific function has been called on some event. For example, you're calling a Redux action creator or a Mobx action. If you're using React Hooks, chances are you'll pass that function as a callback to `useEffect()`. 73 | 74 | No problems! You can easily test it with simple Jest mocks. 75 | 76 | Say, we have a component like this: 77 | 78 | ```js 79 | import someAction from './some-action'; 80 | 81 | const ComponentWithHooks = () => { 82 | const [text, setText] = useState<>(); 83 | const [buttonClicked, setButtonClicked] = useState(false); 84 | 85 | useEffect(someAction, [buttonClicked]); 86 | 87 | return ( 88 |
89 |
{text}
90 | 91 |
92 | ); 93 | }; 94 | ``` 95 | 96 | ```js 97 | test('Calls `myAction()` on the first render and on clicking the button`', () => { 98 | const component = shallow(); 99 | expect(callback).toHaveBeenCalledTimes(1); 100 | 101 | component.find('button').simulate('click'); 102 | expect(callback).toHaveBeenCalledTimes(2); 103 | }); 104 | ``` 105 | 106 | Usage with `mount()` 107 | ==== 108 | 109 | There have been a reported problems about using the library on tests that rely on `mount()`. 110 | 111 | You don't need this library to trigger `useEffect()` and `useLayoutEffect()` when doing full rendering with `mount()`. However, you may have a mix of tests that rely both rely on `shallow()` and `mount()`. In those cases, you may run into issues with the tests that call `mount()`. 112 | 113 | Version `1.4.0` of this library provides two solutions for that: 114 | 115 | * Initialise the library with `enableHooks(jest, { dontMockByDefault: true })` and wrap tests for hook components relying on `shallow()` with `withHooks()` 116 | * That's useful when you have a lot of tests with `mount()` 117 | * Wrap `mount()`-based tests for hooks components with `withoutHooks()`. 118 | 119 | Option #1 - `{ dontMockByDefault: true }` 120 | ---- 121 | 122 | That will disable effect hooks mocks by default. And you can wrap tests with that rely on `shallow()` and hooks with `withHooks()`, e.g.: 123 | 124 | **setupJest.js** 125 | ```js 126 | import enableHooks from 'jest-react-hooks-shallow'; 127 | 128 | // pass an instance of jest to `enableHooks()` 129 | enableHooks(jest, { dontMockByDefault: true }); 130 | ``` 131 | 132 | **App.test.js** 133 | ```js 134 | import { withHooks } from 'jest-react-hooks-shallow'; 135 | 136 | test('Shallow rendering of component with hooks', () => { 137 | withHooks(() => { 138 | 139 | const component = shallow(); 140 | // your test code 141 | }); 142 | }); 143 | ``` 144 | 145 | Option #2 - `withoutHook()` 146 | ---- 147 | 148 | Or you can enable hooks in shallow by default and surround tests using `mount()` with `withoutHooks()`, e.g.: 149 | 150 | ```js 151 | import { withoutHooks } from 'jest-react-hooks-shallow'; 152 | 153 | test('Full rendering of component with hooks', () => { 154 | withoutHooks(() => { 155 | 156 | const component = mount(); 157 | // your test code 158 | }); 159 | }); 160 | ``` 161 | 162 | `disableHooks()` and `reenableHooks()` are now deprecated 163 | ---- 164 | 165 | `disableHooks()` and `reenableHooks()` from version `1.3.0` are now marked deprecated. You can still use them, but it's not recommended.`. 166 | 167 | 168 | Examples 169 | ---- 170 | Please, see two samples projects in the [samples-and-e2e-tests](samples-and-e2e-tests/) as an example. 171 | 172 | How does that work? 173 | ---- 174 | You enable `useEffect()`/`useLayoutEffect()` by calling `enableHooks()` in a file specified by `setupFilesAfterEnv` in the Jest configuration. The code in that code will be called before each test suite. 175 | 176 | So if in our test suite you call `disableHooks()` it will not affect the other ones. But it will disable hooks in shallow rendering for the tests that come after the one with `disableHooks()`. So if any the tests defined after need shallow rendering and hooks, just call `reenableHooks()`. 177 | 178 | Dependencies 179 | ==== 180 | 181 | This library expects that you use Jest as a testing library. 182 | 183 | Frankly speaking, you don't have to use enzyme, as it's not the only library which providers shallow rendering. In fact, it doesn't even implement shadow rendering. See the Long Story for details. 184 | 185 | Hooks Support Status 186 | ==== 187 | |Hook|Support| 188 | |-----|------| 189 | |`useEffect`|✅| 190 | |`useLayoutEffect`|✅| 191 | |`useImperativeHandle`|Coming soon| 192 | |`useDebugValue`|No support plans| 193 | 194 | All other hooks (e.g. `useState()`, `useReducer()`) already work with shallow rendering. 195 | 196 | FAQ 197 | === 198 | Q: Does it call cleanup functions? 199 | 200 | A: Yes, it does, but only before calling the same effect again. It won't call cleanup functions when component gets unmounted. That's because, unfortunately, this library doesn't have access to the component life cycle. 201 | 202 | 203 | Q: I'm getting a `beforeEach is not defined` error after adding the lines to my Jest setup file. 204 | 205 | A: There's a quick fix for this. We want to pass an instance of Jest to `enableHooks` AFTER the environment is set up. So move `enableHooks(jest);` into a separate file in the same folder as your Jest setup file (ex. `enableHooks.js`) and call it in your Jest setup file like this: 206 | 207 | ``` 208 | module.exports = { 209 | ... 210 | setupFilesAfterEnv: ['./enableHooks.js'], 211 | ... 212 | } 213 | ``` 214 | 215 | 216 | Long Story 217 | ==== 218 | 219 | Context 220 | ---- 221 | 222 | In case, you wonder why I have created this package instead of extending enzyme, here's a slightly longer story. 223 | 224 | Actually, it's not enzyme's per se fault that `useEffect()` doesn't work in shallow rendering. It relies on `react-test-renderer` for some aspects of shallow rendering. And it is `react-test-renderer` that implements certain hooks, like `useState()` and does not implement the other ones (e.g. `useEffect()`). 225 | 226 | Now, `react-test-renderer` is part of the React library. And there is a [PR](https://github.com/facebook/react/pull/16168) that brings `useEffect()` to shallow rendering. However, that PR has been closed by Facebook. 227 | 228 | According to the comments on the same PR, there are plans to spin off `react-test-renderer` as a separate package. And indeed, the `master` branch of React does have it as an NPM dependency. However, all currently released versions of React have `react-test-renderer` built-in. 229 | 230 | Once it's a standalone package, I believe it'll be easier to merge the said PR and the need for this library will go away. 231 | 232 | How this Library is Implemented 233 | ---- 234 | 235 | If someone wonders how this library is implemented, then we just provide a naïve implementation of `useEffect()`. After all, it's just a function that takes two arguments and executes the first one, if the values of the second argument change (or the function is called for the first time with them or the second argument is undefined). 236 | 237 | Okay, the actual implementation of React does have knowledge on whether it's the first render or the second. 238 | 239 | We don't, so we have to rely on mapping callback functions to a list of dependencies (the second argument). If `useEffect()` is called with the same function, but with different dependency values, we will execute that function. 240 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testPathIgnorePatterns: ['samples-and-e2e-tests'] 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-react-hooks-shallow", 3 | "version": "1.5.1", 4 | "description": "React Hooks for shallow rendering", 5 | "homepage": "https://github.com/mikeborozdin/jest-react-hooks-shallow", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/mikeborozdin/jest-react-hooks-shallow" 9 | }, 10 | "main": "lib/enable-hooks.js", 11 | "scripts": { 12 | "build": "tsc", 13 | "test:unit": "jest", 14 | "test:e2e:with-mocking-by-default": "cd samples-and-e2e-tests/with-mocking-by-default && npm i && npm test && cd ../..", 15 | "test:e2e:without-mocking-by-default": "cd samples-and-e2e-tests/without-mocking-by-default && npm i && npm test && cd ../..", 16 | "test:e2e:cra-with-mocking-by-default": "cd samples-and-e2e-tests/cra-with-mocking-by-default && npm i && npm test && cd ../..", 17 | "test:e2e:cra-without-mocking-by-default": "cd samples-and-e2e-tests/cra-without-mocking-by-default && npm i && npm test && cd ../..", 18 | "test:e2e": "npm run test:e2e:with-mocking-by-default && npm run test:e2e:with-mocking-by-default && npm run test:e2e:without-mocking-by-default", 19 | "test": "npm run test:unit && npm run test:e2e", 20 | "lint": "eslint ./src/**/*.ts" 21 | }, 22 | "author": "Mike Borozdin ", 23 | "license": "ISC", 24 | "keywords": [ 25 | "react", 26 | "hooks", 27 | "react hooks", 28 | "shallow", 29 | "testing", 30 | "enzyme", 31 | "mock", 32 | "jest" 33 | ], 34 | "devDependencies": { 35 | "@types/jest": "^25.1.3", 36 | "@typescript-eslint/eslint-plugin": "^2.22.0", 37 | "@typescript-eslint/parser": "^2.22.0", 38 | "eslint": "^6.8.0", 39 | "jest": "^25.1.0", 40 | "ts-jest": "^25.4.0", 41 | "typescript": "^3.8.3" 42 | }, 43 | "dependencies": { 44 | "react": "^16.8.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-with-mocking-by-default", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-scripts": "4.0.1", 12 | "web-vitals": "^0.2.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --watchAll=false", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@wojtekmaj/enzyme-adapter-react-17": "^0.4.1", 40 | "enzyme": "^3.11.0", 41 | "jest-react-hooks-shallow": "../.." 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | 7 | import Enzyme from 'enzyme'; 8 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 9 | import enableHooks from 'jest-react-hooks-shallow'; 10 | 11 | Enzyme.configure({ adapter: new Adapter() }); 12 | 13 | enableHooks(jest); 14 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/src/use-effect-and-use-layout-effect/callback.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('callback'); }; 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/src/use-effect-and-use-layout-effect/cleanup.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('cleanup'); } 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/src/use-effect-and-use-layout-effect/components.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import UseEffectComponent from './use-effect-component'; 4 | import UseLayoutEffectComponent from './use-layout-effect-component'; 5 | import callback from './callback'; 6 | import cleanup from './cleanup'; 7 | import { withoutHooks } from 'jest-react-hooks-shallow'; 8 | 9 | jest.mock('./callback', () => jest.fn()); 10 | jest.mock('./cleanup', () => jest.fn()) 11 | 12 | const tests = (Component) => { 13 | test('effect is called on first render and then on a button press', () => { 14 | const component = shallow(); 15 | 16 | expect(component.text()).toContain('false'); 17 | component.find('button').simulate('click'); 18 | expect(component.text()).toContain('true'); 19 | }); 20 | 21 | test('effect is mockable', () => { 22 | const component = shallow(); 23 | 24 | expect(component.text()).toContain('false'); 25 | 26 | expect(callback).toHaveBeenCalledTimes(1); 27 | 28 | component.find('button').simulate('click'); 29 | 30 | expect(callback).toHaveBeenCalledTimes(2); 31 | }); 32 | 33 | test('effects mockable when used with mount() and withoutHooks', () => { 34 | withoutHooks(() => { 35 | const component = mount(); 36 | 37 | expect(component.text()).toContain('false'); 38 | 39 | expect(callback).toHaveBeenCalledTimes(1); 40 | 41 | component.find('button').simulate('click'); 42 | 43 | expect(callback).toHaveBeenCalledTimes(2); 44 | }); 45 | }); 46 | 47 | test('cleanup function', () => { 48 | expect(cleanup).toHaveBeenCalledTimes(0); 49 | 50 | const component = shallow(); 51 | 52 | component.find('button').simulate('click'); 53 | 54 | expect(cleanup).toHaveBeenCalledTimes(1); 55 | }); 56 | } 57 | 58 | describe('useEffect', () => { 59 | beforeEach(() => { 60 | jest.clearAllMocks(); 61 | }); 62 | 63 | tests(UseEffectComponent); 64 | }); 65 | 66 | // describe('useLayoutEffect', () => { 67 | // beforeEach(() => { 68 | // jest.clearAllMocks(); 69 | // }); 70 | 71 | // tests(UseLayoutEffectComponent); 72 | // }); 73 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/src/use-effect-and-use-layout-effect/use-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseEffectComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseEffectComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-with-mocking-by-default/src/use-effect-and-use-layout-effect/use-layout-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseLayoutComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useLayoutEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseLayoutComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-without-mocking-by-default", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "react": "^17.0.1", 10 | "react-dom": "^17.0.1", 11 | "react-scripts": "4.0.1", 12 | "web-vitals": "^0.2.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": [ 22 | "react-app", 23 | "react-app/jest" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "devDependencies": { 39 | "@wojtekmaj/enzyme-adapter-react-17": "^0.4.1", 40 | "enzyme": "^3.11.0", 41 | "jest-react-hooks-shallow": "../../" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | 7 | import Enzyme from 'enzyme'; 8 | import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; 9 | import enableHooks from 'jest-react-hooks-shallow'; 10 | 11 | Enzyme.configure({ adapter: new Adapter() }); 12 | 13 | enableHooks(jest, { dontMockByDefault: true }); 14 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/src/use-effect-and-use-layout-effect/callback.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('callback'); }; 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/src/use-effect-and-use-layout-effect/cleanup.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('cleanup'); } 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/src/use-effect-and-use-layout-effect/components.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import UseEffectComponent from './use-effect-component'; 4 | import UseLayoutEffectComponent from './use-layout-effect-component'; 5 | import callback from './callback'; 6 | import cleanup from './cleanup'; 7 | import { withHooks } from 'jest-react-hooks-shallow'; 8 | 9 | jest.mock('./callback', () => jest.fn()); 10 | jest.mock('./cleanup', () => jest.fn()) 11 | 12 | const tests = (Component) => { 13 | test('effect is not called when not using withHooks', () => { 14 | const component = shallow(); 15 | 16 | expect(component.text()).toContain('Press me'); 17 | expect(callback).not.toHaveBeenCalled(); 18 | }); 19 | 20 | test('effect is called on first render and then on a button press', () => { 21 | withHooks(() => { 22 | const component = shallow(); 23 | 24 | expect(component.text()).toContain('false'); 25 | component.find('button').simulate('click'); 26 | expect(component.text()).toContain('true'); 27 | }); 28 | }); 29 | 30 | test('effect is mockable', () => { 31 | withHooks(() => { 32 | const component = shallow(); 33 | 34 | expect(component.text()).toContain('false'); 35 | 36 | expect(callback).toHaveBeenCalledTimes(1); 37 | 38 | component.find('button').simulate('click'); 39 | 40 | expect(callback).toHaveBeenCalledTimes(2); 41 | }); 42 | }); 43 | 44 | test('effects mockable when used with mount() and without withHooks', () => { 45 | const component = mount(); 46 | 47 | expect(component.text()).toContain('false'); 48 | 49 | expect(callback).toHaveBeenCalledTimes(1); 50 | 51 | component.find('button').simulate('click'); 52 | 53 | expect(callback).toHaveBeenCalledTimes(2); 54 | 55 | }); 56 | 57 | test('cleanup function', () => { 58 | withHooks(() => { 59 | expect(cleanup).toHaveBeenCalledTimes(0); 60 | 61 | const component = shallow(); 62 | 63 | component.find('button').simulate('click'); 64 | 65 | expect(cleanup).toHaveBeenCalledTimes(1); 66 | }); 67 | }); 68 | } 69 | 70 | describe('useEffect', () => { 71 | beforeEach(() => { 72 | jest.clearAllMocks(); 73 | }); 74 | 75 | tests(UseEffectComponent); 76 | }); 77 | 78 | // describe('useLayoutEffect', () => { 79 | // beforeEach(() => { 80 | // jest.clearAllMocks(); 81 | // }); 82 | 83 | // tests(UseLayoutEffectComponent); 84 | // }); 85 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/src/use-effect-and-use-layout-effect/use-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseEffectComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseEffectComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/cra-without-mocking-by-default/src/use-effect-and-use-layout-effect/use-layout-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseLayoutComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useLayoutEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseLayoutComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | "@babel/preset-react", 4 | '@babel/preset-env' 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.[j]sx?$": "babel-jest" 4 | }, 5 | setupFilesAfterEnv: ['setupTests.js'], 6 | resetMocks: true 7 | }; 8 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-react-hooks-shallow-e2e-with-mocking-by-default", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config ./webpack.config.js --mode development --env=dev", 8 | "test": "jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.4.3", 14 | "@babel/preset-env": "^7.8.7", 15 | "@babel/preset-react": "^7.8.3", 16 | "@hot-loader/react-dom": "^16.11.0", 17 | "babel-jest": "^25.1.0", 18 | "babel-loader": "^8.0.6", 19 | "enzyme": "^3.11.0", 20 | "enzyme-adapter-react-16": "^1.12.1", 21 | "html-webpack-plugin": "^3.2.0", 22 | "jest": "^26.6.3", 23 | "jest-react-hooks-shallow": "../..", 24 | "react-hot-loader": "^4.12.18", 25 | "source-map-loader": "^0.2.4", 26 | "webpack": "^4.30.0", 27 | "webpack-cli": "^3.3.1", 28 | "webpack-dev-server": "^3.3.1" 29 | }, 30 | "dependencies": { 31 | "@material-ui/core": "^4.9.12", 32 | "material-ui": "^0.20.2", 33 | "react": "^16.8.6", 34 | "react-dom": "^16.8.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | import enableHooks from 'jest-react-hooks-shallow'; 4 | 5 | configure({ adapter: new Adapter() }); 6 | 7 | enableHooks(jest); 8 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jest-react-hooks-shallow 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/index.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import UseEffectComponent from './use-effect/use-effect-component'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('app'), 8 | ); 9 | 10 | module.hot.accept(); 11 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/material-ui/material-ui-component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | 4 | const MaterialUiComponent = () => { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default MaterialUiComponent; 11 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/material-ui/material-ui-component.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import MaterialUiComponent from './material-ui-component'; 4 | import { disableHooks,withoutHooks } from 'jest-react-hooks-shallow'; 5 | 6 | describe('MaterialUiComponent', () => { 7 | test('Renders a component with Material-UI without errors when using withoutHooks', () => { 8 | withoutHooks(() => { 9 | expect(() => mount()).not.toThrow(); 10 | }); 11 | }); 12 | 13 | test('Renders a component with Material-UI without errors when using disableHooks', () => { 14 | disableHooks(); 15 | 16 | expect(() => mount()).not.toThrow(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/use-effect-and-use-layout-effect/callback.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('callback'); }; 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/use-effect-and-use-layout-effect/cleanup.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('cleanup'); } 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/use-effect-and-use-layout-effect/components.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import UseEffectComponent from './use-effect-component'; 4 | import UseLayoutEffectComponent from './use-layout-effect-component'; 5 | import callback from './callback'; 6 | import cleanup from './cleanup'; 7 | import { withoutHooks } from 'jest-react-hooks-shallow'; 8 | 9 | jest.mock('./callback', () => jest.fn()); 10 | jest.mock('./cleanup', () => jest.fn()) 11 | 12 | const tests = (Component) => { 13 | test('effect is called on first render and then on a button press', () => { 14 | const component = shallow(); 15 | 16 | expect(component.text()).toContain('false'); 17 | component.find('button').simulate('click'); 18 | expect(component.text()).toContain('true'); 19 | }); 20 | 21 | test('effect is mockable', () => { 22 | const component = shallow(); 23 | 24 | expect(component.text()).toContain('false'); 25 | 26 | expect(callback).toHaveBeenCalledTimes(1); 27 | 28 | component.find('button').simulate('click'); 29 | 30 | expect(callback).toHaveBeenCalledTimes(2); 31 | }); 32 | 33 | test('effects mockable when used with mount() and withoutHooks', () => { 34 | withoutHooks(() => { 35 | const component = mount(); 36 | 37 | expect(component.text()).toContain('false'); 38 | 39 | expect(callback).toHaveBeenCalledTimes(1); 40 | 41 | component.find('button').simulate('click'); 42 | 43 | expect(callback).toHaveBeenCalledTimes(2); 44 | }); 45 | }); 46 | 47 | test('cleanup function', () => { 48 | expect(cleanup).toHaveBeenCalledTimes(0); 49 | 50 | const component = shallow(); 51 | 52 | component.find('button').simulate('click'); 53 | 54 | expect(cleanup).toHaveBeenCalledTimes(1); 55 | }); 56 | } 57 | 58 | describe('useEffect', () => { 59 | beforeEach(() => { 60 | jest.clearAllMocks(); 61 | }); 62 | 63 | tests(UseEffectComponent); 64 | }); 65 | 66 | describe('useLayoutEffect', () => { 67 | beforeEach(() => { 68 | jest.clearAllMocks(); 69 | }); 70 | 71 | tests(UseLayoutEffectComponent); 72 | }); 73 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/use-effect-and-use-layout-effect/use-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseEffectComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseEffectComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/src/use-effect-and-use-layout-effect/use-layout-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseLayoutComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useLayoutEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseLayoutComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default-reset-mocks-true/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = (env) => { 5 | return { 6 | entry: ['@hot-loader/react-dom', './src/index.jsx'], 7 | output: { 8 | filename: '[name].[hash].js', 9 | path: __dirname + '/dist', 10 | }, 11 | // Enable sourcemaps for debugging webpack's output. 12 | devtool: env === 'production' ? false : 'source-map', 13 | resolve: { 14 | extensions: ['.js', '.jsx'], 15 | alias: { 16 | 'react-dom': '@hot-loader/react-dom', 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|jsx)$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader', 26 | }, 27 | }, 28 | { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, 29 | ] 30 | }, 31 | plugins: [ 32 | new webpack.HotModuleReplacementPlugin(), 33 | new HtmlWebPackPlugin({ template: './src/index.html', favicon: "./src/favicon.ico" }) 34 | ], 35 | devServer: { 36 | contentBase: './dist', 37 | hot: true, 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | "@babel/preset-react", 4 | '@babel/preset-env' 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.[j]sx?$": "babel-jest" 4 | }, 5 | setupFilesAfterEnv: ['setupTests.js'], 6 | }; 7 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-react-hooks-shallow-e2e-with-mocking-by-default", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config ./webpack.config.js --mode development --env=dev", 8 | "test": "jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.4.3", 14 | "@babel/preset-env": "^7.8.7", 15 | "@babel/preset-react": "^7.8.3", 16 | "@hot-loader/react-dom": "^16.11.0", 17 | "babel-jest": "^25.1.0", 18 | "babel-loader": "^8.0.6", 19 | "enzyme": "^3.11.0", 20 | "enzyme-adapter-react-16": "^1.12.1", 21 | "html-webpack-plugin": "^3.2.0", 22 | "jest": "^25.1.0", 23 | "jest-react-hooks-shallow": "../..", 24 | "react-hot-loader": "^4.12.18", 25 | "source-map-loader": "^0.2.4", 26 | "webpack": "^4.30.0", 27 | "webpack-cli": "^3.3.1", 28 | "webpack-dev-server": "^3.3.1" 29 | }, 30 | "dependencies": { 31 | "@material-ui/core": "^4.9.12", 32 | "material-ui": "^0.20.2", 33 | "react": "^16.8.6", 34 | "react-dom": "^16.8.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | import enableHooks from 'jest-react-hooks-shallow'; 4 | 5 | configure({ adapter: new Adapter() }); 6 | 7 | enableHooks(jest); 8 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jest-react-hooks-shallow 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/index.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import UseEffectComponent from './use-effect/use-effect-component'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('app'), 8 | ); 9 | 10 | module.hot.accept(); 11 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/material-ui/material-ui-component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | 4 | const MaterialUiComponent = () => { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default MaterialUiComponent; 11 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/material-ui/material-ui-component.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import MaterialUiComponent from './material-ui-component'; 4 | import { disableHooks,withoutHooks } from 'jest-react-hooks-shallow'; 5 | 6 | describe('MaterialUiComponent', () => { 7 | test('Renders a component with Material-UI without errors when using withoutHooks', () => { 8 | withoutHooks(() => { 9 | expect(() => mount()).not.toThrow(); 10 | }); 11 | }); 12 | 13 | test('Renders a component with Material-UI without errors when using disableHooks', () => { 14 | disableHooks(); 15 | 16 | expect(() => mount()).not.toThrow(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/use-effect-and-use-layout-effect/callback.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('callback'); }; 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/use-effect-and-use-layout-effect/cleanup.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('cleanup'); } 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/use-effect-and-use-layout-effect/components.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import UseEffectComponent from './use-effect-component'; 4 | import UseLayoutEffectComponent from './use-layout-effect-component'; 5 | import callback from './callback'; 6 | import cleanup from './cleanup'; 7 | import { withoutHooks } from 'jest-react-hooks-shallow'; 8 | 9 | jest.mock('./callback', () => jest.fn()); 10 | jest.mock('./cleanup', () => jest.fn()) 11 | 12 | const tests = (Component) => { 13 | test('effect is called on first render and then on a button press', () => { 14 | const component = shallow(); 15 | 16 | expect(component.text()).toContain('false'); 17 | component.find('button').simulate('click'); 18 | expect(component.text()).toContain('true'); 19 | }); 20 | 21 | test('effect is mockable', () => { 22 | const component = shallow(); 23 | 24 | expect(component.text()).toContain('false'); 25 | 26 | expect(callback).toHaveBeenCalledTimes(1); 27 | 28 | component.find('button').simulate('click'); 29 | 30 | expect(callback).toHaveBeenCalledTimes(2); 31 | }); 32 | 33 | test('effects mockable when used with mount() and withoutHooks', () => { 34 | withoutHooks(() => { 35 | const component = mount(); 36 | 37 | expect(component.text()).toContain('false'); 38 | 39 | expect(callback).toHaveBeenCalledTimes(1); 40 | 41 | component.find('button').simulate('click'); 42 | 43 | expect(callback).toHaveBeenCalledTimes(2); 44 | }); 45 | }); 46 | 47 | test('cleanup function', () => { 48 | expect(cleanup).toHaveBeenCalledTimes(0); 49 | 50 | const component = shallow(); 51 | 52 | component.find('button').simulate('click'); 53 | 54 | expect(cleanup).toHaveBeenCalledTimes(1); 55 | }); 56 | } 57 | 58 | describe('useEffect', () => { 59 | beforeEach(() => { 60 | jest.clearAllMocks(); 61 | }); 62 | 63 | tests(UseEffectComponent); 64 | }); 65 | 66 | describe('useLayoutEffect', () => { 67 | beforeEach(() => { 68 | jest.clearAllMocks(); 69 | }); 70 | 71 | tests(UseLayoutEffectComponent); 72 | }); 73 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/use-effect-and-use-layout-effect/use-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseEffectComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseEffectComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/src/use-effect-and-use-layout-effect/use-layout-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseLayoutComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useLayoutEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseLayoutComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/with-mocking-by-default/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = (env) => { 5 | return { 6 | entry: ['@hot-loader/react-dom', './src/index.jsx'], 7 | output: { 8 | filename: '[name].[hash].js', 9 | path: __dirname + '/dist', 10 | }, 11 | // Enable sourcemaps for debugging webpack's output. 12 | devtool: env === 'production' ? false : 'source-map', 13 | resolve: { 14 | extensions: ['.js', '.jsx'], 15 | alias: { 16 | 'react-dom': '@hot-loader/react-dom', 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|jsx)$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader', 26 | }, 27 | }, 28 | { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, 29 | ] 30 | }, 31 | plugins: [ 32 | new webpack.HotModuleReplacementPlugin(), 33 | new HtmlWebPackPlugin({ template: './src/index.html', favicon: "./src/favicon.ico" }) 34 | ], 35 | devServer: { 36 | contentBase: './dist', 37 | hot: true, 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | "@babel/preset-react", 4 | '@babel/preset-env' 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.[j]sx?$": "babel-jest" 4 | }, 5 | setupFilesAfterEnv: ['setupTests.js'], 6 | }; 7 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-react-hooks-shallow-e2e-without-mocking-by-default", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config ./webpack.config.js --mode development --env=dev", 8 | "test": "jest" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/core": "^7.4.3", 14 | "@babel/preset-env": "^7.8.7", 15 | "@babel/preset-react": "^7.8.3", 16 | "@hot-loader/react-dom": "^16.11.0", 17 | "babel-jest": "^25.1.0", 18 | "babel-loader": "^8.0.6", 19 | "enzyme": "^3.11.0", 20 | "enzyme-adapter-react-16": "^1.12.1", 21 | "html-webpack-plugin": "^3.2.0", 22 | "jest": "^25.1.0", 23 | "jest-react-hooks-shallow": "../../", 24 | "react-hot-loader": "^4.12.18", 25 | "source-map-loader": "^0.2.4", 26 | "webpack": "^4.30.0", 27 | "webpack-cli": "^3.3.1", 28 | "webpack-dev-server": "^3.3.1" 29 | }, 30 | "dependencies": { 31 | "@material-ui/core": "^4.9.12", 32 | "material-ui": "^0.20.2", 33 | "react": "^16.8.6", 34 | "react-dom": "^16.8.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | import enableHooks from 'jest-react-hooks-shallow'; 4 | 5 | configure({ adapter: new Adapter() }); 6 | 7 | enableHooks(jest, { dontMockByDefault: true }); 8 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | jest-react-hooks-shallow 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/index.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import UseEffectComponent from './use-effect/use-effect-component'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById('app'), 8 | ); 9 | 10 | module.hot.accept(); 11 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/material-ui/material-ui-component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TextField from '@material-ui/core/TextField'; 3 | 4 | const MaterialUiComponent = () => { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default MaterialUiComponent; 11 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/material-ui/material-ui-component.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import MaterialUiComponent from './material-ui-component'; 4 | 5 | describe('MaterialUiComponent', () => { 6 | test('Renders a component with Material-UI without errors', () => { 7 | expect(() => mount()).not.toThrow(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/use-effect-and-use-layout-effect/callback.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('callback'); }; 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/use-effect-and-use-layout-effect/cleanup.js: -------------------------------------------------------------------------------- 1 | export default () => { console.log('cleanup'); } 2 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/use-effect-and-use-layout-effect/components.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow, mount } from 'enzyme'; 3 | import UseEffectComponent from './use-effect-component'; 4 | import UseLayoutEffectComponent from './use-layout-effect-component'; 5 | import callback from './callback'; 6 | import cleanup from './cleanup'; 7 | import { withHooks } from 'jest-react-hooks-shallow'; 8 | 9 | jest.mock('./callback', () => jest.fn()); 10 | jest.mock('./cleanup', () => jest.fn()) 11 | 12 | const tests = (Component) => { 13 | test('effect is not called when not using withHooks', () => { 14 | const component = shallow(); 15 | 16 | expect(component.text()).toContain('Press me'); 17 | expect(callback).not.toHaveBeenCalled(); 18 | }); 19 | 20 | test('effect is called on first render and then on a button press', () => { 21 | withHooks(() => { 22 | const component = shallow(); 23 | 24 | expect(component.text()).toContain('false'); 25 | component.find('button').simulate('click'); 26 | expect(component.text()).toContain('true'); 27 | }); 28 | }); 29 | 30 | test('effect is mockable', () => { 31 | withHooks(() => { 32 | const component = shallow(); 33 | 34 | expect(component.text()).toContain('false'); 35 | 36 | expect(callback).toHaveBeenCalledTimes(1); 37 | 38 | component.find('button').simulate('click'); 39 | 40 | expect(callback).toHaveBeenCalledTimes(2); 41 | }); 42 | }); 43 | 44 | test('effects mockable when used with mount() and without withHooks', () => { 45 | const component = mount(); 46 | 47 | expect(component.text()).toContain('false'); 48 | 49 | expect(callback).toHaveBeenCalledTimes(1); 50 | 51 | component.find('button').simulate('click'); 52 | 53 | expect(callback).toHaveBeenCalledTimes(2); 54 | 55 | }); 56 | 57 | test('cleanup function', () => { 58 | withHooks(() => { 59 | expect(cleanup).toHaveBeenCalledTimes(0); 60 | 61 | const component = shallow(); 62 | 63 | component.find('button').simulate('click'); 64 | 65 | expect(cleanup).toHaveBeenCalledTimes(1); 66 | }); 67 | }); 68 | } 69 | 70 | describe('useEffect', () => { 71 | beforeEach(() => { 72 | jest.clearAllMocks(); 73 | }); 74 | 75 | tests(UseEffectComponent); 76 | }); 77 | 78 | describe('useLayoutEffect', () => { 79 | beforeEach(() => { 80 | jest.clearAllMocks(); 81 | }); 82 | 83 | tests(UseLayoutEffectComponent); 84 | }); 85 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/use-effect-and-use-layout-effect/use-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseEffectComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseEffectComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/src/use-effect-and-use-layout-effect/use-layout-effect-component.jsx: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useState } from 'react'; 2 | import callback from './callback'; 3 | import cleanup from './cleanup'; 4 | 5 | const UseLayoutComponent = () => { 6 | const [text, setText] = useState(); 7 | const [buttonClicked, setButtonClicked] = useState(false); 8 | 9 | useLayoutEffect(() => { 10 | setText(`Button pressed: ${buttonClicked.toString()}`); 11 | 12 | callback(); 13 | 14 | return cleanup; 15 | }, [buttonClicked]); 16 | 17 | return ( 18 |
19 |
{text}
20 | 21 |
22 | ); 23 | }; 24 | 25 | export default UseLayoutComponent; 26 | -------------------------------------------------------------------------------- /samples-and-e2e-tests/without-mocking-by-default/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const HtmlWebPackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = (env) => { 5 | return { 6 | entry: ['@hot-loader/react-dom', './src/index.jsx'], 7 | output: { 8 | filename: '[name].[hash].js', 9 | path: __dirname + '/dist', 10 | }, 11 | // Enable sourcemaps for debugging webpack's output. 12 | devtool: env === 'production' ? false : 'source-map', 13 | resolve: { 14 | extensions: ['.js', '.jsx'], 15 | alias: { 16 | 'react-dom': '@hot-loader/react-dom', 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.(js|jsx)$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader', 26 | }, 27 | }, 28 | { enforce: 'pre', test: /\.js$/, loader: 'source-map-loader' }, 29 | ] 30 | }, 31 | plugins: [ 32 | new webpack.HotModuleReplacementPlugin(), 33 | new HtmlWebPackPlugin({ template: './src/index.html', favicon: "./src/favicon.ico" }) 34 | ], 35 | devServer: { 36 | contentBase: './dist', 37 | hot: true, 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/enable-hooks.ts: -------------------------------------------------------------------------------- 1 | import mockUseEffect from "./mock-use-effect/mock-use-effect"; 2 | 3 | interface Jest { 4 | requireActual: (module: string) => object; 5 | mock: (module: string, factory?: () => unknown, options?: { virtual?: boolean }) => unknown; 6 | } 7 | 8 | interface React { 9 | useEffect: (...args: unknown[]) => unknown; 10 | useLayoutEffect: (...args: unknown[]) => unknown; 11 | } 12 | 13 | interface EnableHooksOptions { 14 | dontMockByDefault: boolean; 15 | } 16 | 17 | let originalUseEffect: (...args: unknown[]) => unknown; 18 | let originalUseLayoutEffect: (...args: unknown[]) => unknown; 19 | 20 | const useEffectMock = jest.fn(); 21 | const useLayoutEffectMock = jest.fn(); 22 | 23 | const enableHooks = (jestInstance: Jest, { dontMockByDefault }: EnableHooksOptions = { dontMockByDefault: false }): void => { 24 | const react = jestInstance.requireActual('react') as React; 25 | 26 | originalUseEffect = react.useEffect; 27 | originalUseLayoutEffect = react.useLayoutEffect; 28 | 29 | beforeEach(() => { 30 | if (dontMockByDefault) { 31 | useEffectMock.mockImplementation(originalUseEffect); 32 | useLayoutEffectMock.mockImplementation(originalUseLayoutEffect); 33 | } else { 34 | useEffectMock.mockImplementation(mockUseEffect()); 35 | useLayoutEffectMock.mockImplementation(mockUseEffect()); 36 | } 37 | }); 38 | 39 | jestInstance.mock('react', () => ({ 40 | ...react, 41 | useEffect: useEffectMock, 42 | useLayoutEffect: useLayoutEffectMock, 43 | })); 44 | }; 45 | 46 | const withHooks = (testFn: () => void): void => { 47 | useEffectMock.mockImplementation(mockUseEffect()); 48 | useLayoutEffectMock.mockImplementation(mockUseEffect()); 49 | 50 | try { 51 | testFn(); 52 | } finally { 53 | useEffectMock.mockImplementation(originalUseEffect); 54 | useLayoutEffectMock.mockImplementation(originalUseLayoutEffect); 55 | } 56 | }; 57 | 58 | const withoutHooks = (testFn: () => void): void => { 59 | if (!originalUseEffect) { 60 | throw new Error('Cannot call `disableHooks()` if `enableHooks()` has not been invoked') 61 | } 62 | 63 | useEffectMock.mockImplementation(originalUseEffect); 64 | useLayoutEffectMock.mockImplementation(originalUseLayoutEffect); 65 | 66 | try { 67 | testFn(); 68 | } finally { 69 | useEffectMock.mockImplementation(mockUseEffect()); 70 | useLayoutEffectMock.mockImplementation(mockUseEffect()); 71 | } 72 | }; 73 | 74 | /** 75 | * @deprecated 76 | */ 77 | const disableHooks = (): void => { 78 | console.warn('`disableHooks()` is deprecated. Please, use `withoutHooks()` instead'); 79 | 80 | if (!originalUseEffect) { 81 | throw new Error('Cannot call `disableHooks()` if `enableHooks()` has not been invoked') 82 | } 83 | 84 | useEffectMock.mockImplementation(originalUseEffect); 85 | useLayoutEffectMock.mockImplementation(originalUseLayoutEffect); 86 | }; 87 | 88 | const reenableHooks = (): void => { 89 | console.warn('`reenableHooks()` is deprecated.'); 90 | 91 | useEffectMock.mockImplementation(mockUseEffect()); 92 | useLayoutEffectMock.mockImplementation(mockUseEffect()); 93 | }; 94 | 95 | export default enableHooks; 96 | 97 | export { disableHooks, reenableHooks, withHooks, withoutHooks }; 98 | -------------------------------------------------------------------------------- /src/mock-use-effect/mock-use-effect.test.ts: -------------------------------------------------------------------------------- 1 | import mockUseEffect from './mock-use-effect'; 2 | 3 | describe('mock-use-effect', () => { 4 | test('calls `effect` multiple times if no dependencies', () => { 5 | const fn = jest.fn(); 6 | 7 | const useEffect = mockUseEffect(); 8 | 9 | useEffect(fn); 10 | useEffect(fn); 11 | 12 | expect(fn).toHaveBeenCalledTimes(2); 13 | }); 14 | 15 | test('calls `effect` once if dependencies do not change', () => { 16 | const fn = jest.fn(); 17 | 18 | const useEffect = mockUseEffect(); 19 | 20 | const dep = true; 21 | 22 | useEffect(fn, [dep]); 23 | useEffect(fn, [dep]); 24 | 25 | expect(fn).toHaveBeenCalledTimes(1); 26 | }); 27 | 28 | test('calls `effect` once if dependencies are an empty array', () => { 29 | const fn = jest.fn(); 30 | 31 | const useEffect = mockUseEffect(); 32 | 33 | useEffect(fn, []); 34 | useEffect(fn, []); 35 | 36 | expect(fn).toHaveBeenCalledTimes(1); 37 | }); 38 | 39 | test('calls `effect` again if dependencies change', () => { 40 | const fn = jest.fn(); 41 | 42 | const useEffect = mockUseEffect(); 43 | 44 | let dep = true; 45 | 46 | useEffect(fn, [dep]); 47 | 48 | dep = false; 49 | 50 | useEffect(fn, [dep]); 51 | 52 | expect(fn).toHaveBeenCalledTimes(2); 53 | }); 54 | 55 | /** 56 | * This also tests that `useEffect()` differentiates between different arrow functions 57 | */ 58 | test('calls different `effects` even if they have same dependencies', () => { 59 | let fn1CalledTimes = 0; 60 | const fn1 = (): void => { fn1CalledTimes++ }; 61 | 62 | let fn2CalledTimes = 0; 63 | const fn2 = (): void => { fn2CalledTimes++ }; 64 | 65 | const useEffect = mockUseEffect(); 66 | 67 | const dep = true; 68 | 69 | useEffect(fn1, [dep]); 70 | useEffect(fn2, [dep]); 71 | 72 | expect(fn1CalledTimes).toBe(1); 73 | expect(fn2CalledTimes).toBe(1); 74 | }); 75 | 76 | test('calls cleanup function before another calling the same effect again function', () => { 77 | const useEffect = mockUseEffect(); 78 | 79 | const cleanupFn = jest.fn(); 80 | 81 | useEffect(() => cleanupFn); 82 | useEffect(() => cleanupFn); 83 | 84 | expect(cleanupFn).toHaveBeenCalledTimes(1); 85 | }); 86 | 87 | test('does not call cleanup function if no other `useEffect()` function is called', () => { 88 | const useEffect = mockUseEffect(); 89 | 90 | const cleanupFn = jest.fn(); 91 | 92 | useEffect(() => cleanupFn); 93 | 94 | expect(cleanupFn).toHaveBeenCalledTimes(0); 95 | }); 96 | 97 | test('does not call cleanup function before calling another effect', () => { 98 | const useEffect = mockUseEffect(); 99 | 100 | const cleanupFn = jest.fn(); 101 | 102 | const firstEffect = (): Function => cleanupFn; 103 | const secondEffect = (): void => { jest.fn() }; 104 | 105 | useEffect(firstEffect); 106 | useEffect(secondEffect); 107 | 108 | expect(cleanupFn).toHaveBeenCalledTimes(0); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /src/mock-use-effect/mock-use-effect.ts: -------------------------------------------------------------------------------- 1 | type UseEffectSignature = (fn: () => void, triggers?: unknown[]) => void; 2 | type FunctionBody = string; 3 | type CleanupFunction = () => void; 4 | 5 | const noDepsOrDifferent = (previousDependencies: unknown[], currentDependencies: unknown[]): boolean => { 6 | return previousDependencies === undefined || 7 | previousDependencies.some((prevDep, index) => prevDep !== currentDependencies[index]); 8 | } 9 | 10 | const mockUseEffect = (): UseEffectSignature => { 11 | const previousCalls = new Map(); 12 | const cleanupFunctions = new Map(); 13 | 14 | return (effect: () => CleanupFunction | void, dependencies?: unknown[]): void => { 15 | const effectBody = effect.toString(); 16 | 17 | const shouldCall = previousCalls.has(effectBody) ? 18 | noDepsOrDifferent(previousCalls.get(effectBody), dependencies) : 19 | true; 20 | 21 | if (shouldCall) { 22 | previousCalls.set(effectBody, dependencies); 23 | 24 | if (cleanupFunctions.has(effectBody)) { 25 | cleanupFunctions.get(effectBody)(); 26 | } 27 | 28 | const cleanupFunction = effect(); 29 | 30 | if (typeof cleanupFunction === 'function') { 31 | cleanupFunctions.set(effectBody, cleanupFunction); 32 | } 33 | } 34 | } 35 | }; 36 | 37 | export default mockUseEffect; 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "commonjs", 7 | "target": "es6", 8 | "declaration": true 9 | }, 10 | "include": [ 11 | "./src/**/*" 12 | ], 13 | "exclude": [ 14 | "src/**/*.test.ts" 15 | ], 16 | "rules": { 17 | "quotemark": [ 18 | true, 19 | "single" 20 | ] 21 | } 22 | } 23 | --------------------------------------------------------------------------------