Control flow — if, else and loop
Conditional branches and loops: how the flat marker steps pair up, the six condition types, and why AI-generated tests never contain them.
Tests are not always straight lines. A cookie banner may or may not appear; a list may have one page or six. Vera has two control-flow constructs for that, and one structural rule you need to know before you use them.
Flat with markers, not nested blocks
if, else, end-if, loop and end-loop are individual steps in the same
flat list as everything else. They are not containers holding children.
1 goto /dashboard
2 if element-exists .cookie-banner
3 click .cookie-banner .accept
4 end-if
5 assert visible .welcomeStep 3 is not "inside" step 2 in the data — the list is flat, and steps 2 and 4 are markers that the runner pairs up. The builder indents the steps between them so it reads as a block.
This matters in one practical way: a marker without its partner breaks the
pairing. Delete the end-if and everything after the if becomes conditional.
Drag a step out from between the markers and it stops being conditional. The
builder's indentation is what tells you where the boundaries currently are, so
watch it when you reorder.
The runner pairs markers with depth-aware matching, so nesting works correctly: an
if inside an if finds its own end-if, not the inner one's.
if / else / end-if
if <condition>
…steps run when the condition is true…
else ← optional
…steps run when it is false…
end-ifThe condition is evaluated once, when the if step is reached. If it is false and
there is no else, the runner jumps past the end-if.
loop / end-loop
loop <condition>
…steps repeated while the condition holds…
end-loopThe condition is re-evaluated before every iteration. A loop whose condition never goes false would run forever, so loops carry a maximum iteration count — when it is hit, the loop exits and the run continues rather than hanging. Treat that cap as a safety net, not a control: a loop that reliably hits it is a loop with a condition that does not describe what you meant.
Conditions
Both constructs use the same six condition types:
| Condition | True when |
|---|---|
element-exists | The selector matches an element on the page. |
element-not-exists | The selector matches nothing. |
variable-equals | A variable equals a value exactly. |
variable-contains | A variable contains a substring. |
url-matches | The page URL matches a pattern. |
url-contains | The page URL contains a substring. |
The variable conditions read the same variable map the rest of the run uses, so
anything captured by extract or api-request earlier is available — that is the
usual way to branch on something the app told you. See
Variables.
Worked example: an optional login
1 goto /app
2 if element-exists [data-testid=login-form]
3 fill [data-testid=email] {{email}}
4 fill [data-testid=password] {{password}}
5 click [data-testid=submit]
6 wait selector [data-testid=dashboard]
7 end-if
8 assert visible [data-testid=dashboard]This passes whether or not the session was already authenticated, which is what makes it safe to run both against a fresh browser and after a captured session.
Worked example: paging through a list
1 goto /orders
2 loop element-exists [data-testid=next-page]:not([disabled])
3 click [data-testid=next-page]
4 wait selector [data-testid=order-row]
5 end-loop
6 assert visible [data-testid=orders-empty-next]A note on AI-generated tests
Vera's AI generator does not emit if or loop. That is deliberate: a
branch an AI guessed at is a branch nobody verified, and a wrong loop condition
fails in a way that looks like a flaky test. Where branching is genuinely needed,
it is assembled deterministically from what was actually observed on the page, or
you add it yourself in the builder.
See also
- Step actions reference — every action, including the markers
- Variables — what the variable conditions read
- The test builder — the indentation that shows the nesting