Test Real Code
Your routes, middleware, and business logic execute normally. Only external HTTP calls are mocked.
Modern frameworks like Next.js blur the lines between frontend and backend. A single Server Component might validate input, query a database, call Stripe, and render HTML—all in one request.
The testing dilemma:
Scenarist’s approach: Run Playwright tests against your real application. Your Server Components render, your middleware executes, your validation runs—for real. Only external services (Stripe, Auth0, SendGrid) are mocked, and you control exactly what they return per test.
import type { ScenaristScenarios } from '@scenarist/express-adapter';// Or: import type { ScenaristScenarios } from '@scenarist/nextjs-adapter/app';
// Define scenarios as data (not functions)const scenarios = { default: { id: 'default', mocks: [ { method: 'POST', url: 'https://api.stripe.com/v1/charges', response: { status: 200, body: { id: 'ch_123', status: 'succeeded' } }, }, ], }, cardDeclined: { id: 'cardDeclined', mocks: [ { method: 'POST', url: 'https://api.stripe.com/v1/charges', response: { status: 402, body: { error: { code: 'card_declined' } } }, }, ], },} as const satisfies ScenaristScenarios;Pick your framework to get started with a complete, working setup:
Each framework has a complete, working example you can clone and run:
| Framework | Example App |
|---|---|
| Express | apps/express-example |
| Next.js App Router | apps/nextjs-app-router-example |
| Next.js Pages Router | apps/nextjs-pages-router-example |
Each framework guide covers:
Before diving into your framework guide, here’s what makes Scenarist different:
Test Real Code
Your routes, middleware, and business logic execute normally. Only external HTTP calls are mocked.
Declarative Scenarios
Scenarios are data structures, not functions. Inspectable, composable, and versionable.
Parallel Execution
Each test gets isolated scenario state via test IDs. Run hundreds of tests simultaneously.
Zero Production Code
Conditional exports eliminate all Scenarist code from production builds.
When scenarios don’t match as expected, Scenarist’s built-in logging shows you exactly what’s happening:
# See which mocks are matching your requestsSCENARIST_LOG=1 pnpm test09:49:09.715 DBG [test-checkout] 🎯 matching mock_candidates_found count=5 url="/api/cart"09:49:09.716 INF [test-checkout] 🎯 matching mock_selected mockIndex=2 specificity=5When tests fail, inspect the current mock state directly from your Playwright tests using the debug fixtures:
import { test, expect } from './fixtures';
test('checkout flow', async ({ page, switchScenario, debugState }) => { await switchScenario(page, 'checkout'); await page.goto('/cart'); await page.click('#add-item');
// Inspect current state captured by mocks const state = await debugState(page); console.log('Cart state:', state); // → { 'cart.items': 1, 'cart.total': 29.99 }});For async workflows, wait for state to reach a condition:
test('approval flow', async ({ page, switchScenario, waitForDebugState }) => { await switchScenario(page, 'approvalFlow'); await page.click('#submit-for-approval');
// Wait for backend state to update const state = await waitForDebugState( page, (s) => s['approval.status'] === 'approved', { timeout: 10000 } );});→ Full Playwright debug helpers guide