One day, we got a mockup from designer.

Then we start crafting the page. At first, we might work on making UI and UX of components. That’s the main part. E2E testing is not a concern at the moment.

When developing, if we were luckily get the test case from QAs (or whoever wrote it), should we consider it when crafting our components? (And yeah only if we have enough time.)

By the way, test case I mentioned here is for E2E testing (automation testing), not for human.

To make writing E2E testing smoothly, it is good to add some HTML attributes which the test case is planned to query. Some ACTIONS in test cases like click a button, type something in input, get text from a div element, is the elements we want to query in the future.

But how about we haven’t got the test case yet? If so, we don’t know what elements will be queried very clearly. And here it comes the thought in my mind:

We might need some rules, then we don’t have to guess every time. With rules, the coverage will be guaranteed at acceptable level.

So, I made some rules for myself. I will discuss it into several parts:

  1. Component specific
  2. Location specific
  3. State specific
  4. Invisible data specific
  5. Structure specific

In this post, I will use React component to explain the concept. In fact, it doesn’t matter which front end framework are you using. It will always lead to HTML itself. Whether it is JSX or not.

Component Specific

Page Components

Use data-test-page attribute in the outer HTML tag: data-test-page='<PAGE_NAME>'. With the data-test-page attribute, we can easily know the BOUNDARY of the page component.

PS. The attribute name is up to you! I will show the best choice of mine. 🙃

For example, if we have a home page component:

function HomePage() {
  return <div data-test-page="home">{/* ... */}</div>;
}

Basic components

Use data-test-comp attribute in the outer HTML tag: data-test-comp='<COMPONENT_NAME>'. With the data-test-comp attribute, we can easily know the BOUNDARY of the basic component.

function Button() {
  return <div data-test-comp="button">{/* ... */}</div>;
}

Location Specific

The component might be used many times. (That’s why we make a component) Thus, data-test-comp will be duplicate and not easy to be recognized. Saying if we used 5 button components at the same page, but we just want to query the specific one and then click it. At this point, only we can do is just get all button elements, then search the label in the button. Finally click the button we want. The process might be tedious. So I am wondering if there was a better way to do it. What if we have unique attribute (or nearly unique, which means rarely used), it will be easier.

Therefore, we still need another FEATURE attribute for this case. (The attribute name is better to be UNIQUE) Use data-test-feat attribute to tell us what feature (or purpose) of the component is.

In my opinion, this attribute is optional. We still can query the right element we want with CSS selector. And also, overuse it might cause duplicate attribute name very easily. We never remember every line of codes we write, right? So make a good balance.

So there might be 2 ways to do it.

In Container Element

function HomePage() {
  return (
    <>
      {/* ... */}
      <div data-test-feat="confirmBtn">
        <Button />
      </div>
      <div data-test-feat="cancelBtn">
        <Button />
      </div>
      {/* ... */}
    </>
  );
}

In the example above, we clearly know that in home page, there are two buttons: confirm button and cancel button.

If wrapping component is annoying for you, another way is to insert it into component by props. Here’s the way:

Insert Into Component Through Props

Another way without the container element, is to pass the attribute name into the component through props:

function HomePage() {
  return (
    <>
      {/* ... */}
      <Button dataTestFeat="confirmBtn" />
      <Button dataTestFeat="cancelBtn" />
      {/* ... */}
    </>
  );
}

In button component:

function Button({ dataTestFeat, btnText, labelText }) {
  return (
    <div data-test-comp="button">
      <label>
        {labelText}
        <button data-test-feat={dataTestFeat}>{btnText}</button>
      </label>
    </div>
  );
}

State Specific

Sometimes, we may want to know the state of some components. Saying we have a toggle button component like this:

toggle button

In test case, we want to know that after clicked the toggle button, it will change to OFF state correctly, for example.

We can still know what the state is by its style. If you used SASS that might not be big problem. But as I recently used tailwindcss very often, it become not quite straightforward… Here is what the component looks like:

