Skip to content

Statement Types

TypeSyntaxDescription
ACTION (action:)- intent: Enter email + action: input_textFast replay with AI self-healing fallback
ACTION (js:)- intent: Click login + js: "await ..."Fast replay (Playwright code) with AI self-healing
VERIFY- VERIFY: page shows welcome messageAI assertion, optional js: cache
DRAFT- intent: Click the login buttonAI resolves at runtime (~5-10s)
URL- URL: /pathNavigation shorthand
Code- description: ... + js: await request.get(...)Inline Playwright code (no self-healing)
STEP- STEP: Login + statements: [...]Group related actions
IF/ELSE- IF: cookie banner is visible + THEN: [...]Conditional execution
WHILE- WHILE: more items to load + DO: [...]Repeat until condition
WAIT_UNTIL- WAIT_UNTIL: dashboard loaded + optional js:Poll until condition met (AI, or a js: predicate — no model calls)
WAIT- WAIT: for animation + seconds: 3Fixed-duration pause
Function- call: "file#export" + args: [...]Call custom TypeScript function
Template- template: ./path.yamlInline reusable statement flow

The Code type is covered in detail on the Code Steps page, Function on the Custom Functions page, and Template on the Templates page.

VERIFY

Asserts a condition using AI. Use the VERIFY: shorthand (unquoted key):

yaml
statements:
  - VERIFY: The success message is displayed
  - VERIFY: The order total is $49.99
    js: "await expect(page.getByTestId('order-total')).toHaveText('$49.99')"

The js: cache speeds up simple checks. If the js: assertion fails, it automatically falls back to AI verification using the natural language statement.

ACTION

Fast deterministic replay (<1s) with AI self-healing fallback. Use the structured action: form for all supported actions:

yaml
statements:
  - intent: Type email address
    action: input_text
    text: "{{USER_EMAIL}}"
    locator: "getByLabel('Email')"

intent describes what the step should accomplish in natural language; the action:/locator: field is a cache for fast replay. When the cache fails (e.g., a locator becomes stale), Shiplight's agentic layer falls back to the intent to self-heal: see How self-healing works.

For complex interactions that don't map to a supported action (e.g., drag-and-drop), use the description: + js: code step, but note raw JS does not self-heal:

yaml
statements:
  - description: Drag the card to the Done column
    js: |
      const card = page.getByText('My Task');
      const target = page.getByTestId('column-done');
      await card.dragTo(target);

STEP (grouping)

Groups related statements under a label.

yaml
statements:
  - STEP: Fill in the registration form
    statements:
      - intent: Type "John" in the first name field
      - intent: Type "Doe" in the last name field
      - intent: Type "john@example.com" in the email field

Frames

For elements inside iframes, use frame_path with action: form:

yaml
- intent: Click Hello inside iframe
  action: click
  frame_path:
    - "iframe#main"
  locator: "getByText('Hello')"

Conditional Logic

Handle optional UI elements with IF/ELSE:

yaml
statements:
  - IF: cookie consent dialog is visible
    THEN:
      - intent: Click "Accept All"
  - IF: user is logged in
    THEN:
      - intent: Click the logout button
    ELSE:
      - intent: Click the login button
      - intent: Enter credentials and submit

Conditions are evaluated by the AI at runtime using the current page state. JavaScript conditions are also supported with the js: prefix:

yaml
- IF: "js: testContext.retryCount < 3"
  THEN:
    - intent: Click the retry button

WARNING

js: conditions have no AI fallback: if the JavaScript throws, the test fails. They run in the Playwright test context (Node.js), so browser globals like document/window are not defined; a condition that references them throws. Use js: only for simple, reliable checks like URL matching or counters (testContext.*). For UI state checks, prefer natural language conditions — they self-heal when the DOM changes.

Loops

Repeat actions until a condition is met with WHILE:

yaml
statements:
  - WHILE: "Load More" button is visible
    DO:
      - intent: Click the "Load More" button
      - intent: Wait for new items to appear
    timeout_ms: 30000
  - VERIFY: all items are loaded

JavaScript conditions work in loops too (same js: caveat applies):

yaml
- WHILE: "js: testContext.itemCount < 10"
  DO:
    - intent: Click "Load More"
    - description: Increment the item counter
      js: "testContext.itemCount = (testContext.itemCount || 0) + 1"

Waiting

Use WAIT_UNTIL to pause until a condition becomes true, then continue. The condition can be natural language (checked by the AI), or — preferred for simple DOM/state checks — a natural-language intent plus a js: expression polled in-process with no model calls:

yaml
statements:
  # AI wait — semantic and self-healing, but each check is a model call
  - WAIT_UNTIL: Dashboard data has finished loading
    timeout_seconds: 15

  # Intent + js: wait — polled ~4x/second in-process, no model calls.
  # The intent labels the step in reports; the js: expression is what runs.
  - WAIT_UNTIL: The loading spinner has disappeared
    js: "(await page.locator('.spinner').count()) === 0"
    timeout_seconds: 10

When js: is present it is polled exclusively — there is no AI fallback (unlike VERIFY, where js: is a cache and the AI re-checks the assertion if it throws). The intent is what reports show, and what an agent uses to regenerate a stale expression. Prefer this form for simple checks (an element appearing or disappearing, a count reaching a threshold): it is far cheaper and faster than an AI wait, which costs a model call on every poll. Keep natural language alone for semantic conditions or when the selector may drift.

The expression runs in the Playwright test context (Node.js): page, expect, and agent are in scope and await is allowed, but browser globals like document/window are not — wrap browser-side checks in page.evaluate(() => ...). The predicate must return truthy/falsy — a Playwright waitFor() resolves to undefined and never registers as met, so use a boolean check like the count() example above.

A wait never fails the test

WAIT_UNTIL synchronizes; it is not an assertion. When the timeout expires, the step records a warning and the test continues. If correctness depends on the condition, follow the wait with a VERIFY:.

For a fixed pause, use WAIT, but prefer waiting on a condition whenever you can:

yaml
- WAIT: Wait for the open animation
  seconds: 2

Released under the MIT License.