@capillarytech/creatives-library 8.0.207 → 8.0.209
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/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
- package/v2Containers/Whatsapp/constants.js +26 -2
- package/v2Containers/Whatsapp/index.js +4 -1
- package/v2Containers/Whatsapp/tests/index.test.js +460 -18
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeEditorPane - Code editor panel with tabs and CodeMirror 6 integration
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Syntax highlighting for HTML, CSS, and JavaScript
|
|
6
|
+
* - Auto-completion and error checking
|
|
7
|
+
* - Line numbers and code folding
|
|
8
|
+
* - Theme support with proper styling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { forwardRef, useImperativeHandle, useRef, useEffect, useState } from 'react';
|
|
12
|
+
import PropTypes from 'prop-types';
|
|
13
|
+
|
|
14
|
+
// CodeMirror 6 imports
|
|
15
|
+
import { EditorState } from '@codemirror/state';
|
|
16
|
+
import { EditorView, lineNumbers, highlightActiveLine } from '@codemirror/view';
|
|
17
|
+
|
|
18
|
+
// Import our comprehensive syntax highlighting solution
|
|
19
|
+
import { createRobustExtensions } from '../../utils/properSyntaxHighlighting';
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
import { injectIntl, intlShape } from 'react-intl';
|
|
23
|
+
|
|
24
|
+
// Messages
|
|
25
|
+
import messages from '../../messages';
|
|
26
|
+
|
|
27
|
+
// Cap UI Components
|
|
28
|
+
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
29
|
+
|
|
30
|
+
// Components
|
|
31
|
+
import TagList from '../../../../v2Containers/TagList';
|
|
32
|
+
|
|
33
|
+
// Context
|
|
34
|
+
import { useEditorContext } from '../common/EditorContext';
|
|
35
|
+
|
|
36
|
+
// Styles
|
|
37
|
+
import './_codeEditorPane.scss';
|
|
38
|
+
|
|
39
|
+
// Legacy CodeMirrorEditor removed - using enhanced implementation only
|
|
40
|
+
|
|
41
|
+
const CodeEditorPaneComponent = ({
|
|
42
|
+
intl,
|
|
43
|
+
readOnly = false,
|
|
44
|
+
className = '',
|
|
45
|
+
isFullscreenMode = false,
|
|
46
|
+
onLabelInsert,
|
|
47
|
+
forwardedRef
|
|
48
|
+
}) => {
|
|
49
|
+
const { content, validation } = useEditorContext();
|
|
50
|
+
const { content: contentValue, updateContent } = content;
|
|
51
|
+
const editorRef = useRef(null);
|
|
52
|
+
const viewRef = useRef(null);
|
|
53
|
+
|
|
54
|
+
// Create a ref to the latest updateContent function to avoid stale closures
|
|
55
|
+
const updateContentRef = useRef(updateContent);
|
|
56
|
+
updateContentRef.current = updateContent;
|
|
57
|
+
|
|
58
|
+
// Expose methods and EditorView instance via ref
|
|
59
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
60
|
+
// Expose the EditorView instance directly for parent access
|
|
61
|
+
get view() {
|
|
62
|
+
return viewRef.current;
|
|
63
|
+
},
|
|
64
|
+
viewRef: viewRef, // For compatibility with existing code
|
|
65
|
+
|
|
66
|
+
focus: () => {
|
|
67
|
+
if (viewRef.current) {
|
|
68
|
+
viewRef.current.focus();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
insertText: (text, position) => {
|
|
72
|
+
if (viewRef.current) {
|
|
73
|
+
const view = viewRef.current;
|
|
74
|
+
const { state: { selection: { main: { head } } } } = view;
|
|
75
|
+
const pos = position !== undefined ? position : head;
|
|
76
|
+
view.dispatch({
|
|
77
|
+
changes: { from: pos, insert: text },
|
|
78
|
+
selection: { anchor: pos + text.length }
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
throw new Error('CodeMirror view not initialized');
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
getCursor: () => {
|
|
85
|
+
if (viewRef.current) {
|
|
86
|
+
const { state: { selection: { main: { head } } } } = viewRef.current;
|
|
87
|
+
return head;
|
|
88
|
+
}
|
|
89
|
+
return 0;
|
|
90
|
+
},
|
|
91
|
+
getValue: () => {
|
|
92
|
+
return contentValue || '';
|
|
93
|
+
},
|
|
94
|
+
setValue: (value) => {
|
|
95
|
+
updateContent(value);
|
|
96
|
+
},
|
|
97
|
+
navigateToLine: (line, column = 1) => {
|
|
98
|
+
if (viewRef.current) {
|
|
99
|
+
try {
|
|
100
|
+
const view = viewRef.current;
|
|
101
|
+
const { state: { doc } } = view;
|
|
102
|
+
const { lines } = doc;
|
|
103
|
+
const lineNumber = Math.min(Math.max(1, line), lines);
|
|
104
|
+
const lineObj = doc.line(lineNumber);
|
|
105
|
+
const { from, length } = lineObj;
|
|
106
|
+
const pos = from + Math.min(Math.max(0, column - 1), length);
|
|
107
|
+
|
|
108
|
+
view.dispatch({
|
|
109
|
+
selection: { anchor: pos },
|
|
110
|
+
effects: EditorView.scrollIntoView(pos, { y: 'center' })
|
|
111
|
+
});
|
|
112
|
+
view.focus();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.warn('Could not navigate to line:', error);
|
|
115
|
+
if (viewRef.current) {
|
|
116
|
+
viewRef.current.focus();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}), [contentValue]);
|
|
122
|
+
|
|
123
|
+
// Note: handleContentChange removed - using updateContentRef directly in CodeMirror
|
|
124
|
+
|
|
125
|
+
const handleTagSelect = (tagData) => {
|
|
126
|
+
// Get the unified editor
|
|
127
|
+
if (viewRef.current) {
|
|
128
|
+
const view = viewRef.current;
|
|
129
|
+
const { state: { selection: { main: { head: pos } } } } = view;
|
|
130
|
+
|
|
131
|
+
// Extract tag text from the tagData object using destructuring
|
|
132
|
+
let tagText = '';
|
|
133
|
+
if (typeof tagData === 'string') {
|
|
134
|
+
tagText = tagData;
|
|
135
|
+
} else if (tagData) {
|
|
136
|
+
const { text, name, label, value } = tagData;
|
|
137
|
+
tagText = text || name || label || value;
|
|
138
|
+
if (!tagText) {
|
|
139
|
+
console.warn('Invalid tag data:', tagData);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
console.warn('Invalid tag data:', tagData);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// For unified HTML editor, insert as template variable
|
|
148
|
+
const formattedTag = `{{${tagText}}}`;
|
|
149
|
+
|
|
150
|
+
// Insert the tag at cursor position
|
|
151
|
+
view.dispatch({
|
|
152
|
+
changes: { from: pos, insert: formattedTag },
|
|
153
|
+
selection: { anchor: pos + formattedTag.length }
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Focus back to editor
|
|
157
|
+
view.focus();
|
|
158
|
+
|
|
159
|
+
// Call the parent's handleLabelInsert if available
|
|
160
|
+
if (onLabelInsert) {
|
|
161
|
+
onLabelInsert(formattedTag, pos);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Initialize CodeMirror effect
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (editorRef.current && !viewRef.current) {
|
|
169
|
+
// Use the comprehensive extensions from properSyntaxHighlighting.js
|
|
170
|
+
// This includes: html(), syntaxHighlighting(comprehensiveVSCodeTheme), cleanEditorTheme
|
|
171
|
+
const robustExtensions = createRobustExtensions();
|
|
172
|
+
|
|
173
|
+
// Add additional extensions for line numbers, active line, and update listener
|
|
174
|
+
const extensions = [
|
|
175
|
+
lineNumbers(),
|
|
176
|
+
highlightActiveLine(),
|
|
177
|
+
...robustExtensions, // Spread the robust extensions (html, syntax highlighting, theme)
|
|
178
|
+
EditorView.updateListener.of((update) => {
|
|
179
|
+
if (update.docChanged) {
|
|
180
|
+
updateContentRef.current(update.state.doc.toString());
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const state = EditorState.create({
|
|
186
|
+
doc: contentValue || '',
|
|
187
|
+
extensions
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
viewRef.current = new EditorView({
|
|
191
|
+
state,
|
|
192
|
+
parent: editorRef.current
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
// Cleanup cursor styling
|
|
198
|
+
if (editorRef.current && editorRef.current._cleanupCursorStyling) {
|
|
199
|
+
editorRef.current._cleanupCursorStyling();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (viewRef.current) {
|
|
203
|
+
viewRef.current.destroy();
|
|
204
|
+
viewRef.current = null;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}, []);
|
|
208
|
+
|
|
209
|
+
// Update editor content when content changes
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (viewRef.current && contentValue !== viewRef.current.state.doc.toString()) {
|
|
212
|
+
const { current: view } = viewRef;
|
|
213
|
+
const { state: { doc: { length } } } = view;
|
|
214
|
+
view.dispatch({
|
|
215
|
+
changes: {
|
|
216
|
+
from: 0,
|
|
217
|
+
to: length,
|
|
218
|
+
insert: contentValue || ''
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}, [contentValue]);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<CapRow className={`code-editor-pane ${className}`}>
|
|
226
|
+
{/* Unified Code Editor with Floating Add Label Button */}
|
|
227
|
+
<CapRow className="code-editor-pane__content">
|
|
228
|
+
<div className="codemirror-wrapper">
|
|
229
|
+
<div ref={editorRef} className="codemirror-editor" />
|
|
230
|
+
{/* Floating Add Label Button */}
|
|
231
|
+
<CapRow className="code-editor-pane__actions">
|
|
232
|
+
<TagList
|
|
233
|
+
key="html-editor-taglist"
|
|
234
|
+
label={intl.formatMessage(messages.addLabel)}
|
|
235
|
+
onTagSelect={handleTagSelect}
|
|
236
|
+
onContextChange={(context) => {
|
|
237
|
+
}}
|
|
238
|
+
className="tag-list-trigger"
|
|
239
|
+
tags={[]} // Empty initially - TagList will fetch from API
|
|
240
|
+
injectedTags={{
|
|
241
|
+
// Add common HTML/Email specific tags as fallback
|
|
242
|
+
'Customer Info': {
|
|
243
|
+
name: 'Customer Info',
|
|
244
|
+
desc: 'Customer information tags',
|
|
245
|
+
resolved: true,
|
|
246
|
+
'tag-header': true,
|
|
247
|
+
subtags: {
|
|
248
|
+
'customer.firstName': {
|
|
249
|
+
name: 'First Name',
|
|
250
|
+
desc: 'Customer first name',
|
|
251
|
+
resolved: true
|
|
252
|
+
},
|
|
253
|
+
'customer.lastName': {
|
|
254
|
+
name: 'Last Name',
|
|
255
|
+
desc: 'Customer last name',
|
|
256
|
+
resolved: true
|
|
257
|
+
},
|
|
258
|
+
'customer.email': {
|
|
259
|
+
name: 'Email',
|
|
260
|
+
desc: 'Customer email address',
|
|
261
|
+
resolved: true
|
|
262
|
+
},
|
|
263
|
+
'customer.phone': {
|
|
264
|
+
name: 'Phone',
|
|
265
|
+
desc: 'Customer phone number',
|
|
266
|
+
resolved: true
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
'Common Tags': {
|
|
271
|
+
name: 'Common Tags',
|
|
272
|
+
desc: 'Commonly used template tags',
|
|
273
|
+
resolved: true,
|
|
274
|
+
'tag-header': true,
|
|
275
|
+
subtags: {
|
|
276
|
+
'organization.name': {
|
|
277
|
+
name: 'Organization Name',
|
|
278
|
+
desc: 'Organization name',
|
|
279
|
+
resolved: true
|
|
280
|
+
},
|
|
281
|
+
'currentDate': {
|
|
282
|
+
name: 'Current Date',
|
|
283
|
+
desc: 'Current date',
|
|
284
|
+
resolved: true
|
|
285
|
+
},
|
|
286
|
+
'unsubscribeLink': {
|
|
287
|
+
name: 'Unsubscribe Link',
|
|
288
|
+
desc: 'Unsubscribe link',
|
|
289
|
+
resolved: true
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}}
|
|
294
|
+
moduleFilterEnabled={true}
|
|
295
|
+
userLocale="en"
|
|
296
|
+
channel="email"
|
|
297
|
+
disabled={readOnly}
|
|
298
|
+
location={{
|
|
299
|
+
query: {
|
|
300
|
+
type: 'html-editor' // Identify the context
|
|
301
|
+
}
|
|
302
|
+
}}
|
|
303
|
+
selectedOfferDetails={[]}
|
|
304
|
+
eventContextTags={[]}
|
|
305
|
+
/>
|
|
306
|
+
</CapRow>
|
|
307
|
+
</div>
|
|
308
|
+
</CapRow>
|
|
309
|
+
|
|
310
|
+
</CapRow>
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Create the forwardRef wrapper
|
|
315
|
+
const CodeEditorPane = forwardRef((props, ref) => (
|
|
316
|
+
<CodeEditorPaneComponent {...props} forwardedRef={ref} />
|
|
317
|
+
));
|
|
318
|
+
|
|
319
|
+
CodeEditorPane.displayName = 'CodeEditorPane';
|
|
320
|
+
|
|
321
|
+
CodeEditorPane.propTypes = {
|
|
322
|
+
intl: intlShape.isRequired,
|
|
323
|
+
readOnly: PropTypes.bool,
|
|
324
|
+
className: PropTypes.string,
|
|
325
|
+
isFullscreenMode: PropTypes.bool,
|
|
326
|
+
onLabelInsert: PropTypes.func
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Export with injectIntl - ref forwarding is handled by forwardRef wrapper
|
|
330
|
+
export default injectIntl(CodeEditorPane);
|
|
331
|
+
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeviceToggle Component Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the DeviceToggle component used in InApp variant.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import { IntlProvider } from 'react-intl';
|
|
11
|
+
import DeviceToggle from '../index';
|
|
12
|
+
import { DEVICE_TYPES } from '../../../constants';
|
|
13
|
+
|
|
14
|
+
// Mock Cap UI components
|
|
15
|
+
jest.mock('@capillarytech/cap-ui-library/CapButton', () => {
|
|
16
|
+
return function MockCapButton({ children, onClick, className, type }) {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
onClick={onClick}
|
|
20
|
+
className={className}
|
|
21
|
+
data-type={type}
|
|
22
|
+
data-testid="cap-button"
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</button>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
jest.mock('@capillarytech/cap-ui-library/CapCheckbox', () => {
|
|
31
|
+
return function MockCapCheckbox({ children, checked, onChange, className }) {
|
|
32
|
+
return (
|
|
33
|
+
<label className={className}>
|
|
34
|
+
<input
|
|
35
|
+
type="checkbox"
|
|
36
|
+
checked={checked}
|
|
37
|
+
onChange={onChange}
|
|
38
|
+
data-testid="cap-checkbox"
|
|
39
|
+
/>
|
|
40
|
+
{children}
|
|
41
|
+
</label>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
jest.mock('@capillarytech/cap-ui-library/CapIcon', () => {
|
|
47
|
+
return function MockCapIcon({ type, size }) {
|
|
48
|
+
return <span data-testid="cap-icon" data-type={type} data-size={size} />;
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Test messages
|
|
53
|
+
const messages = {
|
|
54
|
+
android: { id: 'android', defaultMessage: 'Android' },
|
|
55
|
+
ios: { id: 'ios', defaultMessage: 'iOS' },
|
|
56
|
+
keepContentSameForBoth: { id: 'keepSame', defaultMessage: 'Keep content same for both' }
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const TestWrapper = ({ children }) => (
|
|
60
|
+
<IntlProvider locale="en" messages={messages}>
|
|
61
|
+
{children}
|
|
62
|
+
</IntlProvider>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
describe('DeviceToggle', () => {
|
|
66
|
+
const defaultProps = {
|
|
67
|
+
activeDevice: DEVICE_TYPES.ANDROID,
|
|
68
|
+
onDeviceChange: jest.fn(),
|
|
69
|
+
keepContentSame: false,
|
|
70
|
+
onKeepContentSameChange: jest.fn()
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
jest.clearAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('renders with default props', () => {
|
|
78
|
+
render(
|
|
79
|
+
<TestWrapper>
|
|
80
|
+
<DeviceToggle {...defaultProps} />
|
|
81
|
+
</TestWrapper>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(screen.getByText('Android')).toBeInTheDocument();
|
|
85
|
+
expect(screen.getByText('iOS')).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText('Keep content same for both')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('shows Android as active by default', () => {
|
|
90
|
+
render(
|
|
91
|
+
<TestWrapper>
|
|
92
|
+
<DeviceToggle {...defaultProps} />
|
|
93
|
+
</TestWrapper>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const buttons = screen.getAllByTestId('cap-button');
|
|
97
|
+
const androidButton = buttons.find(btn => btn.textContent.includes('Android'));
|
|
98
|
+
const iosButton = buttons.find(btn => btn.textContent.includes('iOS'));
|
|
99
|
+
|
|
100
|
+
expect(androidButton).toHaveAttribute('data-type', 'primary');
|
|
101
|
+
expect(iosButton).toHaveAttribute('data-type', 'default');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('shows iOS as active when specified', () => {
|
|
105
|
+
render(
|
|
106
|
+
<TestWrapper>
|
|
107
|
+
<DeviceToggle {...defaultProps} activeDevice={DEVICE_TYPES.IOS} />
|
|
108
|
+
</TestWrapper>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const buttons = screen.getAllByTestId('cap-button');
|
|
112
|
+
const androidButton = buttons.find(btn => btn.textContent.includes('Android'));
|
|
113
|
+
const iosButton = buttons.find(btn => btn.textContent.includes('iOS'));
|
|
114
|
+
|
|
115
|
+
expect(androidButton).toHaveAttribute('data-type', 'default');
|
|
116
|
+
expect(iosButton).toHaveAttribute('data-type', 'primary');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('calls onDeviceChange when Android button is clicked', () => {
|
|
120
|
+
const onDeviceChange = jest.fn();
|
|
121
|
+
|
|
122
|
+
render(
|
|
123
|
+
<TestWrapper>
|
|
124
|
+
<DeviceToggle
|
|
125
|
+
{...defaultProps}
|
|
126
|
+
activeDevice={DEVICE_TYPES.IOS}
|
|
127
|
+
onDeviceChange={onDeviceChange}
|
|
128
|
+
/>
|
|
129
|
+
</TestWrapper>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const buttons = screen.getAllByTestId('cap-button');
|
|
133
|
+
const androidButton = buttons.find(btn => btn.textContent.includes('Android'));
|
|
134
|
+
|
|
135
|
+
fireEvent.click(androidButton);
|
|
136
|
+
|
|
137
|
+
expect(onDeviceChange).toHaveBeenCalledWith(DEVICE_TYPES.ANDROID);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('calls onDeviceChange when iOS button is clicked', () => {
|
|
141
|
+
const onDeviceChange = jest.fn();
|
|
142
|
+
|
|
143
|
+
render(
|
|
144
|
+
<TestWrapper>
|
|
145
|
+
<DeviceToggle
|
|
146
|
+
{...defaultProps}
|
|
147
|
+
activeDevice={DEVICE_TYPES.ANDROID}
|
|
148
|
+
onDeviceChange={onDeviceChange}
|
|
149
|
+
/>
|
|
150
|
+
</TestWrapper>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const buttons = screen.getAllByTestId('cap-button');
|
|
154
|
+
const iosButton = buttons.find(btn => btn.textContent.includes('iOS'));
|
|
155
|
+
|
|
156
|
+
fireEvent.click(iosButton);
|
|
157
|
+
|
|
158
|
+
expect(onDeviceChange).toHaveBeenCalledWith(DEVICE_TYPES.IOS);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('does not call onDeviceChange when clicking already active device', () => {
|
|
162
|
+
const onDeviceChange = jest.fn();
|
|
163
|
+
|
|
164
|
+
render(
|
|
165
|
+
<TestWrapper>
|
|
166
|
+
<DeviceToggle
|
|
167
|
+
{...defaultProps}
|
|
168
|
+
activeDevice={DEVICE_TYPES.ANDROID}
|
|
169
|
+
onDeviceChange={onDeviceChange}
|
|
170
|
+
/>
|
|
171
|
+
</TestWrapper>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const buttons = screen.getAllByTestId('cap-button');
|
|
175
|
+
const androidButton = buttons.find(btn => btn.textContent.includes('Android'));
|
|
176
|
+
|
|
177
|
+
fireEvent.click(androidButton);
|
|
178
|
+
|
|
179
|
+
expect(onDeviceChange).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('renders checkbox in checked state when keepContentSame is true', () => {
|
|
183
|
+
render(
|
|
184
|
+
<TestWrapper>
|
|
185
|
+
<DeviceToggle {...defaultProps} keepContentSame={true} />
|
|
186
|
+
</TestWrapper>
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const checkbox = screen.getByTestId('cap-checkbox');
|
|
190
|
+
expect(checkbox).toBeChecked();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('renders checkbox in unchecked state when keepContentSame is false', () => {
|
|
194
|
+
render(
|
|
195
|
+
<TestWrapper>
|
|
196
|
+
<DeviceToggle {...defaultProps} keepContentSame={false} />
|
|
197
|
+
</TestWrapper>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const checkbox = screen.getByTestId('cap-checkbox');
|
|
201
|
+
expect(checkbox).not.toBeChecked();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('calls onKeepContentSameChange when checkbox is clicked', () => {
|
|
205
|
+
const onKeepContentSameChange = jest.fn();
|
|
206
|
+
|
|
207
|
+
render(
|
|
208
|
+
<TestWrapper>
|
|
209
|
+
<DeviceToggle
|
|
210
|
+
{...defaultProps}
|
|
211
|
+
onKeepContentSameChange={onKeepContentSameChange}
|
|
212
|
+
/>
|
|
213
|
+
</TestWrapper>
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
const checkbox = screen.getByTestId('cap-checkbox');
|
|
217
|
+
fireEvent.click(checkbox);
|
|
218
|
+
|
|
219
|
+
expect(onKeepContentSameChange).toHaveBeenCalledWith(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('handles checkbox change with boolean value', () => {
|
|
223
|
+
const onKeepContentSameChange = jest.fn();
|
|
224
|
+
|
|
225
|
+
render(
|
|
226
|
+
<TestWrapper>
|
|
227
|
+
<DeviceToggle
|
|
228
|
+
{...defaultProps}
|
|
229
|
+
keepContentSame={true}
|
|
230
|
+
onKeepContentSameChange={onKeepContentSameChange}
|
|
231
|
+
/>
|
|
232
|
+
</TestWrapper>
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const checkbox = screen.getByTestId('cap-checkbox');
|
|
236
|
+
|
|
237
|
+
// Simulate direct boolean value (as might come from Cap UI)
|
|
238
|
+
fireEvent.click(checkbox);
|
|
239
|
+
|
|
240
|
+
expect(onKeepContentSameChange).toHaveBeenCalledWith(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('renders device icons correctly', () => {
|
|
244
|
+
render(
|
|
245
|
+
<TestWrapper>
|
|
246
|
+
<DeviceToggle {...defaultProps} />
|
|
247
|
+
</TestWrapper>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const icons = screen.getAllByTestId('cap-icon');
|
|
251
|
+
expect(icons).toHaveLength(2);
|
|
252
|
+
|
|
253
|
+
expect(icons[0]).toHaveAttribute('data-type', 'android');
|
|
254
|
+
expect(icons[1]).toHaveAttribute('data-type', 'ios');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('applies custom className', () => {
|
|
258
|
+
const { container } = render(
|
|
259
|
+
<TestWrapper>
|
|
260
|
+
<DeviceToggle {...defaultProps} className="custom-class" />
|
|
261
|
+
</TestWrapper>
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(container.firstChild).toHaveClass('device-toggle', 'custom-class');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('handles missing callback functions gracefully', () => {
|
|
268
|
+
render(
|
|
269
|
+
<TestWrapper>
|
|
270
|
+
<DeviceToggle
|
|
271
|
+
activeDevice={DEVICE_TYPES.ANDROID}
|
|
272
|
+
keepContentSame={false}
|
|
273
|
+
/>
|
|
274
|
+
</TestWrapper>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
const buttons = screen.getAllByTestId('cap-button');
|
|
278
|
+
const iosButton = buttons.find(btn => btn.textContent.includes('iOS'));
|
|
279
|
+
const checkbox = screen.getByTestId('cap-checkbox');
|
|
280
|
+
|
|
281
|
+
// Should not throw errors
|
|
282
|
+
fireEvent.click(iosButton);
|
|
283
|
+
fireEvent.change(checkbox, { target: { checked: true } });
|
|
284
|
+
|
|
285
|
+
expect(screen.getByText('iOS')).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('renders with proper ARIA attributes', () => {
|
|
289
|
+
render(
|
|
290
|
+
<TestWrapper>
|
|
291
|
+
<DeviceToggle {...defaultProps} />
|
|
292
|
+
</TestWrapper>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const deviceToggle = screen.getByText('Android').closest('.device-toggle');
|
|
296
|
+
expect(deviceToggle).toBeInTheDocument();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('maintains proper tab order', () => {
|
|
300
|
+
render(
|
|
301
|
+
<TestWrapper>
|
|
302
|
+
<DeviceToggle {...defaultProps} />
|
|
303
|
+
</TestWrapper>
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const buttons = screen.getAllByTestId('cap-button');
|
|
307
|
+
const checkbox = screen.getByTestId('cap-checkbox');
|
|
308
|
+
|
|
309
|
+
// All interactive elements should be in the DOM
|
|
310
|
+
expect(buttons).toHaveLength(2);
|
|
311
|
+
expect(checkbox).toBeInTheDocument();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|