117 lines
4.2 KiB
Markdown
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 |
|