Skip to main content

How to add and use a JavaScript Step

Written by Ines

What is a JavaScript Step?

A JavaScript Step lets you run your own JavaScript at a specific point in a test. Reach for it when a plain-language step or a Web Step can't express what you need: reading a computed value, combining two DOM reads, calling an API to check backend state, or building dynamic test data.

You write the body of a Step() function. It gives you a ready-to-use context (your variables, the page, an API client, the console, the network, and the earlier steps), and you return a result: the step passes or fails.

When to use it

  • Capture a value: read a value from the page (or compute one) and store it in a variable for later steps.

  • Validate backend state: call an internal or external API mid-test and compare it to what the UI shows.

  • Assert on network traffic: check that the page fired the right request with the right payload.

  • Build dynamic data: generate dates, references, or composite values so tests don't rely on hardcoded data.

  • Unblock the page: dismiss a cookie banner or set a localStorage value that a later step depends on.

When to use a dedicated step instead

For everyday actions, the purpose-built steps are simpler. Reach for a JavaScript Step when you need real logic.

If you want to…

Prefer

Click, type, select, hover, wait for an element

A plain-language step or a web step

Call an API and assert on its response as a first-class step

An API Call step

Branch or stop the run based on a simple condition

A Conditional step (IF)

Run custom logic that none of the above can express

A JavaScript step

How to add a JavaScript Step

  1. Open your Test Case.

  2. Click Add New Step.

  3. In Step type, select JavaScript Step.

  4. Add a Description: a short, human-friendly summary of what the step does. This label is required and is what identifies the step in the run view.

  5. Write your code in the editor.

The Step() function

The editor opens with a scaffold. You write the body of the function; the signature and the closing brace stay in place.

function Step(response, variables, DOM, api_client, console, network, test_steps) {
// Write your logic here.
return response.Success();
}

Every step returns one result: response.Success() when it passes, or response.Fail('reason') when it doesn't.

The context objects

Your Step() function receives seven objects. They're ready to use, with nothing to import or set up.

Object

What it gives you

response

The outcome builder. Return response.Success() or response.Fail('reason') to tell the runner what happened.

variables

Read and write Thunders variables. variables['MY_VAR'] returns the current value; assigning to it updates the variable for the rest of the test. Works with environment, project, and CSV batch variables.

DOM

The live page document. Use standard DOM APIs such as DOM.querySelector(...), .textContent, .getAttribute(...), and so on.

api_client

An HTTP client for calling APIs from inside the step. See Calling APIs.

console

console.log, console.info, console.warn, console.error. Messages appear in the step's result in the run view. This is your main debugging tool.

network

A read-only view of the requests the page made. network.requests is a list of { url, method, status, headers, body }. See Reading network requests.

test_steps

A read-only list of the steps already run. Each has name (the step's text), outcome (passed, failed, or skipped), and duration in ms, so your step can react to what happened earlier.

Response methods

Return exactly one of these.

Method

Behavior

response.Success()

The step passes and the run continues to the next step.

response.Success(value)

Passes and attaches value to the step result, handy for surfacing an API response or a computed object in the run view.

response.Fail('reason')

The step fails. The reason string surfaces in the run recap, and the run halts (consistent with other step failures).

Using variables

There are two ways to work with variables, and they complement each other:

  • Interpolation with [VARIABLE] : Thunders replaces any [MY_VAR] token in your code with the variable's value before the script runs, just like in other steps.

  • The variables object : variables['MY_VAR'] reads the current value at runtime, and assigning to it (variables['MY_VAR'] = 'new value') updates the variable for the rest of the test.

Calling APIs with api_client

api_client lets you call an API from inside the step and assert on the result, for example, to confirm the backend agrees with what the UI shows.

Method

Description

api_client.get(url, opts)

Sends a GET request.

api_client.post(url, body, opts)

Sends a POST request with a JSON body.

api_client.put(url, body, opts)

Sends a PUT request with a JSON body.

api_client.patch(url, body, opts)

Sends a PATCH request with a JSON body.

api_client.delete(url, opts)

Sends a DELETE request.

Every call is asynchronous, so await it. Pass custom headers via opts, e.g. { headers: { 'X-Env': 'staging' } }. Each call gives you back a response you can read:

{
status, // HTTP status code, e.g. 200
ok, // true when status is 2xx
data, // the parsed response body
headers // response headers
}
function Step(response, variables, DOM, api_client, console, network, test_steps) {
const res = await api_client.get(`/api/carts/${variables['CART_ID']}`);
if (!res.ok) {
return response.Fail(`Cart lookup failed with status ${res.status}`);
}
return response.Success(res.data);
}

Reading network requests

network.requests is a read-only list of the requests the page made while the step ran, each with { url, method, status, headers, body } (JSON bodies are parsed for you). Use it to confirm a request fired, check a status code, or read a response payload.

function Step(response, variables, DOM, api_client, console, network, test_steps) {
const analytics = network.requests.find(r => r.url.includes('/analytics/event'));
if (!analytics) {
return response.Fail('No analytics event was fired');
}
if (analytics.body.event_name !== 'checkout_completed') {
return response.Fail(`Wrong event name: ${analytics.body.event_name}`);
}
return response.Success();
}

Examples

1. Capture a value from the page into a variable

function Step(response, variables, DOM, api_client, console, network, test_steps) {
const orderId = DOM.querySelector('[data-order-id]').textContent;
variables['ORDER_ID'] = orderId;
return response.Success();
}

After submitting an order, the order ID appears on the confirmation page. Capture it so later steps can navigate to /orders/[ORDER_ID] or call an API with it.

2. Validate the UI against the backend

function Step(response, variables, DOM, api_client, console, network, test_steps) {
const uiTotal = parseFloat(DOM.querySelector('.cart-total').textContent.replace('$', ''));
const res = await api_client.get(`/api/carts/${variables['CART_ID']}`);
if (res.data.total !== uiTotal) {
return response.Fail(`Cart mismatch: UI ${uiTotal}, API ${res.data.total}`);
}
return response.Success();
}

Catches data-integrity bugs that pure UI testing misses. The page renders fine, but the backend disagrees.

3. Build dynamic test data

function Step(response, variables, DOM, api_client, console, network, test_steps) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
variables['BILLING_PERIOD'] = `${year}-${month}`;
variables['INVOICE_REF'] = `INV-${variables['CUSTOMER_ID']}-${year}${month}`;
return response.Success();
}

Avoid hardcoded dates that break next month, and build composite values from existing variables.

4. Unblock the page

function Step(response, variables, DOM, api_client, console, network, test_steps) {
// A cookie banner intercepts the login button on staging, so dismiss it.
const banner = DOM.querySelector('.cookie-banner');
if (banner) banner.remove();
return response.Success();
}

5. React to a prior step

function Step(response, variables, DOM, api_client, console, network, test_steps) {
const login = test_steps.find(s => s.name.includes('Log in'));
if (login && login.duration > 5000) {
variables['USE_RECOVERY_FLOW'] = 'true';
console.warn('Login was slow, switching to the recovery flow');
}
return response.Success();
}

Write a flag into a variable, then branch on it with a following Conditional Step.

How results appear in a Test Run

In the run view, select the JavaScript Step to open its JavaScript tab. It shows:

  • A Result block, the value your step returned (if any) and your console output. If the code hit an error, you'll also see the message and the line number; click it to open the editor at that line.

Result

What it means

Success

The step returned response.Success(). It's marked green and the run continues.

Failure

The step returned response.Fail('reason'). The reason is shown and the run stops.

Error

Something in your code went wrong (for example a typo or a missing element). The error message and line number are shown so you can fix it.

Did this answer your question?