Bump playkit to v0.3 and use Zod API schemas
CI / skip-ci-check (pull_request) Successful in 4s
CI / docker-ci (pull_request) Successful in 7s
CI / secret-scan (pull_request) Successful in 12s
CI / viewer-unit (pull_request) Successful in 1m59s
CI / e2e (pull_request) Successful in 1m54s

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:
2026-07-14 19:02:48 -04:00
parent 47b15049bc
commit 217b82d912
5 changed files with 46 additions and 30 deletions
+18 -4
View File
@@ -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
View File
@@ -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"
}
}
+2 -3
View File
@@ -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'] } }],
+17 -18
View File
@@ -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] }),
);
+5 -2
View File
@@ -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' });
});
});