@solucx/react-native-solucx-widget 0.1.15 → 0.2.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.
Files changed (46) hide show
  1. package/README.intern.md +513 -513
  2. package/README.md +285 -285
  3. package/package.json +50 -23
  4. package/src/SoluCXWidget.tsx +172 -119
  5. package/src/__mocks__/expo-modules-core-web.js +16 -0
  6. package/src/__mocks__/expo-modules-core.js +33 -0
  7. package/src/__tests__/ClientVersionCollector.test.ts +55 -0
  8. package/src/__tests__/CloseButton.test.tsx +47 -0
  9. package/src/__tests__/Constants.test.ts +17 -0
  10. package/src/__tests__/InlineWidget.rendering.test.tsx +81 -0
  11. package/src/__tests__/ModalWidget.rendering.test.tsx +157 -0
  12. package/src/__tests__/OverlayWidget.rendering.test.tsx +123 -0
  13. package/src/__tests__/SoluCXWidget.rendering.test.tsx +315 -0
  14. package/src/__tests__/e2e/widget-lifecycle.test.tsx +353 -0
  15. package/src/__tests__/integration/webview-communication-simple.test.tsx +147 -0
  16. package/src/__tests__/integration/webview-communication.test.tsx +417 -0
  17. package/src/__tests__/urlUtils.test.ts +56 -56
  18. package/src/__tests__/useDeviceInfoCollector.test.ts +109 -0
  19. package/src/__tests__/useWidgetState.test.ts +181 -189
  20. package/src/__tests__/widgetBootstrapService.test.ts +182 -0
  21. package/src/components/CloseButton.tsx +36 -36
  22. package/src/components/InlineWidget.tsx +36 -36
  23. package/src/components/ModalWidget.tsx +57 -59
  24. package/src/components/OverlayWidget.tsx +88 -88
  25. package/src/constants/Constants.ts +4 -0
  26. package/src/constants/webViewConstants.ts +15 -14
  27. package/src/hooks/index.ts +2 -2
  28. package/src/hooks/useDeviceInfoCollector.ts +67 -0
  29. package/src/hooks/useHeightAnimation.ts +22 -22
  30. package/src/hooks/useWidgetHeight.ts +38 -38
  31. package/src/hooks/useWidgetState.ts +101 -101
  32. package/src/index.ts +12 -8
  33. package/src/interfaces/WidgetCallbacks.ts +15 -0
  34. package/src/interfaces/WidgetData.ts +19 -19
  35. package/src/interfaces/WidgetOptions.ts +7 -7
  36. package/src/interfaces/WidgetResponse.ts +15 -15
  37. package/src/interfaces/WidgetSamplerLog.ts +5 -5
  38. package/src/interfaces/index.ts +25 -24
  39. package/src/services/ClientVersionCollector.ts +15 -0
  40. package/src/services/storage.ts +21 -21
  41. package/src/services/widgetBootstrapService.ts +67 -0
  42. package/src/services/widgetEventService.ts +110 -111
  43. package/src/services/widgetValidationService.ts +102 -86
  44. package/src/setupTests.js +43 -0
  45. package/src/styles/widgetStyles.ts +58 -58
  46. package/src/utils/urlUtils.ts +13 -13
