├── .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 |
--------------------------------------------------------------------------------