npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@scenarist/playwright-helpers

v0.4.3

Published

Playwright test helpers for Scenarist scenario management

Downloads

2,863

Readme

@scenarist/playwright-helpers

npm version License: MIT

Playwright test helpers for Scenarist scenario management with guaranteed test isolation.

📖 Documentation

Playwright Integration Guide — Fixtures, type-safe scenarios, test isolation

| Topic | Link | | ----------------------- | ------------------------------------------------------------------------------------------------ | | Why Scenarist? | scenarist.io/getting-started/why-scenarist | | Parallel Testing | scenarist.io/testing/parallel-testing | | Tool Comparison | scenarist.io/comparison | | Writing Scenarios | scenarist.io/scenarios/basic-structure | | Request Matching | scenarist.io/scenarios/request-matching | | Response Sequences | scenarist.io/scenarios/response-sequences | | State-Aware Mocking | scenarist.io/scenarios/state-aware-mocking | | Logging & Debugging | scenarist.io/reference/logging |

Installation

# npm
npm install -D @scenarist/playwright-helpers

# pnpm
pnpm add -D @scenarist/playwright-helpers

# yarn
yarn add -D @scenarist/playwright-helpers

Quick Start (Recommended: Fixtures API)

The fixtures API is the recommended way to use Scenarist with Playwright. It provides:

  • ✅ Guaranteed unique test IDs (no collisions, even with parallel execution)
  • ✅ Configuration in one place (no repetition across tests)
  • ✅ Clean composition with your existing fixtures
  • ✅ Type-safe with full TypeScript support

1. Configure in playwright.config.ts

import { defineConfig } from "@playwright/test";
import type { ScenaristOptions } from "@scenarist/playwright-helpers";

export default defineConfig<ScenaristOptions>({
  use: {
    baseURL: "http://localhost:3000", // Standard Playwright config
    scenaristEndpoint: "/api/__scenario__", // Scenarist-specific config
  },
});

Note: The <ScenaristOptions> type parameter enables TypeScript to recognize scenaristEndpoint as a valid configuration option.

2. Create a Fixtures File

Create tests/fixtures.ts with your scenarios:

// tests/fixtures.ts
import { withScenarios, expect } from "@scenarist/playwright-helpers";
import { scenarios } from "../lib/scenarios"; // Your scenario definitions

export const test = withScenarios(scenarios);
export { expect };

3. Use in Tests

// tests/my-test.spec.ts
import { test, expect } from "./fixtures"; // Import from YOUR fixtures file

test("premium user sees premium pricing", async ({ page, switchScenario }) => {
  // Configuration read from playwright.config.ts - no repetition!
  await switchScenario(page, "premiumUser"); // Type-safe! Autocomplete works

  await page.goto("/");
  await expect(page.getByRole("heading", { name: "Premium" })).toBeVisible();
});

That's it! No manual test ID generation, no repeating baseURL/endpoint, guaranteed test isolation.

Composing with Your Existing Fixtures

If your team already has custom Playwright fixtures, you can easily compose them with Scenarist fixtures:

// tests/fixtures.ts
import { withScenarios, expect } from "@scenarist/playwright-helpers";
import { scenarios } from "../lib/scenarios";
import type { Page } from "@playwright/test";

type MyFixtures = {
  authenticatedPage: Page;
  database: Database;
};

export const test = withScenarios(scenarios).extend<MyFixtures>({
  authenticatedPage: async ({ page }, use) => {
    // Your custom fixture logic
    await page.goto("/login");
    await page.fill("[name=email]", "[email protected]");
    await page.fill("[name=password]", "password");
    await page.click("button[type=submit]");
    await use(page);
  },

  database: async ({}, use) => {
    const db = await connectToTestDatabase();
    await use(db);
    await db.close();
  },
});

export { expect };

Now use your extended test object:

// tests/my-test.spec.ts
import { test, expect } from "./fixtures";

test("authenticated premium user flow", async ({
  authenticatedPage,
  switchScenario,
  database,
}) => {
  // All fixtures available: yours + Scenarist's
  await switchScenario(authenticatedPage, "premiumUser");
  await authenticatedPage.goto("/dashboard");

  const user = await database.getUser("[email protected]");
  expect(user.tier).toBe("premium");
});

Type-Safe Scenario IDs (Automatic)

