Bump playkit to v0.3 and use Zod API schemas
Adopt assertSchema via ApiClient, playkitFailureArtifacts in Playwright config, and keep e2e login on e2e@levkine.ca credentials from CI secrets.
This commit is contained in:
Generated
+18
-4
@@ -8,20 +8,24 @@
|
||||
"name": "punimtag-e2e",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@levkin/playkit": "git+https://git.levkin.ca/ilia/playkit.git#v0.2.1",
|
||||
"@levkin/playkit": "git+https://git.levkin.ca/ilia/playkit.git#v0.3.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/node": "^22.15.0",
|
||||
"typescript": "^5.8.0"
|
||||
"typescript": "^5.8.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@levkin/playkit": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "git+https://git.levkin.ca/ilia/playkit.git#a52fe8937217a1a6fbe539d89d66b739b5fac607",
|
||||
"version": "0.3.0",
|
||||
"resolved": "git+https://git.levkin.ca/ilia/playkit.git#7369b1c5e53e1f7511bea96be9042e663b60b315",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -112,6 +116,16 @@
|
||||
"version": "6.21.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -2,7 +2,7 @@
|
||||
"name": "punimtag-e2e",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "PunimTag Playwright e2e — powered by @levkin/playkit",
|
||||
"description": "PunimTag Playwright e2e \u2014 powered by @levkin/playkit",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:ui": "playwright test --ui",
|
||||
@@ -13,9 +13,10 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@levkin/playkit": "git+https://git.levkin.ca/ilia/playkit.git#v0.2.1",
|
||||
"@levkin/playkit": "git+https://git.levkin.ca/ilia/playkit.git#v0.3.0",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@types/node": "^22.15.0",
|
||||
"typescript": "^5.8.0"
|
||||
"typescript": "^5.8.0",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { playkitFailureArtifacts } from '@levkin/playkit';
|
||||
import { DEFAULT_BASE_URL } from './env-defaults';
|
||||
|
||||
const baseURL =
|
||||
@@ -17,9 +18,7 @@ export default defineConfig({
|
||||
expect: { timeout: 15_000 },
|
||||
use: {
|
||||
baseURL,
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
...playkitFailureArtifacts(),
|
||||
...devices['Desktop Chrome'],
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
const PeopleList = z.object({
|
||||
items: z.array(z.object({ id: z.number() }).passthrough()).min(1),
|
||||
});
|
||||
|
||||
const TagsList = z.object({
|
||||
items: z.array(z.object({ id: z.number() }).passthrough()).min(1),
|
||||
});
|
||||
|
||||
/**
|
||||
* Public / lightly-gated FastAPI surfaces — shape + status contracts via playkit ApiClient.
|
||||
* Does not require e2e login credentials.
|
||||
@@ -7,35 +16,26 @@ import { test, expect } from '../fixtures';
|
||||
test.describe('api catalog @smoke', () => {
|
||||
test('GET /api/v1/people returns items list', async ({ api, timings }) => {
|
||||
const res = await timings.measure('api_people', () =>
|
||||
api.get<{ items: Array<{ id: number; first_name?: string }> }>('/api/v1/people', {
|
||||
expectedStatus: 200,
|
||||
}),
|
||||
);
|
||||
expect(Array.isArray(res.data.items)).toBe(true);
|
||||
expect(res.data.items.length).toBeGreaterThan(0);
|
||||
expect(res.data.items[0]).toEqual(
|
||||
expect.objectContaining({ id: expect.any(Number) }),
|
||||
api.get('/api/v1/people', { expectedStatus: 200, schema: PeopleList }),
|
||||
);
|
||||
expect(res.data.items[0]).toEqual(expect.objectContaining({ id: expect.any(Number) }));
|
||||
});
|
||||
|
||||
test('GET /api/v1/people/with-faces returns items list', async ({ api, timings }) => {
|
||||
const res = await timings.measure('api_people_faces', () =>
|
||||
api.get<{ items: unknown[] }>('/api/v1/people/with-faces', { expectedStatus: 200 }),
|
||||
api.get('/api/v1/people/with-faces', {
|
||||
expectedStatus: 200,
|
||||
schema: z.object({ items: z.array(z.unknown()) }),
|
||||
}),
|
||||
);
|
||||
expect(Array.isArray(res.data.items)).toBe(true);
|
||||
});
|
||||
|
||||
test('GET /api/v1/tags returns items list', async ({ api, timings }) => {
|
||||
const res = await timings.measure('api_tags', () =>
|
||||
api.get<{ items: Array<{ id: number; tag_name?: string }> }>('/api/v1/tags', {
|
||||
expectedStatus: 200,
|
||||
}),
|
||||
);
|
||||
expect(Array.isArray(res.data.items)).toBe(true);
|
||||
expect(res.data.items.length).toBeGreaterThan(0);
|
||||
expect(res.data.items[0]).toEqual(
|
||||
expect.objectContaining({ id: expect.any(Number) }),
|
||||
api.get('/api/v1/tags', { expectedStatus: 200, schema: TagsList }),
|
||||
);
|
||||
expect(res.data.items[0]).toEqual(expect.objectContaining({ id: expect.any(Number) }));
|
||||
});
|
||||
|
||||
test('POST /api/v1/auth/login rejects empty body with 422', async ({ api, timings }) => {
|
||||
@@ -73,7 +73,6 @@ test.describe('api catalog @smoke', () => {
|
||||
});
|
||||
|
||||
test('GET /api/v1/photos/{id} for missing photo is 404 (or 401)', async ({ api, timings }) => {
|
||||
// Current DEV returns 404 without auth for missing ids; accept either contract.
|
||||
const res = await timings.measure('api_photo_missing', () =>
|
||||
api.get('/api/v1/photos/999999999', { expectedStatus: [401, 404] }),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
import { test, expect } from '../fixtures';
|
||||
|
||||
const HealthSchema = z.object({ status: z.literal('ok') });
|
||||
|
||||
test.describe('api @smoke', () => {
|
||||
test('backend /health returns ok', async ({ api, timings }) => {
|
||||
const res = await timings.measure('api_health', () =>
|
||||
api.get<{ status: string }>('/health', { expectedStatus: 200 }),
|
||||
api.get('/health', { expectedStatus: 200, schema: HealthSchema }),
|
||||
);
|
||||
expect(res.data).toMatchObject({ status: 'ok' });
|
||||
expect(res.data).toEqual({ status: 'ok' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user