Some web-elements are dynamically generated as a result of a previous action. For example, selecting a check-box in a web-form might populate the UI with another panel or field. Often these elements similarly have a dynamically generated Class or Attribute name and no static ID or Class we can reference when running cy.get(). A great example of this can be found over at Amazon, inspecting the DOM there should give you an idea of just how dynamic everything is. The following is a div Amazon use which contains a list of products:
<div id="ProductGrid-gVjGQiI">
Refreshing the the site will return a new ID:
<div id="ProductGrid-MenYuRd">
As you can see, the ID starts with “ProductGrid-” and is followed by a dynamically generated value.
I have worked on projects that similarly do this but with data-testids, an element Attribute I am certain a number of us QA have seen and worked with.
Ultimately, with React and other frontend libraries this is often the case, and it can make selecting these elements as part of an automated test complicated. And naturally, traversing the DOM hierarchy by specifying where an element is located by supplying a sequence of web-elements (this is when you do something like: cy.get(“#someId > div > class > ul”) or cy.get(#someId).child(“div”).first() ) might impact selecting speed and introduce flaky tests if the hierarchy of the DOM changes.
In this short guide, I’ll go over some of my own solutions, demonstrating how I approached this issue.
Using Regex
We find elements by supplying a string that represents an element in most test automation frameworks, the same goes for Cypress. We run cy.get(“#someBox”) to get an element which has an ID of “someBox”. Because we supply strings, we can leverage Regular Expressions (Regex) to find us a specified string that matches a specific pattern. The most common Expressions I use are:
- ^ to indicate that a string must start with the specified string
- * to indicate that a string must contain the specified string
Lets put this into an example. Assume that we have the following element:
<div class=”MuiButtonBase-root MuiListItem-root jss457 MuiListItem-gutters MuiListItem-divider MuiListItem-button” tabindex=”0″ role=”button” aria-disabled=”false” data-testid=”list-item-elkjhk”> |
Within this div, we have data-testid which has a value of “list-item-elkjhk”, to find this web-element we could then do:
cy.get(‘[data-testid^=”list-item-”]’)
This would find any element that has an attribute of data-testid which has a value that starts with “list-item-” and it wouldn’t care what comes after the final dash.
This might return more than one element as it matched multiple elements with a data-testid that has a value starting with “list-item-”, you would get results of the cy.get() returned as an array if so. What you could then do is add an .eq(index) or even .first() at the end of the cy.get():
cy.get(‘[data-testid^=”list-item-”]’).eq(3)
(remember that you’re running this against an array, so eq(0) would get the first element)
If you have an element that starts with the dynamic value or if the static bit is somewhere in the middle of the string, you could try the wildcard (*) Expression. Assuming we have a div like so:
<div class=”MuiButtonBase-root MuiListItem-root jss457 MuiListItem-gutters MuiListItem-divider MuiListItem-button” tabindex=”0″ role=”button” aria-disabled=”false” data-testid=”elkjhk-static-stuff-pgyt”> |
You could use:
cy.get(‘[data-testid*=”-static-stuff-”]’)
This would find any element that has an attribute of data-testid which contains a value of “-static-stuff-”, not caring what comes before or after.
Reusability
Finally, we could build on-top of this approach to find elements by reusing it as a custom command:
//..Custom Command File function getByDataTestId(dataTestId) { cy.get( [data-testid${dataTestId}] );} //..Test Spec cy.getByDataTestId(^=”list-item-“) |
- A custom command that accepts a parameter to populate the value part of the data-testid
- Once you import & bind the custom command, you can then call it in the test spec
- We need to make sure that the = part stays in the test, because we need this to place a * or ^ Expression before the equals for the pattern to work and the custom command to remain reusable
- You should then be able to chain a .eq(), .click(), .type() or .then(()=>) on the custom command in the test if you need to process the element further
One of my favourite things with the wildcard Expression (*) is that you could leverage it to return all elements that match the specified pattern and run an assertion on each one or interact with each one. This is particularly useful in lists and tables when needing to assert a number of values without duplicating code, or even interacting with multiple inputs that always start or have a specific string as the Attribute or Class value. Something like:
//..Custom Command File function typeAmountInEachInput(dataTestId, amounts) { cy.getByDataTestId(dataTestId).each(($element, index) => { cy.wrap($element).type(amounts[index]); }); } //..Test Spec cy.typeAmountInEachInput(“*=input-field”, [25, 34, 99, 55]) |
Conclusion
A simple but effective way to work with web-elements that have dynamic values. You could use other Regular Expressions of course which may suit your needs better and this should work for any element Attribute, not only data-testids.
Overall, whether this approach works or not heavily depends on the DOM you’re working with, but I hope that some of you might find this useful.