From playwright-api
Generate page.route() mocks for hybrid E2E+API tests and standalone mock setups. Covers response factory helpers, route interception patterns, GraphQL mock patterns, and network condition simulation.
How this skill is triggered — by the user, by Claude, or both
Slash command
/playwright-api:api-mock-generationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use `page.route()` mocks in two scenarios:
Use page.route() mocks in two scenarios:
Do not use mocks for pure API tests that target a running API server — use the real apiClient fixture instead.
page.route() Mockimport { test, expect } from '@playwright/test';
test('should display users from API', async ({ page }) => {
// arrange — intercept and mock the API call
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: [
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' },
],
total: 2,
}),
});
});
// act
await page.goto('/users');
// assert
await expect(page.getByRole('row', { name: 'Alice' })).toBeVisible();
await expect(page.getByRole('row', { name: 'Bob' })).toBeVisible();
});
Create factory functions in tests/api/helpers/mock-factories.ts to keep mock data consistent across test files:
// tests/api/helpers/mock-factories.ts
export function makeUser(overrides: Partial<User> = {}): User {
return {
id: Math.floor(Math.random() * 10000),
name: 'Test User',
email: '[email protected]',
role: 'viewer',
createdAt: new Date().toISOString(),
...overrides,
};
}
export function makeUserList(count: number, overrides: Partial<User> = {}): User[] {
return Array.from({ length: count }, (_, i) =>
makeUser({ id: i + 1, name: `User ${i + 1}`, ...overrides })
);
}
export function makeApiList<T>(data: T[]): { data: T[]; total: number } {
return { data, total: data.length };
}
Usage in a test:
import { makeUser, makeApiList } from '../helpers/mock-factories';
test('should render user list', async ({ page }) => {
const users = makeApiList([makeUser({ name: 'Alice' }), makeUser({ name: 'Bob' })]);
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(users),
});
});
await page.goto('/users');
await expect(page.getByText('Alice')).toBeVisible();
});
test('should show error state when API fails', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ message: 'Internal Server Error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Something went wrong')).toBeVisible();
});
test('should show empty state when no users', async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: [], total: 0 }),
});
});
await page.goto('/users');
await expect(page.getByText('No users found')).toBeVisible();
});
Simulate slow network responses to test loading states:
test('should show loading spinner during slow response', async ({ page }) => {
await page.route('**/api/users', async (route) => {
// Simulate 1.5 s network delay
await new Promise((resolve) => setTimeout(resolve, 1500));
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: [], total: 0 }),
});
});
await page.goto('/users');
await expect(page.getByRole('progressbar')).toBeVisible();
await expect(page.getByRole('progressbar')).not.toBeVisible();
});
Intercept and capture the request payload the frontend sends, then continue with the real response:
test('should send correct payload on form submit', async ({ page, request }) => {
let capturedBody: unknown;
await page.route('**/api/users', async (route) => {
capturedBody = route.request().postDataJSON();
// Continue to the real server (or fulfill with a mock)
await route.continue();
});
await page.goto('/users/new');
await page.getByLabel('Name').fill('Alice');
await page.getByLabel('Email').fill('[email protected]');
await page.getByRole('button', { name: 'Save' }).click();
expect(capturedBody).toMatchObject({ name: 'Alice', email: '[email protected]' });
});
page.routeFromHARFor large suites with many endpoints, record a HAR file and replay it:
test('should replay recorded API interactions', async ({ page }) => {
await page.routeFromHAR('tests/api/fixtures/users-har.json', {
url: '**/api/**',
update: false,
});
await page.goto('/users');
await expect(page.getByRole('row')).toHaveCount(5);
});
Record a fresh HAR by setting update: true on a single run, then commit the HAR file.
GraphQL sends all operations to a single endpoint. Use the operationName in the request body to route to the correct mock:
await page.route('**/graphql', async (route) => {
const body = route.request().postDataJSON() as { operationName: string };
switch (body.operationName) {
case 'GetUsers':
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: {
users: [
{ id: '1', name: 'Alice', email: '[email protected]' },
],
},
}),
});
break;
case 'CreateUser':
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data: {
createUser: { id: '99', name: 'New User', email: '[email protected]' },
},
}),
});
break;
default:
// Pass through unmatched operations to the real server
await route.continue();
}
});
beforeEachWhen the same mocks are needed across multiple tests, register them in test.beforeEach:
test.beforeEach(async ({ page }) => {
await page.route('**/api/users', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(makeApiList([makeUser({ name: 'Alice' })])),
});
});
});
test('should display user name', async ({ page }) => {
await page.goto('/users');
await expect(page.getByText('Alice')).toBeVisible();
});
Routes registered with page.route() persist for the lifetime of the page. To remove a specific route after it is no longer needed, call page.unroute():
const handler = async (route: Route) => {
await route.fulfill({ status: 200, body: '{}' });
};
await page.route('**/api/users', handler);
// ... test steps
await page.unroute('**/api/users', handler);
npx claudepluginhub gagandeepp/software-agent-teams --plugin playwright-apiGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.