PW API Tester
Use ajv, not zod
This repo validates response contracts with ajv + ajv-formats and queries payloads with
jsonpath-plus. All three are in devDependencies. Do not introduce zod.
require('zod') may succeed on a dev machine anyway, because Node walks up to ~/node_modules
and can find a stray copy there. That is not a dependency. Trust the project tree, not the import:
npm ls zod # "(empty)" means it is not yours to use
A test built on a package that resolves only from a home directory passes locally and fails in CI,
the same way a missing .env does.
Where things go
- Request helpers:
src/api/(currently empty, so you are setting the pattern). - Schemas: alongside the helper, or
src/testdata/when shared. - Specs:
src/tests/api/.
Base URL
playwright.config.ts:resolveBaseURL() returns API_BASE_URL (default
https://restful-booker.herokuapp.com) when TTA_ENV=api. Read it through @config/env; never
hard-code a host.
TTA_ENV=api npx playwright test src/tests/api/
Workflow
- Wrap the endpoint in a client class in
src/api/takingAPIRequestContext. Give it a scoped logger viacreateLogger, matching the page objects. - Cover happy path, schema, auth, and negative/boundary cases. Assert status and body separately so a failure names which one broke.
- Compile the schema once at module scope, not per test.
Output shape
import Ajv, { type JSONSchemaType } from 'ajv';
import addFormats from 'ajv-formats';
import { test, expect } from '@fixtures/test-base';
import { createLogger } from '@utils/logger';
const log = createLogger('booking.api.spec');
const ajv = addFormats(new Ajv({ allErrors: true }));
const bookingSchema = {
type: 'object',
required: ['bookingid', 'booking'],
properties: {
bookingid: { type: 'integer' },
booking: {
type: 'object',
required: ['firstname', 'lastname', 'totalprice'],
properties: {
firstname: { type: 'string' },
lastname: { type: 'string' },
totalprice: { type: 'number' },
},
},
},
} as const;
const validateBooking = ajv.compile(bookingSchema as any);
test('@API creates a booking matching the contract', async ({ request }) => {
const res = await request.post('/booking', { data: { /* ... */ } });
expect(res.status()).toBe(200);
const body = await res.json();
const valid = validateBooking(body);
// Surface ajv's own errors, otherwise the failure says only "false".
expect(valid, JSON.stringify(validateBooking.errors, null, 2)).toBe(true);
});
Note
The request fixture is Playwright's built-in and is available through @fixtures/test-base,
since that module extends the base test.