Files
playkit/README.md
T
ilia 05f93d0330
CI / skip-ci-check (pull_request) Successful in 6s
CI / release (pull_request) Has been skipped
CI / build-and-test (pull_request) Successful in 46s
CI / secret-scan (pull_request) Successful in 4s
CI / selftest (pull_request) Successful in 17s
docs: OPS checklist for npm token + Outline scopes
Record outstanding one-time UI steps after v0.4.0, add publish-gitea-npm.sh,
and align outline-sync body with the live Playkit page format.
2026-07-15 09:39:45 -04:00

171 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @levkin/playkit
Shared **Playwright + API** test kit for Levkin / homelab apps.
Use it as a library from any consumer repo (punimtag, MirrorMatch, …). App-specific Page Objects and specs stay in the consumer; reusable helpers, logging, API client, performance timings, and Grafana/Prometheus metrics live here.
## Install (consumer)
```bash
# Preferred — Gitea npm registry (docs/NPM_REGISTRY.md)
npm install @levkin/playkit@0.4.0
# Fallback — git tag
npm install git+https://git.levkin.ca/ilia/playkit.git#v0.4.0
# Scaffold e2e/
npx @levkin/playkit init
# peer
npm install -D @playwright/test
npx playwright install chromium
```
```ts
// e2e/fixtures.ts
import { test as base } from '@playwright/test';
import { createPlaykitRuntime, type PlaykitFixtures } from '@levkin/playkit';
const runtime = createPlaykitRuntime();
export const test = base.extend<PlaykitFixtures>({
playkitConfig: async ({}, use) => use(runtime.playkitConfig),
playkitLog: async ({}, use) => use(runtime.playkitLog),
api: async ({}, use) => use(runtime.api),
timings: async ({}, use) => use(runtime.timings),
});
export { expect } from '@playwright/test';
```
```ts
// e2e/auth.signout.spec.ts — catches NEXTAUTH_URL → 10.x redirect (punimtag #57)
import { assertPublicHost, waitForUrlHost } from '@levkin/playkit';
import { test } from './fixtures';
test('sign-out stays on public host', async ({ page, playkitConfig, timings }) => {
assertPublicHost(playkitConfig.baseUrl);
await timings.measure('open', () => page.goto(playkitConfig.baseUrl));
// … login …
// … sign out …
await waitForUrlHost(page, playkitConfig.expectedHost);
});
```
## Environment
| Variable | Required | Purpose |
|----------|----------|---------|
| `PLAYKIT_BASE_URL` / `BASE_URL` | yes | Public UI URL (e.g. `https://punimtagdev.levkin.ca`) |
| `PLAYKIT_API_BASE_URL` | no | Defaults to base URL |
| `PLAYKIT_EXPECTED_HOST` | no | Defaults to host of base URL |
| `PLAYKIT_FORBID_PRIVATE_HOSTS` | no | Default `true` — refuse `10.x` / localhost as expected host |
| `PLAYKIT_RETRY_PRESET` | no | `default` \| `strictCi` \| `flakyNetwork` (timeouts/retries) |
| `PLAYKIT_ACTION_RETRIES` / `PLAYKIT_TIMEOUT_MS` | no | Override preset values |
| `PLAYKIT_PROJECT` | no | Metric / log label |
| `PLAYKIT_ENV` | no | Metric / log label (`dev`/`qa`/`prod`) |
| `PLAYKIT_METRICS_ENABLED` | no | Push timings to Pushgateway |
| `PLAYKIT_PUSHGATEWAY_URL` | if metrics | e.g. `http://10.0.10.24:9091` |
| `PLAYKIT_LOG_LEVEL` | no | `debug` \| `info` \| `warn` \| `error` |
**Secrets:** put test credentials and pushgateway tokens in Infisical (`LevkinOps`) and sync into Gitea Actions — see ansible `docs/hardening/SECRETS.md`. Never commit passwords.
## Whats in the box
| Module | Role |
|--------|------|
| `BasePage`, `click` / `fill` / `safeGoto` | Retried browser actions + Page Object base |
| `waitForUrlHost` / `assertPublicHost` | Guard against LAN redirect bugs |
| `ApiClient` | Typed HTTP client with status asserts + redacted logs |
| `createLogger` / `redactSecrets` | Structured JSON logs |
| `TimingCollector` / `pushPrometheusMetrics` | Action timings → Prometheus Pushgateway → Grafana |
| `createPlaykitRuntime` | One-shot config + logger + API + timings |
| `createMailInbox` / `MailpitClient` / `MailtrapClient` | Assert password-reset / verify emails (Mailpit homelab trap or Mailtrap SaaS) |
| `assertSchema` | Zod schema asserts (standalone or via `ApiClient` `schema` option) |
| `saveStorageState` / `storageStateUse` | Auth once, reuse across specs |
| `playkitFailureArtifacts` | Trace/video/screenshot only on failure |
| `interceptNetworkCall` | Spy or mock the next matching page network call |
| `playkit` CLI | `init` scaffold + `smoke` post-deploy gate |
See also `docs/NETWORK.md`, `docs/SELFTEST.md`, `docs/NPM_REGISTRY.md`, `docs/OPS.md`, `docs/IDEAS.md`, `docs/OUTLINE.md`.
## Network (page traffic)
```ts
import { interceptNetworkCall, startNetworkErrorMonitor } from '@levkin/playkit';
const users = interceptNetworkCall({ page, url: '**/api/users' });
const net = startNetworkErrorMonitor(page, { excludePatterns: ['sentry.io'] });
try {
await page.goto('/users');
const { status } = await users;
expect(status).toBe(200);
} finally {
net.assertNoErrors();
}
```
## Email testing (Mailpit default, Mailtrap optional)
`createMailInbox()` picks the provider from `PLAYKIT_MAIL_PROVIDER` (default
`mailpit`) so specs don't need to know which backend is behind it. Prefer
**Mailpit** — it's our homelab SMTP trap (`10.0.10.45`, no external
dependency); use Mailtrap only if you specifically want the SaaS sandbox.
```ts
import { createMailInbox, readMailHtml, firstLinkMatching, assertPublicHost } from '@levkin/playkit';
const mail = createMailInbox(); // reads PLAYKIT_MAIL_PROVIDER / MAILPIT_* / MAILTRAP_*
if (!mail) throw new Error('set MAILPIT_BASE_URL (or MAILTRAP_API_TOKEN + MAILTRAP_INBOX_ID)');
const after = new Date();
// … trigger forgot-password in the app …
const msg = await mail.waitForEmail({ to: 'e2e@example.com', subject: /reset/i, after });
const html = await readMailHtml(mail, msg); // normalizes Mailpit vs Mailtrap message shape
const link = firstLinkMatching(html, /reset-password/);
assertPublicHost(link!);
```
**Important:** the mail client only sees mail if the app's SMTP actually
points at that trap (Mailpit `10.0.10.45:1025` in DEV, or Mailtrap's
`sandbox.smtp.mailtrap.io` + inbox credentials for SaaS). Sending via Gmail to
a real address will not appear in either. See ansible `docs/hardening/SECRETS.md`
(`## Playkit / punimtag e2e secrets`).
## Develop this repo
```bash
npm ci
npm run typecheck
npm test
npm run selftest # fake site + Chromium (docs/SELFTEST.md)
npm run build
```
## Outline live docs
After each release (or when consumer-facing docs change):
```bash
# needs OUTLINE_URL + OUTLINE_API_KEY (ansible: make vault-export-env)
python3 scripts/outline-sync-playkit.py
```
See `docs/OUTLINE.md`.
## Release
1. Bump `version` in `package.json`
2. Update `CHANGELOG.md` with a `## X.Y.Z` section (the release job extracts this verbatim as release notes)
3. Tag `vX.Y.Z` and push
Pushing the tag triggers `.gitea/workflows/ci.yml`'s `release` job: it re-runs typecheck/test/build, verifies the tag matches `package.json`'s `version` and that `CHANGELOG.md` documents it, then creates a Gitea release (with the `npm pack` tarball attached) via the API using a repo-scoped `RELEASE_TOKEN` Actions secret. If any check fails, no release is created — fix and re-tag. Consumers still pin the git tag (`#vX.Y.Z`); the Gitea release is for visibility/changelog, not an npm registry publish (see ROADMAP "private Gitea npm registry").
4. **Update Outline**`python3 scripts/outline-sync-playkit.py` (checklist: `docs/OUTLINE.md`).
**One-time setup:** add a `RELEASE_TOKEN` secret (repo `Settings → Actions → Secrets` on `ilia/playkit`) scoped to create releases on this repo — separate from the `PLAYKIT_GIT_TOKEN` consumers use to clone it. (Named `RELEASE_TOKEN`, not `GITEA_TOKEN` — Gitea's Actions secrets API rejects that literal name as reserved, same reason `PLAYKIT_GIT_TOKEN` isn't called `GITEA_TOKEN` either.)
## License
MIT