@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,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { SDK } from '@lytics/sdk-kit';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { inlinePlugin } from './inline';
|
|
7
|
+
|
|
8
|
+
// Helper to initialize SDK with inline plugin
|
|
9
|
+
function initPlugin(config = {}) {
|
|
10
|
+
const sdk = new SDK({
|
|
11
|
+
name: 'test-sdk',
|
|
12
|
+
...config,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
sdk.use(inlinePlugin);
|
|
16
|
+
|
|
17
|
+
// Ensure body exists
|
|
18
|
+
if (!document.body) {
|
|
19
|
+
document.body = document.createElement('body');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return sdk;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Inline Plugin', () => {
|
|
26
|
+
let sdk: SDK & { inline?: any };
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
vi.useFakeTimers();
|
|
30
|
+
sdk = initPlugin();
|
|
31
|
+
await sdk.init();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
// Clean up any inline content
|
|
36
|
+
document.querySelectorAll('.xp-inline').forEach((el) => {
|
|
37
|
+
el.remove();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Clean up any target elements
|
|
41
|
+
document.body.innerHTML = '';
|
|
42
|
+
|
|
43
|
+
if (sdk) {
|
|
44
|
+
await sdk.destroy();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
vi.useRealTimers();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should register the inline plugin', () => {
|
|
52
|
+
expect(sdk.inline).toBeDefined();
|
|
53
|
+
expect(sdk.inline.show).toBeInstanceOf(Function);
|
|
54
|
+
expect(sdk.inline.remove).toBeInstanceOf(Function);
|
|
55
|
+
expect(sdk.inline.isShowing).toBeInstanceOf(Function);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should set default configuration', () => {
|
|
59
|
+
// Check that defaults were set by verifying plugin registered
|
|
60
|
+
expect(sdk.inline).toBeDefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('Insertion Methods', () => {
|
|
64
|
+
it('should insert content with "replace" position', () => {
|
|
65
|
+
// Create target element
|
|
66
|
+
const target = document.createElement('div');
|
|
67
|
+
target.id = 'test-target';
|
|
68
|
+
target.innerHTML = '<p>Original content</p>';
|
|
69
|
+
document.body.appendChild(target);
|
|
70
|
+
|
|
71
|
+
const experience = {
|
|
72
|
+
id: 'replace-test',
|
|
73
|
+
type: 'inline',
|
|
74
|
+
content: {
|
|
75
|
+
selector: '#test-target',
|
|
76
|
+
position: 'replace',
|
|
77
|
+
message: '<p>New content</p>',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
sdk.inline.show(experience);
|
|
82
|
+
|
|
83
|
+
const inline = document.querySelector('.xp-inline');
|
|
84
|
+
expect(inline).toBeTruthy();
|
|
85
|
+
expect(inline?.innerHTML).toBe('<p>New content</p>');
|
|
86
|
+
expect(target.querySelector('p')?.textContent).toBe('New content'); // Content replaced
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should insert content with "append" position', () => {
|
|
90
|
+
const target = document.createElement('div');
|
|
91
|
+
target.id = 'test-target';
|
|
92
|
+
target.innerHTML = '<p>Original</p>';
|
|
93
|
+
document.body.appendChild(target);
|
|
94
|
+
|
|
95
|
+
const experience = {
|
|
96
|
+
id: 'append-test',
|
|
97
|
+
type: 'inline',
|
|
98
|
+
content: {
|
|
99
|
+
selector: '#test-target',
|
|
100
|
+
position: 'append',
|
|
101
|
+
message: '<span>Appended</span>',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
sdk.inline.show(experience);
|
|
106
|
+
|
|
107
|
+
const inline = document.querySelector('.xp-inline');
|
|
108
|
+
expect(inline).toBeTruthy();
|
|
109
|
+
expect(target.children.length).toBe(2); // Original + appended
|
|
110
|
+
expect(target.children[0].tagName).toBe('P'); // Original first
|
|
111
|
+
expect(target.children[1].className).toBe('xp-inline'); // Appended second
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should insert content with "prepend" position', () => {
|
|
115
|
+
const target = document.createElement('div');
|
|
116
|
+
target.id = 'test-target';
|
|
117
|
+
target.innerHTML = '<p>Original</p>';
|
|
118
|
+
document.body.appendChild(target);
|
|
119
|
+
|
|
120
|
+
const experience = {
|
|
121
|
+
id: 'prepend-test',
|
|
122
|
+
type: 'inline',
|
|
123
|
+
content: {
|
|
124
|
+
selector: '#test-target',
|
|
125
|
+
position: 'prepend',
|
|
126
|
+
message: '<span>Prepended</span>',
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
sdk.inline.show(experience);
|
|
131
|
+
|
|
132
|
+
const inline = document.querySelector('.xp-inline');
|
|
133
|
+
expect(inline).toBeTruthy();
|
|
134
|
+
expect(target.children.length).toBe(2); // Prepended + original
|
|
135
|
+
expect(target.children[0].className).toBe('xp-inline'); // Prepended first
|
|
136
|
+
expect(target.children[1].tagName).toBe('P'); // Original second
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should insert content with "before" position', () => {
|
|
140
|
+
const container = document.createElement('div');
|
|
141
|
+
const target = document.createElement('p');
|
|
142
|
+
target.id = 'test-target';
|
|
143
|
+
target.textContent = 'Target';
|
|
144
|
+
container.appendChild(target);
|
|
145
|
+
document.body.appendChild(container);
|
|
146
|
+
|
|
147
|
+
const experience = {
|
|
148
|
+
id: 'before-test',
|
|
149
|
+
type: 'inline',
|
|
150
|
+
content: {
|
|
151
|
+
selector: '#test-target',
|
|
152
|
+
position: 'before',
|
|
153
|
+
message: '<span>Before</span>',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
sdk.inline.show(experience);
|
|
158
|
+
|
|
159
|
+
const inline = document.querySelector('.xp-inline');
|
|
160
|
+
expect(inline).toBeTruthy();
|
|
161
|
+
expect(container.children.length).toBe(2); // Inline + target
|
|
162
|
+
expect(container.children[0].className).toBe('xp-inline'); // Inline first
|
|
163
|
+
expect(container.children[1].id).toBe('test-target'); // Target second
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should insert content with "after" position', () => {
|
|
167
|
+
const container = document.createElement('div');
|
|
168
|
+
const target = document.createElement('p');
|
|
169
|
+
target.id = 'test-target';
|
|
170
|
+
target.textContent = 'Target';
|
|
171
|
+
container.appendChild(target);
|
|
172
|
+
document.body.appendChild(container);
|
|
173
|
+
|
|
174
|
+
const experience = {
|
|
175
|
+
id: 'after-test',
|
|
176
|
+
type: 'inline',
|
|
177
|
+
content: {
|
|
178
|
+
selector: '#test-target',
|
|
179
|
+
position: 'after',
|
|
180
|
+
message: '<span>After</span>',
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
sdk.inline.show(experience);
|
|
185
|
+
|
|
186
|
+
const inline = document.querySelector('.xp-inline');
|
|
187
|
+
expect(inline).toBeTruthy();
|
|
188
|
+
expect(container.children.length).toBe(2); // Target + inline
|
|
189
|
+
expect(container.children[0].id).toBe('test-target'); // Target first
|
|
190
|
+
expect(container.children[1].className).toBe('xp-inline'); // Inline second
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should default to "replace" when position not specified', () => {
|
|
194
|
+
const target = document.createElement('div');
|
|
195
|
+
target.id = 'test-target';
|
|
196
|
+
target.innerHTML = '<p>Original</p>';
|
|
197
|
+
document.body.appendChild(target);
|
|
198
|
+
|
|
199
|
+
const experience = {
|
|
200
|
+
id: 'default-position',
|
|
201
|
+
type: 'inline',
|
|
202
|
+
content: {
|
|
203
|
+
selector: '#test-target',
|
|
204
|
+
message: '<h2>Replaced</h2>',
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
sdk.inline.show(experience);
|
|
209
|
+
|
|
210
|
+
const inline = document.querySelector('.xp-inline');
|
|
211
|
+
expect(inline).toBeTruthy();
|
|
212
|
+
expect(target.querySelector('p')).toBeFalsy(); // Original replaced
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('Error Handling', () => {
|
|
217
|
+
it('should emit error event when selector not found', async () => {
|
|
218
|
+
const errorHandler = vi.fn();
|
|
219
|
+
sdk.on('experiences:inline:error', errorHandler);
|
|
220
|
+
|
|
221
|
+
const experience = {
|
|
222
|
+
id: 'not-found',
|
|
223
|
+
type: 'inline',
|
|
224
|
+
content: {
|
|
225
|
+
selector: '#does-not-exist',
|
|
226
|
+
message: '<p>Content</p>',
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
sdk.inline.show(experience);
|
|
231
|
+
|
|
232
|
+
await vi.waitFor(() => {
|
|
233
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
experienceId: 'not-found',
|
|
236
|
+
error: 'selector-not-found',
|
|
237
|
+
selector: '#does-not-exist',
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should not throw error when removing non-existent inline', () => {
|
|
244
|
+
expect(() => {
|
|
245
|
+
sdk.inline.remove('does-not-exist');
|
|
246
|
+
}).not.toThrow();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('Dismissal', () => {
|
|
251
|
+
it('should render close button when dismissable is true', () => {
|
|
252
|
+
const target = document.createElement('div');
|
|
253
|
+
target.id = 'test-target';
|
|
254
|
+
document.body.appendChild(target);
|
|
255
|
+
|
|
256
|
+
const experience = {
|
|
257
|
+
id: 'dismissable-test',
|
|
258
|
+
type: 'inline',
|
|
259
|
+
content: {
|
|
260
|
+
selector: '#test-target',
|
|
261
|
+
message: '<p>Content</p>',
|
|
262
|
+
dismissable: true,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
sdk.inline.show(experience);
|
|
267
|
+
|
|
268
|
+
const closeBtn = document.querySelector('.xp-inline__close');
|
|
269
|
+
expect(closeBtn).toBeTruthy();
|
|
270
|
+
expect(closeBtn?.getAttribute('aria-label')).toBe('Close');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should not render close button when dismissable is false', () => {
|
|
274
|
+
const target = document.createElement('div');
|
|
275
|
+
target.id = 'test-target';
|
|
276
|
+
document.body.appendChild(target);
|
|
277
|
+
|
|
278
|
+
const experience = {
|
|
279
|
+
id: 'not-dismissable',
|
|
280
|
+
type: 'inline',
|
|
281
|
+
content: {
|
|
282
|
+
selector: '#test-target',
|
|
283
|
+
message: '<p>Content</p>',
|
|
284
|
+
dismissable: false,
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
sdk.inline.show(experience);
|
|
289
|
+
|
|
290
|
+
const closeBtn = document.querySelector('.xp-inline__close');
|
|
291
|
+
expect(closeBtn).toBeFalsy();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should remove inline when close button is clicked', async () => {
|
|
295
|
+
const target = document.createElement('div');
|
|
296
|
+
target.id = 'test-target';
|
|
297
|
+
document.body.appendChild(target);
|
|
298
|
+
|
|
299
|
+
const dismissHandler = vi.fn();
|
|
300
|
+
sdk.on('experiences:dismissed', dismissHandler);
|
|
301
|
+
|
|
302
|
+
const experience = {
|
|
303
|
+
id: 'dismiss-test',
|
|
304
|
+
type: 'inline',
|
|
305
|
+
content: {
|
|
306
|
+
selector: '#test-target',
|
|
307
|
+
message: '<p>Content</p>',
|
|
308
|
+
dismissable: true,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
sdk.inline.show(experience);
|
|
313
|
+
|
|
314
|
+
const closeBtn = document.querySelector('.xp-inline__close') as HTMLElement;
|
|
315
|
+
closeBtn.click();
|
|
316
|
+
|
|
317
|
+
await vi.waitFor(() => {
|
|
318
|
+
expect(dismissHandler).toHaveBeenCalledWith(
|
|
319
|
+
expect.objectContaining({
|
|
320
|
+
experienceId: 'dismiss-test',
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(document.querySelector('.xp-inline')).toBeFalsy();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should persist dismissal in localStorage when persist is true', async () => {
|
|
329
|
+
const target = document.createElement('div');
|
|
330
|
+
target.id = 'test-target';
|
|
331
|
+
document.body.appendChild(target);
|
|
332
|
+
|
|
333
|
+
const experience = {
|
|
334
|
+
id: 'persist-test',
|
|
335
|
+
type: 'inline',
|
|
336
|
+
content: {
|
|
337
|
+
selector: '#test-target',
|
|
338
|
+
message: '<p>Content</p>',
|
|
339
|
+
dismissable: true,
|
|
340
|
+
persist: true,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
sdk.inline.show(experience);
|
|
345
|
+
|
|
346
|
+
const closeBtn = document.querySelector('.xp-inline__close') as HTMLElement;
|
|
347
|
+
closeBtn.click();
|
|
348
|
+
|
|
349
|
+
// Try to show again
|
|
350
|
+
const dismissedHandler = vi.fn();
|
|
351
|
+
sdk.on('experiences:inline:dismissed', dismissedHandler);
|
|
352
|
+
|
|
353
|
+
sdk.inline.show(experience);
|
|
354
|
+
|
|
355
|
+
await vi.waitFor(() => {
|
|
356
|
+
expect(dismissedHandler).toHaveBeenCalledWith(
|
|
357
|
+
expect.objectContaining({
|
|
358
|
+
experienceId: 'persist-test',
|
|
359
|
+
reason: 'previously-dismissed',
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
expect(document.querySelector('.xp-inline')).toBeFalsy();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('Custom Styling', () => {
|
|
369
|
+
it('should apply custom className', () => {
|
|
370
|
+
const target = document.createElement('div');
|
|
371
|
+
target.id = 'test-target';
|
|
372
|
+
document.body.appendChild(target);
|
|
373
|
+
|
|
374
|
+
const experience = {
|
|
375
|
+
id: 'class-test',
|
|
376
|
+
type: 'inline',
|
|
377
|
+
content: {
|
|
378
|
+
selector: '#test-target',
|
|
379
|
+
message: '<p>Content</p>',
|
|
380
|
+
className: 'custom-class',
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
sdk.inline.show(experience);
|
|
385
|
+
|
|
386
|
+
const inline = document.querySelector('.xp-inline');
|
|
387
|
+
expect(inline?.classList.contains('custom-class')).toBe(true);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should apply custom inline styles', () => {
|
|
391
|
+
const target = document.createElement('div');
|
|
392
|
+
target.id = 'test-target';
|
|
393
|
+
document.body.appendChild(target);
|
|
394
|
+
|
|
395
|
+
const experience = {
|
|
396
|
+
id: 'style-test',
|
|
397
|
+
type: 'inline',
|
|
398
|
+
content: {
|
|
399
|
+
selector: '#test-target',
|
|
400
|
+
message: '<p>Content</p>',
|
|
401
|
+
style: {
|
|
402
|
+
padding: '20px',
|
|
403
|
+
backgroundColor: 'red',
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
sdk.inline.show(experience);
|
|
409
|
+
|
|
410
|
+
const inline = document.querySelector('.xp-inline') as HTMLElement;
|
|
411
|
+
expect(inline?.style.padding).toBe('20px');
|
|
412
|
+
expect(inline?.style.backgroundColor).toBe('red');
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('Multi-instance', () => {
|
|
417
|
+
it('should support multiple inline experiences', () => {
|
|
418
|
+
const target1 = document.createElement('div');
|
|
419
|
+
target1.id = 'target-1';
|
|
420
|
+
document.body.appendChild(target1);
|
|
421
|
+
|
|
422
|
+
const target2 = document.createElement('div');
|
|
423
|
+
target2.id = 'target-2';
|
|
424
|
+
document.body.appendChild(target2);
|
|
425
|
+
|
|
426
|
+
sdk.inline.show({
|
|
427
|
+
id: 'inline-1',
|
|
428
|
+
type: 'inline',
|
|
429
|
+
content: {
|
|
430
|
+
selector: '#target-1',
|
|
431
|
+
message: '<p>Content 1</p>',
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
sdk.inline.show({
|
|
436
|
+
id: 'inline-2',
|
|
437
|
+
type: 'inline',
|
|
438
|
+
content: {
|
|
439
|
+
selector: '#target-2',
|
|
440
|
+
message: '<p>Content 2</p>',
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const inlines = document.querySelectorAll('.xp-inline');
|
|
445
|
+
expect(inlines.length).toBe(2);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should prevent duplicate inline experiences', () => {
|
|
449
|
+
const target = document.createElement('div');
|
|
450
|
+
target.id = 'test-target';
|
|
451
|
+
document.body.appendChild(target);
|
|
452
|
+
|
|
453
|
+
const experience = {
|
|
454
|
+
id: 'duplicate-test',
|
|
455
|
+
type: 'inline' as const,
|
|
456
|
+
content: {
|
|
457
|
+
selector: '#test-target',
|
|
458
|
+
message: '<p>Content</p>',
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Show the same experience twice
|
|
463
|
+
sdk.inline.show(experience);
|
|
464
|
+
sdk.inline.show(experience);
|
|
465
|
+
|
|
466
|
+
// Should only insert once
|
|
467
|
+
const inlines = document.querySelectorAll('[data-xp-id="duplicate-test"]');
|
|
468
|
+
expect(inlines.length).toBe(1);
|
|
469
|
+
expect(sdk.inline.isShowing('duplicate-test')).toBe(true);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('should check if specific inline is showing', () => {
|
|
473
|
+
const target = document.createElement('div');
|
|
474
|
+
target.id = 'test-target';
|
|
475
|
+
document.body.appendChild(target);
|
|
476
|
+
|
|
477
|
+
sdk.inline.show({
|
|
478
|
+
id: 'check-test',
|
|
479
|
+
type: 'inline',
|
|
480
|
+
content: {
|
|
481
|
+
selector: '#test-target',
|
|
482
|
+
message: '<p>Content</p>',
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(sdk.inline.isShowing('check-test')).toBe(true);
|
|
487
|
+
expect(sdk.inline.isShowing('does-not-exist')).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should check if any inline is showing', () => {
|
|
491
|
+
expect(sdk.inline.isShowing()).toBe(false);
|
|
492
|
+
|
|
493
|
+
const target = document.createElement('div');
|
|
494
|
+
target.id = 'test-target';
|
|
495
|
+
document.body.appendChild(target);
|
|
496
|
+
|
|
497
|
+
sdk.inline.show({
|
|
498
|
+
id: 'any-test',
|
|
499
|
+
type: 'inline',
|
|
500
|
+
content: {
|
|
501
|
+
selector: '#test-target',
|
|
502
|
+
message: '<p>Content</p>',
|
|
503
|
+
},
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
expect(sdk.inline.isShowing()).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe('Events', () => {
|
|
511
|
+
it('should emit shown event when inline is displayed', async () => {
|
|
512
|
+
const target = document.createElement('div');
|
|
513
|
+
target.id = 'test-target';
|
|
514
|
+
document.body.appendChild(target);
|
|
515
|
+
|
|
516
|
+
const shownHandler = vi.fn();
|
|
517
|
+
sdk.on('experiences:shown', shownHandler);
|
|
518
|
+
|
|
519
|
+
sdk.inline.show({
|
|
520
|
+
id: 'shown-test',
|
|
521
|
+
type: 'inline',
|
|
522
|
+
content: {
|
|
523
|
+
selector: '#test-target',
|
|
524
|
+
message: '<p>Content</p>',
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
await vi.waitFor(() => {
|
|
529
|
+
expect(shownHandler).toHaveBeenCalledWith(
|
|
530
|
+
expect.objectContaining({
|
|
531
|
+
experienceId: 'shown-test',
|
|
532
|
+
type: 'inline',
|
|
533
|
+
selector: '#test-target',
|
|
534
|
+
position: 'replace',
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
describe('Cleanup', () => {
|
|
542
|
+
it('should remove inline on explicit remove call', () => {
|
|
543
|
+
const target = document.createElement('div');
|
|
544
|
+
target.id = 'test-target';
|
|
545
|
+
document.body.appendChild(target);
|
|
546
|
+
|
|
547
|
+
sdk.inline.show({
|
|
548
|
+
id: 'remove-test',
|
|
549
|
+
type: 'inline',
|
|
550
|
+
content: {
|
|
551
|
+
selector: '#test-target',
|
|
552
|
+
message: '<p>Content</p>',
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
expect(document.querySelector('.xp-inline')).toBeTruthy();
|
|
557
|
+
|
|
558
|
+
sdk.inline.remove('remove-test');
|
|
559
|
+
|
|
560
|
+
expect(document.querySelector('.xp-inline')).toBeFalsy();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('should remove all inlines on destroy', async () => {
|
|
564
|
+
const target1 = document.createElement('div');
|
|
565
|
+
target1.id = 'target-1';
|
|
566
|
+
document.body.appendChild(target1);
|
|
567
|
+
|
|
568
|
+
const target2 = document.createElement('div');
|
|
569
|
+
target2.id = 'target-2';
|
|
570
|
+
document.body.appendChild(target2);
|
|
571
|
+
|
|
572
|
+
sdk.inline.show({
|
|
573
|
+
id: 'destroy-1',
|
|
574
|
+
type: 'inline',
|
|
575
|
+
content: {
|
|
576
|
+
selector: '#target-1',
|
|
577
|
+
message: '<p>Content 1</p>',
|
|
578
|
+
},
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
sdk.inline.show({
|
|
582
|
+
id: 'destroy-2',
|
|
583
|
+
type: 'inline',
|
|
584
|
+
content: {
|
|
585
|
+
selector: '#target-2',
|
|
586
|
+
message: '<p>Content 2</p>',
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
expect(document.querySelectorAll('.xp-inline').length).toBe(2);
|
|
591
|
+
|
|
592
|
+
await sdk.destroy();
|
|
593
|
+
|
|
594
|
+
expect(document.querySelectorAll('.xp-inline').length).toBe(0);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
describe('HTML Sanitization', () => {
|
|
599
|
+
it('should sanitize HTML content', () => {
|
|
600
|
+
const target = document.createElement('div');
|
|
601
|
+
target.id = 'test-target';
|
|
602
|
+
document.body.appendChild(target);
|
|
603
|
+
|
|
604
|
+
const experience = {
|
|
605
|
+
id: 'sanitize-test',
|
|
606
|
+
type: 'inline',
|
|
607
|
+
content: {
|
|
608
|
+
selector: '#test-target',
|
|
609
|
+
message: '<p>Safe content</p><script>alert("xss")</script>',
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
sdk.inline.show(experience);
|
|
614
|
+
|
|
615
|
+
const inline = document.querySelector('.xp-inline');
|
|
616
|
+
expect(inline?.innerHTML).not.toContain('<script>');
|
|
617
|
+
expect(inline?.innerHTML).toContain('<p>Safe content</p>');
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
});
|