How Teststream finds elements
A test says in words what a user does: when: I click "Add recurring invoice". The recording behind it holds Playwright code that finds the same element again on every replay. This page explains which locator that code uses, why text comes last, and what to change in the app when a step still depends on its text.
Where locators are chosen
| When | What happens |
|---|---|
| The AI runs a test (first run, a changed step, a heal) | The AI acts in the browser; Playwright writes a line for each action. Teststream reads the element the moment it is pressed, and again right after the action, and, when something below finds exactly that element, saves that instead of the generated line. Reading it on the press matters for buttons that open another page ("Create", "Back"): after the click they are gone. |
| A recorded test replays | Before each action Teststream reads the element again. A line that still finds its element by text is rewritten when a better locator now finds the same element, for example after the app gained test ids. The run notes "recorded locators now use stable attributes". |
The rewrite is checked in the page: the new locator must find the same element, never a guess.
Checks (expect) keep the text the step names: "the message 'Invoice saved' is shown" checks that text. A check whose text the step does not name ("the Quote list is shown", recorded as "the Create KVA button is visible") moves to the element's test id, id or name. Where an element sits (its part class, its component, its field type) is used for actions only: it finds the element, but says nothing about what a check looks for.
The order
- A test id with the project's attribute (Project settings → Test id attribute, for example
data-test-id;data-testid,data-test,data-cyanddata-qaare recognised too). - An id a person wrote, a field's
nameorautocomplete, or an Angularformcontrolnameon the field. - A field type that says what it is: the only password field, or the only submit button, on the page.
- The element inside its part of the page: the closest parent with a test id or
formcontrolname(rows of a repeated form are told apart by their order), or with an id a person wrote, a component's tag (app-user-menu button) or a part class in the BEM style (.actionbar__nav button). - Only then the text: the element's role and name, label, placeholder or visible text.
When a locator finds several elements, they are told apart only in ways that do not depend on the page's data:
| Several matches | Locator |
|---|---|
| One is visible, the others are hidden copies (a mobile action bar) | [data-test-id="billings_edit_quicksave"] + .filter({ visible: true }) |
| Siblings in one component (the two halves of a split button) | [data-test-id="create_quote"] + .nth(0) |
| Siblings among the visible ones | .filter({ visible: true }).nth(0) |
Rows of a repeated form, inside a parent with a test id or formcontrolname | [formcontrolname="price"] input + .nth(1) |
Otherwise the next way in the order is tried.
An option in a dropdown, a row, a cell, a list item or a menu entry is its content: which customer, which invoice, which action. Its class or its place in the page says only what kind of thing it is, and its order follows the data. Such an element is found by a test id or id of its own that finds exactly it, or else by its text, even when it is the only one shown at that moment.
Never used: a path from body down, a position across the whole page, ids a UI library generates (mat-input-15), framework attributes that change with every build (_ngcontent-*, _nghost-*, ng-reflect-*) and a UI library's style classes (mat-mdc-button, mdc-*, cdk-*, ng-*, modifiers such as --mobile).
Why text comes last
A button's name, a label and a placeholder change with the app's language and with every copy edit. When a replay cannot find an element:
- Teststream looks for it by what it remembered about the element (test id, id, name, role and name, label, placeholder, text).
- Still not found: the AI takes over from that step, in the same browser.
- The AI passes: the test passes, and its new recording waits for review on the test (unless the project turned the review off). The AI fails too: the test failed.
A locator that breaks therefore costs time and an AI run rather than a false failure. In an app with several languages, text locators would send nearly every run in another language to the AI.
Steps found by text
The test's Replay tab shows how many recorded steps still find an element by its text, and marks each one by text, with those texts. To make them work in every language:
- Set Project settings → Test id attribute to the attribute the app uses.
- Give those elements a test id in the app. A coding agent with the Teststream skills does this itself: the
teststream-test-idsskill and the MCP tooltest_ids, which lists the steps found by text (see coding agents). - Deploy the change. The tests themselves do not change: the next run finds the same element by its test id and keeps that.
What to give a test id: every element a user acts on (buttons, icon buttons, links, menu items, tabs, fields, selects), every element that shows a value a test checks (amounts, dates, statuses, counters, messages, empty states), and the containers tests look inside (dialogs, cards, rows). A status that is translated or shown only as an icon also gets its untranslated value, for example data-test-id="invoice_status" data-status="paid".
Keep test ids in production builds: Teststream tests the deployed app.