@prosdevlab/experience-sdk-plugins 0.1.4 → 0.3.0
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/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +150 -0
- package/README.md +141 -79
- package/dist/index.d.ts +813 -35
- package/dist/index.js +1910 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +63 -62
- package/src/exit-intent/exit-intent.test.ts +423 -0
- package/src/exit-intent/exit-intent.ts +371 -0
- package/src/exit-intent/index.ts +6 -0
- package/src/exit-intent/types.ts +59 -0
- package/src/index.ts +7 -0
- package/src/inline/index.ts +3 -0
- package/src/inline/inline.test.ts +620 -0
- package/src/inline/inline.ts +269 -0
- package/src/inline/insertion.ts +66 -0
- package/src/inline/types.ts +52 -0
- package/src/integration.test.ts +421 -0
- package/src/modal/form-rendering.ts +262 -0
- package/src/modal/form-styles.ts +212 -0
- package/src/modal/form-validation.test.ts +413 -0
- package/src/modal/form-validation.ts +126 -0
- package/src/modal/index.ts +3 -0
- package/src/modal/modal-styles.ts +204 -0
- package/src/modal/modal.browser.test.ts +164 -0
- package/src/modal/modal.test.ts +1294 -0
- package/src/modal/modal.ts +685 -0
- package/src/modal/types.ts +114 -0
- package/src/page-visits/index.ts +6 -0
- package/src/page-visits/page-visits.test.ts +562 -0
- package/src/page-visits/page-visits.ts +314 -0
- package/src/page-visits/types.ts +119 -0
- package/src/scroll-depth/index.ts +6 -0
- package/src/scroll-depth/scroll-depth.test.ts +580 -0
- package/src/scroll-depth/scroll-depth.ts +398 -0
- package/src/scroll-depth/types.ts +122 -0
- package/src/time-delay/index.ts +6 -0
- package/src/time-delay/time-delay.test.ts +477 -0
- package/src/time-delay/time-delay.ts +296 -0
- package/src/time-delay/types.ts +89 -0
- package/src/types.ts +20 -36
- package/src/utils/sanitize.ts +5 -2
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { SDK } from '@lytics/sdk-kit';
|
|
6
|
+
import { storagePlugin } from '@lytics/sdk-kit-plugins';
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { scrollDepthPlugin } from './index';
|
|
9
|
+
import type { ScrollDepthPluginConfig } from './types';
|
|
10
|
+
|
|
11
|
+
// Extend SDK type to include scrollDepth API
|
|
12
|
+
interface SDKWithScrollDepth extends SDK {
|
|
13
|
+
scrollDepth: {
|
|
14
|
+
getMaxPercent: () => number;
|
|
15
|
+
getCurrentPercent: () => number;
|
|
16
|
+
getThresholdsCrossed: () => number[];
|
|
17
|
+
reset: () => void;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('scrollDepthPlugin', () => {
|
|
22
|
+
let sdk: SDKWithScrollDepth;
|
|
23
|
+
let scrollEventListeners: Record<string, EventListener> = {};
|
|
24
|
+
let resizeEventListeners: Record<string, EventListener> = {};
|
|
25
|
+
let addEventListenerSpy: any;
|
|
26
|
+
let _removeEventListenerSpy: any;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Helper to initialize plugin with config
|
|
30
|
+
*/
|
|
31
|
+
const initPlugin = async (config?: ScrollDepthPluginConfig['scrollDepth']) => {
|
|
32
|
+
sdk = new SDK({
|
|
33
|
+
name: 'test-sdk',
|
|
34
|
+
storage: { backend: 'memory' },
|
|
35
|
+
}) as SDKWithScrollDepth;
|
|
36
|
+
|
|
37
|
+
if (config) {
|
|
38
|
+
sdk.set('scrollDepth', config);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
sdk.use(storagePlugin);
|
|
42
|
+
sdk.use(scrollDepthPlugin);
|
|
43
|
+
await sdk.init();
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Helper to set document height and scroll position
|
|
48
|
+
*/
|
|
49
|
+
const setScrollPosition = (scrollTop: number, scrollHeight: number, clientHeight: number) => {
|
|
50
|
+
// Mock scrollingElement
|
|
51
|
+
Object.defineProperty(document, 'scrollingElement', {
|
|
52
|
+
writable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
value: {
|
|
55
|
+
scrollTop,
|
|
56
|
+
scrollHeight,
|
|
57
|
+
clientHeight,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Also mock documentElement as fallback
|
|
62
|
+
Object.defineProperty(document, 'documentElement', {
|
|
63
|
+
writable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
value: {
|
|
66
|
+
scrollTop,
|
|
67
|
+
scrollHeight,
|
|
68
|
+
clientHeight,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Helper to simulate scroll event
|
|
75
|
+
*/
|
|
76
|
+
const simulateScroll = (scrollTop: number, scrollHeight: number, clientHeight: number) => {
|
|
77
|
+
setScrollPosition(scrollTop, scrollHeight, clientHeight);
|
|
78
|
+
const handler = scrollEventListeners.scroll;
|
|
79
|
+
if (handler) {
|
|
80
|
+
handler();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
vi.clearAllMocks();
|
|
86
|
+
vi.useFakeTimers();
|
|
87
|
+
|
|
88
|
+
// Reset event listener tracking
|
|
89
|
+
scrollEventListeners = {};
|
|
90
|
+
resizeEventListeners = {};
|
|
91
|
+
|
|
92
|
+
// Spy on addEventListener/removeEventListener
|
|
93
|
+
addEventListenerSpy = vi
|
|
94
|
+
.spyOn(window, 'addEventListener')
|
|
95
|
+
.mockImplementation((event: string, handler: any) => {
|
|
96
|
+
if (event === 'scroll') {
|
|
97
|
+
scrollEventListeners[event] = handler;
|
|
98
|
+
} else if (event === 'resize') {
|
|
99
|
+
resizeEventListeners[event] = handler;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
_removeEventListenerSpy = vi
|
|
104
|
+
.spyOn(window, 'removeEventListener')
|
|
105
|
+
.mockImplementation((event: string) => {
|
|
106
|
+
if (event === 'scroll') {
|
|
107
|
+
delete scrollEventListeners[event];
|
|
108
|
+
} else if (event === 'resize') {
|
|
109
|
+
delete resizeEventListeners[event];
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(async () => {
|
|
115
|
+
if (sdk) {
|
|
116
|
+
await sdk.destroy();
|
|
117
|
+
}
|
|
118
|
+
vi.restoreAllMocks();
|
|
119
|
+
vi.useRealTimers();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('initialization', () => {
|
|
123
|
+
it('should register with default config', async () => {
|
|
124
|
+
await initPlugin();
|
|
125
|
+
|
|
126
|
+
expect(sdk.scrollDepth).toBeDefined();
|
|
127
|
+
expect(sdk.scrollDepth.getMaxPercent).toBeDefined();
|
|
128
|
+
expect(sdk.scrollDepth.getCurrentPercent).toBeDefined();
|
|
129
|
+
expect(sdk.scrollDepth.getThresholdsCrossed).toBeDefined();
|
|
130
|
+
expect(sdk.scrollDepth.reset).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should use default thresholds [25, 50, 75, 100]', async () => {
|
|
134
|
+
await initPlugin();
|
|
135
|
+
vi.advanceTimersByTime(0);
|
|
136
|
+
|
|
137
|
+
// No thresholds crossed initially (no scroll event yet)
|
|
138
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([]);
|
|
139
|
+
|
|
140
|
+
// After scrolling, thresholds should trigger
|
|
141
|
+
simulateScroll(1000, 2000, 1000); // 100%
|
|
142
|
+
vi.advanceTimersByTime(100);
|
|
143
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50, 75, 100]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should register scroll listener', async () => {
|
|
147
|
+
await initPlugin();
|
|
148
|
+
vi.advanceTimersByTime(0);
|
|
149
|
+
|
|
150
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function), {
|
|
151
|
+
passive: true,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should register resize listener by default', async () => {
|
|
156
|
+
await initPlugin();
|
|
157
|
+
vi.advanceTimersByTime(0);
|
|
158
|
+
|
|
159
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function), {
|
|
160
|
+
passive: true,
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should not register resize listener when disabled', async () => {
|
|
165
|
+
await initPlugin({ recalculateOnResize: false });
|
|
166
|
+
vi.advanceTimersByTime(0);
|
|
167
|
+
|
|
168
|
+
const resizeCalls = addEventListenerSpy.mock.calls.filter((call) => call[0] === 'resize');
|
|
169
|
+
expect(resizeCalls).toHaveLength(0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should not check initial scroll position (waits for user interaction)', async () => {
|
|
173
|
+
// Set initial scroll to 100%
|
|
174
|
+
setScrollPosition(1000, 2000, 1000);
|
|
175
|
+
|
|
176
|
+
await initPlugin();
|
|
177
|
+
vi.advanceTimersByTime(0);
|
|
178
|
+
|
|
179
|
+
// Should not trigger thresholds until first scroll event
|
|
180
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([]);
|
|
181
|
+
|
|
182
|
+
// Simulate scroll - now thresholds should trigger
|
|
183
|
+
simulateScroll(1000, 2000, 1000);
|
|
184
|
+
vi.advanceTimersByTime(100);
|
|
185
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50, 75, 100]);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe('scroll percentage calculation', () => {
|
|
190
|
+
it('should calculate percentage with viewport included (default)', async () => {
|
|
191
|
+
await initPlugin();
|
|
192
|
+
vi.advanceTimersByTime(0);
|
|
193
|
+
|
|
194
|
+
// scrollTop=0, scrollHeight=2000, clientHeight=1000
|
|
195
|
+
// (0 + 1000) / 2000 = 50%
|
|
196
|
+
setScrollPosition(0, 2000, 1000);
|
|
197
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(50);
|
|
198
|
+
|
|
199
|
+
// scrollTop=500, scrollHeight=2000, clientHeight=1000
|
|
200
|
+
// (500 + 1000) / 2000 = 75%
|
|
201
|
+
setScrollPosition(500, 2000, 1000);
|
|
202
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(75);
|
|
203
|
+
|
|
204
|
+
// scrollTop=1000, scrollHeight=2000, clientHeight=1000
|
|
205
|
+
// (1000 + 1000) / 2000 = 100%
|
|
206
|
+
setScrollPosition(1000, 2000, 1000);
|
|
207
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(100);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should calculate percentage without viewport (Pathfora method)', async () => {
|
|
211
|
+
await initPlugin({ includeViewportHeight: false });
|
|
212
|
+
vi.advanceTimersByTime(0);
|
|
213
|
+
|
|
214
|
+
// scrollTop=0, scrollHeight=2000, clientHeight=1000
|
|
215
|
+
// 0 / (2000 - 1000) = 0%
|
|
216
|
+
setScrollPosition(0, 2000, 1000);
|
|
217
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(0);
|
|
218
|
+
|
|
219
|
+
// scrollTop=500, scrollHeight=2000, clientHeight=1000
|
|
220
|
+
// 500 / (2000 - 1000) = 50%
|
|
221
|
+
setScrollPosition(500, 2000, 1000);
|
|
222
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(50);
|
|
223
|
+
|
|
224
|
+
// scrollTop=1000, scrollHeight=2000, clientHeight=1000
|
|
225
|
+
// 1000 / (2000 - 1000) = 100%
|
|
226
|
+
setScrollPosition(1000, 2000, 1000);
|
|
227
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(100);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should handle content shorter than viewport', async () => {
|
|
231
|
+
await initPlugin();
|
|
232
|
+
vi.advanceTimersByTime(0);
|
|
233
|
+
|
|
234
|
+
// scrollHeight <= clientHeight → treat as 100%
|
|
235
|
+
setScrollPosition(0, 500, 1000);
|
|
236
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(100);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should cap percentage at 100', async () => {
|
|
240
|
+
await initPlugin();
|
|
241
|
+
vi.advanceTimersByTime(0);
|
|
242
|
+
|
|
243
|
+
// Edge case: scrollTop + clientHeight > scrollHeight
|
|
244
|
+
setScrollPosition(1500, 2000, 1000);
|
|
245
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(100);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('threshold triggering', () => {
|
|
250
|
+
it('should emit event when threshold is crossed', async () => {
|
|
251
|
+
const emitSpy = vi.fn();
|
|
252
|
+
|
|
253
|
+
await initPlugin({ thresholds: [50] });
|
|
254
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
255
|
+
vi.advanceTimersByTime(0);
|
|
256
|
+
|
|
257
|
+
// Scroll to 50% (scrollTop=0, scrollHeight=2000, clientHeight=1000 → 50%)
|
|
258
|
+
simulateScroll(0, 2000, 1000);
|
|
259
|
+
vi.advanceTimersByTime(100); // Throttle delay
|
|
260
|
+
|
|
261
|
+
expect(emitSpy).toHaveBeenCalledWith(
|
|
262
|
+
expect.objectContaining({
|
|
263
|
+
triggered: true,
|
|
264
|
+
threshold: 50,
|
|
265
|
+
percent: 50,
|
|
266
|
+
maxPercent: 50,
|
|
267
|
+
thresholdsCrossed: [50],
|
|
268
|
+
})
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should trigger multiple thresholds in order', async () => {
|
|
273
|
+
const events: any[] = [];
|
|
274
|
+
|
|
275
|
+
await initPlugin({ thresholds: [25, 50, 75] });
|
|
276
|
+
sdk.on('trigger:scrollDepth', (payload) => events.push(payload));
|
|
277
|
+
vi.advanceTimersByTime(0);
|
|
278
|
+
|
|
279
|
+
// Scroll to 55%
|
|
280
|
+
simulateScroll(100, 2000, 1000); // (100 + 1000) / 2000 = 55%
|
|
281
|
+
vi.advanceTimersByTime(100);
|
|
282
|
+
|
|
283
|
+
expect(events).toHaveLength(2);
|
|
284
|
+
expect(events[0].threshold).toBe(25);
|
|
285
|
+
expect(events[1].threshold).toBe(50);
|
|
286
|
+
|
|
287
|
+
// Scroll to 80%
|
|
288
|
+
simulateScroll(600, 2000, 1000); // (600 + 1000) / 2000 = 80%
|
|
289
|
+
vi.advanceTimersByTime(100);
|
|
290
|
+
|
|
291
|
+
expect(events).toHaveLength(3);
|
|
292
|
+
expect(events[2].threshold).toBe(75);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should only trigger each threshold once', async () => {
|
|
296
|
+
const emitSpy = vi.fn();
|
|
297
|
+
|
|
298
|
+
await initPlugin({ thresholds: [50] });
|
|
299
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
300
|
+
vi.advanceTimersByTime(0);
|
|
301
|
+
|
|
302
|
+
// Scroll to 50%
|
|
303
|
+
simulateScroll(0, 2000, 1000);
|
|
304
|
+
vi.advanceTimersByTime(100);
|
|
305
|
+
expect(emitSpy).toHaveBeenCalledTimes(1);
|
|
306
|
+
|
|
307
|
+
// Scroll to 60% (should not re-trigger)
|
|
308
|
+
simulateScroll(200, 2000, 1000);
|
|
309
|
+
vi.advanceTimersByTime(100);
|
|
310
|
+
expect(emitSpy).toHaveBeenCalledTimes(1);
|
|
311
|
+
|
|
312
|
+
// Scroll back to 40% and then to 50% again (should not re-trigger)
|
|
313
|
+
simulateScroll(0, 2000, 2000); // 0%
|
|
314
|
+
vi.advanceTimersByTime(100);
|
|
315
|
+
simulateScroll(0, 2000, 1000); // 50%
|
|
316
|
+
vi.advanceTimersByTime(100);
|
|
317
|
+
expect(emitSpy).toHaveBeenCalledTimes(1);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should track max scroll percentage', async () => {
|
|
321
|
+
await initPlugin({ thresholds: [50, 75] });
|
|
322
|
+
vi.advanceTimersByTime(0);
|
|
323
|
+
|
|
324
|
+
// Scroll to 60%
|
|
325
|
+
simulateScroll(200, 2000, 1000); // (200 + 1000) / 2000 = 60%
|
|
326
|
+
vi.advanceTimersByTime(100);
|
|
327
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(60);
|
|
328
|
+
|
|
329
|
+
// Scroll to 80%
|
|
330
|
+
simulateScroll(600, 2000, 1000); // (600 + 1000) / 2000 = 80%
|
|
331
|
+
vi.advanceTimersByTime(100);
|
|
332
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(80);
|
|
333
|
+
|
|
334
|
+
// Scroll back to 50% (max should still be 80%)
|
|
335
|
+
simulateScroll(0, 2000, 1000); // 50%
|
|
336
|
+
vi.advanceTimersByTime(100);
|
|
337
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(80);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should handle custom thresholds', async () => {
|
|
341
|
+
const events: any[] = [];
|
|
342
|
+
|
|
343
|
+
await initPlugin({ thresholds: [10, 90] });
|
|
344
|
+
sdk.on('trigger:scrollDepth', (payload) => events.push(payload));
|
|
345
|
+
vi.advanceTimersByTime(0);
|
|
346
|
+
|
|
347
|
+
// Scroll to 50% (should trigger 10 only)
|
|
348
|
+
simulateScroll(0, 2000, 1000); // 50%
|
|
349
|
+
vi.advanceTimersByTime(100);
|
|
350
|
+
expect(events).toHaveLength(1);
|
|
351
|
+
expect(events[0].threshold).toBe(10);
|
|
352
|
+
|
|
353
|
+
// Scroll to 95%
|
|
354
|
+
simulateScroll(900, 2000, 1000); // (900 + 1000) / 2000 = 95%
|
|
355
|
+
vi.advanceTimersByTime(100);
|
|
356
|
+
expect(events).toHaveLength(2);
|
|
357
|
+
expect(events[1].threshold).toBe(90);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe('throttling', () => {
|
|
362
|
+
it('should throttle scroll events (default 100ms)', async () => {
|
|
363
|
+
const emitSpy = vi.fn();
|
|
364
|
+
|
|
365
|
+
await initPlugin({ thresholds: [25, 50, 75] });
|
|
366
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
367
|
+
vi.advanceTimersByTime(0);
|
|
368
|
+
|
|
369
|
+
// First scroll triggers immediately
|
|
370
|
+
simulateScroll(100, 2000, 1000); // 55%
|
|
371
|
+
expect(emitSpy).toHaveBeenCalledTimes(2); // 25%, 50%
|
|
372
|
+
|
|
373
|
+
// Rapid subsequent scrolls should be throttled
|
|
374
|
+
simulateScroll(150, 2000, 1000); // 57.5%
|
|
375
|
+
simulateScroll(200, 2000, 1000); // 60%
|
|
376
|
+
simulateScroll(250, 2000, 1000); // 62.5%
|
|
377
|
+
simulateScroll(300, 2000, 1000); // 65%
|
|
378
|
+
|
|
379
|
+
// Still only 2 events (throttled)
|
|
380
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
381
|
+
|
|
382
|
+
// Advance past throttle - no new thresholds crossed yet
|
|
383
|
+
vi.advanceTimersByTime(100);
|
|
384
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
385
|
+
|
|
386
|
+
// Now scroll past next threshold
|
|
387
|
+
simulateScroll(600, 2000, 1000); // 80%
|
|
388
|
+
vi.advanceTimersByTime(100);
|
|
389
|
+
expect(emitSpy).toHaveBeenCalledTimes(3); // 75%
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should respect custom throttle interval', async () => {
|
|
393
|
+
const emitSpy = vi.fn();
|
|
394
|
+
|
|
395
|
+
await initPlugin({ thresholds: [50], throttle: 200 });
|
|
396
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
397
|
+
vi.advanceTimersByTime(0);
|
|
398
|
+
|
|
399
|
+
simulateScroll(0, 2000, 1000); // 50%
|
|
400
|
+
|
|
401
|
+
// First scroll fires immediately
|
|
402
|
+
expect(emitSpy).toHaveBeenCalledTimes(1);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('resize handling', () => {
|
|
407
|
+
it('should recalculate scroll on resize', async () => {
|
|
408
|
+
const emitSpy = vi.fn();
|
|
409
|
+
|
|
410
|
+
await initPlugin({ thresholds: [50] });
|
|
411
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
412
|
+
vi.advanceTimersByTime(0);
|
|
413
|
+
|
|
414
|
+
// Initial: scrollTop=0, scrollHeight=2000, clientHeight=1000 → 50%
|
|
415
|
+
setScrollPosition(0, 2000, 1000);
|
|
416
|
+
const resizeHandler = resizeEventListeners.resize;
|
|
417
|
+
if (resizeHandler && typeof resizeHandler === 'function') {
|
|
418
|
+
resizeHandler(new Event('resize') as any);
|
|
419
|
+
}
|
|
420
|
+
vi.advanceTimersByTime(100);
|
|
421
|
+
|
|
422
|
+
expect(emitSpy).toHaveBeenCalledWith(
|
|
423
|
+
expect.objectContaining({
|
|
424
|
+
threshold: 50,
|
|
425
|
+
percent: 50,
|
|
426
|
+
})
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('API methods', () => {
|
|
432
|
+
it('should return max scroll percentage', async () => {
|
|
433
|
+
await initPlugin();
|
|
434
|
+
vi.advanceTimersByTime(0);
|
|
435
|
+
|
|
436
|
+
// Initially 0
|
|
437
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(0);
|
|
438
|
+
|
|
439
|
+
// After scrolling, should update
|
|
440
|
+
simulateScroll(500, 2000, 1000); // 75%
|
|
441
|
+
vi.advanceTimersByTime(100);
|
|
442
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(75);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('should return current scroll percentage', async () => {
|
|
446
|
+
await initPlugin();
|
|
447
|
+
vi.advanceTimersByTime(0);
|
|
448
|
+
|
|
449
|
+
setScrollPosition(300, 2000, 1000); // (300 + 1000) / 2000 = 65%
|
|
450
|
+
expect(sdk.scrollDepth.getCurrentPercent()).toBe(65);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should return crossed thresholds in sorted order', async () => {
|
|
454
|
+
await initPlugin({ thresholds: [75, 25, 50, 100] });
|
|
455
|
+
vi.advanceTimersByTime(0);
|
|
456
|
+
|
|
457
|
+
simulateScroll(500, 2000, 1000); // 75%
|
|
458
|
+
vi.advanceTimersByTime(100);
|
|
459
|
+
|
|
460
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50, 75]);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('should reset tracking', async () => {
|
|
464
|
+
await initPlugin({ thresholds: [50, 75] });
|
|
465
|
+
vi.advanceTimersByTime(0);
|
|
466
|
+
|
|
467
|
+
// Scroll and trigger threshold
|
|
468
|
+
simulateScroll(500, 2000, 1000); // 75%
|
|
469
|
+
vi.advanceTimersByTime(100);
|
|
470
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(75);
|
|
471
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([50, 75]);
|
|
472
|
+
|
|
473
|
+
// Reset
|
|
474
|
+
sdk.scrollDepth.reset();
|
|
475
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(0);
|
|
476
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([]);
|
|
477
|
+
|
|
478
|
+
// Can re-trigger after reset
|
|
479
|
+
const emitSpy = vi.fn();
|
|
480
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
481
|
+
simulateScroll(500, 2000, 1000); // 75%
|
|
482
|
+
vi.advanceTimersByTime(100);
|
|
483
|
+
expect(emitSpy).toHaveBeenCalledTimes(2); // Both 50 and 75 fire again
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('cleanup', () => {
|
|
488
|
+
it('should allow manual cleanup via returned function', async () => {
|
|
489
|
+
await initPlugin();
|
|
490
|
+
vi.advanceTimersByTime(0);
|
|
491
|
+
|
|
492
|
+
// Verify listeners were added
|
|
493
|
+
expect(scrollEventListeners.scroll).toBeDefined();
|
|
494
|
+
expect(resizeEventListeners.resize).toBeDefined();
|
|
495
|
+
|
|
496
|
+
// Plugin exposes a cleanup function that can be called manually
|
|
497
|
+
// In practice, this is handled automatically by sdk-kit on destroy
|
|
498
|
+
// For now, we just verify the listeners exist
|
|
499
|
+
expect(sdk.scrollDepth).toBeDefined();
|
|
500
|
+
expect(sdk.scrollDepth.getMaxPercent).toBeDefined();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
describe('reset()', () => {
|
|
505
|
+
it('should clear triggered thresholds and max scroll', async () => {
|
|
506
|
+
const emitSpy = vi.fn();
|
|
507
|
+
|
|
508
|
+
await initPlugin({ thresholds: [25, 50, 75] });
|
|
509
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
510
|
+
vi.advanceTimersByTime(0);
|
|
511
|
+
|
|
512
|
+
// Scroll to 50%
|
|
513
|
+
simulateScroll(1000, 3000, 1000);
|
|
514
|
+
vi.advanceTimersByTime(200);
|
|
515
|
+
|
|
516
|
+
// Should have triggered 25% and 50%
|
|
517
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50]);
|
|
518
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBeGreaterThan(0);
|
|
519
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
520
|
+
|
|
521
|
+
// Reset
|
|
522
|
+
sdk.scrollDepth.reset();
|
|
523
|
+
|
|
524
|
+
// Should clear state
|
|
525
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([]);
|
|
526
|
+
expect(sdk.scrollDepth.getMaxPercent()).toBe(0);
|
|
527
|
+
|
|
528
|
+
// Scroll again to 50% should trigger again
|
|
529
|
+
emitSpy.mockClear();
|
|
530
|
+
simulateScroll(1000, 3000, 1000);
|
|
531
|
+
vi.advanceTimersByTime(200);
|
|
532
|
+
|
|
533
|
+
// Should trigger both 25% and 50% again
|
|
534
|
+
expect(emitSpy).toHaveBeenCalledTimes(2);
|
|
535
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50]);
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe('Pathfora compatibility tests', () => {
|
|
540
|
+
it('should match Pathfora test: scrollPercentageToDisplay 50', async () => {
|
|
541
|
+
const emitSpy = vi.fn();
|
|
542
|
+
|
|
543
|
+
// Pathfora config
|
|
544
|
+
await initPlugin({
|
|
545
|
+
thresholds: [50],
|
|
546
|
+
includeViewportHeight: false, // Pathfora method
|
|
547
|
+
});
|
|
548
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
549
|
+
vi.advanceTimersByTime(0);
|
|
550
|
+
|
|
551
|
+
// Body height: 4000px, scroll to full height
|
|
552
|
+
simulateScroll(4000, 4000, 1000); // 100% scrolled
|
|
553
|
+
vi.advanceTimersByTime(200); // Pathfora test uses 200ms delay
|
|
554
|
+
|
|
555
|
+
expect(emitSpy).toHaveBeenCalled();
|
|
556
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toContain(50);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should match Pathfora test: scrollPercentageToDisplay 30 with scroll to height/2', async () => {
|
|
560
|
+
const emitSpy = vi.fn();
|
|
561
|
+
|
|
562
|
+
// Pathfora config
|
|
563
|
+
await initPlugin({
|
|
564
|
+
thresholds: [30],
|
|
565
|
+
includeViewportHeight: false,
|
|
566
|
+
});
|
|
567
|
+
sdk.on('trigger:scrollDepth', emitSpy);
|
|
568
|
+
vi.advanceTimersByTime(0);
|
|
569
|
+
|
|
570
|
+
// Body height: 4000px, scroll to height/2 (2000px)
|
|
571
|
+
// scrollTop=2000, scrollHeight=4000, clientHeight=1000
|
|
572
|
+
// 2000 / (4000 - 1000) = 66.67%
|
|
573
|
+
simulateScroll(2000, 4000, 1000);
|
|
574
|
+
vi.advanceTimersByTime(100);
|
|
575
|
+
|
|
576
|
+
expect(emitSpy).toHaveBeenCalled();
|
|
577
|
+
expect(sdk.scrollDepth.getThresholdsCrossed()).toContain(30);
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
});
|