iTranslated by AI
The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🎭
Simulating Passkey (WebAuthn) Authentication and Login in Playwright E2E Tests
What I want to achieve
- Reproduce authentication and login using passkeys (WebAuthn) in Playwright tests
Approach
Use Playwright's CDPSession to enable Chrome's Virtual Authenticators.
Implementation
Create a CDP session with newCDPSession and add a Virtual Authenticator using addVirtualAuthenticator. Once added, there's no need for further manual operation; it automatically simulates a WebAuthn Authenticator during tests.
Challenges encountered
- When you want to reproduce passkey authentication (Discoverable credentials), you must specify
hasResidentKeywhen passingaddVirtualAuthenticatorto the cdpSession. - While Authenticator behavior is simulated automatically, it can sometimes be extremely slow, so it is necessary to include longer wait times.
- This might be especially noticeable in resource-constrained environments like CI.
*.test.ts
import { test, expect } from "@playwright/test";
test.describe.serial("Authentications & API Calling", () => {
let savedCookies: Cookie[] = [];
test("Sign up => Sign in with passkey (WebAuthn)", async ({ browserName, page }, testInfo) => {
// Currently, only Chrome supports Virtual Authenticator
test.skip(browserName !== "chromium", "This test runs only in Chromium");
await page.goto("/");
// Add Virtual Authenticator
const cdpSession = await page.context().newCDPSession(page);
await cdpSession.send("WebAuthn.enable");
await cdpSession.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
ctap2Version: "ctap2_1",
hasUserVerification: true,
transport: "internal",
automaticPresenceSimulation: true,
isUserVerified: true,
// Be careful: This must be enabled for passkey authentication (authentication via WebAuthn alone, without entering a username)
hasResidentKey: true,
}
});
cdpSession.on("WebAuthn.credentialAdded", () => {
console.log("Credential Added!");
});
// Sign up (please modify this part to fit your environment)
await page.click("#sign-up-button");
await page.fill("#name", "Test User");
await page.click("#terms");
const beforeSignUpScreenshot = await page.screenshot();
await testInfo.attach("Before sign-up", {
body: beforeSignUpScreenshot,
contentType: "image/png",
});
await page.click("#sign-up-submit-button");
await page.waitForTimeout(10);
const afterSignUpScreenshot = await page.screenshot();
await testInfo.attach("After click sign-up button", {
body: afterSignUpScreenshot,
contentType: "image/png",
});
// Insert a longer wait time as it can be very slow
await page.waitForTimeout(8000);
await page.reload();
// Sign in
await page.click("#button-sign-in-with-passkey");
await page.waitForTimeout(500);
const afterSignInScreenshot = await page.screenshot();
await testInfo.attach("After click sign-in button", {
body: afterSignInScreenshot,
contentType: "image/png",
});
// Insert a longer wait time as it can be very slow
await page.waitForTimeout(8000);
const appScreenshot = await page.screenshot();
await testInfo.attach("After sign-in", {
body: appScreenshot,
contentType: "image/png",
});
// Now just verify if login was successful!
expect(await page.locator("#my-app-sidebar").count()).toBeGreaterThan(0);
// Save cookies
savedCookies = await page.context().cookies();
});
test("Test requiring authentication", async ({ browserName, page }, testInfo) => {
// This is also required here
test.skip(browserName !== "chromium", "This test runs only in Chromium");
await page.context().addInitScript(() => {
// Set localStorage
window.localStorage.setItem("isLoggedIn", "true");
});
// Restore signed-in state
await page.context().addCookies(savedCookies);
// ...rest omitted
});
});
If it doesn't work well
- Depending on the environment, registration and authentication can take a long time, so try increasing the wait times.
- Try verifying if it works using the Chrome DevTools Virtual Authenticator.
- Compare the parameters set there with the options passed to
addVirtualAuthenticatorand try adjusting the test options.
- Compare the parameters set there with the options passed to
- Check for errors on the client side by adding
page.on("console", msg => console.log(msg.text()));. - Investigate what is happening by attaching screenshots to the report using the following code:
const homeScreenshot = await page.screenshot(); await testInfo.attach("Top page", { body: homeScreenshot, contentType: "image/png", });
Discussion