Skip to content

Code Steps

A code step — a js: block with a description: label — lets you run arbitrary JavaScript directly inside a YAML test. It executes in the same Node.js context as Playwright, giving you full access to page, browser APIs, HTTP requests, and the test variable store.

Use it when a natural language step isn't precise enough — calling an API, intercepting network requests, seeding browser state, capturing values mid-test, or writing exact assertions.

Table of Contents

  1. Syntax
  2. Available Context
  3. Failing a Step
  4. Common Use Cases
  5. Code step vs VERIFY js: vs structured actions
  6. Limitations

Syntax

A code step is a js: field with a description: label that says what the code does. The description: is optional but recommended — it's the human-readable label shown in reports.

Single-line:

yaml
statements:
  - description: Wait for the network to be idle
    js: "await page.waitForLoadState('networkidle')"

Multi-line (block scalar):

yaml
statements:
  - description: Mock the users API
    js: |
      await page.route('**/api/users', (route) => route.fulfill({
        status: 200,
        body: JSON.stringify([{ name: 'Alice' }]),
      }));
      testContext.apiMocked = true;

Quoting rules

Use double quotes for single-line code that doesn't contain double quotes. Use the block scalar (|) for multi-line code or any code containing special YAML characters ({, }, :, #).

Available Context

The code runs in Node.js (not the browser). The following variables are available without any imports:

VariableTypeDescription
pagePlaywright PageThe current browser page
contextPlaywright BrowserContextThe browser context (cookies, permissions, etc.)
expectPlaywright assertionsFor writing assertions
requestPlaywright APIRequestContextFor making HTTP requests
testContextShiplight variable storeRead and write test variables. Aliases: $, ctx
agentShiplight WebAgentAI-powered actions — assertions, natural language step execution
consoleCaptured consoleLogs appear in test output

Async/await is fully supported. All code runs inside an async context, so you can freely use await.

To run code inside the browser (accessing window, document, localStorage, etc.), use page.evaluate():

yaml
- description: Set a feature flag in localStorage
  js: |
    await page.evaluate(() => {
      localStorage.setItem('featureFlag', 'true');
    });

Working with testContext

testContext (also $ and ctx) is a proxy to the test's variable store. Values set here are available in all subsequent steps, including natural language steps.

javascript
// Set a variable
testContext.orderId = "12345";
$.theme = "dark";

// Read a variable
const id = testContext.orderId;

// Set a sensitive value (masked in logs)
testContext.set("apiToken", "secret-value", true);

// Read explicitly
const token = testContext.get("apiToken");

Using agent

The agent object gives you access to Shiplight's AI capabilities from inside a code step.

agent.assert(page, statement) — AI-powered assertion. The agent looks at the current page and verifies whether the statement is true.

yaml
- description: Assert the cart shows 3 items
  js: |
    await agent.assert(page, 'The shopping cart shows 3 items');

agent.execute(page, statement) — Execute a natural language instruction. The agent reads the page and performs the described action, just like a natural language step.

yaml
- description: Fill and submit the registration form
  js: |
    await agent.execute(page, 'Fill out the registration form with test data');
    await agent.execute(page, 'Click the Submit button');

agent.extract(page, description, variableName) — Extract a value from the page using AI and store it in testContext.

yaml
- description: Extract the order confirmation number
  js: |
    await agent.extract(page, 'the order confirmation number', 'orderNumber');
    console.log('Order:', testContext.orderNumber);

agent.getRecentDownloadedFilePath() — Get the path of the most recently downloaded file.

yaml
- description: Capture the downloaded file path
  js: |
    await agent.waitForDownloadComplete(page, 10);
    const filePath = agent.getRecentDownloadedFilePath();
    console.log('Downloaded:', filePath);

Failing a Step

The return value of the code is ignored. To fail a step, either throw an error or use expect():

javascript
// Fail via expect (throws if assertion doesn't hold):
const title = await page.title();
expect(title).toContain("Dashboard");

// Fail via throw:
const count = await page.locator(".item").count();
if (count === 0) {
  throw new Error("Expected at least one item, found none");
}

Common Use Cases

Call APIs

Use request to call HTTP endpoints directly from a test step — useful for seeding test data, triggering backend actions, or checking state that isn't visible in the UI.

yaml
statements:
  - description: Create an order via the API
    js: |
      const response = await request.post('https://api.example.com/orders', {
        headers: { Authorization: `Bearer ${testContext.apiToken}` },
        data: { productId: 'prod_123', quantity: 2 },
      });
      expect(response.ok()).toBeTruthy();
      const order = await response.json();
      testContext.orderId = order.id;
  - URL: "https://app.example.com/orders/{{orderId}}"
  - VERIFY: Order details page is displayed

request uses the same authentication cookies as the browser session, so it works for authenticated API calls without any extra setup.

