@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.
Files changed (77) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/package.json +16 -2
  4. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  6. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  8. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  10. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  11. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  12. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  14. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  15. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  19. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  20. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  21. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  29. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  30. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  31. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  32. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  35. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  36. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  38. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  39. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  42. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  43. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  44. package/v2Components/HtmlEditor/constants.js +241 -0
  45. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  46. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  52. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  53. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  54. package/v2Components/HtmlEditor/index.js +29 -0
  55. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  56. package/v2Components/HtmlEditor/messages.js +389 -0
  57. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  58. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  63. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  64. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  65. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  66. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  67. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  69. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  70. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  71. package/v2Containers/EmailWrapper/index.js +8 -1
  72. package/v2Containers/Templates/constants.js +8 -0
  73. package/v2Containers/Templates/index.js +56 -28
  74. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  75. package/v2Containers/Whatsapp/constants.js +26 -2
  76. package/v2Containers/Whatsapp/index.js +4 -1
  77. 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
+