@interfere/next 0.0.0-alpha.10 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/__tests__/build/with-interfere-coverage.test.d.ts +2 -0
- package/dist/__tests__/build/with-interfere-coverage.test.d.ts.map +1 -0
- package/dist/__tests__/build/with-interfere-coverage.test.js +338 -0
- package/dist/__tests__/build/with-interfere-coverage.test.js.map +1 -0
- package/dist/__tests__/build/with-interfere.test.d.ts +2 -0
- package/dist/__tests__/build/with-interfere.test.d.ts.map +1 -0
- package/dist/__tests__/build/with-interfere.test.js +466 -0
- package/dist/__tests__/build/with-interfere.test.js.map +1 -0
- package/dist/__tests__/core/client.test.d.ts.map +1 -0
- package/dist/__tests__/core/client.test.js +373 -0
- package/dist/__tests__/core/client.test.js.map +1 -0
- package/dist/__tests__/core/encoders.test.d.ts.map +1 -0
- package/dist/{core → __tests__/core}/encoders.test.js +20 -19
- package/dist/__tests__/core/encoders.test.js.map +1 -0
- package/dist/__tests__/core/rage-click.test.d.ts +2 -0
- package/dist/__tests__/core/rage-click.test.d.ts.map +1 -0
- package/dist/__tests__/core/rage-click.test.js +121 -0
- package/dist/__tests__/core/rage-click.test.js.map +1 -0
- package/dist/__tests__/core/session-manager.test.d.ts +2 -0
- package/dist/__tests__/core/session-manager.test.d.ts.map +1 -0
- package/dist/__tests__/core/session-manager.test.js +1132 -0
- package/dist/__tests__/core/session-manager.test.js.map +1 -0
- package/dist/__tests__/integration/release-upload.test.d.ts +2 -0
- package/dist/__tests__/integration/release-upload.test.d.ts.map +1 -0
- package/dist/__tests__/integration/release-upload.test.js +173 -0
- package/dist/__tests__/integration/release-upload.test.js.map +1 -0
- package/dist/__tests__/session/persistence.test.d.ts +2 -0
- package/dist/__tests__/session/persistence.test.d.ts.map +1 -0
- package/dist/__tests__/session/persistence.test.js +129 -0
- package/dist/__tests__/session/persistence.test.js.map +1 -0
- package/dist/__tests__/session/session-summary.test.d.ts +2 -0
- package/dist/__tests__/session/session-summary.test.d.ts.map +1 -0
- package/dist/__tests__/session/session-summary.test.js +763 -0
- package/dist/__tests__/session/session-summary.test.js.map +1 -0
- package/dist/build/index.d.ts +3 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +2 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/with-interfere.d.ts +54 -0
- package/dist/build/with-interfere.d.ts.map +1 -0
- package/dist/build/with-interfere.js +267 -0
- package/dist/build/with-interfere.js.map +1 -0
- package/dist/core/client-core.d.ts +27 -0
- package/dist/core/client-core.d.ts.map +1 -0
- package/dist/core/client-core.js +164 -0
- package/dist/core/client-core.js.map +1 -0
- package/dist/core/client.d.ts +70 -18
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +112 -104
- package/dist/core/client.js.map +1 -1
- package/dist/core/constants.d.ts +12 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +17 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/debug.d.ts +47 -0
- package/dist/core/debug.d.ts.map +1 -0
- package/dist/core/debug.js +79 -0
- package/dist/core/debug.js.map +1 -0
- package/dist/core/error-handlers.d.ts.map +1 -1
- package/dist/core/error-handlers.js +42 -41
- package/dist/core/error-handlers.js.map +1 -1
- package/dist/core/runtime.d.ts +7 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +16 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -7
- package/dist/index.js.map +1 -1
- package/dist/next/middleware.d.ts +1 -1
- package/dist/next/middleware.js +13 -13
- package/dist/persistence/storage.d.ts +5 -0
- package/dist/persistence/storage.d.ts.map +1 -0
- package/dist/persistence/storage.js +67 -0
- package/dist/persistence/storage.js.map +1 -0
- package/dist/react/provider.d.ts +9 -5
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react/provider.jsx +33 -10
- package/dist/react/provider.jsx.map +1 -1
- package/dist/session/constants.d.ts +19 -0
- package/dist/session/constants.d.ts.map +1 -0
- package/dist/session/constants.js +34 -0
- package/dist/session/constants.js.map +1 -0
- package/dist/session/persistence.d.ts +58 -0
- package/dist/session/persistence.d.ts.map +1 -0
- package/dist/session/persistence.js +180 -0
- package/dist/session/persistence.js.map +1 -0
- package/dist/session/rage-click.d.ts +17 -0
- package/dist/session/rage-click.d.ts.map +1 -0
- package/dist/session/rage-click.js +104 -0
- package/dist/session/rage-click.js.map +1 -0
- package/dist/session/replay.d.ts +2 -2
- package/dist/session/replay.d.ts.map +1 -1
- package/dist/session/replay.js +57 -24
- package/dist/session/replay.js.map +1 -1
- package/dist/session/session-manager.d.ts +126 -0
- package/dist/session/session-manager.d.ts.map +1 -0
- package/dist/session/session-manager.js +635 -0
- package/dist/session/session-manager.js.map +1 -0
- package/dist/session/session-summary.d.ts +2 -2
- package/dist/session/session-summary.d.ts.map +1 -1
- package/dist/session/session-summary.js +94 -47
- package/dist/session/session-summary.js.map +1 -1
- package/dist/types/storage.d.ts +7 -0
- package/dist/types/storage.d.ts.map +1 -0
- package/dist/types/storage.js +2 -0
- package/dist/types/storage.js.map +1 -0
- package/package.json +24 -7
- package/dist/core/client.test.d.ts.map +0 -1
- package/dist/core/client.test.js +0 -238
- package/dist/core/client.test.js.map +0 -1
- package/dist/core/encoders.test.d.ts.map +0 -1
- package/dist/core/encoders.test.js.map +0 -1
- package/dist/next/error-boundary.d.ts +0 -17
- package/dist/next/error-boundary.d.ts.map +0 -1
- package/dist/next/error-boundary.jsx +0 -32
- package/dist/next/error-boundary.jsx.map +0 -1
- /package/dist/{core → __tests__/core}/client.test.d.ts +0 -0
- /package/dist/{core → __tests__/core}/encoders.test.d.ts +0 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { sessionStore } from "../../persistence/storage.js";
|
|
3
|
+
import { DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS, MAX_SESSION_IDLE_TIMEOUT_SECONDS, SESSION_ID, toMs, } from "../../session/constants.js";
|
|
4
|
+
import { SessionChangeReason, SessionIdManager, } from "../../session/session-manager.js";
|
|
5
|
+
// Mock dependencies
|
|
6
|
+
vi.mock("../storage/index.js");
|
|
7
|
+
vi.mock("./persistence.js");
|
|
8
|
+
describe("SessionIdManager", () => {
|
|
9
|
+
let mockClock;
|
|
10
|
+
let sessionIdGenerator;
|
|
11
|
+
let windowIdGenerator;
|
|
12
|
+
let currentTime;
|
|
13
|
+
// Test constants
|
|
14
|
+
const TEST_SURFACE = "test_surface";
|
|
15
|
+
const TEST_SESSION_ID = "test-session-id";
|
|
16
|
+
const TEST_WINDOW_ID = "test-window-id";
|
|
17
|
+
const TEST_TIMESTAMP = 1_603_107_479_471;
|
|
18
|
+
const MINUTES_IN_HOUR = 60;
|
|
19
|
+
const SECONDS_IN_MINUTE = 60;
|
|
20
|
+
const MS_IN_SECOND = 1000;
|
|
21
|
+
const HOURS_IN_DAY = 24;
|
|
22
|
+
const THIRTY_MINUTES = 30;
|
|
23
|
+
const THIRTY_MINUTES_MS = THIRTY_MINUTES * SECONDS_IN_MINUTE * MS_IN_SECOND;
|
|
24
|
+
const TWENTY_FOUR_HOURS_MS = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SECOND;
|
|
25
|
+
const ONE_HOUR_MS = MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SECOND;
|
|
26
|
+
const ONE_SECOND_MS = MS_IN_SECOND;
|
|
27
|
+
const FIVE_MINUTES = 5;
|
|
28
|
+
const FIVE_MINUTES_SECONDS = FIVE_MINUTES * SECONDS_IN_MINUTE;
|
|
29
|
+
const THIRTY_SECONDS = 30;
|
|
30
|
+
const ONE_HUNDRED_EXTRA_SECONDS = 100;
|
|
31
|
+
const FIVE_THOUSAND_MULTIPLIER = 5;
|
|
32
|
+
const FIVE_THOUSAND_MS = FIVE_THOUSAND_MULTIPLIER * MS_IN_SECOND;
|
|
33
|
+
const ONE_HUNDRED_MS = 100;
|
|
34
|
+
const TWO_HUNDRED_MS = 200;
|
|
35
|
+
const THROTTLE_PERCENTAGE = 0.1;
|
|
36
|
+
const IDLE_TIMEOUT_BUFFER = 1.1;
|
|
37
|
+
const HEARTBEAT_INTERVAL_MS = 5000;
|
|
38
|
+
const STALE_WINDOW_TIMEOUT_MS = 61_000; // Updated to match new 60s timeout + 1s
|
|
39
|
+
const RECENT_HEARTBEAT_MS = 10_000;
|
|
40
|
+
const NEWER_TIMESTAMP_OFFSET = 1000;
|
|
41
|
+
const OLDER_TIMESTAMP_OFFSET = 500;
|
|
42
|
+
const FUTURE_TIMESTAMP_OFFSET = 10_000;
|
|
43
|
+
const PAST_TIMESTAMP_OFFSET = 1000;
|
|
44
|
+
const ZERO_TIMESTAMP = 0;
|
|
45
|
+
const createTestConfig = (overrides = {}) => ({
|
|
46
|
+
persistence: "localStorage",
|
|
47
|
+
...overrides,
|
|
48
|
+
});
|
|
49
|
+
const createSessionManager = (config = createTestConfig(), persistenceOverrides = {}) => {
|
|
50
|
+
const persistence = {
|
|
51
|
+
register: vi.fn(),
|
|
52
|
+
getProperty: vi
|
|
53
|
+
.fn()
|
|
54
|
+
.mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
|
|
55
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
56
|
+
...persistenceOverrides,
|
|
57
|
+
};
|
|
58
|
+
return new SessionIdManager(TEST_SURFACE, config, persistence, {
|
|
59
|
+
sessionId: sessionIdGenerator,
|
|
60
|
+
windowId: windowIdGenerator,
|
|
61
|
+
clock: mockClock,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
currentTime = TEST_TIMESTAMP;
|
|
66
|
+
// Mock clock
|
|
67
|
+
mockClock = {
|
|
68
|
+
now: vi.fn(() => currentTime),
|
|
69
|
+
setTimeout: vi.fn((callback, delay) => setTimeout(callback, delay)),
|
|
70
|
+
clearTimeout: vi.fn((id) => clearTimeout(id)),
|
|
71
|
+
};
|
|
72
|
+
// Mock ID generators
|
|
73
|
+
sessionIdGenerator = vi.fn(() => TEST_SESSION_ID);
|
|
74
|
+
windowIdGenerator = vi.fn(() => TEST_WINDOW_ID);
|
|
75
|
+
// Mock sessionStore
|
|
76
|
+
const mockSessionStore = sessionStore;
|
|
77
|
+
mockSessionStore._is_supported = vi.fn().mockReturnValue(true);
|
|
78
|
+
mockSessionStore._parse = vi.fn().mockReturnValue(null);
|
|
79
|
+
mockSessionStore._set = vi.fn();
|
|
80
|
+
mockSessionStore._remove = vi.fn();
|
|
81
|
+
// Clear all mocks
|
|
82
|
+
vi.clearAllMocks();
|
|
83
|
+
});
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
vi.useRealTimers();
|
|
86
|
+
});
|
|
87
|
+
describe("initialization", () => {
|
|
88
|
+
it("generates initial session and window IDs on first use", () => {
|
|
89
|
+
const sessionManager = createSessionManager();
|
|
90
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
91
|
+
expect(result).toMatchObject({
|
|
92
|
+
sessionId: TEST_SESSION_ID,
|
|
93
|
+
windowId: TEST_WINDOW_ID,
|
|
94
|
+
sessionStartTimestamp: currentTime,
|
|
95
|
+
lastActivityTimestamp: currentTime,
|
|
96
|
+
changeReason: {
|
|
97
|
+
[SessionChangeReason.NO_SESSION_ID]: true,
|
|
98
|
+
[SessionChangeReason.ACTIVITY_TIMEOUT]: true,
|
|
99
|
+
[SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: false,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
it("generates initial session even in read-only mode", () => {
|
|
104
|
+
const sessionManager = createSessionManager();
|
|
105
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
|
|
106
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
107
|
+
expect(result.windowId).toBe(TEST_WINDOW_ID);
|
|
108
|
+
});
|
|
109
|
+
it("uses bootstrap session ID from config", () => {
|
|
110
|
+
const bootstrapSessionId = "bootstrap-session-id";
|
|
111
|
+
const config = createTestConfig({ sessionId: bootstrapSessionId });
|
|
112
|
+
const sessionManager = createSessionManager(config);
|
|
113
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
114
|
+
expect(result.sessionId).toBe(bootstrapSessionId);
|
|
115
|
+
});
|
|
116
|
+
it("handles invalid bootstrap session ID gracefully", () => {
|
|
117
|
+
const config = createTestConfig({ sessionId: "invalid-id" });
|
|
118
|
+
const mockPersistence = {
|
|
119
|
+
register: vi.fn(() => {
|
|
120
|
+
throw new Error("Invalid session ID");
|
|
121
|
+
}),
|
|
122
|
+
getProperty: vi
|
|
123
|
+
.fn()
|
|
124
|
+
.mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
|
|
125
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
126
|
+
};
|
|
127
|
+
// Should not throw and should generate new session ID
|
|
128
|
+
expect(() => createSessionManager(config, mockPersistence)).not.toThrow();
|
|
129
|
+
});
|
|
130
|
+
it("sets up window ID persistence correctly", () => {
|
|
131
|
+
const mockSessionStore = sessionStore;
|
|
132
|
+
mockSessionStore._parse = vi
|
|
133
|
+
.fn()
|
|
134
|
+
.mockReturnValueOnce("existing-window-id") // window ID
|
|
135
|
+
.mockReturnValueOnce(undefined); // primary window exists
|
|
136
|
+
createSessionManager();
|
|
137
|
+
expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists", true);
|
|
138
|
+
});
|
|
139
|
+
it("cycles window ID when primary window already exists", () => {
|
|
140
|
+
const mockSessionStore = sessionStore;
|
|
141
|
+
mockSessionStore._parse = vi
|
|
142
|
+
.fn()
|
|
143
|
+
.mockReturnValueOnce("existing-window-id") // window ID
|
|
144
|
+
.mockReturnValueOnce(true); // primary window exists
|
|
145
|
+
createSessionManager();
|
|
146
|
+
expect(mockSessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_window_id");
|
|
147
|
+
});
|
|
148
|
+
it("handles disabled persistence correctly", () => {
|
|
149
|
+
const mockPersistence = {
|
|
150
|
+
register: vi.fn(),
|
|
151
|
+
getProperty: vi
|
|
152
|
+
.fn()
|
|
153
|
+
.mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
|
|
154
|
+
isDisabled: vi.fn().mockReturnValue(true),
|
|
155
|
+
};
|
|
156
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
157
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
158
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
159
|
+
expect(sessionStore._set).not.toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe("session timeout configuration", () => {
|
|
163
|
+
const createSessionManagerWithTimeout = (timeout) => {
|
|
164
|
+
const config = createTestConfig({
|
|
165
|
+
sessionIdleTimeoutSeconds: timeout,
|
|
166
|
+
});
|
|
167
|
+
return createSessionManager(config);
|
|
168
|
+
};
|
|
169
|
+
it("uses static session timeout regardless of config", () => {
|
|
170
|
+
const sessionManager = createSessionManagerWithTimeout(FIVE_MINUTES_SECONDS);
|
|
171
|
+
// Session timeout is now static - always uses default
|
|
172
|
+
expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
|
|
173
|
+
});
|
|
174
|
+
it("clamps timeout to default when below minimum", () => {
|
|
175
|
+
const sessionManager = createSessionManagerWithTimeout(THIRTY_SECONDS);
|
|
176
|
+
expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
|
|
177
|
+
});
|
|
178
|
+
it("clamps timeout to default when above maximum", () => {
|
|
179
|
+
const sessionManager = createSessionManagerWithTimeout(MAX_SESSION_IDLE_TIMEOUT_SECONDS + ONE_HUNDRED_EXTRA_SECONDS);
|
|
180
|
+
expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
|
|
181
|
+
});
|
|
182
|
+
it("uses default timeout when undefined", () => {
|
|
183
|
+
const sessionManager = createSessionManagerWithTimeout(undefined);
|
|
184
|
+
expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe("session persistence and retrieval", () => {
|
|
188
|
+
it("reuses existing session when not expired", () => {
|
|
189
|
+
const existingTimestamp = currentTime - ONE_SECOND_MS;
|
|
190
|
+
const startTimestamp = existingTimestamp - ONE_HOUR_MS;
|
|
191
|
+
const mockPersistence = {
|
|
192
|
+
register: vi.fn(),
|
|
193
|
+
getProperty: vi
|
|
194
|
+
.fn()
|
|
195
|
+
.mockReturnValue([
|
|
196
|
+
existingTimestamp,
|
|
197
|
+
"existing-session-id",
|
|
198
|
+
startTimestamp,
|
|
199
|
+
]),
|
|
200
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
201
|
+
};
|
|
202
|
+
const mockSessionStore = sessionStore;
|
|
203
|
+
mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
|
|
204
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
205
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
206
|
+
expect(result).toEqual({
|
|
207
|
+
sessionId: "existing-session-id",
|
|
208
|
+
windowId: "existing-window-id",
|
|
209
|
+
sessionStartTimestamp: startTimestamp,
|
|
210
|
+
lastActivityTimestamp: currentTime, // Updated to current time
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
it("generates new session when activity timeout exceeded", () => {
|
|
214
|
+
const oldTimestamp = currentTime - (THIRTY_MINUTES_MS + ONE_SECOND_MS);
|
|
215
|
+
const startTimestamp = oldTimestamp - ONE_HOUR_MS;
|
|
216
|
+
const mockPersistence = {
|
|
217
|
+
register: vi.fn(),
|
|
218
|
+
getProperty: vi
|
|
219
|
+
.fn()
|
|
220
|
+
.mockReturnValue([oldTimestamp, "old-session-id", startTimestamp]),
|
|
221
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
222
|
+
};
|
|
223
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
224
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
225
|
+
expect(result).toMatchObject({
|
|
226
|
+
sessionId: TEST_SESSION_ID,
|
|
227
|
+
windowId: TEST_WINDOW_ID,
|
|
228
|
+
sessionStartTimestamp: currentTime,
|
|
229
|
+
lastActivityTimestamp: currentTime,
|
|
230
|
+
changeReason: {
|
|
231
|
+
[SessionChangeReason.ACTIVITY_TIMEOUT]: true,
|
|
232
|
+
[SessionChangeReason.NO_SESSION_ID]: false,
|
|
233
|
+
[SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: false,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
it("generates new session when maximum session length exceeded", () => {
|
|
238
|
+
const oldTimestamp = currentTime - ONE_SECOND_MS;
|
|
239
|
+
const startTimestamp = currentTime - (TWENTY_FOUR_HOURS_MS + ONE_SECOND_MS);
|
|
240
|
+
const mockPersistence = {
|
|
241
|
+
register: vi.fn(),
|
|
242
|
+
getProperty: vi
|
|
243
|
+
.fn()
|
|
244
|
+
.mockReturnValue([oldTimestamp, "old-session-id", startTimestamp]),
|
|
245
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
246
|
+
};
|
|
247
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
248
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
249
|
+
expect(result).toMatchObject({
|
|
250
|
+
sessionId: TEST_SESSION_ID,
|
|
251
|
+
windowId: TEST_WINDOW_ID,
|
|
252
|
+
sessionStartTimestamp: currentTime,
|
|
253
|
+
changeReason: {
|
|
254
|
+
[SessionChangeReason.ACTIVITY_TIMEOUT]: false,
|
|
255
|
+
[SessionChangeReason.NO_SESSION_ID]: false,
|
|
256
|
+
[SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: true,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
it("generates new session when maximum length exceeded even in read-only mode", () => {
|
|
261
|
+
const oldTimestamp = currentTime - ONE_SECOND_MS;
|
|
262
|
+
const startTimestamp = currentTime - (TWENTY_FOUR_HOURS_MS + ONE_SECOND_MS);
|
|
263
|
+
const mockPersistence = {
|
|
264
|
+
register: vi.fn(),
|
|
265
|
+
getProperty: vi
|
|
266
|
+
.fn()
|
|
267
|
+
.mockReturnValue([oldTimestamp, "old-session-id", startTimestamp]),
|
|
268
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
269
|
+
};
|
|
270
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
271
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
|
|
272
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
273
|
+
expect(result.changeReason?.[SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
it("does not update activity timestamp in read-only mode for active sessions", () => {
|
|
276
|
+
const oldTimestamp = currentTime - ONE_SECOND_MS;
|
|
277
|
+
const startTimestamp = oldTimestamp - ONE_HOUR_MS;
|
|
278
|
+
const mockPersistence = {
|
|
279
|
+
register: vi.fn(),
|
|
280
|
+
getProperty: vi
|
|
281
|
+
.fn()
|
|
282
|
+
.mockReturnValue([
|
|
283
|
+
oldTimestamp,
|
|
284
|
+
"existing-session-id",
|
|
285
|
+
startTimestamp,
|
|
286
|
+
]),
|
|
287
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
288
|
+
};
|
|
289
|
+
const mockSessionStore = sessionStore;
|
|
290
|
+
mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
|
|
291
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
292
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
|
|
293
|
+
expect(result.lastActivityTimestamp).toBe(oldTimestamp); // Not updated
|
|
294
|
+
expect(mockPersistence.register).toHaveBeenCalledWith({
|
|
295
|
+
[SESSION_ID]: [oldTimestamp, "existing-session-id", startTimestamp],
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
it("populates session start timestamp when missing", () => {
|
|
299
|
+
const mockPersistence = {
|
|
300
|
+
register: vi.fn(),
|
|
301
|
+
getProperty: vi.fn().mockReturnValue([
|
|
302
|
+
currentTime,
|
|
303
|
+
"existing-session-id",
|
|
304
|
+
// Missing start timestamp (legacy format)
|
|
305
|
+
]),
|
|
306
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
307
|
+
};
|
|
308
|
+
const mockSessionStore = sessionStore;
|
|
309
|
+
mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
|
|
310
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
311
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
312
|
+
expect(result.sessionStartTimestamp).toBe(currentTime);
|
|
313
|
+
expect(mockPersistence.register).toHaveBeenCalledWith({
|
|
314
|
+
[SESSION_ID]: [currentTime, "existing-session-id", currentTime],
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
it("uses current time when no timestamp provided", () => {
|
|
318
|
+
const sessionManager = createSessionManager();
|
|
319
|
+
// Mock Date.now to return a specific time
|
|
320
|
+
const mockNow = TEST_TIMESTAMP + FIVE_THOUSAND_MS;
|
|
321
|
+
mockClock.now = vi.fn(() => mockNow);
|
|
322
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false);
|
|
323
|
+
expect(result.sessionStartTimestamp).toBe(mockNow);
|
|
324
|
+
expect(result.lastActivityTimestamp).toBe(mockNow);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
describe("window ID management", () => {
|
|
328
|
+
it("generates new window ID when none exists", () => {
|
|
329
|
+
const mockPersistence = {
|
|
330
|
+
register: vi.fn(),
|
|
331
|
+
getProperty: vi
|
|
332
|
+
.fn()
|
|
333
|
+
.mockReturnValue([currentTime, "existing-session-id", currentTime]),
|
|
334
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
335
|
+
};
|
|
336
|
+
const mockSessionStore = sessionStore;
|
|
337
|
+
mockSessionStore._parse = vi.fn().mockReturnValue(null); // No existing window ID
|
|
338
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
339
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
340
|
+
expect(result.windowId).toBe(TEST_WINDOW_ID);
|
|
341
|
+
expect(result.sessionId).toBe("existing-session-id"); // Session ID unchanged
|
|
342
|
+
expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_window_id", TEST_WINDOW_ID);
|
|
343
|
+
});
|
|
344
|
+
it("handles session storage not supported", () => {
|
|
345
|
+
const mockSessionStore = sessionStore;
|
|
346
|
+
mockSessionStore._is_supported = vi.fn().mockReturnValue(false);
|
|
347
|
+
const sessionManager = createSessionManager();
|
|
348
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
349
|
+
expect(result.windowId).toBe(TEST_WINDOW_ID);
|
|
350
|
+
expect(mockSessionStore._set).not.toHaveBeenCalled();
|
|
351
|
+
});
|
|
352
|
+
it("stores and retrieves window ID correctly", () => {
|
|
353
|
+
const mockSessionStore = sessionStore;
|
|
354
|
+
mockSessionStore._parse = vi.fn().mockReturnValue("stored-window-id");
|
|
355
|
+
const sessionManager = createSessionManager();
|
|
356
|
+
// Access private method for testing
|
|
357
|
+
const windowId = sessionManager._getWindowId();
|
|
358
|
+
expect(windowId).toBe("stored-window-id");
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
describe("session callbacks", () => {
|
|
362
|
+
it("calls registered callbacks when session changes", () => {
|
|
363
|
+
const callback = vi.fn();
|
|
364
|
+
const sessionManager = createSessionManager();
|
|
365
|
+
sessionManager.onSessionId(callback);
|
|
366
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
367
|
+
expect(callback).toHaveBeenCalledWith(TEST_SESSION_ID, TEST_WINDOW_ID, expect.objectContaining({
|
|
368
|
+
[SessionChangeReason.NO_SESSION_ID]: true,
|
|
369
|
+
}));
|
|
370
|
+
});
|
|
371
|
+
it("calls callback immediately if session already exists", () => {
|
|
372
|
+
const mockPersistence = {
|
|
373
|
+
register: vi.fn(),
|
|
374
|
+
getProperty: vi
|
|
375
|
+
.fn()
|
|
376
|
+
.mockReturnValue([currentTime, "existing-session-id", currentTime]),
|
|
377
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
378
|
+
};
|
|
379
|
+
const mockSessionStore = sessionStore;
|
|
380
|
+
mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
|
|
381
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
382
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime); // Initialize session
|
|
383
|
+
const callback = vi.fn();
|
|
384
|
+
sessionManager.onSessionId(callback);
|
|
385
|
+
expect(callback).toHaveBeenCalledWith("existing-session-id", "existing-window-id", undefined // No change reason for existing session
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
it("allows unsubscribing from callbacks", () => {
|
|
389
|
+
const callback = vi.fn();
|
|
390
|
+
const sessionManager = createSessionManager();
|
|
391
|
+
const unsubscribe = sessionManager.onSessionId(callback);
|
|
392
|
+
// Callback is called immediately with initial state (null, null) and change reason
|
|
393
|
+
expect(callback).toHaveBeenCalledWith(null, null, {
|
|
394
|
+
[SessionChangeReason.NO_SESSION_ID]: true,
|
|
395
|
+
});
|
|
396
|
+
callback.mockClear();
|
|
397
|
+
unsubscribe();
|
|
398
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
399
|
+
// Should not be called again after unsubscribing
|
|
400
|
+
expect(callback).not.toHaveBeenCalled();
|
|
401
|
+
});
|
|
402
|
+
it("does not call callbacks when values do not change", () => {
|
|
403
|
+
const mockPersistence = {
|
|
404
|
+
register: vi.fn(),
|
|
405
|
+
getProperty: vi
|
|
406
|
+
.fn()
|
|
407
|
+
.mockReturnValue([
|
|
408
|
+
currentTime - ONE_SECOND_MS,
|
|
409
|
+
"existing-session-id",
|
|
410
|
+
currentTime - ONE_HOUR_MS,
|
|
411
|
+
]),
|
|
412
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
413
|
+
};
|
|
414
|
+
const mockSessionStore = sessionStore;
|
|
415
|
+
mockSessionStore._parse = vi.fn().mockReturnValue("existing-window-id");
|
|
416
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
417
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime); // Initialize
|
|
418
|
+
const callback = vi.fn();
|
|
419
|
+
sessionManager.onSessionId(callback);
|
|
420
|
+
callback.mockClear(); // Clear the immediate call
|
|
421
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime + ONE_SECOND_MS); // Second call
|
|
422
|
+
expect(callback).not.toHaveBeenCalled();
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
describe("idle timeout enforcement", () => {
|
|
426
|
+
beforeEach(() => {
|
|
427
|
+
vi.useFakeTimers();
|
|
428
|
+
});
|
|
429
|
+
it("starts idle timeout timer on initialization", () => {
|
|
430
|
+
const sessionManager = createSessionManager();
|
|
431
|
+
expect(sessionManager
|
|
432
|
+
._enforceIdleTimeout).toBeDefined();
|
|
433
|
+
expect(mockClock.setTimeout).toHaveBeenCalled();
|
|
434
|
+
});
|
|
435
|
+
it("resets idle timer when checking session (non-read-only)", () => {
|
|
436
|
+
const sessionManager = createSessionManager();
|
|
437
|
+
// Advance time to ensure throttling doesn't prevent timer reset
|
|
438
|
+
currentTime +=
|
|
439
|
+
sessionManager.sessionTimeoutMs * THROTTLE_PERCENTAGE + ONE_SECOND_MS;
|
|
440
|
+
mockClock.now = vi.fn(() => currentTime);
|
|
441
|
+
const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
|
|
442
|
+
const setTimeoutSpy = vi.spyOn(mockClock, "setTimeout");
|
|
443
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
444
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
445
|
+
expect(setTimeoutSpy).toHaveBeenCalled();
|
|
446
|
+
});
|
|
447
|
+
it("does not reset idle timer in read-only mode", () => {
|
|
448
|
+
const sessionManager = createSessionManager();
|
|
449
|
+
const originalTimer = sessionManager._enforceIdleTimeout;
|
|
450
|
+
sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
|
|
451
|
+
expect(sessionManager
|
|
452
|
+
._enforceIdleTimeout).toEqual(originalTimer);
|
|
453
|
+
});
|
|
454
|
+
it("resets session when idle timeout is exceeded", () => {
|
|
455
|
+
const mockPersistence = {
|
|
456
|
+
register: vi.fn(),
|
|
457
|
+
getProperty: vi.fn(),
|
|
458
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
459
|
+
};
|
|
460
|
+
// Set up initial active session
|
|
461
|
+
mockPersistence.getProperty.mockReturnValue([
|
|
462
|
+
currentTime,
|
|
463
|
+
"active-session-id",
|
|
464
|
+
currentTime,
|
|
465
|
+
]);
|
|
466
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
467
|
+
const resetSpy = vi.spyOn(sessionManager, "resetSessionId");
|
|
468
|
+
const callback = vi.fn();
|
|
469
|
+
sessionManager.onSessionId(callback);
|
|
470
|
+
callback.mockClear(); // Clear initial call
|
|
471
|
+
// Simulate session becoming idle
|
|
472
|
+
const idleTimestamp = currentTime - (sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
|
|
473
|
+
mockPersistence.getProperty.mockReturnValue([
|
|
474
|
+
idleTimestamp,
|
|
475
|
+
"active-session-id",
|
|
476
|
+
currentTime,
|
|
477
|
+
]);
|
|
478
|
+
// Fast-forward time to trigger idle timeout
|
|
479
|
+
vi.advanceTimersByTime(sessionManager.sessionTimeoutMs * IDLE_TIMEOUT_BUFFER + ONE_SECOND_MS);
|
|
480
|
+
// Verify reset was called with correct reason and callbacks were fired
|
|
481
|
+
expect(resetSpy).toHaveBeenCalledWith({
|
|
482
|
+
[SessionChangeReason.ACTIVITY_TIMEOUT]: true,
|
|
483
|
+
});
|
|
484
|
+
expect(callback).toHaveBeenCalledWith(null, null, {
|
|
485
|
+
[SessionChangeReason.ACTIVITY_TIMEOUT]: true,
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
it("does not reset session if activity is recent when timer fires", () => {
|
|
489
|
+
const mockPersistence = {
|
|
490
|
+
register: vi.fn(),
|
|
491
|
+
getProperty: vi.fn(),
|
|
492
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
493
|
+
};
|
|
494
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
495
|
+
const resetSpy = vi.spyOn(sessionManager, "resetSessionId");
|
|
496
|
+
// Initially set up idle session
|
|
497
|
+
const idleTimestamp = currentTime - (sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
|
|
498
|
+
mockPersistence.getProperty.mockReturnValue([
|
|
499
|
+
idleTimestamp,
|
|
500
|
+
"session-id",
|
|
501
|
+
currentTime,
|
|
502
|
+
]);
|
|
503
|
+
// Fast-forward almost to timeout (buffer now divides, so timer fires earlier)
|
|
504
|
+
const timeAdvance1 = sessionManager.sessionTimeoutMs / IDLE_TIMEOUT_BUFFER - ONE_HUNDRED_MS;
|
|
505
|
+
vi.advanceTimersByTime(timeAdvance1);
|
|
506
|
+
currentTime += timeAdvance1;
|
|
507
|
+
mockClock.now = vi.fn(() => currentTime);
|
|
508
|
+
// Before timer fires, simulate recent activity
|
|
509
|
+
const recentTimestamp = currentTime - ONE_SECOND_MS; // Recent activity
|
|
510
|
+
mockPersistence.getProperty.mockReturnValue([
|
|
511
|
+
recentTimestamp,
|
|
512
|
+
"session-id",
|
|
513
|
+
currentTime,
|
|
514
|
+
]);
|
|
515
|
+
// Let timer fire
|
|
516
|
+
const timeAdvance2 = TWO_HUNDRED_MS;
|
|
517
|
+
vi.advanceTimersByTime(timeAdvance2);
|
|
518
|
+
currentTime += timeAdvance2;
|
|
519
|
+
mockClock.now = vi.fn(() => currentTime);
|
|
520
|
+
expect(resetSpy).not.toHaveBeenCalled();
|
|
521
|
+
});
|
|
522
|
+
it("always resets idle timer for non-read-only calls", () => {
|
|
523
|
+
const sessionManager = createSessionManager();
|
|
524
|
+
const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
|
|
525
|
+
const setTimeoutSpy = vi.spyOn(mockClock, "setTimeout");
|
|
526
|
+
clearTimeoutSpy.mockClear();
|
|
527
|
+
setTimeoutSpy.mockClear();
|
|
528
|
+
// First call should reset timer
|
|
529
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
530
|
+
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
531
|
+
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
532
|
+
clearTimeoutSpy.mockClear();
|
|
533
|
+
setTimeoutSpy.mockClear();
|
|
534
|
+
// Second call should also reset timer (no more throttling)
|
|
535
|
+
currentTime += ONE_HUNDRED_MS;
|
|
536
|
+
mockClock.now = vi.fn(() => currentTime);
|
|
537
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
538
|
+
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
539
|
+
expect(setTimeoutSpy).toHaveBeenCalledTimes(1);
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
describe("session reset", () => {
|
|
543
|
+
it("clears existing session data", () => {
|
|
544
|
+
const mockPersistence = {
|
|
545
|
+
register: vi.fn(),
|
|
546
|
+
getProperty: vi
|
|
547
|
+
.fn()
|
|
548
|
+
.mockReturnValue([currentTime, "existing-session-id", currentTime]),
|
|
549
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
550
|
+
};
|
|
551
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
552
|
+
sessionManager.resetSessionId();
|
|
553
|
+
expect(mockPersistence.register).toHaveBeenCalledWith({
|
|
554
|
+
[SESSION_ID]: [null, null, null],
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
it("generates new session after reset", () => {
|
|
558
|
+
const mockPersistence = {
|
|
559
|
+
register: vi.fn(),
|
|
560
|
+
getProperty: vi.fn(),
|
|
561
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
562
|
+
};
|
|
563
|
+
// Start with existing session
|
|
564
|
+
mockPersistence.getProperty.mockReturnValue([
|
|
565
|
+
currentTime,
|
|
566
|
+
"existing-session-id",
|
|
567
|
+
currentTime,
|
|
568
|
+
]);
|
|
569
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
570
|
+
// Reset the session
|
|
571
|
+
sessionManager.resetSessionId();
|
|
572
|
+
// After reset, getProperty should return null values
|
|
573
|
+
mockPersistence.getProperty.mockReturnValue([null, null, null]);
|
|
574
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
575
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
576
|
+
expect(result.changeReason?.[SessionChangeReason.NO_SESSION_ID]).toBe(true);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
describe("cleanup and disposal", () => {
|
|
580
|
+
it("clears timeout on disposal", () => {
|
|
581
|
+
const sessionManager = createSessionManager();
|
|
582
|
+
const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
|
|
583
|
+
sessionManager.dispose();
|
|
584
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
585
|
+
expect(sessionManager
|
|
586
|
+
._enforceIdleTimeout).toBeUndefined();
|
|
587
|
+
});
|
|
588
|
+
it("removes event listeners on disposal", () => {
|
|
589
|
+
// Mock window and addEventListener
|
|
590
|
+
const mockRemoveEventListener = vi.fn();
|
|
591
|
+
const originalWindow = globalThis.window;
|
|
592
|
+
globalThis.window = {
|
|
593
|
+
addEventListener: vi.fn(),
|
|
594
|
+
removeEventListener: mockRemoveEventListener,
|
|
595
|
+
};
|
|
596
|
+
const sessionManager = createSessionManager();
|
|
597
|
+
sessionManager.dispose();
|
|
598
|
+
expect(mockRemoveEventListener).toHaveBeenCalledWith("beforeunload", expect.any(Function));
|
|
599
|
+
// Restore original window
|
|
600
|
+
globalThis.window = originalWindow;
|
|
601
|
+
});
|
|
602
|
+
it("clears session change handlers on disposal", () => {
|
|
603
|
+
const sessionManager = createSessionManager();
|
|
604
|
+
const callback = vi.fn();
|
|
605
|
+
sessionManager.onSessionId(callback);
|
|
606
|
+
sessionManager.dispose();
|
|
607
|
+
expect(sessionManager
|
|
608
|
+
._sessionIdChangedHandlers).toEqual([]);
|
|
609
|
+
});
|
|
610
|
+
it("handles disposal when no window is available", () => {
|
|
611
|
+
const originalWindow = globalThis.window;
|
|
612
|
+
// Set window to undefined to simulate no window environment
|
|
613
|
+
globalThis.window = undefined;
|
|
614
|
+
const sessionManager = createSessionManager();
|
|
615
|
+
expect(() => sessionManager.dispose()).not.toThrow();
|
|
616
|
+
// Restore window
|
|
617
|
+
globalThis.window = originalWindow;
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
describe("beforeunload handling", () => {
|
|
621
|
+
it("removes window ID and conditionally removes primary window flag on beforeunload", () => {
|
|
622
|
+
const mockRemoveEventListener = vi.fn();
|
|
623
|
+
const mockAddEventListener = vi.fn();
|
|
624
|
+
const originalWindow = globalThis.window;
|
|
625
|
+
globalThis.window = {
|
|
626
|
+
addEventListener: mockAddEventListener,
|
|
627
|
+
removeEventListener: mockRemoveEventListener,
|
|
628
|
+
};
|
|
629
|
+
// Mock sessionStore to simulate this window being the primary window
|
|
630
|
+
const mockSessionStore = sessionStore;
|
|
631
|
+
mockSessionStore._parse.mockImplementation((key) => {
|
|
632
|
+
if (key.includes("window_id")) {
|
|
633
|
+
return TEST_WINDOW_ID; // Return the same window ID that the manager will have
|
|
634
|
+
}
|
|
635
|
+
return null;
|
|
636
|
+
});
|
|
637
|
+
const sessionManager = createSessionManager();
|
|
638
|
+
// Initialize session to set up window ID
|
|
639
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
640
|
+
// Get the registered beforeunload handler
|
|
641
|
+
const beforeUnloadHandler = mockAddEventListener.mock.calls.find((call) => call[0] === "beforeunload")?.[1];
|
|
642
|
+
expect(beforeUnloadHandler).toBeDefined();
|
|
643
|
+
// Call the handler
|
|
644
|
+
beforeUnloadHandler?.();
|
|
645
|
+
// Should remove window ID, heartbeat, and primary window flag (since this is the primary window)
|
|
646
|
+
expect(sessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_window_id");
|
|
647
|
+
expect(sessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat");
|
|
648
|
+
expect(sessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
|
|
649
|
+
const EXPECTED_REMOVE_CALLS = 3; // window ID + heartbeat + primary window flag
|
|
650
|
+
expect(sessionStore._remove).toHaveBeenCalledTimes(EXPECTED_REMOVE_CALLS);
|
|
651
|
+
// Restore window
|
|
652
|
+
globalThis.window = originalWindow;
|
|
653
|
+
});
|
|
654
|
+
it("does not set up beforeunload handler when window is undefined", () => {
|
|
655
|
+
const originalWindow = globalThis.window;
|
|
656
|
+
globalThis.window = undefined;
|
|
657
|
+
expect(() => createSessionManager()).not.toThrow();
|
|
658
|
+
// Restore window
|
|
659
|
+
globalThis.window = originalWindow;
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
describe("heartbeat logic", () => {
|
|
663
|
+
let originalWindow;
|
|
664
|
+
let originalDocument;
|
|
665
|
+
beforeEach(() => {
|
|
666
|
+
vi.useFakeTimers();
|
|
667
|
+
// Store originals
|
|
668
|
+
originalWindow = globalThis.window;
|
|
669
|
+
originalDocument = globalThis.document;
|
|
670
|
+
// Mock window and document
|
|
671
|
+
globalThis.window = {
|
|
672
|
+
addEventListener: vi.fn(),
|
|
673
|
+
removeEventListener: vi.fn(),
|
|
674
|
+
};
|
|
675
|
+
globalThis.document = {
|
|
676
|
+
visibilityState: "visible",
|
|
677
|
+
addEventListener: vi.fn(),
|
|
678
|
+
removeEventListener: vi.fn(),
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
afterEach(() => {
|
|
682
|
+
vi.useRealTimers();
|
|
683
|
+
// Restore originals
|
|
684
|
+
globalThis.window = originalWindow;
|
|
685
|
+
globalThis.document = originalDocument;
|
|
686
|
+
});
|
|
687
|
+
it("writes initial heartbeat on initialization", () => {
|
|
688
|
+
const mockSessionStore = sessionStore;
|
|
689
|
+
mockSessionStore._is_supported.mockReturnValue(true);
|
|
690
|
+
mockSessionStore._parse.mockReturnValue(null); // No existing data
|
|
691
|
+
createSessionManager();
|
|
692
|
+
expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat", currentTime);
|
|
693
|
+
});
|
|
694
|
+
it("starts periodic heartbeat interval", () => {
|
|
695
|
+
const setIntervalSpy = vi.spyOn(globalThis, "setInterval");
|
|
696
|
+
createSessionManager();
|
|
697
|
+
expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), HEARTBEAT_INTERVAL_MS);
|
|
698
|
+
});
|
|
699
|
+
it("skips heartbeat when document visibility is not visible", () => {
|
|
700
|
+
const mockSessionStore = sessionStore;
|
|
701
|
+
// Set document to hidden
|
|
702
|
+
globalThis.document = {
|
|
703
|
+
visibilityState: "hidden",
|
|
704
|
+
addEventListener: vi.fn(),
|
|
705
|
+
removeEventListener: vi.fn(),
|
|
706
|
+
};
|
|
707
|
+
createSessionManager();
|
|
708
|
+
// Clear initial heartbeat call
|
|
709
|
+
mockSessionStore._set.mockClear();
|
|
710
|
+
// Advance time to trigger heartbeat
|
|
711
|
+
vi.advanceTimersByTime(HEARTBEAT_INTERVAL_MS);
|
|
712
|
+
// Should not have written heartbeat due to hidden visibility
|
|
713
|
+
expect(mockSessionStore._set).not.toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat", expect.any(Number));
|
|
714
|
+
});
|
|
715
|
+
it("writes heartbeat when document becomes visible", () => {
|
|
716
|
+
const mockSessionStore = sessionStore;
|
|
717
|
+
// Start hidden
|
|
718
|
+
globalThis.document = {
|
|
719
|
+
visibilityState: "hidden",
|
|
720
|
+
addEventListener: vi.fn(),
|
|
721
|
+
removeEventListener: vi.fn(),
|
|
722
|
+
};
|
|
723
|
+
createSessionManager();
|
|
724
|
+
mockSessionStore._set.mockClear();
|
|
725
|
+
// Make visible and advance time
|
|
726
|
+
globalThis.document = {
|
|
727
|
+
visibilityState: "visible",
|
|
728
|
+
addEventListener: vi.fn(),
|
|
729
|
+
removeEventListener: vi.fn(),
|
|
730
|
+
};
|
|
731
|
+
currentTime += HEARTBEAT_INTERVAL_MS;
|
|
732
|
+
mockClock.now = vi.fn(() => currentTime);
|
|
733
|
+
vi.advanceTimersByTime(HEARTBEAT_INTERVAL_MS);
|
|
734
|
+
expect(mockSessionStore._set).toHaveBeenCalledWith("interfere_test_surface_window_id_heartbeat", currentTime);
|
|
735
|
+
});
|
|
736
|
+
it("recovers correctly when storage throws during heartbeat", () => {
|
|
737
|
+
const mockSessionStore = sessionStore;
|
|
738
|
+
createSessionManager();
|
|
739
|
+
// Make storage throw on heartbeat after initial setup
|
|
740
|
+
let heartbeatCalls = 0;
|
|
741
|
+
mockSessionStore._set.mockImplementation((key, _value) => {
|
|
742
|
+
if (key.includes("heartbeat")) {
|
|
743
|
+
heartbeatCalls++;
|
|
744
|
+
if (heartbeatCalls > 1) {
|
|
745
|
+
// Allow first heartbeat, fail subsequent ones
|
|
746
|
+
throw new Error("Storage quota exceeded");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
// Advance time to trigger heartbeat - should not throw
|
|
751
|
+
expect(() => {
|
|
752
|
+
vi.advanceTimersByTime(HEARTBEAT_INTERVAL_MS);
|
|
753
|
+
}).not.toThrow();
|
|
754
|
+
// Verify that heartbeat errors were handled gracefully (no exception thrown)
|
|
755
|
+
// The storage cache invalidation happens when actual storage errors occur during heartbeat writes
|
|
756
|
+
});
|
|
757
|
+
it("does not start heartbeat when window is undefined", () => {
|
|
758
|
+
const savedWindow = globalThis.window;
|
|
759
|
+
globalThis.window = undefined;
|
|
760
|
+
const setIntervalSpy = vi.spyOn(globalThis, "setInterval");
|
|
761
|
+
createSessionManager();
|
|
762
|
+
expect(setIntervalSpy).not.toHaveBeenCalled();
|
|
763
|
+
// Restore window
|
|
764
|
+
globalThis.window = savedWindow;
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
describe("stale window cleanup", () => {
|
|
768
|
+
beforeEach(() => {
|
|
769
|
+
vi.useFakeTimers();
|
|
770
|
+
});
|
|
771
|
+
afterEach(() => {
|
|
772
|
+
vi.useRealTimers();
|
|
773
|
+
});
|
|
774
|
+
it("removes flags when heartbeat is older than 60 seconds", () => {
|
|
775
|
+
const mockSessionStore = sessionStore;
|
|
776
|
+
mockSessionStore._is_supported.mockReturnValue(true);
|
|
777
|
+
// Mock stale heartbeat (older than 60 seconds)
|
|
778
|
+
const staleHeartbeat = currentTime - STALE_WINDOW_TIMEOUT_MS;
|
|
779
|
+
mockSessionStore._parse.mockImplementation((key) => {
|
|
780
|
+
if (key.includes("heartbeat")) {
|
|
781
|
+
return staleHeartbeat;
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
});
|
|
785
|
+
createSessionManager();
|
|
786
|
+
// Should have removed the primary window flag due to stale heartbeat
|
|
787
|
+
expect(mockSessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
|
|
788
|
+
});
|
|
789
|
+
it("removes phantom primary flag when no heartbeat exists", () => {
|
|
790
|
+
const mockSessionStore = sessionStore;
|
|
791
|
+
mockSessionStore._is_supported.mockReturnValue(true);
|
|
792
|
+
// Mock no heartbeat but primary window exists
|
|
793
|
+
mockSessionStore._parse.mockImplementation((key) => {
|
|
794
|
+
if (key.includes("heartbeat")) {
|
|
795
|
+
return null; // No heartbeat
|
|
796
|
+
}
|
|
797
|
+
if (key.includes("primary_window_exists")) {
|
|
798
|
+
return true; // But primary flag exists (phantom)
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
801
|
+
});
|
|
802
|
+
createSessionManager();
|
|
803
|
+
// Should have removed the phantom primary window flag
|
|
804
|
+
expect(mockSessionStore._remove).toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
|
|
805
|
+
});
|
|
806
|
+
it("does not remove flags when heartbeat is recent", () => {
|
|
807
|
+
const mockSessionStore = sessionStore;
|
|
808
|
+
mockSessionStore._is_supported.mockReturnValue(true);
|
|
809
|
+
// Mock recent heartbeat (within 60 seconds)
|
|
810
|
+
const recentHeartbeat = currentTime - RECENT_HEARTBEAT_MS;
|
|
811
|
+
mockSessionStore._parse.mockImplementation((key) => {
|
|
812
|
+
if (key.includes("heartbeat")) {
|
|
813
|
+
return recentHeartbeat;
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
});
|
|
817
|
+
createSessionManager();
|
|
818
|
+
// Should not have removed any flags due to recent heartbeat
|
|
819
|
+
expect(mockSessionStore._remove).not.toHaveBeenCalledWith("interfere_test_surface_primary_window_exists");
|
|
820
|
+
});
|
|
821
|
+
it("handles cleanup gracefully when storage throws", () => {
|
|
822
|
+
const mockSessionStore = sessionStore;
|
|
823
|
+
mockSessionStore._is_supported.mockReturnValue(true);
|
|
824
|
+
mockSessionStore._parse.mockImplementation((key) => {
|
|
825
|
+
if (key.includes("heartbeat")) {
|
|
826
|
+
throw new Error("Storage error");
|
|
827
|
+
}
|
|
828
|
+
return null;
|
|
829
|
+
});
|
|
830
|
+
// Should not throw even when storage fails during cleanup
|
|
831
|
+
expect(() => createSessionManager()).not.toThrow();
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
describe("optimistic locking in persistence", () => {
|
|
835
|
+
it("syncs local state when another tabs activity timestamp is newer", () => {
|
|
836
|
+
const newerTimestamp = currentTime + NEWER_TIMESTAMP_OFFSET;
|
|
837
|
+
const olderLocalTimestamp = currentTime;
|
|
838
|
+
const mockPersistence = {
|
|
839
|
+
register: vi.fn(),
|
|
840
|
+
getProperty: vi
|
|
841
|
+
.fn()
|
|
842
|
+
.mockReturnValueOnce([
|
|
843
|
+
olderLocalTimestamp,
|
|
844
|
+
"local-session-id",
|
|
845
|
+
currentTime,
|
|
846
|
+
])
|
|
847
|
+
.mockReturnValue([
|
|
848
|
+
newerTimestamp,
|
|
849
|
+
"remote-session-id",
|
|
850
|
+
currentTime - PAST_TIMESTAMP_OFFSET,
|
|
851
|
+
]),
|
|
852
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
853
|
+
};
|
|
854
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
855
|
+
// Initialize with local session
|
|
856
|
+
sessionManager.checkAndGetSessionAndWindowId(false, olderLocalTimestamp);
|
|
857
|
+
// Try to update with older timestamp - should sync from persistence instead
|
|
858
|
+
sessionManager.checkAndGetSessionAndWindowId(false, olderLocalTimestamp + OLDER_TIMESTAMP_OFFSET);
|
|
859
|
+
// Should have synced to the newer remote session
|
|
860
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(true, currentTime);
|
|
861
|
+
expect(result.sessionId).toBe("remote-session-id");
|
|
862
|
+
expect(result.lastActivityTimestamp).toBe(newerTimestamp);
|
|
863
|
+
});
|
|
864
|
+
it("adds jitter in production but not in test environment", () => {
|
|
865
|
+
// Test environment should not add jitter
|
|
866
|
+
const mockPersistence = {
|
|
867
|
+
register: vi.fn(),
|
|
868
|
+
getProperty: vi
|
|
869
|
+
.fn()
|
|
870
|
+
.mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
|
|
871
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
872
|
+
};
|
|
873
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
874
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
875
|
+
// In test environment, timestamp should be exact (no jitter)
|
|
876
|
+
expect(mockPersistence.register).toHaveBeenCalledWith({
|
|
877
|
+
[SESSION_ID]: [currentTime, TEST_SESSION_ID, currentTime],
|
|
878
|
+
});
|
|
879
|
+
});
|
|
880
|
+
it("handles equal timestamps correctly", () => {
|
|
881
|
+
const sameTimestamp = currentTime;
|
|
882
|
+
const mockPersistence = {
|
|
883
|
+
register: vi.fn(),
|
|
884
|
+
getProperty: vi
|
|
885
|
+
.fn()
|
|
886
|
+
.mockReturnValue([sameTimestamp, "existing-session-id", currentTime]),
|
|
887
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
888
|
+
};
|
|
889
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
890
|
+
// With same timestamp, should proceed with local update
|
|
891
|
+
sessionManager.checkAndGetSessionAndWindowId(false, sameTimestamp);
|
|
892
|
+
expect(mockPersistence.register).toHaveBeenCalledWith({
|
|
893
|
+
[SESSION_ID]: [sameTimestamp, "existing-session-id", currentTime],
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
it("gracefully handles persistence failures during optimistic locking", () => {
|
|
897
|
+
const mockPersistence = {
|
|
898
|
+
register: vi.fn().mockImplementation(() => {
|
|
899
|
+
throw new Error("Storage quota exceeded");
|
|
900
|
+
}),
|
|
901
|
+
getProperty: vi
|
|
902
|
+
.fn()
|
|
903
|
+
.mockReturnValue([ZERO_TIMESTAMP, null, ZERO_TIMESTAMP]),
|
|
904
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
905
|
+
};
|
|
906
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
907
|
+
// Should not throw even when persistence fails
|
|
908
|
+
expect(() => {
|
|
909
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
910
|
+
}).not.toThrow();
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
describe("activity timestamp edge cases", () => {
|
|
914
|
+
it("handles clock skew backwards gracefully", () => {
|
|
915
|
+
const futureTimestamp = currentTime + FUTURE_TIMESTAMP_OFFSET;
|
|
916
|
+
const pastTimestamp = currentTime - PAST_TIMESTAMP_OFFSET;
|
|
917
|
+
const mockPersistence = {
|
|
918
|
+
register: vi.fn(),
|
|
919
|
+
getProperty: vi
|
|
920
|
+
.fn()
|
|
921
|
+
.mockReturnValue([futureTimestamp, "session-id", currentTime]),
|
|
922
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
923
|
+
};
|
|
924
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
925
|
+
// Access with past timestamp (clock moved backwards)
|
|
926
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, pastTimestamp);
|
|
927
|
+
// Session should still be considered active despite backwards clock
|
|
928
|
+
expect(result.sessionId).toBe("session-id");
|
|
929
|
+
expect(result.lastActivityTimestamp).toBe(pastTimestamp); // Updated to current
|
|
930
|
+
});
|
|
931
|
+
it("does not update last activity timestamp when regenerating due to timeout", () => {
|
|
932
|
+
const oldTimestamp = currentTime - (THIRTY_MINUTES_MS + ONE_SECOND_MS);
|
|
933
|
+
const mockPersistence = {
|
|
934
|
+
register: vi.fn(),
|
|
935
|
+
getProperty: vi
|
|
936
|
+
.fn()
|
|
937
|
+
.mockReturnValue([
|
|
938
|
+
oldTimestamp,
|
|
939
|
+
"old-session-id",
|
|
940
|
+
currentTime - ONE_HOUR_MS,
|
|
941
|
+
]),
|
|
942
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
943
|
+
};
|
|
944
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
945
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
946
|
+
// New session should have current time as last activity
|
|
947
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
948
|
+
expect(result.lastActivityTimestamp).toBe(currentTime);
|
|
949
|
+
expect(result.changeReason?.[SessionChangeReason.ACTIVITY_TIMEOUT]).toBe(true);
|
|
950
|
+
});
|
|
951
|
+
it("handles timestamp validation errors gracefully", () => {
|
|
952
|
+
const mockPersistence = {
|
|
953
|
+
register: vi.fn(),
|
|
954
|
+
getProperty: vi.fn().mockReturnValue([
|
|
955
|
+
"invalid-timestamp", // Not a number
|
|
956
|
+
"session-id",
|
|
957
|
+
"invalid-start-timestamp",
|
|
958
|
+
]),
|
|
959
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
960
|
+
};
|
|
961
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
962
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
963
|
+
// Should generate new session due to invalid data
|
|
964
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
965
|
+
// Invalid timestamp data triggers activity timeout (not no session)
|
|
966
|
+
expect(result.changeReason).toMatchObject({
|
|
967
|
+
[SessionChangeReason.ACTIVITY_TIMEOUT]: true,
|
|
968
|
+
});
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
describe("reset session with explicit reason", () => {
|
|
972
|
+
it("propagates explicit reset reason to handlers", () => {
|
|
973
|
+
const callback = vi.fn();
|
|
974
|
+
const sessionManager = createSessionManager();
|
|
975
|
+
sessionManager.onSessionId(callback);
|
|
976
|
+
callback.mockClear(); // Clear initial call
|
|
977
|
+
const explicitReason = {
|
|
978
|
+
[SessionChangeReason.SESSION_PAST_MAXIMUM_LENGTH]: true,
|
|
979
|
+
};
|
|
980
|
+
sessionManager.resetSessionId(explicitReason);
|
|
981
|
+
expect(callback).toHaveBeenCalledWith(null, null, explicitReason);
|
|
982
|
+
});
|
|
983
|
+
it("uses default reason when no explicit reason provided", () => {
|
|
984
|
+
const callback = vi.fn();
|
|
985
|
+
const sessionManager = createSessionManager();
|
|
986
|
+
sessionManager.onSessionId(callback);
|
|
987
|
+
callback.mockClear(); // Clear initial call
|
|
988
|
+
sessionManager.resetSessionId();
|
|
989
|
+
expect(callback).toHaveBeenCalledWith(null, null, {
|
|
990
|
+
[SessionChangeReason.NO_SESSION_ID]: true,
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
it("calls resetSessionId with correct reason during idle timeout", () => {
|
|
994
|
+
vi.useFakeTimers();
|
|
995
|
+
const mockPersistence = {
|
|
996
|
+
register: vi.fn(),
|
|
997
|
+
getProperty: vi.fn(),
|
|
998
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
999
|
+
};
|
|
1000
|
+
// Set up initial active session
|
|
1001
|
+
mockPersistence.getProperty.mockReturnValue([
|
|
1002
|
+
currentTime,
|
|
1003
|
+
"active-session-id",
|
|
1004
|
+
currentTime,
|
|
1005
|
+
]);
|
|
1006
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
1007
|
+
const callback = vi.fn();
|
|
1008
|
+
sessionManager.onSessionId(callback);
|
|
1009
|
+
callback.mockClear(); // Clear initial call
|
|
1010
|
+
// Simulate session becoming idle
|
|
1011
|
+
const idleTimestamp = currentTime - (sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
|
|
1012
|
+
mockPersistence.getProperty.mockReturnValue([
|
|
1013
|
+
idleTimestamp,
|
|
1014
|
+
"active-session-id",
|
|
1015
|
+
currentTime,
|
|
1016
|
+
]);
|
|
1017
|
+
// Fast-forward time to trigger idle timeout
|
|
1018
|
+
vi.advanceTimersByTime(sessionManager.sessionTimeoutMs + ONE_SECOND_MS);
|
|
1019
|
+
expect(callback).toHaveBeenCalledWith(null, null, {
|
|
1020
|
+
[SessionChangeReason.ACTIVITY_TIMEOUT]: true,
|
|
1021
|
+
});
|
|
1022
|
+
vi.useRealTimers();
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
describe("dispose with active heartbeat", () => {
|
|
1026
|
+
it("clears heartbeat interval on disposal", () => {
|
|
1027
|
+
// Ensure window exists for heartbeat to start
|
|
1028
|
+
const originalWindow = globalThis.window;
|
|
1029
|
+
globalThis.window = {
|
|
1030
|
+
addEventListener: vi.fn(),
|
|
1031
|
+
removeEventListener: vi.fn(),
|
|
1032
|
+
};
|
|
1033
|
+
const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval");
|
|
1034
|
+
const sessionManager = createSessionManager();
|
|
1035
|
+
sessionManager.dispose();
|
|
1036
|
+
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
1037
|
+
// Verify heartbeat interval is cleared
|
|
1038
|
+
expect(sessionManager._heartbeatInterval).toBeNull();
|
|
1039
|
+
// Restore window
|
|
1040
|
+
globalThis.window = originalWindow;
|
|
1041
|
+
});
|
|
1042
|
+
it("handles disposal when no heartbeat interval exists", () => {
|
|
1043
|
+
const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval");
|
|
1044
|
+
// Create manager without heartbeat (window undefined)
|
|
1045
|
+
const originalWindow = globalThis.window;
|
|
1046
|
+
globalThis.window = undefined;
|
|
1047
|
+
const sessionManager = createSessionManager();
|
|
1048
|
+
sessionManager.dispose();
|
|
1049
|
+
// Should not call clearInterval if no interval was created
|
|
1050
|
+
expect(clearIntervalSpy).not.toHaveBeenCalled();
|
|
1051
|
+
// Restore window
|
|
1052
|
+
globalThis.window = originalWindow;
|
|
1053
|
+
});
|
|
1054
|
+
it("clears both timeout and interval on disposal", () => {
|
|
1055
|
+
// Ensure window exists for heartbeat to start
|
|
1056
|
+
const originalWindow = globalThis.window;
|
|
1057
|
+
globalThis.window = {
|
|
1058
|
+
addEventListener: vi.fn(),
|
|
1059
|
+
removeEventListener: vi.fn(),
|
|
1060
|
+
};
|
|
1061
|
+
const clearTimeoutSpy = vi.spyOn(mockClock, "clearTimeout");
|
|
1062
|
+
const clearIntervalSpy = vi.spyOn(globalThis, "clearInterval");
|
|
1063
|
+
const sessionManager = createSessionManager();
|
|
1064
|
+
sessionManager.dispose();
|
|
1065
|
+
expect(clearTimeoutSpy).toHaveBeenCalled();
|
|
1066
|
+
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
1067
|
+
// Verify both are cleared
|
|
1068
|
+
expect(sessionManager
|
|
1069
|
+
._enforceIdleTimeout).toBeUndefined();
|
|
1070
|
+
expect(sessionManager._heartbeatInterval).toBeNull();
|
|
1071
|
+
// Restore window
|
|
1072
|
+
globalThis.window = originalWindow;
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
describe("edge cases and error handling", () => {
|
|
1076
|
+
it("handles malformed session data gracefully", () => {
|
|
1077
|
+
const mockPersistence = {
|
|
1078
|
+
register: vi.fn(),
|
|
1079
|
+
getProperty: vi.fn().mockReturnValue("invalid-data"),
|
|
1080
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
1081
|
+
};
|
|
1082
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
1083
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
1084
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
1085
|
+
expect(result.changeReason?.[SessionChangeReason.NO_SESSION_ID]).toBe(true);
|
|
1086
|
+
});
|
|
1087
|
+
it("handles empty session array gracefully", () => {
|
|
1088
|
+
const mockPersistence = {
|
|
1089
|
+
register: vi.fn(),
|
|
1090
|
+
getProperty: vi.fn().mockReturnValue([]),
|
|
1091
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
1092
|
+
};
|
|
1093
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
1094
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
1095
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
1096
|
+
});
|
|
1097
|
+
it("handles session data with wrong types", () => {
|
|
1098
|
+
const NUMBER_NOT_STRING = 123;
|
|
1099
|
+
const mockPersistence = {
|
|
1100
|
+
register: vi.fn(),
|
|
1101
|
+
getProperty: vi.fn().mockReturnValue([
|
|
1102
|
+
"not-a-number",
|
|
1103
|
+
NUMBER_NOT_STRING, // Not a string
|
|
1104
|
+
"also-not-a-number",
|
|
1105
|
+
]),
|
|
1106
|
+
isDisabled: vi.fn().mockReturnValue(false),
|
|
1107
|
+
};
|
|
1108
|
+
const sessionManager = createSessionManager(createTestConfig(), mockPersistence);
|
|
1109
|
+
const result = sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
1110
|
+
expect(result.sessionId).toBe(TEST_SESSION_ID);
|
|
1111
|
+
});
|
|
1112
|
+
it("handles storage errors gracefully", () => {
|
|
1113
|
+
// Create session manager first, then mock the storage to throw
|
|
1114
|
+
const sessionManager = createSessionManager();
|
|
1115
|
+
const mockSessionStore = sessionStore;
|
|
1116
|
+
mockSessionStore._set = vi.fn((_key, _value) => {
|
|
1117
|
+
throw new Error("Storage error");
|
|
1118
|
+
});
|
|
1119
|
+
// This should not throw even though storage fails
|
|
1120
|
+
expect(() => {
|
|
1121
|
+
sessionManager.checkAndGetSessionAndWindowId(false, currentTime);
|
|
1122
|
+
}).not.toThrow();
|
|
1123
|
+
});
|
|
1124
|
+
it("generates consistent session timeout", () => {
|
|
1125
|
+
const config = createTestConfig();
|
|
1126
|
+
const sessionManager = createSessionManager(config);
|
|
1127
|
+
// Session timeout is now static - always uses default
|
|
1128
|
+
expect(sessionManager.sessionTimeoutMs).toBe(toMs(DEFAULT_SESSION_IDLE_TIMEOUT_SECONDS));
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
});
|
|
1132
|
+
//# sourceMappingURL=session-manager.test.js.map
|