@prosdevlab/experience-sdk-plugins 0.2.0 → 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.
@@ -1,362 +1,421 @@
1
1
  /**
2
- * Integration Tests - Display Condition Plugins
2
+ * Integration Tests
3
3
  *
4
- * Tests all 4 display condition plugins working together:
5
- * - Exit Intent
6
- * - Scroll Depth
7
- * - Page Visits
8
- * - Time Delay
4
+ * Tests the interaction between plugins to ensure they work together correctly.
5
+ *
6
+ * @vitest-environment happy-dom
9
7
  */
10
-
11
8
  import { SDK } from '@lytics/sdk-kit';
12
9
  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
- });
10
+ import { inlinePlugin } from './inline';
11
+ import { modalPlugin } from './modal';
64
12
 
65
- it('should handle multiple triggers firing independently', async () => {
66
- const events: Array<{ type: string; data: any }> = [];
13
+ function initSDK() {
14
+ const sdk = new SDK({ name: 'integration-test' });
15
+ sdk.use(modalPlugin);
16
+ sdk.use(inlinePlugin);
67
17
 
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;
18
+ if (!document.body) {
19
+ document.body = document.createElement('body');
20
+ }
74
21
 
75
- sdk.use(exitIntentPlugin);
76
- sdk.use(scrollDepthPlugin);
77
- sdk.use(pageVisitsPlugin);
78
- sdk.use(timeDelayPlugin);
22
+ return sdk;
23
+ }
79
24
 
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 }));
25
+ describe('Plugin Integration Tests', () => {
26
+ let sdk: SDK & { modal?: any; inline?: any };
85
27
 
86
- await sdk.init();
28
+ beforeEach(async () => {
29
+ sdk = initSDK();
30
+ await sdk.init();
31
+ });
87
32
 
88
- // Page visits should fire on init
89
- expect(events.some((e) => e.type === 'pageVisits')).toBe(true);
33
+ afterEach(async () => {
34
+ for (const el of document.querySelectorAll('.xp-modal, .xp-inline')) {
35
+ el.remove();
36
+ }
37
+ document.body.innerHTML = '';
38
+ if (sdk) {
39
+ await sdk.destroy();
40
+ }
41
+ });
90
42
 
91
- // Time delay should fire after 2s
92
- vi.advanceTimersByTime(2000);
93
- expect(events.some((e) => e.type === 'timeDelay')).toBe(true);
43
+ describe('Modal + Inline Interaction', () => {
44
+ it('should show modal and inline simultaneously', async () => {
45
+ const shownHandler = vi.fn();
46
+ sdk.on('experiences:shown', shownHandler);
47
+
48
+ const target = document.createElement('div');
49
+ target.id = 'content';
50
+ document.body.appendChild(target);
51
+
52
+ const modalExp = {
53
+ id: 'popup',
54
+ type: 'modal',
55
+ content: {
56
+ title: 'Special Offer',
57
+ message: 'Limited time only!',
58
+ buttons: [{ text: 'Learn More', variant: 'primary' }],
59
+ },
60
+ };
61
+
62
+ const inlineExp = {
63
+ id: 'inline-banner',
64
+ type: 'inline',
65
+ content: {
66
+ selector: '#content',
67
+ message: '<p>Related: Check out our guide.</p>',
68
+ },
69
+ };
70
+
71
+ sdk.modal.show(modalExp);
72
+ sdk.inline.show(inlineExp);
73
+
74
+ await vi.waitFor(() => {
75
+ expect(shownHandler).toHaveBeenCalledTimes(2);
76
+ });
94
77
 
95
- // All events should be distinct
96
- const types = new Set(events.map((e) => e.type));
97
- expect(types.size).toBeGreaterThan(1);
78
+ expect(document.querySelector('.xp-modal')).toBeTruthy();
79
+ expect(document.querySelector('.xp-inline')).toBeTruthy();
98
80
  });
99
81
 
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() });
82
+ it('should dismiss modal without affecting inline', async () => {
83
+ const dismissedHandler = vi.fn();
84
+ sdk.on('experiences:dismissed', dismissedHandler);
85
+
86
+ const target = document.createElement('div');
87
+ target.id = 'content';
88
+ document.body.appendChild(target);
89
+
90
+ const modalExp = {
91
+ id: 'dismissable-modal',
92
+ type: 'modal',
93
+ content: {
94
+ title: 'Notification',
95
+ message: 'This is a modal.',
96
+ dismissable: true,
97
+ },
98
+ };
99
+
100
+ const inlineExp = {
101
+ id: 'persistent-inline',
102
+ type: 'inline',
103
+ content: {
104
+ selector: '#content',
105
+ message: '<p>This stays.</p>',
106
+ },
107
+ };
108
+
109
+ sdk.modal.show(modalExp);
110
+ sdk.inline.show(inlineExp);
111
+
112
+ await vi.waitFor(() => {
113
+ expect(document.querySelector('.xp-modal')).toBeTruthy();
114
+ expect(document.querySelector('.xp-inline')).toBeTruthy();
121
115
  });
122
116
 
123
- await sdk.init();
117
+ // Dismiss modal
118
+ const closeBtn = document.querySelector('.xp-modal__close') as HTMLElement;
119
+ closeBtn.click();
124
120
 
125
- vi.advanceTimersByTime(1000);
121
+ await vi.waitFor(() => {
122
+ expect(dismissedHandler).toHaveBeenCalledWith(
123
+ expect.objectContaining({
124
+ experienceId: 'dismissable-modal',
125
+ })
126
+ );
127
+ });
126
128
 
127
- // Should have multiple context updates
128
- expect(contextUpdates.length).toBeGreaterThan(0);
129
+ // Modal gone, inline remains
130
+ expect(document.querySelector('.xp-modal')).toBeFalsy();
131
+ expect(document.querySelector('.xp-inline')).toBeTruthy();
129
132
  });
130
- });
131
133
 
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;
134
+ it('should dismiss inline without affecting modal', async () => {
135
+ const target = document.createElement('div');
136
+ target.id = 'content';
137
+ document.body.appendChild(target);
138
+
139
+ const modalExp = {
140
+ id: 'persistent-modal',
141
+ type: 'modal',
142
+ content: {
143
+ title: 'Stay Open',
144
+ message: 'This modal stays.',
145
+ },
146
+ };
147
+
148
+ const inlineExp = {
149
+ id: 'dismissable-inline',
150
+ type: 'inline',
151
+ content: {
152
+ selector: '#content',
153
+ message: '<p>Can dismiss</p>',
154
+ dismissable: true,
155
+ },
156
+ };
157
+
158
+ sdk.modal.show(modalExp);
159
+ sdk.inline.show(inlineExp);
160
+
161
+ await vi.waitFor(() => {
162
+ expect(document.querySelector('.xp-modal')).toBeTruthy();
163
+ expect(document.querySelector('.xp-inline')).toBeTruthy();
164
+ });
138
165
 
139
- sdk.use(scrollDepthPlugin);
140
- sdk.use(timeDelayPlugin);
166
+ // Dismiss inline
167
+ const closeBtn = document.querySelector('.xp-inline__close') as HTMLElement;
168
+ closeBtn.click();
141
169
 
142
- await sdk.init();
170
+ await vi.waitFor(() => {
171
+ expect(document.querySelector('.xp-inline')).toBeFalsy();
172
+ });
143
173
 
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);
174
+ // Inline gone, modal remains
175
+ expect(document.querySelector('.xp-modal')).toBeTruthy();
176
+ });
177
+ });
148
178
 
