@capillarytech/creatives-library 8.0.208 → 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/config/app.js +1 -2
- package/package.json +16 -2
- package/services/api.js +0 -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/CreativesContainer/SlideBoxContent.js +0 -2
- 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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeviceFrame Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the DeviceFrame component
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render, screen } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import { DEVICE_TYPES } from '../../../constants';
|
|
11
|
+
|
|
12
|
+
// Mock the entire DeviceFrame module to bypass image import issues
|
|
13
|
+
jest.mock('../../../../assets/Android.png', () => 'mocked-android-frame.png');
|
|
14
|
+
jest.mock('../../../../assets/iOS.png', () => 'mocked-ios-frame.png');
|
|
15
|
+
|
|
16
|
+
// Import after mocking
|
|
17
|
+
import DeviceFrame from '../DeviceFrame';
|
|
18
|
+
|
|
19
|
+
describe('DeviceFrame', () => {
|
|
20
|
+
describe('Basic Rendering', () => {
|
|
21
|
+
it('renders without crashing', () => {
|
|
22
|
+
const { container } = render(<DeviceFrame />);
|
|
23
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders with default props', () => {
|
|
27
|
+
const { container } = render(<DeviceFrame />);
|
|
28
|
+
const frame = container.firstChild;
|
|
29
|
+
|
|
30
|
+
expect(frame).toHaveClass('device-frame');
|
|
31
|
+
expect(frame).toHaveClass('device-frame--android'); // Default device
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders children correctly', () => {
|
|
35
|
+
render(
|
|
36
|
+
<DeviceFrame>
|
|
37
|
+
<div data-testid="child-content">Test Content</div>
|
|
38
|
+
</DeviceFrame>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByTestId('child-content')).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByTestId('child-content')).toHaveTextContent('Test Content');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders multiple children', () => {
|
|
46
|
+
render(
|
|
47
|
+
<DeviceFrame>
|
|
48
|
+
<div data-testid="child-1">Child 1</div>
|
|
49
|
+
<div data-testid="child-2">Child 2</div>
|
|
50
|
+
<div data-testid="child-3">Child 3</div>
|
|
51
|
+
</DeviceFrame>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(screen.getByTestId('child-1')).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByTestId('child-2')).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByTestId('child-3')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Device Types', () => {
|
|
61
|
+
it('renders Android frame correctly', () => {
|
|
62
|
+
const { container } = render(<DeviceFrame device={DEVICE_TYPES.ANDROID} />);
|
|
63
|
+
const frame = container.firstChild;
|
|
64
|
+
|
|
65
|
+
expect(frame).toHaveClass('device-frame--android');
|
|
66
|
+
expect(frame.style.backgroundImage).toBeTruthy();
|
|
67
|
+
expect(frame.style.backgroundImage).toMatch(/url\(/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders iOS frame correctly', () => {
|
|
71
|
+
const { container } = render(<DeviceFrame device={DEVICE_TYPES.IOS} />);
|
|
72
|
+
const frame = container.firstChild;
|
|
73
|
+
|
|
74
|
+
expect(frame).toHaveClass('device-frame--ios');
|
|
75
|
+
expect(frame.style.backgroundImage).toBeTruthy();
|
|
76
|
+
expect(frame.style.backgroundImage).toMatch(/url\(/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('defaults to Android when no device specified', () => {
|
|
80
|
+
const { container } = render(<DeviceFrame />);
|
|
81
|
+
const frame = container.firstChild;
|
|
82
|
+
|
|
83
|
+
expect(frame).toHaveClass('device-frame--android');
|
|
84
|
+
expect(frame.style.backgroundImage).toBeTruthy();
|
|
85
|
+
expect(frame.style.backgroundImage).toMatch(/url\(/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('handles device type case insensitivity', () => {
|
|
89
|
+
const { container: androidContainer } = render(
|
|
90
|
+
<DeviceFrame device={DEVICE_TYPES.ANDROID} />
|
|
91
|
+
);
|
|
92
|
+
expect(androidContainer.firstChild).toHaveClass('device-frame--android');
|
|
93
|
+
|
|
94
|
+
const { container: iosContainer } = render(
|
|
95
|
+
<DeviceFrame device={DEVICE_TYPES.IOS} />
|
|
96
|
+
);
|
|
97
|
+
expect(iosContainer.firstChild).toHaveClass('device-frame--ios');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Styling', () => {
|
|
102
|
+
it('applies correct dimensions', () => {
|
|
103
|
+
const { container } = render(<DeviceFrame />);
|
|
104
|
+
const frame = container.firstChild;
|
|
105
|
+
|
|
106
|
+
expect(frame.style.width).toBe('450px');
|
|
107
|
+
expect(frame.style.height).toBe('920px');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('applies unified dimensions for both devices', () => {
|
|
111
|
+
const { container: androidContainer } = render(
|
|
112
|
+
<DeviceFrame device={DEVICE_TYPES.ANDROID} />
|
|
113
|
+
);
|
|
114
|
+
const androidFrame = androidContainer.firstChild;
|
|
115
|
+
|
|
116
|
+
const { container: iosContainer } = render(
|
|
117
|
+
<DeviceFrame device={DEVICE_TYPES.IOS} />
|
|
118
|
+
);
|
|
119
|
+
const iosFrame = iosContainer.firstChild;
|
|
120
|
+
|
|
121
|
+
expect(androidFrame.style.width).toBe(iosFrame.style.width);
|
|
122
|
+
expect(androidFrame.style.height).toBe(iosFrame.style.height);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('applies background styles correctly', () => {
|
|
126
|
+
const { container } = render(<DeviceFrame />);
|
|
127
|
+
const frame = container.firstChild;
|
|
128
|
+
|
|
129
|
+
expect(frame.style.position).toBe('relative');
|
|
130
|
+
expect(frame.style.display).toBe('inline-block');
|
|
131
|
+
expect(frame.style.backgroundRepeat).toBe('no-repeat');
|
|
132
|
+
expect(frame.style.backgroundPosition).toBe('center');
|
|
133
|
+
expect(frame.style.backgroundSize).toBe('contain');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('applies drop-shadow filter', () => {
|
|
137
|
+
const { container } = render(<DeviceFrame />);
|
|
138
|
+
const frame = container.firstChild;
|
|
139
|
+
|
|
140
|
+
expect(frame.style.filter).toContain('drop-shadow');
|
|
141
|
+
expect(frame.style.filter).toContain('brightness');
|
|
142
|
+
expect(frame.style.filter).toContain('contrast');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('sets correct background image for Android', () => {
|
|
146
|
+
const { container } = render(<DeviceFrame device={DEVICE_TYPES.ANDROID} />);
|
|
147
|
+
const frame = container.firstChild;
|
|
148
|
+
|
|
149
|
+
// Verify background image is set (actual mock value may vary)
|
|
150
|
+
expect(frame.style.backgroundImage).toBeTruthy();
|
|
151
|
+
expect(frame.style.backgroundImage).toMatch(/^url\(.+\)$/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('sets correct background image for iOS', () => {
|
|
155
|
+
const { container } = render(<DeviceFrame device={DEVICE_TYPES.IOS} />);
|
|
156
|
+
const frame = container.firstChild;
|
|
157
|
+
|
|
158
|
+
// Verify background image is set (actual mock value may vary)
|
|
159
|
+
expect(frame.style.backgroundImage).toBeTruthy();
|
|
160
|
+
expect(frame.style.backgroundImage).toMatch(/^url\(.+\)$/);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('Custom Props', () => {
|
|
165
|
+
it('accepts and applies custom className', () => {
|
|
166
|
+
const { container } = render(<DeviceFrame className="custom-frame" />);
|
|
167
|
+
const frame = container.firstChild;
|
|
168
|
+
|
|
169
|
+
expect(frame).toHaveClass('custom-frame');
|
|
170
|
+
expect(frame).toHaveClass('device-frame'); // Base class still present
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('merges multiple classNames correctly', () => {
|
|
174
|
+
const { container } = render(
|
|
175
|
+
<DeviceFrame className="custom-class another-class" />
|
|
176
|
+
);
|
|
177
|
+
const frame = container.firstChild;
|
|
178
|
+
|
|
179
|
+
expect(frame).toHaveClass('custom-class');
|
|
180
|
+
expect(frame).toHaveClass('another-class');
|
|
181
|
+
expect(frame).toHaveClass('device-frame');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('forwards additional props to container', () => {
|
|
185
|
+
const { container } = render(
|
|
186
|
+
<DeviceFrame data-testid="custom-frame" aria-label="Device Preview" />
|
|
187
|
+
);
|
|
188
|
+
const frame = container.firstChild;
|
|
189
|
+
|
|
190
|
+
expect(frame).toHaveAttribute('data-testid', 'custom-frame');
|
|
191
|
+
expect(frame).toHaveAttribute('aria-label', 'Device Preview');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('handles onClick event', () => {
|
|
195
|
+
const handleClick = jest.fn();
|
|
196
|
+
const { container } = render(<DeviceFrame onClick={handleClick} />);
|
|
197
|
+
const frame = container.firstChild;
|
|
198
|
+
|
|
199
|
+
frame.click();
|
|
200
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('handles custom styles via props', () => {
|
|
204
|
+
const customStyle = { border: '2px solid red' };
|
|
205
|
+
const { container } = render(<DeviceFrame style={customStyle} />);
|
|
206
|
+
const frame = container.firstChild;
|
|
207
|
+
|
|
208
|
+
// Note: inline styles from props override component styles
|
|
209
|
+
expect(frame.style.border).toBe('2px solid red');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('PropTypes Validation', () => {
|
|
214
|
+
// Suppress console errors for these tests
|
|
215
|
+
const originalError = console.error;
|
|
216
|
+
beforeAll(() => {
|
|
217
|
+
console.error = jest.fn();
|
|
218
|
+
});
|
|
219
|
+
afterAll(() => {
|
|
220
|
+
console.error = originalError;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('accepts valid device types', () => {
|
|
224
|
+
expect(() => {
|
|
225
|
+
render(<DeviceFrame device={DEVICE_TYPES.ANDROID} />);
|
|
226
|
+
}).not.toThrow();
|
|
227
|
+
|
|
228
|
+
expect(() => {
|
|
229
|
+
render(<DeviceFrame device={DEVICE_TYPES.IOS} />);
|
|
230
|
+
}).not.toThrow();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('accepts string className', () => {
|
|
234
|
+
expect(() => {
|
|
235
|
+
render(<DeviceFrame className="test-class" />);
|
|
236
|
+
}).not.toThrow();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('accepts valid children types', () => {
|
|
240
|
+
expect(() => {
|
|
241
|
+
render(<DeviceFrame><div>Test</div></DeviceFrame>);
|
|
242
|
+
}).not.toThrow();
|
|
243
|
+
|
|
244
|
+
expect(() => {
|
|
245
|
+
render(<DeviceFrame>Plain text</DeviceFrame>);
|
|
246
|
+
}).not.toThrow();
|
|
247
|
+
|
|
248
|
+
expect(() => {
|
|
249
|
+
render(<DeviceFrame>{null}</DeviceFrame>);
|
|
250
|
+
}).not.toThrow();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe('Edge Cases', () => {
|
|
255
|
+
it('renders without children', () => {
|
|
256
|
+
const { container } = render(<DeviceFrame />);
|
|
257
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('renders with null children', () => {
|
|
261
|
+
const { container } = render(<DeviceFrame>{null}</DeviceFrame>);
|
|
262
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('renders with undefined children', () => {
|
|
266
|
+
const { container } = render(<DeviceFrame>{undefined}</DeviceFrame>);
|
|
267
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('renders with empty string className', () => {
|
|
271
|
+
const { container } = render(<DeviceFrame className="" />);
|
|
272
|
+
const frame = container.firstChild;
|
|
273
|
+
|
|
274
|
+
expect(frame).toHaveClass('device-frame');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('renders with fragment children', () => {
|
|
278
|
+
render(
|
|
279
|
+
<DeviceFrame>
|
|
280
|
+
<>
|
|
281
|
+
<div data-testid="fragment-child-1">Child 1</div>
|
|
282
|
+
<div data-testid="fragment-child-2">Child 2</div>
|
|
283
|
+
</>
|
|
284
|
+
</DeviceFrame>
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
expect(screen.getByTestId('fragment-child-1')).toBeInTheDocument();
|
|
288
|
+
expect(screen.getByTestId('fragment-child-2')).toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('handles complex nested children', () => {
|
|
292
|
+
render(
|
|
293
|
+
<DeviceFrame>
|
|
294
|
+
<div>
|
|
295
|
+
<div>
|
|
296
|
+
<div data-testid="nested-child">Deeply Nested</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</DeviceFrame>
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(screen.getByTestId('nested-child')).toBeInTheDocument();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Component Structure', () => {
|
|
307
|
+
it('renders a single root div element', () => {
|
|
308
|
+
const { container } = render(<DeviceFrame />);
|
|
309
|
+
|
|
310
|
+
expect(container.firstChild.tagName).toBe('DIV');
|
|
311
|
+
expect(container.children).toHaveLength(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('maintains consistent class structure', () => {
|
|
315
|
+
const { container: androidContainer } = render(
|
|
316
|
+
<DeviceFrame device={DEVICE_TYPES.ANDROID} className="custom" />
|
|
317
|
+
);
|
|
318
|
+
const androidFrame = androidContainer.firstChild;
|
|
319
|
+
|
|
320
|
+
const classes = androidFrame.className.split(' ').filter(c => c);
|
|
321
|
+
expect(classes).toContain('device-frame');
|
|
322
|
+
expect(classes).toContain('device-frame--android');
|
|
323
|
+
expect(classes).toContain('custom');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('applies styles as inline styles object', () => {
|
|
327
|
+
const { container } = render(<DeviceFrame />);
|
|
328
|
+
const frame = container.firstChild;
|
|
329
|
+
|
|
330
|
+
// Verify style is an object
|
|
331
|
+
expect(typeof frame.style).toBe('object');
|
|
332
|
+
expect(frame.style).not.toBeNull();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('Accessibility', () => {
|
|
337
|
+
it('can receive accessibility props', () => {
|
|
338
|
+
const { container } = render(
|
|
339
|
+
<DeviceFrame
|
|
340
|
+
role="img"
|
|
341
|
+
aria-label="Mobile Device Preview"
|
|
342
|
+
aria-describedby="device-description"
|
|
343
|
+
/>
|
|
344
|
+
);
|
|
345
|
+
const frame = container.firstChild;
|
|
346
|
+
|
|
347
|
+
expect(frame).toHaveAttribute('role', 'img');
|
|
348
|
+
expect(frame).toHaveAttribute('aria-label', 'Mobile Device Preview');
|
|
349
|
+
expect(frame).toHaveAttribute('aria-describedby', 'device-description');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('allows custom data attributes', () => {
|
|
353
|
+
const { container } = render(
|
|
354
|
+
<DeviceFrame
|
|
355
|
+
data-device="android"
|
|
356
|
+
data-testid="device-frame"
|
|
357
|
+
data-analytics="preview-frame"
|
|
358
|
+
/>
|
|
359
|
+
);
|
|
360
|
+
const frame = container.firstChild;
|
|
361
|
+
|
|
362
|
+
expect(frame).toHaveAttribute('data-device', 'android');
|
|
363
|
+
expect(frame).toHaveAttribute('data-testid', 'device-frame');
|
|
364
|
+
expect(frame).toHaveAttribute('data-analytics', 'preview-frame');
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
describe('Integration Scenarios', () => {
|
|
369
|
+
it('works with ContentOverlay-style content', () => {
|
|
370
|
+
render(
|
|
371
|
+
<DeviceFrame device={DEVICE_TYPES.IOS}>
|
|
372
|
+
<div style={{ width: '100%', height: '100%' }}>
|
|
373
|
+
<iframe
|
|
374
|
+
title="preview"
|
|
375
|
+
srcDoc="<p>Preview Content</p>"
|
|
376
|
+
data-testid="preview-iframe"
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
</DeviceFrame>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(screen.getByTestId('preview-iframe')).toBeInTheDocument();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('maintains structure when device changes', () => {
|
|
386
|
+
const { container, rerender } = render(
|
|
387
|
+
<DeviceFrame device={DEVICE_TYPES.ANDROID}>
|
|
388
|
+
<div data-testid="content">Content</div>
|
|
389
|
+
</DeviceFrame>
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
expect(container.firstChild).toHaveClass('device-frame--android');
|
|
393
|
+
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
394
|
+
|
|
395
|
+
rerender(
|
|
396
|
+
<DeviceFrame device={DEVICE_TYPES.IOS}>
|
|
397
|
+
<div data-testid="content">Content</div>
|
|
398
|
+
</DeviceFrame>
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
expect(container.firstChild).toHaveClass('device-frame--ios');
|
|
402
|
+
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('handles dynamic children updates', () => {
|
|
406
|
+
const { rerender } = render(
|
|
407
|
+
<DeviceFrame>
|
|
408
|
+
<div data-testid="dynamic-content">Initial</div>
|
|
409
|
+
</DeviceFrame>
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
expect(screen.getByTestId('dynamic-content')).toHaveTextContent('Initial');
|
|
413
|
+
|
|
414
|
+
rerender(
|
|
415
|
+
<DeviceFrame>
|
|
416
|
+
<div data-testid="dynamic-content">Updated</div>
|
|
417
|
+
</DeviceFrame>
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(screen.getByTestId('dynamic-content')).toHaveTextContent('Updated');
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutSelector Component Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for the LayoutSelector component that provides layout type selection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { render, screen, fireEvent, within } from '@testing-library/react';
|
|
9
|
+
import '@testing-library/jest-dom';
|
|
10
|
+
import LayoutSelector from '../LayoutSelector';
|
|
11
|
+
import { LAYOUT_TYPES, LAYOUT_OPTIONS } from '../constants';
|
|
12
|
+
|
|
13
|
+
describe('LayoutSelector', () => {
|
|
14
|
+
const defaultProps = {
|
|
15
|
+
value: LAYOUT_TYPES.MODAL,
|
|
16
|
+
onChange: jest.fn()
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
jest.clearAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Rendering', () => {
|
|
24
|
+
it('renders without crashing', () => {
|
|
25
|
+
const { container } = render(<LayoutSelector {...defaultProps} />);
|
|
26
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders all layout option labels', () => {
|
|
30
|
+
render(<LayoutSelector {...defaultProps} />);
|
|
31
|
+
|
|
32
|
+
expect(screen.getByText('Modal')).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText('Top Banner')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Bottom Banner')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Fullscreen')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('renders with custom className', () => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<LayoutSelector {...defaultProps} className="custom-layout-selector" />
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const selector = container.querySelector('.layout-selector');
|
|
44
|
+
expect(selector).toHaveClass('custom-layout-selector');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('applies base layout-selector class', () => {
|
|
48
|
+
const { container } = render(<LayoutSelector {...defaultProps} />);
|
|
49
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders radio group with proper structure', () => {
|
|
53
|
+
const { container } = render(<LayoutSelector {...defaultProps} />);
|
|
54
|
+
|
|
55
|
+
const radioGroup = container.querySelector('.layout-selector__radio-group');
|
|
56
|
+
expect(radioGroup).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Value Selection', () => {
|
|
61
|
+
it('renders with MODAL value by default', () => {
|
|
62
|
+
render(<LayoutSelector onChange={defaultProps.onChange} />);
|
|
63
|
+
|
|
64
|
+
// Component should render without error
|
|
65
|
+
expect(screen.getByText('Modal')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('accepts HEADER value', () => {
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<LayoutSelector value={LAYOUT_TYPES.HEADER} onChange={defaultProps.onChange} />
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('accepts FOOTER value', () => {
|
|
77
|
+
const { container } = render(
|
|
78
|
+
<LayoutSelector value={LAYOUT_TYPES.FOOTER} onChange={defaultProps.onChange} />
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('accepts FULLSCREEN value', () => {
|
|
85
|
+
const { container } = render(
|
|
86
|
+
<LayoutSelector value={LAYOUT_TYPES.FULLSCREEN} onChange={defaultProps.onChange} />
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('User Interactions', () => {
|
|
94
|
+
it('calls onChange when a radio button is clicked', () => {
|
|
95
|
+
const handleChange = jest.fn();
|
|
96
|
+
const { container } = render(
|
|
97
|
+
<LayoutSelector value={LAYOUT_TYPES.MODAL} onChange={handleChange} />
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Find all radio inputs in the component
|
|
101
|
+
const radioInputs = container.querySelectorAll('input[type="radio"]');
|
|
102
|
+
|
|
103
|
+
// Should have 4 radio buttons (one for each layout type)
|
|
104
|
+
expect(radioInputs.length).toBe(4);
|
|
105
|
+
|
|
106
|
+
// Click the second radio button (Header/Top Banner)
|
|
107
|
+
if (radioInputs[1]) {
|
|
108
|
+
fireEvent.click(radioInputs[1]);
|
|
109
|
+
expect(handleChange).toHaveBeenCalled();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('handles missing onChange gracefully', () => {
|
|
114
|
+
// Should not throw error when onChange is not provided
|
|
115
|
+
expect(() => {
|
|
116
|
+
render(<LayoutSelector value={LAYOUT_TYPES.MODAL} />);
|
|
117
|
+
}).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Layout Options', () => {
|
|
122
|
+
it('renders correct number of options', () => {
|
|
123
|
+
const { container } = render(<LayoutSelector {...defaultProps} />);
|
|
124
|
+
|
|
125
|
+
// Should render 4 layout options
|
|
126
|
+
const radioInputs = container.querySelectorAll('input[type="radio"]');
|
|
127
|
+
expect(radioInputs.length).toBe(LAYOUT_OPTIONS.length);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('renders each layout option label', () => {
|
|
131
|
+
render(<LayoutSelector {...defaultProps} />);
|
|
132
|
+
|
|
133
|
+
LAYOUT_OPTIONS.forEach(option => {
|
|
134
|
+
expect(screen.getByText(option.label)).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('CSS Classes', () => {
|
|
140
|
+
it('applies radio group class', () => {
|
|
141
|
+
const { container } = render(<LayoutSelector {...defaultProps} />);
|
|
142
|
+
|
|
143
|
+
const radioGroup = container.querySelector('.layout-selector__radio-group');
|
|
144
|
+
expect(radioGroup).toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('combines custom className with base class', () => {
|
|
148
|
+
const { container } = render(
|
|
149
|
+
<LayoutSelector {...defaultProps} className="my-custom-class" />
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const selector = container.querySelector('.layout-selector');
|
|
153
|
+
expect(selector).toHaveClass('layout-selector');
|
|
154
|
+
expect(selector).toHaveClass('my-custom-class');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('Props Spreading', () => {
|
|
159
|
+
it('spreads additional props to container', () => {
|
|
160
|
+
const { container } = render(
|
|
161
|
+
<LayoutSelector
|
|
162
|
+
{...defaultProps}
|
|
163
|
+
data-custom-attr="test-value"
|
|
164
|
+
id="custom-id"
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const selector = container.querySelector('.layout-selector');
|
|
169
|
+
expect(selector).toHaveAttribute('data-custom-attr', 'test-value');
|
|
170
|
+
expect(selector).toHaveAttribute('id', 'custom-id');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('PropTypes Validation', () => {
|
|
175
|
+
it('accepts all valid layout type values', () => {
|
|
176
|
+
const validValues = Object.values(LAYOUT_TYPES);
|
|
177
|
+
|
|
178
|
+
validValues.forEach(value => {
|
|
179
|
+
expect(() => {
|
|
180
|
+
render(<LayoutSelector value={value} onChange={defaultProps.onChange} />);
|
|
181
|
+
}).not.toThrow();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('accepts all valid size values', () => {
|
|
186
|
+
const validSizes = ['small', 'middle', 'large'];
|
|
187
|
+
|
|
188
|
+
validSizes.forEach(size => {
|
|
189
|
+
expect(() => {
|
|
190
|
+
render(<LayoutSelector {...defaultProps} size={size} />);
|
|
191
|
+
}).not.toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Integration', () => {
|
|
197
|
+
it('maintains structure across re-renders', () => {
|
|
198
|
+
const { container, rerender } = render(
|
|
199
|
+
<LayoutSelector value={LAYOUT_TYPES.MODAL} onChange={defaultProps.onChange} />
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
203
|
+
|
|
204
|
+
rerender(<LayoutSelector value={LAYOUT_TYPES.HEADER} onChange={defaultProps.onChange} />);
|
|
205
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
206
|
+
|
|
207
|
+
rerender(<LayoutSelector value={LAYOUT_TYPES.FULLSCREEN} onChange={defaultProps.onChange} />);
|
|
208
|
+
expect(container.querySelector('.layout-selector')).toBeInTheDocument();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('works with controlled component pattern', () => {
|
|
212
|
+
const ControlledComponent = () => {
|
|
213
|
+
const [value, setValue] = React.useState(LAYOUT_TYPES.MODAL);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div>
|
|
217
|
+
<div data-testid="current-value">{value}</div>
|
|
218
|
+
<LayoutSelector value={value} onChange={setValue} />
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
render(<ControlledComponent />);
|
|
224
|
+
|
|
225
|
+
expect(screen.getByTestId('current-value')).toHaveTextContent(LAYOUT_TYPES.MODAL);
|
|
226
|
+
expect(screen.getByText('Modal')).toBeInTheDocument();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('Component Structure', () => {
|
|
231
|
+
it('renders with proper DOM hierarchy', () => {
|
|
232
|
+
const { container } = render(<LayoutSelector {...defaultProps} />);
|
|
233
|
+
|
|
234
|
+
const selector = container.querySelector('.layout-selector');
|
|
235
|
+
expect(selector).toBeInTheDocument();
|
|
236
|
+
|
|
237
|
+
const radioGroup = selector.querySelector('.layout-selector__radio-group');
|
|
238
|
+
expect(radioGroup).toBeInTheDocument();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('contains radio inputs', () => {
|
|
242
|
+
const { container } = render(<LayoutSelector {...defaultProps} />);
|
|
243
|
+
|
|
244
|
+
const radioInputs = container.querySelectorAll('input[type="radio"]');
|
|
245
|
+
expect(radioInputs.length).toBeGreaterThan(0);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|