visit
Demo app: request form
If the form is submitted without being properly filled out, error messages are shown below each invalid input.Demo app: invalid input
Upon successful form submission, the app shows some confirmation text.Demo app: filling out the form
Demo app: confirmation page
Simple enough, right? If you’d like to see the demo in action, you can or .Now, let’s look at how the app was made.npx create-react-app testproject-demo
With the skeleton app generated, I then removed the default app content and wrote a simple form component in a file called
RequestForm.js
. Here’s the request form code reproduced in full:import React, { useState } from 'react'
import './RequestForm.css'
export const RequestForm = () => {
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
const [email, setEmail] = useState('')
const handleFirstNameChange = e => {
setFirstName(e.target.value)
}
const handleLastNameChange = e => {
setLastName(e.target.value)
}
const handleEmailChange = e => {
setEmail(e.target.value)
}
const [firstNameError, setFirstNameError] = useState('')
const [lastNameError, setLastNameError] = useState('')
const [emailError, setEmailError] = useState('')
const [submitted, setSubmitted] = useState(false)
const handleSubmit = e => {
e.preventDefault()
setFirstNameError(firstName ? '' : 'First Name field is required')
setLastNameError(lastName ? '' : 'Last Name field is required')
setEmailError(email ? '' : 'Email field is required')
if (firstName && lastName && email) {
setSubmitted(true)
}
}
return submitted ? (
<p id="submissionConfirmationText">
Thank you! We will be in touch with you shortly.
</p>
) : (
<form className="requestForm" onSubmit={handleSubmit}>
<div className={`formGroup${firstNameError ? ' error' : ''}`}>
<label htmlFor="firstName">First Name</label>
<input
name="firstName"
id="firstName"
data-testid="firstName"
value={firstName}
onChange={handleFirstNameChange}
/>
</div>
{firstNameError && (
<p className="errorMessage" id="firstNameError">
{firstNameError}
</p>
)}
<div className={`formGroup${lastNameError ? ' error' : ''}`}>
<label htmlFor="lastName">Last Name</label>
<input
name="lastName"
id="lastName"
data-testid="lastName"
value={lastName}
onChange={handleLastNameChange}
/>
</div>
{lastNameError && (
<p className="errorMessage" id="lastNameError">
{lastNameError}
</p>
)}
<div className={`formGroup${emailError ? ' error' : ''}`}>
<label htmlFor="email">Email</label>
<input
type="email"
name="email"
id="email"
data-testid="email"
value={email}
onChange={handleEmailChange}
/>
</div>
{emailError && (
<p className="errorMessage" id="emailError">
{emailError}
</p>
)}
<button type="submit" id="requestDemo">
Request Demo
</button>
</form>
)
}
Next, we’ll to use in our project. Once we have a developer token, we’ll create an
.env
file in the root directory of our project and add the following line of code to store our token in the TP_DEV_TOKEN
environment variable:TP_DEV_TOKEN=<YOUR DEV TOKEN HERE>
You’ll note that we tell Git in our
.gitignore
file to ignore our .env
file so that our token or other environment secrets don’t get committed into our version control and accidentally shared with others.Finally, we’ll need to install a couple of npm packages as
devDependencies
to use the TestProject JavaScript OpenSDK in our app:yarn add --dev @tpio/javascript-opensdk selenium-webdriver
To do this, I added the following npm scripts to the “scripts” section of my
package.json
file. Each one contains some specific Jest CLI configuration options:"scripts": {
...other scripts here
"start": "react-scripts start",
"test:e2e": "wait-on //localhost:3000/testproject-demo/build/ && react-scripts test --testPathPattern=\"(\\.|/)e2e\\.(test|spec)\\.[jt]sx?$\" --testTimeout=30000 --runInBand --watchAll=false",
"test:e2e:ci": "run-p start test:e2e",
"test:e2e:watch": "wait-on //localhost:3000/testproject-demo/build/ && react-scripts test --testPathPattern=\"(\\.|/)e2e\\.(test|spec)\\.[jt]sx?$\" --testTimeout=30000 --runInBand",
"test:unit": "react-scripts test --testPathPattern=\"(\\.|/)unit.(test|spec)\\.[jt]sx?$\" --watchAll=false",
"test:unit:coverage": "react-scripts test --testPathPattern=\"(\\.|/)unit.(test|spec)\\.[jt]sx?$\" --watchAll=false --coverage",
"test:unit:watch": "react-scripts test --testPathPattern=\"(\\.|/)unit.(test|spec)\\.[jt]sx?$\""
},
First, we see the
start
script. That one is easy enough: it runs our app locally in development mode. This is important because e2e tests require the app to be running to work properly.Next, we see the
test:e2e
script. This command waits for the app to run locally on port 3000 before running any tests. It then uses the react-scripts
test command to run our app’s tests but with several Jest CLI configuration options applied. The
testPathPattern
option tells Jest to only run our tests that end in e2e.test.js
(and a few other variations). The testTimeout
option increases Jest’s default timeout of 5 seconds per test to 30 seconds per test since e2e tests take a little longer to run than simple unit tests. The
runInBand
option tells Jest to run our test files serially instead of in parallel since we only have one TestProject agent installed on our machine.And finally, the
watchAll=false
option makes it so that the tests do not run in “watch” mode, which is the default setting for Jest with react-scripts. Whew, that was a lot!The third script is
test:e2e:ci
. This command is a combination of the start
and test:e2e
commands to help simplify the testing process. In order to use the original test:e2e
command, we first must be running the app locally. So we’d need to first run
yarn start
and then run yarn test:e2e
. That’s not a huge deal, but now we have an even simpler process in which we can just run yarn test:e2e:ci
to both start the app and run the e2e tests.The fourth script,
test:e2e:watch
, is very similar to the test:e2e
script but runs the tests in “watch” mode in case you want your tests to be continuously running in the background as you make changes to your app.The last three scripts are for running unit tests. The
test:unit
script runs the unit tests with Jest and React Testing Library and only looks for tests that end in unit.test.js
(and a few other variations). The
test:unit:coverage
script runs those same unit tests but also includes a test coverage report. And finally, the test:unit:watch
script runs the unit tests in watch mode.This may seem like a lot of information to take in, but the takeaway here is that we’ve now created several helpful npm scripts that allow us to easily run our unit and e2e tests with short and simple commands. All of the hard configuration work is out of the way, so now we can focus on writing the actual tests.Our complete
App.e2e.test.js
file looks like this:import { By } from 'selenium-webdriver'
import { Builder } from '@tpio/javascript-opensdk'
describe('App', () => {
const testUrl = '//localhost:3000/testproject-demo/build/'
let driver
beforeEach(async () => {
driver = await new Builder()
.forBrowser('chrome')
.withProjectName('TestProject Demo')
.withJobName('Request Form')
.build()
})
afterEach(async () => {
await driver.quit()
})
it('allows the user to submit the form when filled out properly', async () => {
await driver.get(testUrl)
await driver.findElement(By.css('#firstName')).sendKeys('John')
await driver.findElement(By.css('#lastName')).sendKeys('Doe')
await driver.findElement(By.css('#email')).sendKeys('[email protected]')
await driver.findElement(By.css('#requestDemo')).click()
await driver
.findElement(By.css('#submissionConfirmationText'))
.isDisplayed()
})
it('prevents the user from submitting the form when not filled out properly', async () => {
await driver.get(testUrl)
await driver.findElement(By.css('#requestDemo')).click()
await driver.findElement(By.css('#firstNameError')).isDisplayed()
await driver.findElement(By.css('#lastNameError')).isDisplayed()
await driver.findElement(By.css('#emailError')).isDisplayed()
})
})
In our first test, we ensure that a user can successfully submit the form. We navigate to our app’s url, use the
sendKeys
method to enter text into the three input fields, and then click the submit button. We then wait for the confirmation text to appear on the screen to validate that our submission was successful.You’ll note that all of the selectors look just like normal Selenium selectors. You will typically find elements using CSS selectors or using the XPath selector.In our second test, we ensure that a user is prevented from submitting the form when there are invalid inputs on the page. We first navigate to our app’s url and then immediately click the submit button without filling out any of the input fields. We then verify that the three error messages are displayed on the screen.You’ll also note that we’ve extracted out some of the shared test setup and teardown into the
beforeEach
and afterEach
blocks. In the beforeEach
block, we create our web driver for Chrome. In the afterEach
block, we quit the driver.Here’s the moment of truth: Let’s try running our end-to-end tests. In our terminal, we’ll run
yarn test:e2e:ci
to start the app and run the e2e tests. And… the two tests pass! You should see the app open on the Chrome browser, see the steps for each test be executed, and then see the test results back in the terminal:Running our e2e tests in the terminal
TestProject even provides its own free, built-in report dashboards as well as local HTML and PDF reports so that you can see the test results within the browser. This is perfect when viewing tests that are run as part of a CI pipeline. Here is my report after running the test suite twice:TestProject report dashboard