Multi-Page Workflow
Multi-Page Workflow
This example demonstrates complex, real-world automation scenarios that span multiple pages and involve various interactions.
E-Commerce Checkout Flow
Complete purchase flow from product search to order confirmation:
import { Stepwright } from '@korvol/stepwright';
interface CheckoutData { // Product selection searchTerm: string; selectedProduct: { name: string; price: number; quantity: number; };
// Customer info customer: { email: string; firstName: string; lastName: string; };
// Shipping shipping: { address: string; city: string; state: string; zip: string; method: string; cost: number; };
// Payment payment: { cardNumber: string; expiry: string; cvv: string; nameOnCard: string; };
// Order result orderId: string; orderTotal: number; estimatedDelivery: string;}
const script = Stepwright.create<CheckoutData>('E-Commerce Checkout') .config({ headless: true, screenshotOnFailure: true, domOnFailure: true, defaultTimeout: 30000, }) .data({ searchTerm: 'wireless headphones', selectedProduct: { name: '', price: 0, quantity: 2 }, customer: { email: 'customer@example.com', firstName: 'John', lastName: 'Doe', }, shipping: { address: '123 Main Street', city: 'New York', state: 'NY', zip: '10001', method: '', cost: 0, }, payment: { cardNumber: '4111111111111111', expiry: '12/26', cvv: '123', nameOnCard: 'John Doe', }, orderId: '', orderTotal: 0, estimatedDelivery: '', })
// ========== SEARCH & PRODUCT SELECTION ========== .checkpoint('Product Search')
.step('Navigate to store', async (ctx) => { await ctx.page.goto('https://store.example.com'); await ctx.page.waitForSelector('.search-bar'); ctx.log('Store loaded'); })
.step('Search for product', async (ctx) => { await ctx.page.fill('.search-bar input', ctx.data.searchTerm); await ctx.page.click('.search-bar button'); await ctx.page.waitForSelector('.search-results');
const resultCount = await ctx.page.locator('.product-card').count(); ctx.log(`Found ${resultCount} products for "${ctx.data.searchTerm}"`); })
.step('Select first product', async (ctx) => { const firstProduct = ctx.page.locator('.product-card').first();
// Extract product info before clicking ctx.data.selectedProduct.name = await firstProduct.locator('.product-name').textContent() || ''; const priceText = await firstProduct.locator('.product-price').textContent() || '0'; ctx.data.selectedProduct.price = parseFloat(priceText.replace(/[^0-9.]/g, ''));
await firstProduct.click(); await ctx.page.waitForSelector('.product-detail');
ctx.log('Selected:', ctx.data.selectedProduct.name); ctx.log('Price:', ctx.data.selectedProduct.price); })
.endCheckpoint()
// ========== ADD TO CART ========== .checkpoint('Add to Cart')
.step('Set quantity', async (ctx) => { await ctx.page.fill('#quantity', ctx.data.selectedProduct.quantity.toString()); ctx.log('Quantity:', ctx.data.selectedProduct.quantity); })
.step('Add to cart', async (ctx) => { await ctx.page.click('#add-to-cart'); await ctx.page.waitForSelector('.cart-notification');
const cartCount = await ctx.page.locator('.cart-count').textContent(); ctx.log('Cart items:', cartCount); })
.step('Go to cart', async (ctx) => { await ctx.page.click('.cart-icon'); await ctx.page.waitForURL('**/cart**'); await ctx.page.waitForSelector('.cart-items');
await ctx.screenshot('cart-contents'); })
.step('Verify cart contents', async (ctx) => { const cartItem = ctx.page.locator('.cart-item').first(); const itemName = await cartItem.locator('.item-name').textContent(); const itemQty = await cartItem.locator('.item-quantity').inputValue();
if (itemName !== ctx.data.selectedProduct.name) { throw new Error(`Wrong product in cart: ${itemName}`); } if (itemQty !== ctx.data.selectedProduct.quantity.toString()) { throw new Error(`Wrong quantity: ${itemQty}`); }
ctx.log('Cart verified'); })
.step('Proceed to checkout', async (ctx) => { await ctx.page.click('#proceed-to-checkout'); await ctx.page.waitForURL('**/checkout**'); })
.endCheckpoint()
// ========== CUSTOMER INFORMATION ========== .checkpoint('Customer Information')
.step('Fill email', async (ctx) => { await ctx.page.fill('#email', ctx.data.customer.email); })
.step('Fill name', async (ctx) => { await ctx.page.fill('#firstName', ctx.data.customer.firstName); await ctx.page.fill('#lastName', ctx.data.customer.lastName); })
.step('Continue to shipping', async (ctx) => { await ctx.page.click('#continue-to-shipping'); await ctx.page.waitForSelector('#shipping-form'); })
.endCheckpoint()
// ========== SHIPPING ========== .checkpoint('Shipping', { maxRetries: 2 })
.step('Fill shipping address', async (ctx) => { await ctx.page.fill('#address', ctx.data.shipping.address); await ctx.page.fill('#city', ctx.data.shipping.city); await ctx.page.selectOption('#state', ctx.data.shipping.state); await ctx.page.fill('#zip', ctx.data.shipping.zip);
// Wait for shipping options to calculate await ctx.page.waitForSelector('.shipping-options:not(.loading)'); })
.step('Select shipping method', async (ctx) => { // Get available shipping options const options = ctx.page.locator('.shipping-option'); const count = await options.count();
ctx.log('Available shipping methods:', count);
// Select standard shipping (or first available) const standardOption = options.filter({ hasText: 'Standard' }).first(); if (await standardOption.count() > 0) { await standardOption.click(); ctx.data.shipping.method = 'Standard'; } else { await options.first().click(); ctx.data.shipping.method = await options.first().locator('.method-name').textContent() || 'Unknown'; }
// Get shipping cost const costText = await ctx.page.locator('.selected .shipping-cost').textContent() || '0'; ctx.data.shipping.cost = parseFloat(costText.replace(/[^0-9.]/g, ''));
ctx.log('Shipping method:', ctx.data.shipping.method); ctx.log('Shipping cost:', ctx.data.shipping.cost); })
.step('Continue to payment', async (ctx) => { await ctx.page.click('#continue-to-payment'); await ctx.page.waitForSelector('#payment-form'); await ctx.screenshot('payment-form'); })
.endCheckpoint()
// ========== PAYMENT ========== .checkpoint('Payment', { maxRetries: 2 })
.step('Fill payment details', async (ctx) => { // Handle iframe if payment is in iframe const paymentFrame = ctx.page.frameLocator('#payment-iframe');
if (await ctx.page.locator('#payment-iframe').count() > 0) { // Payment form is in iframe await paymentFrame.locator('#card-number').fill(ctx.data.payment.cardNumber); await paymentFrame.locator('#expiry').fill(ctx.data.payment.expiry); await paymentFrame.locator('#cvv').fill(ctx.data.payment.cvv); } else { // Payment form is in main page await ctx.page.fill('#card-number', ctx.data.payment.cardNumber); await ctx.page.fill('#expiry', ctx.data.payment.expiry); await ctx.page.fill('#cvv', ctx.data.payment.cvv); }
await ctx.page.fill('#name-on-card', ctx.data.payment.nameOnCard);
ctx.log('Payment details entered'); })
.step('Review order', async (ctx) => { // Calculate expected total const subtotal = ctx.data.selectedProduct.price * ctx.data.selectedProduct.quantity; const expectedTotal = subtotal + ctx.data.shipping.cost;
// Get displayed total const displayedTotal = await ctx.page.locator('.order-total').textContent() || '0'; ctx.data.orderTotal = parseFloat(displayedTotal.replace(/[^0-9.]/g, ''));
ctx.log('Expected total:', expectedTotal); ctx.log('Displayed total:', ctx.data.orderTotal);
// Verify totals match (with small tolerance for taxes) if (Math.abs(ctx.data.orderTotal - expectedTotal) > expectedTotal * 0.15) { throw new Error('Order total mismatch'); }
await ctx.screenshot('order-review'); })
.step('Place order', async (ctx) => { // Wait for place order button to be enabled await ctx.page.waitForSelector('#place-order:not([disabled])'); await ctx.page.click('#place-order');
// Wait for confirmation await ctx.page.waitForURL('**/confirmation**', { timeout: 60000 }); await ctx.page.waitForSelector('.order-confirmation');
ctx.log('Order placed!'); })
.endCheckpoint()
// ========== CONFIRMATION ========== .checkpoint('Order Confirmation')
.step('Extract order details', async (ctx) => { ctx.data.orderId = await ctx.page.locator('.order-id').textContent() || ''; ctx.data.estimatedDelivery = await ctx.page.locator('.delivery-date').textContent() || '';
ctx.log('Order ID:', ctx.data.orderId); ctx.log('Estimated delivery:', ctx.data.estimatedDelivery);
await ctx.screenshot('order-confirmation'); })
.step('Verify confirmation email sent', async (ctx) => { const emailSent = await ctx.page.locator('.email-confirmation').textContent(); ctx.log('Confirmation email:', emailSent);
if (!emailSent?.includes(ctx.data.customer.email)) { ctx.log('Warning: Email confirmation not displayed'); } })
.endCheckpoint();
// Run the scriptasync function main() { const result = await script.run();
if (result.success) { console.log('\n✅ Checkout completed successfully!'); console.log('Order ID:', result.data.orderId); console.log('Total:', result.data.orderTotal); console.log('Delivery:', result.data.estimatedDelivery); } else { console.error('\n❌ Checkout failed!'); console.error('Failed at:', result.failedStep?.name); console.error('Error:', result.failedStep?.error.message);
if (result.failedStep?.artifacts.screenshot) { console.error('Screenshot:', result.failedStep.artifacts.screenshot); } }
console.log('\nDuration:', result.duration, 'ms');}
main().catch(console.error);Multi-Step Form Wizard
Handle multi-step form with navigation:
import { Stepwright } from '@korvol/stepwright';
interface WizardData { currentStep: number; totalSteps: number;
// Step 1: Personal personal: { firstName: string; lastName: string; dob: string; };
// Step 2: Contact contact: { email: string; phone: string; address: string; };
// Step 3: Preferences preferences: { newsletter: boolean; notifications: string[]; };
// Step 4: Review & Submit submissionId: string;}
const script = Stepwright.create<WizardData>('Form Wizard') .config({ headless: true, }) .data({ currentStep: 1, totalSteps: 4, personal: { firstName: 'Jane', lastName: 'Smith', dob: '1990-05-15', }, contact: { email: 'jane@example.com', phone: '+1-555-123-4567', address: '456 Oak Avenue, Boston, MA 02101', }, preferences: { newsletter: true, notifications: ['email', 'sms'], }, submissionId: '', })
.step('Navigate to wizard', async (ctx) => { await ctx.page.goto('https://example.com/signup-wizard'); await ctx.page.waitForSelector('.wizard-container');
// Verify we're on step 1 const currentStep = await ctx.page.locator('.step.active').getAttribute('data-step'); ctx.data.currentStep = parseInt(currentStep || '1'); ctx.log('Starting at step:', ctx.data.currentStep); })
// ========== STEP 1: PERSONAL INFO ========== .checkpoint('Step 1: Personal Information')
.step('Fill personal info', async (ctx) => { await ctx.page.fill('#firstName', ctx.data.personal.firstName); await ctx.page.fill('#lastName', ctx.data.personal.lastName); await ctx.page.fill('#dob', ctx.data.personal.dob);
ctx.log('Personal info filled'); })
.step('Navigate to step 2', async (ctx) => { await ctx.page.click('#next-step');
// Wait for step transition await ctx.page.waitForSelector('.step[data-step="2"].active'); ctx.data.currentStep = 2;
ctx.log('Moved to step:', ctx.data.currentStep); })
.endCheckpoint()
// ========== STEP 2: CONTACT INFO ========== .checkpoint('Step 2: Contact Information')
.step('Fill contact info', async (ctx) => { await ctx.page.fill('#email', ctx.data.contact.email); await ctx.page.fill('#phone', ctx.data.contact.phone); await ctx.page.fill('#address', ctx.data.contact.address); })
.step('Navigate to step 3', async (ctx) => { await ctx.page.click('#next-step'); await ctx.page.waitForSelector('.step[data-step="3"].active'); ctx.data.currentStep = 3; })
.endCheckpoint()
// ========== STEP 3: PREFERENCES ========== .checkpoint('Step 3: Preferences')
.step('Set preferences', async (ctx) => { // Newsletter if (ctx.data.preferences.newsletter) { await ctx.page.check('#newsletter'); }
// Notifications for (const type of ctx.data.preferences.notifications) { await ctx.page.check(`#notification-${type}`); }
ctx.log('Preferences set'); })
.step('Navigate to review', async (ctx) => { await ctx.page.click('#next-step'); await ctx.page.waitForSelector('.step[data-step="4"].active'); ctx.data.currentStep = 4; })
.endCheckpoint()
// ========== STEP 4: REVIEW & SUBMIT ========== .checkpoint('Step 4: Review and Submit')
.step('Verify review data', async (ctx) => { // Check that review shows correct data const reviewName = await ctx.page.locator('.review-name').textContent(); const expectedName = `${ctx.data.personal.firstName} ${ctx.data.personal.lastName}`;
if (reviewName !== expectedName) { throw new Error(`Name mismatch: ${reviewName} vs ${expectedName}`); }
const reviewEmail = await ctx.page.locator('.review-email').textContent(); if (reviewEmail !== ctx.data.contact.email) { throw new Error(`Email mismatch: ${reviewEmail} vs ${ctx.data.contact.email}`); }
ctx.log('Review data verified'); await ctx.screenshot('wizard-review'); })
.step('Go back and edit', async (ctx) => { // Test back navigation await ctx.page.click('#prev-step'); await ctx.page.waitForSelector('.step[data-step="3"].active');
// Make a change await ctx.page.uncheck('#notification-sms'); ctx.data.preferences.notifications = ['email'];
// Go forward again await ctx.page.click('#next-step'); await ctx.page.waitForSelector('.step[data-step="4"].active');
ctx.log('Edited and returned to review'); })
.step('Submit form', async (ctx) => { await ctx.page.click('#submit-form');
// Wait for success await ctx.page.waitForSelector('.submission-success');
ctx.data.submissionId = await ctx.page.locator('.submission-id').textContent() || ''; ctx.log('Submission ID:', ctx.data.submissionId); })
.endCheckpoint();Data Pipeline Workflow
Extract, process, and export data:
import { Stepwright } from '@korvol/stepwright';import fs from 'fs';
interface DataRecord { id: string; name: string; value: number; category: string;}
interface PipelineData { sourceUrl: string; rawRecords: DataRecord[]; processedRecords: DataRecord[]; summary: { totalRecords: number; totalValue: number; byCategory: Record<string, number>; }; exportPath: string;}
const script = Stepwright.create<PipelineData>('Data Pipeline') .config({ headless: true, }) .data({ sourceUrl: 'https://example.com/data-source', rawRecords: [], processedRecords: [], summary: { totalRecords: 0, totalValue: 0, byCategory: {}, }, exportPath: './output/data-export.json', })
// ========== EXTRACTION ========== .checkpoint('Data Extraction')
.step('Navigate to data source', async (ctx) => { await ctx.page.goto(ctx.data.sourceUrl); await ctx.page.waitForSelector('.data-table'); ctx.log('Data source loaded'); })
.step('Extract all records', async (ctx) => { let hasMore = true;
while (hasMore) { // Extract current page const rows = ctx.page.locator('.data-row'); const count = await rows.count();
for (let i = 0; i < count; i++) { const row = rows.nth(i); const record: DataRecord = { id: await row.locator('.col-id').textContent() || '', name: await row.locator('.col-name').textContent() || '', value: parseFloat(await row.locator('.col-value').textContent() || '0'), category: await row.locator('.col-category').textContent() || '', }; ctx.data.rawRecords.push(record); }
// Check for next page const nextBtn = ctx.page.locator('.pagination-next:not([disabled])'); if (await nextBtn.count() > 0) { await nextBtn.click(); await ctx.page.waitForLoadState('networkidle'); } else { hasMore = false; } }
ctx.log('Raw records extracted:', ctx.data.rawRecords.length); })
.endCheckpoint()
// ========== PROCESSING ========== .checkpoint('Data Processing')
.step('Filter and clean records', async (ctx) => { ctx.data.processedRecords = ctx.data.rawRecords // Remove invalid records .filter((r) => r.id && r.name && r.value > 0) // Normalize names .map((r) => ({ ...r, name: r.name.trim().toLowerCase(), category: r.category.trim().toUpperCase(), })) // Remove duplicates by ID .filter((r, i, arr) => arr.findIndex((x) => x.id === r.id) === i);
ctx.log('Processed records:', ctx.data.processedRecords.length); ctx.log('Removed:', ctx.data.rawRecords.length - ctx.data.processedRecords.length); })
.step('Calculate summary', async (ctx) => { ctx.data.summary.totalRecords = ctx.data.processedRecords.length; ctx.data.summary.totalValue = ctx.data.processedRecords.reduce((sum, r) => sum + r.value, 0);
// Group by category for (const record of ctx.data.processedRecords) { ctx.data.summary.byCategory[record.category] = (ctx.data.summary.byCategory[record.category] || 0) + record.value; }
ctx.log('Total value:', ctx.data.summary.totalValue); ctx.log('Categories:', Object.keys(ctx.data.summary.byCategory).length); })
.endCheckpoint()
// ========== EXPORT ========== .checkpoint('Data Export')
.step('Export to JSON', async (ctx) => { const exportData = { extractedAt: new Date().toISOString(), source: ctx.data.sourceUrl, summary: ctx.data.summary, records: ctx.data.processedRecords, };
// Ensure output directory exists const dir = ctx.data.exportPath.substring(0, ctx.data.exportPath.lastIndexOf('/')); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); }
fs.writeFileSync(ctx.data.exportPath, JSON.stringify(exportData, null, 2)); ctx.log('Exported to:', ctx.data.exportPath); })
.step('Generate report', async (ctx) => { const report = `# Data Pipeline ReportGenerated: ${new Date().toISOString()}
## Summary- Total Records: ${ctx.data.summary.totalRecords}- Total Value: $${ctx.data.summary.totalValue.toLocaleString()}
## By Category${Object.entries(ctx.data.summary.byCategory) .map(([cat, val]) => `- ${cat}: $${val.toLocaleString()}`) .join('\n')} `.trim();
fs.writeFileSync('./output/report.md', report); ctx.log('Report generated'); })
.endCheckpoint();Monitoring Dashboard Workflow
Automated monitoring and alerting:
import { Stepwright } from '@korvol/stepwright';
interface HealthCheck { name: string; url: string; status: 'healthy' | 'degraded' | 'down'; responseTime: number; lastChecked: string;}
interface MonitoringData { services: HealthCheck[]; alerts: string[]; overallStatus: 'healthy' | 'degraded' | 'critical';}
const script = Stepwright.create<MonitoringData>('Service Monitoring') .config({ headless: true, defaultTimeout: 10000, }) .data({ services: [], alerts: [], overallStatus: 'healthy', })
.step('Check API health', async (ctx) => { const startTime = Date.now();
try { const response = await ctx.page.request.get('https://api.example.com/health'); const responseTime = Date.now() - startTime;
ctx.data.services.push({ name: 'API', url: 'https://api.example.com/health', status: response.status() === 200 ? 'healthy' : 'degraded', responseTime, lastChecked: new Date().toISOString(), });
if (responseTime > 5000) { ctx.data.alerts.push('API response time > 5s'); } } catch (error) { ctx.data.services.push({ name: 'API', url: 'https://api.example.com/health', status: 'down', responseTime: -1, lastChecked: new Date().toISOString(), }); ctx.data.alerts.push('API is down'); } })
.step('Check web app health', async (ctx) => { const startTime = Date.now();
try { await ctx.page.goto('https://app.example.com'); await ctx.page.waitForSelector('#app-loaded');
const responseTime = Date.now() - startTime;
ctx.data.services.push({ name: 'Web App', url: 'https://app.example.com', status: responseTime < 3000 ? 'healthy' : 'degraded', responseTime, lastChecked: new Date().toISOString(), }); } catch (error) { ctx.data.services.push({ name: 'Web App', url: 'https://app.example.com', status: 'down', responseTime: -1, lastChecked: new Date().toISOString(), }); ctx.data.alerts.push('Web App is down'); } })
.step('Check authentication flow', async (ctx) => { try { await ctx.page.goto('https://app.example.com/login'); await ctx.page.fill('#email', 'monitor@example.com'); await ctx.page.fill('#password', process.env.MONITOR_PASSWORD!); await ctx.page.click('#login');
await ctx.page.waitForURL('**/dashboard**', { timeout: 10000 });
ctx.data.services.push({ name: 'Auth Flow', url: 'https://app.example.com/login', status: 'healthy', responseTime: 0, lastChecked: new Date().toISOString(), }); } catch (error) { ctx.data.services.push({ name: 'Auth Flow', url: 'https://app.example.com/login', status: 'down', responseTime: -1, lastChecked: new Date().toISOString(), }); ctx.data.alerts.push('Authentication flow broken'); } })
.step('Determine overall status', async (ctx) => { const downCount = ctx.data.services.filter((s) => s.status === 'down').length; const degradedCount = ctx.data.services.filter((s) => s.status === 'degraded').length;
if (downCount > 0) { ctx.data.overallStatus = 'critical'; } else if (degradedCount > 0) { ctx.data.overallStatus = 'degraded'; } else { ctx.data.overallStatus = 'healthy'; }
ctx.log('Overall status:', ctx.data.overallStatus); ctx.log('Alerts:', ctx.data.alerts.length); })
.step('Send alerts if needed', async (ctx) => { if (ctx.data.alerts.length > 0) { // Send to monitoring endpoint await ctx.page.request.post('https://alerts.example.com/webhook', { data: { status: ctx.data.overallStatus, alerts: ctx.data.alerts, services: ctx.data.services, }, });
ctx.log('Alerts sent:', ctx.data.alerts); } });Running the Examples
# E-commerce checkoutnpx tsx scripts/ecommerce-checkout.ts
# With visible browser for debuggingnpx stepwright run scripts/form-wizard.ts --headed
# Data pipeline with outputnpx tsx scripts/data-pipeline.ts && cat output/report.mdKey Takeaways
- Use checkpoints to group related steps and define retry boundaries
- Store state in data for verification and debugging
- Take screenshots at key points in the workflow
- Handle errors gracefully with appropriate retry strategies
- Extract and validate data throughout the flow
- Use type-safe data for complex workflows
Next Steps
- Best Practices - Production-ready patterns
- CI/CD Integration - Run workflows in pipelines
- Fixwright Overview - Auto-fix broken workflows