Control Flow

Add conditional logic and loops to your tests with If/Else branching, Loop iteration, and 6 built-in condition types. Create dynamic test flows that adapt to page state at runtime.

Overview

Control flow actions let you build tests that make decisions and repeat actions based on runtime conditions. Instead of a strictly linear sequence of steps, you can create branching paths and loops that respond to the actual state of the page under test.

QA Studio provides five control flow actions:

  • If — Evaluate a condition and execute the following steps only if the condition is true
  • Else — Define an alternate branch that executes when the If condition is false
  • End If — Close the conditional block
  • Loop — Repeat a block of steps while a condition remains true (up to a maximum iteration count)
  • End Loop — Close the loop block

The test runner uses an index-based state machine to evaluate control flow. Rather than pre-processing the step list, the runner walks through steps sequentially and uses the index positions of If, Else, End If, Loop, and End Loop markers to determine which steps to execute, skip, or jump back to.

If / Else / End If

Conditional blocks let you execute different sets of steps depending on whether a condition is true or false at runtime.

Structure

A conditional block always follows this structure:

Structure
If (condition)
  ... steps executed when condition is TRUE ...
Else                    (optional)
  ... steps executed when condition is FALSE ...
End If                  (required)
  • The If step specifies the condition to evaluate
  • Steps between If and Else (or End If if there is no Else) execute when the condition is true
  • The Else branch is optional. If present, its steps execute when the condition is false
  • The End If step is always required to close the block

How It Works

When the runner reaches an If step, it evaluates the condition. The evaluation works as follows:

  1. The runner evaluates the If step's condition (e.g., checking if an element exists on the page)
  2. If the condition is true, the runner proceeds to execute the next step normally
  3. If the condition is false, the runner scans forward to find the matching Else or End If step by index, and jumps to that position
  4. When the runner encounters an Else while executing the true branch, it jumps forward to the matching End If
  5. When the runner reaches End If, execution continues with the next step after it

Steps in the skipped branch receive a skipped status in the run results.

Example: Conditional Login

This example checks if a "Log Out" button is visible (meaning the user is already logged in) and skips the login steps if so:

Conditional Login Example
[
  { "action": "goto", "url": "{{BASE_URL}}" },

  { "action": "if",
    "conditionType": "element-exists",
    "selector": "Log Out",
    "selectorMode": "text" },

    { "action": "goto", "url": "{{BASE_URL}}/dashboard" },

  { "action": "else" },

    { "action": "click", "selector": "Log In", "selectorMode": "text" },
    { "action": "fill", "selector": "#email", "value": "{{EMAIL}}" },
    { "action": "fill", "selector": "#password", "value": "{{PASSWORD}}" },
    { "action": "click", "selector": "Submit", "selectorMode": "text" },

  { "action": "end-if" },

  { "action": "assert", "assertType": "url", "condition": "contains", "value": "/dashboard" }
]

In this example:

  • If the "Log Out" text is visible, the user is already authenticated, so we just navigate to the dashboard
  • If the "Log Out" text is not visible, the else branch runs the full login flow
  • After the conditional block, the assert step runs regardless of which branch executed

Loop / End Loop

Loop blocks repeat a set of steps as long as a condition remains true, up to a configurable maximum number of iterations.

Structure

Structure
Loop (condition, maxIterations)
  ... steps repeated while condition is TRUE ...
End Loop
  • The Loop step specifies the condition and a maximum iteration count
  • Steps between Loop and End Loop execute repeatedly as long as the condition evaluates to true
  • When the condition becomes false, or the max iterations are reached, execution jumps past End Loop
  • The End Loop step is always required to close the block

Max Iterations

Field Type Default Description
maxIterations number 10 The maximum number of times the loop body will execute, regardless of the condition. This is a safety limit to prevent infinite loops.
Warning: Infinite Loop Prevention

The maxIterations field exists to prevent infinite loops that could hang your test runner. The default value is 10. If your loop condition never becomes false, the loop will stop after reaching this limit. Always set maxIterations to a reasonable value for your use case. If the loop exits due to hitting the max iterations limit, execution continues normally with the step after End Loop.

How It Works

  1. The runner reaches the Loop step and evaluates the condition
  2. If the condition is true and iterations are below the max, the runner executes the loop body
  3. When the runner reaches End Loop, it jumps back to the Loop step's index to re-evaluate the condition
  4. If the condition is now false (or max iterations reached), the runner jumps past End Loop and continues

Example: Pagination Loop

This example clicks through paginated results until the "Next" button is no longer present:

Pagination Loop Example
[
  { "action": "goto", "url": "{{BASE_URL}}/results" },

  { "action": "loop",
    "conditionType": "element-exists",
    "selector": "next-page-btn",
    "selectorMode": "testid",
    "maxIterations": 20 },

    { "action": "screenshot", "name": "results-page", "fullPage": false },
    { "action": "click", "selector": "next-page-btn", "selectorMode": "testid" },
    { "action": "wait", "waitType": "time", "value": "1000" },

  { "action": "end-loop" },

  { "action": "screenshot", "name": "last-page", "fullPage": true }
]

In this example:

  • The loop checks if a "Next" button exists before each iteration
  • Each iteration takes a screenshot, clicks "Next", and waits 1 second for the page to load
  • When the "Next" button disappears (last page reached), or after 20 iterations, the loop exits
  • A final screenshot is taken of the last page