function ToggleButton() {
  const [isOn, setIsOn] = useState(false);
  const handleChange = () => setIsOn((prev) => !prev);
  return (
    <button
      type="button"
      className="rounded-full overflow-hidden relative w-16 h-7 shrink-0 text-white font-semibold text-sm uppercase ml-2.5 bg-neutral-500"
      onClick={handleChange}
    >
      {/* here is what the difference by state */}
      <div className={`absolute transition left-1.5 top-1/2 -translate-y-1/2 ${isOn && "translate-x-9"}`}>
        <div className="rounded-full bg-white w-4 h-4 relative">
          <div className="absolute right-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">On</div>
          <div className="absolute left-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">Off</div>
        </div>
      </div>
    </button>
  );
}

In here, CSS class translate-x-9 is which the style difference between ON and OFF state. It is still possible to recognize, but just like hell to query a long class like this… it especially happen when you use utility first css library like tailwindcss.

So in this situation, it is better to have a state attribute:

function Button({ btnText, labelText }) {
  const [isOn, setIsOn] = useState(false);
  const handleChange = () => setIsOn((prev) => !prev);

  return (
    <div data-test-comp="button">
      <label>
        {labelText}
        <button data-test-state={isOn} onClick={handleChange}>
          {btnText}
        </button>
      </label>
    </div>
  );
}

Another common case is loading state. Suppose that we have a list and search bar component here:

function SearchPage() {
  // ...some state here
  return (
    <div data-test-page="searchPage">
      <div data-test-comp="searchBar">
        <input value={searchValue} data-test-feat="searchInput" />
        <button onClick={handleSearch} data-test-feat="searchBtn">
          Search
        </button>
      </div>
      <div data-test-comp="list" data-test-loading={isLoading}>
        {/* list data here */}
      </div>
    </div>
  );
}

PS. For readability, I used raw HTML instead of wrapped components.

After click the search button, it will start loading to fetch new data from API. Then finish the loading, update new data in list component.

Invisible Data Specific

Suppose that we have a card component with product info. To find specific product very quickly, it is nice to have a product id on the component. But the product id does not expose to user. So we need to add it as attribute into the element:

function ProductCard({ productId, ...restProps }) {
  return (
    <div data-test-comp="productCard" data-test-product-id={productId}>
      {/* product card content */}
    </div>
  );
}

For naming rule here, I think every SEMANTIC name will be fine.

Structure Specific

Simulated Elements

Under some special situation, we will use div element to simulate specific HTML elements.

For example, like table-simulated elements, use data-test-el attribute: data-test-el='<ELEMENT_NAME>'

function Table() {
  return (
    <div data-test-el="table">
      <div data-test-el="tbody">{/* ... */}</div>
    </div>
  );
}

This case might not be familiar for everyone. But still I can explain why I have to use this.

I met some CSS issue of table related elements (namely, <table>, <thead>, <tbody>, …etc) in Safari browser. So I have to use divs to re-build the same structure as table. Adding attributes is easier for me to read the whole structure.

Element For Component Structure

Sometimes, we need to understand a component structure quickly. To do this, insert attribute in element as flag, will improve the readability. Saying we have a dialog component here:

function Dialog() {
  return (
    <div data-test-comp="dialog">
      <div data-test-el="dialogHeader">this is dialog header</div>
      <div data-test-el="dialogBody">this is dialog body</div>
      <div data-test-el="dialogFooter">this is dialog footer</div>
    </div>
  );
}

After adding data-test-el attributes, it is more efficient to understand the component structure at a glance.

Conclusion

It will be a long journey adding a lot of attributes when developing. Besides, at this moment, test case is not the biggest concern. With some rules, we can do the tedious tasks without thinking too much. After finish the development, writing test cases might be smoothly. We might be thankful for our past selves.

And after all, it’s just my personal thoughts, NOT an industrial standard. 😎 I will be glad if someone benefit from it.

Cheers.