@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,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure validation functions for form fields
|
|
3
|
+
*
|
|
4
|
+
* These functions are intentionally pure (no side effects) to make them:
|
|
5
|
+
* - Easy to test
|
|
6
|
+
* - Easy to extract into a separate form plugin later
|
|
7
|
+
* - Reusable across different contexts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FormConfig, FormField, ValidationResult } from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate a single form field
|
|
14
|
+
*
|
|
15
|
+
* @param field - Field configuration
|
|
16
|
+
* @param value - Current field value
|
|
17
|
+
* @returns Validation result with errors if invalid
|
|
18
|
+
*/
|
|
19
|
+
export function validateField(field: FormField, value: string): ValidationResult {
|
|
20
|
+
const errors: Record<string, string> = {};
|
|
21
|
+
|
|
22
|
+
// Required field validation
|
|
23
|
+
if (field.required && (!value || value.trim() === '')) {
|
|
24
|
+
errors[field.name] = field.errorMessage || `${field.label || field.name} is required`;
|
|
25
|
+
return { valid: false, errors };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Skip further validation if field is empty and not required
|
|
29
|
+
if (!value || value.trim() === '') {
|
|
30
|
+
return { valid: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Type-specific validation
|
|
34
|
+
switch (field.type) {
|
|
35
|
+
case 'email': {
|
|
36
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
37
|
+
if (!emailRegex.test(value)) {
|
|
38
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid email address';
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'url': {
|
|
44
|
+
try {
|
|
45
|
+
new URL(value);
|
|
46
|
+
} catch {
|
|
47
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid URL';
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'tel': {
|
|
53
|
+
// Basic phone validation (allows digits, spaces, dashes, parentheses, plus)
|
|
54
|
+
const phoneRegex = /^[\d\s\-()+]+$/;
|
|
55
|
+
if (!phoneRegex.test(value)) {
|
|
56
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid phone number';
|
|
57
|
+
}
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case 'number': {
|
|
62
|
+
if (Number.isNaN(Number(value))) {
|
|
63
|
+
errors[field.name] = field.errorMessage || 'Please enter a valid number';
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Custom pattern validation (regex)
|
|
70
|
+
if (field.pattern && value) {
|
|
71
|
+
try {
|
|
72
|
+
const regex = new RegExp(field.pattern);
|
|
73
|
+
if (!regex.test(value)) {
|
|
74
|
+
errors[field.name] =
|
|
75
|
+
field.errorMessage || `Invalid format for ${field.label || field.name}`;
|
|
76
|
+
}
|
|
77
|
+
} catch (_error) {
|
|
78
|
+
// Invalid regex pattern - log warning but don't break validation
|
|
79
|
+
console.warn(`Invalid regex pattern for field ${field.name}:`, field.pattern);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
valid: Object.keys(errors).length === 0,
|
|
85
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate entire form
|
|
91
|
+
*
|
|
92
|
+
* @param config - Form configuration
|
|
93
|
+
* @param data - Current form data
|
|
94
|
+
* @returns Validation result with all field errors if invalid
|
|
95
|
+
*/
|
|
96
|
+
export function validateForm(config: FormConfig, data: Record<string, string>): ValidationResult {
|
|
97
|
+
const errors: Record<string, string> = {};
|
|
98
|
+
|
|
99
|
+
// Validate each field
|
|
100
|
+
config.fields.forEach((field) => {
|
|
101
|
+
const value = data[field.name] || '';
|
|
102
|
+
const result = validateField(field, value);
|
|
103
|
+
|
|
104
|
+
if (!result.valid && result.errors) {
|
|
105
|
+
Object.assign(errors, result.errors);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Custom validation function
|
|
110
|
+
if (config.validate) {
|
|
111
|
+
try {
|
|
112
|
+
const customResult = config.validate(data);
|
|
113
|
+
if (!customResult.valid && customResult.errors) {
|
|
114
|
+
Object.assign(errors, customResult.errors);
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Custom validation function threw an error:', error);
|
|
118
|
+
// Don't prevent submission if custom validation has a bug
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
valid: Object.keys(errors).length === 0,
|
|
124
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal styling with CSS variables for theming
|
|
3
|
+
*
|
|
4
|
+
* Design tokens for modal dialogs, fully customizable via CSS variables.
|
|
5
|
+
* Users can override by setting CSS variables in their stylesheet.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```css
|
|
9
|
+
* :root {
|
|
10
|
+
* --xp-modal-backdrop-bg: rgba(0, 0, 0, 0.7);
|
|
11
|
+
* --xp-modal-dialog-bg: #ffffff;
|
|
12
|
+
* --xp-modal-dialog-radius: 12px;
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get CSS for modal backdrop
|
|
19
|
+
*/
|
|
20
|
+
export function getBackdropStyles(): string {
|
|
21
|
+
return `
|
|
22
|
+
position: absolute;
|
|
23
|
+
inset: 0;
|
|
24
|
+
background-color: var(--xp-modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
|
25
|
+
`.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get CSS for modal dialog
|
|
30
|
+
*/
|
|
31
|
+
export function getDialogStyles(params: {
|
|
32
|
+
width: string;
|
|
33
|
+
maxWidth: string;
|
|
34
|
+
height: string;
|
|
35
|
+
maxHeight: string;
|
|
36
|
+
borderRadius: string;
|
|
37
|
+
padding: string;
|
|
38
|
+
}): string {
|
|
39
|
+
return `
|
|
40
|
+
position: relative;
|
|
41
|
+
background: var(--xp-modal-dialog-bg, white);
|
|
42
|
+
border-radius: var(--xp-modal-dialog-radius, ${params.borderRadius});
|
|
43
|
+
box-shadow: var(--xp-modal-dialog-shadow, 0 4px 6px rgba(0, 0, 0, 0.1));
|
|
44
|
+
max-width: ${params.width};
|
|
45
|
+
width: ${params.maxWidth};
|
|
46
|
+
height: ${params.height};
|
|
47
|
+
max-height: ${params.maxHeight};
|
|
48
|
+
overflow-y: auto;
|
|
49
|
+
padding: ${params.padding};
|
|
50
|
+
`.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get CSS for hero image
|
|
55
|
+
*/
|
|
56
|
+
export function getHeroImageStyles(params: { maxHeight: number; borderRadius: string }): string {
|
|
57
|
+
return `
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: auto;
|
|
60
|
+
max-height: ${params.maxHeight}px;
|
|
61
|
+
object-fit: cover;
|
|
62
|
+
border-radius: ${params.borderRadius};
|
|
63
|
+
display: block;
|
|
64
|
+
margin: 0;
|
|
65
|
+
`.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get CSS for close button
|
|
70
|
+
*/
|
|
71
|
+
export function getCloseButtonStyles(): string {
|
|
72
|
+
return `
|
|
73
|
+
position: absolute;
|
|
74
|
+
top: var(--xp-modal-close-top, 16px);
|
|
75
|
+
right: var(--xp-modal-close-right, 16px);
|
|
76
|
+
background: none;
|
|
77
|
+
border: none;
|
|
78
|
+
font-size: var(--xp-modal-close-size, 24px);
|
|
79
|
+
line-height: 1;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
padding: var(--xp-modal-close-padding, 4px 8px);
|
|
82
|
+
color: var(--xp-modal-close-color, #666);
|
|
83
|
+
opacity: var(--xp-modal-close-opacity, 0.7);
|
|
84
|
+
transition: opacity 0.2s;
|
|
85
|
+
`.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get close button hover opacity
|
|
90
|
+
*/
|
|
91
|
+
export function getCloseButtonHoverOpacity(): string {
|
|
92
|
+
return 'var(--xp-modal-close-hover-opacity, 1)';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get close button default opacity
|
|
97
|
+
*/
|
|
98
|
+
export function getCloseButtonDefaultOpacity(): string {
|
|
99
|
+
return 'var(--xp-modal-close-opacity, 0.7)';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get CSS for content wrapper
|
|
104
|
+
*/
|
|
105
|
+
export function getContentWrapperStyles(padding: string): string {
|
|
106
|
+
return `padding: ${padding};`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get CSS for modal title
|
|
111
|
+
*/
|
|
112
|
+
export function getTitleStyles(): string {
|
|
113
|
+
return `
|
|
114
|
+
margin: 0 0 var(--xp-modal-title-margin-bottom, 16px) 0;
|
|
115
|
+
font-size: var(--xp-modal-title-size, 20px);
|
|
116
|
+
font-weight: var(--xp-modal-title-weight, 600);
|
|
117
|
+
color: var(--xp-modal-title-color, #111);
|
|
118
|
+
`.trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get CSS for modal message
|
|
123
|
+
*/
|
|
124
|
+
export function getMessageStyles(): string {
|
|
125
|
+
return `
|
|
126
|
+
margin: 0 0 var(--xp-modal-message-margin-bottom, 20px) 0;
|
|
127
|
+
font-size: var(--xp-modal-message-size, 14px);
|
|
128
|
+
line-height: var(--xp-modal-message-line-height, 1.5);
|
|
129
|
+
color: var(--xp-modal-message-color, #444);
|
|
130
|
+
`.trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get CSS for button container
|
|
135
|
+
*/
|
|
136
|
+
export function getButtonContainerStyles(): string {
|
|
137
|
+
return `
|
|
138
|
+
display: flex;
|
|
139
|
+
gap: var(--xp-modal-buttons-gap, 8px);
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
`.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get CSS for primary button
|
|
146
|
+
*/
|
|
147
|
+
export function getPrimaryButtonStyles(): string {
|
|
148
|
+
return `
|
|
149
|
+
padding: var(--xp-button-padding, 10px 20px);
|
|
150
|
+
font-size: var(--xp-button-font-size, 14px);
|
|
151
|
+
font-weight: var(--xp-button-font-weight, 500);
|
|
152
|
+
border-radius: var(--xp-button-radius, 6px);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
transition: all 0.2s;
|
|
155
|
+
border: none;
|
|
156
|
+
background: var(--xp-button-primary-bg, #2563eb);
|
|
157
|
+
color: var(--xp-button-primary-color, white);
|
|
158
|
+
`.trim();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get primary button hover background
|
|
163
|
+
*/
|
|
164
|
+
export function getPrimaryButtonHoverBg(): string {
|
|
165
|
+
return 'var(--xp-button-primary-bg-hover, #1d4ed8)';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get primary button default background
|
|
170
|
+
*/
|
|
171
|
+
export function getPrimaryButtonDefaultBg(): string {
|
|
172
|
+
return 'var(--xp-button-primary-bg, #2563eb)';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get CSS for secondary button
|
|
177
|
+
*/
|
|
178
|
+
export function getSecondaryButtonStyles(): string {
|
|
179
|
+
return `
|
|
180
|
+
padding: var(--xp-button-padding, 10px 20px);
|
|
181
|
+
font-size: var(--xp-button-font-size, 14px);
|
|
182
|
+
font-weight: var(--xp-button-font-weight, 500);
|
|
183
|
+
border-radius: var(--xp-button-radius, 6px);
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
transition: all 0.2s;
|
|
186
|
+
border: none;
|
|
187
|
+
background: var(--xp-button-secondary-bg, #f3f4f6);
|
|
188
|
+
color: var(--xp-button-secondary-color, #374151);
|
|
189
|
+
`.trim();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get secondary button hover background
|
|
194
|
+
*/
|
|
195
|
+
export function getSecondaryButtonHoverBg(): string {
|
|
196
|
+
return 'var(--xp-button-secondary-bg-hover, #e5e7eb)';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get secondary button default background
|
|
201
|
+
*/
|
|
202
|
+
export function getSecondaryButtonDefaultBg(): string {
|
|
203
|
+
return 'var(--xp-button-secondary-bg, #f3f4f6)';
|
|
204
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-specific tests for modal plugin
|
|
3
|
+
* Run with: pnpm test:browser
|
|
4
|
+
*
|
|
5
|
+
* These tests run in a real browser (Chromium via Playwright) to test
|
|
6
|
+
* features that depend on actual browser APIs like window.innerWidth.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SDK } from '@lytics/sdk-kit';
|
|
10
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { modalPlugin } from './modal';
|
|
12
|
+
|
|
13
|
+
// Helper to initialize SDK with modal plugin
|
|
14
|
+
function initPlugin(config = {}) {
|
|
15
|
+
const sdk = new SDK({
|
|
16
|
+
name: 'test-sdk',
|
|
17
|
+
...config,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
sdk.use(modalPlugin);
|
|
21
|
+
return sdk;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('Modal Plugin - Browser Tests', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// Clean up any existing modals
|
|
27
|
+
document.querySelectorAll('.xp-modal').forEach((el) => {
|
|
28
|
+
el.remove();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Mobile Viewport Detection', () => {
|
|
33
|
+
it('should detect mobile viewport (width < 640px)', () => {
|
|
34
|
+
// This test verifies that window.innerWidth works in the browser environment
|
|
35
|
+
// The actual viewport size is set by the test runner config
|
|
36
|
+
expect(window.innerWidth).toBeGreaterThan(0);
|
|
37
|
+
expect(typeof window.innerWidth).toBe('number');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should auto-enable fullscreen for lg size on mobile viewport @mobile', async () => {
|
|
41
|
+
// Note: @mobile tag would be used with test.describe.each for different viewports
|
|
42
|
+
// For now, we'll test the logic assuming the viewport is set externally
|
|
43
|
+
|
|
44
|
+
// Mock small viewport
|
|
45
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
46
|
+
writable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
value: 375,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const sdk = initPlugin({
|
|
52
|
+
modal: {
|
|
53
|
+
size: 'lg',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await sdk.init();
|
|
57
|
+
|
|
58
|
+
const experience = {
|
|
59
|
+
id: 'lg-mobile',
|
|
60
|
+
type: 'modal' as const,
|
|
61
|
+
targeting: {},
|
|
62
|
+
content: {
|
|
63
|
+
message: 'Large on mobile',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
sdk.modal.show(experience);
|
|
68
|
+
|
|
69
|
+
// In a real mobile viewport, lg should become fullscreen
|
|
70
|
+
const modal = document.querySelector('.xp-modal');
|
|
71
|
+
const hasFullscreenClass = modal?.classList.contains('xp-modal--fullscreen');
|
|
72
|
+
const hasLgClass = modal?.classList.contains('xp-modal--lg');
|
|
73
|
+
|
|
74
|
+
// Since we're mocking innerWidth, this tests the check logic
|
|
75
|
+
// With innerWidth=375, isMobile() should return true
|
|
76
|
+
expect(modal).toBeTruthy();
|
|
77
|
+
expect(hasFullscreenClass || hasLgClass).toBe(true);
|
|
78
|
+
|
|
79
|
+
// Cleanup
|
|
80
|
+
await sdk.destroy();
|
|
81
|
+
|
|
82
|
+
// Restore
|
|
83
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
84
|
+
writable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
value: 1024,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should respect mobileFullscreen: false configuration', async () => {
|
|
91
|
+
// Mock mobile viewport
|
|
92
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
93
|
+
writable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
value: 375,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const sdk = initPlugin({
|
|
99
|
+
modal: {
|
|
100
|
+
size: 'lg',
|
|
101
|
+
mobileFullscreen: false,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
await sdk.init();
|
|
105
|
+
|
|
106
|
+
const experience = {
|
|
107
|
+
id: 'lg-no-fullscreen',
|
|
108
|
+
type: 'modal' as const,
|
|
109
|
+
targeting: {},
|
|
110
|
+
content: {
|
|
111
|
+
message: 'Large without fullscreen',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
sdk.modal.show(experience);
|
|
116
|
+
|
|
117
|
+
const modal = document.querySelector('.xp-modal');
|
|
118
|
+
expect(modal?.classList.contains('xp-modal--lg')).toBe(true);
|
|
119
|
+
|
|
120
|
+
// Cleanup
|
|
121
|
+
await sdk.destroy();
|
|
122
|
+
|
|
123
|
+
// Restore
|
|
124
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
125
|
+
writable: true,
|
|
126
|
+
configurable: true,
|
|
127
|
+
value: 1024,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('Real Browser APIs', () => {
|
|
133
|
+
it('should access window object', () => {
|
|
134
|
+
expect(window).toBeDefined();
|
|
135
|
+
expect(window.document).toBeDefined();
|
|
136
|
+
expect(window.innerWidth).toBeGreaterThan(0);
|
|
137
|
+
expect(window.innerHeight).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should manipulate DOM', async () => {
|
|
141
|
+
const sdk = initPlugin();
|
|
142
|
+
await sdk.init();
|
|
143
|
+
|
|
144
|
+
const experience = {
|
|
145
|
+
id: 'dom-test',
|
|
146
|
+
type: 'modal' as const,
|
|
147
|
+
targeting: {},
|
|
148
|
+
content: {
|
|
149
|
+
message: 'DOM test',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
sdk.modal.show(experience);
|
|
154
|
+
|
|
155
|
+
// Modal should be in the DOM
|
|
156
|
+
const modal = document.querySelector('.xp-modal');
|
|
157
|
+
expect(modal).toBeInstanceOf(HTMLElement);
|
|
158
|
+
expect(modal?.getAttribute('data-xp-id')).toBe('dom-test');
|
|
159
|
+
|
|
160
|
+
// Cleanup
|
|
161
|
+
await sdk.destroy();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|