Next.js Example App
Overview
Section titled “Overview”The Next.js App Router example demonstrates HTTP-level testing for Server Components, Client Components, API routes, and Server Actions using Scenarist.
GitHub: apps/nextjs-app-router-example
What It Demonstrates
Section titled “What It Demonstrates”This example app showcases all major Scenarist features:
Core Features
Section titled “Core Features”- Server Components - Test async Server Components without mocking Next.js internals
- Client Components - Test client-side hydration with backend scenarios
- API Routes - Test Route Handlers with different external API responses
- Runtime Scenario Switching - Multiple scenarios running concurrently
Dynamic Response Features
Section titled “Dynamic Response Features”- Request Matching - Different responses based on request content (tier-based pricing)
- Sequences - Polling scenarios (pending → processing → complete)
- Stateful Mocks - Shopping cart with state capture and injection
Installation
Section titled “Installation”Prerequisites
Section titled “Prerequisites”- Node.js 20+
- pnpm 9+
Clone and Install
Section titled “Clone and Install”# Clone the repositorygit clone https://github.com/citypaul/scenarist.gitcd scenarist
# Install dependenciespnpm install
# Navigate to Next.js examplecd apps/nextjs-app-router-exampleRunning the Example
Section titled “Running the Example”Development Mode
Section titled “Development Mode”# Start the Next.js dev serverpnpm devVisit http://localhost:3002 to see the app.
Run Tests
Section titled “Run Tests”# Run all testspnpm test
# Run tests in UI modepnpm test:ui
# Run specific test filepnpm test products-server-componentsKey Files
Section titled “Key Files”Scenarist Setup
Section titled “Scenarist Setup”lib/scenarist.ts - Scenarist configuration
import { createScenarist } from '@scenarist/nextjs-adapter';import { scenarios } from './scenarios';
export const scenarist = createScenarist({ enabled: process.env.NODE_ENV === 'test', scenarios,});app/api/[[...route]]/route.ts - Catch-all route for Scenarist endpoints
import { scenarist } from '@/lib/scenarist';
export const { GET, POST } = scenarist;This creates the /__scenario__ endpoint used by tests to switch scenarios.
Scenario Definitions
Section titled “Scenario Definitions”lib/scenarios.ts - All scenario definitions (view on GitHub)
Key scenarios:
default - Standard user, successful API responses
premiumUser - Premium tier with request matching
premiumUser: { id: 'premiumUser', mocks: [{ method: 'GET', url: 'https://api.products.example.com/pricing', match: { query: { tier: 'premium' } }, response: { status: 200, body: { price: 799, discount: 20 } } }]}githubPolling - Polling sequence (pending → processing → complete)
githubPolling: { id: 'githubPolling', mocks: [{ method: 'GET', url: 'https://api.github.com/repos/user/repo/status', sequence: { responses: [ { status: 200, body: { status: 'pending' } }, { status: 200, body: { status: 'processing' } }, { status: 200, body: { status: 'complete' } } ], repeat: 'last' } }]}cartWithState - Stateful shopping cart
cartWithState: { id: 'cartWithState', mocks: [ { method: 'POST', url: 'https://api.cart.example.com/add', captureState: { cartItems: { from: 'body', path: 'productId' } }, response: { status: 201 } }, { method: 'GET', url: 'https://api.cart.example.com/items', response: { status: 200, body: { items: '{{state.cartItems}}' } } } ]}Test Examples
Section titled “Test Examples”tests/playwright/products-server-components.spec.ts - Server Components with request matching
test('premium users see discounted pricing', async ({ page, switchScenario }) => { await switchScenario(page, 'premiumUser');
await page.goto('/products');
// Server Component fetches pricing with tier=premium query param // Scenarist returns mock matching { query: { tier: 'premium' } } await expect(page.getByText('$799')).toBeVisible(); await expect(page.getByText('20% off')).toBeVisible();});tests/playwright/sequences.spec.ts - Polling with sequences
test('polling updates status through sequence', async ({ page, switchScenario }) => { await switchScenario(page, 'githubPolling');
await page.goto('/polling');
// First request: pending await expect(page.getByText('Status: pending')).toBeVisible();
await page.getByRole('button', { name: 'Refresh' }).click(); // Second request: processing await expect(page.getByText('Status: processing')).toBeVisible();
await page.getByRole('button', { name: 'Refresh' }).click(); // Third request: complete await expect(page.getByText('Status: complete')).toBeVisible();});tests/playwright/cart-server-rsc.spec.ts - Stateful mocks with Server Components
test('cart maintains state across requests', async ({ page, switchScenario }) => { const testId = await switchScenario(page, 'cartWithState');
// Add product - state captured await page.request.post('http://localhost:3002/api/cart/add', { headers: { 'x-scenarist-test-id': testId }, data: { productId: 'prod-1' } });
await page.goto('/cart-server');
// Cart shows added product - state injected await expect(page.getByText('Product A')).toBeVisible();});Architecture
Section titled “Architecture”How It Works
Section titled “How It Works”- Setup - Next.js app includes Scenarist catch-all route
- Test starts - Calls
switchScenario()to set active scenario - HTTP request - Test makes request to Next.js app
- Backend execution - Server Components, API routes execute normally
- External API call - Intercepted by MSW with scenario-defined response
- Test assertion - Verifies rendered output or API response
Test Isolation
Section titled “Test Isolation”Each test gets a unique test ID:
- Scenario switching:
POST /__scenario__withx-scenarist-test-idheader - All requests include
x-scenarist-test-idheader automatically (Playwright helper) - Server routes requests to correct scenario based on test ID
- Parallel tests don’t interfere with each other
File Structure
Section titled “File Structure”Directoryapps/nextjs-app-router-example/
Directoryapp/
Directoryapi/
- [[…route]]/route.ts Scenarist endpoints
Directoryproducts/ Server Components
- …
Directorypolling/ Sequence example
- …
Directorycart-server/ Stateful mock example
- …
Directorylib/
- scenarist.ts Scenarist setup
- scenarios.ts Scenario definitions
Directorytests/
Directoryplaywright/
- products-server-components.spec.ts
- sequences.spec.ts
- cart-server-rsc.spec.ts
Common Patterns
Section titled “Common Patterns”Testing Server Components
Section titled “Testing Server Components”// Server Component fetches external APIexport default async function ProductsPage() { const response = await fetch('https://api.products.example.com/list'); const products = await response.json(); return <div>{products.map(p => <Product key={p.id} {...p} />)}</div>;}
// Test with different scenariostest('standard products', async ({ page, switchScenario }) => { await switchScenario(page, 'default'); await page.goto('/products'); await expect(page.getByText('Product A')).toBeVisible();});
test('premium products', async ({ page, switchScenario }) => { await switchScenario(page, 'premiumUser'); await page.goto('/products'); await expect(page.getByText('Premium Product')).toBeVisible();});Testing with Request Matching
Section titled “Testing with Request Matching”Use request content to determine response:
// Scenario with tier-based pricingmocks: [{ method: 'GET', url: 'https://api.products.example.com/pricing', match: { query: { tier: 'premium' } }, response: { status: 200, body: { price: 799, discount: 20 } }}, { method: 'GET', url: 'https://api.products.example.com/pricing', // No match criteria - fallback for standard tier response: { status: 200, body: { price: 999, discount: 0 } }}]Testing Polling Scenarios
Section titled “Testing Polling Scenarios”Use sequences to simulate async operations:
// Scenario with polling sequencemocks: [{ method: 'GET', url: 'https://api.github.com/repos/user/repo/status', sequence: { responses: [ { status: 200, body: { status: 'pending' } }, { status: 200, body: { status: 'processing' } }, { status: 200, body: { status: 'complete' } } ], repeat: 'last' // After sequence exhausts, repeat last response }}]Next Steps
Section titled “Next Steps”- Next.js App Router Getting Started → - Integrate Scenarist into your Next.js app
- Request Matching → - Learn about request content matching
- Sequences → - Learn about response sequences
- Stateful Mocks → - Learn about state capture and injection