End-to-end Testing
How-tos & Guides
6 min read

Accessing pseudo-elements in Playwright

Create Playwright tests that access pseudo-elements like ::before and ::after using this guide.

Oluwatomisin Bamimore
Published December 5, 2022
Table of contents
Tired of flaky Playwright tests?
Create fast and reliable tests for anything that runs in a browser.
Learn more

Pseudo-elements are one of the stranger constructs supported in the HTML and CSS specs. Pseudo-elements don’t just modify the styling of an existing element, but can also add content to the page. But unlike a normal element, pseudo-elements are not considered a distinct node within the Document Object Model (DOM) and thus cannot be accessed through the querySelector or querySelectorAll.

Because of these pecularities, pseudo-elements can be quite difficult to test in an automated way, lack native support in many testing libraries including Microsoft’s popular Playwright testing framework. In this article we’ll show you how you can workaround these limitations to test pseudo-elements in Playwright.

Syntax and uses

Pseudo-elements are represented by a double colon (::) followed by the name of the pseudo-element:

css-selector::pseudo-element {
  property: value;
}

For example, you can use the ::first-letter pseudo-element to style the first letter in an element.

<html>
  <style>
    p::first-letter {
      color: #0000ff;
    }
  </style>

  <body>
    <p>Hello world</p>
  </body>
</html>

Other popular pseudo-elements include:

Note that pseudo-elements look very similar to pseudo-classes, such as foo:hover, but are quite different. Pseudo-classes define the styling of an element in a certain state, such as in the state of hovering, whereas pseudo-elements add content or styling to the default state of an element.

To make things even more confusing, Playwright has a similarly-named construct called pseudo-selectors.. Pseudo-selectors, such as :has() and :text(), are methods for accessing elements on the page in ways that are not supported in the CSS spec. In other words, with pseudo-selectors Playwright has extended the CSS spec to offer additional convenience methods for referencing elements in the DOM.

Even though Playwright has extended the CSS spec to provide these convenience methods, it does not include a native way to access pseudo-elements. We’ll show you the workaround for accessing pseudo-elements in Playwright, but first let’s cover how to get your development environment up and running.

Playwright support for pseudo elements

Here’s a simple HTML example with a single pseudo-element defined:

<html>
  <style>
    p::before {
      content: "Yay. ";
    }
  </style>
  <body>
    <p>Hello world</p>
  </body>
</html>

If you view this example in your web browser, you’ll see that the <p> element will the content ‘Yay. ' prepended to the string ‘Hello World’:

Pseudo-elements let you use CSS to render elements that don’t exist in the DOM. Hence the name “Pseudo”. Playwright does not support accessing these pseudo-elements with pure selectors.

Install Playwright using pip:

pip3 install playwright
playwright install
import os
from playwright.sync_api import sync_playwright

current_path = os.getcwd()

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context()
    page = context.new_page()
    page.goto(f"file://{current_path}/test.html")

    h = page.locator("p")

    print(h.inner_text())

After running this code, you’ll observe that Playwright will only return the contents of the original HTML element.

Accessing pseudo elements in Playwright

Playwright provides a page.evaluate() method that executes javascript code. The window.getComputedStyle() function gets all the styles related to an element or pseudo-element.

<html>
  <style>
    #p-text::before {
      content: "CSS Pseudo Content. ";
    }
  </style>
  <body>
    <p id="p-text">HTML Content</p>
  </body>
</html>

Using JavaScript, you can extract the value of the pseudo element’s content.

window.getComputedStyle(document.getElementById("p-text"), "::before")["content"];

The getComputedStyle() function accepts two arguments:

import os
from playwright.sync_api import sync_playwright

current_path = os.getcwd()

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context()
    page = context.new_page()
    page.goto(f"file://{current_path}/test.html")

    content = page.evaluate("window.getComputedStyle(document.getElementById('p-text'), '::before')['content']")

    print(content)

The snippet outputs the content of the pseudo-element.

Pseudo elements and radio buttons

::before is a common pseudo-element for styling checkboxes and radio buttons. Consider the simple HTML page below:

<html>
  <body>
    <form>
      <input type="radio" name="fruit" id="apple" class="fruit-radio" /> <label for="apple">Apple</label>
      <input type="radio" name="fruit" id="orange" class="fruit-radio" /> <label for="apple">Orange</label>
    </form>
  </body>
</html>

Using CSS pseudo-elements, we can change the colour of a radio item, both before and after it has been checked.

.fruit-radio::after {
  width: 15px;
  height: 15px;
  border-radius: 15px;
  top: -2px;
  left: -1px;
  content: "";
  position: relative;
  background-color: #d1d3d1;
  display: inline-block;
  visibility: visible;
  border: 2px solid white;
}

.fruit-radio:checked::after {
  width: 15px;
  height: 15px;
  border-radius: 15px;
  content: "";
  top: -2px;
  left: -1px;
  position: relative;
  background-color: #ffa500;
  display: inline-block;
  visibility: visible;
  border: 2px solid white;
}

CSS Credit: Stackoverflow

While this is a very simple example, this same approach is used by many UI libraries to provide custom-styled radios and checkboxes that still preserve some native behaviors like keyboard support.

The page.click() method in Playwright is used for clicking on selected elements. In some cases, the default click method will work as expected and will click on the input that contains a pseudo-element:

import os
from playwright.sync_api import sync_playwright

current_path = os.getcwd()

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context()
    page = context.new_page()
    page.goto(f"file://{current_path}/test.html")
    page.click("input[id='apple']")

    content = page.evaluate("window.getComputedStyle(document.getElementById('apple'), '::after').getPropertyValue('background-color')")

    print(content)

The snippet outputs:

In this example, Playwright correctly clicks on the element and extracts the expected background color of #ffa500 (rgb(255, 165, 0)). If you find that clicks are not working correctly on elements that contain pseudo-elements, the likely culprit is that the click needs to be modified with an offset so that it clicks on the pseudo-element which lies outside the boundaries of the actual element. To do this, you’ll need to define the options argument in page.click and pass in the position property. So for example if you need to click 10 pixels to the left of the top-left corner of the main element, and 5 pixels above the top-left corner, you can pass in a value of { position: { x: -10, y: -5 }}.

If we wanted to test the default state of the element, we could instead use the following code:

import os
from playwright.sync_api import sync_playwright

current_path = os.getcwd()

with sync_playwright() as p:
    browser = p.chromium.launch()
    context = browser.new_context()
    page = context.new_page()
    page.goto(f"file://{current_path}/test.html")

    content = page.evaluate("window.getComputedStyle(document.getElementById('apple'), '::after').getPropertyValue('')")

    print(content)

The new snippet outputs:

rgb(209, 211, 209 is equivalent to #d1d3d1, the radio button’s original background colour in an unchecked state.

Conclusion

In summary, pseudo-elements lets you add fake elements to the DOM using CSS. These pseudo-elements cannot be selected using ordinary Playwright selectors. However, via the use of the native window.getComputedStyle() method, and Playwright’s ability to evaluate Javascript code, we can fetch and test pseudo-elements in Playwright.

Reflect: An alternative to Playwright with native support for pseudo-elements

Reflect is a no-code testing tool that can interact with pseudo-elements like in the example above.

Creating a test in Reflect is easy: the tool records your actions as you use your site and automatically translates those actions into a repeatable test that you can run any time.

Get automated test coverage for your web app today — try it for free.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © Reflect Software Inc. All Rights Reserved.