diff --git a/.changeset/passkey-e2e-suite.md b/.changeset/passkey-e2e-suite.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/passkey-e2e-suite.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 31a81470de1..6acba638337 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -249,6 +249,8 @@ const withProtectService = withInstanceKeys('with-protect-service', base.clone() const withNeedsClientTrust = withInstanceKeys('with-needs-client-trust', base.clone().setId('withNeedsClientTrust')); +const withPasskeys = withInstanceKeys('with-passkeys', base.clone().setId('withPasskeys')); + export const envs = { base, sessionsProd1, @@ -269,6 +271,7 @@ export const envs = { withKeyless, withLegalConsent, withNeedsClientTrust, + withPasskeys, withRestrictedMode, withReverification, withSessionTasks, diff --git a/integration/tests/passkeys.test.ts b/integration/tests/passkeys.test.ts new file mode 100644 index 00000000000..30435d028d1 --- /dev/null +++ b/integration/tests/passkeys.test.ts @@ -0,0 +1,110 @@ +import type { BrowserContext } from '@playwright/test'; +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils } from '../testUtils'; + +type SavedCredential = { + id: string; + rpId: string; + userHandle: string; + privateKey: string; + publicKey: string; +}; + +// clerk-js gates WebAuthn behind isValidBrowser(), which bails when +// navigator.webdriver is true, so mask it before installing the virtual authenticator. +const installVirtualAuthenticator = async (context: BrowserContext) => { + await context.addInitScript(() => { + Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', { get: () => false }); + }); + await context.credentials.install(); +}; + +test.describe('passkeys @generic', () => { + test.describe.configure({ mode: 'serial' }); + + let app: Application; + let fakeUser: FakeUser; + let savedCredential: SavedCredential; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter.commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withPasskeys); + await app.dev(); + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser({ fictionalEmail: true, withPassword: true }); + await u.services.users.createBapiUser(fakeUser); + }); + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('registers a passkey through UserProfile', async ({ page, context }) => { + await installVirtualAuthenticator(context); + + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email!, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + await u.po.userProfile.goTo(); + await u.po.userProfile.switchToSecurityTab(); + await u.page.getByRole('button', { name: /add a passkey/i }).click(); + + await expect(u.page.locator('.cl-profileSectionItem__passkeys')).toBeVisible(); + + const credentials = await context.credentials.get(); + expect(credentials).toHaveLength(1); + savedCredential = credentials[0]; + }); + + test('signs in with passkey via conditional UI autofill', async ({ page, context }) => { + await context.credentials.create(savedCredential.rpId, savedCredential); + await installVirtualAuthenticator(context); + + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + + // The start page requests the passkey with mediation: 'conditional' and the + // virtual authenticator answers it, so the sign-in completes on its own. + await u.po.expect.toBeSignedIn(); + }); + + test('signs in with passkey via identifier-first flow', async ({ page, context }) => { + await installVirtualAuthenticator(context); + + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + await u.po.signIn.setIdentifier(fakeUser.email!); + await u.po.signIn.continue(); + + await expect(u.page.getByText('Use your passkey')).toBeVisible(); + // Seed only now: the start page's conditional-UI autofill is answered + // instantly by the virtual authenticator and would preempt this flow. + await context.credentials.create(savedCredential.rpId, savedCredential); + await u.po.signIn.continue(); + + await u.po.expect.toBeSignedIn(); + }); + + test('signs in with a seeded passkey', async ({ page, context }) => { + await installVirtualAuthenticator(context); + + const u = createTestUtils({ app, page, context }); + await u.po.signIn.goTo(); + + const usePasskeyLink = u.page.getByRole('link', { name: /use passkey instead/i }); + await expect(usePasskeyLink).toBeVisible(); + // Seed after load for the same autofill-preemption reason as above. + await context.credentials.create(savedCredential.rpId, savedCredential); + await usePasskeyLink.click(); + + await u.po.expect.toBeSignedIn(); + }); +}); diff --git a/package.json b/package.json index ae0ce1cb7e5..0aa0af3a51f 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "@eslint/js": "9.31.0", "@faker-js/faker": "^9.9.0", "@octokit/rest": "^20.1.2", - "@playwright/test": "^1.56.1", + "@playwright/test": "^1.62.1", "@stylexjs/eslint-plugin": "0.19.0", "@testing-library/dom": "^10.1.0", "@testing-library/jest-dom": "^6.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d071c614880..8fef2cf5974 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,8 +151,8 @@ importers: specifier: ^20.1.2 version: 20.1.2 '@playwright/test': - specifier: ^1.56.1 - version: 1.56.1 + specifier: ^1.62.1 + version: 1.62.1 '@stylexjs/eslint-plugin': specifier: 0.19.0 version: 0.19.0 @@ -847,7 +847,7 @@ importers: version: 2.13.6(@types/node@25.6.0)(typescript@6.0.3) next: specifier: '>=15.5.18' - version: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -881,7 +881,7 @@ importers: version: 2.1.0 next: specifier: 15.5.19 - version: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) packages/nuxt: dependencies: @@ -1071,7 +1071,7 @@ importers: version: 1.17.0(react@18.3.1) next: specifier: ^15.0.0 - version: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1191,7 +1191,7 @@ importers: devDependencies: '@playwright/test': specifier: ^1.56.1 - version: 1.56.1 + version: 1.62.1 cypress: specifier: ^14.5.4 version: 14.5.4 @@ -4835,9 +4835,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.56.1': - resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} - engines: {node: '>=18'} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} hasBin: true '@polka/url@1.0.0-next.29': @@ -12524,14 +12524,14 @@ packages: engines: {node: '>=18'} hasBin: true - playwright-core@1.56.1: - resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} - engines: {node: '>=18'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} hasBin: true - playwright@1.56.1: - resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} - engines: {node: '>=18'} + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} hasBin: true plist@3.1.0: @@ -19467,9 +19467,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.56.1': + '@playwright/test@1.62.1': dependencies: - playwright: 1.56.1 + playwright: 1.62.1 '@polka/url@1.0.0-next.29': {} @@ -28306,7 +28306,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.56.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.5.19(@babel/core@7.29.7)(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 15.5.19 '@swc/helpers': 0.5.15 @@ -28325,7 +28325,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 15.5.19 '@next/swc-win32-x64-msvc': 15.5.19 '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.56.1 + '@playwright/test': 1.62.1 babel-plugin-react-compiler: 1.0.0 sharp: 0.34.5 transitivePeerDependencies: @@ -29307,11 +29307,11 @@ snapshots: pkglab-linux-arm64: 0.17.1 pkglab-linux-x64: 0.17.1 - playwright-core@1.56.1: {} + playwright-core@1.62.1: {} - playwright@1.56.1: + playwright@1.62.1: dependencies: - playwright-core: 1.56.1 + playwright-core: 1.62.1 optionalDependencies: fsevents: 2.3.2