@solucx/react-native-solucx-widget 0.2.0 → 0.2.2
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/lib/SoluCXWidget.d.ts +12 -0
- package/lib/SoluCXWidget.d.ts.map +1 -0
- package/lib/SoluCXWidget.js +110 -0
- package/lib/SoluCXWidget.js.map +1 -0
- package/lib/components/CloseButton.d.ts +8 -0
- package/lib/components/CloseButton.d.ts.map +1 -0
- package/lib/components/CloseButton.js +31 -0
- package/lib/components/CloseButton.js.map +1 -0
- package/lib/components/InlineWidget.d.ts +10 -0
- package/lib/components/InlineWidget.d.ts.map +1 -0
- package/lib/components/InlineWidget.js +19 -0
- package/lib/components/InlineWidget.js.map +1 -0
- package/lib/components/ModalWidget.d.ts +10 -0
- package/lib/components/ModalWidget.d.ts.map +1 -0
- package/lib/components/ModalWidget.js +27 -0
- package/lib/components/ModalWidget.js.map +1 -0
- package/lib/components/OverlayWidget.d.ts +12 -0
- package/lib/components/OverlayWidget.d.ts.map +1 -0
- package/lib/components/OverlayWidget.js +55 -0
- package/lib/components/OverlayWidget.js.map +1 -0
- package/lib/constants/Constants.d.ts +3 -0
- package/lib/constants/Constants.d.ts.map +1 -0
- package/lib/constants/Constants.js +10 -0
- package/lib/constants/Constants.js.map +1 -0
- package/lib/constants/webViewConstants.d.ts +12 -0
- package/lib/constants/webViewConstants.d.ts.map +1 -0
- package/lib/constants/webViewConstants.js +19 -0
- package/lib/constants/webViewConstants.js.map +1 -0
- package/lib/hooks/index.d.ts +3 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +8 -0
- package/lib/hooks/index.js.map +1 -0
- package/lib/hooks/useDeviceInfoCollector.d.ts +14 -0
- package/lib/hooks/useDeviceInfoCollector.d.ts.map +1 -0
- package/lib/hooks/useDeviceInfoCollector.js +54 -0
- package/lib/hooks/useDeviceInfoCollector.js.map +1 -0
- package/lib/hooks/useHeightAnimation.d.ts +9 -0
- package/lib/hooks/useHeightAnimation.d.ts.map +1 -0
- package/lib/hooks/useHeightAnimation.js +19 -0
- package/lib/hooks/useHeightAnimation.js.map +1 -0
- package/lib/hooks/useWidgetHeight.d.ts +13 -0
- package/lib/hooks/useWidgetHeight.d.ts.map +1 -0
- package/lib/hooks/useWidgetHeight.js +21 -0
- package/lib/hooks/useWidgetHeight.js.map +1 -0
- package/lib/hooks/useWidgetState.d.ts +15 -0
- package/lib/hooks/useWidgetState.d.ts.map +1 -0
- package/lib/hooks/useWidgetState.js +79 -0
- package/lib/hooks/useWidgetState.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +43 -0
- package/lib/index.js.map +1 -0
- package/lib/interfaces/WidgetCallbacks.d.ts +14 -0
- package/lib/interfaces/WidgetCallbacks.d.ts.map +1 -0
- package/lib/interfaces/WidgetCallbacks.js +3 -0
- package/lib/interfaces/WidgetCallbacks.js.map +1 -0
- package/lib/interfaces/WidgetData.d.ts +21 -0
- package/lib/interfaces/WidgetData.d.ts.map +1 -0
- package/lib/interfaces/WidgetData.js +3 -0
- package/lib/interfaces/WidgetData.js.map +1 -0
- package/lib/interfaces/WidgetOptions.d.ts +9 -0
- package/lib/interfaces/WidgetOptions.d.ts.map +1 -0
- package/lib/interfaces/WidgetOptions.js +3 -0
- package/lib/interfaces/WidgetOptions.js.map +1 -0
- package/lib/interfaces/WidgetResponse.d.ts +10 -0
- package/lib/interfaces/WidgetResponse.d.ts.map +1 -0
- package/lib/interfaces/WidgetResponse.js +12 -0
- package/lib/interfaces/WidgetResponse.js.map +1 -0
- package/lib/interfaces/WidgetSamplerLog.d.ts +7 -0
- package/lib/interfaces/WidgetSamplerLog.d.ts.map +1 -0
- package/lib/interfaces/WidgetSamplerLog.js +3 -0
- package/lib/interfaces/WidgetSamplerLog.js.map +1 -0
- package/lib/interfaces/index.d.ts +12 -0
- package/lib/interfaces/index.d.ts.map +1 -0
- package/lib/interfaces/index.js +3 -0
- package/lib/interfaces/index.js.map +1 -0
- package/lib/services/ClientVersionCollector.d.ts +2 -0
- package/lib/services/ClientVersionCollector.d.ts.map +1 -0
- package/lib/services/ClientVersionCollector.js +20 -0
- package/lib/services/ClientVersionCollector.js.map +1 -0
- package/lib/services/storage.d.ts +8 -0
- package/lib/services/storage.d.ts.map +1 -0
- package/lib/services/storage.js +23 -0
- package/lib/services/storage.js.map +1 -0
- package/lib/services/widgetBootstrapService.d.ts +5 -0
- package/lib/services/widgetBootstrapService.d.ts.map +1 -0
- package/lib/services/widgetBootstrapService.js +60 -0
- package/lib/services/widgetBootstrapService.js.map +1 -0
- package/lib/services/widgetEventService.d.ts +19 -0
- package/lib/services/widgetEventService.d.ts.map +1 -0
- package/lib/services/widgetEventService.js +79 -0
- package/lib/services/widgetEventService.js.map +1 -0
- package/lib/services/widgetValidationService.d.ts +18 -0
- package/lib/services/widgetValidationService.d.ts.map +1 -0
- package/lib/services/widgetValidationService.js +71 -0
- package/lib/services/widgetValidationService.js.map +1 -0
- package/lib/styles/widgetStyles.d.ts +87 -0
- package/lib/styles/widgetStyles.d.ts.map +1 -0
- package/lib/styles/widgetStyles.js +59 -0
- package/lib/styles/widgetStyles.js.map +1 -0
- package/lib/utils/urlUtils.d.ts +3 -0
- package/lib/utils/urlUtils.d.ts.map +1 -0
- package/lib/utils/urlUtils.js +13 -0
- package/lib/utils/urlUtils.js.map +1 -0
- package/package.json +5 -3
- package/src/SoluCXWidget.tsx +23 -17
- package/src/__tests__/SoluCXWidget.rendering.test.tsx +492 -153
- package/src/__tests__/e2e/widget-lifecycle.test.tsx +9 -10
- package/src/__tests__/integration/webview-communication.test.tsx +9 -9
- package/src/__tests__/useWidgetState.test.ts +2 -2
- package/src/__tests__/widgetBootstrapService.test.ts +45 -10
- package/src/hooks/useWidgetState.ts +3 -3
- package/src/interfaces/WidgetCallbacks.ts +0 -1
- package/src/services/widgetBootstrapService.ts +13 -4
- package/src/services/widgetEventService.ts +1 -1
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import React from
|
|
2
|
-
import { render, waitFor } from
|
|
3
|
-
import { SoluCXWidget } from
|
|
4
|
-
import { WidgetData, WidgetOptions } from
|
|
5
|
-
import { requestWidgetUrl } from
|
|
6
|
-
|
|
7
|
-
jest.mock(
|
|
8
|
-
const { forwardRef } = require(
|
|
9
|
-
const { View } = require(
|
|
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
10
|
return {
|
|
11
11
|
__esModule: true,
|
|
12
|
-
WebView: forwardRef((props: any, ref: any) =>
|
|
13
|
-
<View testID="webview" {...props} ref={ref} />
|
|
14
|
-
)),
|
|
12
|
+
WebView: forwardRef((props: any, ref: any) => <View testID="webview" {...props} ref={ref} />),
|
|
15
13
|
};
|
|
16
14
|
});
|
|
17
15
|
|
|
@@ -21,7 +19,7 @@ const mockSetIsWidgetVisible = jest.fn();
|
|
|
21
19
|
const mockOpen = jest.fn();
|
|
22
20
|
const mockShouldDisplayWidget = jest.fn();
|
|
23
21
|
|
|
24
|
-
jest.mock(
|
|
22
|
+
jest.mock("../hooks/useWidgetState", () => ({
|
|
25
23
|
useWidgetState: () => ({
|
|
26
24
|
widgetHeight: 400,
|
|
27
25
|
isWidgetVisible: true,
|
|
@@ -30,59 +28,59 @@ jest.mock('../hooks/useWidgetState', () => ({
|
|
|
30
28
|
resize: jest.fn(),
|
|
31
29
|
open: mockOpen,
|
|
32
30
|
close: mockClose,
|
|
33
|
-
userId:
|
|
31
|
+
userId: "test-user-123",
|
|
34
32
|
}),
|
|
35
33
|
}));
|
|
36
34
|
|
|
37
|
-
jest.mock(
|
|
35
|
+
jest.mock("../services/widgetEventService", () => ({
|
|
38
36
|
WidgetEventService: jest.fn().mockImplementation(() => ({
|
|
39
37
|
handleMessage: jest.fn(),
|
|
40
38
|
})),
|
|
41
39
|
}));
|
|
42
40
|
|
|
43
|
-
jest.mock(
|
|
41
|
+
jest.mock("../services/widgetValidationService", () => ({
|
|
44
42
|
WidgetValidationService: jest.fn().mockImplementation(() => ({
|
|
45
43
|
shouldDisplayWidget: mockShouldDisplayWidget,
|
|
46
44
|
})),
|
|
47
45
|
}));
|
|
48
46
|
|
|
49
|
-
jest.mock(
|
|
47
|
+
jest.mock("../services/widgetBootstrapService", () => ({
|
|
50
48
|
requestWidgetUrl: jest.fn(),
|
|
51
49
|
}));
|
|
52
50
|
|
|
53
|
-
jest.mock(
|
|
54
|
-
SDK_NAME:
|
|
55
|
-
SDK_VERSION:
|
|
51
|
+
jest.mock("../constants/Constants", () => ({
|
|
52
|
+
SDK_NAME: "rn-widget-sdk",
|
|
53
|
+
SDK_VERSION: "0.1.16",
|
|
56
54
|
}));
|
|
57
55
|
|
|
58
|
-
jest.mock(
|
|
59
|
-
getClientVersion: jest.fn(() =>
|
|
56
|
+
jest.mock("../services/ClientVersionCollector", () => ({
|
|
57
|
+
getClientVersion: jest.fn(() => "1.0.0"),
|
|
60
58
|
}));
|
|
61
59
|
|
|
62
|
-
jest.mock(
|
|
60
|
+
jest.mock("../hooks/useDeviceInfoCollector", () => ({
|
|
63
61
|
getDeviceInfo: jest.fn(() => ({
|
|
64
|
-
platform:
|
|
65
|
-
osVersion:
|
|
62
|
+
platform: "ios",
|
|
63
|
+
osVersion: "16.0",
|
|
66
64
|
screenWidth: 390,
|
|
67
65
|
screenHeight: 844,
|
|
68
66
|
windowWidth: 390,
|
|
69
67
|
windowHeight: 844,
|
|
70
68
|
scale: 3,
|
|
71
69
|
fontScale: 1,
|
|
72
|
-
deviceType:
|
|
73
|
-
model:
|
|
70
|
+
deviceType: "phone",
|
|
71
|
+
model: "iPhone 14 Pro",
|
|
74
72
|
})),
|
|
75
73
|
}));
|
|
76
74
|
|
|
77
75
|
const mockRequestWidgetUrl = requestWidgetUrl as jest.MockedFunction<typeof requestWidgetUrl>;
|
|
78
|
-
const bootstrappedWidgetUrl =
|
|
76
|
+
const bootstrappedWidgetUrl = "https://widgets.solucx.com/widget/bootstrap-result";
|
|
79
77
|
|
|
80
78
|
const baseProps = {
|
|
81
|
-
soluCXKey:
|
|
79
|
+
soluCXKey: "test-key-abc",
|
|
82
80
|
data: {
|
|
83
|
-
customer_id:
|
|
84
|
-
transaction_id:
|
|
85
|
-
form_id:
|
|
81
|
+
customer_id: "cust-001",
|
|
82
|
+
transaction_id: "txn-999",
|
|
83
|
+
form_id: "form-123",
|
|
86
84
|
} as WidgetData,
|
|
87
85
|
options: {
|
|
88
86
|
height: 400,
|
|
@@ -97,219 +95,560 @@ beforeEach(() => {
|
|
|
97
95
|
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
98
96
|
});
|
|
99
97
|
|
|
100
|
-
describe(
|
|
101
|
-
it(
|
|
102
|
-
|
|
103
|
-
<SoluCXWidget {...baseProps} type="modal" />
|
|
104
|
-
);
|
|
98
|
+
describe("SoluCXWidget type routing", () => {
|
|
99
|
+
it("should render ModalWidget when type is modal", async () => {
|
|
100
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
105
101
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const { UNSAFE_getByType } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
103
|
+
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
const modal = UNSAFE_getByType(require("react-native").Modal);
|
|
106
|
+
expect(modal).toBeTruthy();
|
|
107
|
+
});
|
|
108
|
+
}, 10000);
|
|
109
|
+
|
|
110
|
+
it("should render close button inside ModalWidget", async () => {
|
|
111
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
const { getByText } = render(
|
|
112
|
-
<SoluCXWidget {...baseProps} type="modal" />
|
|
113
|
-
);
|
|
113
|
+
const { getByText } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(getByText("✕")).toBeTruthy();
|
|
117
|
+
});
|
|
116
118
|
});
|
|
117
119
|
|
|
118
|
-
it(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
120
|
+
it("should render WebView inside ModalWidget with correct source", async () => {
|
|
121
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
122
|
+
|
|
123
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
const webview = getByTestId("webview");
|
|
127
|
+
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
128
|
+
});
|
|
126
129
|
});
|
|
127
130
|
|
|
128
|
-
it(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
);
|
|
131
|
+
it("should render InlineWidget when type is inline", async () => {
|
|
132
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
133
|
+
|
|
134
|
+
const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="inline" />);
|
|
132
135
|
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
138
|
+
expect(getByText("✕")).toBeTruthy();
|
|
139
|
+
});
|
|
135
140
|
});
|
|
136
141
|
|
|
137
|
-
it(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
);
|
|
142
|
+
it("should not render Modal when type is inline", async () => {
|
|
143
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
144
|
+
|
|
145
|
+
const { UNSAFE_queryByType } = render(<SoluCXWidget {...baseProps} type="inline" />);
|
|
141
146
|
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
const modal = UNSAFE_queryByType(require("react-native").Modal);
|
|
149
|
+
expect(modal).toBeNull();
|
|
150
|
+
});
|
|
144
151
|
});
|
|
145
152
|
|
|
146
|
-
it(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
);
|
|
153
|
+
it("should render OverlayWidget when type is bottom", async () => {
|
|
154
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
155
|
+
|
|
156
|
+
const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="bottom" />);
|
|
150
157
|
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
160
|
+
expect(getByText("✕")).toBeTruthy();
|
|
161
|
+
});
|
|
153
162
|
});
|
|
154
163
|
|
|
155
|
-
it(
|
|
156
|
-
|
|
157
|
-
<SoluCXWidget {...baseProps} type="top" />
|
|
158
|
-
);
|
|
164
|
+
it("should render OverlayWidget when type is top", async () => {
|
|
165
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
159
166
|
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="top" />);
|
|
168
|
+
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
171
|
+
expect(getByText("✕")).toBeTruthy();
|
|
172
|
+
});
|
|
162
173
|
});
|
|
163
174
|
|
|
164
|
-
it(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
);
|
|
175
|
+
it("should not render Modal when type is bottom", async () => {
|
|
176
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
177
|
+
|
|
178
|
+
const { UNSAFE_queryByType } = render(<SoluCXWidget {...baseProps} type="bottom" />);
|
|
168
179
|
|
|
169
|
-
|
|
170
|
-
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
const modal = UNSAFE_queryByType(require("react-native").Modal);
|
|
182
|
+
expect(modal).toBeNull();
|
|
183
|
+
});
|
|
171
184
|
});
|
|
172
185
|
|
|
173
|
-
it(
|
|
186
|
+
it("should call loadSavedData on mount", () => {
|
|
174
187
|
render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
175
188
|
|
|
176
189
|
expect(mockLoadSavedData).toHaveBeenCalledTimes(1);
|
|
177
190
|
});
|
|
178
191
|
});
|
|
179
192
|
|
|
180
|
-
describe(
|
|
181
|
-
it(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
);
|
|
193
|
+
describe("SoluCXWidget WebView configuration", () => {
|
|
194
|
+
it("should set originWhitelist to allow all origins", async () => {
|
|
195
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
196
|
+
|
|
197
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
185
198
|
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
const webview = getByTestId("webview");
|
|
201
|
+
expect(webview.props.originWhitelist).toEqual(["*"]);
|
|
202
|
+
});
|
|
188
203
|
});
|
|
189
204
|
|
|
190
|
-
it(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
);
|
|
205
|
+
it("should set WebView width style to screen width", async () => {
|
|
206
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
207
|
+
|
|
208
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
194
209
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
(s: any) => s.width !== undefined
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
await waitFor(() => {
|
|
211
|
+
const webview = getByTestId("webview");
|
|
212
|
+
const widthStyle = webview.props.style.find((s: any) => s.width !== undefined);
|
|
213
|
+
expect(widthStyle.width).toBeGreaterThan(0);
|
|
214
|
+
});
|
|
200
215
|
});
|
|
201
216
|
|
|
202
|
-
it(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
217
|
+
it("should set WebView height style to widgetHeight", async () => {
|
|
218
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
219
|
+
|
|
220
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
206
221
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
(s: any) => s.height !== undefined
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
await waitFor(() => {
|
|
223
|
+
const webview = getByTestId("webview");
|
|
224
|
+
const heightStyle = webview.props.style.find((s: any) => s.height !== undefined);
|
|
225
|
+
expect(heightStyle.height).toBe(400);
|
|
226
|
+
});
|
|
212
227
|
});
|
|
213
228
|
|
|
214
|
-
it(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
);
|
|
229
|
+
it("should use bootstrapped URL for widgets", async () => {
|
|
230
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
231
|
+
|
|
232
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="inline" />);
|
|
218
233
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
const webview = getByTestId("webview");
|
|
236
|
+
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
237
|
+
});
|
|
222
238
|
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("SoluCXWidget bootstrap flow", () => {
|
|
242
|
+
it("fetches widget URL for form widgets", async () => {
|
|
243
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
223
244
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
245
|
+
const props = {
|
|
246
|
+
...baseProps,
|
|
247
|
+
type: "modal" as const,
|
|
248
|
+
};
|
|
228
249
|
|
|
229
|
-
const
|
|
230
|
-
|
|
250
|
+
const { queryByTestId, getByTestId } = render(<SoluCXWidget {...props} />);
|
|
251
|
+
|
|
252
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
253
|
+
|
|
254
|
+
await waitFor(() => {
|
|
255
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
256
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledWith(
|
|
257
|
+
"test-key-abc",
|
|
258
|
+
expect.objectContaining({
|
|
259
|
+
form_id: "form-123",
|
|
260
|
+
}),
|
|
261
|
+
"test-user-123",
|
|
262
|
+
);
|
|
263
|
+
expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
|
|
264
|
+
expect(mockOpen).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const webview = getByTestId("webview");
|
|
269
|
+
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
231
270
|
});
|
|
232
|
-
});
|
|
233
271
|
|
|
234
|
-
|
|
235
|
-
it('fetches widget URL before rendering non-form widgets', async () => {
|
|
272
|
+
it("fetches widget URL before rendering non-form widgets", async () => {
|
|
236
273
|
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
237
274
|
|
|
238
275
|
const props = {
|
|
239
276
|
...baseProps,
|
|
240
277
|
data: {
|
|
241
|
-
customer_id:
|
|
242
|
-
journey:
|
|
278
|
+
customer_id: "cust-777",
|
|
279
|
+
journey: "sac",
|
|
243
280
|
},
|
|
244
|
-
type:
|
|
281
|
+
type: "bottom" as const,
|
|
245
282
|
};
|
|
246
283
|
|
|
247
|
-
const { queryByTestId, getByTestId } = render(
|
|
248
|
-
<SoluCXWidget {...props} />
|
|
249
|
-
);
|
|
284
|
+
const { queryByTestId, getByTestId } = render(<SoluCXWidget {...props} />);
|
|
250
285
|
|
|
251
|
-
expect(queryByTestId(
|
|
286
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
252
287
|
|
|
253
288
|
await waitFor(() => {
|
|
254
289
|
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
255
290
|
expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
|
|
256
291
|
expect(mockOpen).toHaveBeenCalledTimes(1);
|
|
257
|
-
expect(queryByTestId(
|
|
292
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
258
293
|
});
|
|
259
294
|
|
|
260
|
-
const webview = getByTestId(
|
|
295
|
+
const webview = getByTestId("webview");
|
|
261
296
|
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
262
297
|
});
|
|
263
298
|
|
|
264
|
-
it(
|
|
265
|
-
mockRequestWidgetUrl.mockRejectedValue(new Error(
|
|
299
|
+
it("keeps widget hidden when identifier fetch fails", async () => {
|
|
300
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("network"));
|
|
266
301
|
|
|
267
302
|
const props = {
|
|
268
303
|
...baseProps,
|
|
269
304
|
data: {
|
|
270
|
-
customer_id:
|
|
271
|
-
journey:
|
|
305
|
+
customer_id: "cust-777",
|
|
306
|
+
journey: "sac",
|
|
272
307
|
},
|
|
273
|
-
type:
|
|
308
|
+
type: "bottom" as const,
|
|
274
309
|
};
|
|
275
310
|
|
|
276
|
-
const { queryByTestId } = render(
|
|
277
|
-
<SoluCXWidget {...props} />
|
|
278
|
-
);
|
|
311
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
279
312
|
|
|
280
|
-
expect(queryByTestId(
|
|
313
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
281
314
|
|
|
282
315
|
await waitFor(() => {
|
|
283
316
|
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
284
317
|
expect(mockShouldDisplayWidget).not.toHaveBeenCalled();
|
|
285
|
-
expect(queryByTestId(
|
|
318
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
286
319
|
});
|
|
287
320
|
});
|
|
288
321
|
|
|
289
|
-
it(
|
|
322
|
+
it("keeps widget hidden when validation fails after bootstrap", async () => {
|
|
290
323
|
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
291
|
-
mockShouldDisplayWidget.mockResolvedValue({ canDisplay: false, blockReason:
|
|
324
|
+
mockShouldDisplayWidget.mockResolvedValue({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_RETRY_ATTEMPTS" });
|
|
292
325
|
|
|
293
326
|
const props = {
|
|
294
327
|
...baseProps,
|
|
295
328
|
data: {
|
|
296
|
-
customer_id:
|
|
297
|
-
journey:
|
|
329
|
+
customer_id: "cust-777",
|
|
330
|
+
journey: "sac",
|
|
298
331
|
},
|
|
299
|
-
type:
|
|
332
|
+
type: "bottom" as const,
|
|
300
333
|
};
|
|
301
334
|
|
|
302
|
-
const { queryByTestId } = render(
|
|
303
|
-
<SoluCXWidget {...props} />
|
|
304
|
-
);
|
|
335
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
305
336
|
|
|
306
|
-
expect(queryByTestId(
|
|
337
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
307
338
|
|
|
308
339
|
await waitFor(() => {
|
|
309
340
|
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
310
341
|
expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
|
|
311
342
|
expect(mockOpen).not.toHaveBeenCalled();
|
|
312
|
-
expect(queryByTestId(
|
|
343
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe("SoluCXWidget callbacks", () => {
|
|
349
|
+
it("calls onError when data is not provided", () => {
|
|
350
|
+
const onError = jest.fn();
|
|
351
|
+
const props = {
|
|
352
|
+
...baseProps,
|
|
353
|
+
data: null as any,
|
|
354
|
+
callbacks: { onError },
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
render(<SoluCXWidget {...props} type="modal" />);
|
|
358
|
+
|
|
359
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
360
|
+
expect(onError).toHaveBeenCalledWith("Widget data is required but was not provided");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("calls onError with Error message when bootstrap fails with Error instance", async () => {
|
|
364
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Network timeout"));
|
|
365
|
+
const onError = jest.fn();
|
|
366
|
+
|
|
367
|
+
const props = {
|
|
368
|
+
...baseProps,
|
|
369
|
+
data: {
|
|
370
|
+
customer_id: "cust-777",
|
|
371
|
+
journey: "sac",
|
|
372
|
+
},
|
|
373
|
+
type: "bottom" as const,
|
|
374
|
+
callbacks: { onError },
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
render(<SoluCXWidget {...props} />);
|
|
378
|
+
|
|
379
|
+
await waitFor(() => {
|
|
380
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
381
|
+
expect(onError).toHaveBeenCalledWith("Network timeout");
|
|
382
|
+
expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("calls onError with string when bootstrap fails with string error", async () => {
|
|
387
|
+
mockRequestWidgetUrl.mockRejectedValue("Connection failed");
|
|
388
|
+
const onError = jest.fn();
|
|
389
|
+
|
|
390
|
+
const props = {
|
|
391
|
+
...baseProps,
|
|
392
|
+
data: {
|
|
393
|
+
customer_id: "cust-777",
|
|
394
|
+
journey: "sac",
|
|
395
|
+
},
|
|
396
|
+
type: "bottom" as const,
|
|
397
|
+
callbacks: { onError },
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
render(<SoluCXWidget {...props} />);
|
|
401
|
+
|
|
402
|
+
await waitFor(() => {
|
|
403
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
404
|
+
expect(onError).toHaveBeenCalledWith("Connection failed");
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("calls onError with JSON string when bootstrap fails with object error", async () => {
|
|
409
|
+
mockRequestWidgetUrl.mockRejectedValue({ code: 500, message: "Server error" });
|
|
410
|
+
const onError = jest.fn();
|
|
411
|
+
|
|
412
|
+
const props = {
|
|
413
|
+
...baseProps,
|
|
414
|
+
data: {
|
|
415
|
+
customer_id: "cust-777",
|
|
416
|
+
journey: "sac",
|
|
417
|
+
},
|
|
418
|
+
type: "bottom" as const,
|
|
419
|
+
callbacks: { onError },
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
render(<SoluCXWidget {...props} />);
|
|
423
|
+
|
|
424
|
+
await waitFor(() => {
|
|
425
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
426
|
+
expect(onError).toHaveBeenCalledWith('{"code":500,"message":"Server error"}');
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('calls onError with "Unknown error" when bootstrap fails with null', async () => {
|
|
431
|
+
mockRequestWidgetUrl.mockRejectedValue(null);
|
|
432
|
+
const onError = jest.fn();
|
|
433
|
+
|
|
434
|
+
const props = {
|
|
435
|
+
...baseProps,
|
|
436
|
+
data: {
|
|
437
|
+
customer_id: "cust-777",
|
|
438
|
+
journey: "sac",
|
|
439
|
+
},
|
|
440
|
+
type: "bottom" as const,
|
|
441
|
+
callbacks: { onError },
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
render(<SoluCXWidget {...props} />);
|
|
445
|
+
|
|
446
|
+
await waitFor(() => {
|
|
447
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
448
|
+
expect(onError).toHaveBeenCalledWith("Unknown error");
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("calls onBlock when validation fails", async () => {
|
|
453
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
454
|
+
mockShouldDisplayWidget.mockResolvedValue({
|
|
455
|
+
canDisplay: false,
|
|
456
|
+
blockReason: "BLOCKED_BY_MAX_RETRY_ATTEMPTS",
|
|
457
|
+
});
|
|
458
|
+
const onBlock = jest.fn();
|
|
459
|
+
|
|
460
|
+
const props = {
|
|
461
|
+
...baseProps,
|
|
462
|
+
data: {
|
|
463
|
+
customer_id: "cust-777",
|
|
464
|
+
journey: "sac",
|
|
465
|
+
},
|
|
466
|
+
type: "bottom" as const,
|
|
467
|
+
callbacks: { onBlock },
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
render(<SoluCXWidget {...props} />);
|
|
471
|
+
|
|
472
|
+
await waitFor(() => {
|
|
473
|
+
expect(onBlock).toHaveBeenCalledTimes(1);
|
|
474
|
+
expect(onBlock).toHaveBeenCalledWith("BLOCKED_BY_MAX_RETRY_ATTEMPTS");
|
|
475
|
+
expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("calls onPreOpen, onOpened callbacks for successful bootstrap", async () => {
|
|
480
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
481
|
+
mockShouldDisplayWidget.mockResolvedValue({ canDisplay: true });
|
|
482
|
+
const onPreOpen = jest.fn();
|
|
483
|
+
const onOpened = jest.fn();
|
|
484
|
+
|
|
485
|
+
const props = {
|
|
486
|
+
...baseProps,
|
|
487
|
+
data: {
|
|
488
|
+
customer_id: "cust-777",
|
|
489
|
+
journey: "sac",
|
|
490
|
+
},
|
|
491
|
+
type: "bottom" as const,
|
|
492
|
+
callbacks: { onPreOpen, onOpened },
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
render(<SoluCXWidget {...props} />);
|
|
496
|
+
|
|
497
|
+
await waitFor(() => {
|
|
498
|
+
expect(onPreOpen).toHaveBeenCalledTimes(1);
|
|
499
|
+
expect(onPreOpen).toHaveBeenCalledWith("test-user-123");
|
|
500
|
+
expect(onOpened).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(onOpened).toHaveBeenCalledWith("test-user-123");
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe("SoluCXWidget error handling from requestWidgetUrl", () => {
|
|
507
|
+
it("calls onError when requestWidgetUrl throws error for non-ok response (404)", async () => {
|
|
508
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Failed to fetch widget URL: 404 Not Found"));
|
|
509
|
+
const onError = jest.fn();
|
|
510
|
+
|
|
511
|
+
const props = {
|
|
512
|
+
...baseProps,
|
|
513
|
+
type: "modal" as const,
|
|
514
|
+
callbacks: { onError },
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
518
|
+
|
|
519
|
+
await waitFor(() => {
|
|
520
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
521
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
522
|
+
expect(onError).toHaveBeenCalledWith("Failed to fetch widget URL: 404 Not Found");
|
|
523
|
+
expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
|
|
524
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("calls onError when requestWidgetUrl throws error for non-ok response (500)", async () => {
|
|
529
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Failed to fetch widget URL: 500 Internal Server Error"));
|
|
530
|
+
const onError = jest.fn();
|
|
531
|
+
|
|
532
|
+
const props = {
|
|
533
|
+
...baseProps,
|
|
534
|
+
type: "inline" as const,
|
|
535
|
+
callbacks: { onError },
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
539
|
+
|
|
540
|
+
await waitFor(() => {
|
|
541
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
542
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
543
|
+
expect(onError).toHaveBeenCalledWith("Failed to fetch widget URL: 500 Internal Server Error");
|
|
544
|
+
expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
|
|
545
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("calls onError when requestWidgetUrl throws error for fetch not available", async () => {
|
|
550
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Fetch is not available"));
|
|
551
|
+
const onError = jest.fn();
|
|
552
|
+
|
|
553
|
+
const props = {
|
|
554
|
+
...baseProps,
|
|
555
|
+
type: "bottom" as const,
|
|
556
|
+
callbacks: { onError },
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
560
|
+
|
|
561
|
+
await waitFor(() => {
|
|
562
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
563
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
564
|
+
expect(onError).toHaveBeenCalledWith("Fetch is not available");
|
|
565
|
+
expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
|
|
566
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("calls onError when requestWidgetUrl throws error for missing URL in response", async () => {
|
|
571
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Widget URL not found in response"));
|
|
572
|
+
const onError = jest.fn();
|
|
573
|
+
|
|
574
|
+
const props = {
|
|
575
|
+
...baseProps,
|
|
576
|
+
type: "top" as const,
|
|
577
|
+
callbacks: { onError },
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
581
|
+
|
|
582
|
+
await waitFor(() => {
|
|
583
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
584
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
585
|
+
expect(onError).toHaveBeenCalledWith("Widget URL not found in response");
|
|
586
|
+
expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
|
|
587
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
it("does not call shouldDisplayWidget when requestWidgetUrl fails", async () => {
|
|
592
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Failed to fetch widget URL: 403 Forbidden"));
|
|
593
|
+
const onError = jest.fn();
|
|
594
|
+
|
|
595
|
+
const props = {
|
|
596
|
+
...baseProps,
|
|
597
|
+
type: "modal" as const,
|
|
598
|
+
callbacks: { onError },
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
render(<SoluCXWidget {...props} />);
|
|
602
|
+
|
|
603
|
+
await waitFor(() => {
|
|
604
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
605
|
+
expect(mockShouldDisplayWidget).not.toHaveBeenCalled();
|
|
606
|
+
expect(mockOpen).not.toHaveBeenCalled();
|
|
607
|
+
expect(onError).toHaveBeenCalledWith("Failed to fetch widget URL: 403 Forbidden");
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("does not call onPreOpen or onOpened when requestWidgetUrl fails", async () => {
|
|
612
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Failed to fetch widget URL: 401 Unauthorized"));
|
|
613
|
+
const onPreOpen = jest.fn();
|
|
614
|
+
const onOpened = jest.fn();
|
|
615
|
+
const onError = jest.fn();
|
|
616
|
+
|
|
617
|
+
const props = {
|
|
618
|
+
...baseProps,
|
|
619
|
+
type: "inline" as const,
|
|
620
|
+
callbacks: { onPreOpen, onOpened, onError },
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
render(<SoluCXWidget {...props} />);
|
|
624
|
+
|
|
625
|
+
await waitFor(() => {
|
|
626
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
627
|
+
expect(onPreOpen).not.toHaveBeenCalled();
|
|
628
|
+
expect(onOpened).not.toHaveBeenCalled();
|
|
629
|
+
expect(onError).toHaveBeenCalledWith("Failed to fetch widget URL: 401 Unauthorized");
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it("widget remains hidden when requestWidgetUrl fails with 404", async () => {
|
|
634
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("Failed to fetch widget URL: 404 Not Found"));
|
|
635
|
+
|
|
636
|
+
const props = {
|
|
637
|
+
...baseProps,
|
|
638
|
+
type: "bottom" as const,
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
642
|
+
|
|
643
|
+
// Initially null
|
|
644
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
645
|
+
|
|
646
|
+
await waitFor(() => {
|
|
647
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
313
648
|
});
|
|
649
|
+
|
|
650
|
+
// Should remain null after failure
|
|
651
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
652
|
+
expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
|
|
314
653
|
});
|
|
315
654
|
});
|