@prosdevlab/experience-sdk-plugins 0.1.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/package.json +34 -0
- package/src/banner/banner.test.ts +728 -0
- package/src/banner/banner.ts +369 -0
- package/src/banner/index.ts +6 -0
- package/src/debug/debug.test.ts +230 -0
- package/src/debug/debug.ts +106 -0
- package/src/debug/index.ts +6 -0
- package/src/frequency/frequency.test.ts +361 -0
- package/src/frequency/frequency.ts +247 -0
- package/src/frequency/index.ts +6 -0
- package/src/index.ts +22 -0
- package/src/types.ts +92 -0
- package/tsconfig.json +9 -0
- package/tsup.config.ts +14 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { SDK } from '@lytics/sdk-kit';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import type { Experience } from '../types';
|
|
4
|
+
import { type BannerPlugin, bannerPlugin } from './banner';
|
|
5
|
+
|
|
6
|
+
type SDKWithBanner = SDK & { banner: BannerPlugin };
|
|
7
|
+
|
|
8
|
+
describe('Banner Plugin', () => {
|
|
9
|
+
let sdk: SDKWithBanner;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
sdk = new SDK({
|
|
13
|
+
banner: { position: 'top', dismissable: true },
|
|
14
|
+
}) as SDKWithBanner;
|
|
15
|
+
sdk.use(bannerPlugin);
|
|
16
|
+
|
|
17
|
+
// Clean up any existing banners
|
|
18
|
+
document.body.innerHTML = '';
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
// Clean up DOM
|
|
23
|
+
document.body.innerHTML = '';
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('Plugin Registration', () => {
|
|
27
|
+
it('should register banner plugin', () => {
|
|
28
|
+
expect(sdk.banner).toBeDefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should expose banner API methods', () => {
|
|
32
|
+
expect(sdk.banner.show).toBeTypeOf('function');
|
|
33
|
+
expect(sdk.banner.remove).toBeTypeOf('function');
|
|
34
|
+
expect(sdk.banner.isShowing).toBeTypeOf('function');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Configuration', () => {
|
|
39
|
+
it('should use default config', () => {
|
|
40
|
+
const position = sdk.get('banner.position');
|
|
41
|
+
const dismissable = sdk.get('banner.dismissable');
|
|
42
|
+
const zIndex = sdk.get('banner.zIndex');
|
|
43
|
+
|
|
44
|
+
expect(position).toBe('top');
|
|
45
|
+
expect(dismissable).toBe(true);
|
|
46
|
+
expect(zIndex).toBe(10000);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should allow custom config', () => {
|
|
50
|
+
const customSdk = new SDK({
|
|
51
|
+
banner: { position: 'bottom', dismissable: false, zIndex: 5000 },
|
|
52
|
+
}) as SDKWithBanner;
|
|
53
|
+
customSdk.use(bannerPlugin);
|
|
54
|
+
|
|
55
|
+
expect(customSdk.get('banner.position')).toBe('bottom');
|
|
56
|
+
expect(customSdk.get('banner.dismissable')).toBe(false);
|
|
57
|
+
expect(customSdk.get('banner.zIndex')).toBe(5000);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('Banner Creation', () => {
|
|
62
|
+
it('should create and show a banner', () => {
|
|
63
|
+
const experience: Experience = {
|
|
64
|
+
id: 'test-banner',
|
|
65
|
+
type: 'banner',
|
|
66
|
+
targeting: { url: { contains: '/' } },
|
|
67
|
+
content: {
|
|
68
|
+
title: 'Test Title',
|
|
69
|
+
message: 'Test message',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
sdk.banner.show(experience);
|
|
74
|
+
|
|
75
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
76
|
+
expect(banner).toBeTruthy();
|
|
77
|
+
expect(banner?.textContent).toContain('Test Title');
|
|
78
|
+
expect(banner?.textContent).toContain('Test message');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should show banner at top position by default', () => {
|
|
82
|
+
const experience: Experience = {
|
|
83
|
+
id: 'test-banner',
|
|
84
|
+
type: 'banner',
|
|
85
|
+
targeting: {},
|
|
86
|
+
content: {
|
|
87
|
+
title: 'Test',
|
|
88
|
+
message: 'Message',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
sdk.banner.show(experience);
|
|
93
|
+
|
|
94
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
|
|
95
|
+
expect(banner.style.top).toBe('0px');
|
|
96
|
+
expect(banner.style.bottom).toBe('');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should show banner at bottom position when configured', () => {
|
|
100
|
+
const customSdk = new SDK({ banner: { position: 'bottom' } }) as SDKWithBanner;
|
|
101
|
+
customSdk.use(bannerPlugin);
|
|
102
|
+
|
|
103
|
+
const experience: Experience = {
|
|
104
|
+
id: 'test-banner',
|
|
105
|
+
type: 'banner',
|
|
106
|
+
targeting: {},
|
|
107
|
+
content: {
|
|
108
|
+
title: 'Test',
|
|
109
|
+
message: 'Message',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
customSdk.banner.show(experience);
|
|
114
|
+
|
|
115
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
|
|
116
|
+
expect(banner.style.bottom).toBe('0px');
|
|
117
|
+
expect(banner.style.top).toBe('');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should create banner with title and message', () => {
|
|
121
|
+
const experience: Experience = {
|
|
122
|
+
id: 'test-banner',
|
|
123
|
+
type: 'banner',
|
|
124
|
+
targeting: {},
|
|
125
|
+
content: {
|
|
126
|
+
title: 'Welcome!',
|
|
127
|
+
message: 'This is a test banner',
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
sdk.banner.show(experience);
|
|
132
|
+
|
|
133
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
134
|
+
expect(banner?.textContent).toContain('Welcome!');
|
|
135
|
+
expect(banner?.textContent).toContain('This is a test banner');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should create banner with only message (no title)', () => {
|
|
139
|
+
const experience: Experience = {
|
|
140
|
+
id: 'test-banner',
|
|
141
|
+
type: 'banner',
|
|
142
|
+
targeting: {},
|
|
143
|
+
content: {
|
|
144
|
+
title: '',
|
|
145
|
+
message: 'Just a message',
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
sdk.banner.show(experience);
|
|
150
|
+
|
|
151
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
152
|
+
expect(banner?.textContent).toContain('Just a message');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('Banner Dismissal', () => {
|
|
157
|
+
it('should include close button when dismissable', () => {
|
|
158
|
+
const experience: Experience = {
|
|
159
|
+
id: 'test-banner',
|
|
160
|
+
type: 'banner',
|
|
161
|
+
targeting: {},
|
|
162
|
+
content: {
|
|
163
|
+
title: 'Test',
|
|
164
|
+
message: 'Message',
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
sdk.banner.show(experience);
|
|
169
|
+
|
|
170
|
+
const closeButton = document.querySelector('[data-experience-id="test-banner"] button');
|
|
171
|
+
expect(closeButton).toBeTruthy();
|
|
172
|
+
expect(closeButton?.getAttribute('aria-label')).toBe('Close banner');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should not include close button when not dismissable', () => {
|
|
176
|
+
const customSdk = new SDK({ banner: { dismissable: false } }) as SDKWithBanner;
|
|
177
|
+
customSdk.use(bannerPlugin);
|
|
178
|
+
|
|
179
|
+
const experience: Experience = {
|
|
180
|
+
id: 'test-banner',
|
|
181
|
+
type: 'banner',
|
|
182
|
+
targeting: {},
|
|
183
|
+
content: {
|
|
184
|
+
title: 'Test',
|
|
185
|
+
message: 'Message',
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
customSdk.banner.show(experience);
|
|
190
|
+
|
|
191
|
+
const closeButton = document.querySelector('[data-experience-id="test-banner"] button');
|
|
192
|
+
expect(closeButton).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should remove banner when close button is clicked', () => {
|
|
196
|
+
const experience: Experience = {
|
|
197
|
+
id: 'test-banner',
|
|
198
|
+
type: 'banner',
|
|
199
|
+
targeting: {},
|
|
200
|
+
content: {
|
|
201
|
+
title: 'Test',
|
|
202
|
+
message: 'Message',
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
sdk.banner.show(experience);
|
|
207
|
+
|
|
208
|
+
const closeButton = document.querySelector(
|
|
209
|
+
'[data-experience-id="test-banner"] button'
|
|
210
|
+
) as HTMLElement;
|
|
211
|
+
closeButton.click();
|
|
212
|
+
|
|
213
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
214
|
+
expect(banner).toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should emit experiences:dismissed event when banner is dismissed', () => {
|
|
218
|
+
const handler = vi.fn();
|
|
219
|
+
sdk.on('experiences:dismissed', handler);
|
|
220
|
+
|
|
221
|
+
const experience: Experience = {
|
|
222
|
+
id: 'test-banner',
|
|
223
|
+
type: 'banner',
|
|
224
|
+
targeting: {},
|
|
225
|
+
content: {
|
|
226
|
+
title: 'Test',
|
|
227
|
+
message: 'Message',
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
sdk.banner.show(experience);
|
|
232
|
+
|
|
233
|
+
const closeButton = document.querySelector(
|
|
234
|
+
'[data-experience-id="test-banner"] button'
|
|
235
|
+
) as HTMLElement;
|
|
236
|
+
closeButton.click();
|
|
237
|
+
|
|
238
|
+
expect(handler).toHaveBeenCalledWith({
|
|
239
|
+
experienceId: 'test-banner',
|
|
240
|
+
type: 'banner',
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe('Banner Management', () => {
|
|
246
|
+
it('should return false for isShowing when no banner is active', () => {
|
|
247
|
+
expect(sdk.banner.isShowing()).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return true for isShowing when banner is active', () => {
|
|
251
|
+
const experience: Experience = {
|
|
252
|
+
id: 'test-banner',
|
|
253
|
+
type: 'banner',
|
|
254
|
+
targeting: {},
|
|
255
|
+
content: {
|
|
256
|
+
title: 'Test',
|
|
257
|
+
message: 'Message',
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
sdk.banner.show(experience);
|
|
262
|
+
expect(sdk.banner.isShowing()).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should support multiple banners simultaneously', () => {
|
|
266
|
+
const experience1: Experience = {
|
|
267
|
+
id: 'banner-1',
|
|
268
|
+
type: 'banner',
|
|
269
|
+
targeting: {},
|
|
270
|
+
content: {
|
|
271
|
+
title: 'First',
|
|
272
|
+
message: 'First message',
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const experience2: Experience = {
|
|
277
|
+
id: 'banner-2',
|
|
278
|
+
type: 'banner',
|
|
279
|
+
targeting: {},
|
|
280
|
+
content: {
|
|
281
|
+
title: 'Second',
|
|
282
|
+
message: 'Second message',
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
sdk.banner.show(experience1);
|
|
287
|
+
expect(document.querySelector('[data-experience-id="banner-1"]')).toBeTruthy();
|
|
288
|
+
|
|
289
|
+
sdk.banner.show(experience2);
|
|
290
|
+
// Both banners should be present
|
|
291
|
+
expect(document.querySelector('[data-experience-id="banner-1"]')).toBeTruthy();
|
|
292
|
+
expect(document.querySelector('[data-experience-id="banner-2"]')).toBeTruthy();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should manually remove banner via remove()', () => {
|
|
296
|
+
const experience: Experience = {
|
|
297
|
+
id: 'test-banner',
|
|
298
|
+
type: 'banner',
|
|
299
|
+
targeting: {},
|
|
300
|
+
content: {
|
|
301
|
+
title: 'Test',
|
|
302
|
+
message: 'Message',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
sdk.banner.show(experience);
|
|
307
|
+
expect(sdk.banner.isShowing()).toBe(true);
|
|
308
|
+
|
|
309
|
+
sdk.banner.remove();
|
|
310
|
+
expect(sdk.banner.isShowing()).toBe(false);
|
|
311
|
+
expect(document.querySelector('[data-experience-id="test-banner"]')).toBeNull();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should handle remove() when no banner is showing', () => {
|
|
315
|
+
expect(() => sdk.banner.remove()).not.toThrow();
|
|
316
|
+
expect(sdk.banner.isShowing()).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('Events', () => {
|
|
321
|
+
it('should emit experiences:shown event when banner is shown', () => {
|
|
322
|
+
const handler = vi.fn();
|
|
323
|
+
sdk.on('experiences:shown', handler);
|
|
324
|
+
|
|
325
|
+
const experience: Experience = {
|
|
326
|
+
id: 'test-banner',
|
|
327
|
+
type: 'banner',
|
|
328
|
+
targeting: {},
|
|
329
|
+
content: {
|
|
330
|
+
title: 'Test',
|
|
331
|
+
message: 'Message',
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
sdk.banner.show(experience);
|
|
336
|
+
|
|
337
|
+
expect(handler).toHaveBeenCalledWith({
|
|
338
|
+
experienceId: 'test-banner',
|
|
339
|
+
type: 'banner',
|
|
340
|
+
timestamp: expect.any(Number),
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should auto-render banner on experiences:evaluated event', () => {
|
|
345
|
+
const experience: Experience = {
|
|
346
|
+
id: 'auto-banner',
|
|
347
|
+
type: 'banner',
|
|
348
|
+
targeting: {},
|
|
349
|
+
content: {
|
|
350
|
+
title: 'Auto Banner',
|
|
351
|
+
message: 'Auto message',
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
// Emit the event that runtime would emit
|
|
356
|
+
sdk.emit('experiences:evaluated', {
|
|
357
|
+
decision: {
|
|
358
|
+
show: true,
|
|
359
|
+
experienceId: 'auto-banner',
|
|
360
|
+
reasons: ['test'],
|
|
361
|
+
trace: [],
|
|
362
|
+
context: {},
|
|
363
|
+
metadata: { evaluatedAt: Date.now(), totalDuration: 0, experiencesEvaluated: 1 },
|
|
364
|
+
},
|
|
365
|
+
experience,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Banner should be automatically rendered
|
|
369
|
+
const banner = document.querySelector('[data-experience-id="auto-banner"]');
|
|
370
|
+
expect(banner).toBeTruthy();
|
|
371
|
+
expect(banner?.textContent).toContain('Auto Banner');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should auto-hide banner when decision is false', () => {
|
|
375
|
+
const experience: Experience = {
|
|
376
|
+
id: 'hide-banner',
|
|
377
|
+
type: 'banner',
|
|
378
|
+
targeting: {},
|
|
379
|
+
content: {
|
|
380
|
+
title: 'Hide Test',
|
|
381
|
+
message: 'Will hide',
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// First show the banner
|
|
386
|
+
sdk.banner.show(experience);
|
|
387
|
+
expect(sdk.banner.isShowing()).toBe(true);
|
|
388
|
+
|
|
389
|
+
// Emit event with show: false
|
|
390
|
+
sdk.emit('experiences:evaluated', {
|
|
391
|
+
decision: {
|
|
392
|
+
show: false,
|
|
393
|
+
experienceId: 'hide-banner',
|
|
394
|
+
reasons: ['Frequency cap reached'],
|
|
395
|
+
trace: [],
|
|
396
|
+
context: {},
|
|
397
|
+
metadata: { evaluatedAt: Date.now(), totalDuration: 0, experiencesEvaluated: 1 },
|
|
398
|
+
},
|
|
399
|
+
experience,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Banner should be removed
|
|
403
|
+
expect(sdk.banner.isShowing()).toBe(false);
|
|
404
|
+
expect(document.querySelector('[data-experience-id="hide-banner"]')).toBeNull();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should only handle banner-type experiences', () => {
|
|
408
|
+
const modalExperience = {
|
|
409
|
+
id: 'modal',
|
|
410
|
+
type: 'modal' as const,
|
|
411
|
+
targeting: {},
|
|
412
|
+
content: {
|
|
413
|
+
title: 'Modal',
|
|
414
|
+
body: 'Modal content',
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// Emit event with modal type
|
|
419
|
+
sdk.emit('experiences:evaluated', {
|
|
420
|
+
decision: {
|
|
421
|
+
show: true,
|
|
422
|
+
experienceId: 'modal',
|
|
423
|
+
reasons: ['test'],
|
|
424
|
+
trace: [],
|
|
425
|
+
context: {},
|
|
426
|
+
metadata: { evaluatedAt: Date.now(), totalDuration: 0, experiencesEvaluated: 1 },
|
|
427
|
+
},
|
|
428
|
+
experience: modalExperience,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Banner should NOT be rendered
|
|
432
|
+
expect(document.querySelector('[data-experience-id="modal"]')).toBeNull();
|
|
433
|
+
expect(sdk.banner.isShowing()).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should remove banner on destroy event', async () => {
|
|
437
|
+
const experience: Experience = {
|
|
438
|
+
id: 'test-banner',
|
|
439
|
+
type: 'banner',
|
|
440
|
+
targeting: {},
|
|
441
|
+
content: {
|
|
442
|
+
title: 'Test',
|
|
443
|
+
message: 'Message',
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
sdk.banner.show(experience);
|
|
448
|
+
expect(sdk.banner.isShowing()).toBe(true);
|
|
449
|
+
expect(document.querySelector('[data-experience-id="test-banner"]')).toBeTruthy();
|
|
450
|
+
|
|
451
|
+
await sdk.destroy();
|
|
452
|
+
|
|
453
|
+
// After destroy, the banner should be removed from DOM
|
|
454
|
+
expect(document.querySelector('[data-experience-id="test-banner"]')).toBeNull();
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
describe('CTA Button', () => {
|
|
459
|
+
it('should render multiple buttons from buttons array', () => {
|
|
460
|
+
const experience: Experience = {
|
|
461
|
+
id: 'test-banner',
|
|
462
|
+
type: 'banner',
|
|
463
|
+
targeting: {},
|
|
464
|
+
content: {
|
|
465
|
+
message: 'Cookie consent',
|
|
466
|
+
buttons: [
|
|
467
|
+
{ text: 'Accept all', action: 'accept', variant: 'primary' },
|
|
468
|
+
{ text: 'Reject', action: 'reject', variant: 'secondary' },
|
|
469
|
+
{ text: 'Preferences', action: 'preferences', variant: 'link' },
|
|
470
|
+
],
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
sdk.banner.show(experience);
|
|
475
|
+
|
|
476
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
477
|
+
const buttons = banner?.querySelectorAll('button');
|
|
478
|
+
|
|
479
|
+
// Should have 3 action buttons + 1 dismiss button (default)
|
|
480
|
+
expect(buttons?.length).toBe(4);
|
|
481
|
+
expect(buttons?.[0].textContent).toBe('Accept all');
|
|
482
|
+
expect(buttons?.[1].textContent).toBe('Reject');
|
|
483
|
+
expect(buttons?.[2].textContent).toBe('Preferences');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should use default primary variant when not specified', () => {
|
|
487
|
+
const experience: Experience = {
|
|
488
|
+
id: 'test-banner',
|
|
489
|
+
type: 'banner',
|
|
490
|
+
targeting: {},
|
|
491
|
+
content: {
|
|
492
|
+
message: 'Test',
|
|
493
|
+
buttons: [{ text: 'Click Me', action: 'test' }],
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
sdk.banner.show(experience);
|
|
498
|
+
|
|
499
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
500
|
+
const button = banner?.querySelector('button') as HTMLElement;
|
|
501
|
+
|
|
502
|
+
expect(button).toBeTruthy();
|
|
503
|
+
// Primary variant should have white text
|
|
504
|
+
expect(button.style.color).toContain('255'); // rgb(255, 255, 255)
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should emit action event with variant and metadata', () => {
|
|
508
|
+
const experience: Experience = {
|
|
509
|
+
id: 'test-banner',
|
|
510
|
+
type: 'banner',
|
|
511
|
+
targeting: {},
|
|
512
|
+
content: {
|
|
513
|
+
message: 'Cookie consent',
|
|
514
|
+
buttons: [
|
|
515
|
+
{
|
|
516
|
+
text: 'Accept',
|
|
517
|
+
action: 'accept',
|
|
518
|
+
variant: 'primary',
|
|
519
|
+
metadata: { consent_categories: ['all'] },
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
let emittedEvent: any;
|
|
526
|
+
sdk.on('experiences:action', (event: any) => {
|
|
527
|
+
emittedEvent = event;
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
sdk.banner.show(experience);
|
|
531
|
+
|
|
532
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
533
|
+
const button = banner?.querySelector('button') as HTMLElement;
|
|
534
|
+
button.click();
|
|
535
|
+
|
|
536
|
+
expect(emittedEvent).toBeTruthy();
|
|
537
|
+
expect(emittedEvent.action).toBe('accept');
|
|
538
|
+
expect(emittedEvent.variant).toBe('primary');
|
|
539
|
+
expect(emittedEvent.metadata).toEqual({ consent_categories: ['all'] });
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('should not render CTA button when not provided', () => {
|
|
543
|
+
const experience: Experience = {
|
|
544
|
+
id: 'test-banner',
|
|
545
|
+
type: 'banner',
|
|
546
|
+
targeting: {},
|
|
547
|
+
content: {
|
|
548
|
+
message: 'Test message',
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
sdk.banner.show(experience);
|
|
553
|
+
|
|
554
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
555
|
+
// Should only have dismiss button (×), not a CTA button
|
|
556
|
+
const buttons = banner?.querySelectorAll('button');
|
|
557
|
+
expect(buttons?.length).toBe(1);
|
|
558
|
+
expect(buttons?.[0].textContent).toBe('×');
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('should emit experiences:action event when button clicked', () => {
|
|
562
|
+
const handler = vi.fn();
|
|
563
|
+
sdk.on('experiences:action', handler);
|
|
564
|
+
|
|
565
|
+
const experience: Experience = {
|
|
566
|
+
id: 'test-banner',
|
|
567
|
+
type: 'banner',
|
|
568
|
+
targeting: {},
|
|
569
|
+
content: {
|
|
570
|
+
message: 'Test message',
|
|
571
|
+
buttons: [
|
|
572
|
+
{
|
|
573
|
+
text: 'Click Me',
|
|
574
|
+
action: 'test-action',
|
|
575
|
+
url: '/test',
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
sdk.banner.show(experience);
|
|
582
|
+
|
|
583
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
584
|
+
const ctaButton = Array.from(banner?.querySelectorAll('button') || []).find(
|
|
585
|
+
(btn) => btn.textContent === 'Click Me'
|
|
586
|
+
) as HTMLElement;
|
|
587
|
+
|
|
588
|
+
ctaButton.click();
|
|
589
|
+
|
|
590
|
+
expect(handler).toHaveBeenCalledWith({
|
|
591
|
+
experienceId: 'test-banner',
|
|
592
|
+
type: 'banner',
|
|
593
|
+
action: 'test-action',
|
|
594
|
+
url: '/test',
|
|
595
|
+
variant: 'primary',
|
|
596
|
+
metadata: undefined,
|
|
597
|
+
timestamp: expect.any(Number),
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('should respect dismissable: false config', () => {
|
|
602
|
+
const experience: Experience = {
|
|
603
|
+
id: 'test-banner',
|
|
604
|
+
type: 'banner',
|
|
605
|
+
targeting: {},
|
|
606
|
+
content: {
|
|
607
|
+
message: 'Test message',
|
|
608
|
+
buttons: [
|
|
609
|
+
{
|
|
610
|
+
text: 'Click Me',
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
dismissable: false,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
sdk.banner.show(experience);
|
|
618
|
+
|
|
619
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
620
|
+
const buttons = banner?.querySelectorAll('button');
|
|
621
|
+
|
|
622
|
+
// Should only have action button, no dismiss button
|
|
623
|
+
expect(buttons?.length).toBe(1);
|
|
624
|
+
expect(buttons?.[0].textContent).toBe('Click Me');
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
it('should show both action and dismiss button when both provided', () => {
|
|
628
|
+
const experience: Experience = {
|
|
629
|
+
id: 'test-banner',
|
|
630
|
+
type: 'banner',
|
|
631
|
+
targeting: {},
|
|
632
|
+
content: {
|
|
633
|
+
message: 'Test message',
|
|
634
|
+
buttons: [
|
|
635
|
+
{
|
|
636
|
+
text: 'Learn More',
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
dismissable: true,
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
sdk.banner.show(experience);
|
|
644
|
+
|
|
645
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]');
|
|
646
|
+
const buttons = banner?.querySelectorAll('button');
|
|
647
|
+
|
|
648
|
+
// Should have both buttons
|
|
649
|
+
expect(buttons?.length).toBe(2);
|
|
650
|
+
expect(buttons?.[0].textContent).toBe('Learn More');
|
|
651
|
+
expect(buttons?.[1].textContent).toBe('×');
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe('Styling', () => {
|
|
656
|
+
it('should apply correct z-index', () => {
|
|
657
|
+
const experience: Experience = {
|
|
658
|
+
id: 'test-banner',
|
|
659
|
+
type: 'banner',
|
|
660
|
+
targeting: {},
|
|
661
|
+
content: {
|
|
662
|
+
title: 'Test',
|
|
663
|
+
message: 'Message',
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
sdk.banner.show(experience);
|
|
668
|
+
|
|
669
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
|
|
670
|
+
expect(banner.style.zIndex).toBe('10000');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('should apply custom z-index', () => {
|
|
674
|
+
const customSdk = new SDK({ banner: { zIndex: 99999 } }) as SDKWithBanner;
|
|
675
|
+
customSdk.use(bannerPlugin);
|
|
676
|
+
|
|
677
|
+
const experience: Experience = {
|
|
678
|
+
id: 'test-banner',
|
|
679
|
+
type: 'banner',
|
|
680
|
+
targeting: {},
|
|
681
|
+
content: {
|
|
682
|
+
title: 'Test',
|
|
683
|
+
message: 'Message',
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
customSdk.banner.show(experience);
|
|
688
|
+
|
|
689
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
|
|
690
|
+
expect(banner.style.zIndex).toBe('99999');
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
it('should be fixed position', () => {
|
|
694
|
+
const experience: Experience = {
|
|
695
|
+
id: 'test-banner',
|
|
696
|
+
type: 'banner',
|
|
697
|
+
targeting: {},
|
|
698
|
+
content: {
|
|
699
|
+
title: 'Test',
|
|
700
|
+
message: 'Message',
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
sdk.banner.show(experience);
|
|
705
|
+
|
|
706
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
|
|
707
|
+
expect(banner.style.position).toBe('fixed');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('should span full width', () => {
|
|
711
|
+
const experience: Experience = {
|
|
712
|
+
id: 'test-banner',
|
|
713
|
+
type: 'banner',
|
|
714
|
+
targeting: {},
|
|
715
|
+
content: {
|
|
716
|
+
title: 'Test',
|
|
717
|
+
message: 'Message',
|
|
718
|
+
},
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
sdk.banner.show(experience);
|
|
722
|
+
|
|
723
|
+
const banner = document.querySelector('[data-experience-id="test-banner"]') as HTMLElement;
|
|
724
|
+
expect(banner.style.left).toBe('0px');
|
|
725
|
+
expect(banner.style.right).toBe('0px');
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
});
|