---
title: "Code Step"
description: "Write inline JavaScript in the Shiplight test editor to call APIs, manipulate browser state, store values, write custom assertions, and verify downloaded files."
---

# Code Step

<div class="view-markdown-wrapper">
<ViewMarkdown />
</div>

The Code step lets you write JavaScript directly inside a test — in the same editor, alongside your natural language steps. It runs 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 mid-test, seeding browser state, capturing values to reuse later, or writing exact assertions.

## Table of Contents

1. [Adding a Code Step](#adding-a-code-step)
2. [Available Context](#available-context)
3. [Importing Packages](#importing-packages)
4. [Failing a Step](#failing-a-step)
5. [Common Use Cases](#common-use-cases)
   - [Call APIs](#call-apis)
   - [Page Operations](#page-operations)
   - [Store Values for Later Steps](#store-values-for-later-steps)
   - [Custom Assertions](#custom-assertions)
   - [Verify a Downloaded File](#verify-a-downloaded-file)
6. [Limitations](#limitations)

## Adding a Code Step

**From the step menu:**

1. Hover over any step to reveal the **⋯** menu
2. Click it and select **Code** from the action type dropdown

**Keyboard shortcut:** `Ctrl + Alt + C` converts the current step to a Code step.

A code editor will appear inline where you can write multi-line JavaScript.

## Available Context

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

| Variable      | Type                                                                                      | Description                                                      |
| ------------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `page`        | Playwright [`Page`](https://playwright.dev/docs/api/class-page)                           | The current browser page                                         |
| `context`     | Playwright [`BrowserContext`](https://playwright.dev/docs/api/class-browsercontext)       | The browser context (cookies, permissions, etc.)                 |
| `expect`      | Playwright assertions                                                                     | For writing assertions                                           |
| `request`     | Playwright [`APIRequestContext`](https://playwright.dev/docs/api/class-apirequestcontext) | For making HTTP requests                                         |
| `testContext` | Shiplight variable store                                                                  | Read and write test variables. Aliases: `$`, `ctx`               |
| `agent`       | Shiplight `WebAgent`                                                                      | AI-powered actions — assertions, natural language step execution |
| `console`     | Captured console                                                                          | Logs appear in the test run output                               |

**Async/await is fully supported.** All code runs inside an async context.

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

```javascript
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 — for the duration of the test run.

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

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

// Write 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.

```javascript
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.

```javascript
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`.

```javascript
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.

```javascript
await agent.waitForDownloadComplete(page, 10);
const filePath = agent.getRecentDownloadedFilePath();
console.log("Downloaded:", filePath);
```

## Importing Packages

You can use `await import()` to import Node.js built-in modules and pre-installed npm packages inside a code step.

```javascript
// Node.js built-in modules
const fs = await import("node:fs");
const crypto = await import("node:crypto");
const path = await import("node:path");

// Pre-installed npm packages
const { v4: uuidv4 } = await import("uuid");
testContext.uniqueId = uuidv4();
```

### Pre-installed Packages

The following third-party packages are available out of the box:

| Package     | Description                                    |
| ----------- | ---------------------------------------------- |
| `pdf-parse` | Extract text from PDF files                    |
| `mammoth`   | Convert Word documents (.docx) to text or HTML |
| `exceljs`   | Read and write Excel files (.xlsx)             |

**Read a PDF:**

```javascript
let pdfParse = await import("pdf-parse");
pdfParse = pdfParse.default || pdfParse;
const fs = await import("fs");
const filePath = agent.getRecentDownloadedFilePath();
const parser = new pdfParse.PDFParse({ url: filePath });
const result = await parser.getText();
const text = result.text;
console.log("PDF text:", text);
expect(text).toContain("Invoice #12345");
```

**Read a Word document:**

```javascript
let mammoth = await import("mammoth");
mammoth = mammoth.default || mammoth;
const filePath = agent.getRecentDownloadedFilePath();
const result = await mammoth.extractRawText({ path: filePath });
const text = result.value;
console.log("Document text:", text);
expect(text).toContain("Contract Agreement");
```

**Read an Excel file:**

```javascript
let ExcelJS = await import("exceljs");
ExcelJS = ExcelJS.default || ExcelJS;
const workbook = new ExcelJS.Workbook();
const filePath = agent.getRecentDownloadedFilePath();
await workbook.xlsx.readFile(filePath);

const sheet = workbook.getWorksheet(1);
const headerRow = sheet.getRow(1).values;
console.log("Headers:", headerRow);
expect(sheet.rowCount).toBeGreaterThan(1);
```

::: warning Dynamic import only
Static `import` statements (e.g., `import fs from 'fs'`) are **not supported** — only `await import()` works. This is because the code step runs as an inline script, not as an ES module.
:::

::: tip CommonJS packages
Some older packages only expose a `default` export when imported this way. If destructuring doesn't work, access the module via `.default`:

```javascript
const mod = await import("some-package");
const lib = mod.default;
```

:::

## Failing a Step

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

```javascript
// Fail via expect:
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.

```javascript
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;
```

`request` shares the browser session's authentication cookies, so it works for authenticated API calls without 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:**

```javascript
await page.route("**/api/products", (route) =>
  route.fulfill({
    status: 200,
    contentType: "application/json",
    body: JSON.stringify([{ id: 1, name: "Widget", price: 9.99 }]),
  }),
);
```

**Seed browser storage before a page load:**

```javascript
await page.evaluate(() => {
  localStorage.setItem("onboarding_complete", "true");
});
```

**Inject a session cookie:**

```javascript
await context.addCookies([
  {
    name: "session",
    value: "abc123",
    domain: "app.example.com",
    path: "/",
  },
]);
```

**Wait for a condition:**

```javascript
await page.waitForLoadState("networkidle");

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.

```javascript
const orderNumber = await page.locator('[data-testid="order-number"]').textContent();
testContext.orderNumber = orderNumber?.trim();
```

The value is then available in all subsequent steps. In a natural language step you can reference it as `{{orderNumber}}`, or read it in another code step via `testContext.orderNumber`.

### Custom Assertions

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

```javascript
// Exact row count
const rows = await page.locator('table tbody tr').count();
expect(rows).toBe(5);

// Numeric range
const total = await page.locator('[data-testid="cart-total"]').textContent();
const amount = parseFloat(total!.replace('$', ''));
expect(amount).toBeGreaterThan(0);
expect(amount).toBeLessThan(1000);

// Custom failure message
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 inspect the result.

```javascript
// 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 to:", filePath);
```

::: tip Verifying file contents
You can read file contents by importing `fs` directly in the code step:

```javascript
const fs = await import("node:fs");
const content = fs.readFileSync(filePath, "utf-8");
expect(content).toContain("expected data");
```

Alternatively, use a [Function](/cloud/test-editor/functions) for reusable file verification logic.
:::

## Limitations

- **Dynamic import only.** Static `import` statements are not supported. Use `await import()` instead. See [Importing Packages](#importing-packages).
- **Pre-installed packages only.** You can only import packages that are already available in the Shiplight runtime. You cannot install arbitrary npm packages.
- **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.
