@@ -226,5 +226,106 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
226226 expect ( tab1FinalInfo . userId ) . toBe ( user1SessionInfo . userId ) ;
227227 expect ( tab1FinalInfo . activeSessionId ) . toBe ( user1SessionInfo . sessionId ) ;
228228 } ) ;
229+
230+ /**
231+ * Test Flow:
232+ * 1. Tab1: Sign in as user1
233+ * 2. Tab2: Inherits user1's session, then signs in as user2 (multi-session)
234+ * 3. Tab1 has user1's active session; tab2 has user2's active session
235+ * 4. Each tab's active session independently hydrates its token cache
236+ * 5. Start counting /tokens requests, wait for both refresh timers to fire
237+ * 6. Assert exactly 2 /tokens requests (one per session), with each session
238+ * represented exactly once
239+ *
240+ * Expected Behavior:
241+ * - Two different sessions produce two independent refresh requests
242+ * - BroadcastChannel does NOT deduplicate across sessions (different tokenIds)
243+ * - Each session refreshes exactly once
244+ *
245+ * Note that this test does not currently assert in which tab the updates happen,
246+ * this might be something we want to add in the future, but currently it is not
247+ * deterministic.
248+ */
249+ test ( 'multi-session scheduled refreshes produce one request per session' , async ( { context } ) => {
250+ test . setTimeout ( 90_000 ) ;
251+
252+ const page1 = await context . newPage ( ) ;
253+ await page1 . goto ( app . serverUrl ) ;
254+ await page1 . waitForFunction ( ( ) => ( window as any ) . Clerk ?. loaded ) ;
255+
256+ const u1 = createTestUtils ( { app, page : page1 } ) ;
257+ await u1 . po . signIn . goTo ( ) ;
258+ await u1 . po . signIn . setIdentifier ( fakeUser1 . email ) ;
259+ await u1 . po . signIn . continue ( ) ;
260+ await u1 . po . signIn . setPassword ( fakeUser1 . password ) ;
261+ await u1 . po . signIn . continue ( ) ;
262+ await u1 . po . expect . toBeSignedIn ( ) ;
263+
264+ const user1SessionId = await page1 . evaluate ( ( ) => ( window as any ) . Clerk ?. session ?. id ) ;
265+ expect ( user1SessionId ) . toBeDefined ( ) ;
266+
267+ const page2 = await context . newPage ( ) ;
268+ await page2 . goto ( app . serverUrl ) ;
269+ await page2 . waitForFunction ( ( ) => ( window as any ) . Clerk ?. loaded ) ;
270+
271+ // eslint-disable-next-line playwright/no-wait-for-timeout
272+ await page2 . waitForTimeout ( 1000 ) ;
273+
274+ const u2 = createTestUtils ( { app, page : page2 } ) ;
275+ await u2 . po . expect . toBeSignedIn ( ) ;
276+
277+ // Sign in as user2 on tab2, creating a second session
278+ const signInResult = await page2 . evaluate (
279+ async ( { email, password } ) => {
280+ const clerk = ( window as any ) . Clerk ;
281+ const signIn = await clerk . client . signIn . create ( { identifier : email , password } ) ;
282+ await clerk . setActive ( { session : signIn . createdSessionId } ) ;
283+ return {
284+ sessionCount : clerk ?. client ?. sessions ?. length || 0 ,
285+ sessionId : clerk ?. session ?. id ,
286+ success : true ,
287+ } ;
288+ } ,
289+ { email : fakeUser2 . email , password : fakeUser2 . password } ,
290+ ) ;
291+
292+ expect ( signInResult . success ) . toBe ( true ) ;
293+ expect ( signInResult . sessionCount ) . toBe ( 2 ) ;
294+
295+ const user2SessionId = signInResult . sessionId ;
296+ expect ( user2SessionId ) . toBeDefined ( ) ;
297+ expect ( user2SessionId ) . not . toBe ( user1SessionId ) ;
298+
299+ // Tab1 has user1's active session; tab2 has user2's active session.
300+ // Start counting /tokens requests.
301+ const refreshRequests : Array < { sessionId : string ; url : string } > = [ ] ;
302+ await context . route ( '**/v1/client/sessions/*/tokens*' , async route => {
303+ const url = route . request ( ) . url ( ) ;
304+ const match = url . match ( / s e s s i o n s \/ ( [ ^ / ] + ) \/ t o k e n s / ) ;
305+ refreshRequests . push ( { sessionId : match ?. [ 1 ] || 'unknown' , url } ) ;
306+ await route . continue ( ) ;
307+ } ) ;
308+
309+ // Wait for proactive refresh timers to fire.
310+ // Default token TTL is 60s; onRefresh fires at 60 - 15 - 2 = 43s from iat.
311+ // Uses page.evaluate to avoid the global actionTimeout (10s) capping the wait.
312+ await page1 . evaluate ( ( ) => new Promise ( resolve => setTimeout ( resolve , 50_000 ) ) ) ;
313+
314+ // Two different sessions should each produce exactly one refresh request.
315+ // BroadcastChannel deduplication is per-tokenId, so different sessions refresh independently.
316+ expect ( refreshRequests . length ) . toBe ( 2 ) ;
317+
318+ const refreshedSessionIds = new Set ( refreshRequests . map ( r => r . sessionId ) ) ;
319+ expect ( refreshedSessionIds . has ( user1SessionId ) ) . toBe ( true ) ;
320+ expect ( refreshedSessionIds . has ( user2SessionId ) ) . toBe ( true ) ;
321+
322+ // Both tabs should still have valid tokens after the refresh cycle
323+ const page1Token = await page1 . evaluate ( ( ) => ( window as any ) . Clerk . session ?. getToken ( ) ) ;
324+ const page2Token = await page2 . evaluate ( ( ) => ( window as any ) . Clerk . session ?. getToken ( ) ) ;
325+
326+ expect ( page1Token ) . toBeTruthy ( ) ;
327+ expect ( page2Token ) . toBeTruthy ( ) ;
328+ expect ( page1Token ) . not . toBe ( page2Token ) ;
329+ } ) ;
229330 } ,
230331) ;
0 commit comments