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
localStoragevalue 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
Open your Test Case.
Click Add New Step.
In Step type, select JavaScript Step.
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.
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 |
| The outcome builder. Return |
| Read and write Thunders variables. |
| The live page |
| An HTTP client for calling APIs from inside the step. See Calling APIs. |
|
|
| A read-only view of the requests the page made. |
| A read-only list of the steps already run. Each has |
Response methods
Return exactly one of these.
Method | Behavior |
| The step passes and the run continues to the next step. |
| Passes and attaches |
| 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
variablesobject :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 |
| Sends a GET request. |
| Sends a POST request with a JSON |
| Sends a PUT request with a JSON |
| Sends a PATCH request with a JSON |
| 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
consoleoutput. 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 |
Failure | The step returned |
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. |



