Skip to content

Writing Test Flows

Shiplight tests are written in YAML using natural language. AI agents author and enrich the test flows, while the natural language format keeps tests readable for human review and modification.

No lock-in: Test flows can be run locally with Playwright using shiplightai. The YAML format is an authoring layer — what actually runs is standard Playwright with an AI agent on top. You can eject at any time.

Basic Structure

yaml
goal: Description of what this test verifies
url: https://your-app.com/starting-page

statements:
  - Step described in natural language
  - Another step
  - "VERIFY: Expected outcome"

teardown:
  - Clean up step
FieldRequiredDescription
goalYesTest description (used as the Playwright test name)
urlYesStarting URL to navigate to
statementsYesList of test steps
teardownNoSteps that always run after the test (like finally)

Basic Test

Every line is a natural language instruction. The web agent resolves each one at runtime by looking at the page and performing the right action.

yaml
goal: Verify user can create a new project
url: https://app.example.com/projects
statements:
  - Click the "New Project" button
  - Enter "My Test Project" in the project name field
  - Select "Public" from the visibility dropdown
  - Click "Create"
  - "VERIFY: Project page shows title 'My Test Project'"
teardown:
  - Delete the created project

Enriched Test

After exploring the UI with the browser automation tools (get_dom, act, etc.), the coding agent enriches natural language steps with locators for deterministic, fast replay:

yaml
goal: Verify user can create a new project
url: https://app.example.com/projects
statements:
  - STEP: Create project
    statements:
      - description: Click the New Project button
        action_entity:
          action_data:
            action_name: click
          locator: "getByRole('button', { name: 'New Project' })"
      - description: Enter project name
        action_entity:
          action_data:
            action_name: input_text
            kwargs:
              text: "My Test Project"
          locator: "getByRole('textbox', { name: 'Project name' })"
      - description: Click Create
        action_entity:
          action_data:
            action_name: click
          locator: "getByRole('button', { name: 'Create' })"
  - "VERIFY: Project page shows title 'My Test Project'"
teardown:
  - Delete the created project

Natural language statements (~10-15s each): The web agent reads the page and figures out what to do. Action statements with locator (~1s each): Replay deterministically without AI. VERIFY statements: Always use the web agent — just provide a clear assertion in natural language.

Locators Are a Cache

Locators are a performance cache, not a hard dependency. When the UI changes and a locator becomes stale, Shiplight's agentic layer auto-heals by falling back to the natural language description to find the right element.

However, when a locator is permanently changed (e.g., a button was renamed or moved), the cached locator will fail on every run. When running on the Shiplight cloud, the platform self-updates the cached locator after a successful self-heal — so future runs replay at full speed again without manual intervention. This self-adjusting behavior is a key benefit of running tests on the Shiplight platform.

Statement Types

TypeSyntaxDescription
Natural language- Click the login buttonWeb agent resolves at runtime
Action with locator- description: ... + action_entity: ...Deterministic replay
Verify- "VERIFY: page shows welcome message"AI-powered assertion
Step group- STEP: Login + statements: [...]Group related actions
Conditional- IF: cookie banner is visible + THEN: [...]Conditional execution
Loop- WHILE: more items to load + DO: [...]Repeat until condition

VERIFY

Asserts a condition using AI. Must be a quoted string prefixed with VERIFY:.

yaml
statements:
  - "VERIFY: The success message is displayed"
  - "VERIFY: User is redirected to the dashboard"

ACTION (with action entity)

Deterministic actions with explicit locators. These replay fast (~1s) without AI.

yaml
statements:
  - description: Click the Submit button
    action_entity:
      action_description: Click the Submit button
      locator: "getByRole('button', { name: 'Submit' })"
      action_data:
        action_name: click
        kwargs: {}

  - description: Type email address
    action_entity:
      action_description: Type email address
      locator: "getByLabel('Email')"
      action_data:
        action_name: input_text
        kwargs:
          text: "user@example.com"

  - description: Press Enter
    action_entity:
      action_data:
        action_name: press
        kwargs:
          keys: Enter

STEP (grouping)

Groups related statements under a label.

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

Locators

Action entities can specify element locators in two ways:

yaml
# Playwright locator (preferred)
locator: "getByRole('button', { name: 'Submit' })"

# XPath
xpath: "//button[@id='submit']"

If both are present, locator takes priority. If neither is present, the AI agent resolves the element from the action description.

Frames

For elements inside iframes:

yaml
action_entity:
  frame_path:
    - "iframe#main"
  locator: "getByText('Hello')"
  action_data:
    action_name: click
    kwargs: {}

