@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.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +56 -0
- package/dist/index.d.ts +626 -2
- package/dist/index.js +799 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +149 -51
- package/src/exit-intent/exit-intent.test.ts +423 -0
- package/src/exit-intent/exit-intent.ts +372 -0
- package/src/exit-intent/index.ts +6 -0
- package/src/exit-intent/types.ts +59 -0
- package/src/index.ts +5 -0
- package/src/integration.test.ts +362 -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 +545 -0
- package/src/scroll-depth/scroll-depth.ts +400 -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 +297 -0
- package/src/time-delay/types.ts +89 -0
- package/src/utils/sanitize.ts +1 -1
|
@@ -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
|
+
});
|