E2E Helper API
Test organization API for hierarchical step management.
Overview
The e2e helper provides three methods for organizing tests:
e2e.quick()- NEW in v1.1.0 - Compact syntax for simple workflowse2e.major()- High-level workflows with sub-stepse2e.minor()- Individual actions
Import
import { e2e } from 'fair-playwright';e2e.quick()
NEW in v1.1.0 - Compact syntax for simple test workflows.
Signature
async function quick(
title: string,
steps: QuickStepDefinition[],
options?: QuickModeOptions
): Promise<void>Why Quick Mode?
Quick Mode addresses the most common feedback: the declarative API can be verbose for simple tests. It provides a compact tuple syntax while maintaining the same MAJOR/MINOR hierarchy.
Before (Declarative Mode):
await e2e.major('User login', {
success: 'Logged in',
failure: 'Login failed',
steps: [
{
title: 'Open page',
success: 'Page opened',
action: async () => { await page.goto('/login') }
},
{
title: 'Fill form',
success: 'Form filled',
action: async () => { await page.fill('#email', 'test@example.com') }
}
]
})After (Quick Mode):
await e2e.quick('User login', [
['Open page', async () => { await page.goto('/login') }],
['Fill form', async () => { await page.fill('#email', 'test@example.com') }]
])Parameters
title
Major step title displayed in output.
- Type:
string - Required: Yes
await e2e.quick('User checkout flow', [...]);steps
Array of step tuples: [title, action] or [title, action, options].
- Type:
QuickStepDefinition[] - Required: Yes
type QuickStepDefinition =
| [string, () => Promise<void>]
| [string, () => Promise<void>, StepOptions];Simple syntax (no options):
['Step title', async () => { /* action */ }]With success/failure messages:
['Step title', async () => { /* action */ }, {
success: 'Success message',
failure: 'Failure message'
}]options
Optional configuration for the major step.
- Type:
QuickModeOptions - Required: No
interface QuickModeOptions {
success?: string;
failure?: string;
}Examples
Basic Usage
import { test } from '@playwright/test';
import { e2e } from 'fair-playwright';
test('user login', async ({ page }) => {
await e2e.quick('User login flow', [
['Open login page', async () => {
await page.goto('/login');
}],
['Enter credentials', async () => {
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password123');
}],
['Submit form', async () => {
await page.click('button[type="submit"]');
}],
['Verify redirect', async () => {
await expect(page).toHaveURL('/dashboard');
}]
]);
});With Success/Failure Messages
await e2e.quick(
'Complete checkout',
[
[
'Add to cart',
async () => { await page.click('[data-test="add-to-cart"]') },
{ success: 'Item added', failure: 'Failed to add item' }
],
[
'Proceed to checkout',
async () => { await page.click('[data-test="checkout"]') },
{ success: 'Navigated to checkout' }
],
[
'Enter payment',
async () => { await page.fill('#card', '4111111111111111') },
{ success: 'Payment info entered' }
]
],
{
success: 'Order placed successfully',
failure: 'Checkout failed'
}
);Multiple Workflows
test('complete user journey', async ({ page }) => {
await e2e.quick('Setup phase', [
['Navigate to site', async () => await page.goto('/')],
['Accept cookies', async () => await page.click('#accept-cookies')]
]);
await e2e.quick('Registration phase', [
['Fill registration form', async () => { /* ... */ }],
['Submit form', async () => { /* ... */ }],
['Verify email sent', async () => { /* ... */ }]
]);
await e2e.quick('Login phase', [
['Enter credentials', async () => { /* ... */ }],
['Click login', async () => { /* ... */ }],
['Verify dashboard', async () => { /* ... */ }]
]);
});Output
✓ MAJOR: User login flow
✓ Open login page
✓ Enter credentials
✓ Submit form
✓ Verify redirectWith success message:
✓ MAJOR: Complete checkout
✓ Add to cart
✓ Proceed to checkout
✓ Enter payment
→ Order placed successfullyReturn Value
Returns Promise<void> that resolves when all steps complete or rejects on first failure.
try {
await e2e.quick('Workflow', [...]);
// All steps passed
} catch (error) {
// Step failed, error contains details
}When to Use Quick Mode
Use Quick Mode when:
- Writing simple, linear test flows
- You want minimal syntax
- Success/failure messages are optional or simple
- Tests have 2-10 steps
Use Declarative Mode (e2e.major()) when:
- Complex workflows with detailed error handling
- Each step needs specific success/failure messages
- Building reusable step definitions
- Tests have many steps that need clear documentation
Use Inline Mode (e2e.minor()) when:
- Single, standalone actions
- Quick one-off operations
- Testing individual components
Comparison
| Feature | Quick Mode | Declarative Mode | Inline Mode |
|---|---|---|---|
| Syntax | Compact tuples | Object-based | Function-based |
| Verbosity | Low | High | Medium |
| Type Safety | Full | Full | Full |
| Success Messages | Optional | Required | Optional |
| Nested Steps | Yes (MAJOR → MINOR) | Yes (MAJOR → MINOR) | No |
| Best For | Simple workflows | Complex workflows | Single actions |
e2e.major()
Execute a MAJOR step with hierarchical sub-steps.
Signature
async function major(
title: string,
options: MajorStepOptions
): Promise<void>Parameters
title
Step title displayed in output.
- Type:
string - Required: Yes
await e2e.major('User login flow', { ... });options
Configuration for the MAJOR step.
- Type:
MajorStepOptions - Required: Yes
interface MajorStepOptions {
success: string;
failure: string;
steps: StepDefinition[];
}MajorStepOptions
success
Message displayed on successful completion.
- Type:
string - Required: Yes
{
success: 'User logged in successfully and redirected to dashboard'
}failure
Message displayed on failure.
- Type:
string - Required: Yes
{
failure: 'Login failed: invalid credentials or network error'
}steps
Array of MINOR step definitions.
- Type:
StepDefinition[] - Required: Yes (can be empty)
interface StepDefinition {
title: string;
success: string;
failure?: string;
action: () => Promise<void>;
}StepDefinition
title
MINOR step title.
- Type:
string - Required: Yes
{
title: 'Open login page'
}success
Success message for this step.
- Type:
string - Required: Yes
{
success: 'Login page loaded successfully'
}failure
Failure message for this step.
- Type:
string - Required: No
- Default: Uses parent MAJOR failure message
{
failure: 'Failed to load login page: network timeout'
}action
Async function to execute.
- Type:
() => Promise<void> - Required: Yes
{
action: async () => {
await page.goto('/login');
await expect(page).toHaveURL('/login');
}
}Example
import { test } from '@playwright/test';
import { e2e } from 'fair-playwright';
test('complete checkout', async ({ page }) => {
await e2e.major('User checkout flow', {
success: 'Order placed successfully, confirmation email sent',
failure: 'Checkout failed: payment declined or network error',
steps: [
{
title: 'Add item to cart',
success: 'Item added to cart',
failure: 'Failed to add item: product out of stock',
action: async () => {
await page.goto('/products/123');
await page.click('[data-test="add-to-cart"]');
await expect(page.locator('.cart-count')).toHaveText('1');
}
},
{
title: 'Proceed to checkout',
success: 'Checkout page loaded',
action: async () => {
await page.click('[data-test="checkout-button"]');
await page.waitForURL('**/checkout');
}
},
{
title: 'Enter shipping info',
success: 'Shipping information saved',
action: async () => {
await page.fill('[name="address"]', '123 Main St');
await page.fill('[name="city"]', 'Springfield');
await page.fill('[name="zip"]', '12345');
}
},
{
title: 'Enter payment info',
success: 'Payment information saved',
action: async () => {
await page.fill('[name="cardNumber"]', '4111111111111111');
await page.fill('[name="expiry"]', '12/25');
await page.fill('[name="cvv"]', '123');
}
},
{
title: 'Place order',
success: 'Order confirmed',
action: async () => {
await page.click('[data-test="place-order"]');
await expect(page.locator('.success-message')).toBeVisible();
}
}
]
});
});Output
✓ MAJOR: User checkout flow
✓ Add item to cart
✓ Proceed to checkout
✓ Enter shipping info
✓ Enter payment info
✓ Place order
→ Order placed successfully, confirmation email sentReturn Value
Returns Promise<void> that resolves when all steps complete or rejects on first failure.
try {
await e2e.major('Workflow', { ... });
// All steps passed
} catch (error) {
// Step failed, error contains details
}e2e.minor()
Execute a standalone MINOR step.
Signature
async function minor(
title: string,
action: () => Promise<void>,
options: MinorStepOptions
): Promise<void>Parameters
title
Step title displayed in output.
- Type:
string - Required: Yes
await e2e.minor('Click submit button', async () => { ... }, { ... });action
Async function to execute.
- Type:
() => Promise<void> - Required: Yes
async () => {
await page.click('[data-test="submit"]');
}options
Configuration for the MINOR step.
- Type:
MinorStepOptions - Required: Yes
interface MinorStepOptions {
success: string;
failure?: string;
}MinorStepOptions
success
Message displayed on successful completion.
- Type:
string - Required: Yes
{
success: 'Submit button clicked, form submitted'
}failure
Message displayed on failure.
- Type:
string - Required: No
- Default: Generic failure message with error
{
failure: 'Failed to click submit: button not found or disabled'
}Example
import { test } from '@playwright/test';
import { e2e } from 'fair-playwright';
test('simple actions', async ({ page }) => {
await e2e.minor('Navigate to homepage', async () => {
await page.goto('https://example.com');
}, {
success: 'Homepage loaded',
failure: 'Failed to load homepage'
});
await e2e.minor('Click login link', async () => {
await page.click('a[href="/login"]');
}, {
success: 'Login link clicked'
});
await e2e.minor('Fill email field', async () => {
await page.fill('[name="email"]', 'user@example.com');
}, {
success: 'Email entered',
failure: 'Email field not found'
});
});Output
✓ Navigate to homepage → Homepage loaded
✓ Click login link → Login link clicked
✓ Fill email field → Email enteredReturn Value
Returns Promise<void> that resolves on success or rejects on failure.
try {
await e2e.minor('Action', async () => { ... }, { ... });
// Action passed
} catch (error) {
// Action failed
}Error Handling
Automatic Error Capture
Both methods automatically capture errors:
await e2e.major('Workflow', {
steps: [
{
title: 'Click button',
success: 'Clicked',
action: async () => {
// If this throws, step fails automatically
await page.click('.does-not-exist');
}
}
],
success: 'Done',
failure: 'Failed' // This message shown with error
});Output:
✗ MAJOR: Workflow
✗ Click button
Error: Element not found: .does-not-exist
→ FailedManual Error Handling
You can also handle errors manually:
await e2e.major('Workflow', {
steps: [
{
title: 'Try action',
success: 'Success',
action: async () => {
try {
await page.click('.risky-button', { timeout: 5000 });
} catch (error) {
// Log but don't fail
console.log('Button not found, continuing...');
}
}
}
],
success: 'Complete',
failure: 'Failed'
});Best Practices
1. Descriptive Titles
// Good
await e2e.major('User completes registration and email verification', ...)
// Avoid
await e2e.major('Test 1', ...)2. Clear Success Messages
// Good
success: 'User logged in, session created, redirected to dashboard'
// Avoid
success: 'Done'3. Specific Failure Messages
// Good
failure: 'Login failed: invalid credentials, session expired, or network error'
// Avoid
failure: 'Error'4. Atomic Steps
Each step should be independently verifiable:
// Good - Each step is atomic
{
title: 'Fill email field',
action: async () => {
await page.fill('[name="email"]', 'user@example.com');
await expect(page.locator('[name="email"]')).toHaveValue('user@example.com');
}
}
// Avoid - Multiple unrelated actions
{
title: 'Fill form',
action: async () => {
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password');
await page.click('button');
}
}5. Use MAJOR for Workflows
// Good - Related steps grouped
await e2e.major('Complete purchase', {
steps: [
{ title: 'Add to cart', ... },
{ title: 'Checkout', ... },
{ title: 'Payment', ... }
],
success: 'Purchase complete'
});
// Avoid - Separate MAJOR steps for related actions
await e2e.major('Add to cart', { steps: [...] });
await e2e.major('Checkout', { steps: [...] });
await e2e.major('Payment', { steps: [...] });6. Use MINOR for Quick Actions
// Good - Simple action
await e2e.minor('Click button', async () => {
await page.click('button');
}, { success: 'Clicked' });
// Avoid - Over-engineering simple action
await e2e.major('Click button', {
steps: [
{
title: 'Click button',
action: async () => { await page.click('button'); }
}
],
success: 'Clicked'
});Advanced Usage
Conditional Steps
await e2e.major('User flow', {
success: 'Complete',
failure: 'Failed',
steps: [
{
title: 'Check condition',
success: 'Checked',
action: async () => {
const hasDiscount = await page.locator('.discount').isVisible();
if (hasDiscount) {
await page.click('.apply-discount');
}
}
}
]
});Dynamic Steps
const items = ['Item 1', 'Item 2', 'Item 3'];
await e2e.major('Add multiple items', {
success: `Added ${items.length} items`,
failure: 'Failed to add items',
steps: items.map(item => ({
title: `Add ${item}`,
success: `${item} added`,
action: async () => {
await page.click(`[data-item="${item}"]`);
}
}))
});Nested Workflows
// Don't nest MAJOR inside MAJOR
await e2e.major('Outer', {
steps: [
{
title: 'Inner',
action: async () => {
await e2e.major('Nested', { ... }); // Avoid this!
}
}
],
success: 'Done'
});
// Use sequential MAJOR steps instead
await e2e.major('First phase', { ... });
await e2e.major('Second phase', { ... });TypeScript
Full Type Safety
import { e2e } from 'fair-playwright';
import type { MajorStepOptions, MinorStepOptions, StepDefinition } from 'fair-playwright';
// Type-safe options
const options: MajorStepOptions = {
success: 'Done',
failure: 'Failed',
steps: [
{
title: 'Step 1',
success: 'Step 1 done',
action: async () => {}
} satisfies StepDefinition
]
};
await e2e.major('Workflow', options);Generic Action Type
type Action = () => Promise<void>;
const myAction: Action = async () => {
await page.click('button');
};
await e2e.minor('My action', myAction, {
success: 'Done'
});Performance
Zero Overhead
The e2e helper has no performance impact:
- Delegates to Playwright's test.step()
- No blocking operations
- Minimal memory usage
Benchmarks
Without e2e helper: 10.2s
With e2e helper: 10.3s
Overhead: 0.1s (1%)Next Steps
- FairReporter API - Reporter configuration
- Step Hierarchy Guide - Learn best practices
- Examples - Real-world usage patterns
