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
Adding a Code Step
From the step menu:
- Hover over any step to reveal the ⋯ menu
- 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 | The current browser page |
context | Playwright BrowserContext | The browser context (cookies, permissions, etc.) |
expect | Playwright assertions | For writing assertions |
request | Playwright 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():
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.
// 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.
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.
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.
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.
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.
// 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:
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:
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:
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:
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():
// 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.
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:
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:
await page.evaluate(() => {
localStorage.setItem("onboarding_complete", "true");
});Inject a session cookie:
await context.addCookies([
{
name: "session",
value: "abc123",
domain: "app.example.com",
path: "/",
},
]);Wait for a condition:
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.
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.
// 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.
// 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:
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
importstatements are not supported. Useawait 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 likewindow,document, orlocalStorage. - No return value. The return value of the code block is discarded. Use
testContextto pass values between steps.