Data Management
Data Management
Stepwright provides a type-safe way to share data between steps using the data property on the context.
Basic Usage
Setting Initial Data
Use the .data() method to set initial values:
const script = Stepwright.create('My Script') .data({ username: 'testuser', password: 'secret123', baseUrl: 'https://app.example.com', });Accessing Data in Steps
Access data via ctx.data:
.step('Login', async (ctx) => { await ctx.page.goto(ctx.data.baseUrl); await ctx.page.fill('#username', ctx.data.username); await ctx.page.fill('#password', ctx.data.password);})Modifying Data
Steps can modify data for later steps:
.step('Extract token', async (ctx) => { ctx.data.authToken = await ctx.page.evaluate(() => { return localStorage.getItem('auth_token'); });})
.step('Use token', async (ctx) => { console.log('Using token:', ctx.data.authToken);})Type Safety
Defining Data Types
Use TypeScript generics for type-safe data:
interface TestData { username: string; password: string; authToken?: string; userId?: number;}
const script = Stepwright.create<TestData>('Typed Script') .data({ username: 'testuser', password: 'secret', // authToken and userId are optional });Type Checking in Steps
Data is fully typed within steps:
.step('Example', async (ctx) => { // TypeScript knows these types const user: string = ctx.data.username;
// TypeScript error: userId might be undefined const id: number = ctx.data.userId; // Error!
// Correct: handle undefined if (ctx.data.userId) { const id: number = ctx.data.userId; // OK }})Complex Data Types
Define rich data structures:
interface User { id: number; email: string; profile: { name: string; avatar?: string; };}
interface E2EData { createdUsers: User[]; currentUser?: User; cartItems: Array<{ productId: string; quantity: number }>;}
Stepwright.create<E2EData>('E-commerce Test') .data({ createdUsers: [], cartItems: [], }) .step('Create user', async (ctx) => { const user: User = await createTestUser(); ctx.data.createdUsers.push(user); ctx.data.currentUser = user; });Data Patterns
Accumulating Data
Collect data across multiple steps:
interface ScrapingData { products: Array<{ name: string; price: number }>;}
Stepwright.create<ScrapingData>('Scrape Products') .data({ products: [] })
.step('Navigate to catalog', async (ctx) => { await ctx.page.goto('/products'); })
.step('Scrape page 1', async (ctx) => { const items = await scrapeProducts(ctx.page); ctx.data.products.push(...items); })
.step('Go to next page', async (ctx) => { await ctx.page.click('.next-page'); })
.step('Scrape page 2', async (ctx) => { const items = await scrapeProducts(ctx.page); ctx.data.products.push(...items); });Data-Driven Steps
Use data to control step behavior:
interface FormData { fields: Array<{ selector: string; value: string }>;}
Stepwright.create<FormData>('Fill Dynamic Form') .data({ fields: [ { selector: '#name', value: 'John Doe' }, { selector: '#email', value: 'john@example.com' }, { selector: '#phone', value: '555-1234' }, ], })
.step('Fill all fields', async (ctx) => { for (const field of ctx.data.fields) { await ctx.page.fill(field.selector, field.value); } });Conditional Data
Use optional data for conditional flows:
interface FlowData { skipOptionalSteps?: boolean; promoCode?: string;}
Stepwright.create<FlowData>('Checkout') .data({})
.step('Apply promo code', async (ctx) => { await ctx.page.fill('#promo', ctx.data.promoCode!); await ctx.page.click('#apply-promo'); }, { skip: (ctx) => !ctx.data.promoCode, });Data in Results
After script execution, data is included in the result:
const result = await script.run();
// Access final data stateconsole.log('Extracted data:', result.data);console.log('Auth token:', result.data.authToken);console.log('Created users:', result.data.createdUsers.length);Data Snapshots
For checkpoint recovery, data can be snapshotted:
.step('Take snapshot', async (ctx) => { const snapshot = ctx.snapshot(); // snapshot.state.data contains current data})
.step('Restore if needed', async (ctx) => { // Restores data to last snapshot ctx.restoreLastSnapshot();})Named Locators
For frequently-used elements, use named locators:
.step('Setup locators', async (ctx) => { ctx.locators.submitButton = ctx.page.locator('button[type="submit"]'); ctx.locators.errorMessage = ctx.page.locator('.error-message');})
.step('Use locators', async (ctx) => { await ctx.locators.submitButton.click(); await expect(ctx.locators.errorMessage).not.toBeVisible();})Best Practices
Initialize All Required Fields
// Good - all required fields initialized.data({ users: [], config: { timeout: 5000 },})
// Bad - might cause runtime errors.data({}) // then accessing ctx.data.users.push(...)Use Descriptive Names
// Goodinterface CheckoutData { shippingAddress: Address; billingAddress: Address; paymentMethod: PaymentInfo; orderConfirmationNumber?: string;}
// Badinterface Data { addr1: object; addr2: object; payment: object; num?: string;}Keep Data Minimal
Store only what you need:
// Good - store extracted IDs.step('Create user', async (ctx) => { const response = await api.createUser(); ctx.data.userId = response.id;})
// Bad - store entire responses.step('Create user', async (ctx) => { ctx.data.userResponse = await api.createUser(); // Stores unnecessary data})Next Steps
- Retries and Recovery - Handle failures gracefully
- Artifacts - Capture screenshots and DOM
- API Reference: Context - Full context API