Page Operations

Interact directly with the browser — mock network requests, seed storage, inject cookies, or wait for specific states.

Mock an API response:

yaml
statements:
  - description: Mock the products API
    js: |
      await page.route('**/api/products', (route) => route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([{ id: 1, name: 'Widget', price: 9.99 }]),
      }));
  - URL: https://app.example.com/shop
  - VERIFY: Widget is listed with price $9.99

Seed browser storage before a page load:

yaml
statements:
  - URL: https://app.example.com
  - description: Mark onboarding complete in localStorage
    js: |
      await page.evaluate(() => {
        localStorage.setItem('onboarding_complete', 'true');
      });
  - URL: https://app.example.com/dashboard
  - VERIFY: Onboarding banner is not shown

Inject a session cookie:

yaml
- description: Inject a session cookie
  js: |
    await context.addCookies([{
      name: 'session',
      value: 'abc123',
      domain: 'app.example.com',
      path: '/',
    }]);

Wait for a condition:

yaml
- description: Wait for the network to be idle
  js: "await page.waitForLoadState('networkidle')"

- description: Wait until at least 10 items are loaded
  js: |
    await page.waitForFunction(() => {
      return document.querySelectorAll('.item').length >= 10;
    }, { timeout: 10000 });

Store Values for Later Steps

Read something from the page and save it to testContext so later steps can reference it.

yaml
statements:
  - intent: Submit the order form
  - description: Capture the order number
    js: |
      const orderNumber = await page.locator('[data-testid="order-number"]').textContent();
      testContext.orderNumber = orderNumber?.trim();
  - URL: https://app.example.com/order-history
  - VERIFY: "Order {{orderNumber}} appears in the history"

Values stored in testContext are available in all subsequent steps — both code steps and natural language steps — for the duration of the test run.

Custom Assertions

Write exact, programmatic checks for cases where VERIFY: isn't precise enough — counts, numeric ranges, string patterns, or multi-element checks.

yaml
- description: Assert the table has 5 rows
  js: |
    const rows = await page.locator('table tbody tr').count();
    expect(rows).toBe(5);

- description: Assert the cart total is within range
  js: |
    const total = await page.locator('[data-testid="cart-total"]').textContent();
    const amount = parseFloat(total!.replace('$', ''));
    expect(amount).toBeGreaterThan(0);
    expect(amount).toBeLessThan(1000);

Use throw when expect() doesn't fit:

yaml
- description: Assert the order status is Shipped or Delivered
  js: |
    const status = await page.locator('.order-status').textContent();
    if (!['Shipped', 'Delivered'].includes(status?.trim() ?? '')) {
      throw new Error(`Unexpected order status: ${status}`);
    }

Verify a Downloaded File

Shiplight automatically saves files to disk when a download is triggered. Use agent methods to wait for the download to complete and verify the result.

yaml
statements:
  - intent: Click the Export CSV button
  - description: Verify a CSV file was downloaded
    js: |
      // Wait up to 10 seconds for the download to finish
      await agent.waitForDownloadComplete(page, 10);

      // Get the path of the most recently downloaded file
      const filePath = agent.getRecentDownloadedFilePath();
      expect(filePath).toBeTruthy();
      expect(filePath).toContain('.csv');
      console.log('Downloaded file:', filePath);

Verifying file contents

The code step sandbox does not have access to the filesystem, so you cannot read file contents directly. To verify what's inside a downloaded file, use a custom function — custom functions run with full Node.js access, including fs.

Code step vs VERIFY js: vs structured actions

All three let you write Playwright code, but they serve different purposes:

description: + js: (code step)VERIFY: + js:intent: + action:/locator:
Self-healingNoYes — falls back to AI visionYes — agent re-derives from intent:
PurposeSetup, utilities, mocking, custom assertionsAssertions with an AI fallbackUI interactions with a fast-replay cache
Failure behaviorThrows → test fails immediatelyThrows → agent evaluates VERIFY: via AILocator fails → agent re-derives from intent:
Best forNetwork setup, storage seeding, computed checksChecking visible outcomesClicks, typing, keyboard, hover

Use a code step (description: + js:) when the operation is deterministic and doesn't need recovery — setting up mocks, seeding data, computing derived values, or asserting precise conditions.

Use VERIFY: + js: when you have a preferred assertion but want the AI to verify the outcome in natural language if the code path fails.

Use a structured action: when the step interacts with the UI and you want AI-powered self-healing if the page changes.

Limitations

  • No dynamic imports. You cannot use import() inside a code step. For reusable logic, use custom functions instead.
  • Node.js context only. Code runs on the Node.js side, not in the browser. Use page.evaluate() to access browser globals like window, document, or localStorage.
  • No return value. The return value of the code block is discarded. Use testContext to pass values between steps.

Released under the MIT License.