@frontend-testing/server-stubs
v0.0.1-alpha.4
Published
Decoupled MSW server management and JSON response stubbing for testing
Maintainers
Readme
@frontend-testing/server-stubs
Decoupled MSW stubbing for Vitest. One-liner JSON response mocks with built-in request spies.
Features
- 🎯 Zero Boilerplate - One-liner to stub any endpoint with automatic request capture
- 🔍 Built-in Spies - Track all requests with parsed bodies, headers, and params
- 🔄 Sequential Responses - Simulate retries, pagination, or state changes
- 🎭 Dynamic Responses - Compute responses based on request data
- 📦 TypeScript - Full type support
Installation
npm install -D @frontend-testing/server-stubsPeer Dependencies: msw >= 2.0.0, vitest >= 3.0.0
Quick Start
1. Configure MSW Server
// src/test/setup.ts
import { setupWorker } from "msw/browser"; // or setupServer from "msw/node"
import { serverManager } from "@frontend-testing/server-stubs";
import { beforeAll, afterEach, afterAll } from "vitest";
const server = setupWorker();
serverManager.setDefaultServerLoader(() => server);
beforeAll(() => server.start({ onUnhandledRequest: "warn" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.stop());2. Use in Tests
import { stubJsonResponse } from "@frontend-testing/server-stubs";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
describe("LoginForm", () => {
it("sends credentials and handles success", async () => {
const { spy } = stubJsonResponse({
path: "*/api/auth/login",
method: "POST",
response: { token: "jwt-token", user: { id: 1, name: "John" } },
});
render(<LoginForm />);
await userEvent.type(screen.getByLabelText("Email"), "[email protected]");
await userEvent.type(screen.getByLabelText("Password"), "secret123");
await userEvent.click(screen.getByRole("button", { name: "Login" }));
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].body).toEqual({
email: "[email protected]",
password: "secret123",
});
});
it("displays error on 401", async () => {
stubJsonResponse({
path: "*/api/auth/login",
method: "POST",
response: { error: "Invalid credentials" },
status: 401,
});
render(<LoginForm />);
await userEvent.click(screen.getByRole("button", { name: "Login" }));
await screen.findByText("Invalid credentials");
});
});API
stubJsonResponse(options)
Creates a handler that intercepts matching requests and returns a JSON response.
const { spy } = stubJsonResponse({
path: "*/api/users", // URL pattern (supports MSW wildcards)
method: "POST", // GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, ALL
response: { id: 1 }, // Static value or (ctx) => value
status: 200, // Static number or (ctx) => number
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| path | string | required | URL pattern to match |
| method | HttpMethod | "GET" | HTTP method |
| response | unknown \| (ctx) => unknown | undefined | Response body |
| status | number \| (ctx) => number | 200 | HTTP status code |
| responses | SequentialResponse[] | - | Sequential responses |
Request Spy
Every stub returns a spy that captures request details:
const { spy } = stubJsonResponse({ path: "*/api/users", response: [] });
await fetch("/api/users?page=1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John" }),
});
const call = spy.mock.calls[0][0];
call.url; // Full request URL
call.method; // "POST"
call.body; // { name: "John" } (auto-parsed)
call.headers; // { "content-type": "application/json", ... }
call.params; // { id: "123" } for /users/:id patterns
call.request; // Original Request objectBody Parsing
Request bodies are automatically parsed based on Content-Type:
| Content-Type | Parsed As |
|--------------|-----------|
| application/json | JavaScript object |
| application/x-www-form-urlencoded | Object from key-value pairs |
| multipart/form-data | Object with File instances |
| No header / Unknown | Attempts JSON, falls back to raw text |
Dynamic Responses
Use functions to compute responses based on request data:
stubJsonResponse({
path: "*/api/echo",
method: "POST",
response: (ctx) => ({ received: ctx.body }),
status: (ctx) => ctx.body?.valid ? 200 : 400,
});Response Context:
interface ResponseContext {
url: string;
method: string;
body?: unknown;
headers: Record<string, string>;
params?: Record<string, string>;
callIndex: number; // 0-based call counter
}Sequential Responses
Simulate retry logic or state changes across multiple calls:
stubJsonResponse({
path: "*/api/flaky",
responses: [
{ response: { error: "Service unavailable" }, status: 503 },
{ response: { error: "Service unavailable" }, status: 503 },
{ response: { data: "Success!" }, status: 200 },
],
});
// 1st call → 503
// 2nd call → 503
// 3rd+ calls → 200 (repeats last response)serverManager
Singleton for MSW server management. Configure once, use stubs anywhere.
import { serverManager } from "@frontend-testing/server-stubs";
// Set server directly
serverManager.setServer(myServer);
// Or lazy load (recommended)
serverManager.setDefaultServerLoader(() => myServer);
// Access
serverManager.getServer();
serverManager.hasServer();
serverManager.reset();Advanced Examples
File Uploads
const { spy } = stubJsonResponse({
path: "*/api/upload",
method: "POST",
response: { id: "file-123" },
});
const formData = new FormData();
formData.append("file", new File(["content"], "doc.pdf"));
await fetch("/api/upload", { method: "POST", body: formData });
const body = spy.mock.calls[0][0].body as Record<string, unknown>;
expect(body.file).toBeInstanceOf(File);Catch-All Handler
stubJsonResponse({
path: "*/api/flexible",
method: "ALL",
response: (ctx) => ({ method: ctx.method }),
});Multiple Endpoints
const usersSpy = stubJsonResponse({
path: "*/api/users",
response: [{ id: 1, name: "John" }],
});
const statsSpy = stubJsonResponse({
path: "*/api/stats",
response: { visits: 1000 },
});TypeScript
All types are exported:
import type {
HttpMethod,
RequestCall,
ResponseContext,
ResponseProvider,
SequentialResponse,
StubJsonResponseOptions,
MswServer,
} from "@frontend-testing/server-stubs";How It Works
This library wraps MSW's request handlers with a cleaner API and automatic request tracking. When you call stubJsonResponse(), it:
- Creates an MSW handler for the specified path/method
- Parses incoming request bodies based on
Content-Type - Records all requests to a Vitest spy
- Returns the configured response (static, dynamic, or sequential)
The serverManager decouples MSW server setup from test files, so you configure it once in your test setup and use stubs anywhere without importing the server instance.
License
MIT
