Passkeys are replacing passwords across the web, and if your test automation framework can’t handle WebAuthn flows, you’re already falling behind. Playwright 1.61 introduced browserContext.credentials — a virtual authenticator that lets you register, store, and use passkeys entirely in code. This guide walks you through everything you need to automate passkey login testing, with ready-to-use TypeScript examples.
🔐 What Is WebAuthn and Why Should QA Engineers Care?
WebAuthn (Web Authentication) is the W3C standard behind passkeys. Instead of passwords, users authenticate with biometrics (fingerprint, face recognition) or hardware security keys.
Major platforms — Google, Apple, Microsoft, and most modern SaaS products — now support passkey login. As a QA engineer or SDET, you’ll increasingly encounter apps that use WebAuthn, and your automation framework needs to test these flows reliably.
⚠️ The Problem: Traditional test tools couldn’t interact with hardware authenticators. You’d have to skip those tests or test them manually. Playwright 1.61 solves this completely with a built-in virtual authenticator!
📋 Prerequisites
Before we start, make sure you have:
- ✅ Node.js 18 or later
- ✅ Playwright 1.61+ installed
- ✅ Basic knowledge of TypeScript and Playwright
# Install or upgrade Playwright
npm install @playwright/test@latest
npx playwright install
# Verify version
npx playwright --version
⚙️ Setting Up the Virtual Authenticator
Playwright’s virtual authenticator is available through browserContext.credentials. No additional setup is required — it’s built into the browser context.
import { test, expect } from '@playwright/test';
test('virtual authenticator is available', async ({ context }) => {
// The credentials API is ready to use immediately
const credentials = await context.credentials.list();
expect(credentials).toEqual([]);
console.log('Virtual authenticator is active!');
});
💡 Key Insight: The virtual authenticator behaves like a FIDO2-compliant security key. When a website triggers a WebAuthn ceremony (registration or login), Playwright’s authenticator responds automatically — no user interaction needed!
🔑 Registering a Passkey in Your Tests
Here’s how to automate passkey registration:
import { test, expect } from '@playwright/test';
test('register a new passkey', async ({ context, page }) => {
// Navigate to your app's registration page
await page.goto('https://your-app.com/settings/security');
// Click the "Add Passkey" button
await page.getByRole('button', { name: 'Add Passkey' }).click();
// Playwright's virtual authenticator handles the WebAuthn ceremony
// Wait for confirmation
await expect(page.getByText('Passkey added successfully')).toBeVisible();
// Verify the credential was stored
const credentials = await context.credentials.list();
expect(credentials.length).toBe(1);
console.log('Credential ID:', credentials[0].id);
});
When the browser triggers navigator.credentials.create(), the virtual authenticator generates a key pair and responds with the public key credential — exactly as a real hardware key would.
🚀 Automating Passkey Login
Once a passkey is registered, testing the login flow is straightforward:
test('login with passkey', async ({ context, page }) => {
// Step 1: Register a passkey
await page.goto('https://your-app.com/register');
await page.getByRole('button', { name: 'Register with Passkey' }).click();
await expect(page.getByText('Passkey registered')).toBeVisible();
// Step 2: Navigate to login
await page.goto('https://your-app.com/login');
// Step 3: Sign in with passkey
await page.getByRole('button', { name: 'Sign in with Passkey' }).click();
// The virtual authenticator uses the stored credential
await expect(page.getByText('Welcome back')).toBeVisible();
await expect(page).toHaveURL(/dashboard/);
});
✅ No mocking, no bypassing — you’re testing the REAL WebAuthn implementation end to end. This is the biggest advantage over traditional approaches!
♻️ Reusing Credentials Across Tests with Storage State
Registering a passkey in every test is wasteful. Use Playwright’s storage state to authenticate once and reuse:
// auth.setup.ts
import { test as setup } from '@playwright/test';
setup('register and authenticate with passkey', async ({ context, page }) => {
await page.goto('https://your-app.com/register');
await page.getByRole('button', { name: 'Register with Passkey' }).click();
await expect(page.getByText('Passkey registered')).toBeVisible();
// Save authenticated state
await context.storageState({ path: '.auth/passkey-user.json' });
});
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'tests',
dependencies: ['setup'],
use: { storageState: '.auth/passkey-user.json' },
},
],
});
🚨 Common Mistakes and How to Avoid Them
❌ Mistake 1: Credentials don’t persist across browser contexts.
Each browserContext has its own virtual authenticator. If your test creates a new context, previously registered passkeys won’t be available. Solution: register and login within the same context, or use storage state.
❌ Mistake 2: Not waiting for the WebAuthn ceremony to complete.
The registration and authentication ceremonies are asynchronous. Clicking the next button before the ceremony finishes causes flaky tests. Always wait for a visible confirmation element.
❌ Mistake 3: Forgetting to await credentials API calls.
context.credentials.list() returns a Promise. Missing await leads to empty results and confusing test failures.
❓ FAQ
🤔 Can I test passkeys without a real WebAuthn-enabled app?
Yes! You can use demo sites like webauthn.io to practice. Our practice automation page also supports WebAuthn flows.
🤔 Does Playwright’s virtual authenticator work with all browsers?
The virtual authenticator works with Chromium-based browsers. Firefox and WebKit support may vary — check the Playwright 1.61 release notes for the latest compatibility.
🤔 Can I export and import passkey credentials between test runs?
Yes, context.credentials.list() returns credential objects that can be serialized. However, using storage state (shown above) is the recommended approach for persisting authenticated sessions.
🤔 What’s the difference between passkeys and FIDO2 security keys?
Passkeys are discoverable FIDO2 credentials synced across devices (through iCloud Keychain, Google Password Manager, etc.). Hardware security keys are physical devices. Playwright’s virtual authenticator simulates both.
🤔 Do I need to modify my app’s code to test with the virtual authenticator?
No. The virtual authenticator intercepts standard WebAuthn API calls at the browser level. Your app’s code doesn’t need any changes.
🎯 Next Steps
Now that you can automate passkey testing, try these:
- 🔧 Add passkey tests to your CI/CD pipeline
- 🧪 Test error scenarios (expired credentials, wrong authenticator)
- 📐 Combine passkey auth with your existing Page Object Model — see our Playwright POM tutorial
- 💾 Explore the new
page.localStorageandpage.sessionStorageAPIs in Playwright 1.61
🎬 Want to see this in action? Watch the full video tutorial!
Subscribe to SqaToolsByDY on YouTube for weekly QA automation tutorials
🌐 Practice on sqatools.in — free automation practice pages!