msw-scenarios v0.2.0
msw-scenarios
msw-scenarios
is a type-safe preset management system built on top of MSW (Mock Service Worker) 2.x.x. This library enhances MSW with a powerful preset system while maintaining complete TypeScript integration, ensuring that your API mocks are both flexible and type-safe during development and testing.
This library was inspired by the presentation at WOOWACON 2023:
프론트엔드 모킹 환경에 감칠맛 더하기
✨ Key Features
- 🔒 Enhanced Type Safety: Built from the ground up with TypeScript
- 🔄 MSW Compatibility: Works seamlessly with MSW 2.x.x
- 👥 Profile Management: Create and switch between mock scenarios
- 🎮 UI Integration: Build custom UI tools with state management API
- 🛠 Developer Friendly: Simple, intuitive API design
📦 Installation
npm install msw-scenarios msw
# or
pnpm add msw-scenarios msw
# or
yarn add msw-scenarios msw
📚 Usage
Basic Handler Setup
Setting Up msw-scenarios
import { extendHandlers } from 'msw-scenarios';
import { setupWorker } from 'msw';
const handlers = extendHandlers(userHandler);
const worker = setupWorker(...handlers.handlers);
// Register with workerManager
workerManager.setupWorker(worker);
// Start the worker
worker.start();
For Node.js
import { setupServer } from 'msw/node';
import { workerManager } from 'msw-scenarios';
import { handlers } from './mocks/handlers';
// Create MSW server
const server = setupServer();
// Register with workerManager
workerManager.setupServer(server);
// Start the server in your test setup
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
workerManager.resetHandlers();
});
afterAll(() => {
server.close();
});
Using Presets
handlers.useMock({
method: 'get',
path: '/api/user',
preset: 'success',
override: ({ data }) => {
data.name = 'Jane'; // Modify response data
},
});
Using Profiles
const profiles = handlers.createMockProfiles(
{
name: 'Empty State',
actions: ({ useMock }) => {
useMock({
method: 'get',
path: '/api/user',
preset: 'success',
override: ({ data }) => {
data.name = 'New User';
},
});
},
},
{
name: 'Error State',
actions: ({ useMock }) => {
useMock({
method: 'get',
path: '/api/user',
preset: 'error',
});
},
}
);
// Apply profile
profiles.useMock('Empty State');
🎯 Advanced Examples
Multiple Handlers with Type Safety
import { http } from 'msw-scenarios';
import { HttpResponse } from 'msw';
// User handler
const userHandler = http
.get('/api/user', () => {
return HttpResponse.json({ message: 'default' });
})
.presets(
{
label: 'authenticated',
status: 200,
response: {
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'admin'
}
},
{
label: 'unauthorized',
status: 401,
response: {
error: 'Unauthorized access'
}
}
);
// Posts handler
const postsHandler = http
.get('/api/posts', () => {
return HttpResponse.json({ posts: [] });
})
.presets(
{
label: 'with-posts',
status: 200,
response: {
posts: [
{ id: 1, title: 'First Post', content: 'Hello' },
{ id: 2, title: 'Second Post', content: 'World' }
],
total: 2
}
},
{
label: 'empty',
status: 200,
response: { posts: [], total: 0 }
}
);
// Comments handler
const commentsHandler = http
.get('/api/posts/:postId/comments', () => {
return HttpResponse.json({ comments: [] });
})
.presets(
{
label: 'has-comments',
status: 200,
response: {
comments: [
{ id: 1, text: 'Great post!', author: 'Jane' },
{ id: 2, text: 'Thanks!', author: 'John' }
]
}
},
{
label: 'no-comments',
status: 200,
response: { comments: [] }
}
);
// Combine all handlers
const handlers = extendHandlers(userHandler, postsHandler, commentsHandler);
// TypeScript provides excellent autocompletion and type checking:
handlers.useMock({
method: 'get', // ✨ Autocompletes with only available methods
path: '/api/user', // ✨ Autocompletes with only available paths
preset: 'authenticated' // ✨ Autocompletes with only valid presets for this endpoint
override: ({ data }) => {
data.name = 'Jane Doe'; // ✨ TypeScript knows the shape of the response
data.invalid = true; // ❌ TypeScript Error: Property 'invalid' does not exist
}
});
// Create comprehensive mocking profiles
const profiles = handlers.createMockProfiles(
{
name: 'Authenticated User with Content',
actions: ({ useMock }) => {
// ✨ Full type safety and autocompletion in profile actions
useMock({
method: 'get',
path: '/api/user',
preset: 'authenticated',
override: ({ data }) => {
data.name = 'Jane Doe'; // Type-safe override
}
});
useMock({
method: 'get',
path: '/api/posts',
preset: 'with-posts'
});
useMock({
method: 'get',
path: '/api/posts/:postId/comments',
preset: 'has-comments'
});
}
},
{
name: 'Unauthorized User',
actions: ({ useMock, useRealAPI }) => {
useMock({
method: 'get',
path: '/api/user',
preset: 'unauthorized'
});
// Mix mock and real API calls
useRealAPI({
method: 'get',
path: '/api/posts' // ✨ Path autocomplete works here too
});
useMock({
method: 'get',
path: '/api/posts/:postId/comments',
preset: 'no-comments'
});
}
}
);
// Type-safe profile switching
profiles.useMock('Authenticated User with Content'); // ✨ Autocompletes available profile names
Jest Test Examples
import { HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { http, extendHandlers, workerManager } from 'msw-scenarios';
describe('API Mocking Tests', () => {
// Setup MSW server
const server = setupServer();
beforeAll(() => {
workerManager.setupServer(server);
server.listen({ onUnhandledRequest: 'error' });
});
afterEach(() => {
workerManager.resetHandlers();
});
afterAll(() => {
server.close();
});
it('should handle different API responses using presets', async () => {
// Define handler with presets
const userHandler = http
.get('/api/user', () => {
return HttpResponse.json({ message: 'default' });
})
.presets(
{
label: 'success',
status: 200,
response: { name: 'John', role: 'admin' },
},
{
label: 'error',
status: 404,
response: { error: 'User not found' },
}
);
// Create extended handlers
const handlers = extendHandlers(userHandler);
// Test success case
handlers.useMock({
method: 'get',
path: '/api/user',
preset: 'success',
});
let response = await fetch('/api/user');
let data = await response.json();
expect(data).toEqual({ name: 'John', role: 'admin' });
// Test error case
handlers.useMock({
method: 'get',
path: '/api/user',
preset: 'error',
});
response = await fetch('/api/user');
data = await response.json();
expect(data).toEqual({ error: 'User not found' });
expect(response.status).toBe(404);
});
it('should work with profiles for complex scenarios', async () => {
const userHandler = http
.get('/api/user', () => {
return HttpResponse.json({ message: 'default' });
})
.presets(
{
label: 'authenticated',
status: 200,
response: { name: 'John', isAuthenticated: true },
},
{
label: 'guest',
status: 200,
response: { name: 'Guest', isAuthenticated: false },
}
);
const handlers = extendHandlers(userHandler);
const profiles = handlers.createMockProfiles(
{
name: 'Authenticated User',
actions: ({ useMock }) => {
useMock({
method: 'get',
path: '/api/user',
preset: 'authenticated',
});
},
},
{
name: 'Guest User',
actions: ({ useMock }) => {
useMock({
method: 'get',
path: '/api/user',
preset: 'guest',
});
},
}
);
// Test authenticated profile
profiles.useMock('Authenticated User');
let response = await fetch('/api/user');
let data = await response.json();
expect(data.isAuthenticated).toBe(true);
// Test guest profile
profiles.useMock('Guest User');
response = await fetch('/api/user');
data = await response.json();
expect(data.isAuthenticated).toBe(false);
});
});
🎨 UI Integration
State Management API
import { mockingState } from 'msw-scenarios';
// Get current status
const status = mockingState.getCurrentStatus();
// Subscribe to changes
const unsubscribe = mockingState.subscribeToChanges(
({ mockingStatus, currentProfile }) => {
console.log('Status:', mockingStatus);
console.log('Profile:', currentProfile);
}
);
// Control mocks
mockingState.resetEndpoint('get', '/api/user');
mockingState.resetAll();
React Integration Example
import { useEffect, useState } from 'react';
import { mockingState } from 'msw-scenarios';
import type { MockingStatus } from 'msw-scenarios';
function MockingController({ handlers, profiles }) {
const [status, setStatus] = useState<MockingStatus[]>([]);
const [currentProfile, setCurrentProfile] = useState<string | null>(null);
useEffect(() => {
return mockingState.subscribeToChanges(
({ mockingStatus, currentProfile }) => {
setStatus(mockingStatus);
setCurrentProfile(currentProfile);
}
);
}, []);
return (
<div className="mocking-controller">
{/* Profile Selector */}
<div>
<h3>Profiles</h3>
<select
value={currentProfile ?? ''}
onChange={(e) => profiles.useMock(e.target.value)}
>
<option value="">No Profile</option>
{profiles.getAvailableProfiles().map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
</div>
{/* Endpoint Controls */}
<div>
<h3>Endpoints</h3>
{status.map(({ method, path, currentPreset }) => (
<div key={`${method}-${path}`} className="endpoint-control">
<div>
{method.toUpperCase()} {path}
</div>
<div>
<select
value={currentPreset ?? ''}
onChange={(e) => {
if (e.target.value === '') {
handlers.useRealAPI({ method, path });
} else {
handlers.useMock({
method,
path,
preset: e.target.value,
});
}
}}
>
<option value="">Real API</option>
{handlers.handlers
.find((h) => h._method === method && h._path === path)
?._presets.map((preset) => (
<option key={preset.label} value={preset.label}>
{preset.label}
</option>
))}
</select>
<button onClick={() => mockingState.resetEndpoint(method, path)}>
Reset
</button>
</div>
</div>
))}
<button onClick={mockingState.resetAll}>Reset All</button>
</div>
</div>
);
}
📝 Types
The library provides full TypeScript support with the following key types:
interface MockingState {
getCurrentStatus: () => Array<MockingStatus>;
getCurrentProfile: <Name extends string = string>() => Name | null;
subscribeToChanges: <Name extends string = string>(
callback: (state: {
mockingStatus: Array<MockingStatus>;
currentProfile: Name | null;
}) => void
) => () => void;
resetAll: () => void;
resetEndpoint: (method: string, path: string) => void;
getEndpointState: (
method: string,
path: string
) => SelectedPreset | undefined;
setCurrentProfile: <Name extends string = string>(
profileName: Name | null
) => void;
}
📄 License
MIT
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
7 months ago
8 months ago
8 months ago
7 months ago
8 months ago
7 months ago
7 months ago
7 months ago
7 months ago
8 months ago
7 months ago
7 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago
10 months ago