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

Testing pseudo-elements in Cypress

Make interactions and create assertions for pseudo-elements like ::before and ::after using this guide.

Paul Akinyemi
Published September 30, 2022
Table of contents

Ever needed to make an assertion on a pseudo element in a Cypress test? It’s not as straightforward as you might expect. Trying to access a pseudo element with cy.get(pseudoElementSelector) will fail, and if you can’t access it, how can you test it? In this article, you’ll learn how to access pseudo elements so you can test them in Cypress.

What’s a pseudo-element?

As you can tell from the name, a pseudo element isn’t really an HTML element. It’s a CSS selector that allows you to target a part of a real element’s content, or to insert CSS-defined content before / after the element’s content. The most popular examples of pseudo elements are ::before and ::after (which allow you to insert content), although there are a lot more, like ::first-line, ::first-letter, and ::marker.

In everyday work, pseudo elements are mainly used for decorative purposes. They make it easy to add little flourishes to your markup, like giving the first line of every paragraph a slightly different look, or making irregular shapes from HTML elements. We’ll look at a couple of examples. Here’s some code that uses ::first-line and ::first-letter to style a web page:

p {
  max-width: 50vw;
  margin: 15px auto;
  line-height: 1.5;
  font-size: 1.2rem;
}

p::first-letter {
  font-size: 1.5rem;
}

p::first-line {
  color: blue;
}

And here’s what the result looks like:

Now here’s an example from CSS tricks using ::before and ::after:

blockquote {
  font-style: italic;
  line-height: 1.618;
  font-size: 1.2em;
  width: 30em;
  position: relative;
  padding: 50px 90px;
}

blockquote::before {
  content: open-quote;
  top: 0;
  left: 0;
}

blockquote::after {
  content: close-quote;
  bottom: 0;
  right: 0;
}

blockquote::before,
blockquote::after {
  background-color: #cccccc;
  display: block;
  width: 60px;
  height: 60px;
  line-height: 1.618;
  font-size: 3em;
  border-radius: 100%;
  text-align: center;
  position: absolute;
}

And here’s its output:

Why can’t cy.get() access pseudo elements?

Pseudo elements are perfectly valid CSS selectors, so it seems a little odd that Cypress can’t access them. What’s going on? The answer is that Cypress uses jQuery under the hood, and the jQuery engine simply doesn’t support selecting pseudo elements.

Support in Playwright

Playwright doesn’t support selecting pseudo elements either. Unlike Cypress, Playwright doesn’t use jQuery, but you still can’t access pseudo elements in Playwright directly. Why? Pseudo elements aren’t real HTML elements, and thus you can’t get a JavaScript reference to them. JavaScript treats them as parts of a real element’s content, so you can’t get a direct JavaScript reference to ::before or ::after any more than you can directly select the first space in a p tag.

Accessing pseudo elements in Cypress

While Cypress doesn’t provide direct support for accessing pseudo elements, not all hope is lost – there is a workaround, and we’ll show it to you here. Here’s the HTML page we’ll be using as the base for our pseudo element tests:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Pseudo Elements Demo</title>
  </head>
  <body>
    <p data-cy="p">
      Is your architecture prepared for immersive driver growth? Iteratively touching base about integrating cryptoes
      will make us leaders in the company-wide dot-bomb industry.
    </p>

    <p>
      Intelligently engineering effectively best-of-breed brands is crucial to our long-term enterprise. Our business
      productizes capabilities to globally and reliably impact our actionable vertical.
    </p>

    <div class="container">
      <blockquote data-cy="block">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore
        magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
        consequat.
      </blockquote>
    </div>

    <div class="radio">
      <input type="radio" id="radio-1" name="cadet" />
      <label for="radio-1" data-cy="radio">
        <span>Ender</span>
      </label>
    </div>

    <div class="radio">
      <input type="radio" id="radio-2" name="cadet" />
      <label for="radio-2" data-cy="radio">
        <span>Petra</span>
      </label>
    </div>

    <div class="radio">
      <input type="radio" id="radio-3" name="cadet" />
      <label for="radio-3" data-cy="radio">
        <span>Bean</span>
      </label>
    </div>
  </body>

  <style>
    body {
      background-color: #efefef;
    }

    p {
      max-width: 50vw;
      margin: 15px auto;
      line-height: 1.5;
      font-size: 1.2rem;
    }

    p::first-letter {
      font-size: 1.5rem;
    }

    p::first-line {
      color: blue;
    }

    .container {
      display: flex;
      justify-content: center;
      align-items: center;
    }

    blockquote {
      font-style: italic;
      line-height: 1.618;
      font-size: 1.2em;
      width: 30em;
      position: relative;
      padding: 50px 90px;
    }

    blockquote::before {
      content: open-quote;
      top: 0;
      left: 0;
    }

    blockquote::after {
      content: close-quote;
      bottom: 0;
      right: 0;
    }

    blockquote::before,
    blockquote::after {
      background-color: #cccccc;
      display: block;
      width: 60px;
      height: 60px;
      line-height: 1.618;
      font-size: 3em;
      border-radius: 100%;
      text-align: center;
      position: absolute;
    }

    .radio {
      color: rebeccapurple;
      margin-bottom: 10px;
      max-width: 50%;
      margin-left: auto;
      margin-right: auto;
      position: relative;
      z-index: 10;
    }

    .radio label {
      display: flex;
      align-items: center;
    }

    .radio input[type="radio"] {
      display: none;
    }

    .radio input + label::before {
      content: "";
      font: inherit;
      color: currentColor;
      display: inline-block;
      width: 25px;
      height: 25px;
      border: 0.15em solid currentColor;
      border-radius: 50%;
      position: relative;
      left: -30px;
    }

    .radio input:checked + label::before {
      background-color: currentColor;
    }
  </style>
