Skip to content

Best Practices

Best Practices

Follow these best practices to write maintainable, reliable Stepwright automation scripts.

Script Organization

Use Descriptive Names

// Good
Stepwright.create('E-commerce Checkout Flow')
.step('Add product to cart from search results', ...)
.step('Proceed to checkout with existing account', ...)
// Bad
Stepwright.create('Test 1')
.step('Step 1', ...)
.step('Step 2', ...)
// Good: Logical groupings
.checkpoint('User Authentication')
.step('Navigate to login page', ...)
.step('Enter credentials', ...)
.step('Submit login form', ...)
.step('Verify dashboard loaded', ...)
.endCheckpoint()
.checkpoint('Shopping Cart')
.step('Add item to cart', ...)
.step('Update quantity', ...)
.step('Apply discount code', ...)
.endCheckpoint()
// Bad: No organization
.step('Login', ...)
.step('Add to cart', ...)
.step('Check something', ...)
.step('Do another thing', ...)

Keep Steps Atomic

.step('Fill email field', async (ctx) => {
await ctx.page.fill('#email', ctx.data.email);
})
.step('Fill password field', async (ctx) => {
await ctx.page.fill('#password', ctx.data.password);
})
.step('Submit login form', async (ctx) => {
await ctx.page.click('#submit');
})

Selector Strategy

Prefer Stable Selectors

Priority order:

  1. data-testid attributes
  2. ARIA roles
  3. Text content
  4. CSS classes (if stable)
  5. XPath (last resort)
// Best: Test ID
await ctx.page.locator('[data-testid="submit-button"]').click();
// Good: Role with name
await ctx.page.locator('role=button[name="Submit"]').click();
// OK: Text content
await ctx.page.locator('text=Submit Order').click();
// Avoid: Fragile CSS
await ctx.page.locator('.btn.btn-primary.mt-4').click();
// Avoid: Complex XPath
await ctx.page.locator('//div[@class="form"]/button[2]').click();

Request Test IDs

When working with development teams, request stable test IDs:

<!-- Good: Explicit test ID -->
<button data-testid="checkout-submit">Place Order</button>
<!-- Better: Component-specific test ID -->
<button data-testid="checkout.submit-order">Place Order</button>

Use Locator Variables

.step('Setup locators', async (ctx) => {
ctx.locators.submitBtn = ctx.page.locator('[data-testid="submit"]');
ctx.locators.errorMsg = ctx.page.locator('[data-testid="error-message"]');
ctx.locators.successMsg = ctx.page.locator('[data-testid="success-message"]');
})
.step('Submit and verify', async (ctx) => {
await ctx.locators.submitBtn.click();
await expect(ctx.locators.errorMsg).not.toBeVisible();
await expect(ctx.locators.successMsg).toBeVisible();
})

Data Management

Type Your Data

interface CheckoutData {
user: {
email: string;
password: string;
};
shipping: {
address: string;
city: string;
zip: string;
};
orderId?: string;
totalPrice?: number;
}
Stepwright.create<CheckoutData>('Checkout')
.data({
user: { email: 'test@example.com', password: 'secret' },
shipping: { address: '123 Main St', city: 'NYC', zip: '10001' },
})

Avoid Hardcoding

// Good: Data-driven
.data({
baseUrl: process.env.BASE_URL || 'https://staging.example.com',
testUser: process.env.TEST_USER || 'test@example.com',
})
.step('Navigate', async (ctx) => {
await ctx.page.goto(ctx.data.baseUrl);
})
// Bad: Hardcoded values
.step('Navigate', async (ctx) => {
await ctx.page.goto('https://staging.example.com');
})

Extract Reusable Data

config/test-data.ts
export const testUsers = {
standard: { email: 'user@test.com', password: 'pass123' },
admin: { email: 'admin@test.com', password: 'admin123' },
};
export const testProducts = {
basic: { id: 'PROD-001', name: 'Basic Widget' },
premium: { id: 'PROD-002', name: 'Premium Widget' },
};
// script.ts
import { testUsers, testProducts } from './config/test-data';
Stepwright.create('Test')
.data({
user: testUsers.standard,
product: testProducts.basic,
})

Error Handling

Use Checkpoints for Recovery

.checkpoint('Payment', { maxRetries: 2 })
.step('Enter card details', async (ctx) => {
// If this fails, entire checkpoint retries
await ctx.page.fill('#card-number', ctx.data.card.number);
await ctx.page.fill('#cvv', ctx.data.card.cvv);
})
.step('Submit payment', async (ctx) => {
await ctx.page.click('#pay-button');
await ctx.page.waitForSelector('.payment-success');
})
.endCheckpoint()

