Skip to content

Code Step

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
  2. Available Context
  3. Importing Packages
  4. Failing a Step
  5. Common Use Cases
  6. 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:

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 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:

PackageDescription
pdf-parseExtract text from PDF files
mammothConvert Word documents (.docx) to text or HTML
exceljsRead 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);

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.

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 , 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);

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 for reusable file verification logic.

Limitations

  • Dynamic import only. Static import statements are not supported. Use await import() instead. See 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.

Released under the MIT License.