Skip to content

Scenarist vs Playwright Mocks

Playwright includes built-in request interception via page.route() and HAR file recording. This works great for client-side requests. Scenarist intercepts requests on the server—essential for scenario-based testing of Server Components, API routes, and middleware.

What Scenarist Offers

Before diving into comparisons, here's what Scenarist brings to the table:

  • Simple Architecture — Just an HTTP header (x-scenarist-test-id). No Docker, no separate processes, no complex network configuration
  • Test ID Isolation — Run hundreds of parallel tests with different scenarios against one server. Each test's header routes to its own scenario
  • Runtime Switching — Change scenarios mid-test without restarts (retry flows, error recovery)
  • First-Class Playwright — Dedicated fixtures with type-safe scenarios and automatic test ID handling
  • Response Sequences — Built-in polling, retry flows, state machines
  • Stateful Mocks — Capture request values, inject into responses. State is isolated per test ID, so parallel tests never conflict
  • Advanced Matching — Body, headers, query params, regex with specificity-based selection
  • Framework Adapters — Not thin wrappers—they solve real problems. For example, the Next.js adapter includes built-in singleton protection for the module duplication issue that breaks MSW
  • Developer Tools (Roadmap) — Planned browser-based plugin for switching scenarios during development and debugging—making scenario exploration instant and visual
AspectScenaristPlaywright Mocks
Intercepts atServer (Node.js)Browser
Server Components✓ Full support✗ Cannot intercept
API Routes✓ Full support✗ Cannot intercept
Client-side fetch✓ Yes✓ Yes
SetupFramework adapterBuilt into Playwright
Test isolationPer-test via test IDPer-page
HAR recordingNot supported✓ Built-in
Next.js 15 testProxyNot needed (built-in)✓ Experimental support

The Critical Difference: Where Requests Originate

Section titled “The Critical Difference: Where Requests Originate”
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Browser Server (Node.js) │
│ ──────── ───────────────── │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Client │ HTTP Request │ Server Component │ │
│ │ Component │ ──────────────► │ │ │
│ │ │ │ const data = await │ │
│ │ onClick: │ │ fetch('https:// │ │
│ │ fetch('/api') │ api.stripe.com') │ │
│ │ │ │ │ │
│ └──────┬───────┘ └──────────┬───────────┘ │
│ │ │ │
│ │ ◄── Playwright │ │
│ │ can intercept │ ◄── Scenarist │
│ │ │ intercepts │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ External API │ │ External API │ │
│ │ (from │ │ (from server) │ │
│ │ browser) │ │ │ │
│ └──────────────┘ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

Playwright’s page.route() intercepts requests that originate in the browser.

Scenarist intercepts requests that originate on the server (Node.js).

For Server Components, API routes, and middleware—the server makes HTTP requests that never touch the browser. Playwright can’t see them.

// app/checkout/page.tsx - Server Component
export default async function CheckoutPage() {
// This fetch runs on the SERVER
const products = await fetch('https://api.stripe.com/v1/products');
const data = await products.json();
return <ProductList products={data.products} />;
}
// checkout.spec.ts
test('displays products', async ({ page, switchScenario }) => {
// Scenarist intercepts the server-side fetch
await switchScenario(page, 'products-available');
await page.goto('/checkout');
// Server Component received mocked Stripe response
await expect(page.locator('.product')).toHaveCount(3);
});

Playwright can record and replay HTTP traffic using HAR (HTTP Archive) files:

// Record HAR during test
await page.routeFromHAR('recording.har', {
url: '**/api/**',
update: true, // Record mode
});
// Replay HAR in subsequent tests
await page.routeFromHAR('recording.har', {
url: '**/api/**',
update: false, // Replay mode
});

Limitation: HAR only captures browser-level requests. Server-side requests (Server Components, API routes) are never recorded because they don’t pass through the browser.

Next.js 15 introduced an experimental testProxy feature that proxies server-side fetch calls, allowing MSW to intercept them:

next.config.js
module.exports = {
experimental: {
testProxy: true,
},
}

Status: Experimental with known limitations—doesn’t intercept internal route handler calls (fetches using relative URLs within the same app), and some font loading issues reported.

Why Scenarist is still valuable: Scenarist provides scenario management, test ID isolation, runtime switching, and Playwright fixtures on top of MSW—features you’d need to build yourself even with testProxy.

Client-side SPAs

React, Vue, Angular apps where all fetches originate in the browser.

Client Components

Next.js Client Components that fetch data on the client side.

Static sites

Pre-rendered pages where dynamic data is fetched client-side.

Zero-config mocking

Built into Playwright—no additional setup required.

Server Components

Next.js/React Server Components that fetch data during SSR.

API Routes

Next.js API routes, Express endpoints that call external APIs.

Middleware

Authentication middleware, edge functions that make HTTP calls.

Mixed architectures

Apps with both client and server-side data fetching.

// components/UserProfile.tsx - Client Component
'use client';
export function UserProfile() {
const [user, setUser] = useState(null);
useEffect(() => {
// Client-side fetch - browser makes the request
fetch('/api/user').then(r => r.json()).then(setUser);
}, []);
return <div>{user?.name}</div>;
}
// profile.spec.ts
test('shows user name', async ({ page }) => {
// ✓ Works - intercepts browser request
await page.route('**/api/user', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ name: 'Test User' })
});
});
await page.goto('/profile');
await expect(page.locator('div')).toContainText('Test User');
});
// app/dashboard/page.tsx - Server Component
export default async function Dashboard() {
// Server-side fetch - runs in Node.js
const analytics = await fetch('https://api.analytics.com/data', {
headers: { 'Authorization': `Bearer ${process.env.ANALYTICS_KEY}` }
});
return <AnalyticsChart data={await analytics.json()} />;
}
// dashboard.spec.ts
test('displays analytics', async ({ page, switchScenario }) => {
// ✓ Scenarist intercepts server-side fetch
await switchScenario(page, 'analytics-data');
await page.goto('/dashboard');
await expect(page.locator('.chart')).toBeVisible();
});

