An end-to-end suite locates elements by their visible text, such as a button reading "Add to cart". The product then ships in five locales and the copy team rewords labels regularly. What breaks, and how do you keep the tests readable without coupling every test to product copy?
answer
- copy is owned by writers, not tests
- one pinned locale makes strings deterministic
- look labels up from the same catalogue
- copy as subject versus copy as route
- exact versus substring changes what matches
basics
~20 sEvery locator matching visible text breaks when a label is reworded or the build is localised, because the string is product copy, not a stable identifier. Pin the suite to one locale, look labels up from the translation catalogue where possible, and let only copy-focused assertions depend on wording.
solid answer
~50 sText is the friendliest thing to match on and the least stable thing on the page. A localised build changes every string at once; even in one language, punctuation, casing, an added icon, or an interpolated count can shift the match. The same applies to accessible-name locators, because the name usually comes from that text. The practical answer has three parts. Run the suite in one pinned locale so the strings are deterministic — usually the source language. Where a label is genuinely dynamic, resolve it from the same translation catalogue the app uses, so a copy change updates test and app together instead of breaking one of them. And decide deliberately which tests are *about* copy: a checkout page asserting its own wording should fail when the wording changes, while two hundred tests that merely click through that button on the way somewhere else should not.
go deeper
Recall that matching on visible text ties the test to the words on screen, so translations and copy edits break it even though the app still works.
Explain the concrete break sources — localisation, rewording, interpolated counts, punctuation and whitespace — and the fixes: pin one locale, resolve labels from the catalogue, scope before matching.
Draw the line between tests whose subject is the copy, which should fail on a reword, and tests that merely pass through a labelled control, which should not — and choose the contract per control accordingly.
Own the cross-team consequence: decide who is accountable when copy changes red the pipeline, and design the coupling — shared catalogue, pinned locale, thin per-locale smoke set — so writers can ship wording without a test-suite negotiation.
## Why text feels right and behaves badly Matching on "Add to cart" reads beautifully in a spec: it is exactly what a user looks for, and a failure message quoting it is instantly understandable. That is why text-based and accessible-name locators are recommended. The catch is that the string is **product copy** — owned by writers, translated by a pipeline, and changed for reasons that have nothing to do with behaviour. The ways it moves are worth enumerating, because candidates usually name only translation: - **Localisation.** A German build changes every label simultaneously; one locale switch reds the whole suite. - **Copy edits.** "Add to cart" → "Add to basket" → "Add". Behaviour identical. - **Interpolation and pluralisation.** "Cancel 1 order" versus "Cancel 2 orders": an exact match fails on data you did not control. - **Whitespace and invisible characters.** Text split across elements, non-breaking spaces, or a trailing icon can defeat an exact match even when the rendering looks identical. Most locator APIs normalise whitespace for you, which helps but does not cover casing or punctuation. - **Truncation.** CSS ellipsis does not change the DOM text, but a component that truncates in JavaScript does. This is not an argument for going back to CSS paths. Text remains far more meaningful than a class name. It is an argument for **deciding where copy is allowed to be an input to your tests**. ## Pin the locale The simplest, highest-value move: the suite runs against one known locale, chosen explicitly rather than inherited from whatever machine executes it. That usually means forcing the app's language — through the app's own locale mechanism, a seeded user preference, or the browser context's language configuration your tool exposes — instead of letting a CI runner's system locale decide. Now the strings are deterministic, and the multi-language question becomes a *separate*, small suite rather than a tax on every test. That separate suite is worth having and should be deliberately tiny: a handful of smoke journeys per locale that check the app renders and functions in that language, plus checks for untranslated keys leaking through. Running the entire regression suite once per locale multiplies runtime for very little extra signal. ## Resolve labels from the catalogue Where a test must reference a label that genuinely changes, import the same translation resource the app uses and look up the key: ```js import messages from '../../src/locales/en.json'; await page.getByRole('button', { name: messages['cart.add'] }).click(); ``` Now a writer's edit updates both sides in one commit. The cost is a coupling to the catalogue's shape and a spec that reads slightly less like English; the benefit is that copy churn stops being a source of red builds. Use it for labels that move often, not everywhere — a hard-coded string is more readable, and for stable labels that readability wins. ## Separate "copy is the subject" from "copy is the route" This is the distinction that answers the question. Some assertions are *about* wording: an empty-state message, an error message, a legal disclaimer, the confirmation text after checkout. Those should be matched on text and *should* fail when the copy changes — that is the test doing its job, and a writer updating the string updates the test with it. Most text matches, though, are just navigation: click "Add to cart", click "Continue", click "Pay" — the assertion at the end is about the order being created. For those, the label is incidental, and coupling hundreds of tests to it is what turns a copy edit into an afternoon. Where such a step sits on a high-traffic path and its label churns, an owned test id on that control is the cheaper contract. ## Matching mode matters Exact matching is precise and brittle to punctuation; substring matching tolerates a trailing icon or period but can match more elements than you meant — "Cancel" also matches "Cancel all". Choose per case, and remember that whichever you choose, matching text never identifies an *instance* on a page with repeated rows; you still scope to a container first. ## The summary an interviewer wants Text selectors are excellent identity and poor stability. Make the strings deterministic by pinning locale, derive them from the source of truth when they churn, and be explicit about the small set of tests that are supposed to break when the words change — because those are the ones actually protecting the copy.
- Would you run the entire regression suite once per locale?No — the cost multiplies with almost no extra signal, because the behaviour under test is the same code path. Run the full suite in one pinned locale, then a small per-locale smoke set that checks the app renders, key journeys work, and no untranslated keys leak. Layout-sensitive locales may also justify a few screenshot checks.
- Which is safer for text matching, exact or substring?Neither by default. Exact matching survives nothing but is unambiguous; substring matching tolerates punctuation and adjacent icons but can match unintended elements — 'Cancel' also matches 'Cancel all orders'. Choose per element, and scope to a container first so that either mode only ever sees the candidates you meant.
- A copy edit turned two hundred tests red. What is the real lesson?That a label on a high-traffic path had become an implicit contract for tests that were not about copy at all. Give that control an owned test id, or resolve its label from the translation catalogue, and keep text matching for the assertions where the wording genuinely is the thing under test.
saying these in an interview costs you the question
- Assumes visible text is a stable identifier
- Runs the whole suite in every supported locale
- Hard-codes translated strings into the spec files
- Thinks whitespace normalisation solves copy churn
- Never lets any test depend on copy, so copy is untested