HTML Attribute for Tests & Tool Integration
Websites, especially SaaS products, often integrate with tools like Glassbox, Adobe Analytics, and Cypress. These tools work by querying or binding elements in the DOM and capturing their changes. They are most often configured to use classes and/or ids.
Using classes and ids does work, but there's another approach myself and some coworkers came up with several years ago (yes, before Cypress and Playwright started recommending it ๐), it's a custom data-ref
attribute.
The data-ref attribute
This special attribute serves as a dedicated selector for tests and integrating third-party tools. It looks like:
<button data-ref="..."></button>
It works on any HTML element, which makes it just as viable as classes and ids.
Examples
Let's say an end-to-end test needs to verify the presence of the login button before proceeding to the next step. Add a dedicated data-ref
attribute instead of a class to the button and test:
const loginBtn = document.querySelector('[data-ref="login"]');
expect(loginBtn).toBeDefined();
Another example could be a product manager wanting to analyze user engagement with a new feature on the Home page, like "How many people scrolled to the new feature and how many clicked the see more link?" Add data-ref
attributes to the elements being tracked:
<h2 data-ref="home.new-feature.intro">The New Feature!</h2>
<p>Lorem ipsum...</p>
<a data-ref="home.new-feature.expand" href="...">See more</a>
So there's a couple quick examples of how it gets used. Let's look at the attribute's name and value in a little more detail.
Attribute name
The name includes the familiar "ref" abbreviation used by many frameworks (ref is short for reference). Ref is preferred because it's short and applicable to any tool, whereas "test", "e2e", "track", "session", or "at" (acceptance test) all have too narrow of a meaning.
The standard data-
prefix was added to ensure compatibility and prevent interference with frameworks that use ref
.
Attribute value
Unlike classes which are mixed together with other classes, attribute values are separate and exclusive. This makes them more reliable and also more readable.
Attribute values also have a lot of flexibility. You can put just about anything in them. This means patterns like {page}.{module}.{element}
and {page}.{items}.{id}
can be established that help provide useful context to anyone in contact with these attributes:
<p data-ref="home.announcement">You won!</p>
<button data-ref="account.profile.save">Save</button>
<ul>
<li data-ref="products.shoes.xm4u-020">Yeezy 500 Salt</li>
<li data-ref="products.shoes.ac3e-001">Yeezy Boost 380</li>
</ul>
Get creative and do whatever suits you!
What's wrong with classes?
Nothing, except they're being used for everything. Ever change a style class and accidentally broke an integration or test? Many of us have probably done that!
The problem is so widespread in fact that a lot of methodologies (BEM, SMACSS, SUIT, js- prefix, and more) have popped up over the years attempting to help create more manageable class names. In reality, they're just engineering around the design flaws of class overuse.
The solution is no secret: decoupling and separation of concerns
All but one of the requirements solved with classes have better alternatives:
Event bindings
They never needed a class. Custom attributes, like Bootstrap's data-target
, worked well, but the standard onevent
handlers are and always have been a better design choice and frameworks like React and Vue made them easy to use. Goodbye .js-*
classes!
UI components
They are so much better implemented as custom HTML tags. Take a simple component like Icon or Alert:
<div class="icon icon-user"></div>
<div class="alert alert-success alert-dismissible">...</div>
vs.
<m-icon name="user"></m-icon>
<m-alert type="success" autodismiss>...</m-alert>
Big difference! The familiar tag+attribute design is more robust and readable than the boilerplate div and class.
Seriously, look how bad classes get (and this example is the state-of-the-art when it comes to CSS) compared to the beauty of custom HTML tags :
<div class="mdc-layout-grid">
<div class="mdc-layout-grid__inner">
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-3 mdc-layout-grid__cell--span-6-phone"></div>
<div class="mdc-layout-grid__cell mdc-layout-grid__cell--span-9 mdc-layout-grid__cell--span-6-phone"></div>
</div>
</div>
vs.
<m-row>
<m-col span="3 sm-6"></m-col>
<m-col span="9 sm-6"></m-col>
</m-row>
Hello, genuine API! Goodbye convoluted methodologies!
Test and tool integration
A web standard does not exist, so a custom attribute like data-ref
decoupled from everything else and focused on specific concerns is the next best thing. No more broken tests or missing analytics because "Sorry, I thought that class was only used for styling!"
Styles
Generic style classes like flex
and bg-red
are the one requirement still best solved with classes.
Configuring tools for data-ref
The number of tools that could use data-ref is huge. There are a ton! They generally fall into three categories:
Testing & monitoring
These are used for component, acceptance, end-to-end tests and site monitoring. Examples include:
Datadog Synthetics
Cypress & Playwright (which now recommend using this pattern)
Selenium
Jest
Session replay
These tools record users' sessions, which makes it possible to replay later for tech support situations and user research. Examples include:
Glassbox
FullStory*
Tealeaf
User Analytics
These tools collect lots of user data like pages visited, page events, user interactions, device, and time. Effective product orgs use these tools to run tests and collect data for better decision-making. Examples:
Adobe Analytics
Google Analytics
Quantum Metrics
All of these tools work with data-ref just as well as classes.
*Cool story: When we came up with data-ref, FullStory was our only tool that didn't work with it. We asked them to support it and they cut a new release in less than two weeks. FullStory is awesome!
Configuration
Configuring these tools to use data-ref is simple. Tests using the class selector just use the attribute selector instead:
// Before
const loginBtn = document.querySelector('.login');
expect(loginBtn).toBeDefined();
// After
const loginBtn = document.querySelector('[data-ref="login"]');
expect(loginBtn).toBeDefined();
Tools with an admin console will have a section where you configure the integration. It varies from tool to tool, but there will be a setting where you set the selector(s) to use. For example, this Adobe Launch configuration tracks all clicks on any element with a data-ref:
Other tools support searching through logs/recordings and a universal "data-ref" query is so much better than having to find all the possible classes and searching for those.
Conclusion
Data-ref's decoupled and focused design make its purpose clear and avoids risks associated with classes. So, in the absence of a web standard*, data-ref might be the next best method for test and tool integration.
*I might propose a new standard. I'm thinking something like ARIA...message me if you're interested in collaborating.
What do you think? What other approaches have you tried?