Skip to content

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 state
console.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

// Good
interface CheckoutData {
shippingAddress: Address;
billingAddress: Address;
paymentMethod: PaymentInfo;
orderConfirmationNumber?: string;
}
// Bad
interface 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