DEV Community

Jaime Rios
Jaime Rios

Posted on

Front End Development automation with Puppeteer. Part 1

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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');

Enter fullscreen mode Exit fullscreen mode

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()}`;

Enter fullscreen mode Exit fullscreen mode

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);


Enter fullscreen mode Exit fullscreen mode

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"]'
}
Enter fullscreen mode Exit fullscreen mode

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"]');

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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();
  });

Enter fullscreen mode Exit fullscreen mode

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();


Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
tosirisuk profile image
Richard

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')));

Collapse
 
papaponmx profile image
Jaime Rios

It is a matter of style preference, the idea is to catch important errors.

Collapse
 
jalal246 profile image
Jalal 🚀

You don't need await when using a promise: then.