149
- // Advance time
150
- vi.advanceTimersByTime(2000);
151
- delayed2s = sdk.timeDelay?.isTriggered() || false;
179
+ describe('Modal Forms', () => {
180
+ it('should render and submit form in modal', async () => {
181
+ const formSubmitHandler = vi.fn();
182
+ sdk.on('experiences:modal:form:submit', formSubmitHandler);
183
+
184
+ const experience = {
185
+ id: 'newsletter',
186
+ type: 'modal',
187
+ content: {
188
+ title: 'Subscribe',
189
+ message: 'Get updates.',
190
+ size: 'sm',
191
+ form: {
192
+ fields: [
193
+ { name: 'email', type: 'email', required: true, placeholder: 'you@example.com' },
194
+ ],
195
+ submitButton: { text: 'Subscribe', variant: 'primary' },
196
+ },
197
+ },
198
+ };
199
+
200
+ sdk.modal.show(experience);
201
+
202
+ await vi.waitFor(() => {
203
+ expect(document.querySelector('.xp-modal__form')).toBeTruthy();
204
+ });
152
205
 
153
- // Still false (scroll not met)
154
- expect(scrolled50 && delayed2s).toBe(false);
206
+ // Fill and submit form
207
+ const emailInput = document.querySelector('input[name="email"]') as HTMLInputElement;
208
+ emailInput.value = 'test@example.com';
209
+ emailInput.dispatchEvent(new Event('input', { bubbles: true }));
210
+
211
+ const form = document.querySelector('.xp-modal__form') as HTMLFormElement;
212
+ form.dispatchEvent(new Event('submit', { bubbles: true }));
213
+
214
+ await vi.waitFor(() => {
215
+ expect(formSubmitHandler).toHaveBeenCalledWith(
216
+ expect.objectContaining({
217
+ experienceId: 'newsletter',
218
+ formData: { email: 'test@example.com' },
219
+ })
220
+ );
221
+ });
155
222
  });
156
223
 
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;
224
+ it('should validate form fields', async () => {
225
+ const validationHandler = vi.fn();
226
+ sdk.on('experiences:modal:form:validation', validationHandler);
227
+
228
+ const experience = {
229
+ id: 'form-validation',
230
+ type: 'modal',
231
+ content: {
232
+ form: {
233
+ fields: [{ name: 'email', type: 'email', required: true }],
234
+ submitButton: { text: 'Submit', variant: 'primary' },
235
+ },
236
+ },
237
+ };
238
+
239
+ sdk.modal.show(experience);
240
+
241
+ await vi.waitFor(() => {
242
+ expect(document.querySelector('.xp-modal__form')).toBeTruthy();
243
+ });
162
244
 
163
- sdk.use(exitIntentPlugin);
164
- sdk.use(timeDelayPlugin);
245
+ // Submit empty form (should fail validation)
246
+ const form = document.querySelector('.xp-modal__form') as HTMLFormElement;
247
+ form.dispatchEvent(new Event('submit', { bubbles: true }));
248
+
249
+ await vi.waitFor(() => {
250
+ expect(validationHandler).toHaveBeenCalledWith(
251
+ expect.objectContaining({
252
+ valid: false,
253
+ errors: expect.any(Object),
254
+ })
255
+ );
256
+ });
257
+ });
258
+ });
165
259
 
