@interfere/next 0.0.9 → 0.0.11
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 +308 -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 +152 -0
- package/dist/core/client-core.js.map +1 -0
- package/dist/core/client.d.ts +71 -18
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +107 -97
- 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 +14 -0
- package/dist/core/error-handlers.d.ts.map +1 -0
- package/dist/core/error-handlers.js +192 -0
- package/dist/core/error-handlers.js.map +1 -0
- 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 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/next/middleware.d.ts +8 -0
- package/dist/next/middleware.d.ts.map +1 -0
- package/dist/next/middleware.js +139 -0
- package/dist/next/middleware.js.map +1 -0
- 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 +12 -7
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react/provider.jsx +37 -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 +3 -0
- package/dist/session/replay.d.ts.map +1 -0
- package/dist/session/replay.js +109 -0
- package/dist/session/replay.js.map +1 -0
- 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 +3 -0
- package/dist/session/session-summary.d.ts.map +1 -0
- package/dist/session/session-summary.js +214 -0
- package/dist/session/session-summary.js.map +1 -0
- 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 +28 -9
- package/dist/core/client.test.d.ts.map +0 -1
- package/dist/core/client.test.js +0 -227
- 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/edge/edge.d.ts +0 -11
- package/dist/edge/edge.d.ts.map +0 -1
- package/dist/edge/edge.js +0 -41
- package/dist/edge/edge.js.map +0 -1
- package/dist/edge/edge.test.d.ts +0 -2
- package/dist/edge/edge.test.d.ts.map +0 -1
- package/dist/edge/edge.test.js +0 -109
- package/dist/edge/edge.test.js.map +0 -1
- package/dist/server/server.d.ts +0 -6
- package/dist/server/server.d.ts.map +0 -1
- package/dist/server/server.js +0 -35
- package/dist/server/server.js.map +0 -1
- package/dist/server/server.test.d.ts +0 -2
- package/dist/server/server.test.d.ts.map +0 -1
- package/dist/server/server.test.js +0 -88
- package/dist/server/server.test.js.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,763 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { capture as captureFn } from "../../core/client.js";
|
|
3
|
+
import { setupSessionSummary, stopSessionSummary, } from "../../session/session-summary.js";
|
|
4
|
+
// Mock the client module
|
|
5
|
+
vi.mock("../../core/client.js", () => ({
|
|
6
|
+
capture: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
// Mock constants
|
|
9
|
+
vi.mock("../../session/constants.js", () => ({
|
|
10
|
+
ALLOW_BROWSER_AI: true,
|
|
11
|
+
SESSION_SUMMARY_ENABLED: true,
|
|
12
|
+
SESSION_SUMMARY_MAX_EVENTS: 100,
|
|
13
|
+
SESSION_SUMMARY_WINDOW_MS: 30_000,
|
|
14
|
+
}));
|
|
15
|
+
// Constants for tests
|
|
16
|
+
const SUMMARY_WINDOW_MS = 30_000;
|
|
17
|
+
const MAX_EVENTS_TO_GENERATE = 150;
|
|
18
|
+
const EVENTS_TO_GENERATE = 30;
|
|
19
|
+
const SAMPLE_SIZE = 20;
|
|
20
|
+
const MAX_SUMMARY_LENGTH = 2000;
|
|
21
|
+
const LONGER_THAN_MAX_LENGTH = 3000;
|
|
22
|
+
describe("session-summary", () => {
|
|
23
|
+
let originalWindow;
|
|
24
|
+
let originalDocument;
|
|
25
|
+
let mockAddEventListener;
|
|
26
|
+
let mockRemoveEventListener;
|
|
27
|
+
let mockDocAddEventListener;
|
|
28
|
+
let mockDocRemoveEventListener;
|
|
29
|
+
let mockSetInterval;
|
|
30
|
+
let mockClearInterval;
|
|
31
|
+
let eventListeners;
|
|
32
|
+
let docEventListeners;
|
|
33
|
+
let intervalCallbacks;
|
|
34
|
+
let intervalId = 1;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
// Reset environment variables using vi.stubEnv
|
|
38
|
+
vi.stubEnv("NODE_ENV", "test");
|
|
39
|
+
// Store original globals
|
|
40
|
+
originalWindow = global.window;
|
|
41
|
+
originalDocument = global.document;
|
|
42
|
+
// Create event listener tracking
|
|
43
|
+
eventListeners = new Map();
|
|
44
|
+
docEventListeners = new Map();
|
|
45
|
+
intervalCallbacks = new Map();
|
|
46
|
+
// Mock window addEventListener/removeEventListener
|
|
47
|
+
mockAddEventListener = vi.fn((event, handler) => {
|
|
48
|
+
if (!eventListeners.has(event)) {
|
|
49
|
+
eventListeners.set(event, new Set());
|
|
50
|
+
}
|
|
51
|
+
eventListeners.get(event)?.add(handler);
|
|
52
|
+
});
|
|
53
|
+
mockRemoveEventListener = vi.fn((event, handler) => {
|
|
54
|
+
eventListeners.get(event)?.delete(handler);
|
|
55
|
+
});
|
|
56
|
+
// Mock document addEventListener/removeEventListener
|
|
57
|
+
mockDocAddEventListener = vi.fn((event, handler) => {
|
|
58
|
+
if (!docEventListeners.has(event)) {
|
|
59
|
+
docEventListeners.set(event, new Set());
|
|
60
|
+
}
|
|
61
|
+
docEventListeners.get(event)?.add(handler);
|
|
62
|
+
});
|
|
63
|
+
mockDocRemoveEventListener = vi.fn((event, handler) => {
|
|
64
|
+
docEventListeners.get(event)?.delete(handler);
|
|
65
|
+
});
|
|
66
|
+
// Mock setInterval/clearInterval
|
|
67
|
+
mockSetInterval = vi.fn((callback, _ms) => {
|
|
68
|
+
const id = intervalId++;
|
|
69
|
+
intervalCallbacks.set(id, callback);
|
|
70
|
+
return id;
|
|
71
|
+
});
|
|
72
|
+
mockClearInterval = vi.fn((id) => {
|
|
73
|
+
intervalCallbacks.delete(id);
|
|
74
|
+
});
|
|
75
|
+
// Setup global window and document mocks
|
|
76
|
+
global.window = {
|
|
77
|
+
addEventListener: mockAddEventListener,
|
|
78
|
+
removeEventListener: mockRemoveEventListener,
|
|
79
|
+
setInterval: mockSetInterval,
|
|
80
|
+
clearInterval: mockClearInterval,
|
|
81
|
+
ai: undefined,
|
|
82
|
+
};
|
|
83
|
+
global.document = {
|
|
84
|
+
addEventListener: mockDocAddEventListener,
|
|
85
|
+
removeEventListener: mockDocRemoveEventListener,
|
|
86
|
+
visibilityState: "visible",
|
|
87
|
+
};
|
|
88
|
+
// Mock history API
|
|
89
|
+
global.history = {
|
|
90
|
+
pushState: vi.fn(),
|
|
91
|
+
replaceState: vi.fn(),
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
stopSessionSummary();
|
|
96
|
+
global.window = originalWindow;
|
|
97
|
+
global.document = originalDocument;
|
|
98
|
+
vi.resetAllMocks();
|
|
99
|
+
});
|
|
100
|
+
describe("setupSessionSummary", () => {
|
|
101
|
+
it("should initialize event listeners when in browser environment", () => {
|
|
102
|
+
setupSessionSummary();
|
|
103
|
+
// Check that event listeners were added
|
|
104
|
+
expect(mockDocAddEventListener).toHaveBeenCalledWith("click", expect.any(Function), { capture: true });
|
|
105
|
+
expect(mockDocAddEventListener).toHaveBeenCalledWith("keydown", expect.any(Function), { capture: true });
|
|
106
|
+
expect(mockAddEventListener).toHaveBeenCalledWith("popstate", expect.any(Function));
|
|
107
|
+
expect(mockAddEventListener).toHaveBeenCalledWith("error", expect.any(Function));
|
|
108
|
+
expect(mockAddEventListener).toHaveBeenCalledWith("unhandledrejection", expect.any(Function));
|
|
109
|
+
expect(mockAddEventListener).toHaveBeenCalledWith("beforeunload", expect.any(Function));
|
|
110
|
+
expect(mockDocAddEventListener).toHaveBeenCalledWith("visibilitychange", expect.any(Function));
|
|
111
|
+
// Check that interval was set
|
|
112
|
+
expect(mockSetInterval).toHaveBeenCalledWith(expect.any(Function), SUMMARY_WINDOW_MS);
|
|
113
|
+
});
|
|
114
|
+
it("should not initialize twice", () => {
|
|
115
|
+
setupSessionSummary();
|
|
116
|
+
const firstCallCount = mockAddEventListener.mock.calls.length;
|
|
117
|
+
setupSessionSummary();
|
|
118
|
+
expect(mockAddEventListener).toHaveBeenCalledTimes(firstCallCount);
|
|
119
|
+
});
|
|
120
|
+
it("should not initialize when not in browser environment", () => {
|
|
121
|
+
// Remove window to simulate non-browser environment
|
|
122
|
+
// @ts-expect-error - intentionally setting window to undefined for test
|
|
123
|
+
global.window = undefined;
|
|
124
|
+
setupSessionSummary();
|
|
125
|
+
expect(mockAddEventListener).not.toHaveBeenCalled();
|
|
126
|
+
// Restore window
|
|
127
|
+
global.window = {
|
|
128
|
+
addEventListener: mockAddEventListener,
|
|
129
|
+
removeEventListener: mockRemoveEventListener,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
it("should not initialize when SESSION_SUMMARY_ENABLED is false", () => {
|
|
133
|
+
// This test would require re-mocking the module which is complex in vitest
|
|
134
|
+
// Skipping for now as SESSION_SUMMARY_ENABLED is always true in current implementation
|
|
135
|
+
expect(true).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("event tracking", () => {
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
setupSessionSummary();
|
|
141
|
+
});
|
|
142
|
+
it("should track click events", () => {
|
|
143
|
+
const clickHandler = docEventListeners
|
|
144
|
+
.get("click")
|
|
145
|
+
?.values()
|
|
146
|
+
.next().value;
|
|
147
|
+
expect(clickHandler).toBeDefined();
|
|
148
|
+
if (clickHandler) {
|
|
149
|
+
// Simulate click with aria-label
|
|
150
|
+
const event1 = {
|
|
151
|
+
target: {
|
|
152
|
+
getAttribute: vi.fn((attr) => attr === "aria-label" ? "Submit Button" : null),
|
|
153
|
+
textContent: "Click me",
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
clickHandler(event1);
|
|
157
|
+
// Simulate click with text content
|
|
158
|
+
const event2 = {
|
|
159
|
+
target: {
|
|
160
|
+
getAttribute: vi.fn(() => null),
|
|
161
|
+
textContent: "Another button with very long text that should be truncated after 60 characters",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
clickHandler(event2);
|
|
165
|
+
// Simulate click with no label
|
|
166
|
+
const event3 = {
|
|
167
|
+
target: {
|
|
168
|
+
getAttribute: vi.fn(() => null),
|
|
169
|
+
textContent: null,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
clickHandler(event3);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
it("should track keydown events", () => {
|
|
176
|
+
const keyHandler = docEventListeners
|
|
177
|
+
.get("keydown")
|
|
178
|
+
?.values()
|
|
179
|
+
.next().value;
|
|
180
|
+
expect(keyHandler).toBeDefined();
|
|
181
|
+
if (keyHandler) {
|
|
182
|
+
// Simulate keydown event
|
|
183
|
+
keyHandler({});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
it("should track navigation events", () => {
|
|
187
|
+
const originalPushState = vi.fn();
|
|
188
|
+
const originalReplaceState = vi.fn();
|
|
189
|
+
global.history.pushState = originalPushState;
|
|
190
|
+
global.history.replaceState = originalReplaceState;
|
|
191
|
+
setupSessionSummary();
|
|
192
|
+
// The wrapped functions should be called
|
|
193
|
+
global.history.pushState({}, "", "/new-page");
|
|
194
|
+
expect(originalPushState).toHaveBeenCalled();
|
|
195
|
+
global.history.replaceState({}, "", "/replaced-page");
|
|
196
|
+
expect(originalReplaceState).toHaveBeenCalled();
|
|
197
|
+
// Test popstate
|
|
198
|
+
const popstateHandler = eventListeners
|
|
199
|
+
.get("popstate")
|
|
200
|
+
?.values()
|
|
201
|
+
.next().value;
|
|
202
|
+
expect(popstateHandler).toBeDefined();
|
|
203
|
+
if (popstateHandler) {
|
|
204
|
+
popstateHandler({});
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
it("should track error events", () => {
|
|
208
|
+
const errorHandler = eventListeners.get("error")?.values().next().value;
|
|
209
|
+
expect(errorHandler).toBeDefined();
|
|
210
|
+
if (errorHandler) {
|
|
211
|
+
// Simulate error event
|
|
212
|
+
errorHandler({
|
|
213
|
+
message: "Test error message",
|
|
214
|
+
});
|
|
215
|
+
// Simulate error event without message
|
|
216
|
+
errorHandler({});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
it("should track unhandled rejection events", () => {
|
|
220
|
+
const rejectionHandler = eventListeners
|
|
221
|
+
.get("unhandledrejection")
|
|
222
|
+
?.values()
|
|
223
|
+
.next().value;
|
|
224
|
+
expect(rejectionHandler).toBeDefined();
|
|
225
|
+
if (rejectionHandler) {
|
|
226
|
+
// Simulate rejection with Error object
|
|
227
|
+
rejectionHandler({
|
|
228
|
+
reason: new Error("Promise rejection error"),
|
|
229
|
+
});
|
|
230
|
+
// Simulate rejection with string reason
|
|
231
|
+
rejectionHandler({
|
|
232
|
+
reason: "String rejection",
|
|
233
|
+
});
|
|
234
|
+
// Simulate rejection with object reason without message
|
|
235
|
+
rejectionHandler({
|
|
236
|
+
reason: { someProperty: "value" },
|
|
237
|
+
});
|
|
238
|
+
// Simulate rejection without reason
|
|
239
|
+
rejectionHandler({});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe("event buffer management", () => {
|
|
244
|
+
it("should limit buffer size to max events", () => {
|
|
245
|
+
setupSessionSummary();
|
|
246
|
+
const clickHandler = docEventListeners
|
|
247
|
+
.get("click")
|
|
248
|
+
?.values()
|
|
249
|
+
.next().value;
|
|
250
|
+
if (clickHandler) {
|
|
251
|
+
// Generate more events than max (assuming max is 100)
|
|
252
|
+
for (let i = 0; i < MAX_EVENTS_TO_GENERATE; i++) {
|
|
253
|
+
clickHandler({
|
|
254
|
+
target: {
|
|
255
|
+
getAttribute: vi.fn(() => null),
|
|
256
|
+
textContent: `Click ${i}`,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// Buffer should be limited to max events
|
|
262
|
+
// We can't directly check buffer size, but we can verify behavior
|
|
263
|
+
// by checking that old events are removed
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe("summary emission", () => {
|
|
267
|
+
beforeEach(() => {
|
|
268
|
+
vi.useFakeTimers();
|
|
269
|
+
setupSessionSummary();
|
|
270
|
+
});
|
|
271
|
+
afterEach(() => {
|
|
272
|
+
vi.useRealTimers();
|
|
273
|
+
});
|
|
274
|
+
it("should emit summary on interval", async () => {
|
|
275
|
+
const clickHandler = docEventListeners
|
|
276
|
+
.get("click")
|
|
277
|
+
?.values()
|
|
278
|
+
.next().value;
|
|
279
|
+
if (clickHandler) {
|
|
280
|
+
// Add some events
|
|
281
|
+
clickHandler({
|
|
282
|
+
target: {
|
|
283
|
+
getAttribute: vi.fn(() => "Test Button"),
|
|
284
|
+
textContent: "Click",
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
// Get the interval callback
|
|
289
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
290
|
+
expect(intervalCallback).toBeDefined();
|
|
291
|
+
if (intervalCallback) {
|
|
292
|
+
// Execute the interval callback
|
|
293
|
+
await intervalCallback();
|
|
294
|
+
// Check that capture was called
|
|
295
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
296
|
+
version: 1,
|
|
297
|
+
eventCount: expect.any(Number),
|
|
298
|
+
sample: expect.any(Array),
|
|
299
|
+
summary: null, // No AI available in test
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
it("should emit summary on beforeunload", async () => {
|
|
304
|
+
const clickHandler = docEventListeners
|
|
305
|
+
.get("click")
|
|
306
|
+
?.values()
|
|
307
|
+
.next().value;
|
|
308
|
+
if (clickHandler) {
|
|
309
|
+
// Add some events
|
|
310
|
+
clickHandler({
|
|
311
|
+
target: {
|
|
312
|
+
getAttribute: vi.fn(() => null),
|
|
313
|
+
textContent: "Test",
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// Trigger beforeunload
|
|
318
|
+
const beforeUnloadHandler = eventListeners
|
|
319
|
+
.get("beforeunload")
|
|
320
|
+
?.values()
|
|
321
|
+
.next().value;
|
|
322
|
+
expect(beforeUnloadHandler).toBeDefined();
|
|
323
|
+
if (beforeUnloadHandler) {
|
|
324
|
+
await beforeUnloadHandler();
|
|
325
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalled();
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
it("should emit summary on visibility change to hidden", async () => {
|
|
329
|
+
const clickHandler = docEventListeners
|
|
330
|
+
.get("click")
|
|
331
|
+
?.values()
|
|
332
|
+
.next().value;
|
|
333
|
+
if (clickHandler) {
|
|
334
|
+
// Add some events
|
|
335
|
+
clickHandler({
|
|
336
|
+
target: {
|
|
337
|
+
getAttribute: vi.fn(() => null),
|
|
338
|
+
textContent: "Test",
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
// Change visibility to hidden
|
|
343
|
+
Object.defineProperty(global.document, "visibilityState", {
|
|
344
|
+
value: "hidden",
|
|
345
|
+
writable: true,
|
|
346
|
+
configurable: true,
|
|
347
|
+
});
|
|
348
|
+
const visibilityHandler = docEventListeners
|
|
349
|
+
.get("visibilitychange")
|
|
350
|
+
?.values()
|
|
351
|
+
.next().value;
|
|
352
|
+
expect(visibilityHandler).toBeDefined();
|
|
353
|
+
if (visibilityHandler) {
|
|
354
|
+
await visibilityHandler();
|
|
355
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalled();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
it("should not emit summary when no events", async () => {
|
|
359
|
+
// Get the interval callback
|
|
360
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
361
|
+
if (intervalCallback) {
|
|
362
|
+
// Execute without any events
|
|
363
|
+
await intervalCallback();
|
|
364
|
+
expect(vi.mocked(captureFn)).not.toHaveBeenCalled();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
it("should handle capture errors gracefully", async () => {
|
|
368
|
+
vi.mocked(captureFn).mockImplementation(() => {
|
|
369
|
+
throw new Error("Capture failed");
|
|
370
|
+
});
|
|
371
|
+
const clickHandler = docEventListeners
|
|
372
|
+
.get("click")
|
|
373
|
+
?.values()
|
|
374
|
+
.next().value;
|
|
375
|
+
if (clickHandler) {
|
|
376
|
+
clickHandler({
|
|
377
|
+
target: {
|
|
378
|
+
getAttribute: vi.fn(() => null),
|
|
379
|
+
textContent: "Test",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
384
|
+
if (intervalCallback) {
|
|
385
|
+
// Should not throw
|
|
386
|
+
await expect(Promise.resolve(intervalCallback())).resolves.not.toThrow();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
describe("browser AI summarization", () => {
|
|
391
|
+
it("should use browser AI when available", async () => {
|
|
392
|
+
// Note: Browser AI is mocked but ALLOW_BROWSER_AI constant determines if it's used
|
|
393
|
+
// Since we can't easily change the constant after module import, we'll verify
|
|
394
|
+
// the behavior with the current setting
|
|
395
|
+
const mockPrompt = vi.fn().mockResolvedValue(JSON.stringify({
|
|
396
|
+
summary: "User clicked buttons",
|
|
397
|
+
keyActions: ["click"],
|
|
398
|
+
errors: [],
|
|
399
|
+
}));
|
|
400
|
+
const mockCreateTextSession = vi.fn().mockResolvedValue({
|
|
401
|
+
prompt: mockPrompt,
|
|
402
|
+
});
|
|
403
|
+
const mockCanCreateTextSession = vi.fn().mockResolvedValue("readily");
|
|
404
|
+
global.window.ai = {
|
|
405
|
+
canCreateTextSession: mockCanCreateTextSession,
|
|
406
|
+
createTextSession: mockCreateTextSession,
|
|
407
|
+
};
|
|
408
|
+
setupSessionSummary();
|
|
409
|
+
// Add events
|
|
410
|
+
const clickHandler = docEventListeners
|
|
411
|
+
.get("click")
|
|
412
|
+
?.values()
|
|
413
|
+
.next().value;
|
|
414
|
+
if (clickHandler) {
|
|
415
|
+
clickHandler({
|
|
416
|
+
target: {
|
|
417
|
+
getAttribute: vi.fn(() => "Submit"),
|
|
418
|
+
textContent: "Submit",
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
// Trigger summary
|
|
423
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
424
|
+
if (intervalCallback) {
|
|
425
|
+
await intervalCallback();
|
|
426
|
+
// With ALLOW_BROWSER_AI = true (from mock), AI should be called
|
|
427
|
+
if (mockCanCreateTextSession.mock.calls.length > 0) {
|
|
428
|
+
expect(mockCreateTextSession).toHaveBeenCalledWith({
|
|
429
|
+
temperature: 0.2,
|
|
430
|
+
topK: 40,
|
|
431
|
+
topP: 0.95,
|
|
432
|
+
});
|
|
433
|
+
expect(mockPrompt).toHaveBeenCalled();
|
|
434
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
435
|
+
summary: expect.stringContaining("User clicked buttons"),
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
// If AI wasn't called, verify capture was still called without summary
|
|
440
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
441
|
+
summary: null,
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
it("should handle AI not ready", async () => {
|
|
447
|
+
global.window.ai = {
|
|
448
|
+
canCreateTextSession: vi.fn().mockResolvedValue("no"),
|
|
449
|
+
createTextSession: vi.fn(),
|
|
450
|
+
};
|
|
451
|
+
setupSessionSummary();
|
|
452
|
+
// Add events
|
|
453
|
+
const clickHandler = docEventListeners
|
|
454
|
+
.get("click")
|
|
455
|
+
?.values()
|
|
456
|
+
.next().value;
|
|
457
|
+
if (clickHandler) {
|
|
458
|
+
clickHandler({
|
|
459
|
+
target: {
|
|
460
|
+
getAttribute: vi.fn(() => null),
|
|
461
|
+
textContent: "Test",
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
// Trigger summary
|
|
466
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
467
|
+
if (intervalCallback) {
|
|
468
|
+
await intervalCallback();
|
|
469
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
470
|
+
summary: null,
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
it("should handle AI after-download state", async () => {
|
|
475
|
+
const mockPrompt = vi.fn().mockResolvedValue("AI summary");
|
|
476
|
+
global.window.ai = {
|
|
477
|
+
canCreateTextSession: vi.fn().mockResolvedValue("after-download"),
|
|
478
|
+
createTextSession: vi.fn().mockResolvedValue({
|
|
479
|
+
prompt: mockPrompt,
|
|
480
|
+
}),
|
|
481
|
+
};
|
|
482
|
+
setupSessionSummary();
|
|
483
|
+
// Add events
|
|
484
|
+
const clickHandler = docEventListeners
|
|
485
|
+
.get("click")
|
|
486
|
+
?.values()
|
|
487
|
+
.next().value;
|
|
488
|
+
if (clickHandler) {
|
|
489
|
+
clickHandler({
|
|
490
|
+
target: {
|
|
491
|
+
getAttribute: vi.fn(() => null),
|
|
492
|
+
textContent: "Test",
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// Trigger summary
|
|
497
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
498
|
+
if (intervalCallback) {
|
|
499
|
+
await intervalCallback();
|
|
500
|
+
// AI may or may not be called depending on ALLOW_BROWSER_AI constant
|
|
501
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalled();
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
it("should handle AI errors gracefully", async () => {
|
|
505
|
+
global.window.ai = {
|
|
506
|
+
canCreateTextSession: vi.fn().mockRejectedValue(new Error("AI failed")),
|
|
507
|
+
createTextSession: vi.fn(),
|
|
508
|
+
};
|
|
509
|
+
setupSessionSummary();
|
|
510
|
+
// Add events
|
|
511
|
+
const clickHandler = docEventListeners
|
|
512
|
+
.get("click")
|
|
513
|
+
?.values()
|
|
514
|
+
.next().value;
|
|
515
|
+
if (clickHandler) {
|
|
516
|
+
clickHandler({
|
|
517
|
+
target: {
|
|
518
|
+
getAttribute: vi.fn(() => null),
|
|
519
|
+
textContent: "Test",
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
// Trigger summary
|
|
524
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
525
|
+
if (intervalCallback) {
|
|
526
|
+
await intervalCallback();
|
|
527
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
528
|
+
summary: null,
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
it("should truncate long AI responses", async () => {
|
|
533
|
+
const longResponse = "x".repeat(LONGER_THAN_MAX_LENGTH); // Longer than MAX_LENGTH (2000)
|
|
534
|
+
global.window.ai = {
|
|
535
|
+
canCreateTextSession: vi.fn().mockResolvedValue("readily"),
|
|
536
|
+
createTextSession: vi.fn().mockResolvedValue({
|
|
537
|
+
prompt: vi.fn().mockResolvedValue(longResponse),
|
|
538
|
+
}),
|
|
539
|
+
};
|
|
540
|
+
setupSessionSummary();
|
|
541
|
+
// Add events
|
|
542
|
+
const clickHandler = docEventListeners
|
|
543
|
+
.get("click")
|
|
544
|
+
?.values()
|
|
545
|
+
.next().value;
|
|
546
|
+
if (clickHandler) {
|
|
547
|
+
clickHandler({
|
|
548
|
+
target: {
|
|
549
|
+
getAttribute: vi.fn(() => null),
|
|
550
|
+
textContent: "Test",
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
// Trigger summary
|
|
555
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
556
|
+
if (intervalCallback) {
|
|
557
|
+
await intervalCallback();
|
|
558
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalled();
|
|
559
|
+
const call = vi.mocked(captureFn).mock.calls[0];
|
|
560
|
+
// If AI was used, check truncation
|
|
561
|
+
if (call?.[1]) {
|
|
562
|
+
const payload = call[1];
|
|
563
|
+
if (payload.summary && payload.summary !== null) {
|
|
564
|
+
expect(payload.summary.length).toBeLessThanOrEqual(MAX_SUMMARY_LENGTH);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
it("should handle empty AI response", async () => {
|
|
570
|
+
global.window.ai = {
|
|
571
|
+
canCreateTextSession: vi.fn().mockResolvedValue("readily"),
|
|
572
|
+
createTextSession: vi.fn().mockResolvedValue({
|
|
573
|
+
prompt: vi.fn().mockResolvedValue(""),
|
|
574
|
+
}),
|
|
575
|
+
};
|
|
576
|
+
setupSessionSummary();
|
|
577
|
+
// Add events
|
|
578
|
+
const clickHandler = docEventListeners
|
|
579
|
+
.get("click")
|
|
580
|
+
?.values()
|
|
581
|
+
.next().value;
|
|
582
|
+
if (clickHandler) {
|
|
583
|
+
clickHandler({
|
|
584
|
+
target: {
|
|
585
|
+
getAttribute: vi.fn(() => null),
|
|
586
|
+
textContent: "Test",
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
// Trigger summary
|
|
591
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
592
|
+
if (intervalCallback) {
|
|
593
|
+
await intervalCallback();
|
|
594
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
595
|
+
summary: null,
|
|
596
|
+
}));
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
describe("stopSessionSummary", () => {
|
|
601
|
+
it("should remove all event listeners", () => {
|
|
602
|
+
setupSessionSummary();
|
|
603
|
+
// Verify listeners were added
|
|
604
|
+
expect(mockAddEventListener).toHaveBeenCalled();
|
|
605
|
+
expect(mockDocAddEventListener).toHaveBeenCalled();
|
|
606
|
+
expect(mockSetInterval).toHaveBeenCalled();
|
|
607
|
+
stopSessionSummary();
|
|
608
|
+
// Verify listeners were removed
|
|
609
|
+
expect(mockRemoveEventListener).toHaveBeenCalled();
|
|
610
|
+
expect(mockDocRemoveEventListener).toHaveBeenCalled();
|
|
611
|
+
expect(mockClearInterval).toHaveBeenCalled();
|
|
612
|
+
});
|
|
613
|
+
it("should reset initialized state", () => {
|
|
614
|
+
setupSessionSummary();
|
|
615
|
+
const firstCallCount = mockAddEventListener.mock.calls.length;
|
|
616
|
+
stopSessionSummary();
|
|
617
|
+
// Should be able to initialize again
|
|
618
|
+
setupSessionSummary();
|
|
619
|
+
expect(mockAddEventListener.mock.calls.length).toBeGreaterThan(firstCallCount);
|
|
620
|
+
});
|
|
621
|
+
it("should clear buffer", () => {
|
|
622
|
+
setupSessionSummary();
|
|
623
|
+
// Add some events
|
|
624
|
+
const clickHandler = docEventListeners
|
|
625
|
+
.get("click")
|
|
626
|
+
?.values()
|
|
627
|
+
.next().value;
|
|
628
|
+
if (clickHandler) {
|
|
629
|
+
clickHandler({
|
|
630
|
+
target: {
|
|
631
|
+
getAttribute: vi.fn(() => null),
|
|
632
|
+
textContent: "Test",
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
stopSessionSummary();
|
|
637
|
+
// Re-initialize and check that buffer was cleared
|
|
638
|
+
setupSessionSummary();
|
|
639
|
+
// Trigger summary immediately - should have no events
|
|
640
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
641
|
+
if (intervalCallback) {
|
|
642
|
+
intervalCallback();
|
|
643
|
+
expect(vi.mocked(captureFn)).not.toHaveBeenCalled();
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
it("should handle teardown errors gracefully", () => {
|
|
647
|
+
setupSessionSummary();
|
|
648
|
+
// Make removeEventListener throw
|
|
649
|
+
mockRemoveEventListener.mockImplementation(() => {
|
|
650
|
+
throw new Error("Remove failed");
|
|
651
|
+
});
|
|
652
|
+
// Should not throw
|
|
653
|
+
expect(() => stopSessionSummary()).not.toThrow();
|
|
654
|
+
});
|
|
655
|
+
it("should restore original history methods", () => {
|
|
656
|
+
const originalPushState = vi.fn();
|
|
657
|
+
const originalReplaceState = vi.fn();
|
|
658
|
+
global.history.pushState = originalPushState;
|
|
659
|
+
global.history.replaceState = originalReplaceState;
|
|
660
|
+
setupSessionSummary();
|
|
661
|
+
// History methods should be wrapped
|
|
662
|
+
expect(global.history.pushState).not.toBe(originalPushState);
|
|
663
|
+
expect(global.history.replaceState).not.toBe(originalReplaceState);
|
|
664
|
+
stopSessionSummary();
|
|
665
|
+
// History methods should be restored (they are bound functions)
|
|
666
|
+
// We can verify they work correctly by calling them
|
|
667
|
+
global.history.pushState({}, "", "/test");
|
|
668
|
+
global.history.replaceState({}, "", "/test2");
|
|
669
|
+
expect(originalPushState).toHaveBeenCalledWith({}, "", "/test");
|
|
670
|
+
expect(originalReplaceState).toHaveBeenCalledWith({}, "", "/test2");
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
describe("sample events", () => {
|
|
674
|
+
it("should include last 20 events in sample", async () => {
|
|
675
|
+
setupSessionSummary();
|
|
676
|
+
const clickHandler = docEventListeners
|
|
677
|
+
.get("click")
|
|
678
|
+
?.values()
|
|
679
|
+
.next().value;
|
|
680
|
+
if (clickHandler) {
|
|
681
|
+
// Add 30 events
|
|
682
|
+
for (let i = 0; i < EVENTS_TO_GENERATE; i++) {
|
|
683
|
+
clickHandler({
|
|
684
|
+
target: {
|
|
685
|
+
getAttribute: vi.fn(() => null),
|
|
686
|
+
textContent: `Click ${i}`,
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Trigger summary
|
|
692
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
693
|
+
if (intervalCallback) {
|
|
694
|
+
await intervalCallback();
|
|
695
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
696
|
+
eventCount: EVENTS_TO_GENERATE,
|
|
697
|
+
sample: expect.arrayContaining([
|
|
698
|
+
expect.objectContaining({
|
|
699
|
+
kind: "ui",
|
|
700
|
+
action: "click",
|
|
701
|
+
}),
|
|
702
|
+
]),
|
|
703
|
+
}));
|
|
704
|
+
const call = vi.mocked(captureFn).mock.calls[0];
|
|
705
|
+
if (call?.[1]) {
|
|
706
|
+
const payload = call[1];
|
|
707
|
+
expect(payload.sample.length).toBeLessThanOrEqual(SAMPLE_SIZE);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
describe("ALLOW_BROWSER_AI flag", () => {
|
|
713
|
+
it("should not attempt AI summarization when ALLOW_BROWSER_AI is false", async () => {
|
|
714
|
+
// Re-mock constants with ALLOW_BROWSER_AI = false
|
|
715
|
+
vi.doUnmock("../../session/constants.js");
|
|
716
|
+
vi.mock("../../session/constants.js", () => ({
|
|
717
|
+
ALLOW_BROWSER_AI: false,
|
|
718
|
+
SESSION_SUMMARY_ENABLED: true,
|
|
719
|
+
SESSION_SUMMARY_MAX_EVENTS: 100,
|
|
720
|
+
SESSION_SUMMARY_WINDOW_MS: 30_000,
|
|
721
|
+
}));
|
|
722
|
+
// Setup AI mock
|
|
723
|
+
const mockPrompt = vi.fn();
|
|
724
|
+
global.window.ai = {
|
|
725
|
+
canCreateTextSession: vi.fn(),
|
|
726
|
+
createTextSession: vi.fn().mockResolvedValue({
|
|
727
|
+
prompt: mockPrompt,
|
|
728
|
+
}),
|
|
729
|
+
};
|
|
730
|
+
// Re-import to get new mock
|
|
731
|
+
const module = await import("../../session/session-summary.js");
|
|
732
|
+
module.setupSessionSummary();
|
|
733
|
+
// Add events
|
|
734
|
+
const clickHandler = docEventListeners
|
|
735
|
+
.get("click")
|
|
736
|
+
?.values()
|
|
737
|
+
.next().value;
|
|
738
|
+
if (clickHandler) {
|
|
739
|
+
clickHandler({
|
|
740
|
+
target: {
|
|
741
|
+
getAttribute: vi.fn(() => null),
|
|
742
|
+
textContent: "Test",
|
|
743
|
+
},
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
// Trigger summary
|
|
747
|
+
const intervalCallback = Array.from(intervalCallbacks.values())[0];
|
|
748
|
+
if (intervalCallback) {
|
|
749
|
+
await intervalCallback();
|
|
750
|
+
// AI should not be called
|
|
751
|
+
const windowWithAI = global.window;
|
|
752
|
+
if (windowWithAI.ai) {
|
|
753
|
+
expect(windowWithAI.ai.canCreateTextSession).not.toHaveBeenCalled();
|
|
754
|
+
}
|
|
755
|
+
expect(mockPrompt).not.toHaveBeenCalled();
|
|
756
|
+
expect(vi.mocked(captureFn)).toHaveBeenCalledWith("session_summary", expect.objectContaining({
|
|
757
|
+
summary: null,
|
|
758
|
+
}));
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
//# sourceMappingURL=session-summary.test.js.map
|