Handle Expected Errors

.step('Dismiss cookie banner if present', async (ctx) => {
try {
const banner = ctx.page.locator('.cookie-banner');
if (await banner.isVisible({ timeout: 2000 })) {
await banner.locator('button.accept').click();
}
} catch {
// Banner not present, continue
ctx.log('No cookie banner found');
}
})

Set Appropriate Timeouts

.config({
defaultTimeout: 30000, // 30 seconds default
})
// Fast operations
.step('Click visible button', async (ctx) => {
await ctx.page.click('#visible-button');
}, { timeout: 5000 })
// Slow operations
.step('Wait for report generation', async (ctx) => {
await ctx.page.waitForSelector('#report-ready');
}, { timeout: 120000 })

Performance

Avoid Unnecessary Waits

// Bad: Arbitrary delay
await ctx.page.waitForTimeout(3000);
// Good: Wait for specific condition
await ctx.page.waitForSelector('#content-loaded');
await ctx.page.waitForLoadState('networkidle');

Run Independent Steps in Parallel

// If extracting multiple independent values
.step('Extract page data', async (ctx) => {
const [title, price, stock] = await Promise.all([
ctx.page.textContent('h1'),
ctx.page.textContent('.price'),
ctx.page.textContent('.stock-status'),
]);
ctx.data.product = { title, price, stock };
})

Minimize Browser Restarts

// Good: One browser for multiple checkpoints
Stepwright.create('Full Flow')
.checkpoint('Login').step(...).endCheckpoint()
.checkpoint('Browse').step(...).endCheckpoint()
.checkpoint('Checkout').step(...).endCheckpoint()
// Bad: Separate scripts with browser restarts
await loginScript.run(); // Starts browser
await browseScript.run(); // Starts another browser

Maintainability

Add Logging

.step('Process order', async (ctx) => {
ctx.log('Starting order processing');
const orderNumber = await ctx.page.textContent('#order-number');
ctx.log('Order created:', orderNumber);
ctx.data.orderNumber = orderNumber;
})

Use Meaningful Assertions

.step('Verify order confirmation', async (ctx) => {
// Good: Meaningful assertion with context
const confirmationText = await ctx.page.textContent('.confirmation');
if (!confirmationText?.includes('Thank you')) {
throw new Error(`Expected confirmation message, got: ${confirmationText}`);
}
// Good: Using expect with clear intent
await expect(ctx.page.locator('.order-status'))
.toHaveText('Order Confirmed');
})

Document Complex Logic

.step('Handle dynamic pricing', async (ctx) => {
// Price calculation depends on:
// 1. Base product price
// 2. Quantity discounts (10+ = 10% off)
// 3. Member status (members get 5% off)
// 4. Promotional codes
const basePrice = await extractPrice(ctx.page, '.base-price');
const quantity = ctx.data.quantity;
let expectedTotal = basePrice * quantity;
// Apply quantity discount
if (quantity >= 10) {
expectedTotal *= 0.9;
ctx.log('Applied 10% quantity discount');
}
// Apply member discount
if (ctx.data.isMember) {
expectedTotal *= 0.95;
ctx.log('Applied 5% member discount');
}
ctx.data.expectedTotal = expectedTotal;
})

Security

Never Log Sensitive Data

// Bad
ctx.log('Password:', ctx.data.password);
// Good
ctx.log('Logging in as:', ctx.data.email);

Clear Sensitive Fields Before Screenshots

.step('Fill payment form', async (ctx) => {
await ctx.page.fill('#card-number', ctx.data.card.number);
await ctx.page.fill('#cvv', ctx.data.card.cvv);
// Clear before screenshot
await ctx.page.fill('#card-number', '•••• •••• •••• ••••');
await ctx.page.fill('#cvv', '•••');
await ctx.screenshot('payment-form');
})

Use Environment Variables

.env
TEST_USER_PASSWORD=secret123
ANTHROPIC_API_KEY=sk-ant-...
// script.ts
.data({
password: process.env.TEST_USER_PASSWORD!,
})

Summary Checklist

  • Descriptive script and step names
  • Logical checkpoint groupings
  • Atomic, focused steps
  • Stable selectors (test IDs preferred)
  • Typed data with TypeScript
  • Environment-based configuration
  • Appropriate timeouts
  • Checkpoints for retry boundaries
  • Meaningful logging
  • No sensitive data in logs or screenshots

Next Steps