Playwright Integration
Scenarist provides Playwright helpers that enable type-safe scenario switching with autocomplete, automatic test ID management, and clean test organization.
Quick Start
Section titled “Quick Start”1. Install Playwright Helpers
Section titled “1. Install Playwright Helpers”npm install --save-dev @scenarist/playwright-helpers# orpnpm add -D @scenarist/playwright-helpers2. Create Fixtures File
Section titled “2. Create Fixtures File”Create a tests/fixtures.ts file that exports a typed test object:
import { withScenarios, expect } from '@scenarist/playwright-helpers';import { scenarios } from '../lib/scenarios'; // Your scenario definitions
// Create type-safe test object with scenario IDsexport const test = withScenarios(scenarios);export { expect };3. Use in Tests
Section titled “3. Use in Tests”Import from fixtures:
import { test, expect } from './fixtures'; // Import from your fixtures file
test('premium users see advanced features', async ({ page, switchScenario }) => { await switchScenario(page, 'premiumUser'); // Type-safe! Autocomplete works
await page.goto('/dashboard'); await expect(page.getByText('Advanced Analytics')).toBeVisible();});Do not import directly from @playwright/test:
// DON'T DO THISimport { test, expect } from '@playwright/test'; // No Scenarist fixtures!
test('my test', async ({ page, switchScenario }) => { await switchScenario(page, 'premiumUser'); // Error: switchScenario doesn't exist});Why Use Fixtures?
Section titled “Why Use Fixtures?”The fixtures pattern provides several benefits:
Type-Safe Scenario IDs
Section titled “Type-Safe Scenario IDs”TypeScript knows which scenario IDs exist and provides autocomplete:
await switchScenario(page, 'premiumUser'); // Autocomplete suggests: 'default', 'premiumUser', 'error', etc.await switchScenario(page, 'typo'); // TypeScript error: 'typo' not in scenariosAutomatic Test ID Management
Section titled “Automatic Test ID Management”Each test gets a guaranteed unique test ID to prevent state collisions during parallel execution. You don’t need to manage test IDs yourself - the fixture handles it automatically.
For more details on parallel test execution and test isolation, see Parallel Testing.
Centralized Configuration
Section titled “Centralized Configuration”Configure the scenario endpoint once in your fixtures file or playwright.config.ts:
import { defineConfig } from '@playwright/test';import type { ScenaristOptions } from '@scenarist/playwright-helpers';
export default defineConfig<ScenaristOptions>({ use: { baseURL: 'http://localhost:3000', scenaristEndpoint: '/api/__scenario__', // Default, can be customized },});Clean Test Organization
Section titled “Clean Test Organization”All tests import from the same fixtures file, ensuring consistency:
tests/ fixtures.ts # Single source of truth auth.spec.ts # import { test, expect } from './fixtures'; checkout.spec.ts # import { test, expect } from './fixtures'; dashboard.spec.ts # import { test, expect } from './fixtures';Complete Example
Section titled “Complete Example”1. Define Scenarios:
import type { ScenaristScenarios } from '@scenarist/express-adapter';
export const scenarios = { // Default scenario with complete happy path default: { id: 'default', name: 'Happy Path', description: 'All external APIs succeed with valid responses', mocks: [ // Stripe: Successful payment { method: 'POST', url: 'https://api.stripe.com/v1/charges', response: { status: 200, body: { id: 'ch_123', status: 'succeeded', amount: 5000 }, }, }, // Auth0: Authenticated standard user { method: 'GET', url: 'https://api.auth0.com/userinfo', response: { status: 200, body: { sub: 'user_123', email: 'john@example.com', tier: 'standard' }, }, }, ], }, // Specialized scenario: Override Auth0 for premium user premiumUser: { id: 'premiumUser', name: 'Premium User', description: 'Premium tier user, everything else succeeds', mocks: [ { method: 'GET', url: 'https://api.auth0.com/userinfo', response: { status: 200, body: { sub: 'user_456', email: 'premium@example.com', tier: 'premium' }, }, }, ], }, // Specialized scenario: Stripe payment failure paymentFails: { id: 'paymentFails', name: 'Payment Declined', description: 'Stripe declines payment, everything else succeeds', mocks: [ { method: 'POST', url: 'https://api.stripe.com/v1/charges', response: { status: 402, body: { error: { code: 'card_declined', message: 'Card was declined' } }, }, }, ], },} as const satisfies ScenaristScenarios;2. Create Fixtures:
import { withScenarios, expect } from '@scenarist/playwright-helpers';import { scenarios } from '../lib/scenarios';
export const test = withScenarios(scenarios);export { expect };3. Write Tests:
import { test, expect } from './fixtures';
test('premium users access advanced features', async ({ page, switchScenario }) => { await switchScenario(page, 'premiumUser');
await page.goto('/dashboard'); await expect(page.getByText('Advanced Analytics')).toBeVisible();});
test('standard users see upgrade prompt', async ({ page, switchScenario }) => { await switchScenario(page, 'default');
await page.goto('/dashboard'); await expect(page.getByText('Upgrade to Premium')).toBeVisible();});Composing with Custom Fixtures
Section titled “Composing with Custom Fixtures”If you already have custom Playwright fixtures, extend the Scenarist test object:
import { withScenarios, expect } from '@scenarist/playwright-helpers';import { scenarios } from '../lib/scenarios';
type CustomFixtures = { authenticatedPage: Page; apiToken: string;};
export const test = withScenarios(scenarios).extend<CustomFixtures>({ authenticatedPage: async ({ page }, use) => { await page.goto('/login'); await page.fill('[name="email"]', 'test@example.com'); await page.fill('[name="password"]', 'password'); await page.click('button[type="submit"]'); await use(page); },
apiToken: async ({}, use) => { const token = await generateTestToken(); await use(token); },});
export { expect };Then use custom fixtures in tests:
import { test, expect } from './fixtures';
test('authenticated user sees dashboard', async ({ authenticatedPage, switchScenario }) => { await switchScenario(authenticatedPage, 'premiumUser');
await expect(authenticatedPage.getByText('Welcome Back')).toBeVisible();});Per-Test Configuration Overrides
Section titled “Per-Test Configuration Overrides”Override endpoint or baseURL for specific tests:
import { test, expect } from './fixtures';
test('staging environment test', async ({ page, switchScenario }) => { await switchScenario(page, 'premiumUser', { baseURL: 'https://staging.example.com', endpoint: '/api/custom-endpoint', });
await page.goto('/dashboard'); // Test runs against staging with custom endpoint});Cross-Origin API Servers
Section titled “Cross-Origin API Servers”When your API server runs on a different host or port than your frontend, use an absolute URL for scenaristEndpoint. This is common in architectures where:
- Frontend and API are separate services on different ports
- You’re testing against a staging or production API
- Your test infrastructure uses a dedicated mock server
Configuration
Section titled “Configuration”import { defineConfig } from '@playwright/test';import type { ScenaristOptions } from '@scenarist/playwright-helpers';
// Frontend: http://localhost:3000// API Server: http://localhost:9090export default defineConfig<ScenaristOptions>({ use: { baseURL: 'http://localhost:3000', // For Playwright navigation (page.goto) scenaristEndpoint: 'http://localhost:9090/__scenario__', // Absolute URL to API },});How It Works
Section titled “How It Works”| Endpoint Type | Example | Behavior |
|---|---|---|
| Relative path | /api/__scenario__ | Prepended with baseURL → http://localhost:3000/api/__scenario__ |
| Absolute URL | http://localhost:9090/__scenario__ | Used directly (ignores baseURL) |
Per-Test Cross-Origin Override
Section titled “Per-Test Cross-Origin Override”You can also override for specific tests:
import { test, expect } from './fixtures';
test('test against separate API server', async ({ page, switchScenario }) => { await switchScenario(page, 'premiumUser', { endpoint: 'http://api.staging.example.com/__scenario__', // Absolute URL });
await page.goto('/dashboard');});Debugging State
Section titled “Debugging State”When tests fail, you often need to inspect the current test state to understand what went wrong. Scenarist provides debug fixtures for this.
debugState(page)
Section titled “debugState(page)”Fetch the current test state from the debug endpoint:
import { test, expect } from './fixtures';
test('checkout flow', async ({ page, switchScenario, debugState }) => { await switchScenario(page, 'checkout'); await page.goto('/cart');
// Add item to cart await page.click('#add-item');
// Debug: Check what state was set const state = await debugState(page); console.log('After add item:', state); // → { 'cart.items': 1, 'cart.total': 29.99 }
expect(state['cart.items']).toBe(1);});waitForDebugState(page, condition, options?)
Section titled “waitForDebugState(page, condition, options?)”Wait for state to meet a condition (useful for async workflows):
import { test, expect } from './fixtures';
test('async approval flow', async ({ page, switchScenario, waitForDebugState }) => { await switchScenario(page, 'approvalFlow'); await page.goto('/dashboard');
// Trigger async approval await page.click('#submit-for-approval');
// Wait for backend state to indicate approval completed const state = await waitForDebugState( page, (s) => s['approval.status'] === 'approved', { timeout: 10000, interval: 100 } );
expect(state['approval.status']).toBe('approved');});Options:
timeout?: number- Maximum wait time in milliseconds (default: 5000)interval?: number- Polling interval in milliseconds (default: 100)
Configuration
Section titled “Configuration”Configure the state endpoint in playwright.config.ts:
export default defineConfig<ScenaristOptions>({ use: { baseURL: 'http://localhost:3000', scenaristEndpoint: '/api/__scenario__', scenaristStateEndpoint: '/__scenarist__/state', // Default value },});API Reference
Section titled “API Reference”withScenarios(scenarios)
Section titled “withScenarios(scenarios)”Creates a typed Playwright test object with Scenarist fixtures.
Parameters:
scenarios- Scenarios object (must satisfyScenaristScenariostype)
Returns:
- Extended Playwright test object with
switchScenariofixture
Example:
const test = withScenarios(scenarios);switchScenario(page, scenarioId, options?)
Section titled “switchScenario(page, scenarioId, options?)”Switch to a scenario for the current test.
Parameters:
page: Page- Playwright Page objectscenarioId: string- ID of scenario to activate (type-safe based on your scenarios)options?: { endpoint?: string; baseURL?: string }- Optional overrides
Returns:
Promise<string>- The test ID (for explicitpage.requestcalls)
Example:
test('my test', async ({ page, switchScenario }) => { const testId = await switchScenario(page, 'premiumUser'); // testId can be used for explicit page.request calls});expect
Section titled “expect”Re-exported from @playwright/test for convenience. Use the same expect you’re familiar with.
import { test, expect } from './fixtures';
test('my test', async ({ page }) => { await expect(page.getByText('Hello')).toBeVisible();});Next Steps
Section titled “Next Steps”- Parallel Testing - Test isolation and concurrent test execution
- Testing Best Practices - Patterns for organizing tests and scenarios
- Writing Scenarios - Learn how to define scenarios
- Endpoint APIs - Complete endpoint reference including debug state