@solucx/react-native-solucx-widget 0.2.5 → 2.0.7

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 (256) hide show
  1. package/README.md +526 -182
  2. package/lib/SoluCXWidget.d.ts +50 -7
  3. package/lib/SoluCXWidget.d.ts.map +1 -1
  4. package/lib/SoluCXWidget.js +105 -101
  5. package/lib/SoluCXWidget.js.map +1 -1
  6. package/lib/SoluCXWidgetHost.d.ts +3 -0
  7. package/lib/SoluCXWidgetHost.d.ts.map +1 -0
  8. package/lib/SoluCXWidgetHost.js +34 -0
  9. package/lib/SoluCXWidgetHost.js.map +1 -0
  10. package/lib/SoluCXWidgetView.d.ts +12 -0
  11. package/lib/SoluCXWidgetView.d.ts.map +1 -0
  12. package/lib/SoluCXWidgetView.js +61 -0
  13. package/lib/SoluCXWidgetView.js.map +1 -0
  14. package/lib/components/CloseButton.d.ts +1 -1
  15. package/lib/components/CloseButton.d.ts.map +1 -1
  16. package/lib/components/CloseButton.js +4 -1
  17. package/lib/components/CloseButton.js.map +1 -1
  18. package/lib/components/InlineWidget.d.ts.map +1 -1
  19. package/lib/components/InlineWidget.js +2 -7
  20. package/lib/components/InlineWidget.js.map +1 -1
  21. package/lib/components/ModalWidget.d.ts +1 -1
  22. package/lib/components/ModalWidget.d.ts.map +1 -1
  23. package/lib/components/ModalWidget.js +3 -16
  24. package/lib/components/ModalWidget.js.map +1 -1
  25. package/lib/components/OverlayWidget.d.ts.map +1 -1
  26. package/lib/components/OverlayWidget.js +5 -15
  27. package/lib/components/OverlayWidget.js.map +1 -1
  28. package/lib/components/index.d.ts +5 -0
  29. package/lib/components/index.d.ts.map +1 -0
  30. package/lib/components/index.js +12 -0
  31. package/lib/components/index.js.map +1 -0
  32. package/lib/constants/Constants.d.ts +11 -0
  33. package/lib/constants/Constants.d.ts.map +1 -1
  34. package/lib/constants/Constants.js +16 -1
  35. package/lib/constants/Constants.js.map +1 -1
  36. package/lib/{interfaces → domain}/WidgetCallbacks.d.ts +2 -2
  37. package/lib/domain/WidgetCallbacks.d.ts.map +1 -0
  38. package/lib/domain/WidgetCallbacks.js.map +1 -0
  39. package/{src/interfaces/WidgetData.ts → lib/domain/WidgetData.d.ts} +5 -2
  40. package/lib/domain/WidgetData.d.ts.map +1 -0
  41. package/lib/{interfaces → domain}/WidgetData.js.map +1 -1
  42. package/lib/domain/WidgetDisplayResult.d.ts +6 -0
  43. package/lib/domain/WidgetDisplayResult.d.ts.map +1 -0
  44. package/lib/domain/WidgetDisplayResult.js +3 -0
  45. package/lib/domain/WidgetDisplayResult.js.map +1 -0
  46. package/lib/domain/WidgetOptions.d.ts +27 -0
  47. package/lib/domain/WidgetOptions.d.ts.map +1 -0
  48. package/lib/domain/WidgetOptions.js +30 -0
  49. package/lib/domain/WidgetOptions.js.map +1 -0
  50. package/lib/domain/WidgetResponse.d.ts +5 -0
  51. package/lib/domain/WidgetResponse.d.ts.map +1 -0
  52. package/lib/{interfaces/WidgetOptions.js → domain/WidgetResponse.js} +1 -1
  53. package/lib/domain/WidgetResponse.js.map +1 -0
  54. package/lib/domain/WidgetSamplerLog.d.ts +12 -0
  55. package/lib/domain/WidgetSamplerLog.d.ts.map +1 -0
  56. package/lib/domain/WidgetSamplerLog.js.map +1 -0
  57. package/lib/{interfaces → domain}/index.d.ts +1 -2
  58. package/lib/domain/index.d.ts.map +1 -0
  59. package/lib/{interfaces → domain}/index.js.map +1 -1
  60. package/lib/hooks/index.d.ts +2 -2
  61. package/lib/hooks/index.d.ts.map +1 -1
  62. package/lib/hooks/index.js +5 -5
  63. package/lib/hooks/index.js.map +1 -1
  64. package/lib/hooks/useClientVersionCollector.d.ts +3 -0
  65. package/lib/hooks/useClientVersionCollector.d.ts.map +1 -0
  66. package/lib/{services/ClientVersionCollector.js → hooks/useClientVersionCollector.js} +7 -2
  67. package/lib/hooks/useClientVersionCollector.js.map +1 -0
  68. package/lib/hooks/useHeightAnimation.d.ts +0 -1
  69. package/lib/hooks/useHeightAnimation.d.ts.map +1 -1
  70. package/lib/hooks/useHeightAnimation.js +4 -2
  71. package/lib/hooks/useHeightAnimation.js.map +1 -1
  72. package/lib/hooks/useWidget.d.ts +13 -0
  73. package/lib/hooks/useWidget.d.ts.map +1 -0
  74. package/lib/hooks/useWidget.js +44 -0
  75. package/lib/hooks/useWidget.js.map +1 -0
  76. package/lib/hooks/useWidgetBootstrap.d.ts +21 -0
  77. package/lib/hooks/useWidgetBootstrap.d.ts.map +1 -0
  78. package/lib/hooks/useWidgetBootstrap.js +87 -0
  79. package/lib/hooks/useWidgetBootstrap.js.map +1 -0
  80. package/lib/hooks/useWidgetServices.d.ts +19 -0
  81. package/lib/hooks/useWidgetServices.d.ts.map +1 -0
  82. package/lib/hooks/useWidgetServices.js +34 -0
  83. package/lib/hooks/useWidgetServices.js.map +1 -0
  84. package/lib/hooks/useWidgetUI.d.ts +9 -0
  85. package/lib/hooks/useWidgetUI.d.ts.map +1 -0
  86. package/lib/hooks/useWidgetUI.js +33 -0
  87. package/lib/hooks/useWidgetUI.js.map +1 -0
  88. package/lib/index.d.ts +10 -11
  89. package/lib/index.d.ts.map +1 -1
  90. package/lib/index.js +13 -38
  91. package/lib/index.js.map +1 -1
  92. package/lib/services/UserIdentificationService.d.ts +3 -0
  93. package/lib/services/UserIdentificationService.d.ts.map +1 -0
  94. package/lib/services/UserIdentificationService.js +17 -0
  95. package/lib/services/UserIdentificationService.js.map +1 -0
  96. package/lib/services/WidgetBootstrapService.d.ts +12 -0
  97. package/lib/services/WidgetBootstrapService.d.ts.map +1 -0
  98. package/lib/services/{widgetBootstrapService.js → WidgetBootstrapService.js} +36 -15
  99. package/lib/services/WidgetBootstrapService.js.map +1 -0
  100. package/lib/services/WidgetEventService.d.ts +8 -0
  101. package/lib/services/WidgetEventService.d.ts.map +1 -0
  102. package/lib/services/WidgetEventService.js +14 -0
  103. package/lib/services/WidgetEventService.js.map +1 -0
  104. package/lib/services/WidgetStateManager.d.ts +20 -0
  105. package/lib/services/WidgetStateManager.d.ts.map +1 -0
  106. package/lib/services/WidgetStateManager.js +93 -0
  107. package/lib/services/WidgetStateManager.js.map +1 -0
  108. package/lib/services/WidgetValidationService.d.ts +17 -0
  109. package/lib/services/WidgetValidationService.d.ts.map +1 -0
  110. package/lib/services/WidgetValidationService.js +132 -0
  111. package/lib/services/WidgetValidationService.js.map +1 -0
  112. package/lib/services/events/EventHandlerFactory.d.ts +18 -0
  113. package/lib/services/events/EventHandlerFactory.d.ts.map +1 -0
  114. package/lib/services/events/EventHandlerFactory.js +67 -0
  115. package/lib/services/events/EventHandlerFactory.js.map +1 -0
  116. package/lib/services/events/EventHandlers.d.ts +10 -0
  117. package/lib/services/events/EventHandlers.d.ts.map +1 -0
  118. package/lib/services/events/EventHandlers.js +72 -0
  119. package/lib/services/events/EventHandlers.js.map +1 -0
  120. package/lib/services/events/index.d.ts +3 -0
  121. package/lib/services/events/index.d.ts.map +1 -0
  122. package/lib/services/events/index.js +21 -0
  123. package/lib/services/events/index.js.map +1 -0
  124. package/lib/services/height/HeightStrategies.d.ts +3 -0
  125. package/lib/services/height/HeightStrategies.d.ts.map +1 -0
  126. package/lib/services/height/HeightStrategies.js +14 -0
  127. package/lib/services/height/HeightStrategies.js.map +1 -0
  128. package/lib/services/storage/AsyncStorageService.d.ts +13 -0
  129. package/lib/services/storage/AsyncStorageService.d.ts.map +1 -0
  130. package/lib/services/storage/AsyncStorageService.js +73 -0
  131. package/lib/services/storage/AsyncStorageService.js.map +1 -0
  132. package/lib/services/storage/IStorageService.d.ts +30 -0
  133. package/lib/services/storage/IStorageService.d.ts.map +1 -0
  134. package/lib/services/storage/IStorageService.js +3 -0
  135. package/lib/services/storage/IStorageService.js.map +1 -0
  136. package/lib/services/storage/StorageIdBuilder.d.ts +11 -0
  137. package/lib/services/storage/StorageIdBuilder.d.ts.map +1 -0
  138. package/lib/services/storage/StorageIdBuilder.js +17 -0
  139. package/lib/services/storage/StorageIdBuilder.js.map +1 -0
  140. package/lib/services/storage/index.d.ts +3 -0
  141. package/lib/services/storage/index.d.ts.map +1 -0
  142. package/lib/services/storage/index.js +6 -0
  143. package/lib/services/storage/index.js.map +1 -0
  144. package/lib/styles/widgetStyles.d.ts +1 -1
  145. package/lib/styles/widgetStyles.d.ts.map +1 -1
  146. package/package.json +8 -2
  147. package/src/SoluCXWidget.ts +144 -0
  148. package/src/SoluCXWidgetHost.tsx +44 -0
  149. package/src/SoluCXWidgetView.tsx +97 -0
  150. package/src/__tests__/ClientVersionCollector.test.ts +5 -5
  151. package/src/__tests__/OverlayWidget.rendering.test.tsx +12 -14
  152. package/src/__tests__/SoluCXWidget.rendering.test.tsx +103 -60
  153. package/src/__tests__/SoluCXWidget.test.ts +448 -0
  154. package/src/__tests__/WidgetValidationService.test.ts +408 -0
  155. package/src/__tests__/e2e/widget-lifecycle.test.tsx +14 -23
  156. package/src/__tests__/index.test.tsx +39 -0
  157. package/src/__tests__/integration/webview-communication-simple.test.tsx +8 -6
  158. package/src/__tests__/integration/webview-communication.test.tsx +127 -130
  159. package/src/__tests__/normalizeWidgetOptions.test.ts +80 -0
  160. package/src/__tests__/useWidgetBootstrap.test.ts +634 -0
  161. package/src/__tests__/useWidgetState.test.ts +56 -13
  162. package/src/__tests__/widgetBootstrapService.test.ts +15 -17
  163. package/src/components/CloseButton.tsx +6 -2
  164. package/src/components/InlineWidget.tsx +4 -9
  165. package/src/components/ModalWidget.tsx +15 -45
  166. package/src/components/OverlayWidget.tsx +5 -15
  167. package/src/components/index.ts +4 -0
  168. package/src/constants/Constants.ts +15 -0
  169. package/src/{interfaces → domain}/WidgetCallbacks.ts +2 -2
  170. package/{lib/interfaces/WidgetData.d.ts → src/domain/WidgetData.ts} +3 -2
  171. package/src/domain/WidgetDisplayResult.ts +16 -0
  172. package/src/domain/WidgetOptions.ts +53 -0
  173. package/src/domain/WidgetResponse.ts +5 -0
  174. package/src/domain/WidgetSamplerLog.ts +11 -0
  175. package/src/{interfaces → domain}/index.ts +1 -2
  176. package/src/hooks/index.ts +2 -2
  177. package/src/{services/ClientVersionCollector.ts → hooks/useClientVersionCollector.ts} +6 -0
  178. package/src/hooks/useHeightAnimation.ts +6 -3
  179. package/src/hooks/useWidget.ts +46 -0
  180. package/src/hooks/useWidgetBootstrap.ts +117 -0
  181. package/src/hooks/useWidgetServices.ts +44 -0
  182. package/src/hooks/useWidgetUI.ts +38 -0
  183. package/src/index.ts +16 -11
  184. package/src/services/UserIdentificationService.ts +14 -0
  185. package/src/services/{widgetBootstrapService.ts → WidgetBootstrapService.ts} +43 -19
  186. package/src/services/WidgetEventService.ts +15 -0
  187. package/src/services/WidgetStateManager.ts +115 -0
  188. package/src/services/WidgetValidationService.ts +149 -0
  189. package/src/services/events/EventHandlerFactory.ts +70 -0
  190. package/src/services/events/EventHandlers.ts +67 -0
  191. package/src/services/events/index.ts +2 -0
  192. package/src/services/height/HeightStrategies.ts +15 -0
  193. package/src/services/storage/AsyncStorageService.ts +74 -0
  194. package/src/services/storage/IStorageService.ts +32 -0
  195. package/src/services/storage/StorageIdBuilder.ts +15 -0
  196. package/src/services/storage/index.ts +2 -0
  197. package/src/styles/widgetStyles.ts +1 -1
  198. package/README.intern.md +0 -490
  199. package/lib/constants/webViewConstants.d.ts +0 -12
  200. package/lib/constants/webViewConstants.d.ts.map +0 -1
  201. package/lib/constants/webViewConstants.js +0 -19
  202. package/lib/constants/webViewConstants.js.map +0 -1
  203. package/lib/hooks/useWidgetHeight.d.ts +0 -13
  204. package/lib/hooks/useWidgetHeight.d.ts.map +0 -1
  205. package/lib/hooks/useWidgetHeight.js +0 -21
  206. package/lib/hooks/useWidgetHeight.js.map +0 -1
  207. package/lib/hooks/useWidgetState.d.ts +0 -15
  208. package/lib/hooks/useWidgetState.d.ts.map +0 -1
  209. package/lib/hooks/useWidgetState.js +0 -79
  210. package/lib/hooks/useWidgetState.js.map +0 -1
  211. package/lib/interfaces/WidgetCallbacks.d.ts.map +0 -1
  212. package/lib/interfaces/WidgetCallbacks.js.map +0 -1
  213. package/lib/interfaces/WidgetData.d.ts.map +0 -1
  214. package/lib/interfaces/WidgetOptions.d.ts +0 -9
  215. package/lib/interfaces/WidgetOptions.d.ts.map +0 -1
  216. package/lib/interfaces/WidgetOptions.js.map +0 -1
  217. package/lib/interfaces/WidgetResponse.d.ts +0 -10
  218. package/lib/interfaces/WidgetResponse.d.ts.map +0 -1
  219. package/lib/interfaces/WidgetResponse.js +0 -12
  220. package/lib/interfaces/WidgetResponse.js.map +0 -1
  221. package/lib/interfaces/WidgetSamplerLog.d.ts +0 -7
  222. package/lib/interfaces/WidgetSamplerLog.d.ts.map +0 -1
  223. package/lib/interfaces/WidgetSamplerLog.js.map +0 -1
  224. package/lib/interfaces/index.d.ts.map +0 -1
  225. package/lib/services/ClientVersionCollector.d.ts +0 -2
  226. package/lib/services/ClientVersionCollector.d.ts.map +0 -1
  227. package/lib/services/ClientVersionCollector.js.map +0 -1
  228. package/lib/services/storage.d.ts +0 -8
  229. package/lib/services/storage.d.ts.map +0 -1
  230. package/lib/services/storage.js +0 -23
  231. package/lib/services/storage.js.map +0 -1
  232. package/lib/services/widgetBootstrapService.d.ts +0 -6
  233. package/lib/services/widgetBootstrapService.d.ts.map +0 -1
  234. package/lib/services/widgetBootstrapService.js.map +0 -1
  235. package/lib/services/widgetEventService.d.ts +0 -19
  236. package/lib/services/widgetEventService.d.ts.map +0 -1
  237. package/lib/services/widgetEventService.js +0 -79
  238. package/lib/services/widgetEventService.js.map +0 -1
  239. package/lib/services/widgetValidationService.d.ts +0 -18
  240. package/lib/services/widgetValidationService.d.ts.map +0 -1
  241. package/lib/services/widgetValidationService.js +0 -71
  242. package/lib/services/widgetValidationService.js.map +0 -1
  243. package/src/SoluCXWidget.tsx +0 -179
  244. package/src/constants/webViewConstants.ts +0 -15
  245. package/src/hooks/useWidgetHeight.ts +0 -38
  246. package/src/hooks/useWidgetState.ts +0 -101
  247. package/src/interfaces/WidgetOptions.ts +0 -8
  248. package/src/interfaces/WidgetResponse.ts +0 -15
  249. package/src/interfaces/WidgetSamplerLog.ts +0 -6
  250. package/src/services/storage.ts +0 -21
  251. package/src/services/widgetEventService.ts +0 -110
  252. package/src/services/widgetValidationService.ts +0 -102
  253. /package/lib/{interfaces → domain}/WidgetCallbacks.js +0 -0
  254. /package/lib/{interfaces → domain}/WidgetData.js +0 -0
  255. /package/lib/{interfaces → domain}/WidgetSamplerLog.js +0 -0
  256. /package/lib/{interfaces → domain}/index.js +0 -0
