@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,114 @@
|
|
|
1
|
+
import type { PluginFunction } from '@lytics/sdk-kit';
|
|
2
|
+
import type { ExperienceButton } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface ModalConfig {
|
|
5
|
+
modal?: {
|
|
6
|
+
/** Allow dismissal via close button (default: true) */
|
|
7
|
+
dismissable?: boolean;
|
|
8
|
+
/** Allow dismissal via backdrop click (default: true) */
|
|
9
|
+
backdropDismiss?: boolean;
|
|
10
|
+
/** Z-index for modal (default: 10001) */
|
|
11
|
+
zIndex?: number;
|
|
12
|
+
/** Modal size (default: 'md') */
|
|
13
|
+
size?: 'sm' | 'md' | 'lg' | 'fullscreen' | 'auto';
|
|
14
|
+
/** Auto-fullscreen on mobile screens <640px (default: true for 'lg', false for others) */
|
|
15
|
+
mobileFullscreen?: boolean;
|
|
16
|
+
/** Modal position (default: 'center') */
|
|
17
|
+
position?: 'center' | 'bottom';
|
|
18
|
+
/** Animation type (default: 'fade') */
|
|
19
|
+
animation?: 'fade' | 'slide-up' | 'none';
|
|
20
|
+
/** Animation duration in ms (default: 200) */
|
|
21
|
+
animationDuration?: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ModalContent {
|
|
26
|
+
/** Optional hero image at top of modal */
|
|
27
|
+
image?: {
|
|
28
|
+
/** Image source URL */
|
|
29
|
+
src: string;
|
|
30
|
+
/** Alt text for accessibility */
|
|
31
|
+
alt: string;
|
|
32
|
+
/** Max height in pixels (default: 300, 200 on mobile) */
|
|
33
|
+
maxHeight?: number;
|
|
34
|
+
};
|
|
35
|
+
/** Modal title */
|
|
36
|
+
title?: string;
|
|
37
|
+
/** Modal message (supports HTML via sanitizer) */
|
|
38
|
+
message: string;
|
|
39
|
+
/** Array of action buttons */
|
|
40
|
+
buttons?: ExperienceButton[];
|
|
41
|
+
/** Optional form configuration */
|
|
42
|
+
form?: FormConfig;
|
|
43
|
+
/** Custom CSS class */
|
|
44
|
+
className?: string;
|
|
45
|
+
/** Inline styles */
|
|
46
|
+
style?: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FormConfig {
|
|
50
|
+
/** Array of form fields */
|
|
51
|
+
fields: FormField[];
|
|
52
|
+
/** Submit button configuration */
|
|
53
|
+
submitButton: ExperienceButton;
|
|
54
|
+
/** Success state after submission */
|
|
55
|
+
successState?: FormState;
|
|
56
|
+
/** Error state on submission failure */
|
|
57
|
+
errorState?: FormState;
|
|
58
|
+
/** Custom validation function (optional) */
|
|
59
|
+
validate?: (data: Record<string, string>) => ValidationResult;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface FormField {
|
|
63
|
+
/** Field name (used in form data) */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Field type */
|
|
66
|
+
type: 'email' | 'text' | 'textarea' | 'tel' | 'url' | 'number';
|
|
67
|
+
/** Label text (optional) */
|
|
68
|
+
label?: string;
|
|
69
|
+
/** Placeholder text */
|
|
70
|
+
placeholder?: string;
|
|
71
|
+
/** Required field (default: false) */
|
|
72
|
+
required?: boolean;
|
|
73
|
+
/** Custom validation pattern (regex) */
|
|
74
|
+
pattern?: string;
|
|
75
|
+
/** Error message for validation failure */
|
|
76
|
+
errorMessage?: string;
|
|
77
|
+
/** Custom CSS class */
|
|
78
|
+
className?: string;
|
|
79
|
+
/** Inline styles */
|
|
80
|
+
style?: Record<string, string>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface FormState {
|
|
84
|
+
/** Title to show in success/error state */
|
|
85
|
+
title?: string;
|
|
86
|
+
/** Message to show */
|
|
87
|
+
message: string;
|
|
88
|
+
/** Optional buttons (e.g., "Close", "Try Again") */
|
|
89
|
+
buttons?: ExperienceButton[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface ValidationResult {
|
|
93
|
+
/** Whether validation passed */
|
|
94
|
+
valid: boolean;
|
|
95
|
+
/** Validation errors by field name */
|
|
96
|
+
errors?: Record<string, string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ModalPlugin {
|
|
100
|
+
/** Show a modal experience */
|
|
101
|
+
show(experience: any): void;
|
|
102
|
+
/** Remove a specific modal */
|
|
103
|
+
remove(experienceId: string): void;
|
|
104
|
+
/** Check if a modal is showing */
|
|
105
|
+
isShowing(experienceId?: string): boolean;
|
|
106
|
+
/** Show form success or error state */
|
|
107
|
+
showFormState(experienceId: string, state: 'success' | 'error'): void;
|
|
108
|
+
/** Reset form to initial state */
|
|
109
|
+
resetForm(experienceId: string): void;
|
|
110
|
+
/** Get current form data */
|
|
111
|
+
getFormData(experienceId: string): Record<string, string> | null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type { PluginFunction };
|
|
@@ -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
|
+
});
|