@prosdevlab/experience-sdk-plugins 0.1.3 → 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,362 @@
1
+ /**
2
+ * Integration Tests - Display Condition Plugins
3
+ *
4
+ * Tests all 4 display condition plugins working together:
5
+ * - Exit Intent
6
+ * - Scroll Depth
7
+ * - Page Visits
8
+ * - Time Delay
9
+ */
10
+
11
+ import { SDK } from '@lytics/sdk-kit';
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
13
+ import { exitIntentPlugin, pageVisitsPlugin, scrollDepthPlugin, timeDelayPlugin } from './index';
14
+
15
+ // Type augmentation for plugin APIs
16
+ type SDKWithPlugins = SDK & {
17
+ exitIntent?: any;
18
+ scrollDepth?: any;
19
+ pageVisits?: any;
20
+ timeDelay?: any;
21
+ };
22
+
23
+ describe('Display Condition Plugins - Integration', () => {
24
+ beforeEach(() => {
25
+ vi.useFakeTimers();
26
+ // Reset document state
27
+ Object.defineProperty(document, 'hidden', {
28
+ writable: true,
29
+ configurable: true,
30
+ value: false,
31
+ });
32
+ // Clear storage
33
+ sessionStorage.clear();
34
+ localStorage.clear();
35
+ });
36
+
37
+ afterEach(() => {
38
+ vi.restoreAllMocks();
39
+ vi.useRealTimers();
40
+ });
41
+
42
+ describe('Plugin Composition', () => {
43
+ it('should load all 4 plugins without conflicts', async () => {
44
+ const sdk = new SDK({
45
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
46
+ scrollDepth: { thresholds: [25, 50, 75], throttle: 100 },
47
+ pageVisits: { enabled: true },
48
+ timeDelay: { delay: 5000, pauseWhenHidden: false },
49
+ }) as SDKWithPlugins;
50
+
51
+ sdk.use(exitIntentPlugin);
52
+ sdk.use(scrollDepthPlugin);
53
+ sdk.use(pageVisitsPlugin);
54
+ sdk.use(timeDelayPlugin);
55
+
56
+ await sdk.init();
57
+
58
+ // All plugins should expose their APIs
59
+ expect(sdk.exitIntent).toBeDefined();
60
+ expect(sdk.scrollDepth).toBeDefined();
61
+ expect(sdk.pageVisits).toBeDefined();
62
+ expect(sdk.timeDelay).toBeDefined();
63
+ });
64
+
65
+ it('should handle multiple triggers firing independently', async () => {
66
+ const events: Array<{ type: string; data: any }> = [];
67
+
68
+ const sdk = new SDK({
69
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
70
+ scrollDepth: { thresholds: [50], throttle: 100 },
71
+ pageVisits: { enabled: true, autoIncrement: true },
72
+ timeDelay: { delay: 2000, pauseWhenHidden: false },
73
+ }) as SDKWithPlugins;
74
+
75
+ sdk.use(exitIntentPlugin);
76
+ sdk.use(scrollDepthPlugin);
77
+ sdk.use(pageVisitsPlugin);
78
+ sdk.use(timeDelayPlugin);
79
+
80
+ // Listen to all trigger events
81
+ sdk.on('trigger:exitIntent', (data) => events.push({ type: 'exitIntent', data }));
82
+ sdk.on('trigger:scrollDepth', (data) => events.push({ type: 'scrollDepth', data }));
83
+ sdk.on('trigger:timeDelay', (data) => events.push({ type: 'timeDelay', data }));
84
+ sdk.on('pageVisits:incremented', (data) => events.push({ type: 'pageVisits', data }));
85
+
86
+ await sdk.init();
87
+
88
+ // Page visits should fire on init
89
+ expect(events.some((e) => e.type === 'pageVisits')).toBe(true);
90
+
91
+ // Time delay should fire after 2s
92
+ vi.advanceTimersByTime(2000);
93
+ expect(events.some((e) => e.type === 'timeDelay')).toBe(true);
94
+
95
+ // All events should be distinct
96
+ const types = new Set(events.map((e) => e.type));
97
+ expect(types.size).toBeGreaterThan(1);
98
+ });
99
+
100
+ it('should update context correctly for all triggers', async () => {
101
+ const contextUpdates: any[] = [];
102
+
103
+ const sdk = new SDK({
104
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
105
+ scrollDepth: { thresholds: [50], throttle: 100 },
106
+ pageVisits: { enabled: true },
107
+ timeDelay: { delay: 1000, pauseWhenHidden: false },
108
+ }) as SDKWithPlugins;
109
+
110
+ sdk.use(exitIntentPlugin);
111
+ sdk.use(scrollDepthPlugin);
112
+ sdk.use(pageVisitsPlugin);
113
+ sdk.use(timeDelayPlugin);
114
+
115
+ // Capture context after each trigger
116
+ sdk.on('trigger:exitIntent', () => {
117
+ contextUpdates.push({ trigger: 'exitIntent', timestamp: Date.now() });
118
+ });
119
+ sdk.on('trigger:timeDelay', () => {
120
+ contextUpdates.push({ trigger: 'timeDelay', timestamp: Date.now() });
121
+ });
122
+
123
+ await sdk.init();
124
+
125
+ vi.advanceTimersByTime(1000);
126
+
127
+ // Should have multiple context updates
128
+ expect(contextUpdates.length).toBeGreaterThan(0);
129
+ });
130
+ });
131
+
132
+ describe('Complex Targeting Logic', () => {
133
+ it('should support AND logic (multiple conditions)', async () => {
134
+ const sdk = new SDK({
135
+ scrollDepth: { thresholds: [50], throttle: 100 },
136
+ timeDelay: { delay: 2000, pauseWhenHidden: false },
137
+ }) as SDKWithPlugins;
138
+
139
+ sdk.use(scrollDepthPlugin);
140
+ sdk.use(timeDelayPlugin);
141
+
142
+ await sdk.init();
143
+
144
+ // Before: neither condition met
145
+ const scrolled50 = (sdk.scrollDepth?.getMaxPercent() || 0) >= 50;
146
+ let delayed2s = sdk.timeDelay?.isTriggered() || false;
147
+ expect(scrolled50 && delayed2s).toBe(false);
148
+
149
+ // Advance time
150
+ vi.advanceTimersByTime(2000);
151
+ delayed2s = sdk.timeDelay?.isTriggered() || false;
152
+
153
+ // Still false (scroll not met)
154
+ expect(scrolled50 && delayed2s).toBe(false);
155
+ });
156
+
157
+ it('should support OR logic (any condition)', async () => {
158
+ const sdk = new SDK({
159
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
160
+ timeDelay: { delay: 2000, pauseWhenHidden: false },
161
+ }) as SDKWithPlugins;
162
+
163
+ sdk.use(exitIntentPlugin);
164
+ sdk.use(timeDelayPlugin);
165
+
166
+ await sdk.init();
167
+
168
+ // Trigger time delay
169
+ vi.advanceTimersByTime(2000);
170
+
171
+ const exitTriggered = sdk.exitIntent?.isTriggered() || false;
172
+ const timeTriggered = sdk.timeDelay?.isTriggered() || false;
173
+
174
+ // OR logic: one is true
175
+ expect(exitTriggered || timeTriggered).toBe(true);
176
+ });
177
+
178
+ it('should support NOT logic (inverse conditions)', async () => {
179
+ const sdk = new SDK({
180
+ pageVisits: { enabled: true, autoIncrement: true },
181
+ }) as SDKWithPlugins;
182
+
183
+ sdk.use(pageVisitsPlugin);
184
+
185
+ await sdk.init();
186
+
187
+ // After init with autoIncrement, it's no longer first visit
188
+ // (count was incremented from 0 to 1)
189
+ const isFirstVisit = sdk.pageVisits?.isFirstVisit() || false;
190
+ const totalCount = sdk.pageVisits?.getTotalCount() || 0;
191
+
192
+ expect(totalCount).toBe(1);
193
+ expect(isFirstVisit).toBe(false); // Auto-incremented, so not "first" anymore
194
+
195
+ // Simulate second visit
196
+ sdk.pageVisits?.increment();
197
+ const nowCount = sdk.pageVisits?.getTotalCount() || 0;
198
+ expect(nowCount).toBe(2);
199
+ });
200
+ });
201
+
202
+ describe('Performance', () => {
203
+ it('should have minimal overhead with all plugins loaded', async () => {
204
+ const startTime = performance.now();
205
+
206
+ const sdk = new SDK({
207
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
208
+ scrollDepth: { thresholds: [25, 50, 75], throttle: 100 },
209
+ pageVisits: { enabled: true },
210
+ timeDelay: { delay: 5000, pauseWhenHidden: false },
211
+ }) as SDKWithPlugins;
212
+
213
+ sdk.use(exitIntentPlugin);
214
+ sdk.use(scrollDepthPlugin);
215
+ sdk.use(pageVisitsPlugin);
216
+ sdk.use(timeDelayPlugin);
217
+
218
+ await sdk.init();
219
+
220
+ const endTime = performance.now();
221
+ const duration = endTime - startTime;
222
+
223
+ // Should initialize in less than 50ms
224
+ expect(duration).toBeLessThan(50);
225
+ });
226
+
227
+ it('should not leak memory with multiple resets', async () => {
228
+ const sdk = new SDK({
229
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
230
+ scrollDepth: { thresholds: [50], throttle: 100 },
231
+ pageVisits: { enabled: true },
232
+ timeDelay: { delay: 1000, pauseWhenHidden: false },
233
+ }) as SDKWithPlugins;
234
+
235
+ sdk.use(exitIntentPlugin);
236
+ sdk.use(scrollDepthPlugin);
237
+ sdk.use(pageVisitsPlugin);
238
+ sdk.use(timeDelayPlugin);
239
+
240
+ await sdk.init();
241
+
242
+ // Reset all plugins multiple times
243
+ for (let i = 0; i < 100; i++) {
244
+ sdk.exitIntent?.reset();
245
+ sdk.scrollDepth?.reset();
246
+ sdk.pageVisits?.reset();
247
+ sdk.timeDelay?.reset();
248
+ }
249
+
250
+ // Should not throw or hang
251
+ expect(sdk.exitIntent?.isTriggered()).toBe(false);
252
+ expect(sdk.scrollDepth?.getMaxPercent()).toBe(0);
253
+ expect(sdk.timeDelay?.isTriggered()).toBe(false);
254
+ });
255
+ });
256
+
257
+ describe('Cleanup', () => {
258
+ it('should cleanup all plugins on destroy', async () => {
259
+ const sdk = new SDK({
260
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
261
+ scrollDepth: { thresholds: [50], throttle: 100 },
262
+ pageVisits: { enabled: true },
263
+ timeDelay: { delay: 5000, pauseWhenHidden: false },
264
+ }) as SDKWithPlugins;
265
+
266
+ sdk.use(exitIntentPlugin);
267
+ sdk.use(scrollDepthPlugin);
268
+ sdk.use(pageVisitsPlugin);
269
+ sdk.use(timeDelayPlugin);
270
+
271
+ await sdk.init();
272
+
273
+ // Destroy SDK
274
+ sdk.emit('destroy');
275
+
276
+ // Advance time past all delays
277
+ vi.advanceTimersByTime(10000);
278
+
279
+ // Plugins should be cleaned up (no crashes)
280
+ expect(() => {
281
+ vi.advanceTimersByTime(1000);
282
+ }).not.toThrow();
283
+ });
284
+ });
285
+
286
+ describe('Real-World Scenarios', () => {
287
+ it('should handle "engaged user" scenario (scroll + time)', async () => {
288
+ const sdk = new SDK({
289
+ scrollDepth: { thresholds: [50], throttle: 100 },
290
+ timeDelay: { delay: 5000, pauseWhenHidden: false },
291
+ }) as SDKWithPlugins;
292
+
293
+ sdk.use(scrollDepthPlugin);
294
+ sdk.use(timeDelayPlugin);
295
+
296
+ await sdk.init();
297
+
298
+ // User spends 5 seconds (time condition met)
299
+ vi.advanceTimersByTime(5000);
300
+
301
+ const timeElapsed = sdk.timeDelay?.isTriggered() || false;
302
+ const scrolled50 = (sdk.scrollDepth?.getMaxPercent() || 0) >= 50;
303
+
304
+ // Could show "engaged user" offer even without scroll
305
+ expect(timeElapsed).toBe(true);
306
+ expect(timeElapsed || scrolled50).toBe(true); // OR logic
307
+ });
308
+
309
+ it('should handle "returning visitor exit intent" scenario', async () => {
310
+ const sdk = new SDK({
311
+ exitIntent: { sensitivity: 50, minTimeOnPage: 0, disableOnMobile: false },
312
+ pageVisits: { enabled: true, autoIncrement: true },
313
+ }) as SDKWithPlugins;
314
+
315
+ sdk.use(exitIntentPlugin);
316
+ sdk.use(pageVisitsPlugin);
317
+
318
+ await sdk.init();
319
+
320
+ // Simulate more visits
321
+ sdk.pageVisits?.increment();
322
+ sdk.pageVisits?.increment();
323
+
324
+ const isFirstVisit = sdk.pageVisits?.isFirstVisit() || false;
325
+ const totalVisits = sdk.pageVisits?.getTotalCount() || 0;
326
+
327
+ // After multiple increments
328
+ expect(isFirstVisit).toBe(false);
329
+ expect(totalVisits).toBeGreaterThan(1);
330
+
331
+ // Logic for returning visitor targeting
332
+ const isReturningVisitor = !isFirstVisit && totalVisits > 2;
333
+ expect(isReturningVisitor).toBe(true);
334
+ });
335
+
336
+ it('should handle "first-time visitor welcome" scenario', async () => {
337
+ const sdk = new SDK({
338
+ pageVisits: { enabled: true, autoIncrement: true },
339
+ timeDelay: { delay: 3000, pauseWhenHidden: false },
340
+ }) as SDKWithPlugins;
341
+
342
+ sdk.use(pageVisitsPlugin);
343
+ sdk.use(timeDelayPlugin);
344
+
345
+ await sdk.init();
346
+
347
+ const totalCount = sdk.pageVisits?.getTotalCount() || 0;
348
+
349
+ // Should have at least 1 visit after auto-increment
350
+ expect(totalCount).toBeGreaterThanOrEqual(1);
351
+
352
+ // Wait 3 seconds
353
+ vi.advanceTimersByTime(3000);
354
+ const timeElapsed = sdk.timeDelay?.isTriggered() || false;
355
+ expect(timeElapsed).toBe(true);
356
+
357
+ // Logic: Show welcome after delay for low-count visitors
358
+ const shouldShowWelcome = totalCount <= 1 && timeElapsed;
359
+ expect(shouldShowWelcome).toBe(true);
360
+ });
361
+ });
362
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Page Visits Plugin - Barrel Export
3
+ */
4
+
5
+ export { pageVisitsPlugin } from './page-visits';
6
+ export type { PageVisitsEvent, PageVisitsPlugin, PageVisitsPluginConfig } from './types';