Skip to content

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:

StatusDescription
pendingNot yet executed
runningCurrently executing
passedCompleted successfully
failedThrew an error
skippedSkipped 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 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