@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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +141 -79
  4. package/dist/index.d.ts +813 -35
  5. package/dist/index.js +1910 -66
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/banner/banner.ts +63 -62
  9. package/src/exit-intent/exit-intent.test.ts +423 -0
  10. package/src/exit-intent/exit-intent.ts +371 -0
  11. package/src/exit-intent/index.ts +6 -0
  12. package/src/exit-intent/types.ts +59 -0
  13. package/src/index.ts +7 -0
  14. package/src/inline/index.ts +3 -0
  15. package/src/inline/inline.test.ts +620 -0
  16. package/src/inline/inline.ts +269 -0
  17. package/src/inline/insertion.ts +66 -0
  18. package/src/inline/types.ts +52 -0
  19. package/src/integration.test.ts +421 -0
  20. package/src/modal/form-rendering.ts +262 -0
  21. package/src/modal/form-styles.ts +212 -0
  22. package/src/modal/form-validation.test.ts +413 -0
  23. package/src/modal/form-validation.ts +126 -0
  24. package/src/modal/index.ts +3 -0
  25. package/src/modal/modal-styles.ts +204 -0
  26. package/src/modal/modal.browser.test.ts +164 -0
  27. package/src/modal/modal.test.ts +1294 -0
  28. package/src/modal/modal.ts +685 -0
  29. package/src/modal/types.ts +114 -0
  30. package/src/page-visits/index.ts +6 -0
  31. package/src/page-visits/page-visits.test.ts +562 -0
  32. package/src/page-visits/page-visits.ts +314 -0
  33. package/src/page-visits/types.ts +119 -0
  34. package/src/scroll-depth/index.ts +6 -0
  35. package/src/scroll-depth/scroll-depth.test.ts +580 -0
  36. package/src/scroll-depth/scroll-depth.ts +398 -0
  37. package/src/scroll-depth/types.ts +122 -0
  38. package/src/time-delay/index.ts +6 -0
  39. package/src/time-delay/time-delay.test.ts +477 -0
  40. package/src/time-delay/time-delay.ts +296 -0
  41. package/src/time-delay/types.ts +89 -0
  42. package/src/types.ts +20 -36
  43. package/src/utils/sanitize.ts +5 -2
