@solucx/react-native-solucx-widget 0.1.16 → 0.2.1
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 +108 -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 +504 -0
- package/src/__tests__/e2e/widget-lifecycle.test.tsx +352 -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 +4 -4
- package/src/index.ts +4 -0
- package/src/interfaces/WidgetCallbacks.ts +14 -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,504 @@
|
|
|
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) => <View testID="webview" {...props} ref={ref} />),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const mockLoadSavedData = jest.fn();
|
|
17
|
+
const mockClose = jest.fn();
|
|
18
|
+
const mockSetIsWidgetVisible = jest.fn();
|
|
19
|
+
const mockOpen = jest.fn();
|
|
20
|
+
const mockShouldDisplayWidget = jest.fn();
|
|
21
|
+
|
|
22
|
+
jest.mock("../hooks/useWidgetState", () => ({
|
|
23
|
+
useWidgetState: () => ({
|
|
24
|
+
widgetHeight: 400,
|
|
25
|
+
isWidgetVisible: true,
|
|
26
|
+
setIsWidgetVisible: mockSetIsWidgetVisible,
|
|
27
|
+
loadSavedData: mockLoadSavedData,
|
|
28
|
+
resize: jest.fn(),
|
|
29
|
+
open: mockOpen,
|
|
30
|
+
close: mockClose,
|
|
31
|
+
userId: "test-user-123",
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.mock("../services/widgetEventService", () => ({
|
|
36
|
+
WidgetEventService: jest.fn().mockImplementation(() => ({
|
|
37
|
+
handleMessage: jest.fn(),
|
|
38
|
+
})),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
jest.mock("../services/widgetValidationService", () => ({
|
|
42
|
+
WidgetValidationService: jest.fn().mockImplementation(() => ({
|
|
43
|
+
shouldDisplayWidget: mockShouldDisplayWidget,
|
|
44
|
+
})),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
jest.mock("../services/widgetBootstrapService", () => ({
|
|
48
|
+
requestWidgetUrl: jest.fn(),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
jest.mock("../constants/Constants", () => ({
|
|
52
|
+
SDK_NAME: "rn-widget-sdk",
|
|
53
|
+
SDK_VERSION: "0.1.16",
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
jest.mock("../services/ClientVersionCollector", () => ({
|
|
57
|
+
getClientVersion: jest.fn(() => "1.0.0"),
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
jest.mock("../hooks/useDeviceInfoCollector", () => ({
|
|
61
|
+
getDeviceInfo: jest.fn(() => ({
|
|
62
|
+
platform: "ios",
|
|
63
|
+
osVersion: "16.0",
|
|
64
|
+
screenWidth: 390,
|
|
65
|
+
screenHeight: 844,
|
|
66
|
+
windowWidth: 390,
|
|
67
|
+
windowHeight: 844,
|
|
68
|
+
scale: 3,
|
|
69
|
+
fontScale: 1,
|
|
70
|
+
deviceType: "phone",
|
|
71
|
+
model: "iPhone 14 Pro",
|
|
72
|
+
})),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
const mockRequestWidgetUrl = requestWidgetUrl as jest.MockedFunction<typeof requestWidgetUrl>;
|
|
76
|
+
const bootstrappedWidgetUrl = "https://widgets.solucx.com/widget/bootstrap-result";
|
|
77
|
+
|
|
78
|
+
const baseProps = {
|
|
79
|
+
soluCXKey: "test-key-abc",
|
|
80
|
+
data: {
|
|
81
|
+
customer_id: "cust-001",
|
|
82
|
+
transaction_id: "txn-999",
|
|
83
|
+
form_id: "form-123",
|
|
84
|
+
} as WidgetData,
|
|
85
|
+
options: {
|
|
86
|
+
height: 400,
|
|
87
|
+
} as WidgetOptions,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
jest.clearAllMocks();
|
|
92
|
+
mockShouldDisplayWidget.mockReset();
|
|
93
|
+
mockShouldDisplayWidget.mockResolvedValue({ canDisplay: true });
|
|
94
|
+
mockRequestWidgetUrl.mockReset();
|
|
95
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("SoluCXWidget type routing", () => {
|
|
99
|
+
it("should render ModalWidget when type is modal", async () => {
|
|
100
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
101
|
+
|
|
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);
|
|
112
|
+
|
|
113
|
+
const { getByText } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
114
|
+
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(getByText("✕")).toBeTruthy();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should render WebView inside ModalWidget with correct source", async () => {
|
|
121
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
122
|
+
|
|
123
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
124
|
+
|
|
125
|
+
await waitFor(() => {
|
|
126
|
+
const webview = getByTestId("webview");
|
|
127
|
+
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should render InlineWidget when type is inline", async () => {
|
|
132
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
133
|
+
|
|
134
|
+
const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="inline" />);
|
|
135
|
+
|
|
136
|
+
await waitFor(() => {
|
|
137
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
138
|
+
expect(getByText("✕")).toBeTruthy();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
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" />);
|
|
146
|
+
|
|
147
|
+
await waitFor(() => {
|
|
148
|
+
const modal = UNSAFE_queryByType(require("react-native").Modal);
|
|
149
|
+
expect(modal).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should render OverlayWidget when type is bottom", async () => {
|
|
154
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
155
|
+
|
|
156
|
+
const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="bottom" />);
|
|
157
|
+
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
160
|
+
expect(getByText("✕")).toBeTruthy();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should render OverlayWidget when type is top", async () => {
|
|
165
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
166
|
+
|
|
167
|
+
const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="top" />);
|
|
168
|
+
|
|
169
|
+
await waitFor(() => {
|
|
170
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
171
|
+
expect(getByText("✕")).toBeTruthy();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
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" />);
|
|
179
|
+
|
|
180
|
+
await waitFor(() => {
|
|
181
|
+
const modal = UNSAFE_queryByType(require("react-native").Modal);
|
|
182
|
+
expect(modal).toBeNull();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should call loadSavedData on mount", () => {
|
|
187
|
+
render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
188
|
+
|
|
189
|
+
expect(mockLoadSavedData).toHaveBeenCalledTimes(1);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
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" />);
|
|
198
|
+
|
|
199
|
+
await waitFor(() => {
|
|
200
|
+
const webview = getByTestId("webview");
|
|
201
|
+
expect(webview.props.originWhitelist).toEqual(["*"]);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("should set WebView width style to screen width", async () => {
|
|
206
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
207
|
+
|
|
208
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
209
|
+
|
|
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
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should set WebView height style to widgetHeight", async () => {
|
|
218
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
219
|
+
|
|
220
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
|
|
221
|
+
|
|
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
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should use bootstrapped URL for widgets", async () => {
|
|
230
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
231
|
+
|
|
232
|
+
const { getByTestId } = render(<SoluCXWidget {...baseProps} type="inline" />);
|
|
233
|
+
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
const webview = getByTestId("webview");
|
|
236
|
+
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("SoluCXWidget bootstrap flow", () => {
|
|
242
|
+
it("fetches widget URL for form widgets", async () => {
|
|
243
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
244
|
+
|
|
245
|
+
const props = {
|
|
246
|
+
...baseProps,
|
|
247
|
+
type: "modal" as const,
|
|
248
|
+
};
|
|
249
|
+
|
|
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);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("fetches widget URL before rendering non-form widgets", async () => {
|
|
273
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
274
|
+
|
|
275
|
+
const props = {
|
|
276
|
+
...baseProps,
|
|
277
|
+
data: {
|
|
278
|
+
customer_id: "cust-777",
|
|
279
|
+
journey: "sac",
|
|
280
|
+
},
|
|
281
|
+
type: "bottom" as const,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const { queryByTestId, getByTestId } = render(<SoluCXWidget {...props} />);
|
|
285
|
+
|
|
286
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
287
|
+
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
290
|
+
expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
|
|
291
|
+
expect(mockOpen).toHaveBeenCalledTimes(1);
|
|
292
|
+
expect(queryByTestId("webview")).toBeTruthy();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const webview = getByTestId("webview");
|
|
296
|
+
expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("keeps widget hidden when identifier fetch fails", async () => {
|
|
300
|
+
mockRequestWidgetUrl.mockRejectedValue(new Error("network"));
|
|
301
|
+
|
|
302
|
+
const props = {
|
|
303
|
+
...baseProps,
|
|
304
|
+
data: {
|
|
305
|
+
customer_id: "cust-777",
|
|
306
|
+
journey: "sac",
|
|
307
|
+
},
|
|
308
|
+
type: "bottom" as const,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
312
|
+
|
|
313
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
314
|
+
|
|
315
|
+
await waitFor(() => {
|
|
316
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
317
|
+
expect(mockShouldDisplayWidget).not.toHaveBeenCalled();
|
|
318
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("keeps widget hidden when validation fails after bootstrap", async () => {
|
|
323
|
+
mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
|
|
324
|
+
mockShouldDisplayWidget.mockResolvedValue({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_RETRY_ATTEMPTS" });
|
|
325
|
+
|
|
326
|
+
const props = {
|
|
327
|
+
...baseProps,
|
|
328
|
+
data: {
|
|
329
|
+
customer_id: "cust-777",
|
|
330
|
+
journey: "sac",
|
|
331
|
+
},
|
|
332
|
+
type: "bottom" as const,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const { queryByTestId } = render(<SoluCXWidget {...props} />);
|
|
336
|
+
|
|
337
|
+
expect(queryByTestId("webview")).toBeNull();
|
|
338
|
+
|
|
339
|
+
await waitFor(() => {
|
|
340
|
+
expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
|
|
341
|
+
expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(mockOpen).not.toHaveBeenCalled();
|
|
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
|
+
});
|