@@ -15,8 +15,8 @@ const WEBAUTHN_CREDENTIAL_ID_QUERY_PARAM = 'webauthnCredentialId';
1515test . use ( { browserName : 'chromium' } ) ;
1616
1717test . describe ( 'WebAuthn register, authenticate, and delete device' , ( ) => {
18- let cdp : CDPSession | undefined ;
19- let authenticatorId : string | undefined ;
18+ let cdp ! : CDPSession ;
19+ let authenticatorId ! : string ;
2020
2121 test . beforeEach ( async ( { context, page } ) => {
2222 cdp = await context . newCDPSession ( page ) ;
@@ -35,17 +35,11 @@ test.describe('WebAuthn register, authenticate, and delete device', () => {
3535 } ) ;
3636
3737 test . afterEach ( async ( ) => {
38- if ( cdp && authenticatorId ) {
39- await cdp . send ( 'WebAuthn.removeVirtualAuthenticator' , { authenticatorId } ) ;
40- await cdp . send ( 'WebAuthn.disable' ) ;
41- }
38+ await cdp . send ( 'WebAuthn.removeVirtualAuthenticator' , { authenticatorId } ) ;
39+ await cdp . send ( 'WebAuthn.disable' ) ;
4240 } ) ;
4341
4442 test ( 'should register, authenticate, and delete a device' , async ( { page } ) => {
45- if ( ! cdp || ! authenticatorId ) {
46- throw new Error ( 'Virtual authenticator was not initialized' ) ;
47- }
48-
4943 const { clickButton, navigate } = asyncEvents ( page ) ;
5044
5145 const registeredCredentialId =
@@ -113,3 +107,83 @@ test.describe('WebAuthn register, authenticate, and delete device', () => {
113107 } ) ;
114108 } ) ;
115109} ) ;
110+
111+ test . describe ( 'WebAuthn conditional autofill (passkey)' , ( ) => {
112+ let cdp ! : CDPSession ;
113+ let authenticatorId ! : string ;
114+
115+ test . beforeEach ( async ( { context, page } ) => {
116+ // Chromium + CDP WebAuthn virtual authenticator is required for repeatable automation.
117+ cdp = await context . newCDPSession ( page ) ;
118+ await cdp . send ( 'WebAuthn.enable' ) ;
119+
120+ // Configure a platform authenticator with resident keys and auto presence simulation.
121+ const response = await cdp . send ( 'WebAuthn.addVirtualAuthenticator' , {
122+ options : {
123+ protocol : 'ctap2' ,
124+ transport : 'internal' ,
125+ hasResidentKey : true ,
126+ hasUserVerification : true ,
127+ isUserVerified : true ,
128+ automaticPresenceSimulation : true ,
129+ } ,
130+ } ) ;
131+ authenticatorId = response . authenticatorId ;
132+ } ) ;
133+
134+ test . afterEach ( async ( ) => {
135+ await cdp . send ( 'WebAuthn.removeVirtualAuthenticator' , { authenticatorId } ) ;
136+ await cdp . send ( 'WebAuthn.disable' ) ;
137+ } ) ;
138+
139+ // TODO: This test is currently skipped because the journey used does not allow enabling conditional mediation in admin console
140+ // When we start using v2.0 of Page Node in admin console, this test can be executed again
141+ test . skip ( 'registers a passkey then authenticates via conditional autofill' , async ( { page } ) => {
142+ const { clickButton, navigate } = asyncEvents ( page ) ;
143+
144+ await test . step ( 'Register a WebAuthn credential' , async ( ) => {
145+ // Start with an empty virtual authenticator.
146+ const { credentials : initialCredentials } = await cdp . send ( 'WebAuthn.getCredentials' , {
147+ authenticatorId,
148+ } ) ;
149+ expect ( initialCredentials ) . toHaveLength ( 0 ) ;
150+
151+ // Run a registration journey that creates a credential in the authenticator.
152+ await navigate ( '/?clientId=tenant&journey=TEST_WebAuthn-Registration' ) ;
153+ await expect ( page . getByLabel ( 'User Name' ) ) . toBeVisible ( ) ;
154+ await page . getByLabel ( 'User Name' ) . fill ( username ) ;
155+ await page . getByLabel ( 'Password' ) . fill ( password ) ;
156+ await clickButton ( 'Submit' , '/authenticate' ) ;
157+ await expect ( page . getByRole ( 'button' , { name : 'Logout' } ) ) . toBeVisible ( ) ;
158+
159+ const { credentials } = await cdp . send ( 'WebAuthn.getCredentials' , { authenticatorId } ) ;
160+ expect ( credentials . length ) . toBeGreaterThan ( 0 ) ;
161+ } ) ;
162+
163+ await test . step ( 'Authenticate using conditional UI / passkey autofill' , async ( ) => {
164+ // Ensure we are not reusing an existing AM session.
165+ // This makes the test exercise passkey auth, not cookie auth.
166+ await page . context ( ) . clearCookies ( ) ;
167+
168+ // This journey emits conditional mediation metadata and should complete via background
169+ // WebAuthn (journey-app triggers the request and submits when a credential is returned).
170+ await navigate ( '/?clientId=tenant&journey=TEST_AutofillPasskeyWebAuthn' ) ;
171+
172+ const conditionalInput = page . locator ( 'input[autocomplete="webauthn"]' ) ;
173+ await expect ( conditionalInput ) . toBeVisible ( { timeout : 10000 } ) ;
174+ await conditionalInput . focus ( ) ;
175+ await expect ( conditionalInput ) . toBeFocused ( ) ;
176+
177+ // Re-enable presence simulation so the in-flight WebAuthn request can resolve.
178+ await cdp . send ( 'WebAuthn.setAutomaticPresenceSimulation' , {
179+ authenticatorId,
180+ enabled : true ,
181+ } ) ;
182+
183+ // With a virtual authenticator configured for automatic presence simulation, this should
184+ // complete without any manual click.
185+ await expect ( page . getByRole ( 'button' , { name : 'Logout' } ) ) . toBeVisible ( ) ;
186+ await expect ( page . getByRole ( 'heading' , { name : 'Complete' } ) ) . toBeVisible ( ) ;
187+ } ) ;
188+ } ) ;
189+ } ) ;
0 commit comments