</html>

This HTML renders a page that looks like this:

To follow along, simply set up a local dev server that serves this page and add Cypress to that project.

We’ve already established that we can’t directly access pseudo elements from JavaScript, so how are we going to test them? Here’s the approach we’re going to take:

  1. Get a reference to the parent element of the pseudo element with cy.get().
  2. Use the parent to get a reference to the window object, then use the getComputedStyle method to read the computed CSS of the pseudo element
  3. Use the getPropertyValue method to get the value of the CSS property you want to test
  4. Assert on the returned value.

The following code from Gleb Bahmutov shows this algorithm in action:

cy.get("[data-cy=before-example]")
  .scrollIntoView()
  .within(() => {
    // Cypress does not recognize the selector "p::before"
    // because the jQuery engine does not support them
    // so we get the content through the computed style
    // see https://codepen.io/chriscoyier/pen/Pzzawj
    cy.window().then((win) => {
      cy.contains("Hello").then(($el) => {
        const before = win.getComputedStyle($el[0], "::before");
        const beforeContent = before.getPropertyValue("content");
        // the content is a string, thus we need to quote it
        expect(beforeContent).to.equal('"Greeting"');
      });
    });
  });

And we’re going to adapt this idea to test our web page. Here’s an example of a test that asserts that the color of the p::first-line pseudo element on our page is blue:

it("tests the first-line pseudo element", () => {
  cy.visit("url-to-your-page");

  cy.get('[data-cy="p"]').within(($el) => {
    cy.window().then((win) => {
      const firstLine = win.getComputedStyle($el[0], "::first-line");
      const lineColor = firstLine.getPropertyValue("color");
      expect(lineColor).to.equal("rgb(0, 0, 255)");
    });
  });
});

And here’s a test that checks the width of our blockquote::before pseudo element:

it("tests the before pseudo element", () => {
  cy.visit("url-to-your-page");

  cy.get('[data-cy="block"]').within(($el) => {
    cy.window().then((win) => {
      const before = win.getComputedStyle($el[0], "::before");
      const width = before.getPropertyValue("width");
      expect(width).to.equal("60px");
    });
  });
});

Both tests implement the algorithm we outlined above, and in your test runner, you should see both tests pass:

In our final example, we’ll cover how to get Cypress to click on pseudo elements. The radio buttons on the test page are a little different from normal ones – they’re implemented with ::before pseudo-elements attached to the label tags, instead of being implemented with input tags. You’re going to write a test that clicks on each ::before radio button, and asserts that it has the correct styling after the click.

Add the following code after the previous it block:

it('clicks the before pseudo elements', () => {
   cy.visit('url-to-page')
   cy.get('[data-cy="radio"]').each(($el) => {
     cy.wrap($el).click(-25, 10)
     .within(($el) => {
     cy.window().then((win) => {
       const before = win.getComputedStyle($el[0], '::before')
       const bgColor = before.getPropertyValue('background-color')
       expect(bgColor).to.equal('rgb(102, 51, 153)')
     })
   })

 })

This test is largely similar to the first two. It starts by getting all the label elements with cy.get, then clicks on each of them, and asserts on their ::before pseudo elements. The main difference in this test is the arguments passed to cy.click(). By shifting the location of the click to be 25 pixels left and 10 pixels down from the top-left corner of the targeted element, we are able to get Cypress to click on the pseudo element even though it cannot be targeted directly.

After running the test, you should see that the toggle has been activated as shown below:

The bright red dot shows the location of the click event, and the test passes, showing that the radio button changed its background color as it was supposed to.

And that’s how you test pseudo elements in Cypress!

Reflect: An alternative to Cypress 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 © 2022 Reflect Software Inc. All Rights Reserved.