Intro
If you are here just for the code
, here is the project repo.
Puppeteer is a tool that allows you to write a headless instance of Chrome and automate some repetitive tasks. It's somewhat to Selenium but cooler and easier to use(that's an opinion based on my experience, not a fact). If you want me to write a comparison, let me know.
I'm a Front End Developer and for me, my is a platform to express, connect with interesting people and solve interesting problems. Roses have thorns and certainly, there are some parts of my work that I don't like doing at all. My strong desire to never do them, inspired me to automate these tasks and now share it with you. Please note that the scenarios described are real, but the information changed because of the NDA I signed in my current job.
Initial Setup
For our environment, since we are Front End Developers, we'll try to use Node for everything. I'll be using yarn instead of npm
.
Here is the command for the basic dependencies:
- Puppeteer.Headless Chrome.
-
Signale for cooler
console.log
.
yarn add puppeteer signale -D
Scenario 1: Did your changes break anything?
The problem
You just added some cool feature to a basic CRUD app, your are not supposed to break anything, but you still want to make sure that everything is ok.
All unit tests are working, and yet some manually tests are required. In this particular app, there is a success notification that appears when I register some user.
The solution
Since this app has some html forms, my script needs to do 3 things:
- Write some input with the current date,
- Take a screen shot.
- Let me know when the success notification arrives.
In each case, there are some variables, so this is the folder structure.
/test-folder
|-index.js // Here is where the magic happens
|-config.js // Test variables
|-locators.js // A JSON with all CSS locators needed for this test.
This is what the config.js
for this test looks like:
const config = {
url: 'http://localhost:3000/',
phoneNumber: '123-456-7890',
email: 'test@example.com',
password: 'test1234',
}
module.exports.config = config;
On index.js
, we will write the code that automates this tests. First we need the imports:
const puppeteer = require('puppeteer'); // High level API to interact with headless Chrome
const signale = require('signale');
// import LOCATORS from './locators'; // A JSON with all the CSS locators we need.
const config = require('./config');
We will add the date as a string so we know when the test was run.
const d = new Date();
const dateString = `${d.getDate()}_${d.getHours()}h${d.getMinutes()}`;
For this test, we are going to create a function and run it in the end. What the function will do many things:
1. Open a browser instance and navigate to the a given url, which comes from config.js
;
.
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(LOCAL_HOST_URL);
2. Once the page loads, we need to fill all the inputs forms. We can use CSS selectors and pass them as strings. For simplicity's sake, we are going to store it on a separate file, called locators.js
.
module.exports.locators = {
userNameSelect: 'select[name="userName"]',
birthdayInput: 'input[name="birthDay"]',
submitButton: 'button[type="submit"]'
}
You could be using ID's, but I try to avoid them when writing HTML, in this example I used attributes and names for two reasons:
- Unexperienced developers in my team tend to overuse them, or favor ID's instead of class name conventions or specificity.
- There can be many selects or inputs. Read reason 1 again.
Here is how you pass values to the inputs. Note that the first input is the selector, and the second is the value that we want to type. We could
await page.type(locators.userNameSelect, 'Jaime');
await page.type(locators.birthdayInput, '02/04');
await page.click('button[type="submit"]');
For the select, we are assuming that the html looks something like this. Notice the value attribute:
<select>
<option value="Jaime">Jaime</option>
<option value="James">James</option>
<option value="Bond">James</option>
</select>
3. Wait for the notification.
Eventually, as you progress, you might find scenarios where some operations are asynchronous; HTML forms can present values that are returned from the backend. For example, you might want to present the user with dates available for a dinner reservation.
For this example
The way to solve this is using page.waitForSelector
. It returns a promise, so we can act accordingly.
await page.waitForSelector('.notification-message')
.then( async () => {
signale.success('Form was submitted successfully'); // This is a fancy console.log()
await page.screenshot({path: `automated_test_success_`$dateString`.png`});
browser.close();
});
Here is the code in a single file, and the project repo.
const puppeteer = require('puppeteer'); // High level API to interact with headless Chrome
const signale = require('signale');
// import LOCATORS from './locators'; // A JSON with all the CSS locators we need.
const config = require('./config');
const runTest = async (params) => {
signale.debug('Opening browser...');
const browser = await puppeteer.launch();
const page = await browser.newPage();
const d = new Date();
const dateString = `${d.getDate()}_${d.getHours()}h${d.getMinutes()}`;
const userName = `USER_FROM_TESTING_SCRIPT_${dateString}`;
// Go to the website;
await signale.watch('Navigating to the site');
await page.goto(config.LOCAL_HOST_URL);
// Fill the form
await signale.watch('Filling up the form');
await page.type(locators.userNameSelect, 'Jaime');
await page.type(locators.birthdayInput, '02/04');
await page.click('button[type="submit"]');
await page.waitForSelector('.notification-message')
.then( async () => {
signale.success('Form was submitted successfully'); // This is a fancy console.log()
await page.screenshot({path: `automated_test_success_`$dateString`.png`});
browser.close();
})
.catch(() => signale.fatal(new Error('Submit form failed')));
};
runTest();
I hope this helped you to see the potential and you care enough to share.
Thanks for your reading.
Here are two more scenarios for that I'll be covering in the following parts:
- Scenario 2: Something stoped working, can you take a look?
- Scenario 3: Compare a snapshot of local vs test
Top comments (3)
Is it normal to use await with then() and catch() in Puppeteer?
await page.waitForSelector('.notification-message')
.then( async () => {
signale.success('Form was submitted successfully'); // This is a fancy console.log()
await page.screenshot({path:
automated_test_success_
$dateString.png
});browser.close();
})
.catch(() => signale.fatal(new Error('Submit form failed')));
It is a matter of style preference, the idea is to catch important errors.
You don't need
await
when using a promise:then
.