Endpoint APIs
Scenarist provides HTTP endpoints for scenario management and debugging:
POST /__scenario__— Switch the active scenario for a testGET /__scenario__— Check which scenario is activeGET /__scenarist__/state— Retrieve test state for debugging
These endpoints are framework-agnostic and work consistently across Express, Next.js, and all supported frameworks.
POST /scenario - Switch Scenario
Section titled “POST /scenario - Switch Scenario”Switch the active scenario for a test ID.
Request
Section titled “Request”Method: POST
URL: /__scenario__ (configurable via endpoints.setScenario)
Headers:
Content-Type: application/jsonx-scenarist-test-id: <test-id>Body:
{ scenario: string; // Required: scenario ID to activate variant?: string; // Optional: variant name within scenario}Responses
Section titled “Responses”Success (200):
{ success: true; testId: string; // The test ID used for routing scenarioId: string; // The activated scenario ID variant?: string; // The variant name (if provided)}Example:
{ "success": true, "testId": "test-abc123", "scenarioId": "payment-error", "variant": "visa-declined"}Validation Error (400):
{ error: string; // Error message details?: unknown; // Validation error details (Zod errors)}Example:
{ "error": "Invalid request body", "details": [ { "code": "invalid_type", "expected": "string", "received": "undefined", "path": ["scenario"], "message": "Required" } ]}Scenario Not Found (400):
{ error: string; // "Scenario 'xyz' not found. Did you forget to register it?"}Internal Server Error (500):
{ error: string; // "Internal server error"}Example Usage
Section titled “Example Usage”With Playwright helpers:
import { test } from '@scenarist/playwright-helpers';
test('my test', async ({ page, switchScenario }) => { // Helper handles POST request automatically await switchScenario(page, 'payment-error'); // Sends POST /__scenario__ with auto-generated test ID});Manual (curl):
curl -X POST http://localhost:3000/__scenario__ \ -H "Content-Type: application/json" \ -H "x-scenarist-test-id: test-123" \ -d '{"scenario": "payment-error"}'Manual (fetch):
const response = await fetch('http://localhost:3000/__scenario__', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-scenarist-test-id': 'test-123', }, body: JSON.stringify({ scenario: 'payment-error' }),});
const result = await response.json();// { success: true, testId: 'test-123', scenarioId: 'payment-error' }With Playwright (supertest):
import request from 'supertest';import app from './app';
const response = await request(app) .post('/__scenario__') .set('x-scenarist-test-id', 'test-123') .send({ scenario: 'payment-error' });
expect(response.status).toBe(200);expect(response.body.scenarioId).toBe('payment-error');GET /scenario - Get Active Scenario
Section titled “GET /scenario - Get Active Scenario”Retrieve the currently active scenario for a test ID.
Request
Section titled “Request”Method: GET
URL: /__scenario__ (configurable via endpoints.getScenario)
Headers:
x-scenarist-test-id: <test-id>No body required.
Responses
Section titled “Responses”Success (200):
{ testId: string; // The test ID scenarioId: string; // The active scenario ID scenarioName?: string; // The scenario's human-readable name (if found)}Example:
{ "testId": "test-abc123", "scenarioId": "payment-error", "scenarioName": "Payment Error Scenarios"}No Active Scenario (404):
{ error: string; // "No active scenario for this test ID" testId: string; // The test ID that was queried}Example:
{ "error": "No active scenario for this test ID", "testId": "test-xyz789"}Example Usage
Section titled “Example Usage”With fetch:
const response = await fetch('http://localhost:3000/__scenario__', { headers: { 'x-scenarist-test-id': 'test-123', },});
const result = await response.json();// { testId: 'test-123', scenarioId: 'payment-error', scenarioName: '...' }With curl:
curl http://localhost:3000/__scenario__ \ -H "x-scenarist-test-id: test-123"With Playwright (supertest):
const response = await request(app) .get('/__scenario__') .set('x-scenarist-test-id', 'test-123');
expect(response.status).toBe(200);expect(response.body.scenarioId).toBe('payment-error');GET /scenarist/state - Debug State
Section titled “GET /scenarist/state - Debug State”Retrieve the current test state for debugging. This endpoint exposes state set by afterResponse.setState() in your mocks, useful for debugging async workflows and verifying state transitions.
Request
Section titled “Request”Method: GET
URL: /__scenarist__/state (configurable via endpoints.getState)
Headers:
x-scenarist-test-id: <test-id>No body required.
Response
Section titled “Response”Success (200):
{ testId: string; // The test ID state: Record<string, unknown>; // Current state object (empty {} if no state)}Example:
{ "testId": "test-abc123", "state": { "cart.items": 2, "cart.total": 59.98, "user.tier": "premium" }}Example Usage
Section titled “Example Usage”With Playwright helpers (recommended):
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');
// Fetch current state const state = await debugState(page); console.log('Cart state:', state); // → { 'cart.items': 1, 'cart.total': 29.99 }
expect(state['cart.items']).toBe(1);});Manual (fetch):
const response = await fetch('http://localhost:3000/__scenarist__/state', { headers: { 'x-scenarist-test-id': 'test-123', },});
const { state } = await response.json();// { 'cart.items': 1, 'cart.total': 29.99 }With curl:
curl http://localhost:3000/__scenarist__/state \ -H "x-scenarist-test-id: test-123"When to Use
Section titled “When to Use”The debug state endpoint is useful for:
- Debugging test failures: See what state was set by mocks during a test run
- Async workflows: Wait for state to reach a condition before asserting (use
waitForDebugState) - State-aware mocking verification: Confirm that
afterResponse.setState()is capturing expected values
Test ID Extraction
Section titled “Test ID Extraction”All endpoints extract the test ID from the request header:
// The header name is standardized to 'x-scenarist-test-id'// Use SCENARIST_TEST_ID_HEADER constant from your adapter packageimport { SCENARIST_TEST_ID_HEADER } from '@scenarist/express-adapter';Test ID behavior:
- Header present: Uses value from header (e.g.,
'test-abc123') - Header missing: Uses default test ID (e.g.,
'default-test') - Empty string: Treated as missing, uses default
Example:
// Request with headerHeaders: { 'x-scenarist-test-id': 'test-123' }// Uses test ID: 'test-123'
// Request without header// Uses test ID: 'default-test'Request Validation
Section titled “Request Validation”The POST /__scenario__ endpoint validates the request body using Zod:
import { z } from 'zod';
const ScenarioRequestSchema = z.object({ scenario: z.string().min(1), // Required, non-empty string variant: z.string().optional(), // Optional string});Valid requests:
{ "scenario": "payment-error" }{ "scenario": "payment-error", "variant": "visa-declined" }Invalid requests:
{} // Missing 'scenario'{ "scenario": "" } // Empty string{ "scenario": 123 } // Wrong typeFramework-Specific Implementations
Section titled “Framework-Specific Implementations”While the API is consistent across frameworks, the implementation differs:
Express
Section titled “Express”// Express endpoint handlerexport const handleSetScenario = (manager: ScenarioManager, config: ScenaristConfig) => { return (req: Request, res: Response): void => { const { scenario, variant } = ScenarioRequestSchema.parse(req.body); const context = new ExpressRequestContext(req, config); const testId = context.getTestId();
const result = manager.switchScenario(testId, scenario, variant);
if (!result.success) { res.status(400).json({ error: result.error.message }); return; }
res.status(200).json({ success: true, testId, scenarioId: scenario, ...(variant && { variant }), }); };};Next.js App Router
Section titled “Next.js App Router”// Next.js App Router endpoint handlerexport const POST = async (req: Request): Promise<Response> => { const body = await req.json(); const context = new AppRequestContext(req, config); const result = await handlePostLogic(body, context, manager);
if (!result.success) { return Response.json( { error: result.error, details: result.details }, { status: result.status } ); }
return Response.json( { success: true, testId: result.testId, scenarioId: result.scenarioId, ...(result.variant && { variant: result.variant }), }, { status: 200 } );};Next.js Pages Router
Section titled “Next.js Pages Router”// Next.js Pages Router endpoint handlerexport default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === 'POST') { const { scenario, variant } = ScenarioRequestSchema.parse(req.body); const context = new PagesRequestContext(req, config); const testId = context.getTestId();
const result = manager.switchScenario(testId, scenario, variant);
if (!result.success) { return res.status(400).json({ error: result.error.message }); }
return res.status(200).json({ success: true, testId, scenarioId: scenario, ...(variant && { variant }), }); }
// GET handler...};Configuration
Section titled “Configuration”Endpoint paths are configurable:
const scenarist = createScenarist({ enabled: true, scenarios,
// Customize endpoint paths endpoints: { setScenario: '/__scenario__', // POST endpoint (default) getScenario: '/__scenario__', // GET endpoint (default) // Or customize: // setScenario: '/__test/switch', // getScenario: '/__test/status', },});Why customize endpoint paths?
- Security: Use non-standard paths in production (if
enabled: trueaccidentally) - Compatibility: Avoid conflicts with existing routes
- Convention: Match your team’s naming standards
Note: The test ID header is standardized to 'x-scenarist-test-id' and is not configurable. Use the SCENARIST_TEST_ID_HEADER constant from your adapter package.
Scenario Manager Coordination
Section titled “Scenario Manager Coordination”Both endpoints delegate to ScenarioManager:
// POST /__scenario__ flowconst result = manager.switchScenario(testId, scenarioId);// 1. Looks up scenario definition by ID// 2. Returns error if not found// 3. Stores active scenario for test ID// 4. Resets sequences and state for test ID// 5. Returns success
// GET /__scenario__ flowconst activeScenario = manager.getActiveScenario(testId);// 1. Retrieves active scenario for test ID// 2. Returns undefined if none setconst scenarioDefinition = manager.getScenarioById(activeScenario.scenarioId);// 3. Looks up full definition for metadataState and Sequence Reset
Section titled “State and Sequence Reset”When switching scenarios, state and sequences are reset:
manager.switchScenario(testId, scenarioId);// Internally calls:// - sequenceTracker.reset(testId) // All sequence positions cleared// - stateManager.reset(testId) // All captured state clearedWhy reset?
- Clean slate for new scenario
- Prevents state bleeding between scenarios
- Ensures idempotent test execution
Example:
// Test 1await switchScenario(page, 'shopping-cart');// Add items, state captured: { cartItems: ['item-1', 'item-2'] }
// Switch to error scenarioawait switchScenario(page, 'payment-error');// State reset: { } (empty)// Sequences reset: all positions → 0Security Considerations
Section titled “Security Considerations”Production Safety:
- When
enabled: false, endpoints return 404 - When
enabled: true, consider:- Custom endpoint paths (non-standard)
- Network-level blocking (firewall)
- Environment-based enablement (never
truein production)
Test Isolation:
- Each test ID is isolated
- No cross-test interference
- Test IDs should be unique (use UUIDs)
Header Validation:
- Test ID header validated (
x-scenarist-test-id) - Missing header → uses default test ID
- Empty header → uses default test ID
Error Handling
Section titled “Error Handling”All endpoints return structured errors:
// Validation error{ error: 'Invalid request body', details: [/* Zod validation errors */]}
// Scenario not found{ error: "Scenario 'xyz' not found. Did you forget to register it?"}
// No active scenario{ error: 'No active scenario for this test ID', testId: 'test-123'}
// Internal error{ error: 'Internal server error'}Best practice: Always check response status before parsing body.
Complete Example
Section titled “Complete Example”Full workflow with both endpoints:
import { test } from '@scenarist/playwright-helpers';import { scenarios } from './scenarios';
export const testWithScenarios = test.extend({ scenarios,});
testWithScenarios('payment flow', async ({ page, switchScenario }) => { // 1. Switch to payment success scenario const testId = await switchScenario(page, 'payment-success'); // POST /__scenario__ called with: // { scenario: 'payment-success' } // Response: { success: true, testId: 'test-abc123', scenarioId: 'payment-success' }
// 2. Verify active scenario (optional) const response = await page.request.get('http://localhost:3000/__scenario__', { headers: { 'x-scenarist-test-id': testId }, }); const activeScenario = await response.json(); // { testId: 'test-abc123', scenarioId: 'payment-success', scenarioName: '...' }
// 3. Test with success scenario await page.goto('/checkout'); await page.click('button:has-text("Pay")'); await expect(page.getByText('Payment successful')).toBeVisible();
// 4. Switch to error scenario await switchScenario(page, 'payment-error'); // POST /__scenario__ called with: // { scenario: 'payment-error' } // Sequences and state reset
// 5. Test with error scenario await page.goto('/checkout'); await page.click('button:has-text("Pay")'); await expect(page.getByText('Payment failed')).toBeVisible();});Next Steps
Section titled “Next Steps”- Writing Scenarios → - Learn the complete scenario structure
- Default Scenarios → - Understand override behavior
- Ephemeral Endpoints → - Understand test-only activation