├── .babelrc ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .babelrc ├── package.json ├── src │ ├── Example.jsx │ ├── Example.spec.jsx │ ├── app.jsx │ ├── index.ejs │ └── styles.css └── webpack.config.js ├── package.json └── src ├── __snapshots__ └── index.spec.js.snap ├── index.js └── index.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "node5" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #npm 2 | node_modules/ 3 | 4 | #output 5 | lib/ 6 | 7 | #webstorm 8 | .idea/ 9 | 10 | #jest 11 | coverage -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | For changelog, see the [github releases page](https://github.com/etiennedi/enzyme-wait/releases). 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 etiennedi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # enzyme-wait 2 | Wait for an async element to appear when performing integration tests with enzyme. 3 | Returns a promise which resolves with the root component you performed your search on. 4 | 5 | **NEW**: There is now a [fully working example](#checking-out-the-example-repo) using both Promises and async/await. 6 | 7 | ## How to use: 8 | 9 | `````javascript 10 | createWaitForElement( 11 | enzymeSelector, 12 | /*Optional*/ timeOut, 13 | /*Optional*/ intervalDuration 14 | )(componentToSearchOn) 15 | .then(/* ... */) 16 | .catch(/* ... */) 17 | ````` 18 | 19 | ## Example Usage (Promises): 20 | 21 | `````javascript 22 | import React from 'react'; 23 | import { mount } from 'enzyme' 24 | import { createWaitForElement } from 'enzyme-wait'; 25 | 26 | /** 27 | * The component you want to test. Assume it displays 28 | * the string "ready" after performing some async action 29 | * which takes time. 30 | */ 31 | import SampleComponent from '...'; 32 | 33 | const waitForSample = createWaitForElement('#sample-ready'); 34 | 35 | const component = mount(); 36 | 37 | it('displays ready once it is ready', ()=> { 38 | waitForSample(component) 39 | .then( component => expect(component.text()).to.include('ready') ); 40 | }); 41 | ````` 42 | 43 | ## Example Usage (async/await) 44 | 45 | The same as above but using async/await instead of Promises: 46 | `````javascript 47 | it('displays ready once it is ready', async ()=> { 48 | const componentReady = await waitForSample(component); 49 | expect(componentReady.text()).to.include('ready'); 50 | }); 51 | ````` 52 | 53 | ## Chaining promises 54 | 55 | If you have multiple async actions happening, just make sure to always return a Promise which 56 | resolves with the root component. This way you can create nice looking chains and avoid callback hell. 57 | 58 | Example: 59 | 60 | `````javascript 61 | const component = mount(); 62 | 63 | it('displays ready after multiple interactions', ()=> { 64 | createWaitForElement('#sample-ready')(component) 65 | .then( /* do something and return a resolved promise with the comp */ ) 66 | .then( /* do something and return a resolved promise with the comp */ ) 67 | .then( createWaitForElement('#another-component-ready') ) 68 | .then( component => expect(component.text().to.include('ready') ); 69 | }); 70 | 71 | ````` 72 | 73 | ## Checking out the example repo 74 | 75 | There is now a working example inside this repo using both the Promise-approach as well as the async/await-approach. 76 | 77 | The example uses Jest, but it should work with any test framework. In other frameworks you might need to call done() on asynchronous tests. 78 | 79 | To play around with this example you can: 80 | 81 | 1. clone this repo 82 | 1. run `npm install && npm run dist` on the root repo (this is required to create a lib version of this package which is listed in the example's dependencies ) 83 | 1. go to the example folder `cd example` 84 | 1. in there, run `npm install && npm start` 85 | 1. open your browser at `http://localhost:9000` to see the example or run `npm test` to see the tests working. 86 | -------------------------------------------------------------------------------- /example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "es2015", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "react", 10 | "stage-0" 11 | ], 12 | "plugins": [], 13 | "env": { 14 | "test": { 15 | "plugins": [ 16 | "transform-es2015-modules-commonjs" 17 | ] 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enzyme-wait-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "webpack-dev-server", 8 | "test": "jest" 9 | }, 10 | "author": "etiennedi", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/etiennedi/react-workshop-01/issues" 14 | }, 15 | "homepage": "https://github.com/etiennedi/react-workshop-01#readme", 16 | "devDependencies": { 17 | "babel-core": "^6.23.1", 18 | "babel-jest": "^19.0.0", 19 | "babel-loader": "^6.3.2", 20 | "babel-plugin-transform-es2015-modules-commonjs": "^6.23.0", 21 | "babel-polyfill": "^6.23.0", 22 | "babel-preset-es2015": "^6.22.0", 23 | "babel-preset-react": "^6.23.0", 24 | "babel-preset-stage-0": "^6.22.0", 25 | "css-loader": "^0.26.1", 26 | "enzyme": "^2.7.1", 27 | "enzyme-wait": "file:../", 28 | "html-webpack-plugin": "^2.28.0", 29 | "jest": "^19.0.2", 30 | "react-addons-test-utils": "^15.4.2", 31 | "style-loader": "^0.13.1", 32 | "webpack": "^2.2.1", 33 | "webpack-dev-server": "^2.4.1" 34 | }, 35 | "dependencies": { 36 | "react": "^15.4.2", 37 | "react-dom": "^15.4.2" 38 | }, 39 | "jest": { 40 | "testRegex": "\\.spec\\.(jsx|js)$", 41 | "moduleFileExtensions": [ 42 | "js", 43 | "jsx" 44 | ], 45 | "testPathIgnorePatterns": [ 46 | "/node_modules/", 47 | "/out/" 48 | ], 49 | "collectCoverageFrom": [ 50 | "src/**/*.{js,jsx,ts}", 51 | "!**/node_modules/**", 52 | "!**/out/**" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/src/Example.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const items = [ 4 | 'first item', 5 | 'second item', 6 | 'third item', 7 | ]; 8 | 9 | class Example extends Component { 10 | state = { items : [] }; 11 | 12 | componentWillMount() { 13 | setTimeout(()=> { 14 | this.setState( ()=>({items})) 15 | }, 100 + Math.random() * 600) 16 | } 17 | 18 | render() { 19 | const { items } = this.state; 20 | 21 | return ( 22 |
23 |

