@solucx/react-native-solucx-widget 0.1.16 → 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.
- package/package.json +31 -4
- package/src/SoluCXWidget.tsx +106 -53
- package/src/__mocks__/expo-modules-core-web.js +16 -0
- package/src/__mocks__/expo-modules-core.js +33 -0
- package/src/__tests__/ClientVersionCollector.test.ts +55 -0
- package/src/__tests__/CloseButton.test.tsx +47 -0
- package/src/__tests__/Constants.test.ts +17 -0
- package/src/__tests__/InlineWidget.rendering.test.tsx +81 -0
- package/src/__tests__/ModalWidget.rendering.test.tsx +157 -0
- package/src/__tests__/OverlayWidget.rendering.test.tsx +123 -0
- package/src/__tests__/SoluCXWidget.rendering.test.tsx +315 -0
- package/src/__tests__/e2e/widget-lifecycle.test.tsx +353 -0
- package/src/__tests__/integration/webview-communication-simple.test.tsx +147 -0
- package/src/__tests__/integration/webview-communication.test.tsx +417 -0
- package/src/__tests__/useDeviceInfoCollector.test.ts +109 -0
- package/src/__tests__/useWidgetState.test.ts +76 -84
- package/src/__tests__/widgetBootstrapService.test.ts +182 -0
- package/src/components/ModalWidget.tsx +3 -5
- package/src/components/OverlayWidget.tsx +1 -1
- package/src/constants/Constants.ts +4 -0
- package/src/constants/webViewConstants.ts +1 -0
- package/src/hooks/useDeviceInfoCollector.ts +67 -0
- package/src/hooks/useWidgetState.ts +1 -1
- package/src/index.ts +4 -0
- package/src/interfaces/WidgetCallbacks.ts +15 -0
- package/src/interfaces/index.ts +3 -2
- package/src/services/ClientVersionCollector.ts +15 -0
- package/src/services/storage.ts +2 -2
- package/src/services/widgetBootstrapService.ts +67 -0
- package/src/services/widgetEventService.ts +14 -30
- package/src/services/widgetValidationService.ts +29 -13
- package/src/setupTests.js +43 -0
- package/src/styles/widgetStyles.ts +1 -1
- package/src/utils/urlUtils.ts +2 -2
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent, within } from '@testing-library/react-native';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { ModalWidget } from '../components/ModalWidget';
|
|
5
|
+
|
|
6
|
+
describe('ModalWidget rendering', () => {
|
|
7
|
+
it('should render Modal component when visible is true', () => {
|
|
8
|
+
const { getByTestId, UNSAFE_getByType } = render(
|
|
9
|
+
<ModalWidget visible={true} height={400}>
|
|
10
|
+
<Text>Survey Content</Text>
|
|
11
|
+
</ModalWidget>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
15
|
+
expect(modal).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should render children inside the modal', () => {
|
|
19
|
+
const { getByText } = render(
|
|
20
|
+
<ModalWidget visible={true} height={400}>
|
|
21
|
+
<Text>Survey Content</Text>
|
|
22
|
+
</ModalWidget>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(getByText('Survey Content')).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should render the close button when visible is true', () => {
|
|
29
|
+
const { getByText } = render(
|
|
30
|
+
<ModalWidget visible={true} height={400}>
|
|
31
|
+
<Text>Content</Text>
|
|
32
|
+
</ModalWidget>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(getByText('✕')).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should call onClose when close button is pressed', () => {
|
|
39
|
+
const mockOnClose = jest.fn();
|
|
40
|
+
const { getByText } = render(
|
|
41
|
+
<ModalWidget visible={true} height={400} onClose={mockOnClose}>
|
|
42
|
+
<Text>Content</Text>
|
|
43
|
+
</ModalWidget>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
fireEvent.press(getByText('✕'));
|
|
47
|
+
|
|
48
|
+
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should set Modal visible to true via internal state on initial render', () => {
|
|
52
|
+
const { UNSAFE_getByType } = render(
|
|
53
|
+
<ModalWidget visible={true} height={400}>
|
|
54
|
+
<Text>Content</Text>
|
|
55
|
+
</ModalWidget>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
59
|
+
expect(modal.props.visible).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should use slide animation type', () => {
|
|
63
|
+
const { UNSAFE_getByType } = render(
|
|
64
|
+
<ModalWidget visible={true} height={400}>
|
|
65
|
+
<Text>Content</Text>
|
|
66
|
+
</ModalWidget>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
70
|
+
expect(modal.props.animationType).toBe('slide');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should set Modal transparent prop to true', () => {
|
|
74
|
+
const { UNSAFE_getByType } = render(
|
|
75
|
+
<ModalWidget visible={true} height={400}>
|
|
76
|
+
<Text>Content</Text>
|
|
77
|
+
</ModalWidget>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
81
|
+
expect(modal.props.transparent).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should set hardwareAccelerated prop to true', () => {
|
|
85
|
+
const { UNSAFE_getByType } = render(
|
|
86
|
+
<ModalWidget visible={true} height={400}>
|
|
87
|
+
<Text>Content</Text>
|
|
88
|
+
</ModalWidget>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
92
|
+
expect(modal.props.hardwareAccelerated).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should pass visible=false to Modal when visible prop is false', () => {
|
|
96
|
+
const { UNSAFE_getByType } = render(
|
|
97
|
+
<ModalWidget visible={false} height={400}>
|
|
98
|
+
<Text>Content</Text>
|
|
99
|
+
</ModalWidget>
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
103
|
+
expect(modal.props.visible).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should call onClose when close button is pressed inside modal', () => {
|
|
107
|
+
const mockOnClose = jest.fn();
|
|
108
|
+
const { getByText } = render(
|
|
109
|
+
<ModalWidget visible={true} height={400} onClose={mockOnClose}>
|
|
110
|
+
<Text>Content</Text>
|
|
111
|
+
</ModalWidget>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
fireEvent.press(getByText('✕'));
|
|
115
|
+
|
|
116
|
+
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should reflect updated visible prop after rerender', () => {
|
|
120
|
+
const { UNSAFE_getByType, rerender } = render(
|
|
121
|
+
<ModalWidget visible={true} height={400} onClose={jest.fn()}>
|
|
122
|
+
<Text>Content</Text>
|
|
123
|
+
</ModalWidget>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
rerender(
|
|
127
|
+
<ModalWidget visible={false} height={400} onClose={jest.fn()}>
|
|
128
|
+
<Text>Content</Text>
|
|
129
|
+
</ModalWidget>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
133
|
+
expect(modal.props.visible).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should not call onClose if onClose prop is not provided', () => {
|
|
137
|
+
const { getByText } = render(
|
|
138
|
+
<ModalWidget visible={true} height={400}>
|
|
139
|
+
<Text>Content</Text>
|
|
140
|
+
</ModalWidget>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(() => fireEvent.press(getByText('✕'))).not.toThrow();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should render multiple children', () => {
|
|
147
|
+
const { getByText } = render(
|
|
148
|
+
<ModalWidget visible={true} height={400}>
|
|
149
|
+
<Text>First Child</Text>
|
|
150
|
+
<Text>Second Child</Text>
|
|
151
|
+
</ModalWidget>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
expect(getByText('First Child')).toBeTruthy();
|
|
155
|
+
expect(getByText('Second Child')).toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { OverlayWidget } from '../components/OverlayWidget';
|
|
5
|
+
|
|
6
|
+
describe('OverlayWidget rendering', () => {
|
|
7
|
+
it('should render children when visible is true for bottom position', () => {
|
|
8
|
+
const { getByText } = render(
|
|
9
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom">
|
|
10
|
+
<Text>Bottom Widget</Text>
|
|
11
|
+
</OverlayWidget>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(getByText('Bottom Widget')).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should render children when visible is true for top position', () => {
|
|
18
|
+
const { getByText } = render(
|
|
19
|
+
<OverlayWidget visible={true} width={390} height={300} position="top">
|
|
20
|
+
<Text>Top Widget</Text>
|
|
21
|
+
</OverlayWidget>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(getByText('Top Widget')).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should render close button when visible is true', () => {
|
|
28
|
+
const { getByText } = render(
|
|
29
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom">
|
|
30
|
+
<Text>Content</Text>
|
|
31
|
+
</OverlayWidget>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(getByText('✕')).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should call onClose when close button is pressed', () => {
|
|
38
|
+
const mockOnClose = jest.fn();
|
|
39
|
+
const { getByText } = render(
|
|
40
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom" onClose={mockOnClose}>
|
|
41
|
+
<Text>Content</Text>
|
|
42
|
+
</OverlayWidget>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
fireEvent.press(getByText('✕'));
|
|
46
|
+
|
|
47
|
+
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should hide entirely after close button is pressed', () => {
|
|
51
|
+
const { getByText, queryByText } = render(
|
|
52
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom" onClose={jest.fn()}>
|
|
53
|
+
<Text>Dismissible Content</Text>
|
|
54
|
+
</OverlayWidget>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
fireEvent.press(getByText('✕'));
|
|
58
|
+
|
|
59
|
+
expect(queryByText('Dismissible Content')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should not render content when internal visibility is set to false after close', () => {
|
|
63
|
+
const { getByText, queryByText } = render(
|
|
64
|
+
<OverlayWidget visible={true} width={390} height={300} position="top" onClose={jest.fn()}>
|
|
65
|
+
<Text>Top Content</Text>
|
|
66
|
+
</OverlayWidget>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
fireEvent.press(getByText('✕'));
|
|
70
|
+
|
|
71
|
+
expect(queryByText('✕')).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should not reopen after internal state is set to false even if rerendered', () => {
|
|
75
|
+
const { getByText, queryByText, rerender } = render(
|
|
76
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom" onClose={jest.fn()}>
|
|
77
|
+
<Text>Content</Text>
|
|
78
|
+
</OverlayWidget>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
fireEvent.press(getByText('✕'));
|
|
82
|
+
|
|
83
|
+
rerender(
|
|
84
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom" onClose={jest.fn()}>
|
|
85
|
+
<Text>Content</Text>
|
|
86
|
+
</OverlayWidget>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(queryByText('Content')).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should not call onClose if onClose prop is not provided', () => {
|
|
93
|
+
const { getByText } = render(
|
|
94
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom">
|
|
95
|
+
<Text>Content</Text>
|
|
96
|
+
</OverlayWidget>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(() => fireEvent.press(getByText('✕'))).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should hide close button when visible is false', () => {
|
|
103
|
+
const { queryByText } = render(
|
|
104
|
+
<OverlayWidget visible={false} width={390} height={300} position="bottom">
|
|
105
|
+
<Text>Content</Text>
|
|
106
|
+
</OverlayWidget>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(queryByText('✕')).toBeNull();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should render multiple children', () => {
|
|
113
|
+
const { getByText } = render(
|
|
114
|
+
<OverlayWidget visible={true} width={390} height={300} position="bottom">
|
|
115
|
+
<Text>First</Text>
|
|
116
|
+
<Text>Second</Text>
|
|
117
|
+
</OverlayWidget>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(getByText('First')).toBeTruthy();
|
|
121
|
+
expect(getByText('Second')).toBeTruthy();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, waitFor } from '@testing-library/react-native';
|
|
3
|
+
import { SoluCXWidget } from '../SoluCXWidget';
|
|
4
|
+
import { WidgetData, WidgetOptions } from '../interfaces';
|
|
5
|
+
import { requestWidgetUrl } from '../services/widgetBootstrapService';
|
|
6
|
+
|
|
7
|
+
jest.mock('react-native-webview', () => {
|
|
8
|
+
const { forwardRef } = require('react');
|
|
9
|
+
const { View } = require('react-native');
|
|
10
|
+
return {
|
|
11
|
+
__esModule: true,
|
|
12
|
+
WebView: forwardRef((props: any, ref: any) => (
|
|
13
|
+
<View testID="webview" {...props} ref={ref} />
|
|
14
|
+
)),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const mockLoadSavedData = jest.fn();
|
|
19
|
+
const mockClose = jest.fn();
|
|
20
|
+
const mockSetIsWidgetVisible = jest.fn();
|
|
21
|
+
const mockOpen = jest.fn();
|
|
22
|
+
const mockShouldDisplayWidget = jest.fn();
|
|
23
|
+
|
|
24
|
+
jest.mock('../hooks/useWidgetState', () => ({
|
|
25
|
+
useWidgetState: () => ({
|
|
26
|
+
widgetHeight: 400,
|
|
27
|
+
isWidgetVisible: true,
|
|
28
|
+
setIsWidgetVisible: mockSetIsWidgetVisible,
|
|
29
|
+
loadSavedData: mockLoadSavedData,
|
|
30
|
+
resize: jest.fn(),
|
|
31
|
+
open: mockOpen,
|
|
32
|
+
close: mockClose,
|
|
33
|
+
userId: 'test-user-123',
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
jest.mock('../services/widgetEventService', () => ({
|
|
38
|
+
WidgetEventService: jest.fn().mockImplementation(() => ({
|
|
39
|
+
handleMessage: jest.fn(),
|
|
40
|
+
})),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
jest.mock('../services/widgetValidationService', () => ({
|
|
44
|
+
WidgetValidationService: jest.fn().mockImplementation(() => ({
|
|
45
|
+
shouldDisplayWidget: mockShouldDisplayWidget,
|
|
46
|
+
})),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
jest.mock('../services/widgetBootstrapService', () => ({
|
|
50
|
+
requestWidgetUrl: jest.fn(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
jest.mock('../constants/Constants', () => ({
|
|
54
|
+
SDK_NAME: 'rn-widget-sdk',
|
|
55
|
+
SDK_VERSION: '0.1.16',
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
jest.mock('../services/ClientVersionCollector', () => ({
|
|
59
|
+
getClientVersion: jest.fn(() => '1.0.0'),
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
jest.mock('../hooks/useDeviceInfoCollector', () => ({
|
|
63
|
+
getDeviceInfo: jest.fn(() => ({
|
|
64
|
+
platform: 'ios',
|
|
65
|
+
osVersion: '16.0',
|
|
66
|
+
screenWidth: 390,
|
|
67
|
+
screenHeight: 844,
|
|
68
|
+
windowWidth: 390,
|
|
69
|
+
windowHeight: 844,
|
|
70
|
+
scale: 3,
|
|
71
|
+
fontScale: 1,
|
|
72
|
+
deviceType: 'phone',
|
|
73
|
+
model: 'iPhone 14 Pro',
|
|
74
|
+
})),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const mockRequestWidgetUrl = requestWidgetUrl as jest.MockedFunction<typeof requestWidgetUrl>;
|
|
78
|
+
const bootstrappedWidgetUrl = 'https://widgets.solucx.com/widget/bootstrap-result';
|
|
79
|
+
|
|
80
|
+
const baseProps = {
|
|
81
|
+
soluCXKey: 'test-key-abc',
|
|
82
|
+
data: {
|
|
83
|
+
customer_id: 'cust-001',
|
|
84
|
+
transaction_id: 'txn-999',
|
|
85
|
+
form_id: 'form-123',
|
|
86
|
+
} as WidgetData,
|
|
87
|
+
options: {
|
|
88
|
+
height: 400,
|
|
89
|
+
} as WidgetOptions,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
beforeEach(() => {
|
|
93
|
+
jest.clearAllMocks();
|
|
94
|
+
mockShouldDisplayWidget.mockReset();
|
|
95
|
+
mockShouldDisplayWidget.mockResolvedValue({ canDisplay: true });
|
|
96
|
+
mockRequestWidgetUrl.mockReset();
|
|
97
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('SoluCXWidget type routing', () => {
|
|
101
|
+
it('should render ModalWidget when type is modal', () => {
|
|
102
|
+
const { UNSAFE_getByType } = render(
|
|
103
|
+
<SoluCXWidget {...baseProps} type="modal" />
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const modal = UNSAFE_getByType(require('react-native').Modal);
|
|
107
|
+
expect(modal).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should render close button inside ModalWidget', () => {
|
|
111
|
+
const { getByText } = render(
|
|
112
|
+
<SoluCXWidget {...baseProps} type="modal" />
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(getByText('✕')).toBeTruthy();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should render WebView inside ModalWidget with correct source', () => {
|
|
119
|
+
const { getByTestId } = render(
|
|
120
|
+
<SoluCXWidget {...baseProps} type="modal" />
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const webview = getByTestId('webview');
|
|
124
|
+
expect(webview.props.source.uri).toContain('test-key-abc');
|
|
125
|
+
expect(webview.props.source.uri).toContain('txn-999');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should render InlineWidget when type is inline', () => {
|
|
129
|
+
const { queryByTestId, getByText } = render(
|
|
130
|
+
<SoluCXWidget {...baseProps} type="inline" />
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(queryByTestId('webview')).toBeTruthy();
|
|
134
|
+
expect(getByText('✕')).toBeTruthy();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should not render Modal when type is inline', () => {
|
|
138
|
+
const { UNSAFE_queryByType } = render(
|
|
139
|
+
<SoluCXWidget {...baseProps} type="inline" />
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const modal = UNSAFE_queryByType(require('react-native').Modal);
|
|
143
|
+
expect(modal).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should render OverlayWidget when type is bottom', () => {
|
|
147
|
+
const { queryByTestId, getByText } = render(
|
|
148
|
+
<SoluCXWidget {...baseProps} type="bottom" />
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(queryByTestId('webview')).toBeTruthy();
|
|
152
|
+
expect(getByText('✕')).toBeTruthy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should render OverlayWidget when type is top', () => {
|
|
156
|
+
const { queryByTestId, getByText } = render(
|
|
157
|
+
<SoluCXWidget {...baseProps} type="top" />
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(queryByTestId('webview')).toBeTruthy();
|
|
161
|
+
expect(getByText('✕')).toBeTruthy();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should not render Modal when type is bottom', () => {
|
|
165
|
+
const { UNSAFE_queryByType } = render(
|
|
166
|
+
<SoluCXWidget {...baseProps} type="bottom" />
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const modal = UNSAFE_queryByType(require('react-native').Modal);
|
|
170
|
+
expect(modal).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should call loadSavedData on mount', () => {
|
|
174
|
+
render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
175
|
+
|
|
176
|
+
expect(mockLoadSavedData).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('SoluCXWidget WebView configuration', () => {
|
|
181
|
+
it('should set originWhitelist to allow all origins', () => {
|
|
182
|
+
const { getByTestId } = render(
|
|
183
|
+
<SoluCXWidget {...baseProps} type="modal" />
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const webview = getByTestId('webview');
|
|
187
|
+
expect(webview.props.originWhitelist).toEqual(['*']);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('should set WebView width style to screen width', () => {
|
|
191
|
+
const { getByTestId } = render(
|
|
192
|
+
<SoluCXWidget {...baseProps} type="modal" />
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const webview = getByTestId('webview');
|
|
196
|
+
const widthStyle = webview.props.style.find(
|
|
197
|
+
(s: any) => s.width !== undefined
|
|
198
|
+
);
|
|
199
|
+
expect(widthStyle.width).toBeGreaterThan(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should set WebView height style to widgetHeight', () => {
|
|
203
|
+
const { getByTestId } = render(
|
|
204
|
+
<SoluCXWidget {...baseProps} type="modal" />
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const webview = getByTestId('webview');
|
|
208
|
+
const heightStyle = webview.props.style.find(
|
|
209
|
+
(s: any) => s.height !== undefined
|
|
210
|
+
);
|
|
211
|
+
expect(heightStyle.height).toBe(400);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should build URL containing soluCXKey and transaction_id', () => {
|
|
215
|
+
const { getByTestId } = render(
|
|
216
|
+
<SoluCXWidget {...baseProps} type="inline" />
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const webview = getByTestId('webview');
|
|
220
|
+
expect(webview.props.source.uri).toContain('test-key-abc');
|
|
221
|
+
expect(webview.props.source.uri).toContain('transaction_id=txn-999');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should build URL with mode=widget parameter', () => {
|
|
225
|
+
const { getByTestId } = render(
|
|
226
|
+
<SoluCXWidget {...baseProps} type="inline" />
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const webview = getByTestId('webview');
|
|
230
|
+
expect(webview.props.source.uri).toContain('mode=widget');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('SoluCXWidget bootstrap flow', () => {
|
|
235
|
+
it('fetches widget URL before rendering non-form widgets', async () => {
|
|
236
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
237
|
+
|
|
238
|
+
const props = {
|
|
239
|
+
...baseProps,
|
|
240
|
+
data: {
|
|
241
|
+
customer_id: 'cust-777',
|
|
242
|
+
journey: 'sac',
|
|
243
|
+
},
|
|
244
|
+
type: 'bottom' as const,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const { queryByTestId, getByTestId } = render(
|
|
248
|
+
<SoluCXWidget {...props} />
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(queryByTestId('webview')).toBeNull();
|
|
252
|
+
|
|
253
|
+
await waitFor(() => {
|
|
254
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
255
|
+
expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(mockOpen).toHaveBeenCalledTimes(1);
|
|
257
|
+
expect(queryByTestId('webview')).toBeTruthy();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const webview = getByTestId('webview');
|
|
261
|
+
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('keeps widget hidden when identifier fetch fails', async () => {
|
|
265
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error('network'));
|
|
266
|
+
|
|
267
|
+
const props = {
|
|
268
|
+
...baseProps,
|
|
269
|
+
data: {
|
|
270
|
+
customer_id: 'cust-777',
|
|
271
|
+
journey: 'sac',
|
|
272
|
+
},
|
|
273
|
+
type: 'bottom' as const,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const { queryByTestId } = render(
|
|
277
|
+
<SoluCXWidget {...props} />
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(queryByTestId('webview')).toBeNull();
|
|
281
|
+
|
|
282
|
+
await waitFor(() => {
|
|
283
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(mockShouldDisplayWidget).not.toHaveBeenCalled();
|
|
285
|
+
expect(queryByTestId('webview')).toBeNull();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('keeps widget hidden when validation fails after bootstrap', async () => {
|
|
290
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
291
|
+
mockShouldDisplayWidget.mockResolvedValue({ canDisplay: false, blockReason: 'BLOCKED_BY_MAX_RETRY_ATTEMPTS' });
|
|
292
|
+
|
|
293
|
+
const props = {
|
|
294
|
+
...baseProps,
|
|
295
|
+
data: {
|
|
296
|
+
customer_id: 'cust-777',
|
|
297
|
+
journey: 'sac',
|
|
298
|
+
},
|
|
299
|
+
type: 'bottom' as const,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const { queryByTestId } = render(
|
|
303
|
+
<SoluCXWidget {...props} />
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
expect(queryByTestId('webview')).toBeNull();
|
|
307
|
+
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
310
|
+
expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
|
|
311
|
+
expect(mockOpen).not.toHaveBeenCalled();
|
|
312
|
+
expect(queryByTestId('webview')).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|