Type safety is automatic when you use withScenarios(scenarios). The function infers scenario IDs from your scenarios object.

1. Define Scenarios

// lib/scenarios.ts
import type { ScenaristScenarios } from '@scenarist/core';

export const scenarios = {
  cartWithState: { id: 'cartWithState', name: 'Cart with State', mocks: [...] },
  premiumUser: { id: 'premiumUser', name: 'Premium User', mocks: [...] },
  standardUser: { id: 'standardUser', name: 'Standard User', mocks: [...] },
} as const satisfies ScenaristScenarios;

2. Create Typed Test Object

// tests/fixtures.ts
import { withScenarios, expect } from "@scenarist/playwright-helpers";
import { scenarios } from "../lib/scenarios";

// Type-safe test object - scenario IDs are inferred automatically!
export const test = withScenarios(scenarios);
export { expect };

3. Use with Full Autocomplete

// tests/my-test.spec.ts
import { test, expect } from "./fixtures";

test("my test", async ({ page, switchScenario }) => {
  await switchScenario(page, "cart"); // ❌ TypeScript error: not a valid scenario
  await switchScenario(page, "cartWithState"); // ✅ Autocomplete works!
  //                            ^
  //                            Autocomplete shows all valid scenario IDs
});

Benefits:

  • ✅ Autocomplete shows all valid scenario names
  • ✅ TypeScript errors for typos or invalid scenarios
  • ✅ Type stays in sync with actual scenarios (single source of truth)
  • ✅ Works seamlessly with fixture composition

Advanced: Per-Test Configuration Overrides

Most tests use the global config, but you can override for specific tests:

test("staging environment test", async ({ page, switchScenario }) => {
  await switchScenario(page, "myScenario", {
    baseURL: "https://staging.example.com", // Override for this test only
    endpoint: "/api/custom-endpoint",
  });

  await page.goto("/");
  // Test against staging environment
});

Advanced: Standalone switchScenario Function

For cases where you need manual control over test IDs or can't use fixtures:

import { test, expect } from "@playwright/test";
import { switchScenario } from "@scenarist/playwright-helpers";

test("premium user scenario", async ({ page }) => {
  // Switch to premium scenario (generates unique test ID, sets headers automatically)
  await switchScenario(page, "premiumUser", {
    baseURL: "http://localhost:3000",
    endpoint: "/api/__scenario__",
  });

  // Navigate and test as premium user
  await page.goto("/");
  await expect(page.locator(".premium-badge")).toBeVisible();
});

Note on testing: This package has comprehensive behavior-driven tests at the package level. This is NOT unit testing - we test observable behavior through the public API only. See Testing Philosophy below for full rationale.

Options

type SwitchScenarioOptions = {
  readonly baseURL: string; // Base URL of your application
  readonly endpoint?: string; // Scenario endpoint path or absolute URL (default: '/__scenario__')
  readonly testIdHeader?: string; // Test ID header name (default: 'x-scenarist-test-id')
  readonly variant?: string; // Optional scenario variant
};

Tip: Use an absolute URL for endpoint when your API runs on a different host/port than the frontend. See Cross-Origin API Servers.

What it does

The switchScenario helper:

  1. Generates a unique test ID (test-{scenarioId}-{timestamp})
  2. POSTs to the scenario endpoint with the test ID header
  3. Verifies the scenario switch succeeded (200 response)
  4. Sets the test ID header for all subsequent requests in the test

This reduces scenario switching from 9 lines of boilerplate to 2 lines:

Without helper (9 lines):

const testId = `test-premium-${Date.now()}`;
const response = await page.request.post(
  "http://localhost:3000/api/__scenario__",
  {
    headers: { "x-scenarist-test-id": testId },
    data: { scenario: "premiumUser" },
  },
);
expect(response.status()).toBe(200);
await page.setExtraHTTPHeaders({ "x-scenarist-test-id": testId });

With helper (2 lines):

await switchScenario(page, "premiumUser", {
  baseURL: "http://localhost:3000",
  endpoint: "/api/__scenario__",
});

Code reduction: 77%

API Reference

Fixtures API (Recommended)

withScenarios(scenarios)

Creates a type-safe Playwright test object with Scenarist fixtures.

// tests/fixtures.ts
import { withScenarios, expect } from "@scenarist/playwright-helpers";
import { scenarios } from "../lib/scenarios";

