The Automated UI Testing Methodology You Need To Try (Pt. 2)

Paul Grenier
FactSet
Published in
5 min readFeb 7, 2022

--

In part 1, I approached the methodology for automated testing that’s helpful and easy to write. For this installment, the goal is to explore the art of the possible, allowing you to decide what patterns work best for your application. While the examples work in Cypress, you can use these ideas in other test setups where you have control over the Chrome flags and Chrome DevTools Protocol (CDP).

Three people working on a single laptop.
Photo by John Schnobrich on Unsplash

Find Interactive Elements Like A User

A user doesn’t find interactive elements by the label such as button.primary-action. Instead, users lean on their personal experience to scan the page for something that looks interactive with a label or icon that matches the next step in their workflow. Through years of internet usage, people have developed many heuristics that help them determine what looks interactive. For that reason, this interaction is difficult to approximate in code.

Let’s look at keyboard users. A keyboard user who can’t see the screen (a screen reader user) might find interactive elements from special menus that expose elements by role. Or they might explore the user interface (UI) by tabbing and listening for the accessible name of the element they’re looking for. You might find it challenging to navigate with a screen reader. But it’s actually much easier to automate and approximate the experience of a screen reader user.

Press Tab Until…

Our goal is to create a custom Cypress command that can be used in tests like this: cy.tabTo('.button, [role="button"]'). We'll setup the CDP to send Tab keyboard signals to Chrome. This will change focus to the next interactive (focusable) element and stop tabbing when the conditions are met.

First, copy the contents of Puppeteer’s keyboard file to cypress/support/keys.js. Then, copy the code below to a file called key.js in the same folder.

key.js

Now, add command.tabTo.js to the same folder and paste in the code below. This first evolution of the command takes a CSS selector and turns it into a matcher function.

tabTo.js version 1

To start using your custom command, add require('./command.tabTo') in your commands.js file. Then in your test, add cy.tabTo('button').click(). Because we've added the other key commands, you can also use the keyboard to invoke the button with Space/Enter: cy.tabTo('button').keyPress('Space').

Note: this wouldn’t work with Cypress’s built-in.type(‘{space}’) because it simulates the event.

Content Selector and Matcher (XPath)

If you have this working, you can start updating some tests. What happens when there are multiple buttons to navigate and you only want to stop on the “save” button? Let’s accept an XPath selector! You can use these selectors in Chrome devtools for debugging with $x(...). And if you're new to XPath, check out this handy cheat sheet. We're shooting for this: cy.tabTo('//button[contains(text(), "save")]').

If you just want to start using XPath in Cypress, install the cypress-xpath plugin. But we won’t need that to update tabTo.js.

tabTo.js version 2

No doubt, you’ve already thought of several scenarios where XPath selectors can be helpful, such as parent and content queries. However, XPath selectors are often tightly coupled to the implementation which makes them fragile. Fragile tests break when the implementation changes.

Additionally, XPath selectors are typically case-sensitive. Making every content-based selector case-insensitive is a chore and will make the selectors long and less readable. You will probably create a helper to do it for you, but now the selector is no longer copy/pasteable. sad trombone noise

And then there’s the dreaded [role="button"] scenario. While I could expound on why this is a poor implementation choice, if done well, there's technically nothing wrong with it. However, now I have a union XPath (uses | like a CSS comma) and my long selector just doubled in size again.

What if the UX team changed our “save” button into an icon button breaking our existing selector? The design of our new selector will depend largely on how we mark up the icon. As an <img> we can look at the src, but as an <svg> things could get complicated. We can't even rely on alt attribute because an <svg> won't have it. So many unions! And we haven't considered the possible options for providing an accessible name from aria-* attributes, title, or visually hidden content.

Name/Role Matcher (computedName, computedRole)

Accessible name and accessible role to the rescue. Only, it’s not called that and to access it in JavaScript, you have to enable a flag in your browser. While you’ll still be able to copy/paste your “selector,” you can only do this in Chromium browsers (you can follow the issue for Firefox here).

The accessible name, or computedName property, is available behind the enable-experimental-web-platform-features flag. Once you enable it and restart the browser, every element can tell you its accessible name based on the calculations for accessible name specification. Unlike the Accessibility Object Model (AOM) getComputedAccessibleNode, this is a synchronous API. No need to await the calculation.

To get Cypress to start the browser with the flag enabled, you need to update your plugins/index.js file. Here’s mine:

plugins/index.js

Now we can add custom Cypress commands to write assertions against an element’s name and role:

command.computed.js

In addition to assertions, we can use this to write a matcher function for tabTo: cy.tabTo(el => el.computedRole === 'button' && el.computedName.match(/save/i)). If you know your regular expressions (RegExp), you'll notice that I'm looking for "save" with the case-insensitive flag. You could be strict about start and end, but then you have to worry about whitespace breaking your matcher and it quickly gets ugly, /^(\s+)?save(\s+)?$/i. our design will determine how specific you need to be with your RegExp.

What’s Next?

We need to talk about element state. The value part of Name, Role, and Value which are conveyed to assistive technology (AT) users with aria-* attributes. We can write this into our matcher functions, but I personally like to use objects because I can manipulate them easily across multiple tests. In the next installment, I'll show you how and why I'm writing test code like this cy.tabTo({role: 'button', name: 'save', disabled: true}).

--

--

Paul Grenier
FactSet
Writer for

Web developer and engineering manager focused on accessibility at FactSet.