{"id":35313569,"url":"https://github.com/citypaul/scenarist","last_synced_at":"2026-05-08T22:04:47.948Z","repository":{"id":326961643,"uuid":"1080068753","full_name":"citypaul/scenarist","owner":"citypaul","description":"E2E testing for Node.js with instant scenario switching. Run your real app—mock only external APIs. Express \u0026 Next.js adapters.","archived":false,"fork":false,"pushed_at":"2026-05-08T15:09:11.000Z","size":6201,"stargazers_count":25,"open_issues_count":22,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-08T16:28:50.981Z","etag":null,"topics":["e2e-testing","express","integration-testing","mock-service-worker","msw","nextjs","nodejs","playwright","react-server-components","testing","testing-tools","typescript"],"latest_commit_sha":null,"homepage":"https://scenarist.io","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/citypaul.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-10-20T20:24:24.000Z","updated_at":"2026-05-06T12:56:26.000Z","dependencies_parsed_at":"2026-01-01T21:07:59.296Z","dependency_job_id":null,"html_url":"https://github.com/citypaul/scenarist","commit_stats":null,"previous_names":["citypaul/scenarist"],"tags_count":157,"template":false,"template_full_name":null,"purl":"pkg:github/citypaul/scenarist","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citypaul%2Fscenarist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citypaul%2Fscenarist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citypaul%2Fscenarist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citypaul%2Fscenarist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/citypaul","download_url":"https://codeload.github.com/citypaul/scenarist/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/citypaul%2Fscenarist/sbom","scorecard":{"id":1240264,"data":{"date":"2025-12-07T10:52:40Z","repo":{"name":"github.com/citypaul/scenarist","commit":"f803fe05e3fdb957f19f92b5593cc13c21873fed"},"scorecard":{"version":"v5.3.0","commit":"c22063e786c11f9dd714d777a687ff7c4599b600"},"score":7.4,"checks":[{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: Dependabot: .github/dependabot.yml:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dependency-update-tool"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"project was created within the last 90 days. Please review its contents carefully","details":["Warn: Repository was created within the last 90 days."],"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#maintained"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: SECURITY.md:1","Info: Found linked content: SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: SECURITY.md:1","Info: Found text in security policy: SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#security-policy"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#binary-artifacts"}},{"name":"Token-Permissions","score":10,"reason":"GitHub workflow tokens follow principle of least privilege","details":["Info: jobLevel 'contents' permission set to 'read': .github/workflows/claude-code-review.yml:25","Info: jobLevel 'pull-requests' permission set to 'read': .github/workflows/claude-code-review.yml:26","Info: jobLevel 'issues' permission set to 'read': .github/workflows/claude-code-review.yml:27","Info: jobLevel 'issues' permission set to 'read': .github/workflows/claude.yml:27","Info: jobLevel 'actions' permission set to 'read': .github/workflows/claude.yml:29","Info: jobLevel 'contents' permission set to 'read': .github/workflows/claude.yml:25","Info: jobLevel 'pull-requests' permission set to 'read': .github/workflows/claude.yml:26","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql.yml:24","Info: jobLevel 'packages' permission set to 'read': .github/workflows/codeql.yml:22","Info: jobLevel 'actions' permission set to 'read': .github/workflows/codeql.yml:23","Warn: jobLevel 'contents' permission set to 'write': .github/workflows/pre-release.yml:21","Warn: jobLevel 'contents' permission set to 'write': .github/workflows/release.yml:23","Info: jobLevel 'contents' permission set to 'read': .github/workflows/semgrep.yml:25","Info: topLevel 'contents' permission set to 'read': .github/workflows/ci.yml:14","Info: topLevel 'contents' permission set to 'read': .github/workflows/claude-code-review.yml:14","Info: topLevel 'contents' permission set to 'read': .github/workflows/claude.yml:14","Info: topLevel 'contents' permission set to 'read': .github/workflows/cleanup-pr-workers.yml:12","Info: topLevel 'pull-requests' permission set to 'read': .github/workflows/cleanup-pr-workers.yml:13","Info: topLevel 'contents' permission set to 'read': .github/workflows/codeql.yml:13","Info: topLevel 'contents' permission set to 'read': .github/workflows/dependency-review.yml:8","Info: topLevel 'contents' permission set to 'read': .github/workflows/deploy-docs.yml:23","Info: topLevel 'contents' permission set to 'read': .github/workflows/label-security.yml:20","Info: topLevel 'contents' permission set to 'read': .github/workflows/pre-release.yml:14","Info: topLevel 'contents' permission set to 'read': .github/workflows/release.yml:16","Info: topLevel permissions set to 'read-all': .github/workflows/scorecard.yml:13","Info: topLevel 'contents' permission set to 'read': .github/workflows/secrets-scan.yml:10","Info: topLevel 'contents' permission set to 'read': .github/workflows/semgrep.yml:15"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#token-permissions"}},{"name":"Pinned-Dependencies","score":9,"reason":"dependency not pinned by hash detected -- score normalized to 9","details":["Warn: npmCommand not pinned by hash: .github/workflows/pre-release.yml:90","Warn: npmCommand not pinned by hash: .github/workflows/release.yml:57","Info:  45 out of  45 GitHub-owned GitHubAction dependencies pinned","Info:  13 out of  13 third-party GitHubAction dependencies pinned","Info:   0 out of   2 npmCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#pinned-dependencies"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#license"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#cii-best-practices"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#packaging"}},{"name":"SAST","score":9,"reason":"SAST tool detected but not run on all commits","details":["Info: SAST configuration detected: CodeQL","Warn: 21 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#sast"}},{"name":"Branch-Protection","score":4,"reason":"branch protection is not maximal on development and all release branches","details":["Info: 'allow deletion' disabled on branch 'main'","Info: 'force pushes' disabled on branch 'main'","Info: 'branch protection settings apply to administrators' is required to merge on branch 'main'","Warn: 'stale review dismissal' is disabled on branch 'main'","Warn: branch 'main' does not require approvers","Warn: codeowners review is not required on branch 'main'","Warn: 'last push approval' is disabled on branch 'main'","Info: 'up-to-date branches' is required to merge on branch 'main'","Info: status check found to merge onto on branch 'main'","Info: PRs are required in order to make changes on branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#signed-releases"}},{"name":"Fuzzing","score":10,"reason":"project is fuzzed","details":["Info: TypeScriptPropertyBasedTesting integration found: internal/core/tests/deep-equals.test.ts:2","Info: TypeScriptPropertyBasedTesting integration found: internal/core/tests/fuzz.test.ts:2","Info: TypeScriptPropertyBasedTesting integration found: internal/core/tests/state-condition-evaluator.test.ts:2","Info: TypeScriptPropertyBasedTesting integration found: internal/core/tests/deep-equals.test.ts:2","Info: TypeScriptPropertyBasedTesting integration found: internal/core/tests/fuzz.test.ts:2","Info: TypeScriptPropertyBasedTesting integration found: internal/core/tests/state-condition-evaluator.test.ts:2"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#fuzzing"}},{"name":"Contributors","score":10,"reason":"project has 5 contributing companies or organizations","details":["Info: found contributions from: EqualExperts, NorthOps, What-Good-Looks-Like, newdaycards, pack-software"],"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#contributors"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-869p-cjfg-cm3x"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#vulnerabilities"}},{"name":"CI-Tests","score":10,"reason":"30 out of 30 merged PRs checked by a CI test -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#ci-tests"}}]},"last_synced_at":"2025-12-07T14:53:38.316Z","repository_id":326961643,"created_at":"2025-12-07T14:53:38.317Z","updated_at":"2025-12-07T14:53:38.317Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32799083,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-08T08:22:46.396Z","status":"ssl_error","status_checked_at":"2026-05-08T08:22:45.650Z","response_time":54,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["e2e-testing","express","integration-testing","mock-service-worker","msw","nextjs","nodejs","playwright","react-server-components","testing","testing-tools","typescript"],"created_at":"2025-12-30T18:07:53.160Z","updated_at":"2026-05-08T22:04:47.928Z","avatar_url":"https://github.com/citypaul.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Scenarist 🎭\n\n**Scenario-Based Testing for Modern Web Applications. Switch Backend States Instantly. Run Tests in Parallel.**\n\nTest your Next.js Server Components, Express routes, and API handlers with controlled external API responses. No app restarts. No test conflicts. Built on MSW with runtime scenario management and test ID isolation.\n\nExpress and Next.js adapters available—your real application code runs, only external HTTP calls are mocked.\n\n## What is Scenario-Based Testing?\n\n**Scenario-based testing** is an integration testing approach where your real application code executes while external dependencies (third-party APIs, microservices) return controlled responses. Unlike true end-to-end tests that use zero mocks, scenario-based tests mock only the external services you don't control.\n\n**The key distinction:**\n\n| Testing Approach         | Your Code | External APIs | Best For                                          |\n| ------------------------ | --------- | ------------- | ------------------------------------------------- |\n| **Unit Tests**           | Mocked    | Mocked        | Isolated function logic                           |\n| **Scenario-Based Tests** | Real      | Mocked        | Application behavior with controlled dependencies |\n| **End-to-End Tests**     | Real      | Real          | Full system validation (production-like)          |\n\n**Why \"scenario-based\"?** Because you define complete backend _scenarios_ (success, error, timeout, user tiers) and switch between them at runtime. Each test selects a scenario that describes the complete external API state, enabling comprehensive testing without external dependencies.\n\n[![CI](https://img.shields.io/github/actions/workflow/status/citypaul/scenarist/ci.yml?branch=main\u0026label=CI)](https://github.com/citypaul/scenarist/actions)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/citypaul/scenarist/badge)](https://securityscorecards.dev/viewer/?uri=github.com/citypaul/scenarist)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![Docs](https://img.shields.io/badge/docs-scenarist.io-6366f1)](https://scenarist.io)\n\n[![npm @scenarist/express-adapter](https://img.shields.io/npm/v/@scenarist/express-adapter.svg?label=@scenarist/express-adapter)](https://www.npmjs.com/package/@scenarist/express-adapter)\n[![npm @scenarist/nextjs-adapter](https://img.shields.io/npm/v/@scenarist/nextjs-adapter.svg?label=@scenarist/nextjs-adapter)](https://www.npmjs.com/package/@scenarist/nextjs-adapter)\n[![npm @scenarist/playwright-helpers](https://img.shields.io/npm/v/@scenarist/playwright-helpers.svg?label=@scenarist/playwright-helpers)](https://www.npmjs.com/package/@scenarist/playwright-helpers)\n\n---\n\n## 📖 Documentation\n\n**Full documentation at [scenarist.io](https://scenarist.io)**\n\n| Topic                       | Link                                                                                                                           |\n| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| **Why Scenarist?**          | [scenarist.io/getting-started/why-scenarist](https://scenarist.io/getting-started/why-scenarist)                               |\n| **Quick Start**             | [scenarist.io/getting-started/quick-start](https://scenarist.io/getting-started/quick-start)                                   |\n| **Tool Comparison**         | [scenarist.io/comparison](https://scenarist.io/comparison)                                                                     |\n| **Testing Philosophy**      | [scenarist.io/concepts/philosophy](https://scenarist.io/concepts/philosophy)                                                   |\n| **Architecture**            | [scenarist.io/concepts/architecture](https://scenarist.io/concepts/architecture)                                               |\n| **Express Guide**           | [scenarist.io/frameworks/express/getting-started](https://scenarist.io/frameworks/express/getting-started)                     |\n| **Next.js App Router**      | [scenarist.io/frameworks/nextjs-app-router/getting-started](https://scenarist.io/frameworks/nextjs-app-router/getting-started) |\n| **React Server Components** | [scenarist.io/frameworks/nextjs-app-router/rsc-guide](https://scenarist.io/frameworks/nextjs-app-router/rsc-guide)             |\n| **Parallel Testing**        | [scenarist.io/testing/parallel-testing](https://scenarist.io/testing/parallel-testing)                                         |\n| **Writing Scenarios**       | [scenarist.io/scenarios/basic-structure](https://scenarist.io/scenarios/basic-structure)                                       |\n| **State-Aware Mocking**     | [scenarist.io/scenarios/state-aware-mocking](https://scenarist.io/scenarios/state-aware-mocking)                               |\n\n---\n\n## Test Your Real Application with Mocked External APIs\n\nScenarist lets you write **scenario-based tests** where **your actual application code executes**—Express routes, Next.js Server Components, API handlers, middleware, business logic—all of it runs for real. Only external HTTP calls to third-party services (Stripe, Auth0, SendGrid, AWS) are mocked.\n\n### Why This Matters\n\nTesting full-stack applications is hard:\n\n- **End-to-end tests with real APIs** → Brittle, slow, expensive, hard to test edge cases\n- **Traditional mocking** → Requires app restarts, tests conflict, framework lock-in\n- **MSW alone** → No scenario management, manual setup per test\n\n**Scenarist gives you scenario-based testing where your real code runs:**\n\n✅ **Your application code executes** - Express routes, Next.js Server Components, middleware, business logic—all run for real\n✅ **External APIs return what you need** - Control Stripe, Auth0, SendGrid responses per test scenario\n✅ **Switch scenarios instantly** - Test success, errors, edge cases without restarting your app\n✅ **Tests run in parallel** - Each test gets its own isolated scenario via unique test IDs\n✅ **Express and Next.js adapters** - Works with Server Components, API routes, and traditional backends\n\n### Framework Support\n\n**Available Adapters:**\n\n- **Express** - Full adapter with routes, middleware, error handlers\n- **Next.js** - Full adapter for App Router + Pages Router, Server Components, Server Actions, API Routes\n\n### Real Application, Real Tests\n\n**Example 1: Express API**\n\n```typescript\n// Your actual Express route runs\napp.post(\"/api/checkout\", async (req, res) =\u003e {\n  const { items, userId, tier } = req.body;\n\n  // ✅ Your business logic ACTUALLY EXECUTES\n  const total = calculateTotal(items, tier);\n  const discount = tier === \"premium\" ? 0.2 : 0;\n\n  // ✅ This external API call is mocked by Scenarist\n  const payment = await fetch(\"https://api.stripe.com/v1/charges\", {\n    method: \"POST\",\n    headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` },\n    body: JSON.stringify({ amount: total * (1 - discount) }),\n  });\n\n  const result = await payment.json();\n  res.json({ success: result.status === \"succeeded\" });\n});\n```\n\n**Example 2: Next.js Server Component**\n\n```typescript\n// Your actual Next.js Server Component runs\nexport default async function CheckoutPage({ params }) {\n  // ✅ Your rendering logic ACTUALLY EXECUTES\n\n  // ✅ This external API call is mocked by Scenarist\n  const userResponse = await fetch('https://api.auth0.com/userinfo', {\n    headers: { 'Authorization': `Bearer ${cookies().get('token')}` },\n  });\n  const user = await userResponse.json();\n\n  // ✅ This external API call is also mocked\n  const productsResponse = await fetch('https://api.stripe.com/v1/products');\n  const products = await productsResponse.json();\n\n  return \u003cCheckoutForm user={user} products={products} /\u003e;\n}\n```\n\n**With Scenarist:**\n\n- Your business logic executes (`calculateTotal`, validation, etc.)\n- Your routing and middleware run\n- Your Server Components render on the server\n- Only **external HTTP API calls** (Stripe, Auth0, SendGrid) are mocked\n\n**You're testing the actual application behavior**, not a fake simulation.\n\n---\n\n## The Problem\n\nYou want to thoroughly test your full-stack application, but you face impossible tradeoffs:\n\n### The Pain Points\n\n#### 1. **Scenario Switching Requires App Restarts**\n\n```typescript\n// ❌ Traditional approach - restart app for each scenario\ntest(\"payment succeeds\", async ({ page }) =\u003e {\n  // Start app with success mocks\n  await startApp({ mocks: \"success\" });\n  await page.goto(\"/payment\");\n  // Test happy path\n  await stopApp();\n});\n\ntest(\"payment fails\", async ({ page }) =\u003e {\n  // Restart app with error mocks\n  await startApp({ mocks: \"error\" });\n  await page.goto(\"/payment\");\n  // Test error handling\n  await stopApp();\n});\n```\n\n**Problems:**\n\n- ⏰ Slow tests - restarting the server for each scenario\n- 🐛 Flaky tests - startup timing issues\n- 💸 Expensive CI - more compute time\n\n#### 2. **Parallel Tests Conflict**\n\n```typescript\n// ❌ Tests running in parallel share the same mocks\ntest(\"user A sees success\", async ({ page }) =\u003e {\n  // Sets global mocks to \"success\"\n  setGlobalMocks(\"success\");\n  await page.goto(\"/dashboard\");\n  // But test B might have changed the mocks!\n});\n\ntest(\"user B sees error\", async ({ page }) =\u003e {\n  // Sets global mocks to \"error\"\n  setGlobalMocks(\"error\");\n  // Now test A sees the error mocks too!\n  await page.goto(\"/dashboard\");\n});\n```\n\n**Problems:**\n\n- 🔀 Test isolation broken\n- 🎲 Non-deterministic failures\n- 🚫 Can't run tests in parallel\n\n#### 3. **Framework Lock-In**\n\n```typescript\n// ❌ Your mocking logic is tightly coupled to Express\napp.use((req, res, next) =\u003e {\n  if (req.headers[\"mock-scenario\"] === \"error\") {\n    // Express-specific implementation\n    // Can't reuse across different frameworks\n  }\n});\n```\n\n**Problems:**\n\n- 🔒 Locked into one framework's request/response model\n- 🔄 Code duplication across projects\n- 📦 Can't extract to shared library\n\n---\n\n## The Solution: Scenario-Based Testing with Real Application Execution\n\n**Scenarist** lets you test your real application—Express routes, Next.js Server Components, middleware, business logic—while controlling exactly what external APIs return.\n\n**The key insight:** Your code runs for real. Only external HTTP calls (Stripe, Auth0, SendGrid) are mocked. Switch scenarios at runtime without restarting, and run hundreds of isolated tests in parallel.\n\n**The Architecture:** Built on MSW (Mock Service Worker) and hexagonal design principles for framework independence and extensibility.\n\n### Visual Overview\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                        Playwright Tests                              │\n│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │\n│  │  Test A      │  │  Test B      │  │  Test C      │              │\n│  │  x-scenarist-test-id:  │  │  x-scenarist-test-id:  │  │  x-scenarist-test-id:  │              │\n│  │    \"A\"       │  │    \"B\"       │  │    \"C\"       │              │\n│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘              │\n│         │                  │                  │                       │\n│         ▼                  ▼                  ▼                       │\n│  POST /__scenario__  POST /__scenario__  POST /__scenario__         │\n│  { scenario: \"success\" }  { scenario: \"error\" }  { scenario: \"timeout\" }\n└─────────────┬─────────────┬──────────────┬──────────────────────────┘\n              │             │              │\n              ▼             ▼              ▼\n┌─────────────────────────────────────────────────────────────────────┐\n│                     Scenarist Middleware                             │\n│                                                                       │\n│  ┌─────────────────────────────────────────────────────────────┐   │\n│  │                   Test ID Isolation                          │   │\n│  │                                                               │   │\n│  │  Map\u003ctest-id, scenario\u003e                                      │   │\n│  │  ├─ \"A\" → \"success\"  ──► Apply success mocks for Test A     │   │\n│  │  ├─ \"B\" → \"error\"    ──► Apply error mocks for Test B       │   │\n│  │  └─ \"C\" → \"timeout\"  ──► Apply timeout mocks for Test C     │   │\n│  └─────────────────────────────────────────────────────────────┘   │\n│                                                                       │\n│  ┌─────────────────────────────────────────────────────────────┐   │\n│  │                 MSW Server Integration                       │   │\n│  │                                                               │   │\n│  │  server.use(...scenario.mocks) // Applied per test ID       │   │\n│  └─────────────────────────────────────────────────────────────┘   │\n└───────────────────────────────────────────────────────────────────┬─┘\n                                                                    │\n                                                                    ▼\n                                                            Your Application\n                                                          (Express, Next.js)\n```\n\n### How It Works\n\n```typescript\n// ✅ With Scenarist - switch scenarios at runtime!\ntest(\"payment succeeds\", async ({ page }) =\u003e {\n  // Switch to success scenario - no restart needed!\n  await page.request.post(\"http://localhost:3000/__scenario__\", {\n    headers: { \"x-scenarist-test-id\": \"test-1\" },\n    data: { scenario: \"payment-success\" },\n  });\n\n  await page.goto(\"/payment\");\n  await expect(page.locator(\".success-message\")).toBeVisible();\n});\n\ntest(\"payment fails\", async ({ page }) =\u003e {\n  // Switch to error scenario - runs in parallel with test above!\n  await page.request.post(\"http://localhost:3000/__scenario__\", {\n    headers: { \"x-scenarist-test-id\": \"test-2\" },\n    data: { scenario: \"payment-error\" },\n  });\n\n  await page.goto(\"/payment\");\n  await expect(page.locator(\".error-message\")).toBeVisible();\n});\n\n// Both tests run in parallel, each with their own isolated mocks! 🎉\n```\n\n---\n\n## Key Features\n\n### 🚀 Real Application Code Executes\n\nYour complete application stack executes—Server Components, API routes, middleware, business logic. Test the real user experience, not mocked simulations. Perfect for Next.js Server Components, Express routes, and any Node.js application that calls external HTTP APIs.\n\n### 🎯 Test Isolation with Parallel Execution\n\nEach test gets its own isolated scenario via unique test IDs. Run 100+ tests in parallel without conflicts. Test success paths, error states, and edge cases simultaneously.\n\n### ⚡ Instant Scenario Switching (No Restarts)\n\nSwitch between mock scenarios instantly without restarting your application. No more slow restarts between scenarios.\n\n### 🎭 Mock External APIs Only\n\nMock third-party services (Stripe, Auth0, SendGrid, AWS) while your application code runs normally. Keep test complexity low by only mocking what you don't control.\n\n### 🏗️ Framework Agnostic Architecture\n\nBuilt with hexagonal architecture (ports \u0026 adapters). First-class adapters for Express and Next.js, with the core scenario management working at the HTTP level via MSW. One library for your entire stack.\n\n### 📦 Type-Safe with Full TypeScript Support\n\nStrict TypeScript types for scenarios, configs, and APIs. Catch errors at compile-time. Excellent IntelliSense and autocomplete support.\n\n### 🔧 Next.js Multi-Process Handling (Solved)\n\nNext.js has a [well-documented singleton problem](https://github.com/vercel/next.js/discussions/68572) where modules get bundled multiple times, breaking the singleton pattern. This causes [MSW integration issues](https://github.com/mswjs/msw/issues/1644) with multiple conflicting server instances. Scenarist's Next.js adapter includes built-in singleton protection using `globalThis` guards—you get a single, stable MSW instance regardless of how Next.js loads your modules. No manual workarounds required.\n\n### 🎨 Scenario Variants for Data-Driven Testing\n\nParameterize scenarios with variants. Test the same flow with different user tiers, payment methods, or feature flags without duplicating scenario definitions.\n\n### 🔌 Built on MSW (Mock Service Worker)\n\nLeverage the power of MSW's battle-tested HTTP interception. Scenarist adds runtime management, test isolation, and framework adapters on top of MSW's solid foundation.\n\n### 🧠 Stateful Mocks for Multi-Step Flows\n\nCapture state from requests and inject it into subsequent responses. Perfect for testing shopping carts, multi-step forms, user sessions, and any flow where responses depend on previous requests. State is isolated per test ID for parallel execution.\n\n### 🎯 Declarative Scenario Definitions\n\nScenarios are **declarative patterns**—they describe WHAT responses to return, not HOW to decide. No imperative functions hiding if/else logic. This makes scenarios inspectable, composable, and easy to maintain. Your test intent is always visible.\n\n---\n\n## Architecture\n\nScenarist uses **Hexagonal Architecture** (Ports \u0026 Adapters) for maximum flexibility:\n\n```\n┌─────────────────────────────────────────────────────────────────────┐\n│                                                                       │\n│                      🎯 CORE (The Hexagon)                           │\n│                   Pure Domain Logic - No Dependencies                │\n│                                                                       │\n│  ┌─────────────────────────────────────────────────────────────┐   │\n│  │  Types (Data Structures)                                     │   │\n│  │  • Scenario                                                  │   │\n│  │  • ScenarioVariant                                           │   │\n│  │  • ActiveScenario                                            │   │\n│  │  • ScenaristConfig                                           │   │\n│  └─────────────────────────────────────────────────────────────┘   │\n│                                                                       │\n│  ┌─────────────────────────────────────────────────────────────┐   │\n│  │  Ports (Interfaces - Behavior Contracts)                     │   │\n│  │  • interface ScenarioManager                                 │   │\n│  │  • interface ScenarioStore                                   │   │\n│  │  • interface RequestContext                                  │   │\n│  └─────────────────────────────────────────────────────────────┘   │\n│                                                                       │\n│  ┌─────────────────────────────────────────────────────────────┐   │\n│  │  Domain (Implementations)                                    │   │\n│  │  • createScenarioManager()                                   │   │\n│  │  • buildConfig()                                             │   │\n│  │  • createScenario()                                          │   │\n│  └─────────────────────────────────────────────────────────────┘   │\n│                                                                       │\n└───────────────────────┬───────────────────────┬──────────────────────┘\n                        │                       │\n                        │                       │\n        ┌───────────────▼─────────┐   ┌────────▼──────────────┐\n        │                          │   │                        │\n        │  📦 ADAPTERS (PRIMARY)   │   │ 📦 ADAPTERS (SECONDARY)│\n        │  Drive the application   │   │  Driven by core        │\n        │                          │   │                        │\n        │  • Express Middleware    │   │  • InMemoryStore       │\n        │  • Next.js Adapter       │   │  • MSW Integration     │\n        │  • Playwright Helpers    │   │                        │\n        │                          │   │                        │\n        └──────────────────────────┘   └────────────────────────┘\n```\n\n### Why Hexagonal?\n\n**Technology Independence**\n\n- ✅ Core logic has zero framework dependencies\n- ✅ Add new framework adapters without changing core\n- ✅ Test domain logic without HTTP frameworks\n\n**Clear Boundaries**\n\n- ✅ Ports define explicit contracts\n- ✅ Adapters can be developed independently\n- ✅ Easy to understand and navigate codebase\n\n**Extensibility**\n\n- ✅ Add new framework adapters without touching core\n- ✅ Extend scenario capabilities in core, all adapters benefit\n- ✅ Community can contribute adapters\n\n**Testability**\n\n- ✅ Test core logic in isolation\n- ✅ Test adapters against port contracts\n- ✅ No mocking needed for pure domain tests\n\n---\n\n## Quick Start\n\n### Installation\n\nChoose your framework adapter:\n\n**Express:**\n\n```bash\n# npm\nnpm install @scenarist/express-adapter msw\n\n# pnpm\npnpm add @scenarist/express-adapter msw\n\n# yarn\nyarn add @scenarist/express-adapter msw\n```\n\n**Next.js:**\n\n```bash\n# npm\nnpm install @scenarist/nextjs-adapter msw\n\n# pnpm\npnpm add @scenarist/nextjs-adapter msw\n\n# yarn\nyarn add @scenarist/nextjs-adapter msw\n```\n\n**Playwright Helpers (for scenario-based browser tests):**\n\n```bash\n# npm\nnpm install -D @scenarist/playwright-helpers\n\n# pnpm\npnpm add -D @scenarist/playwright-helpers\n\n# yarn\nyarn add -D @scenarist/playwright-helpers\n```\n\n### Basic Setup\n\n**1. Create your scenarios**\n\nScenarios are defined as declarative patterns (not MSW handlers with imperative functions):\n\n```typescript\n// scenarios/default.ts\nimport type { ScenaristScenario } from \"@scenarist/express-adapter\";\n\nexport const defaultScenario: ScenaristScenario = {\n  id: \"default\",\n  name: \"Default Scenario\",\n  description: \"Baseline responses for all APIs\",\n  mocks: [\n    {\n      method: \"GET\",\n      url: \"https://api.example.com/user\",\n      response: {\n        status: 200,\n        body: {\n          id: \"123\",\n          name: \"John Doe\",\n          email: \"john@example.com\",\n        },\n      },\n    },\n    {\n      method: \"POST\",\n      url: \"https://api.example.com/payment\",\n      response: {\n        status: 200,\n        body: {\n          success: true,\n          transactionId: \"txn_123\",\n        },\n      },\n    },\n  ],\n};\n```\n\n```typescript\n// scenarios/error-state.ts\nimport type { ScenaristScenario } from \"@scenarist/express-adapter\";\n\nexport const errorState: ScenaristScenario = {\n  id: \"error-state\",\n  name: \"Error State\",\n  description: \"API calls fail with errors\",\n  mocks: [\n    {\n      method: \"GET\",\n      url: \"https://api.example.com/user\",\n      response: {\n        status: 404,\n        body: { error: \"User not found\" },\n      },\n    },\n    {\n      method: \"POST\",\n      url: \"https://api.example.com/payment\",\n      response: {\n        status: 400,\n        body: { error: \"Payment failed\" },\n      },\n    },\n  ],\n};\n```\n\n**2. Set up your Express server**\n\n```typescript\n// server.ts\nimport express from \"express\";\nimport { createScenarist } from \"@scenarist/express-adapter\";\nimport type { ScenaristScenarios } from \"@scenarist/express-adapter\";\nimport { defaultScenario, errorState } from \"./scenarios\";\n\nconst app = express();\napp.use(express.json());\n\n// Create scenarios object\nconst scenarios = {\n  default: defaultScenario,\n  errorState: errorState,\n} as const satisfies ScenaristScenarios;\n\n// Create Scenarist instance (wires everything automatically)\nconst scenarist = createScenarist({\n  enabled: process.env.NODE_ENV === \"test\",\n  scenarios, // All scenarios registered upfront\n  strictMode: false,\n});\n\n// Add Scenarist middleware\nif (process.env.NODE_ENV === \"test\") {\n  app.use(scenarist.middleware);\n}\n\n// Your application routes\napp.get(\"/api/profile\", async (req, res) =\u003e {\n  // This calls external API - MSW intercepts based on active scenario\n  const response = await fetch(\"https://api.example.com/user\");\n  const user = await response.json();\n  res.json(user);\n});\n\nexport { app, scenarist };\n\n// Start server\nif (process.env.NODE_ENV !== \"test\") {\n  app.listen(3000, () =\u003e console.log(\"Server running on port 3000\"));\n}\n```\n\n**3. Write tests**\n\n```typescript\n// tests/payment.test.ts\nimport { describe, it, expect, beforeAll, afterAll } from \"vitest\";\nimport request from \"supertest\";\nimport { app, scenarist } from \"../server\";\n\ndescribe(\"Payment Flow\", () =\u003e {\n  beforeAll(() =\u003e scenarist.start());\n  afterAll(() =\u003e scenarist.stop());\n\n  it(\"should return user data from default scenario\", async () =\u003e {\n    const response = await request(app)\n      .get(\"/api/profile\")\n      .set(\"x-scenarist-test-id\", \"test-default\");\n\n    expect(response.status).toBe(200);\n    expect(response.body.name).toBe(\"John Doe\");\n  });\n\n  it(\"should return error when using error scenario\", async () =\u003e {\n    // Switch to error scenario\n    await request(app)\n      .post(\"/__scenario__\")\n      .set(\"x-scenarist-test-id\", \"test-error\")\n      .send({ scenario: \"error-state\" });\n\n    // Make request - gets error response\n    const response = await request(app)\n      .get(\"/api/profile\")\n      .set(\"x-scenarist-test-id\", \"test-error\");\n\n    expect(response.status).toBe(404);\n    expect(response.body.error).toBe(\"User not found\");\n  });\n});\n\n// Both tests run in parallel! 🚀\n```\n\n---\n\n## Advanced Features\n\n### Custom Configuration\n\n```typescript\nconst scenarios = {\n  default: myDefaultScenario,\n  success: mySuccessScenario,\n  error: myErrorScenario,\n} as const satisfies ScenaristScenarios;\n\nconst scenarist = createScenarist({\n  enabled: process.env.NODE_ENV === \"test\",\n  scenarios,\n  strictMode: false,\n\n  // Customize header names\n  headers: {\n    testId: \"x-my-test-id\",\n  },\n\n  // Customize endpoint paths\n  endpoints: {\n    setScenario: \"/api/test/scenario\",\n    getScenario: \"/api/test/scenario\",\n  },\n});\n```\n\n### Scenario Variants\n\nYou can pass optional variant names when switching scenarios:\n\n```typescript\nawait request(app)\n  .post(\"/__scenario__\")\n  .set(\"x-scenarist-test-id\", \"test-123\")\n  .send({\n    scenario: \"user-scenario\",\n    variant: \"premium-tier\", // Optional variant\n  });\n```\n\n### Checking Active Scenario\n\n```typescript\nconst response = await request(app)\n  .get(\"/__scenario__\")\n  .set(\"x-scenarist-test-id\", \"test-123\");\n\nconsole.log(response.body);\n// {\n//   testId: 'test-123',\n//   scenarioId: 'user-scenario',\n//   scenarioName: 'User Scenario'\n// }\n```\n\n### Stateful Mocks\n\nCapture state from requests and inject it into responses for multi-step flows:\n\n```typescript\n// Define a scenario with state capture and injection\nconst shoppingCartScenario: ScenaristScenario = {\n  id: \"shopping-cart\",\n  name: \"Shopping Cart\",\n  mocks: [\n    {\n      method: \"POST\",\n      url: \"https://api.store.com/cart/add\",\n      captureState: {\n        \"items[]\": \"body.item\", // Append to array\n      },\n      response: {\n        status: 200,\n        body: { success: true },\n      },\n    },\n    {\n      method: \"GET\",\n      url: \"https://api.store.com/cart\",\n      response: {\n        status: 200,\n        body: {\n          items: \"{{state.items}}\", // Inject captured items\n          count: \"{{state.items.length}}\", // Inject array length\n        },\n      },\n    },\n  ],\n};\n\n// Use in tests\ntest(\"shopping cart accumulates items\", async () =\u003e {\n  await request(app)\n    .post(\"/__scenario__\")\n    .set(\"x-scenarist-test-id\", \"cart-1\")\n    .send({ scenario: \"shopping-cart\" });\n\n  // Add items\n  await request(app)\n    .post(\"/api/cart/add\")\n    .set(\"x-scenarist-test-id\", \"cart-1\")\n    .send({ item: \"Apple\" });\n\n  await request(app)\n    .post(\"/api/cart/add\")\n    .set(\"x-scenarist-test-id\", \"cart-1\")\n    .send({ item: \"Banana\" });\n\n  // Get cart - state is injected\n  const response = await request(app)\n    .get(\"/api/cart\")\n    .set(\"x-scenarist-test-id\", \"cart-1\");\n\n  expect(response.body.items).toEqual([\"Apple\", \"Banana\"]);\n  expect(response.body.count).toBe(2);\n});\n```\n\nFor more advanced usage patterns, see the [Express Adapter README](./packages/express-adapter/README.md), [Stateful Mocks Guide](./docs/stateful-mocks.md), or the [Express Example App](./apps/express-example/).\n\n---\n\n## Framework Support\n\n### Express ✅\n\n```typescript\nimport { createScenarist } from \"@scenarist/express-adapter\";\n\nconst scenarist = createScenarist({\n  enabled: true,\n  scenarios,\n});\n\napp.use(scenarist.middleware);\n```\n\nSee the [Express Adapter Documentation](./packages/express-adapter/README.md) for complete usage.\n\n### Next.js ✅\n\n```typescript\n// Pages Router\nimport { createScenarist } from \"@scenarist/nextjs-adapter/pages\";\n\n// App Router\nimport { createScenarist } from \"@scenarist/nextjs-adapter/app\";\n\nconst scenarist = createScenarist({\n  enabled: process.env.NODE_ENV === \"test\",\n  scenarios,\n});\n```\n\nSee the [Next.js Adapter Documentation](./packages/nextjs-adapter/README.md) for complete usage.\n\n### Playwright Helpers ✅\n\n```typescript\n// tests/fixtures.ts\nimport { withScenarios, expect } from \"@scenarist/playwright-helpers\";\nimport { scenarios } from \"../lib/scenarios\";\n\nexport const test = withScenarios(scenarios);\nexport { expect };\n```\n\n```typescript\n// tests/my-test.spec.ts\nimport { test, expect } from \"./fixtures\";\n\ntest(\"my test\", async ({ page, switchScenario }) =\u003e {\n  await switchScenario(page, \"premium-user\");\n  await page.goto(\"/dashboard\");\n  // ...\n});\n```\n\nSee the [Playwright Helpers Documentation](./packages/playwright-helpers/README.md) for complete usage.\n\n---\n\n## Parallel Test Example\n\nEach test switches to a different scenario without restarting the application. Tests run in parallel with isolated state.\n\n```typescript\ntest.describe(\"User Dashboard\", () =\u003e {\n  test(\"shows basic features for standard users\", async ({ page }) =\u003e {\n    await switchScenario(page, \"user-standard\");\n    await page.goto(\"/dashboard\");\n    await expect(page.locator(\".basic-features\")).toBeVisible();\n  });\n\n  test(\"shows advanced features for premium users\", async ({ page }) =\u003e {\n    await switchScenario(page, \"user-premium\");\n    await page.goto(\"/dashboard\");\n    await expect(page.locator(\".advanced-features\")).toBeVisible();\n  });\n\n  test(\"shows upgrade prompt for free users\", async ({ page }) =\u003e {\n    await switchScenario(page, \"user-free\");\n    await page.goto(\"/dashboard\");\n    await expect(page.locator(\".upgrade-prompt\")).toBeVisible();\n  });\n\n  test(\"handles API errors gracefully\", async ({ page }) =\u003e {\n    await switchScenario(page, \"api-error\");\n    await page.goto(\"/dashboard\");\n    await expect(page.locator(\".error-message\")).toBeVisible();\n  });\n\n  test(\"handles slow API responses\", async ({ page }) =\u003e {\n    await switchScenario(page, \"api-slow\");\n    await page.goto(\"/dashboard\");\n    await expect(page.locator(\".loading-spinner\")).toBeVisible();\n  });\n\n  test(\"shows empty state for new users\", async ({ page }) =\u003e {\n    await switchScenario(page, \"user-new\");\n    await page.goto(\"/dashboard\");\n    await expect(page.locator(\".empty-state\")).toBeVisible();\n  });\n});\n\n// Helper function\nasync function switchScenario(page: Page, scenario: string) {\n  await page.request.post(\"http://localhost:3000/__scenario__\", {\n    headers: { \"x-scenarist-test-id\": test.info().testId },\n    data: { scenario },\n  });\n}\n```\n\n**Key benefits:**\n\n- 🔀 **Parallel execution** - tests run simultaneously without conflicts\n- ✅ **Isolated state** - each test has its own scenario via test ID\n- 🚫 **No restarts** - switch scenarios at runtime\n\n---\n\n## Benefits Summary\n\n### For Node.js Developers\n\n✅ **Test Real Application Behavior**\n\n- Your Express/Next.js code actually runs—including Server Components\n- Middleware, routing, business logic—all execute normally\n- Only external HTTP APIs are mocked (Stripe, Auth0, etc.)\n- Catch integration bugs where components interact\n\n✅ **Fast Test Development**\n\n- Switch scenarios instantly\n- No app restarts between tests\n- Test all edge cases without setup overhead\n\n✅ **Better Developer Experience**\n\n- Type-safe APIs with excellent IntelliSense\n- Clear error messages when scenarios fail\n- Works with existing Playwright/Cypress tests\n\n✅ **Framework Flexibility**\n\n- Learn once, use with Express and Next.js\n- Extensible architecture for additional frameworks\n- Future-proof your testing strategy\n\n### For Engineering Teams\n\n✅ **Faster CI/CD**\n\n- Tests run in parallel without conflicts\n- No server restarts between scenarios\n- Efficient use of CI resources\n\n✅ **Ship with Confidence**\n\n- Test more scenarios = fewer production bugs\n- Cover edge cases you couldn't test before\n- Real integration testing, not just units\n\n✅ **Maintainable Test Suites**\n\n- Centralized scenario definitions\n- Reusable across all test files\n- Easy refactoring when APIs change\n\n✅ **Onboard Faster**\n\n- New developers understand tests quickly\n- Clear separation: your code vs. external APIs\n- Comprehensive examples and documentation\n\n### For the Modern Web Ecosystem\n\n✅ **Supports Modern Full-Stack Frameworks**\n\n- Full support for Next.js App Router and Pages Router\n- Works with tRPC, GraphQL, REST\n- Full support for Express\n\n✅ **Open Source \u0026 Extensible**\n\n- MIT licensed—use anywhere\n- Hexagonal architecture—build custom adapters\n- Growing community of contributors\n\n✅ **Production Ready**\n\n- 90%+ test coverage\n- Built with strict TDD\n- Battle-tested architectural patterns\n\n---\n\n## Comparison: Integration Testing Approaches\n\n| Feature                       | Traditional Mocking  | MSW Without Scenarist | Scenarist (MSW + Management) | E2E with Real APIs |\n| ----------------------------- | -------------------- | --------------------- | ---------------------------- | ------------------ |\n| **Your App Code Runs**        | ✅ Yes               | ✅ Yes                | ✅ Yes                       | ✅ Yes             |\n| **External HTTP APIs Mocked** | ✅ Yes               | ✅ Yes                | ✅ Yes                       | ❌ Real            |\n| **Test Express/Next.js**      | ✅ Yes               | ✅ Yes                | ✅ Yes                       | ✅ Yes             |\n| **Server Components**         | ⚠️ Complex mocking   | ✅ Yes                | ✅ Yes                       | ✅ Yes             |\n| **Scenario Switching**        | ⚠️ Restart required  | ⚠️ Restart required   | ✅ Runtime                   | Manual setup       |\n| **Parallel Test Isolation**   | ❌ Conflicts         | ❌ Conflicts          | ✅ Test ID isolation         | ❌ Very hard       |\n| **Framework Adapters**        | ⚠️ DIY per framework | ⚠️ DIY per framework  | ✅ Built-in adapters         | ✅ Yes             |\n| **Type Safety**               | ⚠️ Manual            | ⚠️ Manual             | ✅ Full TypeScript           | ✅ If typed        |\n| **Flakiness**                 | ⚠️ Timing issues     | ⚠️ Timing issues      | ✅ Stable                    | ⚠️ Can be flaky    |\n| **Setup Complexity**          | ⚠️ DIY               | ⚠️ DIY                | ✅ Declarative               | ⚠️ Complex         |\n\n---\n\n## Documentation\n\n📖 **[Full Documentation](https://scenarist.io)** - Complete guides, API reference, and examples.\n\n### Core Concepts\n\n- **[Core Functionality Guide](./docs/core-functionality.md)** - Understanding Scenarist's domain logic (framework-agnostic)\n  - Scenario definitions and mock definitions\n  - Dynamic response system (request matching, response sequences, specificity-based selection)\n  - Test isolation and architecture\n  - Independent of any specific framework or adapter\n\n- **[Stateful Mocks Guide](./docs/stateful-mocks.md)** - Complete guide to stateful mock testing\n  - State capture from request body, headers, and query parameters\n  - Template injection with type preservation\n  - Multi-step flows (shopping carts, forms, sessions)\n  - Advanced patterns and troubleshooting\n\n- **[State API Reference](./docs/api-reference-state.md)** - Quick reference for state features\n  - State capture syntax and examples\n  - Template injection rules\n  - Type preservation behavior\n  - Complete API documentation\n\n### Adapter Documentation\n\n- **[Express Adapter README](./packages/express-adapter/README.md)** - Express-specific usage and setup\n- **[Next.js Adapter README](./packages/nextjs-adapter/README.md)** - Next.js App Router and Pages Router setup\n- **[Playwright Helpers README](./packages/playwright-helpers/README.md)** - Playwright test helpers\n- **[MSW Adapter README](./internal/msw-adapter/README.md)** - MSW integration details (internal)\n\n### Examples\n\n**Internal Examples (`apps/`)** - Used for testing and verifying Scenarist features:\n\n- **[Express Example App](./apps/express-example/)** - Complete working Express application with Scenarist\n  - Scenario definitions: `src/scenarios.ts`\n  - Integration tests: `tests/dynamic-matching.test.ts`, `tests/dynamic-sequences.test.ts`, `tests/stateful-scenarios.test.ts`\n  - Bruno API tests: `bruno/Dynamic Responses/`\n\n**Demo Apps (`demo/`)** - Consumer-facing examples that install Scenarist from npm:\n\n- **[PayFlow Demo](./demo/payflow/)** - Payment integration demo showcasing all Scenarist features (used in promotional videos and blog posts)\n\n### Planning \u0026 Architecture\n\n- **[Dynamic Responses Plan](./docs/plans/dynamic-responses.md)** - Complete implementation plan and requirements\n- **[ADR-0002: Dynamic Response System](./docs/adrs/0002-dynamic-response-system.md)** - Architectural decisions\n\n---\n\n## Contributing\n\nContributions welcome! This project follows Test-Driven Development (TDD) and hexagonal architecture principles.\n\n### Development Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/citypaul/scenarist.git\ncd scenarist\n\n# Install dependencies\npnpm install\n\n# Run tests (TDD!)\npnpm test\n\n# Build all packages\npnpm build\n\n# Run tests in watch mode\npnpm test:watch\n```\n\n### Areas for Contribution\n\n- 🔌 **Framework Adapters** - Fastify, Hono, Koa, Remix (see existing adapters as patterns)\n- 📚 **Documentation** - Examples, tutorials, blog posts\n- 🐛 **Bug Fixes** - Check our [issues](https://github.com/citypaul/scenarist/issues)\n- ✨ **Features** - See existing packages for patterns\n\n---\n\n## Common Use Cases\n\n### 🛒 E-Commerce: Test Checkout with Payment Provider Scenarios\n\n```typescript\n// Your real Express or Next.js API runs\n// Only Stripe API is mocked\n\ntest('successful purchase flow', async ({ request }) =\u003e {\n  await switchScenario(request, 'stripe-success');\n\n  const response = await request.post('http://localhost:3000/api/checkout', {\n    data: { items: [...], userId: '123' }\n  });\n\n  // Your Express route executed:\n  // - Database queries ran\n  // - Business logic (calculateTotal) ran\n  // - Order creation happened\n  // Only Stripe API call was mocked\n\n  expect(response.status()).toBe(200);\n});\n\ntest('declined card flow', async ({ request }) =\u003e {\n  await switchScenario(request, 'stripe-declined');\n  // Tests your error handling, user messaging, retry logic\n});\n\ntest('3D Secure required flow', async ({ request }) =\u003e {\n  await switchScenario(request, 'stripe-3ds-required');\n  // Tests your 3D Secure redirect flow\n});\n```\n\n### 🔐 Auth: Test Login/Signup with Auth Provider Scenarios\n\n```typescript\n// Your real Express or Next.js auth routes run\n// Only Auth0/Clerk API is mocked\n\ntest(\"successful OAuth login\", async ({ page }) =\u003e {\n  await switchScenario(page, \"auth0-success\");\n  // Tests your session creation, redirect logic, user setup\n});\n\ntest(\"OAuth error handling\", async ({ page }) =\u003e {\n  await switchScenario(page, \"auth0-error\");\n  // Tests your error UI, retry logic, fallback behavior\n});\n\ntest(\"email verification flow\", async ({ page }) =\u003e {\n  await switchScenario(page, \"auth0-verify-required\");\n  // Tests your verification UI and redirect handling\n});\n```\n\n### 📧 Transactional Emails: Test Email Sending Scenarios\n\n```typescript\n// Your real Express or Next.js API runs\n// Only SendGrid/Resend API is mocked\n\ntest(\"welcome email sent successfully\", async ({ page }) =\u003e {\n  await switchScenario(page, \"sendgrid-success\");\n  // Tests your signup flow, email queueing, success messaging\n});\n\ntest(\"email rate limit handling\", async ({ page }) =\u003e {\n  await switchScenario(page, \"sendgrid-rate-limit\");\n  // Tests your rate limit error handling, retry logic\n});\n```\n\n### 🤖 AI Features: Test OpenAI/Anthropic API Scenarios\n\n```typescript\n// Your real AI feature code runs\n// Only OpenAI API is mocked\n\ntest(\"AI suggestion generation\", async ({ page }) =\u003e {\n  await switchScenario(page, \"openai-success\");\n  // Tests your prompt engineering, response parsing, UI updates\n});\n\ntest(\"AI timeout handling\", async ({ page }) =\u003e {\n  await switchScenario(page, \"openai-timeout\");\n  // Tests your timeout handling, fallback behavior\n});\n\ntest(\"AI content filtering\", async ({ page }) =\u003e {\n  await switchScenario(page, \"openai-content-filtered\");\n  // Tests your content policy violation handling\n});\n```\n\n### 🗄️ SaaS: Test Multi-Tenant Scenarios\n\n```typescript\n// Your real authorization logic runs\n// Only external API calls are mocked\n\ntest(\"free tier limits\", async ({ page }) =\u003e {\n  await switchScenario(page, \"user-free-tier\");\n  // Tests your feature gates, upgrade prompts, limit enforcement\n});\n\ntest(\"premium tier features\", async ({ page }) =\u003e {\n  await switchScenario(page, \"user-premium-tier\");\n  // Tests advanced features, no limits, premium UI elements\n});\n\ntest(\"enterprise SSO login\", async ({ page }) =\u003e {\n  await switchScenario(page, \"user-enterprise-sso\");\n  // Tests SSO flow, custom branding, enterprise features\n});\n```\n\n---\n\n## FAQ\n\n**Q: Does my application really run, or is it mocked?**\n\nA: **Your application really runs!** Whether it's Express routes or Next.js Server Components—all your application code executes normally. Only external API calls (Stripe, Auth0, AWS, etc.) are mocked by MSW. This is true integration testing.\n\n**Q: Does this work with Express APIs?**\n\nA: Absolutely! Express is a first-class citizen. Your Express routes, middleware, and error handlers all execute normally. Only outgoing HTTP calls to external services are intercepted and mocked.\n\n**Q: What's the difference between this and regular MSW?**\n\nA: MSW provides HTTP mocking. Scenarist adds:\n\n- **Runtime scenario switching** (no app restarts)\n- **Test isolation** via test IDs (parallel tests don't conflict)\n- **Framework adapters** (Express, Next.js)\n- **Type-safe scenario management** (TypeScript first)\n\nThink of it as MSW + scenario management + test orchestration.\n\n**Q: Can I use this with Next.js App Router?**\n\nA: Yes! Scenarist works perfectly with Next.js 13+ App Router, Server Components, Server Actions, and the Pages Router. Your React Server Components execute normally, only external API calls are intercepted.\n\n**Q: Does this work with Remix, Fastify, or other frameworks?**\n\nA: We currently provide adapters for Express and Next.js. More are planned.\n\n**Q: What about tRPC? Does my tRPC router execute?**\n\nA: Yes! Your entire tRPC router, procedures, and middleware execute. Only calls to external services from within your procedures are mocked.\n\n**Q: Can I use this in production?**\n\nA: Scenarist is designed for testing/development. The middleware can be disabled in production via config (`enabled: process.env.NODE_ENV !== 'production'`).\n\n**Q: Does this work with Playwright's built-in mocking?**\n\nA: Yes! Scenarist provides server-side scenario management, which complements Playwright's client-side mocking. Use both together or just Scenarist.\n\n**Q: Can I use this without Playwright?**\n\nA: Absolutely! Scenarist works with Cypress, Puppeteer, Selenium, or any test framework that can make HTTP requests. Even `curl` works!\n\n**Q: What about my database? Does Scenarist help with that?**\n\nA: No. Scenarist only intercepts **external HTTP requests** (Stripe, Auth0, etc.). Database calls are not HTTP requests—they go directly to your database. If your app uses databases, use a test database or tools like Testcontainers. See our [Testing Database Apps](/guides/testing-database-apps) guide for strategies.\n\n**Q: How fast is scenario switching?**\n\nA: \u003c100ms. Just an HTTP POST request. No app restart needed.\n\n**Q: What's the performance overhead per request?**\n\nA: ~1ms per request. Negligible impact on test execution time.\n\n**Q: Does this work with TypeScript?**\n\nA: Yes! Scenarist is written in TypeScript with strict mode. Full type safety for scenarios, configs, and APIs.\n\n**Q: Can I mock GraphQL APIs?**\n\nA: Yes! MSW supports GraphQL mocking. Define your GraphQL mocks in scenarios.\n\n**Q: Does this work with monorepos (Nx, Turborepo)?**\n\nA: Absolutely! Scenarist is built with Turborepo. Perfect for monorepo testing strategies.\n\n**Q: What if I need to test with real external APIs sometimes?**\n\nA: Set `enabled: false` to disable mocking globally, or use `strictMode: false` and create scenarios with selective mocks to allow passthrough for specific endpoints.\n\n---\n\n## Support\n\n- 💬 [GitHub Discussions](https://github.com/citypaul/scenarist/discussions)\n- 🐛 [Issue Tracker](https://github.com/citypaul/scenarist/issues)\n\n---\n\n## License\n\nMIT © [Paul Hammond](https://github.com/citypaul)\n\n---\n\n## Acknowledgments\n\nBuilt with:\n\n- [MSW](https://mswjs.io/) - Mock Service Worker\n- [TypeScript](https://www.typescriptlang.org/) - Type safety\n- [Vitest](https://vitest.dev/) - Testing framework\n- [Turborepo](https://turbo.build/) - Monorepo tooling\n\nInspired by hexagonal architecture patterns and the testing community's need for better scenario-based testing tools.\n\n---\n\n## Star History\n\nIf you find Scenarist useful, please consider giving it a star ⭐ on GitHub!\n\n[![Star History Chart](https://api.star-history.com/svg?repos=citypaul/scenarist\u0026type=Date)](https://star-history.com/#citypaul/scenarist\u0026Date)\n\n---\n\n**Made with ❤️ by the testing community**\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcitypaul%2Fscenarist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcitypaul%2Fscenarist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcitypaul%2Fscenarist/lists"}