-
Notifications
You must be signed in to change notification settings - Fork 453
Expand file tree
/
Copy pathsingle-session.test.ts
More file actions
202 lines (164 loc) · 7.53 KB
/
single-session.test.ts
File metadata and controls
202 lines (164 loc) · 7.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import { expect, test } from '@playwright/test';
import { appConfigs } from '../../presets';
import type { FakeUser } from '../../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../../testUtils';
/**
* Tests MemoryTokenCache cross-tab token sharing via BroadcastChannel
*
* This suite validates that when multiple browser tabs share the same user session,
* token fetches in one tab are automatically broadcast and cached in other tabs,
* eliminating redundant network requests.
*/
testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
'MemoryTokenCache Multi-Tab Integration @generic',
({ app }) => {
test.describe.configure({ mode: 'serial' });
let fakeUser: FakeUser;
test.beforeAll(async () => {
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser({
withPhoneNumber: true,
withUsername: true,
});
await u.services.users.createBapiUser(fakeUser);
});
test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});
/**
* Test Flow:
* 1. Open two tabs with the same browser context (shared cookies)
* 2. Sign in on tab1, which creates a session
* 3. Reload tab2 to pick up the session from cookies
* 4. Clear token cache on both tabs
* 5. Fetch token on tab1 (triggers network request + broadcast)
* 6. Fetch token on tab2 (should use broadcasted token, no network request)
*
* Expected Behavior:
* - Both tabs receive identical tokens
* - Only ONE network request is made (from tab1)
* - Tab2 gets the token via BroadcastChannel, proving cross-tab cache sharing
*/
test('multi-tab token sharing works when clearing the cache', async ({ context }) => {
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto(app.serverUrl);
await page2.goto(app.serverUrl);
await page1.waitForFunction(() => (window as any).Clerk?.loaded);
await page2.waitForFunction(() => (window as any).Clerk?.loaded);
const u1 = createTestUtils({ app, page: page1 });
await u1.po.signIn.goTo();
await u1.po.signIn.setIdentifier(fakeUser.email);
await u1.po.signIn.continue();
await u1.po.signIn.setPassword(fakeUser.password);
await u1.po.signIn.continue();
await u1.po.expect.toBeSignedIn();
// eslint-disable-next-line playwright/no-wait-for-timeout
await page1.waitForTimeout(1000);
await page2.reload();
await page2.waitForFunction(() => (window as any).Clerk?.loaded);
const u2 = createTestUtils({ app, page: page2 });
await u2.po.expect.toBeSignedIn();
const page1SessionInfo = await page1.evaluate(() => {
const clerk = (window as any).Clerk;
return {
sessionId: clerk?.session?.id,
userId: clerk?.user?.id,
};
});
expect(page1SessionInfo.sessionId).toBeDefined();
expect(page1SessionInfo.userId).toBeDefined();
await Promise.all([
page1.evaluate(() => (window as any).Clerk.session?.clearCache()),
page2.evaluate(() => (window as any).Clerk.session?.clearCache()),
]);
// Track token fetch requests to verify only one network call happens
const tokenRequests: string[] = [];
await context.route('**/v1/client/sessions/*/tokens*', async route => {
tokenRequests.push(route.request().url());
await route.continue();
});
const page1Token = await page1.evaluate(async () => {
const clerk = (window as any).Clerk;
return await clerk.session?.getToken({ skipCache: true });
});
expect(page1Token).toBeTruthy();
// Wait for broadcast to propagate between tabs (broadcast is nearly instant, but we add buffer)
// eslint-disable-next-line playwright/no-wait-for-timeout
await page2.waitForTimeout(2000);
const page2Result = await page2.evaluate(async () => {
const clerk = (window as any).Clerk;
const token = await clerk.session?.getToken();
return {
sessionId: clerk?.session?.id,
token,
userId: clerk?.user?.id,
};
});
expect(page2Result.sessionId).toBe(page1SessionInfo.sessionId);
expect(page2Result.userId).toBe(page1SessionInfo.userId);
// If BroadcastChannel worked, both tabs should have the EXACT same token
expect(page2Result.token).toBe(page1Token);
// Verify only one token fetch happened (page1), proving page2 got it from BroadcastChannel
expect(tokenRequests.length).toBe(1);
});
/**
* Test Flow:
* 1. Open two tabs with the same browser context (shared cookies)
* 2. Sign in on tab1, reload tab2 to pick up the session
* 3. Both tabs hydrate their token cache with the session token
* 4. Start counting /tokens requests, then wait for the timers to fire
* 5. Assert only 1 /tokens request was made (not 2)
*/
test('multi-tab scheduled refreshes are deduped to a single request', async ({ context }) => {
test.setTimeout(90_000);
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto(app.serverUrl);
await page2.goto(app.serverUrl);
await page1.waitForFunction(() => (window as any).Clerk?.loaded);
await page2.waitForFunction(() => (window as any).Clerk?.loaded);
const u1 = createTestUtils({ app, page: page1 });
await u1.po.signIn.goTo();
await u1.po.signIn.setIdentifier(fakeUser.email);
await u1.po.signIn.continue();
await u1.po.signIn.setPassword(fakeUser.password);
await u1.po.signIn.continue();
await u1.po.expect.toBeSignedIn();
// eslint-disable-next-line playwright/no-wait-for-timeout
await page1.waitForTimeout(1000);
await page2.reload();
await page2.waitForFunction(() => (window as any).Clerk?.loaded);
const u2 = createTestUtils({ app, page: page2 });
await u2.po.expect.toBeSignedIn();
// Both tabs are now signed in and have hydrated their token caches
// via Session constructor -> #hydrateCache, each with an independent
// onRefresh timer that fires at ~43s (TTL 60s - 15s leeway - 2s lead).
// Start counting /tokens requests from this point.
const refreshRequests: string[] = [];
await context.route('**/v1/client/sessions/*/tokens*', async route => {
refreshRequests.push(route.request().url());
await route.continue();
});
// Wait for proactive refresh timers to fire.
// Default token TTL is 60s; onRefresh fires at 60 - 15 - 2 = 43s from iat.
// We wait 50s to give comfortable buffer, this includes the broadcast delay.
//
// Uses page.evaluate instead of page.waitForTimeout to avoid
// the global actionTimeout (10s) silently capping the wait.
await page1.evaluate(() => new Promise(resolve => setTimeout(resolve, 50_000)));
// Only one tab should have made a /tokens request; the other tab should have
// received the refreshed token via BroadcastChannel.
expect(refreshRequests.length).toBe(1);
// Both tabs should still have valid tokens after the refresh cycle
const [page1Token, page2Token] = await Promise.all([
page1.evaluate(() => (window as any).Clerk.session?.getToken()),
page2.evaluate(() => (window as any).Clerk.session?.getToken()),
]);
expect(page1Token).toBeTruthy();
expect(page2Token).toBeTruthy();
expect(page1Token).toBe(page2Token);
});
},
);