November 21, 2025
216 strokes bestowed

data Attributes for Testing

edit

I don’t know if this is the world’s best idea, but given the situation I’m faced with, it’s the best one I’ve got.

At my place of work, we have a design system partially built with custom web components. Given how web components are implemented, occasionally there’s an attribute or piece of data we would like to test for that is in the shadow DOM. This poses a small problem for testing.

I want to keep my tests as declarative as possible, but to get that information I have to imperatively reach into the shadow DOM. I don’t like this. Especially if there’s any chance we might change the implementation of the component. I want the tests to keep passing, so long as we haven’t changed the public interface.

As it stands, we sometimes have to search through the shadow DOM to find the right shadow part of an element we want to test. This requires making a special selector for those parts, and means our tests can be break if we make a change to the implementation.

For example, we have a button component that can be passed an href or to prop and it will render as an a tag in the shadow DOM. If I want to look at the resulting href, I have to use our custom shadowPart selector to get that element. I’d rather not have to do that.

The way I’ve gotten around this is by lifting important attributes that otherwise would be buried in the shadow DOM as data-* attributes on the root element. In the case of that href, I lift it to a data-href attribute.

The dev uses the component like so:

<OurButton href="/some/url">Click me</OurButton>

And the DOM looks something like this:

<our-button data-href="/some/url">
  <!-- in the shadow DOM -->
  <a href="/some/url" part="base">Click me</a>
</our-button>

The tests go from looking like this:

expect(wrapper.getByTestId('our-button').shadowPart('base')).toHaveAttribute(
  'href',
  '/some/url',
)

To something like this:

expect(wrapper.getByTestId('our-button')).toHaveAttribute(
  'data-href',
  '/some/url',
)

Notice I don’t need the shadowPart selector anymore. If, like we’re considering, we drop the custom web component and revert to regular DOM elements, the test will still pass.

You can do this on a number of components for any number of attributes or props. For example, our button has the following data-* attributes:

  • data-loading
  • data-href
  • data-variant
  • data-size

And there are more, but that’s enough for you to see how the pattern can be implemented.

These data attributes remain stable while other implementation details change. I’ll give you another example that might apply to you.

Before I added the data-variant and data-size attributes, our tests would look for specific classnames on the button to ensure that a prop was applied properly. The problem with this was if we migrate our system to use something like Tailwind, then all of these tests would break, even if the public interface hadn’t changed.

By verifying that the data attribute is correct (and assuming all functionality remains the same), we can keep our tests passing even if we make changes under the hood.

So give it a try. If you’re in a situation where you have some challenging components to test, you might find the data-* attributes pattern to be helpful.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Want to read more?
Tags
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Introduction to State Machines and XState
Introduction to State Machines and XState
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.