- Testing your React App with Puppeteer and Jest
- Author: Rajat S
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: jonjia
- Proofread by: old professor sunhaokk
How to perform end-to-end testing of your React application using Puppeteer and Jest
End-to-end testing helps ensure that all components in the React application work as expected, whereas unit and integration tests don’t.
Puppeteer is an end-to-end testing Node library provided by Google, which provides us with the upper level API interface encapsulated by the Dev Tools protocol to control Chromium. With Puppeteer, we can open applications and perform tests.
In this article, I’ll show you how to use Puppeteer and Jest to perform untyped tests on a simple React application.
Project initialization
Let’s start by creating a React project. Then install other dependencies, such as Puppeteer and Faker.
Use the create-react-app command to create the React app and name it testing-app.
create-react-app testing-app
Copy the code
Then, install the development dependencies.
yarn add faker puppeteer --dev
Copy the code
We don’t need to install Jest because it’s already built into the React package. If you install it again, the subsequent tests will not go smoothly because the two versions of Jest will conflict with each other.
Next, we need to update the test script in package.json to call Jest. You also need to add a new debug script. This script is used to set our Node environment to debug mode and call NPM test.
"scripts": {
"start": "react-scripts start"."build": "react-scripts build"."test": "jest"."debug": "NODE_ENV=debug npm test"."eject": "react-scripts eject",}Copy the code
With Puppeteer, we can choose to run tests in headless mode or open them in Chromium. This is a great feature because we can see the specific pages in the test, use developer tools, and view network requests. The only drawback is that it makes continuous integration (CI) testing very slow.
We can configure environment variables to determine whether to run tests in headless mode. When I need to see the specifics of test execution, I turn off headless mode by running debug scripts. When I don’t need it, I run the test script.
Now open the app.test. js file in the SRC directory and replace it with the following:
const puppeteer = require('puppeteer')
const isDebugging = () => {
const debugging_mode = {
headless: false,
slowMo: 250,
devtools: true,}return process.env.NODE_ENV === 'debug' ? debugging_mode : {}
}
describe('on page load', () = > {test('h1 loads correctly', async() => {
let browser = await puppeteer.launch({})
let page = await browser.newPage()
page.emulate({
viewport: {
width: 500,
height: 2400,
}
userAgent: ' '})})})Copy the code
We first introduce puppeteer in our application using require. The first test is then described using Describe to test the initial loading of the page. Here I test that the H1 element contains the correct text.
In our test description, we need to define browser and Page variables. They are needed throughout the test.
The launch method passes configuration options to the browser, allowing us to test the application with different browser Settings. You can even set emulation options to change the Settings of the page.
Let’s set up the browser first. A function called isDebugging is created at the top of the file. We’ll call this function in the launch method. This function defines an object called debugging_mode, which contains the following three properties:
headless: false
– Execute tests in headless mode (true
) or use Chromium to perform tests (false
)slowMo: 250
– The Puppeteer option is set with a delay of 250 ms.devtools: true
– Whether to open the developer tool in the browser when opening the application.
The isDebugging function returns a ternary expression based on the environment variable. The ternary statement determines whether to return the debugging_mode or an empty object.
Back in our package.json file, we create a debug script that sets up Node as a debug environment. Unlike the test above (using the browser default options), if our environment variable is DEBUG, the isDebugging function returns our custom browser options.
Next, configure our page. This is done in the Page.emulate method. We set width and height in the ViewPort property and set the userAgent to an empty string.
The Page.emulate method is useful because it allows you to perform tests in various browser Settings and replicate properties from page to page.
Test HTML content using Puppeteer
We are ready to write tests for the React application. In this section, I’ll test < H1 > tags and navigation content to make sure they work.
Open the app.test. js file and add the following code to the test block below the Page.
await page.goto('http://localhost:3000/');
const html = await page.$eval('.App-title', e => e.innerHTML);
expect(html).toBe('Welcome to React'); browser.close(); }, 16000); });Copy the code
Basically, we tell Puppeteer to open [http://localhost:3000/](http://localhost:3000/.) . The Puppeteer executes the app-title class. And we have this class on our H1 tag.
The $.eval method essentially executes the Document. querySelector method on the calling object.
Puppeteer finds the element that matches the class selector and passes it as an argument to the e => e.innerhtml callback function. Here, the Puppeteer can select the
element and check if the content of this element is Welcome to React.
Once Puppeteer has finished testing, the browser.close method closes the browser.
Open the command terminal and execute the debug script.
yarn debug
Copy the code
If your app passes the test, you’ll see something like the following on the terminal:
Next, create the nav element in the app.js file as follows:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
<nav className='navbar' role='navigation'>
<ul>
<li className="nal-li"><a href="#">Batman</a></li>
<li className="nal-li"><a href="#">Supermman</a></li>
<li className="nal-li"><a href="#">Aquaman</a></li>
<li className="nal-li"><a href="#">Wonder Woman</a></li>
</ul>
</nav>
</header>
<p className="App-intro"> To get started, edit <code>src/App.js</code> and save to reload. </p> </div> ); }}export default App;
Copy the code
Note that all
Until then, let’s refactor our previous code. Under the isDebugging function declaration, define two global variables: browser and Page. Then, call the beforeAll method as follows:
let browser
letpage beforeAll(async () => { browser = await puppeteer.launch(isDebugging()) page = await browser.newPage() await Page. Goto (' http://localhost:3000/') page.setViewport({ width: 500, height: 2400 }) })Copy the code
Earlier, I didn’t need to set up userAgent. So I didn’t use the beforeAll method and just used the setViewport method. Now I can get rid of localhost and Browser.close and use the afterAll method instead. If your application is in debug mode, you need to close the browser.
afterAll(() => {
if (isDebugging()) {
browser.close()
}
})
Copy the code
Now we can write navigation tests. Within the Describe statement block, create a new test statement as follows:
test('nav loads correctly', async () => {
const navbar = await page.$eval('.navbar', el => el ? true : false) const listItems = await page.? ('.nav-li')
expect(navbar).toBe(true)
expect(listItems.length).toBe(4)
}
Copy the code
Here, I first select the navbar element by passing the. Navbar parameter to the $eval method. The ternary operator is then used to return whether the element exists (true or false).
Next, you need to select the list item. As before, we pass the.nav-li argument to the $eval method to select list elements. We use the Expect method to assert that the Navbar element exists (true) and that the number of list items is 4.
You may have noticed that I used it on the pick list item, okay? Methods. This is a shortcut to running the Document. querySelector method within the page. When eval and the $symbol are not used together, the callback cannot be passed.
Run the debug script to see if your code passes both tests.
Simulating user activity
Let’s see how you can test a form submission activity by simulating keyboard input, mouse clicks, and touch events. We will use the user information randomly generated by Faker to accomplish this.
Create a new file named login. js in the SRC directory. This component contains four input boxes and a submit button.
import React from 'react';
import './Login.css';
export default function Login(props) {
return (
<div className="form">
<div className="form">
<form onSubmit={props.submit} className="login-form">
<input data-testid="firstName" type="text" placeholder="first name"/>
<input data-testid="lastName" type="text" placeholder="last name"/>
<input data-testid="email" type="text" placeholder="Email"/>
<input data-testid="password" type="password" placeholder="password"/>
<button data-testid="submit">Login</button>
</form>
</div>
</div>
)
}
Copy the code
Create another login. CSS file, source code.
The following components are shared via Bit, which you can install using NPM or import development in your own projects.
- Bit-login/SRC/app-geekrajat creates the React component: a component that displays a successful login after submitting a message – written using React. Dependency: React. The login form
If the user clicks the Login button, the application needs to display a Success Message. So create a new file named sucessmessage.js in the SRC directory. In addition to create a [SuccessMessage. CSS] (https://gist.github.com/rajatgeekyants/1a77cdf44f296f2399d4b63f40a4900f) files.
import React from 'react';
import './SuccessMessage.css';
export default function Success() {
return (
<div>
<div className="wincc">
<div className="box" />
<div className="check" />
</div>
<h3 data-testid="success" className="success">
Success!!
</h3>
</div>
);
}
Copy the code
Then import them in the app.js file.
import Login from './Login.js
import SuccessMessage from './SuccessMessage.js
Copy the code
Next, add a state to the App component. Add the handleSubmit method, which blocks the default event and sets the complete property to true.
state = { complete: false }
handleSubmit = e => {
e.preventDefault()
this.setState({ complete: true})}Copy the code
Then add a ternary statement at the bottom of the component. It decides whether to display the Login component or the SuccessMessage component.
{ this.state.complete ?
<SuccessMessage/>
:
<Login submit={this.handleSubmit} />
}
Copy the code
Run the yarn start command to ensure that your application works properly.
Now use Puppeteer to write the end-to-end tests to make sure the above functions work. Introduce faker in app.test.js. Then create a user object like this:
const faker = require('faker')
const user = {
email: faker.internet.email(),
password: 'test',
firstName: faker.name.firstName(),
lastName: faker.name.lastName()
}
Copy the code
Faker is very useful in testing, and it generates different data for each test.
Write a new test statement in the Describe statement block to test the login form. The test will click on the input box and type something. It then simulates the display of the component clicking the submit button and waiting for a success message. I’m also going to add a timeout to this test.
test('login form works correctly', async () => {
await page.click('[data-testid="firstName"]')
await page.type('[data-testid="lastName"]', user.firstName)
await page.click('[data-testid="firstName"]')
await page.type('[data-testid="lastName"]', user.lastName)
await page.click('[data-testid="email"]')
await page.type('[data-testid="email"]', user.email)
await page.click('[data-testid="password"]')
await page.type('[data-testid="password"]', user.password)
await page.click('[data.testid="submit"]')
await page.waitForSelector('[data-testid="success"]')}, 1600)Copy the code
Run the debug script to see how Puppeteer performs the tests!
Set cookies in tests
I now want the application to save the information to a cookie when submitting the form. This information includes the user’s name.
For simplicity, I’ll refactor the app.test.js file to open only one page. The client for this page will simulate an iPhone 6.
const puppeteer = require('puppeteer');
const faker = require('faker');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];
const user = {
email: faker.internet.email(),
password: 'test',
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
};
const isDebugging = () => {
let debugging_mode = {
headless: false,
slowMo: 50,
devtools: true};return process.env.NODE_ENV === 'debug' ? debugging_mode : {};
};
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch(isDebugging());
page = await browser.newPage();
await page.goto('http://localhost:3000/');
page.emulate(iPhone);
});
describe('on page load ', () = > {test(
'h1 loads correctly',
async () => {
const html = await page.$eval('.App-title', e => e.innerHTML);
expect(html).toBe('Welcome to React'); }, 1600000);test('nav loads correctly', async () => {
const navbar = await page.$eval('.navbar', el => (el ? true : false)); const listItems = await page.? ('.nav-li');
expect(navbar).toBe(true);
expect(listItems.length).toBe(4);
});
test(
'login form works correctly',
async () => {
const firstNameEl = await page.$('[data-testid="firstName"]');
const lastNameEl = await page.$('[data-testid="lastName"]');
const emailEl = await page.$('[data-testid="email"]');
const passwordEl = await page.$('[data-testid="password"]');
const submitEl = await page.$('[data-testid="submit"]');
await firstNameEl.tap();
await page.type('[data-testid="firstName"]', user.firstName);
await lastNameEl.tap();
await page.type('[data-testid="lastName"]', user.lastName);
await emailEl.tap();
await page.type('[data-testid="email"]', user.email);
await passwordEl.tap();
await page.type('[data-testid="password"]', user.password);
await submitEl.tap();
await page.waitForSelector('[data-testid="success"]'); }, 1600000); }); afterAll(() => {if(isDebugging()) { browser.close(); }});Copy the code
I want to save the cookie when I submit the form, and we’ll add tests in the context of the form.
Write a new Describe statement block for the login form, and then copy and paste the test code we used for the login form.
describe('login form', () => {// Insert the login form test code here})Copy the code
Then rename it to fill out form and submits. Create a new test block called Sets firstName cookie. It checks to see if firstNameCookie is saved to a cookie.
test('sets firstName cookie', async () => {
const cookies = await Page.cookies()
const firstNameCookie = cookies.find(c => c.name === 'firstName' && c.value === user.firstName)
expect(firstNameCookie).not.toBeUndefined()
})
Copy the code
The Page. Cookies method returns an array of each cookie object of the document. Use the array’s find method to check for the existence of cookies. This ensures that the application uses the firstName generated by Faker.
If you run the test script now, you will find that the test fails because it returns the value of undefined. Now let’s solve this problem.
In the app.js file, add a firstName attribute to the state object. The default value is an empty string.
state = {
complete: false,
firstName: ' ',}Copy the code
Inside the handleSubmit method, add the following code:
document.cookie = `firstName=${this.state.firstname}`
Copy the code
Create a new method called handleInput. This method is called with each input to update state.
handleInput = e => {
this.setState({firstName: e.currentTarget.value})
}
Copy the code
Pass this method as a prop to the Login component.
<Login submit={this.handleSubmit} input={this.handleInput} />
Copy the code
In the login. js file, add the onChange={props. Input} method to the firstName element. This way, React calls this method whenever the user enters something in the firstName field.
Now, when the user clicks the Login button, I need the application to save the firstName information to the cookie. Run the NPM test command to see if the application passes all the tests.
If the application requires a cookie before performing any action, should that cookie be set on a previously authorized page?
In the app.js file, refactor the handleSubmit method as follows:
handleSubmit = e => {
e.preventDefault()
if (document.cookie.includes('JWT')){
this.setState({ complete: true })
}
document.cookie = `firstName=${this.state.firstName}`}Copy the code
With the code above, the SuccessMessage component only loads if the COOKIE contains JWT.
In the fills out form and submits test block in the app.test. js file, add the following code:
await page.setCookie({ name: 'JWT', value: 'kdkdkddf' })
Copy the code
This will save a ‘JWT’ that actually sets the page token through some random tests to the cookie. If you run the test script now, your application will execute and pass all the tests!
A screenshot using Puppeteer
Screenshots can help us see what happens when a test fails. Let’s see how Puppeteer can be used to take screenshots and analyze tests.
In app.test. js nav loads correctly test block. Add a conditional statement to check that the number of listItems does not equal 3. If so, the Puppeteer should take screenshots of the page, update the expect statement for the test, and expect listItems to be 3, not 4.
if(listItems.length ! == 3) await page.screenshot({path:'screenshot.png'});
expect(listItems.length).toBe(3);
Copy the code
Obviously, our test will fail because we have four listItems in our application. Description The test script fails to be run on the terminal. You will also find a screenshot.png file in the root directory of your project.
screenshots
You can also configure the screenshot method as follows:
fullPage
– If is set totrue
Puppeteer will take a screenshot of the entire page.quality
– A value from 0 to 100, used to specify picture quality.clip
– Provides an object to specify an area of the page for screen capture.
You can also create a PDF file of the page without using the Page. Screenshot method and use page. This method has its own configuration.
scale
– Set the zoom factor. The default value is 1.format
– Set the paper format. If set, this property will override any width or height options passed to it. The default value isletter
.margin
– Used to set paper margins.
Handle page requests in tests
Let’s see how Puppeteer handles page requests in a test. In the app.js file, I’ll add an asynchronous componentDidMount method. This method gets the data from the Pokemon API. The response to this request will take the form of a JSON file. I will also add this data to the component’s state.
async componentDidMount() {
const data = await fetch('https://pokeapi.co/api/v2/pokedex/1/').then(res => res.json())
this.setState({pokemon: data})
}
Copy the code
Make sure you add Pokemon: {} to the state object. Inside the app component, add a < H3 > tag.
<h3 data-testid="pokemon">
{this.state.pokemon.next ? 'Received Pokemon data! ' : 'Something went wrong'}
</h3>
Copy the code
Run the application and you will see that the application has successfully fetched the data.
Using Puppeteer, I can write tasks to check if our
elements contain content that was successfully requested, or to intercept the request and enforce a failure. This way, I can see how the application works in the case of a successful or failed request.
I first ask the Puppeteer to send a request to intercept the fetch request. Then, if my url contains the PokeAPI, Puppeteer should abort the blocked request. Otherwise, everything should go on.
Open the app.test. js file and add the following code to the beforeAll method:
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.url.includes('pokeapi')) {
interceptedRequest.abort();
} else{ interceptedRequest.continue(); }});Copy the code
SetRequestInterception is an interception that gives me access to every request made by the page. Once a request is intercepted, the request is aborted and a specific error code is returned. You can also set the request to fail or check some logic and continue intercepting the request.
Let’s write a new test called Fails to Fetch Pokemon. This test executes the H3 element. Then grab the content of this element and make sure it’s Received Pokemon Data! .
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.url.include('pokeapi')) {
interceptedRequest.abort();
} else{ interceptedRequest.continue(); }});Copy the code
Execute the debug code and you will actually see the
element. You’ll notice that the content of the element is always Something went wrong. All tests pass, which means we have successfully blocked the Pokemon request.
Note that when aborting a request, we can control the request header, the error code returned, and customize the entity of the response.
Learn more:
-
Learn the React component development process with 9 tools
-
How to write better React code
-
11 React Component Libraries you should know
-
Bit – Share co-create code componentsBit makes building software with widgets easier and more fun. Share and synchronize these components across your team
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.