export const test = withScenarios(scenarios);
export { expect };
// tests/my-test.spec.ts
import { test, expect } from "./fixtures";

test("my test", async ({ page, switchScenario, scenaristTestId }) => {
  // Your test code
});

Fixtures provided:

  • switchScenario(page, scenarioId, options?) - Switch to a scenario (auto-injects test ID)
  • scenaristTestId - Unique test ID for this test (usually don't need to access directly)
  • debugState(page) - Get current test state from debug endpoint (useful for debugging)
  • waitForDebugState(page, condition, options?) - Wait for state to meet a condition

expect

Re-exported from @playwright/test for convenience:

import { withScenarios, expect } from "@scenarist/playwright-helpers";

Configuration Options

Set in playwright.config.ts:

export default defineConfig({
  use: {
    baseURL: "http://localhost:3000", // Standard Playwright (used by switchScenario)
    scenaristEndpoint: "/api/__scenario__", // Scenarist endpoint path (default: '/api/__scenario__')
  },
});

Available options:

  • scenaristEndpoint?: string - The endpoint path or absolute URL for scenario switching (default: '/api/__scenario__')
  • scenaristStateEndpoint?: string - The endpoint path for debug state inspection (default: '/__scenarist__/state')

Cross-Origin API Servers

When your API server runs on a different host or port than your frontend, use an absolute URL for scenaristEndpoint:

// Frontend: http://localhost:3000
// API Server: http://localhost:9090
export default defineConfig<ScenaristOptions>({
  use: {
    baseURL: "http://localhost:3000", // Frontend URL (for Playwright navigation)
    scenaristEndpoint: "http://localhost:9090/__scenario__", // Absolute URL to API
  },
});

How it works:

  • Relative paths (e.g., /api/__scenario__) are prepended with baseURL
  • Absolute URLs (starting with http:// or https://) are used directly, ignoring baseURL

This is useful when:

  • Your API and frontend are separate services on different ports
  • You're testing against a staging/production API endpoint
  • Your test infrastructure uses a separate mock server

Debug State Fixtures

When testing multi-stage flows with state-aware mocking, you may need to inspect or wait for internal state changes. The debugState and waitForDebugState fixtures help debug test failures and verify state mutations.

debugState (Fixture)

Fetches the current test state from the debug endpoint. Useful for debugging and verifying afterResponse.setState worked correctly.

import { test, expect } from "./fixtures";

test("loan application flow", async ({ page, switchScenario, debugState }) => {
  await switchScenario(page, "loanApplication");

  // Before any actions - state should be empty
  const initialState = await debugState(page);
  expect(initialState).toEqual({});

  // Navigate and submit application
  await page.goto("/apply");
  await page.getByRole("button", { name: "Submit" }).click();

  // Verify state was set by afterResponse.setState
  const state = await debugState(page);
  expect(state.submitted).toBe(true);
  expect(state.phase).toBe("review");
});

When to use debugState:

  • Debugging failing tests ("why is the response wrong?")
  • Verifying afterResponse.setState worked correctly
  • Understanding state progression through multi-step flows
  • Testing conditional afterResponse behavior

waitForDebugState (Fixture)

Waits for the test state to meet a condition. Useful for async flows where state changes after a delay.

import { test, expect } from "./fixtures";

test("async approval flow", async ({
  page,
  switchScenario,
  waitForDebugState,
}) => {
  await switchScenario(page, "asyncApproval");
  await page.goto("/dashboard");

  // Trigger async approval process
  await page.getByRole("button", { name: "Request Approval" }).click();

  // Wait for state to indicate approval completed
  const state = await waitForDebugState(
    page,
    (s) => s.approved === true,
    { timeout: 10000 }, // Wait up to 10 seconds
  );

  expect(state.approved).toBe(true);
  expect(state.approvedBy).toBeDefined();
});

Parameters:

  • page: Page - Playwright Page object
  • condition: (state: Record<string, unknown>) => boolean - Function that returns true when state is ready
  • options?: { timeout?: number; interval?: number } - Optional configuration
    • timeout - Maximum time to wait in ms (default: 5000)
    • interval - Polling interval in ms (default: 100)

Throws: Error if condition is not met within timeout.

Setting Up the Debug Endpoint

The debug fixtures require a debug state endpoint in your application. Create it alongside your scenario endpoint:

Pages Router:

// pages/api/__scenarist__/state.ts
import { scenarist } from "@/lib/scenarist";

export default scenarist?.createStateEndpoint();

App Router:

// app/api/%5F%5Fscenarist%5F%5F/state/route.ts
import { scenarist } from "@/lib/scenarist";

const handler = scenarist?.createStateEndpoint();
export const GET = handler;

Express:

// The state endpoint is automatically included in scenarist.middleware
// No additional setup needed!

switchScenario (Fixture)

Switch to a scenario using the automatically generated test ID.

await switchScenario(page, scenarioId, options?)

Parameters:

  • page: Page - Playwright Page object
  • scenarioId: string - The scenario to switch to
  • options?: { baseURL?: string; endpoint?: string } - Optional overrides (rarely needed)

What it does:

  1. Reads baseURL from Playwright config (or uses override)
  2. Reads scenaristEndpoint from Playwright config (or uses override)
  3. Generates unique test ID automatically (via scenaristTestId fixture)
  4. POSTs to scenario endpoint with test ID header
  5. Verifies scenario switch succeeded
  6. Sets test ID header for all subsequent requests

Standalone API (Advanced)

switchScenario (Function)

For manual test ID control:

import { switchScenario } from "@scenarist/playwright-helpers";

await switchScenario(page, scenarioId, {
  baseURL: "http://localhost:3000",
  endpoint: "/api/__scenario__",
  testId: "my-custom-test-id", // Manual test ID
});

Use this only when:

  • You need to share test IDs across multiple tests
  • You're integrating with existing test infrastructure that provides test IDs
  • You can't use Playwright fixtures for some reason

⚠️ Warning: Manual test IDs can cause collisions in parallel execution. The fixture API is safer.

Common Pitfalls

❌ Don't: Switch scenarios after navigation

import { test, expect } from "./fixtures"; // Import from YOUR fixtures file

test("bad example", async ({ page, switchScenario }) => {
  await page.goto("/"); // BAD - Navigating first
  await switchScenario(page, "premium"); // Headers set too late!
});

Why it fails: Headers set AFTER navigation don't affect the already-loaded page.

Solution: ✅ Switch scenario BEFORE navigating:

import { test, expect } from "./fixtures";

test("good example", async ({ page, switchScenario }) => {
  await switchScenario(page, "premium"); // Set headers first
  await page.goto("/"); // Now requests use test ID header
});

❌ Don't: Forget to configure in playwright.config.ts

// playwright.config.ts - Missing configuration!
export default defineConfig({
  use: {
    // Missing: baseURL and scenaristEndpoint
  },
});

Error: switchScenario won't know where to send requests.

Solution: ✅ Configure in playwright.config.ts:

export default defineConfig({
  use: {
    baseURL: "http://localhost:3000",
    scenaristEndpoint: "/api/__scenario__",
  },
});

❌ Don't: Use standalone switchScenario with manual test IDs

import { switchScenario } from "@scenarist/playwright-helpers";

test("bad example", async ({ page }) => {
  // BAD - Manual test ID risks conflicts
  await switchScenario(page, "premium", {
    baseURL: "http://localhost:3000",
    endpoint: "/api/__scenario__",
    testId: "my-test", // Same ID across parallel tests = collision!
  });
});

Why it fails: Multiple tests with the same ID will interfere with each other in parallel execution.

Solution: ✅ Use the fixture API (auto-generates unique IDs):

import { test, expect } from "./fixtures"; // Import from YOUR fixtures file

test("good example", async ({ page, switchScenario }) => {
  await switchScenario(page, "premium");
  // Generates unique ID automatically: test-abc123-{uuid}
});

Why This Package Exists

Before this helper, switching scenarios in Playwright tests required significant boilerplate:

  1. Generate unique test ID
  2. Construct scenario endpoint URL
  3. Send POST request with test ID header
  4. Verify response status
  5. Set test ID header for all subsequent requests

This 9-line pattern was repeated in every test, making tests verbose and error-prone. The switchScenario helper encapsulates this pattern into a single function call, reducing code by 77% while improving readability and maintainability.

Documentation

📖 Full Documentation - Complete guides, API reference, and examples.

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

MIT