Best Practices
Best Practices
Follow these best practices to write maintainable, reliable Stepwright automation scripts.
Script Organization
Use Descriptive Names
// GoodStepwright.create('E-commerce Checkout Flow') .step('Add product to cart from search results', ...) .step('Proceed to checkout with existing account', ...)
// BadStepwright.create('Test 1') .step('Step 1', ...) .step('Step 2', ...)Group Related Steps
// 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');}).step('Login', async (ctx) => { await ctx.page.fill('#email', ctx.data.email); await ctx.page.fill('#password', ctx.data.password); await ctx.page.click('#submit'); await ctx.page.waitForURL('**/dashboard'); const name = await ctx.page.textContent('#user-name'); ctx.data.userName = name; await ctx.screenshot('logged-in');})Selector Strategy
Prefer Stable Selectors
Priority order:
data-testidattributes- ARIA roles
- Text content
- CSS classes (if stable)
- XPath (last resort)
// Best: Test IDawait ctx.page.locator('[data-testid="submit-button"]').click();
// Good: Role with nameawait ctx.page.locator('role=button[name="Submit"]').click();
// OK: Text contentawait ctx.page.locator('text=Submit Order').click();
// Avoid: Fragile CSSawait ctx.page.locator('.btn.btn-primary.mt-4').click();
// Avoid: Complex XPathawait 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
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.tsimport { 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 delayawait ctx.page.waitForTimeout(3000);
// Good: Wait for specific conditionawait 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 checkpointsStepwright.create('Full Flow') .checkpoint('Login').step(...).endCheckpoint() .checkpoint('Browse').step(...).endCheckpoint() .checkpoint('Checkout').step(...).endCheckpoint()
// Bad: Separate scripts with browser restartsawait loginScript.run(); // Starts browserawait browseScript.run(); // Starts another browserMaintainability
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
// Badctx.log('Password:', ctx.data.password);
// Goodctx.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
TEST_USER_PASSWORD=secret123ANTHROPIC_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
- Examples - See best practices in action
- Debugging Failures - When things go wrong
- CI/CD Integration - Production deployment