No Tears Guide to Creating E2E Test Scripts for Playwright & Puppeteer

By Michael ShiLast Updated on Mar 16, 2022

Getting browser automation scripts to do exactly what you want can feel like a never-ending battle between you and your code. I still remember vividly when I was up late one night years ago writing a Selenium script for a change I was making. I was banging my head against the table as I was running out of Selenium incantations to cast on my test to make it work.

Fast forward to today, I’m writing up this guide on a few tricks I wish I knew at the time, to quickly craft reliable E2E tests, instead of crying over error logs and wrangling clueless automation scripts.

Use a Recorder

A recorder is a tool that can record the browser actions you take, and turn those actions into a fully-working automation script. So you can start coding out your test, by simply stepping through your user flow as a user would.

I know there are people who might be skeptical, but hear me out. Just as automation frameworks have dramatically improved over time, test recorders have gotten some serious love over time as well.

I find test recorders pretty useful for a few reasons:

  1. No more hunting for selectors - There’s no reason to be hunting for selectors yourself, when you can automate the work away. The best recorders use selector logic that will give you stable selectors, rather than trying to enumerate every div and class name on the way to your element. (We’ll also talk more about selectors later!)

  2. No more manually writing commands - Just like selectors, there’s no need to manually type out every “click” “fill” and “waitFor” command, when you can get all of that for free by just simply walking through your web pages.

  3. Best practices are built in - While recorders won’t generate perfect code, they will have best-practices built-in. Instead of needing to worry about using “fill” instead of “type”, or remembering how to await for text in Puppeteer, you can just let the recorder generate code that’s aligned with best practices, so you can focus on the rest of your work.

Ultimately, using a test recorder can give you a head start, save you from doing the boring parts of testing, and let you focus on the harder parts of perfecting a test automation script.

Here’s a few recorders that can help you get started:

Use Stable Selectors

As dynamic CSS classes and elements are the norm today, it can be difficult to try to pick selectors that are robust to a rapidly changing code base, or even a rapidly changing web page due to async logic.

Especially if your project uses something like Tailwinds, styled-components, or even a JS framework like React or Vue that use async logic to render elements, you might be struggling to figure out how to target the right element reliably.

First Solution: Test IDs

The best way to combat these issues is to explicitly introduce a stable selector to elements you’re looking to test, and nothing beats stability like introducing test IDs to your application.

The concept is very simple: for elements you need to interact with, append a data-test-id=”my-element” attribute to the HTML element. In your automation scripts, you can easily target the element with

await page.click('[data-test-id="my-element"]');

Now you’ll never need to worry about your selectors breaking the next time your team decides to change button colors or re-builds the application with a new minified class name.

However, this requires adding new attributes to elements you need to target. Depending on how open the application owners are to adding these new attributes, it may be difficult to rely on them. If test IDs aren’t possible, I’d fall back to the next best solution.

Second Solution: Accessibility Selectors

Luckily with accessibility becoming a higher priority for web applications, more and more critical elements that need to be interacted with might already have a machine-friendly label attached to it.

Usually you’ll see attributes such as aria-label, alt, or title for elements you want to interact with. Those attributes tend to be more stable than CSS classes and can serve as a good stop-gap measure until you’re able to implement test IDs for the elements you need to test.

A script that utilizes these attributes might look like

await page.click('[alt="Main Logo"]');

Last Solution: Text Content Selectors

If you’re testing an application that might not have all the accessibility selectors built out yet, and haven’t had time to implement test IDs, the last solution you can look towards is targeting elements by text content.

At first glance, it might sound like an incredibly fragile proposition. Indeed it can be for certain elements, but for others it may be the best stable solution available. Can you remember the last time your team updated the “Sign In” button text on your application?

For elements with non-dynamic text content, usually buttons or input placeholders, text content can be a fairly reliable way of target elements.

Luckily in Playwright, it’s incredibly easy to target elements by text like so:

page.click('text=Sign In');

In Puppeteer, you’ll need to dip into XPaths to target elements by text:

await page.waitForXPath('//*[contains(., "Sign In")]');
const [element] = await page.$x('//*[contains(., "Sign In")]');
await element.click();

Automate Selector Picking

These best practices of stable selectors mentioned here and more are already built-in to DeploySentinel Recorder’s selector picking logic. So you don’t have to hunt for a specific test ID or accessibility selector.

Flip On the Debug Features

If you’re banging your head against a test script trying to figure out why it isn’t working, whipping out the debugging modes is probably the fastest way to find out why your script isn’t doing what you want.

Playwright’s Inspector and Trace Viewer

With Playwright, it’s incredibly easy to append PWDEBUG=1 in front of your script to pull up Playwright Inspector, where it’ll be able to step through everything Playwright is doing in great detail during the test. If there’s a step you have issues with, you can add await page.pause() to pause the test run so you can inspect the page at that point in time.

If you’re executing the script in a remote environment, you can take advantage of Playwright’s Trace Viewer which records detailed logs and DOM snapshots before and after every action.

If you’re using DeploySentinel to run your test - Playwright traces are captured by default and viewable at any time to debug test runs.

Headed Mode and Slow Mo

In general you can also enable headed mode with slow motion enabled to visually see what your script is doing. Both Playwright and Puppeteer support this with just two extra lines of code. See the docs for Playwright and Puppeteer here.

The DeploySentinel Recorder will always have these two options commented out but inserted as part of every script generated to make it easy to debug locally.

Enable Chrome Dev Tools

Lastly if there’s an issue that requires you to look at network requests or browser logs, you can have Playwright and Puppeteer open the Chrome dev tools panel on browser launch so all logs and network requests are captured from the start automatically for you. See the Playwright docs here or the dev tools section of Puppeteer’s debugging docs.

You’re All Set!

I hope this list of tips helps you out in creating testing scripts for Puppeteer or Playwright.

If you’re looking to run tests easily and affordably - you can try DeploySentinel for free, with many of the above best practices included for you with no extra effort.

Eliminate Flaky Cypress Tests with DeploySentinel
Debug & fix Cypress tests with full DOM, network and console events captured from your CI runs. Test parallelization and analytics included.
Learn More with Live Demo