Menu

Playwright Tips

There are many viable UI testing tools out there, and to be honest not a great deal to differentiate them. More or less the same feature sets, more or less the same syntax. So what makes Playwright such a popular option?

For my money it has the feel of a product developed by committed users. It has everything that seasoned testers and developers want from an automation tool - an integrated test framework to have you up and running in minutes, an API that enables concise, readable code and default behaviour such as built-in wait that anticipates the vagaries of web app testing.

Here's a few tips and tricks I've gleaned over the years of my useage:

DOM traversal using has predicate

Playwright has a definite design philosophy: tests should manipulate user-facing elements, using self-explanatory function chains.

Having seen too many brittle and ugly xpaths of the //div[@class='sideNavBar']/div/div[2]/ul/ul[2] variety, I think it's an excellent design choice. And the product is committed to it - contrasting with most browser-driving testing frameworks I've used, there isn't an element at all - the class has been removed from the playwright API.

Each locator method in playwright returns a carved-out chunk of the DOM, be it one tag, a tag containing other tags, or an array of either. In javascript it's trivial to find an element's parent (e.g. document.getElementById('answer-item).parentElement), however playwright has no parentElement property because there is no parent element to the part of the DOM returned by the locator.

Instead, the has filter allows you to search for the parent directly, so for the HTML hierachy:

<div id='answer'>
  <div class='row active' role='row'>
    <span class='question-number-column'>34</span>
    <span class='answer-column'>1886</span>
    <span class='explanation-column'>First coca-cola sale.</span>

If we were interested in retrieving the value of span.answer-column for the question number 34, we might first look for the row:

const answerRow = page.locator(page.getByRole('row'), { has: page.locator("span.question-number-column").filter({hasText: "34"})});

and then find the child element we are after:

const answer = answerRow.locator("span.answer-column").innerText();

Navigating Shadow DOMs

Shadow DOMs have been adding degrees of difficulty to UI testing for over a decade now. If you haven't come across the concept, Shadow DOMs allow web components to be individually styled without the styles affecting the rest of the page. You could consider the Shadow DOM root to be like a reference to a separate HTML page:

<div id='chatBot container'>
  #shadow-root (open)
  <div id='menu-row'>

As they are references rather than attached to the DOM, XPath selectors will not traverse Shadow DOMS - the contents are entirely invisible to XPath selectors. However, css selectors will, and, mercifully, playwright locators work with a Shadow DOMs out of the box. The exception is, of course, locators using Xpath.

If you do really want to use an XPath locator within a Shadow DOM, you'll need to base the XPath locator on a non-XPath locator that selects items in the Shadow DOM:

page.locator("div#menu-row").locator("//div[contains(@class, 'menu-item')])

Connect Over CDP

Playwright UI Mode provides an excellent in-browser test debugging experience. However, I find the bulk of time automating is spent developing the interactions with individual pages or elements - usually in a Page or Component Model rather than the test itself. For that I find connect over CDP invaluable.

This chromium feature allows you to connect to an already running chrome session. So, if you are automating a page - let's say https://example-app.com/sophistiform - that requires a lengthy authentication and data setup process to arrive at, this option will let you:

  • Open a browser and run a script to complete the authentication and setup process
  • Connect to this session and iteratively develop your sophistiform manipulation code

Being able to keep a browser page open on an interface and continuously rerun a script to verify manipulation is an invaluable timesaving way of working.

CDP relies on the browser being run in 'remote debugging' mode - this will mean running chrome with the --remote-debugging-port argument - e.g., on OS X:

/Applications/Google Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=922

You can then connect to this open browser session and automate away:

import {chromium, Browser, Page} from 'playwright'

const browser = await chromium.connectOverCDP('http://localhost:9222');

As a further timesaver, you could script the browser opening logic at the beginning of your automation session:

const chromeAppLocation = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
const spawnedProcess = spawn(chromeAppLocation, [ '--remote-debugging-port=9222', '--no-first-run', '--no-default-browser-check', '--user-data-dir="/tmp/chrome-tasks-debug"' ], { detached: true, stdio: 'ignore' });
spawnedProcess.unref()

I tend to use canary as my reserved browser for operating in this way - I can iteratively test and debug in canary while still running automation scripts in chrome proper.