@@ -1,35 +1,35 @@
1
1
  /**
2
2
  * INTEGRATION TEST: WebView Communication
3
- *
3
+ *
4
4
  * CRITICAL: Tests the core WebView message passing bridge between
5
5
  * the embedded widget and React Native.
6
- *
6
+ *
7
7
  * Coverage Target: handleWebViewMessage, handleWebViewLoad, handleClose
8
8
  * Previous Coverage: 0% (CRITICAL GAP)
9
- *
9
+ *
10
10
  * Strategy: Minimal mocking - use real WidgetEventService and components
11
11
  */
12
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';
13
+ import React from "react";
14
+ import { render, waitFor, fireEvent } from "@testing-library/react-native";
15
+ import { SoluCXWidgetView as SoluCXWidget } from "../../SoluCXWidgetView";
16
+ import type { WidgetCallbacks } from "../../domain";
17
17
 
18
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');
19
+ jest.mock("react-native-webview", () => {
20
+ const React = require("react");
21
+ const { View } = require("react-native");
22
22
  return {
23
23
  __esModule: true,
24
24
  WebView: React.forwardRef((props: any, ref: any) => {
25
25
  // Store ref methods for testing
26
26
  React.useImperativeHandle(ref, () => ({
27
- injectJavaScript: jest.fn((script) => {
27
+ injectJavaScript: jest.fn(script => {
28
28
  // Simulate successful injection
29
29
  if (props.onLoadEnd) props.onLoadEnd();
30
30
  }),
31
31
  }));
32
-
32
+
33
33
  return (
34
34
  <View testID="webview" {...props}>
35
35
  {/* Expose onMessage handler for testing */}
@@ -40,19 +40,21 @@ jest.mock('react-native-webview', () => {
40
40
  };
41
41
  });
42
42
 
43
- jest.mock('../../services/widgetBootstrapService', () => ({
44
- requestWidgetUrl: jest.fn().mockResolvedValue('https://mock.widget.url/form123'),
43
+ jest.mock("../../services/WidgetBootstrapService", () => ({
44
+ requestWidgetUrl: jest.fn().mockResolvedValue({ available: true, url: "https://mock.widget.url/form123" }),
45
45
  }));
46
46
 
47
- jest.mock('../../services/widgetValidationService', () => ({
47
+ jest.mock("../../services/WidgetValidationService", () => ({
48
48
  WidgetValidationService: jest.fn().mockImplementation(() => ({
49
+ shouldDisplayForTransaction: jest.fn().mockResolvedValue({ canDisplay: true }),
50
+ shouldDisplayForTransactionAlreadyAnswered: jest.fn().mockResolvedValue({ canDisplay: true }),
49
51
  shouldDisplayWidget: jest.fn().mockResolvedValue({ canDisplay: true }),
50
52
  })),
51
53
  }));
52
54
 
53
55
  // Mock storage but keep it functional
54
56
  const mockStorage: Record<string, string> = {};
55
- jest.mock('@react-native-async-storage/async-storage', () => ({
57
+ jest.mock("@react-native-async-storage/async-storage", () => ({
56
58
  __esModule: true,
57
59
  default: {
58
60
  getItem: jest.fn((key: string) => Promise.resolve(mockStorage[key] || null)),
@@ -67,13 +69,13 @@ jest.mock('@react-native-async-storage/async-storage', () => ({
67
69
  },
68
70
  }));
69
71
 
70
- describe('Integration: WebView Communication', () => {
72
+ describe("Integration: WebView Communication", () => {
71
73
  const baseProps = {
72
- soluCXKey: 'test-key-123',
73
- type: 'modal' as const,
74
- data: {
75
- customer_id: 'user-456',
76
- form_id: 'form-789', // Required for form mode
74
+ soluCXKey: "test-key-123",
75
+ type: "modal" as const,
76
+ data: {
77
+ customer_id: "user-456",
78
+ form_id: "form-789", // Required for form mode
77
79
  },
78
80
  options: {
79
81
  height: 400,
@@ -85,27 +87,25 @@ describe('Integration: WebView Communication', () => {
85
87
  Object.keys(mockStorage).forEach(key => delete mockStorage[key]);
86
88
  });
87
89
 
88
- describe('Message Handling - Form Events', () => {
89
- it('should handle FORM_CLOSE message and call onClosed callback', async () => {
90
+ describe("Message Handling - Form Events", () => {
91
+ it("should handle FORM_CLOSE message and call onClosed callback", async () => {
90
92
  const mockOnClosed = jest.fn();
91
93
  const callbacks: WidgetCallbacks = {
92
94
  onClosed: mockOnClosed,
93
95
  };
94
96
 
95
- const { getByTestId } = render(
96
- <SoluCXWidget {...baseProps} callbacks={callbacks} />
97
- );
97
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
98
98
 
99
99
  // Wait for widget to be ready
100
100
  await waitFor(() => {
101
- expect(getByTestId('webview')).toBeTruthy();
101
+ expect(getByTestId("webview")).toBeTruthy();
102
102
  });
103
103
 
104
- const webview = getByTestId('webview');
104
+ const webview = getByTestId("webview");
105
105
 
106
106
  // Simulate WebView sending FORM_CLOSE message
107
- fireEvent(webview, 'message', {
108
- nativeEvent: { data: 'FORM_CLOSE' },
107
+ fireEvent(webview, "message", {
108
+ nativeEvent: { data: "FORM_CLOSE" },
109
109
  });
110
110
 
111
111
  // Verify callback was called
@@ -114,71 +114,84 @@ describe('Integration: WebView Communication', () => {
114
114
  });
115
115
  }, 15000); // 15 second timeout for slower CI environments
116
116
 
117
- it('should handle FORM_ERROR message with error text', async () => {
117
+ it("should handle FORM_ERROR message with error text", async () => {
118
118
  const mockOnError = jest.fn();
119
119
  const callbacks: WidgetCallbacks = {
120
120
  onError: mockOnError,
121
121
  };
122
122
 
123
- const { getByTestId } = render(
124
- <SoluCXWidget {...baseProps} callbacks={callbacks} />
125
- );
123
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
126
124
 
127
125
  await waitFor(() => {
128
- expect(getByTestId('webview')).toBeTruthy();
126
+ expect(getByTestId("webview")).toBeTruthy();
129
127
  });
130
128
 
131
129
  // Simulate error message with details
132
- fireEvent(getByTestId('webview'), 'message', {
133
- nativeEvent: { data: 'FORM_ERROR-Network timeout occurred' },
130
+ fireEvent(getByTestId("webview"), "message", {
131
+ nativeEvent: { data: "FORM_ERROR-Network timeout occurred" },
132
+ });
133
+
134
+ await waitFor(() => {
135
+ expect(mockOnError).toHaveBeenCalledWith("Network timeout occurred");
136
+ });
137
+ });
138
+
139
+ it("should preserve FORM_ERROR payloads containing additional hyphens", async () => {
140
+ const mockOnError = jest.fn();
141
+ const callbacks: WidgetCallbacks = {
142
+ onError: mockOnError,
143
+ };
144
+
145
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
146
+
147
+ await waitFor(() => {
148
+ expect(getByTestId("webview")).toBeTruthy();
134
149
  });
135
150
 
151
+ fireEvent(getByTestId("webview"), "message", { nativeEvent: { data: "FORM_ERROR-timeout-api-gateway" } });
152
+
136
153
  await waitFor(() => {
137
- expect(mockOnError).toHaveBeenCalledWith('Network timeout occurred');
154
+ expect(mockOnError).toHaveBeenCalledWith("timeout-api-gateway");
138
155
  });
139
156
  });
140
157
 
141
- it('should handle FORM_RESIZE message and update height', async () => {
158
+ it("should handle FORM_RESIZE message and update height", async () => {
142
159
  const mockOnResize = jest.fn();
143
160
  const callbacks: WidgetCallbacks = {
144
161
  onResize: mockOnResize,
145
162
  };
146
163
 
147
- const { getByTestId } = render(
148
- <SoluCXWidget {...baseProps} callbacks={callbacks} />
149
- );
164
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
150
165
 
151
166
  await waitFor(() => {
152
- expect(getByTestId('webview')).toBeTruthy();
167
+ expect(getByTestId("webview")).toBeTruthy();
153
168
  });
154
169
 
155
170
  // Simulate resize message
156
- fireEvent(getByTestId('webview'), 'message', {
157
- nativeEvent: { data: 'FORM_RESIZE-650' },
171
+ fireEvent(getByTestId("webview"), "message", {
172
+ nativeEvent: { data: "FORM_RESIZE-650" },
158
173
  });
159
174
 
160
175
  await waitFor(() => {
161
- expect(mockOnResize).toHaveBeenCalledWith('650');
176
+ expect(mockOnResize).toHaveBeenCalledWith("650");
162
177
  });
163
178
  });
164
179
 
165
- it('should handle FORM_COMPLETED message with userId', async () => {
180
+ it("should handle FORM_COMPLETED message with userId", async () => {
166
181
  const mockOnCompleted = jest.fn();
167
182
  const callbacks: WidgetCallbacks = {
168
183
  onCompleted: mockOnCompleted,
169
184
  };
170
185
 
171
- const { getByTestId } = render(
172
- <SoluCXWidget {...baseProps} callbacks={callbacks} />
173
- );
186
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
174
187
 
175
188
  await waitFor(() => {
176
- expect(getByTestId('webview')).toBeTruthy();
189
+ expect(getByTestId("webview")).toBeTruthy();
177
190
  });
178
191
 
179
192
  // Simulate form completion
180
- fireEvent(getByTestId('webview'), 'message', {
181
- nativeEvent: { data: 'FORM_COMPLETED' },
193
+ fireEvent(getByTestId("webview"), "message", {
194
+ nativeEvent: { data: "FORM_COMPLETED" },
182
195
  });
183
196
 
184
197
  await waitFor(() => {
@@ -186,47 +199,43 @@ describe('Integration: WebView Communication', () => {
186
199
  });
187
200
  });
188
201
 
189
- it('should handle FORM_PAGECHANGED message', async () => {
202
+ it("should handle FORM_PAGECHANGED message", async () => {
190
203
  const mockOnPageChanged = jest.fn();
191
204
  const callbacks: WidgetCallbacks = {
192
205
  onPageChanged: mockOnPageChanged,
193
206
  };
194
207
 
195
- const { getByTestId } = render(
196
- <SoluCXWidget {...baseProps} callbacks={callbacks} />
197
- );
208
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
198
209
 
199
210
  await waitFor(() => {
200
- expect(getByTestId('webview')).toBeTruthy();
211
+ expect(getByTestId("webview")).toBeTruthy();
201
212
  });
202
213
 
203
214
  // Simulate page change
204
- fireEvent(getByTestId('webview'), 'message', {
205
- nativeEvent: { data: 'FORM_PAGECHANGED-2' },
215
+ fireEvent(getByTestId("webview"), "message", {
216
+ nativeEvent: { data: "FORM_PAGECHANGED-2" },
206
217
  });
207
218
 
208
219
  await waitFor(() => {
209
- expect(mockOnPageChanged).toHaveBeenCalledWith('2');
220
+ expect(mockOnPageChanged).toHaveBeenCalledWith("2");
210
221
  });
211
222
  });
212
223
 
213
- it('should handle QUESTION_ANSWERED message', async () => {
224
+ it("should handle QUESTION_ANSWERED message", async () => {
214
225
  const mockOnQuestionAnswered = jest.fn();
215
226
  const callbacks: WidgetCallbacks = {
216
227
  onQuestionAnswered: mockOnQuestionAnswered,
217
228
  };
218
229
 
219
- const { getByTestId } = render(
220
- <SoluCXWidget {...baseProps} callbacks={callbacks} />
221
- );
230
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
222
231
 
223
232
  await waitFor(() => {
224
- expect(getByTestId('webview')).toBeTruthy();
233
+ expect(getByTestId("webview")).toBeTruthy();
225
234
  });
226
235
 
227
236
  // Simulate question answered
228
- fireEvent(getByTestId('webview'), 'message', {
229
- nativeEvent: { data: 'QUESTION_ANSWERED' },
237
+ fireEvent(getByTestId("webview"), "message", {
238
+ nativeEvent: { data: "QUESTION_ANSWERED" },
230
239
  });
231
240
 
232
241
  await waitFor(() => {
@@ -235,29 +244,27 @@ describe('Integration: WebView Communication', () => {
235
244
  });
236
245
  });
237
246
 
238
- describe('Message Handling - Survey Events', () => {
247
+ describe("Message Handling - Survey Events", () => {
239
248
  const surveyProps = {
240
249
  ...baseProps,
241
250
  data: {
242
- customer_id: 'user-456',
251
+ customer_id: "user-456",
243
252
  // No form_id = survey mode
244
253
  },
245
254
  };
246
255
 
247
- it('should handle closeSoluCXWidget survey event', async () => {
256
+ it("should handle closeSoluCXWidget survey event", async () => {
248
257
  const mockOnClosed = jest.fn();
249
258
 
250
- const { getByTestId } = render(
251
- <SoluCXWidget {...surveyProps} callbacks={{ onClosed: mockOnClosed }} />
252
- );
259
+ const { getByTestId } = render(<SoluCXWidget {...surveyProps} callbacks={{ onClosed: mockOnClosed }} />);
253
260
 
254
261
  await waitFor(() => {
255
- expect(getByTestId('webview')).toBeTruthy();
262
+ expect(getByTestId("webview")).toBeTruthy();
256
263
  });
257
264
 
258
265
  // Survey uses different event names
259
- fireEvent(getByTestId('webview'), 'message', {
260
- nativeEvent: { data: 'closeSoluCXWidget' },
266
+ fireEvent(getByTestId("webview"), "message", {
267
+ nativeEvent: { data: "closeSoluCXWidget" },
261
268
  });
262
269
 
263
270
  await waitFor(() => {
@@ -265,39 +272,35 @@ describe('Integration: WebView Communication', () => {
265
272
  });
266
273
  });
267
274
 
268
- it('should handle resizeSoluCXWidget survey event', async () => {
275
+ it("should handle resizeSoluCXWidget survey event", async () => {
269
276
  const mockOnResize = jest.fn();
270
277
 
271
- const { getByTestId } = render(
272
- <SoluCXWidget {...surveyProps} callbacks={{ onResize: mockOnResize }} />
273
- );
278
+ const { getByTestId } = render(<SoluCXWidget {...surveyProps} callbacks={{ onResize: mockOnResize }} />);
274
279
 
275
280
  await waitFor(() => {
276
- expect(getByTestId('webview')).toBeTruthy();
281
+ expect(getByTestId("webview")).toBeTruthy();
277
282
  });
278
283
 
279
- fireEvent(getByTestId('webview'), 'message', {
280
- nativeEvent: { data: 'resizeSoluCXWidget-800' },
284
+ fireEvent(getByTestId("webview"), "message", {
285
+ nativeEvent: { data: "resizeSoluCXWidget-800" },
281
286
  });
282
287
 
283
288
  await waitFor(() => {
284
- expect(mockOnResize).toHaveBeenCalledWith('800');
289
+ expect(mockOnResize).toHaveBeenCalledWith("800");
285
290
  });
286
291
  });
287
292
 
288
- it('should handle completeSoluCXWidget survey event', async () => {
293
+ it("should handle completeSoluCXWidget survey event", async () => {
289
294
  const mockOnCompleted = jest.fn();
290
295
 
291
- const { getByTestId } = render(
292
- <SoluCXWidget {...surveyProps} callbacks={{ onCompleted: mockOnCompleted }} />
293
- );
296
+ const { getByTestId } = render(<SoluCXWidget {...surveyProps} callbacks={{ onCompleted: mockOnCompleted }} />);
294
297
 
295
298
  await waitFor(() => {
296
- expect(getByTestId('webview')).toBeTruthy();
299
+ expect(getByTestId("webview")).toBeTruthy();
297
300
  });
298
301
 
299
- fireEvent(getByTestId('webview'), 'message', {
300
- nativeEvent: { data: 'completeSoluCXWidget' },
302
+ fireEvent(getByTestId("webview"), "message", {
303
+ nativeEvent: { data: "completeSoluCXWidget" },
301
304
  });
302
305
 
303
306
  await waitFor(() => {
@@ -306,23 +309,21 @@ describe('Integration: WebView Communication', () => {
306
309
  });
307
310
  });
308
311
 
309
- describe('Edge Cases - Message Handling', () => {
310
- it('should handle rapid successive messages', async () => {
312
+ describe("Edge Cases - Message Handling", () => {
313
+ it("should handle rapid successive messages", async () => {
311
314
  const mockOnResize = jest.fn();
312
315
 
313
- const { getByTestId } = render(
314
- <SoluCXWidget {...baseProps} callbacks={{ onResize: mockOnResize }} />
315
- );
316
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={{ onResize: mockOnResize }} />);
316
317
 
317
318
  await waitFor(() => {
318
- expect(getByTestId('webview')).toBeTruthy();
319
+ expect(getByTestId("webview")).toBeTruthy();
319
320
  });
320
321
 
321
- const webview = getByTestId('webview');
322
+ const webview = getByTestId("webview");
322
323
 
323
324
  // Send 10 resize messages rapidly
324
325
  for (let i = 0; i < 10; i++) {
325
- fireEvent(webview, 'message', {
326
+ fireEvent(webview, "message", {
326
327
  nativeEvent: { data: `FORM_RESIZE-${300 + i * 10}` },
327
328
  });
328
329
  }
@@ -333,25 +334,23 @@ describe('Integration: WebView Communication', () => {
333
334
  });
334
335
 
335
336
  // Verify last call had correct value
336
- expect(mockOnResize).toHaveBeenLastCalledWith('390');
337
+ expect(mockOnResize).toHaveBeenLastCalledWith("390");
337
338
  });
338
339
  });
339
340
 
340
- describe('Close Button Integration', () => {
341
- it('should render close button in modal', async () => {
342
- const { getByText, getByTestId } = render(
343
- <SoluCXWidget {...baseProps} type="modal" />
344
- );
341
+ describe("Close Button Integration", () => {
342
+ it("should render close button in modal", async () => {
343
+ const { getByText, getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
345
344
 
346
345
  await waitFor(() => {
347
- expect(getByTestId('webview')).toBeTruthy();
348
- expect(getByText('')).toBeTruthy();
346
+ expect(getByTestId("webview")).toBeTruthy();
347
+ expect(getByText("")).toBeTruthy();
349
348
  });
350
349
  });
351
350
  });
352
351
 
353
- describe('Multiple Callbacks in Single Flow', () => {
354
- it('should call multiple callbacks during form interaction flow', async () => {
352
+ describe("Multiple Callbacks in Single Flow", () => {
353
+ it("should call multiple callbacks during form interaction flow", async () => {
355
354
  const mockOnPageChanged = jest.fn();
356
355
  const mockOnQuestionAnswered = jest.fn();
357
356
  const mockOnResize = jest.fn();
@@ -366,34 +365,32 @@ describe('Integration: WebView Communication', () => {
366
365
  onClosed: mockOnClosed,
367
366
  };
368
367
 
369
- const { getByTestId } = render(
370
- <SoluCXWidget {...baseProps} callbacks={callbacks} />
371
- );
368
+ const { getByTestId } = render(<SoluCXWidget {...baseProps} callbacks={callbacks} />);
372
369
 
373
370
  await waitFor(() => {
374
- expect(getByTestId('webview')).toBeTruthy();
371
+ expect(getByTestId("webview")).toBeTruthy();
375
372
  });
376
373
 
377
- const webview = getByTestId('webview');
374
+ const webview = getByTestId("webview");
378
375
 
379
376
  // Simulate complete user flow
380
377
  // 1. Form resizes
381
- fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-400' } });
382
-
378
+ fireEvent(webview, "message", { nativeEvent: { data: "FORM_RESIZE-400" } });
379
+
383
380
  // 2. User goes to page 2
384
- fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_PAGECHANGED-2' } });
385
-
381
+ fireEvent(webview, "message", { nativeEvent: { data: "FORM_PAGECHANGED-2" } });
382
+
386
383
  // 3. User answers a question
387
- fireEvent(webview, 'message', { nativeEvent: { data: 'QUESTION_ANSWERED' } });
388
-
384
+ fireEvent(webview, "message", { nativeEvent: { data: "QUESTION_ANSWERED" } });
385
+
389
386
  // 4. Form resizes again
390
- fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_RESIZE-500' } });
391
-
387
+ fireEvent(webview, "message", { nativeEvent: { data: "FORM_RESIZE-500" } });
388
+
392
389
  // 5. User completes form
393
- fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_COMPLETED' } });
394
-
390
+ fireEvent(webview, "message", { nativeEvent: { data: "FORM_COMPLETED" } });
391
+
395
392
  // 6. User closes
396
- fireEvent(webview, 'message', { nativeEvent: { data: 'FORM_CLOSE' } });
393
+ fireEvent(webview, "message", { nativeEvent: { data: "FORM_CLOSE" } });
397
394
 
398
395
  // Verify all callbacks were called in order
399
396
  await waitFor(() => {
@@ -405,9 +402,9 @@ describe('Integration: WebView Communication', () => {
405
402
  });
406
403
 
407
404
  // Verify correct arguments
408
- expect(mockOnResize).toHaveBeenNthCalledWith(1, '400');
409
- expect(mockOnResize).toHaveBeenNthCalledWith(2, '500');
410
- expect(mockOnPageChanged).toHaveBeenCalledWith('2');
405
+ expect(mockOnResize).toHaveBeenNthCalledWith(1, "400");
406
+ expect(mockOnResize).toHaveBeenNthCalledWith(2, "500");
407
+ expect(mockOnPageChanged).toHaveBeenCalledWith("2");
411
408
  });
412
409
  });
413
410
  });
@@ -0,0 +1,80 @@
1
+ import { normalizeWidgetOptions } from '../domain/WidgetOptions';
2
+ import type { WidgetOptions } from '../domain/WidgetOptions';
3
+
4
+ describe('normalizeWidgetOptions', () => {
5
+ it('returns options unchanged when no legacy fields are present', () => {
6
+ const options: WidgetOptions = {
7
+ maxAttemptsAfterDismiss: 5,
8
+ waitDaysAfterWidgetDismiss: 2,
9
+ waitDaysAfterWidgetSubmit: 30,
10
+ waitDaysAfterWidgetPartialSubmit: 60,
11
+ };
12
+ expect(normalizeWidgetOptions(options)).toEqual(options);
13
+ });
14
+
15
+ it('maps retry.attempts to maxAttemptsAfterDismiss', () => {
16
+ const result = normalizeWidgetOptions({ retry: { attempts: 3 } });
17
+ expect(result.maxAttemptsAfterDismiss).toBe(3);
18
+ });
19
+
20
+ it('maps retry.interval to waitDaysAfterWidgetDismiss', () => {
21
+ const result = normalizeWidgetOptions({ retry: { interval: 1 } });
22
+ expect(result.waitDaysAfterWidgetDismiss).toBe(1);
23
+ });
24
+
25
+ it('maps waitDelayAfterRating to waitDaysAfterWidgetSubmit and waitDaysAfterWidgetPartialSubmit', () => {
26
+ const result = normalizeWidgetOptions({ waitDelayAfterRating: 30 });
27
+ expect(result.waitDaysAfterWidgetSubmit).toBe(30);
28
+ expect(result.waitDaysAfterWidgetPartialSubmit).toBe(30);
29
+ });
30
+
31
+ it('canonical fields take precedence over legacy retry fields', () => {
32
+ const result = normalizeWidgetOptions({
33
+ maxAttemptsAfterDismiss: 10,
34
+ waitDaysAfterWidgetDismiss: 7,
35
+ retry: { attempts: 3, interval: 1 },
36
+ });
37
+ expect(result.maxAttemptsAfterDismiss).toBe(10);
38
+ expect(result.waitDaysAfterWidgetDismiss).toBe(7);
39
+ });
40
+
41
+ it('canonical fields take precedence over legacy waitDelayAfterRating', () => {
42
+ const result = normalizeWidgetOptions({
43
+ waitDaysAfterWidgetSubmit: 90,
44
+ waitDaysAfterWidgetPartialSubmit: 45,
45
+ waitDelayAfterRating: 30,
46
+ });
47
+ expect(result.waitDaysAfterWidgetSubmit).toBe(90);
48
+ expect(result.waitDaysAfterWidgetPartialSubmit).toBe(45);
49
+ });
50
+
51
+ it('should handle backward-compatible format when legacy fields are provided', (): void => {
52
+ const result = normalizeWidgetOptions({
53
+ retry: { attempts: 3, interval: 1 },
54
+ waitDelayAfterRating: 30,
55
+ });
56
+ expect(result.maxAttemptsAfterDismiss).toBe(3);
57
+ expect(result.waitDaysAfterWidgetDismiss).toBe(1);
58
+ expect(result.waitDaysAfterWidgetSubmit).toBe(30);
59
+ expect(result.waitDaysAfterWidgetPartialSubmit).toBe(30);
60
+ });
61
+
62
+ it('should preserve other options when normalizing legacy fields', (): void => {
63
+ const result = normalizeWidgetOptions({
64
+ enabled: true,
65
+ samplingPercentage: 50,
66
+ height: 400,
67
+ retry: { attempts: 3, interval: 1 },
68
+ waitDelayAfterRating: 30,
69
+ });
70
+ expect(result.enabled).toBe(true);
71
+ expect(result.samplingPercentage).toBe(50);
72
+ expect(result.height).toBe(400);
73
+ });
74
+
75
+ it('should leave fields undefined when retry object is empty', (): void => {
76
+ const result = normalizeWidgetOptions({ retry: {} });
77
+ expect(result.maxAttemptsAfterDismiss).toBeUndefined();
78
+ expect(result.waitDaysAfterWidgetDismiss).toBeUndefined();
79
+ });
80
+ });