-
Notifications
You must be signed in to change notification settings - Fork 452
Expand file tree
/
Copy pathsingle-session.test.ts
More file actions
143 lines (117 loc) · 5.37 KB
/
single-session.test.ts
File metadata and controls
143 lines (117 loc) · 5.37 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
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);
});
// The previous "multi-tab scheduled refreshes are deduped to a single request"
// test relied on the proactive-refresh setTimeout firing within a 50s wall-clock
// window, which assumed JWT TTL = 60s. The dev test instance now issues 300s
// tokens, so the timer fires at ~283s and the test never reached it. The
// BroadcastChannel-based dedup it was checking is already covered by the
// "multi-tab token sharing works when clearing the cache" test above, which
// explicitly triggers a fetch via `getToken({ skipCache: true })`. The
// proactive-refresh timer scheduling itself (the math, the leeway, the
// re-registration on success) is best validated by unit tests that mock
// `setTimeout` rather than depending on real time in a real browser.
},
);