Release playkit v0.3 with Zod asserts and auth helpers
All checks were successful
CI / skip-ci-check (push) Successful in 4s
CI / secret-scan (push) Successful in 3s
CI / build-and-test (push) Successful in 16s

Add assertSchema/ApiClient schema option, storageState helpers, and
playkitFailureArtifacts preset for consumer e2e suites.
This commit is contained in:
ilia 2026-07-14 19:01:42 -04:00
parent a52fe89372
commit 7369b1c5e5
12 changed files with 140 additions and 55 deletions

View File

@ -1,5 +1,12 @@
# Changelog
## 0.3.0 — 2026-07-14
- **Zod schema asserts**`assertSchema()` + optional `schema` on `ApiClient` requests
- **storageState helpers**`saveStorageState()`, `storageStateUse()`
- **Trace-on-failure preset**`playkitFailureArtifacts()` for Playwright `use`
- Mailpit adapter marked shipped (was in 0.2.1)
## 0.2.1 — 2026-07-14
- **Mailpit** client + `createMailInbox()` (prefer homelab Mailpit, else Mailtrap SaaS)

View File

@ -2,7 +2,7 @@
Living plan for making `@levkin/playkit` more useful across Levkin repos.
## Now (v0.2) — Mailtrap + prior kit
## Now (v0.3) — shipped
- [x] Browser helpers with retries (`click`, `fill`, `safeGoto`, visibility waits)
- [x] `BasePage` for Page Objects
@ -14,20 +14,19 @@ Living plan for making `@levkin/playkit` more useful across Levkin repos.
- [x] Unit tests (vitest) + Gitea Actions CI
- [x] Starter Grafana dashboard JSON
- [x] Consumer docs + API/UI examples
- [x] **Mailtrap / Email Testing adapter**`waitForEmail`, link extraction (Kolby #56 class)
- [x] **Mailtrap / Mailpit**`createMailInbox()`, `waitForEmail`, link extraction
- [x] **Auth storage state helpers**`saveStorageState` / `storageStateUse`
- [x] **Trace-on-failure preset**`playkitFailureArtifacts()`
- [x] **Schema assertions** — Zod `assertSchema` + `ApiClient` `schema` option
## Next (v0.3) — high value
## Next (v0.4) — high value
- [ ] **Mailpit adapter** — same `waitForEmail` interface for local/homelab catch-all (no SaaS)
- [ ] **Auth storage state helpers** — save/load Playwright `storageState` for admin vs viewer roles
- [ ] **Trace-on-failure preset** — one-liner Playwright config merge (`trace: 'retain-on-failure'`, screenshot, video)
- [ ] **Schema assertions** — optional Zod (or AJV) helpers on `ApiClient` responses
- [ ] **Private Gitea npm registry publish**`npm install @levkin/playkit` without git URLs
- [ ] **Consumer template**`npx @levkin/playkit init` scaffolding `e2e/` + CI snippet
- [ ] **Deploy-smoke CLI**`playkit smoke --project punimtag` post-deploy gate (health + public host + login)
- [ ] **Deploy-smoke CLI**`playkit smoke --project punimtag` post-deploy gate
- [ ] **Retry policy presets** — flaky-network vs strict-CI profiles
## Later (v0.4+) — professional polish
## Later (v0.5+) — professional polish
- [ ] **Web Vitals** (LCP/CLS/INP) collection via Playwright CDP + metrics labels
- [ ] **A11y** — axe-core wrapper as optional peer
@ -39,46 +38,3 @@ Living plan for making `@levkin/playkit` more useful across Levkin repos.
- [ ] **Infisical SDK helper**`loadSecretsFromInfisical()` for local runs (machine identity)
- [ ] **JUnit + HTML report merge** — single artifact for Gitea PR checks
- [ ] **Network assert helpers** — fail if request hits `10.x` / wrong host after navigation
## Ideas backlog (not scheduled)
| Idea | Why |
|------|-----|
| Shared Page Object for Authentik login | Many apps share SSO |
| Chaos toggles (throttle network, offline) | Catch brittle UIs |
| Seeded persona library (`admin`, `viewer`, `unverified`) | Consistent fixtures across apps |
| “Deploy smoke” CLI | Post-deploy one-command gate before paging humans |
| Recorded HAR attach on API failure | Faster debugging |
| Benchmark budgets in CI | Fail if p95 login > N ms |
| Docs site (VitePress) with cookbook recipes | Onboarding other repos faster |
| Email delivery probe (SMTP accept ≠ inbox) | Catch Spamhaus / bounce class of #56 |
| `NEXTAUTH_URL` / canonical URL checker | Static env lint before e2e |
| Tag-based suite filters (`@smoke`, `@auth`, `@mail`) | Fast PR vs nightly depth |
| Parallel shard helper for self-hosted runners | Keep CI under 5m as suite grows |
| Golden-path checklist generator | Per-app “must pass before claim fixed” |
| Kuma + playkit correlation IDs | Tie synthetic monitor blips to e2e runs |
| Diffable timing baselines in git | Spot regressions without Grafana |
## Punimtag as first consumer
1. [x] `e2e/` depending on `@levkin/playkit`
2. [x] Specs: public host after sign-out; health API; forgot-password via Mailtrap
3. [ ] CI secrets populated + scheduled DEV smoke
4. Deploy rule: PR → green CI → merge → deploy script (no silent `pct exec` “done”)
## Observability
Pushgateway → Prometheus on LXC 240 (`observability` @ `10.0.10.24`) → Grafana dashboard `dashboards/playkit-overview.json`.
Metrics (v0.1):
- `playkit_action_duration_ms{project,env,action}`
- `playkit_action_ok{project,env,action}`
## Success criteria
A bug like “sign-out redirects to LAN IP” or “email accepted by SMTP but bounced by Outlook” cannot be marked fixed without:
1. Public-URL browser assertion (playkit host guards)
2. Delivery/inbox assertion (mail adapter — v0.2) or documented mail-trap substitute
3. Green consumer CI on the merge commit

16
package-lock.json generated
View File

@ -1,13 +1,16 @@
{
"name": "@levkin/playkit",
"version": "0.1.0",
"version": "0.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@levkin/playkit",
"version": "0.1.0",
"version": "0.3.0",
"license": "MIT",
"dependencies": {
"zod": "^3.24.0"
},
"devDependencies": {
"@playwright/test": "^1.52.0",
"@types/node": "^22.15.0",
@ -2668,6 +2671,15 @@
"engines": {
"node": ">=8"
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@levkin/playkit",
"version": "0.2.1",
"version": "0.3.0",
"description": "Shared Playwright + API test kit — browser helpers, API client, logging, performance, Grafana/Prometheus metrics",
"license": "MIT",
"type": "module",
@ -47,6 +47,9 @@
"prepublishOnly": "npm run build",
"example:api": "tsx examples/api/health.example.ts"
},
"dependencies": {
"zod": "^3.24.0"
},
"peerDependencies": {
"@playwright/test": ">=1.40.0"
},

View File

@ -1,5 +1,7 @@
import { createLogger, type Logger } from '../logging/logger.js';
import { redactSecrets } from '../logging/redact.js';
import { assertSchema } from './schema.js';
import type { z } from 'zod';
export interface ApiClientOptions {
baseUrl: string;
@ -16,6 +18,8 @@ export interface ApiRequestOptions {
body?: unknown;
timeoutMs?: number;
expectedStatus?: number | number[];
/** Optional Zod schema — response `data` is parsed/asserted when set. */
schema?: z.ZodType<unknown>;
}
export interface ApiResponse<T = unknown> {
@ -111,6 +115,10 @@ export class ApiClient {
data = text as unknown as T;
}
if (opts.schema) {
data = assertSchema(data, opts.schema as z.ZodType<T>, `${method} ${path}`);
}
const response: ApiResponse<T> = {
status: res.status,
ok: res.ok,

View File

@ -1 +1,2 @@
export { ApiClient, type ApiClientOptions, type ApiRequestOptions, type ApiResponse } from './client.js';
export { assertSchema, SchemaAssertionError } from './schema.js';

17
src/api/schema.test.ts Normal file
View File

@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { z } from 'zod';
import { assertSchema, SchemaAssertionError } from './schema.js';
describe('assertSchema', () => {
const Health = z.object({ status: z.literal('ok') });
it('returns parsed data when valid', () => {
expect(assertSchema({ status: 'ok' }, Health)).toEqual({ status: 'ok' });
});
it('throws SchemaAssertionError when invalid', () => {
expect(() => assertSchema({ status: 'nope' }, Health, 'health')).toThrow(
SchemaAssertionError,
);
});
});

34
src/api/schema.ts Normal file
View File

@ -0,0 +1,34 @@
import type { z } from 'zod';
export class SchemaAssertionError extends Error {
constructor(
message: string,
readonly issues: unknown,
) {
super(message);
this.name = 'SchemaAssertionError';
}
}
/**
* Assert `data` matches a Zod schema. Throws SchemaAssertionError on failure.
* Prefer this over `as T` casts at API boundaries.
*/
export function assertSchema<T>(
data: unknown,
schema: z.ZodType<T>,
label = 'response',
): T {
const result = schema.safeParse(data);
if (!result.success) {
const summary = result.error.issues
.slice(0, 8)
.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
.join('; ');
throw new SchemaAssertionError(
`Schema assertion failed for ${label}: ${summary}`,
result.error.issues,
);
}
return result.data;
}

View File

@ -11,3 +11,5 @@ export {
type FillOptions,
type GotoOptions,
} from './actions.js';
export { saveStorageState, storageStateUse } from './storageState.js';
export { playkitFailureArtifacts } from './playwrightPreset.js';

View File

@ -0,0 +1,18 @@
import type { PlaywrightTestConfig } from '@playwright/test';
type UseOptions = NonNullable<PlaywrightTestConfig['use']>;
/**
* Artifact defaults for CI / flaky debugging: retain trace/screenshot/video on failure.
* Merge into `defineConfig({ use: { ...playkitFailureArtifacts() } })`.
*/
export function playkitFailureArtifacts(
overrides: Partial<UseOptions> = {},
): Partial<UseOptions> {
return {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
...overrides,
};
}

View File

@ -0,0 +1,22 @@
import { mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import type { BrowserContext, Page } from '@playwright/test';
/**
* Save Playwright storage state (cookies + localStorage) for reuse across suites.
* Typical use: one-time admin login save other specs load via `test.use({ storageState })`.
*/
export async function saveStorageState(
source: Page | BrowserContext,
filePath: string,
): Promise<string> {
mkdirSync(dirname(filePath), { recursive: true });
const context = 'context' in source ? source.context() : source;
await context.storageState({ path: filePath });
return filePath;
}
/** Options blob for `test.use({ storageState: path })` / project config. */
export function storageStateUse(filePath: string): { storageState: string } {
return { storageState: filePath };
}

View File

@ -19,6 +19,9 @@ export {
waitForHidden,
waitForUrlHost,
assertPublicHost,
saveStorageState,
storageStateUse,
playkitFailureArtifacts,
type ClickOptions,
type FillOptions,
type GotoOptions,
@ -26,6 +29,8 @@ export {
export {
ApiClient,
assertSchema,
SchemaAssertionError,
type ApiClientOptions,
type ApiRequestOptions,
type ApiResponse,