@@ -182,3 +182,89 @@ export const testSSR = async ({ app, page, context, fakeUser }: TestParams) => {
182182
183183 expect ( await u . po . userButton . waitForMounted ( ) ) . not . toBeUndefined ( ) ;
184184} ;
185+
186+ export const testSignOut = async ( { app, page, context, fakeUser } : TestParams ) => {
187+ const u = createTestUtils ( { app, page, context, useTestingToken : false } ) ;
188+
189+ // Sign in via Account Portal first
190+ await u . page . goToAppHome ( ) ;
191+ await u . page . waitForClerkJsLoaded ( ) ;
192+ await u . po . expect . toBeSignedOut ( ) ;
193+
194+ await u . page . getByRole ( 'button' , { name : / S i g n i n / i } ) . click ( ) ;
195+ await u . po . signIn . waitForMounted ( ) ;
196+ await u . po . signIn . signInWithEmailAndInstantPassword ( { email : fakeUser . email , password : fakeUser . password } ) ;
197+ await u . page . waitForAppUrl ( '/' ) ;
198+ await u . po . expect . toBeSignedIn ( ) ;
199+ await u . po . userButton . waitForMounted ( ) ;
200+
201+ // Verify session cookie is set before sign-out
202+ const sessionBefore = await context
203+ . cookies ( page . url ( ) )
204+ . then ( cookies => cookies . find ( c => c . name === CLERK_SESSION_COOKIE_NAME ) ?. value ) ;
205+ expect ( ! ! sessionBefore ) . toBeTruthy ( ) ;
206+
207+ // Sign out via Clerk.signOut()
208+ await page . evaluate ( ( ) => window . Clerk . signOut ( ) ) ;
209+ await u . po . expect . toBeSignedOut ( ) ;
210+
211+ // Verify session cookie is cleared
212+ const sessionAfter = await context
213+ . cookies ( page . url ( ) )
214+ . then ( cookies => cookies . find ( c => c . name === CLERK_SESSION_COOKIE_NAME ) ?. value ) ;
215+ expect ( ! ! sessionAfter ) . toBeFalsy ( ) ;
216+
217+ // Reload and verify user stays signed out (no auto-sign-in from stale state)
218+ await u . page . goToAppHome ( ) ;
219+ await u . page . waitForClerkJsLoaded ( ) ;
220+ await u . po . expect . toBeSignedOut ( ) ;
221+
222+ // Navigate to AP again and verify sign-in form is shown (not auto-signed-in)
223+ await u . page . getByRole ( 'button' , { name : / S i g n i n / i } ) . click ( ) ;
224+ await u . po . signIn . waitForMounted ( ) ;
225+ const apURL = page . url ( ) ;
226+ expect ( apURL ) . toMatch ( / \. a c c o u n t s ( s t a g e \. d e v | \. d e v | \. s t g ) / ) ;
227+ } ;
228+
229+ export const testHandshakeRecovery = async ( { app, page, context, fakeUser } : TestParams ) => {
230+ const u = createTestUtils ( { app, page, context, useTestingToken : false } ) ;
231+
232+ // Sign in via Account Portal
233+ await u . page . goToAppHome ( ) ;
234+ await u . page . waitForClerkJsLoaded ( ) ;
235+ await u . po . expect . toBeSignedOut ( ) ;
236+
237+ await u . page . getByRole ( 'button' , { name : / S i g n i n / i } ) . click ( ) ;
238+ await u . po . signIn . waitForMounted ( ) ;
239+ await u . po . signIn . signInWithEmailAndInstantPassword ( { email : fakeUser . email , password : fakeUser . password } ) ;
240+ await u . page . waitForAppUrl ( '/' ) ;
241+ await u . po . expect . toBeSignedIn ( ) ;
242+
243+ // Delete the __session cookie to simulate an expired/invalid session.
244+ // Keep __client_uat so the middleware detects a mismatch and triggers a handshake.
245+ const appUrl = new URL ( page . url ( ) ) ;
246+ await context . clearCookies ( { name : CLERK_SESSION_COOKIE_NAME , domain : appUrl . hostname } ) ;
247+
248+ // Reload the page. The middleware should:
249+ // 1. Detect missing session + present client_uat
250+ // 2. Trigger a handshake redirect to FAPI
251+ // 3. FAPI resolves the handshake and returns fresh cookies
252+ // 4. User ends up signed in again (no redirect loop, no error)
253+ await u . page . goToAppHome ( ) ;
254+ await u . page . waitForClerkJsLoaded ( ) ;
255+
256+ // The page should load successfully (not stuck in a redirect loop).
257+ // The user should be signed in because the handshake recovered the session.
258+ await u . po . expect . toBeSignedIn ( ) ;
259+
260+ // Verify the session cookie was re-established by the handshake
261+ const sessionAfterRecovery = await context
262+ . cookies ( page . url ( ) )
263+ . then ( cookies => cookies . find ( c => c . name === CLERK_SESSION_COOKIE_NAME ) ?. value ) ;
264+ expect ( ! ! sessionAfterRecovery ) . toBeTruthy ( ) ;
265+
266+ // Verify no leftover handshake params in the URL
267+ const finalURL = new URL ( page . url ( ) ) ;
268+ expect ( finalURL . searchParams . has ( '__clerk_handshake' ) ) . toBeFalsy ( ) ;
269+ expect ( finalURL . searchParams . has ( '__clerk_handshake_nonce' ) ) . toBeFalsy ( ) ;
270+ } ;
0 commit comments