@@ -0,0 +1,477 @@
1
+ /** @module timeDelayPlugin */
2
+
3
+ import { SDK } from '@lytics/sdk-kit';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { timeDelayPlugin } from './time-delay';
6
+ import type { TimeDelayEvent, TimeDelayPluginConfig } from './types';
7
+
8
+ describe('Time Delay Plugin', () => {
9
+ // Use fake timers for time-based tests
10
+ beforeEach(() => {
11
+ vi.useFakeTimers();
12
+ // Ensure document is visible by default
13
+ Object.defineProperty(document, 'hidden', {
14
+ writable: true,
15
+ configurable: true,
16
+ value: false,
17
+ });
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ vi.useRealTimers();
23
+ });
24
+
25
+ /**
26
+ * Helper to initialize SDK with time delay plugin
27
+ */
28
+ function initPlugin(config: TimeDelayPluginConfig = {}) {
29
+ const sdk = new SDK(config);
30
+ sdk.use(timeDelayPlugin);
31
+ return sdk;
32
+ }
33
+
34
+ describe('Basic Functionality', () => {
35
+ it('should trigger after configured delay', async () => {
36
+ const events: TimeDelayEvent[] = [];
37
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: false } });
38
+
39
+ sdk.on('trigger:timeDelay', (event) => {
40
+ events.push(event);
41
+ });
42
+
43
+ await sdk.init();
44
+
45
+ // Before delay
46
+ expect(events.length).toBe(0);
47
+
48
+ // Fast forward to trigger time
49
+ vi.advanceTimersByTime(5000);
50
+
51
+ // Should have triggered
52
+ expect(events.length).toBe(1);
53
+ expect(events[0].elapsed).toBeGreaterThanOrEqual(5000);
54
+ expect(events[0].activeElapsed).toBeGreaterThanOrEqual(5000);
55
+ expect(events[0].wasPaused).toBe(false);
56
+ });
57
+
58
+ it('should not trigger if delay is 0', async () => {
59
+ const events: TimeDelayEvent[] = [];
60
+ const sdk = initPlugin({ timeDelay: { delay: 0 } });
61
+
62
+ sdk.on('trigger:timeDelay', (event) => {
63
+ events.push(event);
64
+ });
65
+
66
+ await sdk.init();
67
+
68
+ // Advance some time
69
+ vi.advanceTimersByTime(10000);
70
+
71
+ // Should not trigger
72
+ expect(events.length).toBe(0);
73
+ });
74
+
75
+ it('should update context with elapsed time', async () => {
76
+ const events: TimeDelayEvent[] = [];
77
+ const sdk = initPlugin({ timeDelay: { delay: 3000, pauseWhenHidden: false } });
78
+
79
+ sdk.on('trigger:timeDelay', (event) => {
80
+ events.push(event);
81
+ });
82
+
83
+ await sdk.init();
84
+
85
+ vi.advanceTimersByTime(3000);
86
+
87
+ expect(events.length).toBe(1);
88
+ expect(events[0].timestamp).toBeDefined();
89
+ expect(events[0].elapsed).toBeGreaterThanOrEqual(3000);
90
+ expect(events[0].activeElapsed).toBeGreaterThanOrEqual(3000);
91
+ });
92
+
93
+ it('should only trigger once', async () => {
94
+ const events: TimeDelayEvent[] = [];
95
+ const sdk = initPlugin({ timeDelay: { delay: 2000, pauseWhenHidden: false } });
96
+
97
+ sdk.on('trigger:timeDelay', (event) => {
98
+ events.push(event);
99
+ });
100
+
101
+ await sdk.init();
102
+
103
+ vi.advanceTimersByTime(2000);
104
+ expect(events.length).toBe(1);
105
+
106
+ // Advance more time
107
+ vi.advanceTimersByTime(5000);
108
+ expect(events.length).toBe(1); // Still only 1
109
+ });
110
+ });
111
+
112
+ describe('Visibility Handling', () => {
113
+ it('should pause timer when tab is hidden', async () => {
114
+ const events: TimeDelayEvent[] = [];
115
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: true } });
116
+
117
+ sdk.on('trigger:timeDelay', (event) => {
118
+ events.push(event);
119
+ });
120
+
121
+ await sdk.init();
122
+
123
+ // 2 seconds active
124
+ vi.advanceTimersByTime(2000);
125
+
126
+ // Hide tab
127
+ Object.defineProperty(document, 'hidden', {
128
+ writable: true,
129
+ configurable: true,
130
+ value: true,
131
+ });
132
+ document.dispatchEvent(new Event('visibilitychange'));
133
+
134
+ // 3 seconds hidden (should be paused)
135
+ vi.advanceTimersByTime(3000);
136
+
137
+ // Should NOT have triggered yet
138
+ expect(events.length).toBe(0);
139
+
140
+ // Show tab
141
+ Object.defineProperty(document, 'hidden', {
142
+ writable: true,
143
+ configurable: true,
144
+ value: false,
145
+ });
146
+ document.dispatchEvent(new Event('visibilitychange'));
147
+
148
+ // 3 more seconds active (total 5 active)
149
+ vi.advanceTimersByTime(3000);
150
+
151
+ // Should have triggered
152
+ expect(events.length).toBe(1);
153
+ expect(events[0].activeElapsed).toBeGreaterThanOrEqual(5000);
154
+ expect(events[0].elapsed).toBeGreaterThanOrEqual(8000); // 2 + 3 + 3
155
+ expect(events[0].wasPaused).toBe(true);
156
+ expect(events[0].visibilityChanges).toBeGreaterThan(0);
157
+ });
158
+
159
+ it('should not pause if pauseWhenHidden is false', async () => {
160
+ const events: TimeDelayEvent[] = [];
161
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: false } });
162
+
163
+ sdk.on('trigger:timeDelay', (event) => {
164
+ events.push(event);
165
+ });
166
+
167
+ await sdk.init();
168
+
169
+ // 2 seconds active
170
+ vi.advanceTimersByTime(2000);
171
+
172
+ // Hide tab
173
+ Object.defineProperty(document, 'hidden', {
174
+ writable: true,
175
+ configurable: true,
176
+ value: true,
177
+ });
178
+ document.dispatchEvent(new Event('visibilitychange'));
179
+
180
+ // 3 more seconds (should still count)
181
+ vi.advanceTimersByTime(3000);
182
+
183
+ // Should have triggered (total 5 seconds)
184
+ expect(events.length).toBe(1);
185
+ expect(events[0].elapsed).toBeGreaterThanOrEqual(5000);
186
+ expect(events[0].wasPaused).toBe(false); // Never paused
187
+ });
188
+
189
+ it('should handle rapid visibility changes', async () => {
190
+ const events: TimeDelayEvent[] = [];
191
+ const sdk = initPlugin({ timeDelay: { delay: 10000, pauseWhenHidden: true } });
192
+
193
+ sdk.on('trigger:timeDelay', (event) => {
194
+ events.push(event);
195
+ });
196
+
197
+ await sdk.init();
198
+
199
+ // Multiple hide/show cycles
200
+ for (let i = 0; i < 5; i++) {
201
+ vi.advanceTimersByTime(1000); // 1s active
202
+
203
+ // Hide
204
+ Object.defineProperty(document, 'hidden', {
205
+ writable: true,
206
+ configurable: true,
207
+ value: true,
208
+ });
209
+ document.dispatchEvent(new Event('visibilitychange'));
210
+ vi.advanceTimersByTime(500); // 0.5s hidden
211
+
212
+ // Show
213
+ Object.defineProperty(document, 'hidden', {
214
+ writable: true,
215
+ configurable: true,
216
+ value: false,
217
+ });
218
+ document.dispatchEvent(new Event('visibilitychange'));
219
+ }
220
+
221
+ // Total: 5s active, 2.5s hidden = 7.5s elapsed, 5s active
222
+ expect(events.length).toBe(0); // Not triggered yet (need 10s active)
223
+
224
+ // Add 5 more seconds active (total 10s active)
225
+ vi.advanceTimersByTime(5000);
226
+
227
+ // Should trigger
228
+ expect(events.length).toBe(1);
229
+ expect(events[0].activeElapsed).toBeGreaterThanOrEqual(9000); // Allow some tolerance
230
+ expect(events[0].wasPaused).toBe(true);
231
+ expect(events[0].visibilityChanges).toBeGreaterThan(5);
232
+ });
233
+
234
+ it('should handle starting hidden', async () => {
235
+ // Set document hidden before init
236
+ Object.defineProperty(document, 'hidden', {
237
+ writable: true,
238
+ configurable: true,
239
+ value: true,
240
+ });
241
+
242
+ const events: TimeDelayEvent[] = [];
243
+ const sdk = initPlugin({ timeDelay: { delay: 3000, pauseWhenHidden: true } });
244
+
245
+ sdk.on('trigger:timeDelay', (event) => {
246
+ events.push(event);
247
+ });
248
+
249
+ await sdk.init();
250
+
251
+ // 2 seconds hidden (should be paused from start)
252
+ vi.advanceTimersByTime(2000);
253
+ expect(events.length).toBe(0);
254
+
255
+ // Show tab
256
+ Object.defineProperty(document, 'hidden', { writable: true, value: false });
257
+ document.dispatchEvent(new Event('visibilitychange'));
258
+
259
+ // 3 seconds active
260
+ vi.advanceTimersByTime(3000);
261
+
262
+ // Should trigger
263
+ expect(events.length).toBe(1);
264
+ expect(events[0].activeElapsed).toBeGreaterThanOrEqual(3000);
265
+ });
266
+ });
267
+
268
+ describe('API Methods', () => {
269
+ it('should expose getElapsed method', async () => {
270
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: false } });
271
+ await sdk.init();
272
+
273
+ vi.advanceTimersByTime(2000);
274
+
275
+ const elapsed = sdk.timeDelay.getElapsed();
276
+ expect(elapsed).toBeGreaterThanOrEqual(2000);
277
+ expect(elapsed).toBeLessThan(3000);
278
+ });
279
+
280
+ it('should expose getActiveElapsed method', async () => {
281
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: true } });
282
+ await sdk.init();
283
+
284
+ // 2s active
285
+ vi.advanceTimersByTime(2000);
286
+
287
+ // Hide for 3s
288
+ Object.defineProperty(document, 'hidden', { writable: true, value: true });
289
+ document.dispatchEvent(new Event('visibilitychange'));
290
+ vi.advanceTimersByTime(3000);
291
+
292
+ const activeElapsed = sdk.timeDelay.getActiveElapsed();
293
+ const totalElapsed = sdk.timeDelay.getElapsed();
294
+
295
+ expect(activeElapsed).toBeGreaterThanOrEqual(2000);
296
+ expect(activeElapsed).toBeLessThan(3000);
297
+ expect(totalElapsed).toBeGreaterThanOrEqual(5000);
298
+ });
299
+
300
+ it('should expose getRemaining method', async () => {
301
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: false } });
302
+ await sdk.init();
303
+
304
+ vi.advanceTimersByTime(2000);
305
+
306
+ const remaining = sdk.timeDelay.getRemaining();
307
+ expect(remaining).toBeGreaterThan(2000);
308
+ expect(remaining).toBeLessThanOrEqual(3000);
309
+ });
310
+
311
+ it('should return 0 for getRemaining after trigger', async () => {
312
+ const sdk = initPlugin({ timeDelay: { delay: 2000, pauseWhenHidden: false } });
313
+ await sdk.init();
314
+
315
+ vi.advanceTimersByTime(2000);
316
+
317
+ const remaining = sdk.timeDelay.getRemaining();
318
+ expect(remaining).toBe(0);
319
+ });
320
+
321
+ it('should expose isPaused method', async () => {
322
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: true } });
323
+ await sdk.init();
324
+
325
+ expect(sdk.timeDelay.isPaused()).toBe(false);
326
+
327
+ // Hide tab
328
+ Object.defineProperty(document, 'hidden', { writable: true, value: true });
329
+ document.dispatchEvent(new Event('visibilitychange'));
330
+
331
+ expect(sdk.timeDelay.isPaused()).toBe(true);
332
+ });
333
+
334
+ it('should expose isTriggered method', async () => {
335
+ const sdk = initPlugin({ timeDelay: { delay: 2000, pauseWhenHidden: false } });
336
+ await sdk.init();
337
+
338
+ expect(sdk.timeDelay.isTriggered()).toBe(false);
339
+
340
+ vi.advanceTimersByTime(2000);
341
+
342
+ expect(sdk.timeDelay.isTriggered()).toBe(true);
343
+ });
344
+
345
+ it('should expose reset method', async () => {
346
+ const events: TimeDelayEvent[] = [];
347
+ const sdk = initPlugin({ timeDelay: { delay: 3000, pauseWhenHidden: false } });
348
+
349
+ sdk.on('trigger:timeDelay', (event) => {
350
+ events.push(event);
351
+ });
352
+
353
+ await sdk.init();
354
+
355
+ vi.advanceTimersByTime(3000);
356
+ expect(events.length).toBe(1);
357
+ expect(sdk.timeDelay.isTriggered()).toBe(true);
358
+
359
+ // Reset
360
+ sdk.timeDelay.reset();
361
+ expect(sdk.timeDelay.isTriggered()).toBe(false);
362
+
363
+ // Should trigger again after 3s
364
+ vi.advanceTimersByTime(3000);
365
+ expect(events.length).toBe(2);
366
+ });
367
+ });
368
+
369
+ describe('Cleanup', () => {
370
+ it('should cleanup timers on destroy', async () => {
371
+ const events: TimeDelayEvent[] = [];
372
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: false } });
373
+
374
+ sdk.on('trigger:timeDelay', (event) => {
375
+ events.push(event);
376
+ });
377
+
378
+ await sdk.init();
379
+
380
+ vi.advanceTimersByTime(2000);
381
+
382
+ // Destroy SDK
383
+ await sdk.destroy();
384
+
385
+ // Advance past trigger time
386
+ vi.advanceTimersByTime(5000);
387
+
388
+ // Should NOT have triggered (timer was cleared)
389
+ expect(events.length).toBe(0);
390
+ });
391
+
392
+ it('should cleanup visibility listeners on destroy', async () => {
393
+ const sdk = initPlugin({ timeDelay: { delay: 5000, pauseWhenHidden: true } });
394
+ await sdk.init();
395
+
396
+ const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
397
+
398
+ // Destroy SDK
399
+ await sdk.destroy();
400
+
401
+ // Should have removed visibility listener
402
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
403
+ });
404
+ });
405
+
406
+ describe('Edge Cases', () => {
407
+ it('should handle delay elapsed during pause', async () => {
408
+ const events: TimeDelayEvent[] = [];
409
+ const sdk = initPlugin({ timeDelay: { delay: 2000, pauseWhenHidden: true } });
410
+
411
+ sdk.on('trigger:timeDelay', (event) => {
412
+ events.push(event);
413
+ });
414
+
415
+ await sdk.init();
416
+
417
+ // 1s active
418
+ vi.advanceTimersByTime(1000);
419
+
420
+ // Hide tab
421
+ Object.defineProperty(document, 'hidden', {
422
+ writable: true,
423
+ configurable: true,
424
+ value: true,
425
+ });
426
+ document.dispatchEvent(new Event('visibilitychange'));
427
+
428
+ // 5s hidden (enough to complete delay if not paused)
429
+ vi.advanceTimersByTime(5000);
430
+
431
+ // Should NOT have triggered yet
432
+ expect(events.length).toBe(0);
433
+
434
+ // Show tab
435
+ Object.defineProperty(document, 'hidden', {
436
+ writable: true,
437
+ configurable: true,
438
+ value: false,
439
+ });
440
+ document.dispatchEvent(new Event('visibilitychange'));
441
+
442
+ // 1 more second active (total 2s active)
443
+ vi.advanceTimersByTime(1000);
444
+
445
+ // Should trigger
446
+ expect(events.length).toBe(1);
447
+ });
448
+
449
+ it('should work in environments without Page Visibility API', async () => {
450
+ // Mock missing document.hidden
451
+ const originalHidden = Object.getOwnPropertyDescriptor(document, 'hidden');
452
+ Object.defineProperty(document, 'hidden', {
453
+ get: () => undefined,
454
+ configurable: true,
455
+ });
456
+
457
+ const events: TimeDelayEvent[] = [];
458
+ const sdk = initPlugin({ timeDelay: { delay: 2000, pauseWhenHidden: true } });
459
+
460
+ sdk.on('trigger:timeDelay', (event) => {
461
+ events.push(event);
462
+ });
463
+
464
+ await sdk.init();
465
+
466
+ vi.advanceTimersByTime(2000);
467
+
468
+ // Should still trigger (falls back to no pause)
469
+ expect(events.length).toBe(1);
470
+
471
+ // Restore
472
+ if (originalHidden) {
473
+ Object.defineProperty(document, 'hidden', originalHidden);
474
+ }
475
+ });
476
+ });
477
+ });