Selectors

For AI agents: this page is Markdown at https://squiggle.sh/docs/flows/selectors.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.

A selector tells a step which control to act on. Pick one that names what the control is, and the flow keeps finding it when the copy, the language or the layout changes.

Which selector to use

Pick something that happens to apps all the time, and see which selectors still find the Add to cart button:

The screen the flow was written against.

  1. testID{ testID: add-to-cart }Still found: finds the button
  2. text{ text: Add to cart }Still found: finds the button
  3. nth{ role: button, nth: 2 }Still found: the second button is Add to cart
  4. point{ point: "50%, 78%" }Still found: the point is on the button

Use a testID wherever you can. It is the only selector that survives a reword, a translation and a redesign. Text is the quickest to write and the first to break.

Selector fields

A selector must match exactly one control. From strongest to weakest:

FieldMatches
testIDThe control’s testID. The strongest choice.
sourceWhere the component is defined, such as LoginForm.tsx:42. Needs the Babel plugin.
componentThe component’s name, such as CartRow.
roleThe accessibility role, such as button or tab.
labelThe accessibility label.
textText on screen. In a step, text also matches the accessibility label.
callSiteThe exact line that renders this element, as describe reports it. For checking a line you just edited; it never heals.
pointA position, such as "50%, 50%". Use it only when nothing else works.

Combine fields

Fields in one selector all have to match. A testID with text beside it names the control and checks what it says, so this passes only while the counter reads exactly 3 items:

YAML
- assert: { visible: { testID: cart-count, text: "3 items" } }

text has to match the control’s whole text, not part of it. In the accessibility tree it is compared with the control’s accessibility label, ignoring case.

When a selector matches several controls

Narrow it with within, which looks only inside another control, or pick one by position with nth (counting from 1) or index (counting from 0):

YAML
- tap: { text: Remove, within: { testID: cart-row-2 } }- tap: { role: button, nth: 2 }

Prefer within. A position points at a different control as soon as one is added above it.

What a selector can see

Squiggle looks in two places: the components React rendered, and the accessibility tree. The second covers what React doesn’t draw, such as native tab bars and system dialogs, so a selector for a native tab works too. A check like visible reads the current text, so a counter that changed since the screen opened still matches.

The same goes for a control’s name. When a button changes its testID, label, role or hint, or gains or loses its onPress, without being remounted, selectors see the new values from the next render. You don’t need a key to force a new control: a Next button that turns into Get started is found by its new name, and its old name no longer matches.

Name a screen

route takes a screen’s own name, or the full path of navigators that leads to it. Both must match exactly:

YAML
- assert: { route: detail }             # the screen's own name- assert: { route: shop/cart/detail }   # the navigator path to it

cart/detail doesn’t match shop/cart/detail. Use the path when two screens could share a name. Expo Router groups like (tabs) are part of the path.

Make controls easy to select

A Pressable that wraps an icon and some text shows up as two separate elements, and neither one is the button. Give the control a role, a label and a testID, and it becomes one named thing:

SectionToggle.tsx
<Pressable  accessibilityRole="button"  accessibilityLabel={title}  testID="section-toggle">  <Chevron />  <Text>{title}</Text></Pressable>

squiggle legibility scores the screens your app has shown out of 100 and lists the controls that have no good selector, with their file and line. It starts with the edits that raise the score most, one per line of JSX, each with the score it leaves you at. squiggle legibility --fix adds the missing testIDs for you to review. In CI, pass --min-score 80 to fail below that score, and raise the number as you fix things.

To check that a VoiceOver user can reach every control a flow uses, run it with --ax-only, which finds each control through the accessibility tree alone. See Run settings. For a control that must not be pressed by an agent, add agentHint (see the SDK reference).