4.2 KiB
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:
- Migrate all existing E2E tests from the custom runner to
@playwright/test - Add comprehensive WebAuthn E2E tests using CDP virtual authenticators
Phase 1: Migrate to @playwright/test
Infrastructure
- Add
@playwright/testtopackage.json - Create
playwright.config.jswith:baseURLfromTARGET_URLenv var (defaulthttp://localhost:8099)- Chromium-only project (WebAuthn CDP requires Chromium)
testDirpointing to the e2e directorytestMatchfor*.spec.jsfiles
- Update
run.shto callnpx playwright testinstead of looping overtest_*.js
Test conversion
Each test_*.js becomes *.spec.js:
run(async (page, assert) => { ... })becomestest('...', async ({ page }) => { ... })assert(condition, msg)becomesexpect(condition).toBeTruthy()or specific matchers- Shared setup moves to
test.beforeAll()/test.beforeEach() TARGET_URLusage replaced by Playwright'sbaseURL(use relative paths)
What stays the same
run.shstill starts the app, seeds data, runs tests, tears downsetup_db.pyunchanged- Test logic/assertions are equivalent
Files removed
helpers.js-- replaced by Playwright Test's built-in fixtures andexpect
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
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/credentialsafter 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 |