@prosdevlab/experience-sdk-plugins 0.1.4 → 0.2.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.
@@ -0,0 +1,562 @@
1
+ /**
2
+ * Page Visits Plugin Tests
3
+ *
4
+ * Comprehensive tests covering:
5
+ * - Session and lifetime counting
6
+ * - First-visit detection
7
+ * - Storage persistence
8
+ * - DNT (Do Not Track) support
9
+ * - API methods
10
+ * - Event emission
11
+ */
12
+
13
+ import { SDK } from '@lytics/sdk-kit';
14
+ import { storagePlugin } from '@lytics/sdk-kit-plugins';
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
16
+ import { pageVisitsPlugin } from './index';
17
+ import type { PageVisitsEvent, PageVisitsPlugin } from './types';
18
+
19
+ type SDKWithPageVisits = SDK & {
20
+ pageVisits: PageVisitsPlugin;
21
+ };
22
+
23
+ describe('Page Visits Plugin', () => {
24
+ let sdk: SDKWithPageVisits;
25
+
26
+ // Helper to initialize plugin with config
27
+ const initPlugin = async (config?: any) => {
28
+ sdk = new SDK({
29
+ pageVisits: config,
30
+ storage: { backend: 'memory' },
31
+ }) as SDKWithPageVisits;
32
+ sdk.use(storagePlugin);
33
+ sdk.use(pageVisitsPlugin);
34
+ await sdk.init();
35
+ };
36
+
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ // Clear storage
40
+ sessionStorage.clear();
41
+ localStorage.clear();
42
+ // Reset DNT mock
43
+ Object.defineProperty(navigator, 'doNotTrack', {
44
+ value: '0',
45
+ configurable: true,
46
+ });
47
+ });
48
+
49
+ afterEach(() => {
50
+ if (sdk) {
51
+ sdk.destroy?.();
52
+ }
53
+ });
54
+
55
+ describe('Default Configuration', () => {
56
+ it('should initialize with default config', async () => {
57
+ await initPlugin();
58
+ expect(sdk.pageVisits).toBeDefined();
59
+ });
60
+
61
+ it('should auto-increment on initialization', async () => {
62
+ await initPlugin();
63
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
64
+ expect(sdk.pageVisits.getSessionCount()).toBe(1);
65
+ });
66
+
67
+ it('should detect first visit', async () => {
68
+ await initPlugin();
69
+ expect(sdk.pageVisits.isFirstVisit()).toBe(false); // After increment, no longer first
70
+ });
71
+ });
72
+
73
+ describe('Session Counter (sessionStorage)', () => {
74
+ it('should increment session count on each page load', async () => {
75
+ await initPlugin();
76
+ expect(sdk.pageVisits.getSessionCount()).toBe(1);
77
+
78
+ // Simulate second page load (reinitialize)
79
+ sdk.destroy?.();
80
+ await initPlugin();
81
+ expect(sdk.pageVisits.getSessionCount()).toBe(2);
82
+ });
83
+
84
+ it('should reset session count when sessionStorage is cleared', async () => {
85
+ await initPlugin();
86
+ expect(sdk.pageVisits.getSessionCount()).toBe(1);
87
+
88
+ // Clear session storage
89
+ sessionStorage.clear();
90
+
91
+ // Reinitialize
92
+ sdk.destroy?.();
93
+ await initPlugin();
94
+ expect(sdk.pageVisits.getSessionCount()).toBe(1); // Back to 1
95
+ });
96
+
97
+ it('should not persist session count across tabs', async () => {
98
+ await initPlugin();
99
+ expect(sdk.pageVisits.getSessionCount()).toBe(1);
100
+
101
+ // Session storage is tab-specific, so count shouldn't persist
102
+ // (This is a characteristic test, not a functional test)
103
+ expect(sessionStorage.length).toBeGreaterThan(0);
104
+ });
105
+ });
106
+
107
+ describe('Lifetime Counter (localStorage)', () => {
108
+ it('should increment total count on each page load', async () => {
109
+ await initPlugin();
110
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
111
+
112
+ // Simulate second page load (reinitialize)
113
+ sdk.destroy?.();
114
+ await initPlugin();
115
+ expect(sdk.pageVisits.getTotalCount()).toBe(2);
116
+
117
+ // Third page load
118
+ sdk.destroy?.();
119
+ await initPlugin();
120
+ expect(sdk.pageVisits.getTotalCount()).toBe(3);
121
+ });
122
+
123
+ it('should persist total count in localStorage', async () => {
124
+ await initPlugin();
125
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
126
+
127
+ // Check localStorage directly
128
+ const stored = localStorage.getItem('pageVisits:total');
129
+ expect(stored).toBeDefined();
130
+ expect(stored).not.toBeNull();
131
+ });
132
+
133
+ it('should store timestamps for first and last visit', async () => {
134
+ await initPlugin();
135
+
136
+ const firstVisitTime = sdk.pageVisits.getFirstVisitTime();
137
+ const lastVisitTime = sdk.pageVisits.getLastVisitTime();
138
+
139
+ expect(firstVisitTime).toBeDefined();
140
+ expect(lastVisitTime).toBeDefined();
141
+ expect(typeof firstVisitTime).toBe('number');
142
+ expect(typeof lastVisitTime).toBe('number');
143
+ });
144
+
145
+ it('should keep first visit time constant across visits', async () => {
146
+ await initPlugin();
147
+ const firstVisitTime1 = sdk.pageVisits.getFirstVisitTime();
148
+
149
+ // Second visit
150
+ sdk.destroy?.();
151
+ await initPlugin();
152
+ const firstVisitTime2 = sdk.pageVisits.getFirstVisitTime();
153
+
154
+ expect(firstVisitTime1).toBe(firstVisitTime2);
155
+ });
156
+
157
+ it('should update last visit time on each visit', async () => {
158
+ await initPlugin();
159
+ const lastVisitTime1 = sdk.pageVisits.getLastVisitTime();
160
+
161
+ // Wait a bit
162
+ await new Promise((resolve) => setTimeout(resolve, 10));
163
+
164
+ // Second visit
165
+ sdk.destroy?.();
166
+ await initPlugin();
167
+ const lastVisitTime2 = sdk.pageVisits.getLastVisitTime();
168
+
169
+ expect(lastVisitTime2).toBeGreaterThan(lastVisitTime1 ?? 0);
170
+ });
171
+ });
172
+
173
+ describe('First Visit Detection', () => {
174
+ it('should detect first visit when no data exists', async () => {
175
+ const events: PageVisitsEvent[] = [];
176
+ sdk = new SDK({
177
+ pageVisits: { enabled: true },
178
+ storage: { backend: 'memory' },
179
+ }) as SDKWithPageVisits;
180
+ sdk.use(storagePlugin);
181
+ sdk.use(pageVisitsPlugin);
182
+
183
+ sdk.on('pageVisits:incremented', (event: PageVisitsEvent) => {
184
+ events.push(event);
185
+ });
186
+
187
+ await sdk.init();
188
+
189
+ expect(events.length).toBe(1);
190
+ expect(events[0].isFirstVisit).toBe(true);
191
+ expect(events[0].totalVisits).toBe(1);
192
+ expect(events[0].sessionVisits).toBe(1);
193
+ });
194
+
195
+ it('should not be first visit on subsequent loads', async () => {
196
+ // First visit
197
+ await initPlugin();
198
+
199
+ // Second visit - set up event listener BEFORE init
200
+ sdk.destroy?.();
201
+
202
+ const events: PageVisitsEvent[] = [];
203
+ sdk = new SDK({
204
+ pageVisits: { enabled: true },
205
+ storage: { backend: 'memory' },
206
+ }) as SDKWithPageVisits;
207
+ sdk.use(storagePlugin);
208
+ sdk.use(pageVisitsPlugin);
209
+
210
+ sdk.on('pageVisits:incremented', (event: PageVisitsEvent) => {
211
+ events.push(event);
212
+ });
213
+
214
+ await sdk.init();
215
+
216
+ expect(events.length).toBe(1);
217
+ expect(events[0].isFirstVisit).toBe(false);
218
+ expect(events[0].totalVisits).toBe(2);
219
+ });
220
+ });
221
+
222
+ describe('DNT (Do Not Track)', () => {
223
+ it('should respect DNT when enabled', async () => {
224
+ // Mock DNT
225
+ Object.defineProperty(navigator, 'doNotTrack', {
226
+ value: '1',
227
+ configurable: true,
228
+ });
229
+
230
+ const events: any[] = [];
231
+ sdk = new SDK({
232
+ pageVisits: { enabled: true, respectDNT: true },
233
+ storage: { backend: 'memory' },
234
+ }) as SDKWithPageVisits;
235
+ sdk.use(storagePlugin);
236
+ sdk.use(pageVisitsPlugin);
237
+
238
+ sdk.on('pageVisits:disabled', (event: any) => {
239
+ events.push(event);
240
+ });
241
+
242
+ await sdk.init();
243
+
244
+ expect(events.length).toBe(1);
245
+ expect(events[0].reason).toBe('dnt');
246
+ });
247
+
248
+ it('should not track when DNT is enabled', async () => {
249
+ // Mock DNT
250
+ Object.defineProperty(navigator, 'doNotTrack', {
251
+ value: '1',
252
+ configurable: true,
253
+ });
254
+
255
+ await initPlugin({ enabled: true, respectDNT: true });
256
+
257
+ // No tracking should occur
258
+ expect(sdk.pageVisits.getTotalCount()).toBe(0);
259
+ expect(sdk.pageVisits.getSessionCount()).toBe(0);
260
+ });
261
+
262
+ it('should ignore DNT when respectDNT is false', async () => {
263
+ // Mock DNT
264
+ Object.defineProperty(navigator, 'doNotTrack', {
265
+ value: '1',
266
+ configurable: true,
267
+ });
268
+
269
+ await initPlugin({ enabled: true, respectDNT: false });
270
+
271
+ // Should still track
272
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
273
+ expect(sdk.pageVisits.getSessionCount()).toBe(1);
274
+ });
275
+ });
276
+
277
+ describe('API Methods', () => {
278
+ describe('getTotalCount', () => {
279
+ it('should return total visit count', async () => {
280
+ await initPlugin();
281
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
282
+ });
283
+ });
284
+
285
+ describe('getSessionCount', () => {
286
+ it('should return session visit count', async () => {
287
+ await initPlugin();
288
+ expect(sdk.pageVisits.getSessionCount()).toBe(1);
289
+ });
290
+ });
291
+
292
+ describe('isFirstVisit', () => {
293
+ it('should return false after first increment', async () => {
294
+ await initPlugin();
295
+ expect(sdk.pageVisits.isFirstVisit()).toBe(false);
296
+ });
297
+ });
298
+
299
+ describe('getFirstVisitTime', () => {
300
+ it('should return first visit timestamp', async () => {
301
+ await initPlugin();
302
+ const time = sdk.pageVisits.getFirstVisitTime();
303
+ expect(time).toBeDefined();
304
+ expect(typeof time).toBe('number');
305
+ });
306
+ });
307
+
308
+ describe('getLastVisitTime', () => {
309
+ it('should return last visit timestamp', async () => {
310
+ await initPlugin();
311
+ const time = sdk.pageVisits.getLastVisitTime();
312
+ expect(time).toBeDefined();
313
+ expect(typeof time).toBe('number');
314
+ });
315
+ });
316
+
317
+ describe('increment', () => {
318
+ it('should manually increment counters', async () => {
319
+ await initPlugin({ autoIncrement: false });
320
+ expect(sdk.pageVisits.getTotalCount()).toBe(0);
321
+
322
+ sdk.pageVisits.increment();
323
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
324
+ expect(sdk.pageVisits.getSessionCount()).toBe(1);
325
+ });
326
+
327
+ it('should emit pageVisits:incremented event', async () => {
328
+ await initPlugin({ autoIncrement: false });
329
+
330
+ const events: PageVisitsEvent[] = [];
331
+ sdk.on('pageVisits:incremented', (event: PageVisitsEvent) => {
332
+ events.push(event);
333
+ });
334
+
335
+ sdk.pageVisits.increment();
336
+
337
+ expect(events.length).toBe(1);
338
+ expect(events[0].totalVisits).toBe(1);
339
+ expect(events[0].sessionVisits).toBe(1);
340
+ });
341
+ });
342
+
343
+ describe('reset', () => {
344
+ it('should reset all counters', async () => {
345
+ await initPlugin();
346
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
347
+
348
+ sdk.pageVisits.reset();
349
+
350
+ expect(sdk.pageVisits.getTotalCount()).toBe(0);
351
+ expect(sdk.pageVisits.getSessionCount()).toBe(0);
352
+ expect(sdk.pageVisits.isFirstVisit()).toBe(false);
353
+ expect(sdk.pageVisits.getFirstVisitTime()).toBeUndefined();
354
+ expect(sdk.pageVisits.getLastVisitTime()).toBeUndefined();
355
+ });
356
+
357
+ it('should clear storage', async () => {
358
+ await initPlugin();
359
+ sdk.pageVisits.reset();
360
+
361
+ const sessionData = sessionStorage.getItem('pageVisits:session');
362
+ const totalData = localStorage.getItem('pageVisits:total');
363
+
364
+ expect(sessionData).toBeNull();
365
+ expect(totalData).toBeNull();
366
+ });
367
+
368
+ it('should emit pageVisits:reset event', async () => {
369
+ await initPlugin();
370
+
371
+ const events: any[] = [];
372
+ sdk.on('pageVisits:reset', () => {
373
+ events.push(true);
374
+ });
375
+
376
+ sdk.pageVisits.reset();
377
+
378
+ expect(events.length).toBe(1);
379
+ });
380
+ });
381
+
382
+ describe('getState', () => {
383
+ it('should return full page visits state', async () => {
384
+ await initPlugin();
385
+
386
+ const state = sdk.pageVisits.getState();
387
+
388
+ expect(state).toHaveProperty('isFirstVisit');
389
+ expect(state).toHaveProperty('totalVisits');
390
+ expect(state).toHaveProperty('sessionVisits');
391
+ expect(state).toHaveProperty('firstVisitTime');
392
+ expect(state).toHaveProperty('lastVisitTime');
393
+ expect(state).toHaveProperty('timestamp');
394
+ });
395
+ });
396
+ });
397
+
398
+ describe('Configuration', () => {
399
+ it('should support custom storage keys', async () => {
400
+ await initPlugin({
401
+ sessionKey: 'custom:session',
402
+ totalKey: 'custom:total',
403
+ });
404
+
405
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
406
+
407
+ // Check custom keys in storage
408
+ const sessionData = sessionStorage.getItem('custom:session');
409
+ const totalData = localStorage.getItem('custom:total');
410
+
411
+ expect(sessionData).toBeDefined();
412
+ expect(totalData).toBeDefined();
413
+ });
414
+
415
+ it('should support disabling via config', async () => {
416
+ const events: any[] = [];
417
+ sdk = new SDK({
418
+ pageVisits: { enabled: false },
419
+ storage: { backend: 'memory' },
420
+ }) as SDKWithPageVisits;
421
+ sdk.use(storagePlugin);
422
+ sdk.use(pageVisitsPlugin);
423
+
424
+ sdk.on('pageVisits:disabled', (event: any) => {
425
+ events.push(event);
426
+ });
427
+
428
+ await sdk.init();
429
+
430
+ expect(events.length).toBe(1);
431
+ expect(events[0].reason).toBe('config');
432
+ });
433
+
434
+ it('should support disabling auto-increment', async () => {
435
+ await initPlugin({ autoIncrement: false });
436
+
437
+ expect(sdk.pageVisits.getTotalCount()).toBe(0);
438
+ expect(sdk.pageVisits.getSessionCount()).toBe(0);
439
+ });
440
+ });
441
+
442
+ describe('Event Emission', () => {
443
+ it('should emit pageVisits:incremented with full payload', async () => {
444
+ const events: PageVisitsEvent[] = [];
445
+ sdk = new SDK({
446
+ pageVisits: { enabled: true },
447
+ storage: { backend: 'memory' },
448
+ }) as SDKWithPageVisits;
449
+ sdk.use(storagePlugin);
450
+ sdk.use(pageVisitsPlugin);
451
+
452
+ sdk.on('pageVisits:incremented', (event: PageVisitsEvent) => {
453
+ events.push(event);
454
+ });
455
+
456
+ await sdk.init();
457
+
458
+ expect(events.length).toBe(1);
459
+ expect(events[0]).toMatchObject({
460
+ isFirstVisit: true,
461
+ totalVisits: 1,
462
+ sessionVisits: 1,
463
+ });
464
+ expect(events[0].firstVisitTime).toBeDefined();
465
+ expect(events[0].lastVisitTime).toBeDefined();
466
+ expect(events[0].timestamp).toBeDefined();
467
+ });
468
+ });
469
+
470
+ describe('Integration Scenarios', () => {
471
+ it('should track session-scoped visits correctly', async () => {
472
+ // First page load
473
+ await initPlugin();
474
+ const session1 = sdk.pageVisits.getSessionCount();
475
+ expect(session1).toBe(1);
476
+
477
+ // Second page load (same session)
478
+ sdk.destroy?.();
479
+ await initPlugin();
480
+ const session2 = sdk.pageVisits.getSessionCount();
481
+ expect(session2).toBe(2);
482
+
483
+ // Clear sessionStorage (simulate new session)
484
+ sessionStorage.clear();
485
+ sdk.destroy?.();
486
+ await initPlugin();
487
+ const session3 = sdk.pageVisits.getSessionCount();
488
+ expect(session3).toBe(1); // Reset
489
+ });
490
+
491
+ it('should track lifetime visits across sessions', async () => {
492
+ // First visit
493
+ await initPlugin();
494
+ const total1 = sdk.pageVisits.getTotalCount();
495
+ expect(total1).toBe(1);
496
+
497
+ // Second visit
498
+ sdk.destroy?.();
499
+ await initPlugin();
500
+ const total2 = sdk.pageVisits.getTotalCount();
501
+ expect(total2).toBe(2);
502
+
503
+ // Clear sessionStorage (new session) but keep localStorage
504
+ sessionStorage.clear();
505
+ sdk.destroy?.();
506
+ await initPlugin();
507
+ const total3 = sdk.pageVisits.getTotalCount();
508
+ expect(total3).toBe(3); // Continues incrementing
509
+ });
510
+
511
+ it('should support first-visit detection', async () => {
512
+ const events: PageVisitsEvent[] = [];
513
+ sdk = new SDK({
514
+ pageVisits: { enabled: true },
515
+ storage: { backend: 'memory' },
516
+ }) as SDKWithPageVisits;
517
+ sdk.use(storagePlugin);
518
+ sdk.use(pageVisitsPlugin);
519
+
520
+ sdk.on('pageVisits:incremented', (event: PageVisitsEvent) => {
521
+ events.push(event);
522
+ });
523
+
524
+ await sdk.init();
525
+
526
+ expect(events[0].isFirstVisit).toBe(true);
527
+ });
528
+
529
+ it('should support all comparison operators in targeting', async () => {
530
+ await initPlugin();
531
+
532
+ // Simulate 5 visits
533
+ for (let i = 0; i < 4; i++) {
534
+ sdk.destroy?.();
535
+ await initPlugin();
536
+ }
537
+
538
+ const count = sdk.pageVisits.getTotalCount();
539
+ expect(count).toBe(5);
540
+
541
+ // Support all operators for flexible targeting
542
+ expect(count >= 5).toBe(true);
543
+ expect(count === 5).toBe(true);
544
+ expect(count < 10).toBe(true);
545
+ });
546
+ });
547
+
548
+ describe('Storage Backend Integration', () => {
549
+ it('should auto-load storage plugin if missing', async () => {
550
+ // Don't manually load storagePlugin
551
+ sdk = new SDK({
552
+ pageVisits: { enabled: true },
553
+ }) as SDKWithPageVisits;
554
+ sdk.use(pageVisitsPlugin);
555
+
556
+ await sdk.init();
557
+
558
+ // Should still work (auto-loaded)
559
+ expect(sdk.pageVisits.getTotalCount()).toBe(1);
560
+ });
561
+ });
562
+ });