Here is the bug that made me stop trusting position-based selectors, and it is worth more than any rule I could give you.
A test added the first product to the cart:
'.product-card:nth-child(1) button'
It worked for months. Then marketing added a "Featured" product to the top of the page. The test still passed. It was adding the wrong product to the cart, and it could not tell the difference.
That is the worst thing a test can do. Not fail. Pass while being wrong.
A locator is a description of how to find an element. Every framework has its own syntax, and they all express the same handful of strategies. Here they are, roughly best first.
By test id. An attribute developers add to the app for testing and nothing else:
<button data-testid="add-to-cart">Add to cart</button>
Nobody renames it by accident, because it exists for one reason. When you can influence the app's code, ask for these. They are the most stable thing you can target.
By role and accessible name. Finding the element the way assistive technology sees it: a button whose name is "Add to cart". This is stable, and it has a second benefit. If your test cannot find the button by its role, a screen reader user probably cannot find it either. Your test is checking accessibility for free.
By label. For form fields, find the input by the visible label text next to it. This matches how a user fills a form.
By visible text. Good for buttons and links. Know the trade-off: if the wording changes, your test fails. Usually that is right, because the wording is part of what users get.
By ID attribute. #save-button is fast and precise when the app has real IDs. Many apps generate random ones, so check they are stable across page loads before relying on them.
By CSS class. Use with care. Classes exist for styling, and styling changes constantly. A class like .MuiButton-root-2891 is generated by a library and will differ in the next build.
⚠️ Common confusion: beginners reach for CSS selectors first, because they look familiar.
A long path like
div > div:nth-child(2) > span.text-smdescribes where the element sits, not what it is. It breaks the next time anyone edits the page, and worse, it sometimes keeps passing while pointing at the wrong element.Ask one question of every locator you write: would this still be correct if someone reordered the page or restyled it? If not, find a better one.
One more idea you need before any framework: a locator is a description, not a captured element. In modern frameworks, when you write a locator you are not grabbing the element. You are writing down how to find it, and the framework looks it up each time it needs it. This is why locators survive the page changing underneath them, and it is a favourite interview question.
Remember this: target what an element is, never where it sits.
You will meet this same list again in each framework's own syntax: Cypress, Playwright and Selenium. The ideas do not change, only the words.