Conditional Logic

Handle optional UI elements with IF/ELSE:

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

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

yaml
- IF: "js: page.url().includes('/login')"
  THEN:
    - Enter credentials and log in

Loops

Repeat actions until a condition is met with WHILE:

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

JavaScript conditions work in loops too:

yaml
- WHILE: "js: document.querySelectorAll('.item').length < 10"
  DO:
    - Click "Load More"

Supported Actions

Actions used in action_entity.action_data.action_name:

ActionkwargsDescription
clickClick an element
double_clickDouble-click an element
right_clickRight-click an element
hoverHover over an element
input_texttextType text into an input
clear_inputClear an input field
presskeysPress a keyboard key (e.g., Enter, Tab)
send_keys_on_elementkeysPress a key on a specific element
select_dropdown_optiontextSelect a dropdown option by text
scrolldown, num_pagesScroll the page
scroll_to_texttextScroll to text on the page
go_to_urlurl, new_tabNavigate to a URL
go_backBrowser back
reload_pageReload the page
waitsecondsWait for a duration
wait_for_page_readyWait for page load
verifystatement or codeAssert a condition (AI or JS)
js_codecodeRun inline JavaScript
functionfunctionName, parameterNames, parameterValuesCall a function
switch_tabtab_indexSwitch browser tab
close_tabClose current tab
upload_filefile_pathUpload a file
save_variablename, valueSave a variable for later use

Extensions

Custom Test Name

Override the Playwright test name (defaults to goal):

yaml
name: Login with valid credentials
goal: Verify login flow works
url: https://example.com
statements:
  - ...

Tags

Add Playwright tags for filtering with --grep:

yaml
tags:
  - smoke
  - auth
goal: Login test
url: https://example.com
statements:
  - ...

Run: npx playwright test --grep @smoke

Playwright Fixtures

Pass options to test.use():

yaml
use:
  viewport:
    width: 375
    height: 812
  locale: fr-FR
goal: Mobile French layout
url: https://example.com
statements:
  - ...

Variables

Use {{VAR_NAME}} to reference environment variables. At transpile time, these become process.env.VAR_NAME references in the generated code.

yaml
statements:
  - description: Type username
    action_entity:
      locator: "getByLabel('Username')"
      action_data:
        action_name: input_text
        kwargs:
          text: "{{TEST_USER}}"

Set variables when running:

bash
TEST_USER=admin TEST_PASS=secret npx playwright test

Templates

Extract reusable flows into template files and include them with template:.

Template file (templates/login.yaml):

yaml
params:
  - username
  - password

statements:
  - description: Enter username
    action_entity:
      locator: "getByLabel('Username')"
      action_data:
        action_name: input_text
        kwargs:
          text: "{{username}}"
  - description: Enter password
    action_entity:
      locator: "getByLabel('Password')"
      action_data:
        action_name: input_text
        kwargs:
          text: "{{password}}"
  - description: Click login
    action_entity:
      locator: "getByRole('button', { name: 'Log in' })"
      action_data:
        action_name: click
        kwargs: {}

Using the template:

yaml
goal: Purchase flow
url: https://example.com

statements:
  - template: ../templates/login.yaml
    params:
      username: "{{TEST_USER}}"
      password: "{{TEST_PASS}}"
  - Navigate to the checkout page
  - "VERIFY: Order summary is displayed"

Template params ({{username}}) are substituted at transpile time. Environment variables ({{TEST_USER}}) pass through to the generated code for runtime resolution.

Templates can be nested (max depth: 5) and circular references are detected.

Custom Functions

Call TypeScript functions from YAML using the function action with file#export syntax:

yaml
statements:
  - description: Seed test data
    action_entity:
      action_data:
        action_name: function
        kwargs:
          functionName: "../helpers/seed.ts#createTestUser"
          parameterNames:
            - page
            - email
          parameterValues:
            - page
            - "test@example.com"

This generates:

ts
import { createTestUser } from "../helpers/seed";
// ...
await createTestUser(page, "test@example.com");

The Enrichment Workflow

  1. Draft — The agent writes tests in natural language
  2. Explore — The agent uses get_dom and act to walk through the UI
  3. Collect — The agent uses get_locator to capture element locators
  4. Enrich — The agent replaces natural language steps with action_entity + locator
  5. Result — Tests run 10x faster with deterministic replay

Natural language and enriched statements can be mixed in the same test. The agent starts with all natural language, then selectively enriches the most-used flows.

Released under the MIT License.