porchlight/docs/plans/2026-02-18-playwright-migration-webauthn-e2e-design.md
2026-04-10 11:28:51 +02:00

117 lines
4.2 KiB
Markdown

# Playwright Test Migration + WebAuthn E2E Tests
## Problem
The existing 7 E2E tests use a hand-rolled test runner (`helpers.js` with
`run()`/`assert()`). This works but lacks structured reporting, retry logic,
parallel execution, and proper lifecycle hooks.
Additionally, the WebAuthn (passkey) authentication flow has no E2E coverage.
The existing tests acknowledge this gap -- `test_login.js` checks that the
WebAuthn button exists but doesn't exercise the actual flow.
## Decision
Two-phase approach:
1. Migrate all existing E2E tests from the custom runner to `@playwright/test`
2. Add comprehensive WebAuthn E2E tests using CDP virtual authenticators
## Phase 1: Migrate to @playwright/test
### Infrastructure
- Add `@playwright/test` to `package.json`
- Create `playwright.config.js` with:
- `baseURL` from `TARGET_URL` env var (default `http://localhost:8099`)
- Chromium-only project (WebAuthn CDP requires Chromium)
- `testDir` pointing to the e2e directory
- `testMatch` for `*.spec.js` files
- Update `run.sh` to call `npx playwright test` instead of looping over `test_*.js`
### Test conversion
Each `test_*.js` becomes `*.spec.js`:
- `run(async (page, assert) => { ... })` becomes `test('...', async ({ page }) => { ... })`
- `assert(condition, msg)` becomes `expect(condition).toBeTruthy()` or specific matchers
- Shared setup moves to `test.beforeAll()` / `test.beforeEach()`
- `TARGET_URL` usage replaced by Playwright's `baseURL` (use relative paths)
### What stays the same
- `run.sh` still starts the app, seeds data, runs tests, tears down
- `setup_db.py` unchanged
- Test logic/assertions are equivalent
### Files removed
- `helpers.js` -- replaced by Playwright Test's built-in fixtures and `expect`
## Phase 2: WebAuthn E2E tests
### Approach: CDP Virtual Authenticator + Route Interception
Chromium DevTools Protocol exposes `WebAuthn.addVirtualAuthenticator` which
creates a software authenticator that the browser's WebAuthn API treats as real.
This lets us test the full stack: button click -> `navigator.credentials` ->
server round-trip -> redirect.
For error scenarios, we use Playwright's `page.route()` to intercept network
requests and return error responses.
### Virtual authenticator configuration
```js
const cdpSession = await page.context().newCDPSession(page);
await cdpSession.send('WebAuthn.enable');
const { authenticatorId } = await cdpSession.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2',
transport: 'internal',
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
}
});
```
### Test scenarios
**Registration (from credentials page, requires active session):**
- Register a passkey via the "Add security key" button
- Verify the new passkey appears in the credential list
- Verify registration uses resident key (discoverable credential)
**Authentication (usernameless login):**
- Full round-trip: register passkey -> logout -> login via passkey button
- No username needed -- browser's passkey picker selects the credential
- Verify redirect to `/manage/credentials` after successful login
- Verify session is established (can access protected pages)
**Deletion:**
- Register passkey + have password, delete the passkey
- Cannot delete last credential (only has passkey, no password)
**Error handling (route interception):**
- Server error on authentication begin
- Server error on authentication complete
- Expired session (complete without prior begin)
### Test data
Extend `setup_db.py` to create a user for WebAuthn tests:
- User with password credential (for logging in to register a passkey)
- The test flow: login with password -> register passkey -> logout -> login with passkey
### Files changed/created
| File | Change |
|------|--------|
| `tests/e2e/package.json` | Add `@playwright/test` dependency |
| `tests/e2e/playwright.config.js` | New: Playwright Test configuration |
| `tests/e2e/run.sh` | Update to use `npx playwright test` |
| `tests/e2e/helpers.js` | Remove (replaced by Playwright Test) |
| `tests/e2e/test_*.js` -> `*.spec.js` | Migrate all 7 tests to Playwright Test syntax |
| `tests/e2e/test_webauthn.spec.js` | New: WebAuthn E2E test suite |
| `tests/e2e/setup_db.py` | Add WebAuthn test user fixture |