·6 min read
Playwright v1.60 Turns Test Failures Into Evidence
Playwright v1.60 adds scoped HAR recording, locator.drop(), ARIA boxes, and test.abort() so CI failures carry better proof.

Published: · 5 min read
Playwright 1.61 added a virtual authenticator. You can now test passkey login with no hardware key, in every browser. Here is a complete working test.
On this page
You can now test passkey login in Playwright with no hardware key. Playwright 1.61 added a virtual authenticator (a fake security key). Your test seeds a passkey, turns it on, and the page signs in as if a real key answered. It works in every browser and runs in CI. The API is browserContext.credentials, with three methods: create(), install(), and get().
A passkey is a login with no password. You sign in with your face, your fingerprint, or a device PIN. The hard part lives in your device. The website only sees a signed reply.
The browser standard behind this is called WebAuthn. It means "web authentication". When you log in, the browser runs a small back-and-forth with the site. The site asks. Your device answers and signs. This is the part that used to need real hardware.
Apple, Google, and most banks ship passkeys now. If your app has a "sign in with a passkey" button, you have this flow in production today.
Here is the part nobody talks about. Almost nobody tests the passkey login.
For years it was hard. To test a passkey you needed a real security key plugged into the machine. You cannot plug a USB key into a CI server. CI is a remote build machine with no hands and no ports.
So the most important login flow shipped untested. The one thing a user does first. The one thing that locks them out if it breaks.
I test software for a living. An untested login is the scariest gap on the list. If sign-in breaks, nothing else matters. The cart, the dashboard, the settings page, all of it sits behind the door. On one project I watched a broken auth path block every other test for two days. The fix took ten minutes. Finding it took the two days.
Playwright 1.61 closed this gap.
Playwright 1.61 shipped on June 15, 2026. It added a virtual authenticator (a fake security key).
A virtual authenticator is software that pretends to be a hardware key. Your test creates one. It seeds a passkey into it. From then on, when the page calls the browser to sign in, Playwright answers for the key. No real device. No USB port. The page cannot tell the difference.
You reach it through a new class called Credentials, on the browser context: browserContext.credentials. It has three methods you will use most:
create() seeds a test passkey for a site.install() turns the virtual key on for the page.get() reads back any passkey the page registered, so you can save it and reuse it later.This works in all three browser engines Playwright drives. So one test covers Chromium, Firefox, and WebKit.
Here is a complete test. It seeds a passkey, turns on the virtual key, then signs in. Read the comments for what each step does.
// passkey-login.spec.ts
// Tested against Playwright 1.61. Run with: npx playwright test
import { test, expect } from '@playwright/test';
test('user signs in with a passkey', async ({ browser }) => {
// A fresh, clean browser session for this test.
const context = await browser.newContext();
// STEP 1 — Seed a passkey for our site.
// 'example.com' is the site domain (the "relying party id").
// With only the domain, Playwright makes a fresh key for us.
await context.credentials.create('example.com');
// STEP 2 — Turn the virtual key on.
// From now on, the page's sign-in calls are answered by our key,
// not by real hardware. Call this before the page loads.
await context.credentials.install();
// STEP 3 — Let the page use it.
const page = await context.newPage();
await page.goto('https://example.com/login');
// The page calls the browser to sign in. Our key answers.
await page.getByRole('button', { name: 'Sign in with a passkey' }).click();
// Check the user is in.
await expect(page.getByText('Welcome back')).toBeVisible();
});
The three steps map to the three method calls. First you create() a passkey for your site. Then you install() the virtual key, which makes the page's sign-in calls run through it. Then the page does its normal login, and your key answers in place of hardware.
One note on order. Call install() before the page touches sign-in. The virtual key only answers calls that happen after you turn it on.
Often you want to register a passkey once, then reuse it in many tests. A passkey holds a private key (a secret only your device knows). You can read that secret back with get() and seed it into a later test.
// In a setup test: register once, then read the passkey back.
const created = await context.credentials.get({ rpId: 'example.com' });
// `created` holds the passkey fields, including its keys.
// Save them, then seed an identical passkey in a later test:
await otherContext.credentials.create('example.com', {
id: created[0].id,
userHandle: created[0].userHandle,
privateKey: created[0].privateKey,
publicKey: created[0].publicKey,
});
This is how you keep one stable test user across a whole suite. You do not re-register on every test. You seed the same passkey each run. See the Credentials docs for the full field list.
A virtual key is not a real key. So this approach tests your login flow, not the physical hardware. It will not catch a bug in a specific phone's secure chip or a real fingerprint reader. It tests the part you own: the page, the back-and-forth, the server check.
For most teams that is the right line. The browser and the operating system test the hardware path for you. Your job is to test that your app asks the right question and trusts the right answer. That is exactly what the virtual key lets you do, in CI, on every push.
Before 1.61, your passkey login had two test options. Skip it, or test it by hand. Both are bad. A skipped test means a silent break. A by-hand test runs once a release, not once a push.
Now it runs like any other test. It sits in your suite. It runs on every pull request. If someone changes the login and breaks the passkey path, the build goes red before the change ships. That is the whole point of a test. Catch the break in seconds, not from an angry user.
If your app supports passkeys, this is the test you write this week. The excuse is gone. The login everyone ships and nobody verifies is now testable.
Related reads on anton.qa:
Official sources:
Anton Gulin is the AI QA Architect — the first person to claim this title on LinkedIn. He builds AI-powered test automation systems where AI agents and human engineers collaborate on quality. Former Apple SDET (Apple.com / Apple Card pre-release testing). Find him at anton.qa or on LinkedIn.
Get notified when I publish something new, and unsubscribe at any time.
·6 min read
Playwright v1.60 adds scoped HAR recording, locator.drop(), ARIA boxes, and test.abort() so CI failures carry better proof.

·4 min read
AI Test Automation Architecture: The 3-Layer System AI test automation architecture is the system that tells AI what to test. It also defines how to run tests and prove the result. I split it into three layers: orchestration, execution, and evidence. Without all three, AI testing becomes prompt output with no production gate.

·7 min read
Most teams install MCP servers and hope they work. Here is how to test, evaluate, and gate MCP servers before they break your CI pipeline.