Wait a bit and it'll load

24 | {!items.length ?

Loading...

: ( 25 |
    26 | { items.map(item =>
  • {item}
  • ) } 27 |
28 | )} 29 | 30 |
31 | ) 32 | } 33 | } 34 | 35 | export default Example; 36 | -------------------------------------------------------------------------------- /example/src/Example.spec.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { createWaitForElement } from 'enzyme-wait'; 4 | 5 | import Example from './Example'; 6 | 7 | const waitForListItem = createWaitForElement('.list-item'); 8 | 9 | it('doesnt show any list items in the beginning', ()=> { 10 | const component = mount(); 11 | expect(component.find('.list-item')).toHaveLength(0) 12 | }) 13 | 14 | it('shows a Loading message in the beginning', ()=> { 15 | const component = mount(); 16 | expect(component.text()).toContain('Loading') 17 | }); 18 | 19 | describe('examples with PROMISES', ()=> { 20 | it('shows list items after some time', ()=> { 21 | const component = mount(); 22 | return waitForListItem(component).then( 23 | (componentReady)=> { 24 | expect(componentReady.find('.list-item')).toHaveLength(3) 25 | } 26 | ) 27 | }) 28 | 29 | it('doesnt show loading anymore', ()=> { 30 | const component = mount(); 31 | return waitForListItem(component).then( 32 | (componentReady)=> { 33 | expect(componentReady.text()).not.toContain('Loading') 34 | } 35 | ) 36 | }) 37 | }) 38 | 39 | describe('examples with ASYNC / AWAIT ', ()=> { 40 | it('shows list items after some time', async ()=> { 41 | const component = mount(); 42 | const componentReady = await waitForListItem(component); 43 | expect(componentReady.find('.list-item')).toHaveLength(3) 44 | }) 45 | 46 | it('doesnt show loading anymore', async ()=> { 47 | const component = mount(); 48 | const componentReady = await waitForListItem(component); 49 | expect(componentReady.text()).not.toContain('Loading') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /example/src/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import Example from './Example'; 5 | import './styles.css'; 6 | 7 | ReactDOM.render( 8 | , 9 | document.getElementById('app-root') 10 | ); 11 | -------------------------------------------------------------------------------- /example/src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | My app 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /example/src/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: sans-serif; 3 | } 4 | 5 | body { 6 | padding: 15px 30px; 7 | } 8 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | context: path.resolve(__dirname, './src'), 6 | entry: { 7 | app: './app.jsx', 8 | }, 9 | output: { 10 | path: path.resolve(__dirname, './dist'), 11 | filename: '[name].bundle.js', 12 | }, 13 | devServer: { 14 | contentBase: path.join(__dirname, 'dist'), 15 | compress: true, 16 | port: 9000, 17 | historyApiFallback: true, 18 | }, 19 | resolve: { 20 | extensions: ['.js', '.jsx'], 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.(js|jsx)$/, 26 | loader: 'babel-loader', 27 | exclude: /node_modules/, 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: ['style-loader', 'css-loader'], 32 | exclude: /node_modules/, 33 | }, 34 | ], 35 | }, 36 | plugins: [ 37 | new HtmlWebpackPlugin({ 38 | template: 'index.ejs', 39 | }), 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enzyme-wait", 3 | "version": "1.0.9", 4 | "description": "Wait for an async element to appear when performing integration tests with enzyme.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "dist": "babel ./src/ --out-dir lib --ignore .spec.js", 8 | "test": "jest" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-cli": "^6.22.2", 14 | "babel-preset-node5": "^12.0.0", 15 | "jest": "^18.1.0" 16 | }, 17 | "dependencies": { 18 | "assertion-error": "^1.0.2" 19 | }, 20 | "files": [ 21 | "lib" 22 | ], 23 | "keywords": [ 24 | "enzyme", 25 | "waiting", 26 | "promise", 27 | "waitForElement", 28 | "react", 29 | "mount", 30 | "testing", 31 | "integration testing" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/etiennedi/enzyme-wait.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/etiennedi/enzyme-wait/issues" 39 | }, 40 | "homepage": "https://github.com/etiennedi/enzyme-wait#readme" 41 | } 42 | -------------------------------------------------------------------------------- /src/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`component length zero 1`] = `[AssertionError: Specified root component in waitForElement not found.]`; 2 | 3 | exports[`no component 1`] = `[AssertionError: No root component specified in waitForElement.]`; 4 | 5 | exports[`no selector 1`] = `[AssertionError: No selector specified in waitForElement.]`; 6 | 7 | exports[`run into timeout 1`] = `[AssertionError: Expected to find mockSelector within 2000ms, but it was never found.]`; 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import AssertionError from 'assertion-error'; 2 | 3 | const DISPLAY_NAME = 'waitForElement'; 4 | 5 | export const createWaitForElement = (selector, maxTime = 2000, interval = 10) => rootComponent => { 6 | 7 | // Check correct usage 8 | if (!selector) { 9 | return Promise.reject(new AssertionError(`No selector specified in ${DISPLAY_NAME}.`)); 10 | } 11 | 12 | if (!rootComponent) { 13 | return Promise.reject(new AssertionError(`No root component specified in ${DISPLAY_NAME}.`)); 14 | } 15 | 16 | if (!rootComponent.length) { 17 | return Promise.reject(new AssertionError(`Specified root component in ${DISPLAY_NAME} not found.`)); 18 | } 19 | 20 | 21 | // Race component search against maxTime 22 | return new Promise((resolve, reject) => { 23 | 24 | let remainingTime = maxTime; 25 | 26 | const intervalId = setInterval(() => { 27 | if (remainingTime < 0) { 28 | clearInterval(intervalId); 29 | return reject(new AssertionError(`Expected to find ${selector} within ${maxTime}ms, but it was never found.`)) 30 | } 31 | 32 | const targetComponent = rootComponent.update().find(selector); 33 | if (targetComponent.length) { 34 | clearInterval(intervalId); 35 | return resolve(rootComponent); 36 | } 37 | 38 | remainingTime = remainingTime - interval; 39 | }, interval) 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | import { createWaitForElement } from './index' 2 | 3 | const wait = createWaitForElement('mockSelector'); 4 | 5 | describe('wrong usage', () => { 6 | it('throws an Assertion error if no root component is specified.', () => { 7 | wait(undefined) 8 | .catch(err => expect(err).toMatchSnapshot('no component')) 9 | }); 10 | 11 | it('throws an Assertion error if a root component cannot be found', () => { 12 | const mockComponent = { 13 | length: 0, 14 | }; 15 | 16 | wait(mockComponent) 17 | .catch(err => expect(err).toMatchSnapshot('component length zero')) 18 | }); 19 | 20 | it('throws an Assertion error if no selector is specified', () => { 21 | 22 | const wait = createWaitForElement(undefined); 23 | wait() 24 | .catch(err => expect(err).toMatchSnapshot('no selector')) 25 | }); 26 | }); 27 | 28 | describe('core logic', () => { 29 | describe('target component always ready', () => { 30 | 31 | let mockComponent; 32 | 33 | beforeEach(() => { 34 | mockComponent = { 35 | find: jest.fn().mockReturnValue({ length: 1 }), 36 | length: 1, 37 | } 38 | }); 39 | 40 | it('resolves after calling find on the root component with the selector', () => { 41 | return wait(mockComponent) 42 | .then(() => { 43 | expect(mockComponent.find).toHaveBeenCalledWith('mockSelector') 44 | }) 45 | }) 46 | }); 47 | 48 | describe('target component never ready', () => { 49 | 50 | let mockComponent; 51 | 52 | beforeEach(() => { 53 | mockComponent = { 54 | find: jest.fn().mockReturnValue({ length: 0 }), 55 | length: 1, 56 | } 57 | }); 58 | 59 | it('rejects after calling find on the root component with the selector', () => { 60 | return wait(mockComponent) 61 | .catch(() => { 62 | expect(mockComponent.find).toHaveBeenCalledWith('mockSelector') 63 | }) 64 | }); 65 | 66 | it('rejects after calling find on the root component 201 times', () => { 67 | // one initial call, then 200 calls (2000ms / 10ms = 200 times) 68 | 69 | return wait(mockComponent) 70 | .catch(()=> { 71 | expect(mockComponent.find).toHaveBeenCalledTimes(201) 72 | }) 73 | }); 74 | 75 | it('rejects after calling find on the root component with the selector', () => { 76 | return wait(mockComponent) 77 | .catch(e => { 78 | expect(e).toMatchSnapshot('run into timeout') 79 | }) 80 | }) 81 | }); 82 | 83 | describe('target component ready after a couple of tries', () => { 84 | 85 | let mockComponent; 86 | beforeEach(() => { 87 | 88 | const findMock = jest 89 | .fn(()=> ({ length: 1 })) 90 | .mockReturnValueOnce(()=> ({ length: 0 })) 91 | .mockReturnValueOnce(()=> ({ length: 0 })) 92 | .mockReturnValueOnce(()=> ({ length: 0 })); 93 | 94 | mockComponent = { 95 | find: findMock, 96 | length: 1, 97 | } 98 | }); 99 | 100 | it('resolves after calling find on the root component with the selector', () => { 101 | return wait(mockComponent) 102 | .then(() => { 103 | expect(mockComponent.find).toHaveBeenCalledWith('mockSelector') 104 | }) 105 | }); 106 | 107 | it('calls find on the root component 4 times, then resolves', () => { 108 | // three unsuccessful calls, then one successfull call to resolve 109 | 110 | return wait(mockComponent) 111 | .then(()=> { 112 | expect(mockComponent.find).toHaveBeenCalledTimes(4) 113 | }) 114 | }); 115 | }) 116 | }); 117 | --------------------------------------------------------------------------------