Condition Types

Both If and Loop steps use the same set of 6 condition types. Each condition type has its own required fields:

Condition Type Description Required Fields
element-exists True if an element matching the selector is present and visible on the page. selector, selectorMode
element-not-exists True if no element matching the selector is visible on the page. Useful for waiting until a loading spinner disappears. selector, selectorMode
variable-equals True if the specified environment variable's value exactly matches the given value. Comparison is case-sensitive. variable (variable name), value
variable-contains True if the specified environment variable's value contains the given value as a substring. variable (variable name), value
url-matches True if the current page URL exactly matches the specified value. value (full URL)
url-contains True if the current page URL contains the specified value as a substring. value (URL fragment)

Element-Based Conditions

The element-exists and element-not-exists conditions use the same selector system as regular action steps. You specify a selector and selectorMode (css, text, role, placeholder, label, testid) to identify the element.

Element Exists Condition
{
  "action": "if",
  "conditionType": "element-exists",
  "selector": ".error-message",
  "selectorMode": "css"
}
Element Not Exists Condition
{
  "action": "loop",
  "conditionType": "element-not-exists",
  "selector": "loading-spinner",
  "selectorMode": "testid",
  "maxIterations": 30
}

Variable-Based Conditions

The variable-equals and variable-contains conditions check the value of an environment variable from the project. This enables conditional branching based on configuration values.

Variable Equals Condition
{
  "action": "if",
  "conditionType": "variable-equals",
  "variable": "ENV",
  "value": "production"
}
Variable Contains Condition
{
  "action": "if",
  "conditionType": "variable-contains",
  "variable": "FEATURES",
  "value": "dark-mode"
}

URL-Based Conditions

The url-matches and url-contains conditions check the current browser URL at the time the condition is evaluated.

URL Contains Condition
{
  "action": "if",
  "conditionType": "url-contains",
  "value": "/checkout"
}
URL Matches Condition
{
  "action": "loop",
  "conditionType": "url-contains",
  "value": "/wizard/step-",
  "maxIterations": 10
}

Nesting Rules

QA Studio supports full nesting of control flow blocks. You can place If/Else/End If blocks inside Loop/End Loop blocks, loops inside conditionals, and any depth of nesting is supported.

Nested Example
Loop (element-exists: "next-btn")
  If (element-exists: ".promo-banner")
    Screenshot "promo-banner"
    Click ".promo-close"
  End If
  Click "next-btn"
End Loop

Visual Indentation

In the test builder's steps list (center panel), control flow nesting is visualized through indentation. Steps inside a conditional or loop block are indented one level to the right. Each additional level of nesting adds another level of indentation, making it easy to see the structure of complex tests at a glance.

For example, the nested structure above would display as:

Visual Layout in Builder
  Loop (element-exists: "next-btn")
    If (element-exists: ".promo-banner")
      Screenshot "promo-banner"
      Click ".promo-close"
    End If
    Click "next-btn"
  End Loop

Rules

  • Every If must have a matching End If
  • Every Loop must have a matching End Loop
  • Else is optional and can only appear between an If and its End If
  • Only one Else is allowed per If block (there is no "else if" construct; use nested If blocks instead)
  • Control flow blocks must not overlap — if a Loop starts inside an If, it must also end inside that same If (or Else) branch
  • There is no enforced limit on nesting depth, but deeply nested structures become harder to maintain
Tip: Combine with Environment Variables

Control flow conditions become especially powerful when combined with environment variables. Use variable-equals conditions to create tests that behave differently based on the target environment (staging vs. production), feature flags, or user roles. For example, you can define an ENV variable set to "staging" or "production" and branch your test to skip certain assertions that only apply to one environment.

Index-Based State Machine

Under the hood, the test runner implements control flow as an index-based state machine. This means the runner does not pre-process or compile the step list into a tree structure. Instead, it walks through the flat array of steps using the following algorithm:

  1. Start at step index 0
  2. Execute the current step
  3. If the step is an If with a false condition, scan forward to find the matching Else or End If by tracking nesting depth, then jump to that index
  4. If the step is an Else reached while executing the true branch, scan forward to the matching End If and jump to that index
  5. If the step is an End Loop, jump back to the matching Loop step's index to re-evaluate the condition
  6. If the step is a Loop with a false condition (or max iterations reached), scan forward to the matching End Loop and jump past it
  7. Otherwise, increment the step index by 1 and continue

This approach keeps the runner implementation simple and the step data format flat (a plain JSON array), while supporting arbitrary nesting depth.

Best Practices

  • Keep loops bounded. Always set a reasonable maxIterations value. A test that runs forever is worse than one that exits early.
  • Test both branches. When using If/Else, make sure you have test cases that exercise both the true and false paths.
  • Avoid deep nesting. While QA Studio supports arbitrary nesting depth, tests with more than 2-3 levels of nesting become difficult to read and maintain. Consider splitting complex logic into separate tests or reusable flows.
  • Use element conditions for UI state. The element-exists and element-not-exists conditions are the most common and useful for adapting to dynamic page content.
  • Use variable conditions for configuration. The variable-equals and variable-contains conditions are best for environment-level branching (staging vs. production, feature flags, etc.).