@@ -0,0 +1,417 @@
1
+ /**
2
+ * INTEGRATION TEST: WebView Communication
3
+ *
4
+ * CRITICAL: Tests the core WebView message passing bridge between
5
+ * the embedded widget and React Native.
6
+ *
7
+ * Coverage Target: handleWebViewMessage, handleWebViewLoad, handleClose
8
+ * Previous Coverage: 0% (CRITICAL GAP)
9
+ *
10
+ * Strategy: Minimal mocking - use real WidgetEventService and components
11
+ */
12
+
13
+ import React from 'react';
14
+ import { render, waitFor, fireEvent } from '@testing-library/react-native';
15
+ import { SoluCXWidget } from '../../SoluCXWidget';
16
+ import type { WidgetCallbacks } from '../../interfaces';
17
+
18
+ // Mock only external dependencies we can't control
19
+ jest.mock('react-native-webview', () => {
20
+ const React = require('react');
21
+ const { View } = require('react-native');
22
+ return {
23
+ __esModule: true,
24
+ WebView: React.forwardRef((props: any, ref: any) => {
25
+ // Store ref methods for testing
26
+ React.useImperativeHandle(ref, () => ({
27
+ injectJavaScript: jest.fn((script) => {
28
+ // Simulate successful injection
29
+ if (props.onLoadEnd) props.onLoadEnd();
30
+ }),
31
+ }));
32
+
33
+ return (
34
+ <View testID="webview" {...props}>
35
+ {/* Expose onMessage handler for testing */}
36
+ {props.children}
37
+ </View>
38
+ );
39
+ }),
40
+ };
41
+ });
42
+
43
+ jest.mock('../../services/widgetBootstrapService', () => ({
44
+ requestWidgetUrl: jest.fn().mockResolvedValue('https://mock.widget.url/form123'),
45
+ }));
46
+
47
+ jest.mock('../../utils/urlUtils', () => ({
48
+ buildWidgetURL: jest.fn().mockReturnValue('https://mock.widget.url/form123'),
49
+ }));
50
+
51
+ jest.mock('../../services/widgetValidationService', () => ({
52
+ WidgetValidationService: jest.fn().mockImplementation(() => ({
53
+ shouldDisplayWidget: jest.fn().mockResolvedValue({ canDisplay: true }),
54
+ })),
55
+ }));
56
+
57
+ // Mock storage but keep it functional
58
+ const mockStorage: Record<string, string> = {};
59
+ jest.mock('@react-native-async-storage/async-storage', () => ({
60
+ __esModule: true,
61
+ default: {
62
+ getItem: jest.fn((key: string) => Promise.resolve(mockStorage[key] || null)),
63
+ setItem: jest.fn((key: string, value: string) => {
64
+ mockStorage[key] = value;
65
+ return Promise.resolve();
66
+ }),
67
+ removeItem: jest.fn((key: string) => {
68
+ delete mockStorage[key];
69
+ return Promise.resolve();
70
+ }),
71
+ },
72
+ }));
73
+
74
+ describe('Integration: WebView Communication', () => {
75
+ const baseProps = {
76
+ soluCXKey: 'test-key-123',
77
+ type: 'modal' as const,
78
+ data: {
79
+ customer_id: 'user-456',
80
+ form_id: 'form-789', // Required for form mode
81
+ },
82
+ options: {
83
+ height: 400,
84
+ },
85
+ };
86
+
87
+ beforeEach(() => {
88
+ jest.clearAllMocks();
89
+ Object.keys(mockStorage).forEach(key => delete mockStorage[key]);
90
+ });
91
+
92
+ describe('Message Handling - Form Events', () => {
93
+ it('should handle FORM_CLOSE message and call onClosed callback', async () => {
94
+ const mockOnClosed = jest.fn();
95
+ const callbacks: WidgetCallbacks = {
96
+ onClosed: mockOnClosed,
97
+ };
98
+
99
+ const { getByTestId } = render(
100
+ <SoluCXWidget {...baseProps} callbacks={callbacks} />
101
+ );
102
+
103
+ // Wait for widget to be ready
104
+ await waitFor(() => {
105
+ expect(getByTestId('webview')).toBeTruthy();
106
+ });
107
+
108
+ const webview = getByTestId('webview');
109
+
110
+ // Simulate WebView sending FORM_CLOSE message
111
+ fireEvent(webview, 'message', {
112
+ nativeEvent: { data: 'FORM_CLOSE' },
113
+ });
114
+
115
+ // Verify callback was called
116
+ await waitFor(() => {
117
+ expect(mockOnClosed).toHaveBeenCalledTimes(1);
118
+ });
119
+ }, 15000); // 15 second timeout for slower CI environments
120
+
121
+ it('should handle FORM_ERROR message with error text', async () => {
122
+ const mockOnError = jest.fn();
123
+ const callbacks: WidgetCallbacks = {
124
+ onError: mockOnError,
125
+ };
126
+
127
+ const { getByTestId } = render(
128
+ <SoluCXWidget {...baseProps} callbacks={callbacks} />
129
+ );
130
+
131
+ await waitFor(() => {
132
+ expect(getByTestId('webview')).toBeTruthy();
133
+ });
134
+
135
+ // Simulate error message with details
136
+ fireEvent(getByTestId('webview'), 'message', {
137
+ nativeEvent: { data: 'FORM_ERROR-Network timeout occurred' },
138
+ });
139
+
140
+ await waitFor(() => {
141
+ expect(mockOnError).toHaveBeenCalledWith('Network timeout occurred');
142
+ });
143
+ });
144
+
145
+ it('should handle FORM_RESIZE message and update height', async () => {
146
+ const mockOnResize = jest.fn();
147
+ const callbacks: WidgetCallbacks = {
148
+ onResize: mockOnResize,
149
+ };
150
+
151
+ const { getByTestId } = render(
152
+ <SoluCXWidget {...baseProps} callbacks={callbacks} />
153
+ );
154
+
155
+ await waitFor(() => {
156
+ expect(getByTestId('webview')).toBeTruthy();
157
+ });
158
+
159
+ // Simulate resize message
160
+ fireEvent(getByTestId('webview'), 'message', {
161
+ nativeEvent: { data: 'FORM_RESIZE-650' },
162
+ });
163
+
164
+ await waitFor(() => {
165
+ expect(mockOnResize).toHaveBeenCalledWith('650');
166
+ });
167
+ });
168
+
169
+ it('should handle FORM_COMPLETED message with userId', async () => {
170
+ const mockOnPartialCompleted = jest.fn();
171
+ const callbacks: WidgetCallbacks = {
172
+ onPartialCompleted: mockOnPartialCompleted,
173
+ };
174
+
175
+ const { getByTestId } = render(
176
+ <SoluCXWidget {...baseProps} callbacks={callbacks} />
177
+ );
178
+
179
+ await waitFor(() => {
180
+ expect(getByTestId('webview')).toBeTruthy();
181
+ });
182
+
183
+ // Simulate form completion
184
+ fireEvent(getByTestId('webview'), 'message', {
185
+ nativeEvent: { data: 'FORM_COMPLETED' },
186
+ });
187
+
188
+ await waitFor(() => {
189
+ expect(mockOnPartialCompleted).toHaveBeenCalledWith(expect.any(String));
190
+ });
191
+ });
192
+
193
+ it('should handle FORM_PAGECHANGED message', async () => {
194
+ const mockOnPageChanged = jest.fn();
195
+ const callbacks: WidgetCallbacks = {
196
+ onPageChanged: mockOnPageChanged,
197
+ };
198
+
199
+ const { getByTestId } = render(
200
+ <SoluCXWidget {...baseProps} callbacks={callbacks} />
201
+ );
202
+
203
+ await waitFor(() => {
204
+ expect(getByTestId('webview')).toBeTruthy();
205
+ });
206
+
207
+ // Simulate page change
208
+ fireEvent(getByTestId('webview'), 'message', {
209
+ nativeEvent: { data: 'FORM_PAGECHANGED-2' },
210
+ });
211
+
212
+ await waitFor(() => {
213
+ expect(mockOnPageChanged).toHaveBeenCalledWith('2');
214
+ });
215
+ });
216
+
217
+ it('should handle QUESTION_ANSWERED message', async () => {
218
+ const mockOnQuestionAnswered = jest.fn();
219
+ const callbacks: WidgetCallbacks = {
220
+ onQuestionAnswered: mockOnQuestionAnswered,
221
+ };
222
+
223
+ const { getByTestId } = render(
224
+ <SoluCXWidget {...baseProps} callbacks={callbacks} />
225
+ );
226
+
227
+ await waitFor(() => {
228
+ expect(getByTestId('webview')).toBeTruthy();
229
+ });
230
+
231
+ // Simulate question answered
232
+ fireEvent(getByTestId('webview'), 'message', {
233
+ nativeEvent: { data: 'QUESTION_ANSWERED' },
234
+ });
235
+
236
+ await waitFor(() => {
237
+ expect(mockOnQuestionAnswered).toHaveBeenCalledTimes(1);
238
+ });
239
+ });
240
+ });
241
+
242
+ describe('Message Handling - Survey Events', () => {
243
+ const surveyProps = {
244
+ ...baseProps,
245
+ data: {
246
+ customer_id: 'user-456',
247
+ // No form_id = survey mode
248
+ },
249
+ };
250
+
251
+ it('should handle closeSoluCXWidget survey event', async () => {
252
+ const mockOnClosed = jest.fn();
253
+
254
+ const { getByTestId } = render(
255
+ <SoluCXWidget {...surveyProps} callbacks={{ onClosed: mockOnClosed }} />
256
+ );
257
+
258
+ await waitFor(() => {
259
+ expect(getByTestId('webview')).toBeTruthy();
260
+ });
261
+
262
+ // Survey uses different event names
263
+ fireEvent(getByTestId('webview'), 'message', {
264
+ nativeEvent: { data: 'closeSoluCXWidget' },
265
+ });
266
+
267
+ await waitFor(() => {
268
+ expect(mockOnClosed).toHaveBeenCalledTimes(1);
269
+ });
270
+ });
271
+
272
+ it('should handle resizeSoluCXWidget survey event', async () => {
273
+ const mockOnResize = jest.fn();
274
+
275
+ const { getByTestId } = render(
276
+ <SoluCXWidget {...surveyProps} callbacks={{ onResize: mockOnResize }} />
277
+ );
278
+
279
+ await waitFor(() => {
280
+ expect(getByTestId('webview')).toBeTruthy();
281
+ });
282
+
283
+ fireEvent(getByTestId('webview'), 'message', {
284
+ nativeEvent: { data: 'resizeSoluCXWidget-800' },
285
+ });
286
+
287
+ await waitFor(() => {
288
+ expect(mockOnResize).toHaveBeenCalledWith('800');
289
+ });
290
+ });
291
+
292
+ it('should handle completeSoluCXWidget survey event', async () => {
293
+ const mockOnPartialCompleted = jest.fn();
294
+
295
+ const { getByTestId } = render(
296
+ <SoluCXWidget {...surveyProps} callbacks={{ onPartialCompleted: mockOnPartialCompleted }} />
297
+ );
298
+
299
+ await waitFor(() => {
300
+ expect(getByTestId('webview')).toBeTruthy();
301
+ });
302
+
303
+ fireEvent(getByTestId('webview'), 'message', {
304
+ nativeEvent: { data: 'completeSoluCXWidget' },
305
+ });
306
+
307
+ await waitFor(() => {
308
+ expect(mockOnPartialCompleted).toHaveBeenCalled();
309
+ });
310
+ });
311
+ });
312
+
313
+ describe('Edge Cases - Message Handling', () => {
314
+ it('should handle rapid successive messages', async () => {
315
+ const mockOnResize = jest.fn();
316
+
317
+ const { getByTestId } = render(
318
+ <SoluCXWidget {...baseProps} callbacks={{ onResize: mockOnResize }} />
319
+ );
320
+
321
+ await waitFor(() => {
322
+ expect(getByTestId('webview')).toBeTruthy();
323
+ });
324
+
325
+ const webview = getByTestId('webview');
326
+
327
+ // Send 10 resize messages rapidly
328
+ for (let i = 0; i < 10; i++) {
329
+ fireEvent(webview, 'message', {
330
+ nativeEvent: { data: `FORM_RESIZE-${300 + i * 10}` },
331
+ });
332
+ }
333
+
334
+ // All should be processed
335
+ await waitFor(() => {
336
+ expect(mockOnResize).toHaveBeenCalledTimes(10);
337
+ });
338
+
339
+ // Verify last call had correct value
340
+ expect(mockOnResize).toHaveBeenLastCalledWith('390');
341
+ });
342
+ });
343
+
344
+ describe('Close Button Integration', () => {
345
+ it('should render close button in modal', async () => {
346
+ const { getByText, getByTestId } = render(
347
+ <SoluCXWidget {...baseProps} type="modal" />
348
+ );
349
+
350
+ await waitFor(() => {
351
+ expect(getByTestId('webview')).toBeTruthy();
352
+ expect(getByText('✕')).toBeTruthy();
353
+ });
354
+ });
355
+ });
356
+
357
+ describe('Multiple Callbacks in Single Flow', () => {
358
+ it('should call multiple callbacks during form interaction flow', async () => {
359
+ const mockOnPageChanged = jest.fn();
360
+ const mockOnQuestionAnswered = jest.fn();
361
+ const mockOnResize = jest.fn();
362
+ const mockOnPartialCompleted = jest.fn();
363
+ const mockOnClosed = jest.fn();
364
+
365
+ const callbacks: WidgetCallbacks = {
366
+ onPageChanged: mockOnPageChanged,
367
+ onQuestionAnswered: mockOnQuestionAnswered,
368
+ onResize: mockOnResize,
369
+ onPartialCompleted: mockOnPartialCompleted,
370
+ onClosed: mockOnClosed,
371
+ };
372
+
373
+ const { getByTestId } = render(
374
+ <SoluCXWidget {...baseProps} callbacks={callbacks} />
375
+ );
376
+
377
+ await waitFor(() => {
378
+ expect(getByTestId('webview')).toBeTruthy();
379
+ });
380
+
381
+ const webview = getByTestId('webview');
382
+
383
+ // Simulate complete user flow
384
+ // 1. Form resizes
385
+ fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-400' } });
386
+
387
+ // 2. User goes to page 2
388
+ fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_PAGECHANGED-2' } });
389
+
390
+ // 3. User answers a question
391
+ fireEvent(webview, 'message', { nativeEvent: { data: 'QUESTION_ANSWERED' } });
392
+
393
+ // 4. Form resizes again
394
+ fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-500' } });
395
+
396
+ // 5. User completes form
397
+ fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_COMPLETED' } });
398
+
399
+ // 6. User closes
400
+ fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_CLOSE' } });
401
+
402
+ // Verify all callbacks were called in order
403
+ await waitFor(() => {
404
+ expect(mockOnResize).toHaveBeenCalledTimes(2);
405
+ expect(mockOnPageChanged).toHaveBeenCalledTimes(1);
406
+ expect(mockOnQuestionAnswered).toHaveBeenCalledTimes(1);
407
+ expect(mockOnPartialCompleted).toHaveBeenCalledTimes(1);
408
+ expect(mockOnClosed).toHaveBeenCalledTimes(1);
409
+ });
410
+
411
+ // Verify correct arguments
412
+ expect(mockOnResize).toHaveBeenNthCalledWith(1, '400');
413
+ expect(mockOnResize).toHaveBeenNthCalledWith(2, '500');
414
+ expect(mockOnPageChanged).toHaveBeenCalledWith('2');
415
+ });
416
+ });
417
+ });
@@ -1,56 +1,56 @@
1
- import { buildWidgetURL } from '../utils/urlUtils';
2
-
3
- describe('urlUtils', () => {
4
- describe('buildWidgetURL', () => {
5
- const mockKey = 'test-widget-key';
6
-
7
- it('should build URL with transaction_id when provided', () => {
8
- const data = {
9
- customer_id: 'customer123',
10
- form_id: 'form456',
11
- transaction_id: 'trans789'
12
- };
13
-
14
- const result = buildWidgetURL(mockKey, data);
15
-
16
- expect(result).toContain('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget');
17
- expect(result).toContain('customer_id=customer123');
18
- expect(result).toContain('form_id=form456');
19
- expect(result).toContain('transaction_id=trans789');
20
- });
21
-
22
- it('should build URL with empty transaction_id when not provided', () => {
23
- const data = {
24
- customer_id: 'customer123',
25
- form_id: 'form456'
26
- };
27
-
28
- const result = buildWidgetURL(mockKey, data);
29
-
30
- expect(result).toContain('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget');
31
- expect(result).toContain('transaction_id=&');
32
- expect(result).toContain('customer_id=customer123');
33
- expect(result).toContain('form_id=form456');
34
- });
35
-
36
- it('should handle empty data object', () => {
37
- const data = {};
38
-
39
- const result = buildWidgetURL(mockKey, data);
40
-
41
- expect(result).toBe('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget&transaction_id=&');
42
- });
43
-
44
- it('should encode URL parameters correctly', () => {
45
- const data = {
46
- email: 'test@example.com',
47
- name: 'John Doe'
48
- };
49
-
50
- const result = buildWidgetURL(mockKey, data);
51
-
52
- expect(result).toContain('email=test%40example.com');
53
- expect(result).toContain('name=John+Doe');
54
- });
55
- });
56
- });
1
+ import { buildWidgetURL } from '../utils/urlUtils';
2
+
3
+ describe('urlUtils', () => {
4
+ describe('buildWidgetURL', () => {
5
+ const mockKey = 'test-widget-key';
6
+
7
+ it('should build URL with transaction_id when provided', () => {
8
+ const data = {
9
+ customer_id: 'customer123',
10
+ form_id: 'form456',
11
+ transaction_id: 'trans789'
12
+ };
13
+
14
+ const result = buildWidgetURL(mockKey, data);
15
+
16
+ expect(result).toContain('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget');
17
+ expect(result).toContain('customer_id=customer123');
18
+ expect(result).toContain('form_id=form456');
19
+ expect(result).toContain('transaction_id=trans789');
20
+ });
21
+
22
+ it('should build URL with empty transaction_id when not provided', () => {
23
+ const data = {
24
+ customer_id: 'customer123',
25
+ form_id: 'form456'
26
+ };
27
+
28
+ const result = buildWidgetURL(mockKey, data);
29
+
30
+ expect(result).toContain('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget');
31
+ expect(result).toContain('transaction_id=&');
32
+ expect(result).toContain('customer_id=customer123');
33
+ expect(result).toContain('form_id=form456');
34
+ });
35
+
36
+ it('should handle empty data object', () => {
37
+ const data = {};
38
+
39
+ const result = buildWidgetURL(mockKey, data);
40
+
41
+ expect(result).toBe('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget&transaction_id=&');
42
+ });
43
+
44
+ it('should encode URL parameters correctly', () => {
45
+ const data = {
46
+ email: 'test@example.com',
47
+ name: 'John Doe'
48
+ };
49
+
50
+ const result = buildWidgetURL(mockKey, data);
51
+
52
+ expect(result).toContain('email=test%40example.com');
53
+ expect(result).toContain('name=John+Doe');
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,109 @@
1
+ import { Platform, Dimensions } from 'react-native';
2
+ import { getDeviceInfo } from '../hooks/useDeviceInfoCollector';
3
+
4
+ jest.mock('react-native', () => ({
5
+ Platform: {
6
+ OS: 'ios',
7
+ Version: '16.0',
8
+ constants: {},
9
+ },
10
+ Dimensions: {
11
+ get: jest.fn(),
12
+ },
13
+ }));
14
+
15
+ jest.mock('react-native-device-info', () => ({
16
+ getModel: () => 'iPhone 14',
17
+ }), { virtual: true });
18
+
19
+ const mockDimensionsGet = Dimensions.get as jest.MockedFunction<typeof Dimensions.get>;
20
+
21
+ describe('useDeviceInfoCollector', () => {
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ describe('getDeviceInfo', () => {
27
+ it('should return complete device information', () => {
28
+ mockDimensionsGet.mockImplementation((type) => {
29
+ if (type === 'screen') {
30
+ return { width: 390, height: 844, scale: 3, fontScale: 1 };
31
+ }
32
+ return { width: 390, height: 844, scale: 3, fontScale: 1 };
33
+ });
34
+
35
+ const deviceInfo = getDeviceInfo();
36
+
37
+ expect(deviceInfo).toEqual({
38
+ platform: 'ios',
39
+ osVersion: '16.0',
40
+ screenWidth: 390,
41
+ screenHeight: 844,
42
+ windowWidth: 390,
43
+ windowHeight: 844,
44
+ scale: 3,
45
+ fontScale: 1,
46
+ deviceType: 'phone',
47
+ model: 'iPhone 14',
48
+ });
49
+ });
50
+
51
+ it('should identify device as tablet when dimensions are large', () => {
52
+ mockDimensionsGet.mockImplementation((type) => {
53
+ if (type === 'screen') {
54
+ return { width: 768, height: 1024, scale: 2, fontScale: 1 };
55
+ }
56
+ return { width: 768, height: 1024, scale: 2, fontScale: 1 };
57
+ });
58
+
59
+ const deviceInfo = getDeviceInfo();
60
+
61
+ expect(deviceInfo.deviceType).toBe('tablet');
62
+ });
63
+
64
+ it('should identify device as phone when dimensions are small', () => {
65
+ mockDimensionsGet.mockImplementation((type) => {
66
+ if (type === 'screen') {
67
+ return { width: 375, height: 812, scale: 3, fontScale: 1 };
68
+ }
69
+ return { width: 375, height: 812, scale: 3, fontScale: 1 };
70
+ });
71
+
72
+ const deviceInfo = getDeviceInfo();
73
+
74
+ expect(deviceInfo.deviceType).toBe('phone');
75
+ });
76
+
77
+ it('should return model from react-native-device-info', () => {
78
+ mockDimensionsGet.mockImplementation(() => ({
79
+ width: 390,
80
+ height: 844,
81
+ scale: 3,
82
+ fontScale: 1,
83
+ }));
84
+
85
+ const deviceInfo = getDeviceInfo();
86
+
87
+ expect(deviceInfo.model).toBe('iPhone 14');
88
+ });
89
+ });
90
+
91
+ describe('platform detection', () => {
92
+ it('should detect Android platform', () => {
93
+ (Platform.OS as any) = 'android';
94
+ (Platform.Version as any) = 33;
95
+
96
+ mockDimensionsGet.mockImplementation(() => ({
97
+ width: 412,
98
+ height: 915,
99
+ scale: 2.625,
100
+ fontScale: 1,
101
+ }));
102
+
103
+ const deviceInfo = getDeviceInfo();
104
+
105
+ expect(deviceInfo.platform).toBe('android');
106
+ expect(deviceInfo.osVersion).toBe('33');
107
+ });
108
+ });
109
+ });