Steps and Checkpoints
Steps and Checkpoints
Steps are the building blocks of Stepwright scripts. Checkpoints group related steps together for logical organization and retry handling.
Steps
Basic Step
A step has a name and an async function:
.step('Navigate to homepage', async (ctx) => { await ctx.page.goto('https://example.com');})Step Context
The context (ctx) provides access to:
.step('Example step', async (ctx) => { // Playwright page await ctx.page.click('button');
// Browser and context const cookies = await ctx.browserContext.cookies();
// Shared data ctx.data.extractedValue = await ctx.page.textContent('h1');
// Logging ctx.log('Step completed successfully');
// Screenshots await ctx.screenshot('after-click');
// Current step info console.log('Running:', ctx.step?.name);
// Previous results const lastStep = ctx.results[ctx.results.length - 1];})Step Options
Configure individual steps:
.step('Slow API call', async (ctx) => { await ctx.page.waitForResponse('**/api/data');}, { // Timeout for this step (overrides default) timeout: 60000,
// Mark as critical (default: true within checkpoints) critical: true,
// Retry configuration retry: { times: 3, delay: 1000, backoff: 'exponential', },
// Skip condition skip: (ctx) => ctx.data.skipSlowSteps,
// Dependencies dependsOn: ['Login step'],
// Lifecycle hooks beforeRun: async (ctx) => { ctx.log('Starting slow step...'); }, afterRun: async (ctx) => { ctx.log('Slow step completed'); }, onError: async (error, ctx) => { ctx.log('Step failed:', error.message); },})Step Status
Steps can have the following statuses:
| Status | Description |
|---|---|
pending | Not yet executed |
running | Currently executing |
passed | Completed successfully |
failed | Threw an error |
skipped | Skipped due to condition or dependency |
Checkpoints
Checkpoints group related steps together.
Basic Checkpoint
.checkpoint('Login Flow') .step('Enter email', async (ctx) => { await ctx.page.fill('#email', ctx.data.email); }) .step('Enter password', async (ctx) => { await ctx.page.fill('#password', ctx.data.password); }) .step('Click submit', async (ctx) => { await ctx.page.click('button[type="submit"]'); }).endCheckpoint()Checkpoint Options
.checkpoint('Critical Checkout', { // Require this checkpoint to pass required: true,
// Retry entire checkpoint on failure maxRetries: 3,
// Setup before checkpoint (runs on each retry) setup: async () => { console.log('Preparing checkout...'); },
// Teardown after checkpoint teardown: async () => { console.log('Checkout complete'); },})Required vs Optional Checkpoints
.checkpoint('Login', { required: true }) .step('Enter credentials', ...) .step('Submit', ...).endCheckpoint()
// Script fails if this checkpoint fails.checkpoint('Optional Analytics', { required: false }) .step('Track event', ...).endCheckpoint()
// Script continues even if this failsCheckpoint Retries
When a step fails within a checkpoint, the entire checkpoint retries:
.checkpoint('Flaky Login', { maxRetries: 3 }) .step('Fill form', async (ctx) => { await ctx.page.fill('#email', 'user@example.com'); await ctx.page.fill('#password', 'password'); }) .step('Submit', async (ctx) => { await ctx.page.click('button'); // If this fails, checkpoint restarts from 'Fill form' await ctx.page.waitForURL('**/dashboard', { timeout: 5000 }); }).endCheckpoint()Organizing Steps
Multiple Checkpoints
Stepwright.create('E-commerce Flow') .checkpoint('Setup') .step('Navigate', ...) .step('Accept cookies', ...) .endCheckpoint()
.checkpoint('Add to Cart') .step('Search product', ...) .step('Click add', ...) .step('Verify cart', ...) .endCheckpoint()
.checkpoint('Checkout') .step('Enter shipping', ...) .step('Enter payment', ...) .step('Submit order', ...) .endCheckpoint()
.checkpoint('Verify') .step('Check confirmation', ...) .endCheckpoint()Steps Without Checkpoints
Steps outside checkpoints still run, but don’t benefit from checkpoint-level retries:
.step('Global setup', async (ctx) => { // Runs before any checkpoint})
.checkpoint('Main Flow') .step('Step 1', ...).endCheckpoint()
.step('Global teardown', async (ctx) => { // Runs after checkpoints})Conditional Steps
Skip steps based on conditions:
.step('Admin-only action', async (ctx) => { await ctx.page.click('#admin-panel');}, { skip: (ctx) => !ctx.data.isAdmin,})Step Dependencies
Ensure steps run in order:
.step('Create user', async (ctx) => { ctx.data.userId = await createUser();})
.step('Update user', async (ctx) => { await updateUser(ctx.data.userId);}, { dependsOn: ['Create user'],})Best Practices
Name Steps Descriptively
// Good - clear what the step does.step('Fill shipping address form', ...).step('Validate cart total shows $29.99', ...)
// Bad - vague names.step('Step 1', ...).step('Do thing', ...)Keep Steps Atomic
Each step should do one thing:
// Good - single responsibility.step('Fill email', async (ctx) => { await ctx.page.fill('#email', ctx.data.email);}).step('Fill password', async (ctx) => { await ctx.page.fill('#password', ctx.data.password);})
// Bad - multiple concerns.step('Fill form', async (ctx) => { await ctx.page.fill('#email', ctx.data.email); await ctx.page.fill('#password', ctx.data.password); await ctx.page.click('button'); await ctx.page.waitForURL('**/dashboard'); await expect(ctx.page.locator('h1')).toContainText('Welcome');})Use Checkpoints for Recovery Boundaries
Group steps that must succeed together:
// Good - if payment fails, retry entire checkout.checkpoint('Checkout', { maxRetries: 2 }) .step('Enter shipping', ...) .step('Enter payment', ...) .step('Submit order', ...).endCheckpoint()
// Bad - partial retry leaves inconsistent state.step('Enter shipping', ..., { retry: { times: 2 } }).step('Enter payment', ..., { retry: { times: 2 } })Next Steps
- Data Management - Share data between steps
- Retries and Recovery - Handle failures gracefully
- Artifacts - Capture screenshots and DOM