Skip to content

Commit b8810a9

Browse files
authored
test(e2e): Add passkey flows using Playwright virtual authenticator (#9329)
1 parent 2f24826 commit b8810a9

6 files changed

Lines changed: 142 additions & 23 deletions

File tree

.changeset/passkey-e2e-suite.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

integration/.keys.json.sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
"pk": "",
6464
"sk": ""
6565
},
66+
"with-passkeys": {
67+
"pk": "",
68+
"sk": ""
69+
},
6670
"with-enterprise-sso": {
6771
"pk": "",
6872
"sk": ""

integration/presets/envs.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ const withProtectService = withInstanceKeys('with-protect-service', base.clone()
249249

250250
const withNeedsClientTrust = withInstanceKeys('with-needs-client-trust', base.clone().setId('withNeedsClientTrust'));
251251

252+
const withPasskeys = withInstanceKeys('with-passkeys', base.clone().setId('withPasskeys'));
253+
252254
export const envs = {
253255
base,
254256
sessionsProd1,
@@ -269,6 +271,7 @@ export const envs = {
269271
withKeyless,
270272
withLegalConsent,
271273
withNeedsClientTrust,
274+
withPasskeys,
272275
withRestrictedMode,
273276
withReverification,
274277
withSessionTasks,

integration/tests/passkeys.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { BrowserContext } from '@playwright/test';
2+
import { expect, test } from '@playwright/test';
3+
4+
import type { Application } from '../models/application';
5+
import { appConfigs } from '../presets';
6+
import type { FakeUser } from '../testUtils';
7+
import { createTestUtils } from '../testUtils';
8+
9+
type SavedCredential = {
10+
id: string;
11+
rpId: string;
12+
userHandle: string;
13+
privateKey: string;
14+
publicKey: string;
15+
};
16+
17+
// clerk-js gates WebAuthn behind isValidBrowser(), which bails when
18+
// navigator.webdriver is true, so mask it before installing the virtual authenticator.
19+
const installVirtualAuthenticator = async (context: BrowserContext) => {
20+
await context.addInitScript(() => {
21+
Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { get: () => false });
22+
});
23+
await context.credentials.install();
24+
};
25+
26+
test.describe('passkeys @generic', () => {
27+
test.describe.configure({ mode: 'serial' });
28+
29+
let app: Application;
30+
let fakeUser: FakeUser;
31+
let savedCredential: SavedCredential;
32+
33+
test.beforeAll(async () => {
34+
app = await appConfigs.next.appRouter.commit();
35+
await app.setup();
36+
await app.withEnv(appConfigs.envs.withPasskeys);
37+
await app.dev();
38+
const u = createTestUtils({ app });
39+
fakeUser = u.services.users.createFakeUser({ fictionalEmail: true, withPassword: true });
40+
await u.services.users.createBapiUser(fakeUser);
41+
});
42+
43+
test.afterAll(async () => {
44+
await fakeUser.deleteIfExists();
45+
await app.teardown();
46+
});
47+
48+
test('registers a passkey through UserProfile', async ({ page, context }) => {
49+
await installVirtualAuthenticator(context);
50+
51+
const u = createTestUtils({ app, page, context });
52+
await u.po.signIn.goTo();
53+
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email!, password: fakeUser.password });
54+
await u.po.expect.toBeSignedIn();
55+
56+
await u.po.userProfile.goTo();
57+
await u.po.userProfile.switchToSecurityTab();
58+
await u.page.getByRole('button', { name: /add a passkey/i }).click();
59+
60+
await expect(u.page.locator('.cl-profileSectionItem__passkeys')).toBeVisible();
61+
62+
const credentials = await context.credentials.get();
63+
expect(credentials).toHaveLength(1);
64+
savedCredential = credentials[0];
65+
});
66+
67+
test('signs in with passkey via conditional UI autofill', async ({ page, context }) => {
68+
await context.credentials.create(savedCredential.rpId, savedCredential);
69+
await installVirtualAuthenticator(context);
70+
71+
const u = createTestUtils({ app, page, context });
72+
await u.po.signIn.goTo();
73+
74+
// The start page requests the passkey with mediation: 'conditional' and the
75+
// virtual authenticator answers it, so the sign-in completes on its own.
76+
await u.po.expect.toBeSignedIn();
77+
});
78+
79+
test('signs in with passkey via identifier-first flow', async ({ page, context }) => {
80+
await installVirtualAuthenticator(context);
81+
82+
const u = createTestUtils({ app, page, context });
83+
await u.po.signIn.goTo();
84+
await u.po.signIn.setIdentifier(fakeUser.email!);
85+
await u.po.signIn.continue();
86+
87+
await expect(u.page.getByText('Use your passkey')).toBeVisible();
88+
// Seed only now: the start page's conditional-UI autofill is answered
89+
// instantly by the virtual authenticator and would preempt this flow.
90+
await context.credentials.create(savedCredential.rpId, savedCredential);
91+
await u.po.signIn.continue();
92+
93+
await u.po.expect.toBeSignedIn();
94+
});
95+
96+
test('signs in with a seeded passkey', async ({ page, context }) => {
97+
await installVirtualAuthenticator(context);
98+
99+
const u = createTestUtils({ app, page, context });
100+
await u.po.signIn.goTo();
101+
102+
const usePasskeyLink = u.page.getByRole('link', { name: /use passkey instead/i });
103+
await expect(usePasskeyLink).toBeVisible();
104+
// Seed after load for the same autofill-preemption reason as above.
105+
await context.credentials.create(savedCredential.rpId, savedCredential);
106+
await usePasskeyLink.click();
107+
108+
await u.po.expect.toBeSignedIn();
109+
});
110+
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@
8686
"@eslint/js": "9.31.0",
8787
"@faker-js/faker": "^9.9.0",
8888
"@octokit/rest": "^20.1.2",
89-
"@playwright/test": "^1.56.1",
89+
"@playwright/test": "^1.62.1",
9090
"@stylexjs/eslint-plugin": "0.19.0",
9191
"@testing-library/dom": "^10.1.0",
9292
"@testing-library/jest-dom": "^6.4.6",

pnpm-lock.yaml

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)