For apps with both client and server-side fetching, you might use both tools:

// Mixed architecture test
test('complete user flow', async ({ page, switchScenario }) => {
// Scenarist handles server-side fetches (Server Components, API routes)
await switchScenario(page, 'user-flow-success');
await page.goto('/dashboard'); // Server Component fetches analytics
// Playwright route for a specific client-side mock
// (e.g., a third-party widget that loads in browser)
await page.route('**/widget.thirdparty.com/**', route => {
route.fulfill({ status: 200, body: '{}' });
});
await page.click('#load-widget');
await expect(page.locator('.widget')).toBeVisible();
});

However, Scenarist can handle both client and server-side requests, so you typically don’t need Playwright mocks if you’re already using Scenarist.

Are you testing Server Components, API routes, or middleware?

  • Yes → Use Scenarist (Playwright mocks cannot intercept these)

Is your app a pure client-side SPA?

  • Yes → Playwright mocks may be sufficient

Do you need parallel test isolation with different scenarios?

  • Yes → Use Scenarist (built-in test ID isolation)

Do you want zero additional setup?

  • Yes, for simple cases → Playwright mocks are built-in
  • For comprehensive testing → Scenarist is worth the setup
CapabilityScenaristPlaywright Mocks
Server Components✗ (or experimental testProxy)
API Routes
Middleware
SSR data fetching
Client-side fetch
Zero setupFramework adapter✓ Built-in
Parallel isolation✓ Per-test-IDPer-page
Scenario managementManual
HAR recordingNot supported
Runtime switchingManual

Bottom line: For scenario-based testing of modern server-rendered applications (Next.js App Router, Remix, etc.), Scenarist provides server-side request interception with scenario management and test ID isolation. Playwright mocks are great for pure client-side apps, HAR recording, or supplementing Scenarist for browser-specific needs. Next.js 15’s experimental testProxy may help with server-side interception, but Scenarist still provides the scenario management layer.