From playwright-api
Generate API test files using Playwright's `request` context for REST (GET/POST/PUT/DELETE) and GraphQL (query/mutation) endpoints. Produces fully-typed .api.spec.ts files with request chains, status code assertions, body validation, and correct import paths.
How this skill is triggered — by the user, by Claude, or both
Slash command
/playwright-api:api-test-generationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
The skill accepts three input formats. Detect which is provided and adapt accordingly.
The skill accepts three input formats. Detect which is provided and adapt accordingly.
| Format | Example | Notes |
|---|---|---|
| Natural language | "Test the user creation endpoint" | Infer request shape from domain knowledge and endpoint naming |
| Endpoint + description | POST /api/users + "create a user and verify the response" | Use the provided path and derive the request body from the description |
| OpenAPI / GraphQL schema snippet | Pasted schema fragment | Map each operation to a test block |
Each generation produces a single .api.spec.ts file placed in the apiTestDir configured in .playwright-agent.json (default: tests/api/specs/). File names follow the pattern <resource-name>.api.spec.ts using kebab-case.
All generated tests follow the test.describe → test structure:
import { test, expect } from '../fixtures/api-client';
test.describe('Users API', () => {
test('should create a new user', async ({ apiClient }) => {
// arrange
const payload = { name: 'Alice', email: '[email protected]' };
// act
const response = await apiClient.post('/users', { data: payload });
// assert
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toMatchObject({ name: 'Alice', email: '[email protected]' });
expect(body.id).toBeDefined();
});
});
Rules:
test.describe block per API resource or domain area.test block per distinct operation or outcome.// arrange, // act, // assert comments.test.describe more than one level deep.Always import test and expect from the project fixtures barrel, not directly from @playwright/test:
// DO — uses the extended API client fixture
import { test, expect } from '../fixtures/api-client';
// DON'T — bypasses project fixtures and authenticated context
import { test, expect } from '@playwright/test';
Adjust the relative path based on the spec file's location within apiTestDir.
import { test, expect } from '../fixtures/api-client';
test.describe('Users API — GET', () => {
test('should return a list of users', async ({ apiClient }) => {
// act
const response = await apiClient.get('/users');
// assert
expect(response.status()).toBe(200);
const body = await response.json();
expect(Array.isArray(body.data)).toBe(true);
});
test('should return a single user by ID', async ({ apiClient }) => {
// act
const response = await apiClient.get('/users/1');
// assert
expect(response.status()).toBe(200);
const user = await response.json();
expect(user.id).toBe(1);
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('email');
});
test('should return 404 for a non-existent user', async ({ apiClient }) => {
// act
const response = await apiClient.get('/users/99999');
// assert
expect(response.status()).toBe(404);
});
});
import { test, expect } from '../fixtures/api-client';
test.describe('Users API — POST', () => {
test('should create a user and retrieve it', async ({ apiClient }) => {
// arrange
const payload = { name: 'Bob', email: '[email protected]', role: 'viewer' };
// act — create
const createResponse = await apiClient.post('/users', { data: payload });
expect(createResponse.status()).toBe(201);
const created = await createResponse.json();
// act — retrieve
const getResponse = await apiClient.get(`/users/${created.id}`);
// assert
expect(getResponse.status()).toBe(200);
const fetched = await getResponse.json();
expect(fetched.email).toBe('[email protected]');
});
});
test('should update an existing user', async ({ apiClient }) => {
// arrange
const createRes = await apiClient.post('/users', {
data: { name: 'Carol', email: '[email protected]' },
});
const { id } = await createRes.json();
// act
const updateRes = await apiClient.put(`/users/${id}`, {
data: { name: 'Carol Updated' },
});
// assert
expect(updateRes.status()).toBe(200);
const updated = await updateRes.json();
expect(updated.name).toBe('Carol Updated');
});
test('should delete a user', async ({ apiClient }) => {
// arrange
const createRes = await apiClient.post('/users', {
data: { name: 'Dave', email: '[email protected]' },
});
const { id } = await createRes.json();
// act
const deleteRes = await apiClient.delete(`/users/${id}`);
// assert
expect(deleteRes.status()).toBe(204);
// verify deletion
const getRes = await apiClient.get(`/users/${id}`);
expect(getRes.status()).toBe(404);
});
import { test, expect } from '../fixtures/api-client';
test.describe('GraphQL — Users', () => {
test('should fetch users via query', async ({ apiClient }) => {
// arrange
const query = `
query GetUsers {
users {
id
name
email
}
}
`;
// act
const response = await apiClient.post('/graphql', {
data: { query },
});
// assert
expect(response.status()).toBe(200);
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(Array.isArray(data.users)).toBe(true);
});
});
test('should create a user via mutation', async ({ apiClient }) => {
// arrange
const mutation = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const variables = { input: { name: 'Eve', email: '[email protected]' } };
// act
const response = await apiClient.post('/graphql', {
data: { query: mutation, variables },
});
// assert
expect(response.status()).toBe(200);
const { data, errors } = await response.json();
expect(errors).toBeUndefined();
expect(data.createUser.id).toBeDefined();
expect(data.createUser.email).toBe('[email protected]');
});
When testing a multi-step workflow, chain requests within a single test to express the full lifecycle:
test('should complete order workflow', async ({ apiClient }) => {
// step 1 — create order
const orderRes = await apiClient.post('/orders', {
data: { productId: 42, quantity: 2 },
});
expect(orderRes.status()).toBe(201);
const { orderId } = await orderRes.json();
// step 2 — confirm payment
const payRes = await apiClient.post(`/orders/${orderId}/pay`, {
data: { method: 'card', token: 'tok_test' },
});
expect(payRes.status()).toBe(200);
// step 3 — verify order status
const statusRes = await apiClient.get(`/orders/${orderId}`);
expect(statusRes.status()).toBe(200);
const order = await statusRes.json();
expect(order.status).toBe('paid');
});
When categorization.enabled is true, invoke the test-case-categorizer skill before generating test blocks. Each test() receives a { tag: ['@<category>'] } option.
test.describe('Users API', () => {
// --- Positive scenarios ---
test('should create a new user', { tag: ['@positive'] }, async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice', email: '[email protected]' }
});
expect(response.status()).toBe(201);
const body = await response.json();
expect(body).toMatchObject({ name: 'Alice', email: '[email protected]' });
});
test('should return user by ID', { tag: ['@positive'] }, async ({ request }) => {
const response = await request.get('/api/users/1');
expect(response.status()).toBe(200);
});
// --- Negative scenarios ---
test('should return 400 for missing email', { tag: ['@negative'] }, async ({ request }) => {
const response = await request.post('/api/users', {
data: { name: 'Alice' }
});
expect(response.status()).toBe(400);
});
test('should return 404 for non-existent user', { tag: ['@negative'] }, async ({ request }) => {
const response = await request.get('/api/users/99999');
expect(response.status()).toBe(404);
});
// --- Edge scenarios ---
test('should handle empty request body', { tag: ['@edge'] }, async ({ request }) => {
const response = await request.post('/api/users', { data: {} });
expect(response.status()).toBe(400);
expect(response.status()).not.toBe(500);
});
// --- Security scenarios ---
test('should reject SQL injection in query param', { tag: ['@security'] }, async ({ request }) => {
const response = await request.get("/api/users?name=' OR '1'='1");
expect(response.status()).toBe(400);
const body = await response.json();
expect(JSON.stringify(body)).not.toContain('syntax error');
});
});
test() block must include a tag option with exactly one category tag.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.