Testing Pseudo-elements Using Cypress

Elegant testing for pseudo-elements like "::after".

Imagine you have a product store like the one I am using in the course Swag Store. Some of the items are new, so the inventory page marks them with a "NEW" badge

The NEW badge shown next to one of the items

The badge is implemented using a pseudo-element ::after

1
2
3
4
5
6
7
8
9
10
11
12
.inventory_item_name {
font-family: 'Roboto', Arial, Helvetica, sans-serif;
font-size: 20px;
font-weight: 500;
color: #e2231a;
}
.inventory_new_item_badge::after {
content: 'NEW';
position: relative;
left: 10px;
font-weight: 700;
}

The React component simply sets the inventory_new_item_badge class based on the passed prop badge.

1
2
3
4
5
<div
className={`inventory_item_name ${badge ? 'inventory_new_item_badge' : ''}`}
>
{name}
</div>

The HTML markup

How can we confirm the badge is present and has the expected text NEW? Easy. We can get the style of the HTML element and get its style property

1
2
3
4
5
6
7
8
9
const newItemTitle = 'Sauce Labs Bike Light'
// confirm this item has the badge "NEW" after it
// Tip: see how pseudo-elements like "::after" can be tested
// https://glebbahmutov.com/cypress-examples
cy.contains('.inventory_item_name', newItemTitle).then(($el) => {
const after = window.getComputedStyle($el[0], '::after')
const afterContent = after.getPropertyValue('content')
expect(afterContent).to.equal('"NEW"')
})

Tip: I have similar examples under "Recipes" on my site https://glebbahmutov.com/cypress-examples

The solution

What if the badge appears after some delay? cy.then(callback) does not retry, so we can make the above code retry-able using cy.should(callback) since it does not execute any Cypress commands:

1
2
3
4
5
6
7
// test the ::after element with retries
const newItemTitle = 'Sauce Labs Bike Light'
cy.contains('.inventory_item_name', newItemTitle).should(($el) => {
const after = window.getComputedStyle($el[0], '::after')
const afterContent = after.getPropertyValue('content')
expect(afterContent).to.equal('"NEW"')
})

We can improve the above by noticing the individual steps:

  • const after = window.getComputedStyle($el[0], '::after') is simply calling a function window.getComputedStyle on the current subject's first element with the argument ::after. We can write the same code using a query from cypress-map
1
2
cy.contains('.inventory_item_name', newItemTitle)
.applyToFirstRight(window.getComputedStyle, '::after')
  • The above query yields the pseudo-element. We now have to get the property content like const afterContent = after.getPropertyValue('content'). That is a standard cy.invoke command
1
2
3
cy.contains('.inventory_item_name', newItemTitle)
.applyToFirstRight(window.getComputedStyle, '::after')
.invoke('getPropertyValue', 'content')
  • the final assertion inside the cy.should(callback) is the explicit expect(afterContent).to.equal('"NEW"'). If the afterContent is the subject, we can write it using an implicit should('equal', value) assertion completing our refactoring
1
2
3
4
cy.contains('.inventory_item_name', newItemTitle)
.applyToFirstRight(window.getComputedStyle, '::after')
.invoke('getPropertyValue', 'content')
.should('equal', '"NEW"')

It works, as the screenshot below shows

The refactored solution

A dot pseudo-element

Sometimes a pseudo-element is used to simply show a graphical element to get the user's attention. In the next example, a red dot is shown next to the cart icon if the cart has items.

The red dot means the cart has items

The CSS markup for the pseudo-element has no content, just background-color

1
2
3
4
5
6
7
8
9
10
11
.shopping_cart_link_with_items::after {
content: '';
position: absolute;
width: 8px;
height: 8px;
margin-left: 25px;
border-radius: 50%;
background-color: #e2231a;
top: 10%;
transform: translateY(-50%);
}

We can use the background color to verify the dot's presence and absence. For example, let's write a test that confirms that:

  • there is no dot initially
  • a dot appears when the user adds an item to the cart
  • a dot disappears when the cart becomes empty again

We can even make our life harder by adding a delay between the click and the cart update.

cypress/e2e/login/badge.cy.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// a couple of helper functions for simplicity and readability

function haveRedDotBadge($el: JQuery<HTMLElement>) {
const badge = window.getComputedStyle($el[0], '::after')
const backgroundColor = badge.getPropertyValue('background-color')
expect(backgroundColor, 'red dot').to.equal('rgb(226, 35, 26)')
}

function notHaveDotBadge($el: JQuery<HTMLElement>) {
const badge = window.getComputedStyle($el[0], '::after')
const backgroundColor = badge.getPropertyValue('background-color')
expect(backgroundColor, 'no dot').to.equal('rgba(0, 0, 0, 0)')
}

// make the page taller so we can see the entire inventory
// plus the header component with the cart badge
it('shows the "item in the cart" badge', { viewportHeight: 1600 }, () => {
cy.location('pathname').should('equal', '/inventory')

// there is no red dot badge initially
cy.get('.shopping_cart_link').should('be.visible').and(notHaveDotBadge)

const itemName = 'Sauce Labs Bike Light'
cy.contains('.inventory_item', itemName)
.contains('button', 'Add to cart')
.click()

// verify the cart icon has a red dot badge (after possible delay)
cy.get('.shopping_cart_link').should(haveRedDotBadge)

// remove the item from the cart by clicking the "Remove" button
cy.contains('.inventory_item', itemName).contains('button', 'Remove').click()

// and verify the cart icon badge is removed (after possible delay)
cy.get('.shopping_cart_link').should(notHaveDotBadge)
})

Here is the test in action:

Nice!

🎓 This blog post is based on hands-on lessons in my course Testing The Swag Store. Take a look at the lessons Bonus 62: Testing the ::after pseudo-element and Bonus 63: Confirm a dot pseudo-element.