166
- await sdk.init();
260
+ describe('Multiple Instances', () => {
261
+ it('should handle multiple inline experiences in different locations', async () => {
262
+ const target1 = document.createElement('div');
263
+ target1.id = 'sidebar';
264
+ document.body.appendChild(target1);
265
+
266
+ const target2 = document.createElement('div');
267
+ target2.id = 'footer';
268
+ document.body.appendChild(target2);
269
+
270
+ sdk.inline.show({
271
+ id: 'sidebar-promo',
272
+ type: 'inline',
273
+ content: {
274
+ selector: '#sidebar',
275
+ message: '<p>Sidebar content</p>',
276
+ },
277
+ });
167
278
 
168
- // Trigger time delay
169
- vi.advanceTimersByTime(2000);
279
+ sdk.inline.show({
280
+ id: 'footer-cta',
281
+ type: 'inline',
282
+ content: {
283
+ selector: '#footer',
284
+ message: '<p>Footer content</p>',
285
+ },
286
+ });
170
287
 
171
- const exitTriggered = sdk.exitIntent?.isTriggered() || false;
172
- const timeTriggered = sdk.timeDelay?.isTriggered() || false;
288
+ await vi.waitFor(() => {
289
+ expect(document.querySelectorAll('.xp-inline').length).toBe(2);
290
+ });
173
291
 
174
- // OR logic: one is true
175
- expect(exitTriggered || timeTriggered).toBe(true);
292
+ expect(target1.querySelector('.xp-inline')).toBeTruthy();
293
+ expect(target2.querySelector('.xp-inline')).toBeTruthy();
176
294
  });
177
295
 
178
- it('should support NOT logic (inverse conditions)', async () => {
179
- const sdk = new SDK({
180
- pageVisits: { enabled: true, autoIncrement: true },
181
- }) as SDKWithPlugins;
296
+ it('should replace existing modal when showing a new one', async () => {
297
+ const dismissedHandler = vi.fn();
298
+ sdk.on('experiences:dismissed', dismissedHandler);
182
299
 
183
- sdk.use(pageVisitsPlugin);
300
+ // Show first modal
301
+ sdk.modal.show({
302
+ id: 'modal1',
303
+ type: 'modal',
304
+ content: { title: 'First', message: 'Modal 1' },
305
+ });
184
306
 
185
- await sdk.init();
307
+ await vi.waitFor(() => {
308
+ expect(sdk.modal.isShowing('modal1')).toBe(true);
309
+ });
186
310
 
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;
311
+ // Show second modal (should replace first)
312
+ sdk.modal.show({
313
+ id: 'modal2',
314
+ type: 'modal',
315
+ content: { title: 'Second', message: 'Modal 2' },
316
+ });
191
317
 
192
- expect(totalCount).toBe(1);
193
- expect(isFirstVisit).toBe(false); // Auto-incremented, so not "first" anymore
318
+ await vi.waitFor(() => {
319
+ expect(sdk.modal.isShowing('modal2')).toBe(true);
320
+ });
194
321
 
195
- // Simulate second visit
196
- sdk.pageVisits?.increment();
197
- const nowCount = sdk.pageVisits?.getTotalCount() || 0;
198
- expect(nowCount).toBe(2);
322
+ // Only second modal should be showing
323
+ expect(sdk.modal.isShowing('modal1')).toBe(false);
324
+ expect(sdk.modal.isShowing('modal2')).toBe(true);
325
+ expect(document.querySelectorAll('.xp-modal').length).toBe(1);
199
326
  });
200
- });
201
-
202
- describe('Performance', () => {
203
- it('should have minimal overhead with all plugins loaded', async () => {
204
- const startTime = performance.now();
205
327
 
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;
328
+ it('should prevent showing the same modal twice', async () => {
329
+ const shownHandler = vi.fn();
330
+ sdk.on('experiences:shown', shownHandler);
212
331
 
213
- sdk.use(exitIntentPlugin);
214
- sdk.use(scrollDepthPlugin);
215
- sdk.use(pageVisitsPlugin);
216
- sdk.use(timeDelayPlugin);
332
+ const experience = {
333
+ id: 'duplicate-test',
334
+ type: 'modal',
335
+ content: { title: 'Test', message: 'Cannot show twice' },
336
+ };
217
337
 
218
- await sdk.init();
338
+ sdk.modal.show(experience);
339
+ sdk.modal.show(experience); // Try to show again
219
340
 
220
- const endTime = performance.now();
221
- const duration = endTime - startTime;
222
-
223
- // Should initialize in less than 50ms
224
- expect(duration).toBeLessThan(50);
225
- });
341
+ await vi.waitFor(() => {
342
+ expect(shownHandler).toHaveBeenCalledTimes(1);
343
+ });
226
344
 
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);
345
+ // Only one modal in DOM
346
+ expect(document.querySelectorAll('[data-xp-id="duplicate-test"]').length).toBe(1);
254
347
  });
255
348
  });
256
349
 
257
350
  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();
351
+ it('should clean up all experiences on destroy', async () => {
352
+ const target = document.createElement('div');
353
+ target.id = 'content';
354
+ document.body.appendChild(target);
355
+
356
+ sdk.modal.show({
357
+ id: 'modal',
358
+ type: 'modal',
359
+ content: { title: 'Modal', message: 'Content' },
360
+ });
319
361
 
320
- // Simulate more visits
321
- sdk.pageVisits?.increment();
322
- sdk.pageVisits?.increment();
362
+ sdk.inline.show({
363
+ id: 'inline',
364
+ type: 'inline',
365
+ content: { selector: '#content', message: '<p>Inline</p>' },
366
+ });
323
367
 
324
- const isFirstVisit = sdk.pageVisits?.isFirstVisit() || false;
325
- const totalVisits = sdk.pageVisits?.getTotalCount() || 0;
368
+ await vi.waitFor(() => {
369
+ expect(document.querySelector('.xp-modal')).toBeTruthy();
370
+ expect(document.querySelector('.xp-inline')).toBeTruthy();
371
+ });
326
372
 
327
- // After multiple increments
328
- expect(isFirstVisit).toBe(false);
329
- expect(totalVisits).toBeGreaterThan(1);
373
+ await sdk.destroy();
330
374
 
331
- // Logic for returning visitor targeting
332
- const isReturningVisitor = !isFirstVisit && totalVisits > 2;
333
- expect(isReturningVisitor).toBe(true);
375
+ expect(document.querySelector('.xp-modal')).toBeFalsy();
376
+ expect(document.querySelector('.xp-inline')).toBeFalsy();
334
377
  });
378
+ });
335
379
 
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;
380
+ describe('Event Flow', () => {
381
+ it('should emit events in correct order for modal', async () => {
382
+ const events: string[] = [];
383
+
384
+ sdk.on('experiences:shown', () => events.push('shown'));
385
+ sdk.on('experiences:action', () => events.push('action'));
386
+ sdk.on('experiences:dismissed', () => events.push('dismissed'));
387
+
388
+ sdk.modal.show({
389
+ id: 'event-test',
390
+ type: 'modal',
391
+ content: {
392
+ title: 'Test',
393
+ message: 'Testing events',
394
+ buttons: [{ text: 'Click Me', variant: 'primary', action: 'test' }],
395
+ dismissable: true,
396
+ },
397
+ });
341
398
 
342
- sdk.use(pageVisitsPlugin);
343
- sdk.use(timeDelayPlugin);
399
+ await vi.waitFor(() => {
400
+ expect(document.querySelector('.xp-modal')).toBeTruthy();
401
+ });
344
402
 
345
- await sdk.init();
403
+ // Click button
404
+ const button = document.querySelector('.xp-modal__button') as HTMLElement;
405
+ button.click();
346
406
 
347
- const totalCount = sdk.pageVisits?.getTotalCount() || 0;
407
+ await vi.waitFor(() => {
408
+ expect(events).toContain('action');
409
+ });
348
410
 
349
- // Should have at least 1 visit after auto-increment
350
- expect(totalCount).toBeGreaterThanOrEqual(1);
411
+ // Dismiss modal
412
+ sdk.modal.remove('event-test');
351
413
 
352
- // Wait 3 seconds
353
- vi.advanceTimersByTime(3000);
354
- const timeElapsed = sdk.timeDelay?.isTriggered() || false;
355
- expect(timeElapsed).toBe(true);
414
+ await vi.waitFor(() => {
415
+ expect(events).toContain('dismissed');
416
+ });
356
417
 
357
- // Logic: Show welcome after delay for low-count visitors
358
- const shouldShowWelcome = totalCount <= 1 && timeElapsed;
359
- expect(shouldShowWelcome).toBe(true);
418
+ expect(events).toEqual(['shown', 'action', 'dismissed']);
360
419
  });
361
420
  });
362
421
  });