@solucx/react-native-solucx-widget 0.2.4 → 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 -100
  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 -178
  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
@@ -0,0 +1,634 @@
1
+ import { renderHook, waitFor } from '@testing-library/react-native';
2
+ import { useWidgetBootstrap } from '../hooks/useWidgetBootstrap';
3
+ import { requestWidgetUrl, requestWidgetOptions } from '../services/WidgetBootstrapService';
4
+ import type { WidgetStateManager } from '../services/WidgetStateManager';
5
+ import type { WidgetValidationService } from '../services/WidgetValidationService';
6
+ import type { WidgetData, WidgetOptions, WidgetCallbacks } from '../domain';
7
+
8
+ // Mock dos serviços
9
+ jest.mock('../services/WidgetBootstrapService');
10
+
11
+ const mockRequestWidgetUrl = requestWidgetUrl as jest.MockedFunction<typeof requestWidgetUrl>;
12
+ const mockRequestWidgetOptions = requestWidgetOptions as jest.MockedFunction<typeof requestWidgetOptions>;
13
+
14
+ describe('useWidgetBootstrap', () => {
15
+ let mockOpen: jest.Mock;
16
+ let mockHide: jest.Mock;
17
+ let mockOnError: jest.Mock;
18
+ let mockOnBlocked: jest.Mock;
19
+ let mockOnPreOpen: jest.Mock;
20
+ let mockOnOpened: jest.Mock;
21
+ let mockValidationService: jest.Mocked<WidgetValidationService>;
22
+ let mockStateManager: jest.Mocked<WidgetStateManager>;
23
+
24
+ const defaultParams = {
25
+ soluCXKey: 'test-key-123',
26
+ data: {
27
+ journey: 'test-journey',
28
+ user_id: 'user123',
29
+ } as WidgetData,
30
+ userId: 'user123',
31
+ options: {
32
+ maxAttemptsAfterDismiss: 1,
33
+ height: 400,
34
+ } as WidgetOptions,
35
+ callbacks: {} as WidgetCallbacks,
36
+ open: jest.fn(),
37
+ hide: jest.fn(),
38
+ validationService: {} as any,
39
+ stateManager: {} as any,
40
+ };
41
+
42
+ beforeEach(() => {
43
+ jest.clearAllMocks();
44
+
45
+ // Mocks das funções
46
+ mockOpen = jest.fn().mockResolvedValue(undefined);
47
+ mockHide = jest.fn();
48
+ mockOnError = jest.fn();
49
+ mockOnBlocked = jest.fn();
50
+ mockOnPreOpen = jest.fn();
51
+ mockOnOpened = jest.fn();
52
+
53
+ // Mock do ValidationService
54
+ mockValidationService = {
55
+ shouldDisplayWidget: jest.fn().mockResolvedValue({
56
+ canDisplay: true,
57
+ blockReason: undefined,
58
+ }),
59
+ shouldDisplayForTransactionAlreadyAnswered: jest.fn().mockResolvedValue({
60
+ canDisplay: true,
61
+ blockReason: undefined,
62
+ }),
63
+ } as any;
64
+
65
+ // Mock do StateManager
66
+ mockStateManager = {
67
+ getLogs: jest.fn().mockResolvedValue({
68
+ attempts: 0,
69
+ answeredTransactionIds: [],
70
+ lastFirstAccess: undefined,
71
+ }),
72
+ updateTimestamp: jest.fn().mockResolvedValue(undefined),
73
+ incrementAttempt: jest.fn().mockResolvedValue(undefined),
74
+ clearLogs: jest.fn().mockResolvedValue(undefined),
75
+ } as any;
76
+
77
+ // Mock das funções do serviço de bootstrap
78
+ mockRequestWidgetOptions.mockResolvedValue({
79
+ maxAttemptsAfterDismiss: 2,
80
+ height: 400,
81
+ });
82
+
83
+ mockRequestWidgetUrl.mockResolvedValue({
84
+ available: true,
85
+ url: 'https://widget.test.com/survey?token=abc123',
86
+ });
87
+ });
88
+
89
+ describe('bootstrap - Happy Path', () => {
90
+ it('should complete full bootstrap when all validations pass', async () => {
91
+ const { result } = renderHook(() =>
92
+ useWidgetBootstrap({
93
+ ...defaultParams,
94
+ open: mockOpen,
95
+ hide: mockHide,
96
+ callbacks: {
97
+ onPreOpen: mockOnPreOpen,
98
+ onOpened: mockOnOpened,
99
+ },
100
+ validationService: mockValidationService,
101
+ stateManager: mockStateManager,
102
+ })
103
+ );
104
+
105
+ await result.current.bootstrap();
106
+
107
+ await waitFor(() => {
108
+ expect(result.current.widgetUri).toBe('https://widget.test.com/survey?token=abc123');
109
+ });
110
+
111
+ // Verificar ordem das chamadas
112
+ expect(mockValidationService.shouldDisplayForTransactionAlreadyAnswered).toHaveBeenCalledWith(undefined);
113
+ expect(mockValidationService.shouldDisplayWidget).toHaveBeenCalled();
114
+ expect(mockRequestWidgetUrl).toHaveBeenCalledWith('test-key-123', defaultParams.data, 'user123');
115
+ expect(mockOnPreOpen).toHaveBeenCalledWith('user123');
116
+ expect(mockOpen).toHaveBeenCalled();
117
+ expect(mockStateManager.incrementAttempt).toHaveBeenCalled();
118
+ expect(mockStateManager.updateTimestamp).toHaveBeenCalledWith('lastDisplay');
119
+ expect(mockOnOpened).toHaveBeenCalledWith('user123');
120
+ });
121
+
122
+ it('should set lastFirstAccess timestamp when widget is displayed for the first time', async () => {
123
+ mockStateManager.getLogs.mockResolvedValue({
124
+ attempts: 0,
125
+ answeredTransactionIds: [],
126
+ lastFirstAccess: undefined, // Primeira vez
127
+ });
128
+
129
+ const { result } = renderHook(() =>
130
+ useWidgetBootstrap({
131
+ ...defaultParams,
132
+ open: mockOpen,
133
+ hide: mockHide,
134
+ validationService: mockValidationService,
135
+ stateManager: mockStateManager,
136
+ })
137
+ );
138
+
139
+ await result.current.bootstrap();
140
+
141
+ await waitFor(() => {
142
+ expect(mockStateManager.updateTimestamp).toHaveBeenCalledWith('lastFirstAccess');
143
+ });
144
+ });
145
+
146
+ it('should not update lastFirstAccess when widget is displayed again', async () => {
147
+ mockStateManager.getLogs.mockResolvedValue({
148
+ attempts: 2,
149
+ answeredTransactionIds: [],
150
+ lastFirstAccess: 1000, // Já tem valor
151
+ });
152
+
153
+ const { result } = renderHook(() =>
154
+ useWidgetBootstrap({
155
+ ...defaultParams,
156
+ open: mockOpen,
157
+ hide: mockHide,
158
+ validationService: mockValidationService,
159
+ stateManager: mockStateManager,
160
+ })
161
+ );
162
+
163
+ await result.current.bootstrap();
164
+
165
+ await waitFor(() => {
166
+ expect(mockStateManager.updateTimestamp).not.toHaveBeenCalledWith('lastFirstAccess');
167
+ expect(mockStateManager.updateTimestamp).toHaveBeenCalledWith('lastDisplay');
168
+ });
169
+ });
170
+
171
+ it('should use provided options when options are passed', async () => {
172
+ const customOptions: WidgetOptions = {
173
+ maxAttemptsAfterDismiss: 3,
174
+ height: 500,
175
+ };
176
+
177
+ const { result } = renderHook(() =>
178
+ useWidgetBootstrap({
179
+ ...defaultParams,
180
+ options: customOptions,
181
+ open: mockOpen,
182
+ hide: mockHide,
183
+ validationService: mockValidationService,
184
+ stateManager: mockStateManager,
185
+ })
186
+ );
187
+
188
+ await result.current.bootstrap();
189
+
190
+ await waitFor(() => {
191
+ expect(mockRequestWidgetOptions).not.toHaveBeenCalled();
192
+ expect(result.current.widgetOptions).toEqual(customOptions);
193
+ });
194
+ });
195
+
196
+ it('should request options from API when options are not provided', async () => {
197
+ const { result } = renderHook(() =>
198
+ useWidgetBootstrap({
199
+ ...defaultParams,
200
+ options: undefined,
201
+ open: mockOpen,
202
+ hide: mockHide,
203
+ validationService: mockValidationService,
204
+ stateManager: mockStateManager,
205
+ })
206
+ );
207
+
208
+ await result.current.bootstrap();
209
+
210
+ await waitFor(() => {
211
+ expect(mockRequestWidgetOptions).toHaveBeenCalledWith('test-key-123', 'test-journey');
212
+ expect(result.current.widgetOptions).toEqual({
213
+ maxAttemptsAfterDismiss: 2,
214
+ height: 400,
215
+ });
216
+ });
217
+ });
218
+ });
219
+
220
+ describe('bootstrap - Validation Blocking', () => {
221
+ it('should block widget when transaction has already been answered', async () => {
222
+ mockValidationService.shouldDisplayForTransactionAlreadyAnswered.mockResolvedValue({
223
+ canDisplay: false,
224
+ blockReason: 'BLOCKED_BY_TRANSACTION_ALREADY_ANSWERED',
225
+ });
226
+
227
+ const { result } = renderHook(() =>
228
+ useWidgetBootstrap({
229
+ ...defaultParams,
230
+ data: {
231
+ ...defaultParams.data,
232
+ transaction_id: 'tx123',
233
+ },
234
+ callbacks: {
235
+ onBlocked: mockOnBlocked,
236
+ },
237
+ open: mockOpen,
238
+ hide: mockHide,
239
+ validationService: mockValidationService,
240
+ stateManager: mockStateManager,
241
+ })
242
+ );
243
+
244
+ await result.current.bootstrap();
245
+
246
+ await waitFor(() => {
247
+ expect(mockOnBlocked).toHaveBeenCalledWith('BLOCKED_BY_TRANSACTION_ALREADY_ANSWERED');
248
+ expect(mockHide).toHaveBeenCalled();
249
+ expect(mockOpen).not.toHaveBeenCalled();
250
+ expect(mockRequestWidgetUrl).not.toHaveBeenCalled();
251
+ });
252
+ });
253
+
254
+ it('should block widget when validation rules fail', async () => {
255
+ mockValidationService.shouldDisplayWidget.mockResolvedValue({
256
+ canDisplay: false,
257
+ blockReason: 'BLOCKED_BY_MAX_ATTEMPTS',
258
+ });
259
+
260
+ const { result } = renderHook(() =>
261
+ useWidgetBootstrap({
262
+ ...defaultParams,
263
+ callbacks: {
264
+ onBlocked: mockOnBlocked,
265
+ },
266
+ open: mockOpen,
267
+ hide: mockHide,
268
+ validationService: mockValidationService,
269
+ stateManager: mockStateManager,
270
+ })
271
+ );
272
+
273
+ await result.current.bootstrap();
274
+
275
+ await waitFor(() => {
276
+ expect(mockOnBlocked).toHaveBeenCalledWith('BLOCKED_BY_MAX_ATTEMPTS');
277
+ expect(mockHide).toHaveBeenCalled();
278
+ expect(mockStateManager.updateTimestamp).toHaveBeenCalledWith('lastDisplayAttempt');
279
+ expect(mockRequestWidgetUrl).not.toHaveBeenCalled();
280
+ });
281
+ });
282
+
283
+ it('should call onError when saving display attempt timestamp fails', async () => {
284
+ mockValidationService.shouldDisplayWidget.mockResolvedValue({
285
+ canDisplay: false,
286
+ blockReason: 'BLOCKED_BY_DISABLED',
287
+ });
288
+
289
+ mockStateManager.updateTimestamp.mockRejectedValue(new Error('Storage error'));
290
+
291
+ const { result } = renderHook(() =>
292
+ useWidgetBootstrap({
293
+ ...defaultParams,
294
+ callbacks: {
295
+ onBlocked: mockOnBlocked,
296
+ onError: mockOnError,
297
+ },
298
+ open: mockOpen,
299
+ hide: mockHide,
300
+ validationService: mockValidationService,
301
+ stateManager: mockStateManager,
302
+ })
303
+ );
304
+
305
+ await result.current.bootstrap();
306
+
307
+ await waitFor(() => {
308
+ expect(mockOnError).toHaveBeenCalledWith('Cannot save display attempt timestamp');
309
+ expect(mockHide).toHaveBeenCalled();
310
+ });
311
+ });
312
+ });
313
+
314
+ describe('bootstrap - Preflight Blocking', () => {
315
+ it('should block widget when preflight indicates unavailability', async () => {
316
+ mockRequestWidgetUrl.mockResolvedValue({
317
+ available: false,
318
+ reason: 'Survey not active',
319
+ });
320
+
321
+ const { result } = renderHook(() =>
322
+ useWidgetBootstrap({
323
+ ...defaultParams,
324
+ callbacks: {
325
+ onBlocked: mockOnBlocked,
326
+ },
327
+ open: mockOpen,
328
+ hide: mockHide,
329
+ validationService: mockValidationService,
330
+ stateManager: mockStateManager,
331
+ })
332
+ );
333
+
334
+ await result.current.bootstrap();
335
+
336
+ await waitFor(() => {
337
+ expect(mockOnBlocked).toHaveBeenCalledWith('Survey not active');
338
+ expect(mockHide).toHaveBeenCalled();
339
+ expect(mockOpen).not.toHaveBeenCalled();
340
+ });
341
+ });
342
+
343
+ it('should use default block reason when preflight returns no reason', async () => {
344
+ mockRequestWidgetUrl.mockResolvedValue({
345
+ available: false,
346
+ });
347
+
348
+ const { result } = renderHook(() =>
349
+ useWidgetBootstrap({
350
+ ...defaultParams,
351
+ callbacks: {
352
+ onBlocked: mockOnBlocked,
353
+ },
354
+ open: mockOpen,
355
+ hide: mockHide,
356
+ validationService: mockValidationService,
357
+ stateManager: mockStateManager,
358
+ })
359
+ );
360
+
361
+ await result.current.bootstrap();
362
+
363
+ await waitFor(() => {
364
+ expect(mockOnBlocked).toHaveBeenCalledWith('for the given parameters');
365
+ expect(mockHide).toHaveBeenCalled();
366
+ });
367
+ });
368
+
369
+ it('should call onError when widget URL is missing in response', async () => {
370
+ mockRequestWidgetUrl.mockResolvedValue({
371
+ available: true,
372
+ url: undefined as any,
373
+ });
374
+
375
+ const { result } = renderHook(() =>
376
+ useWidgetBootstrap({
377
+ ...defaultParams,
378
+ callbacks: {
379
+ onError: mockOnError,
380
+ },
381
+ open: mockOpen,
382
+ hide: mockHide,
383
+ validationService: mockValidationService,
384
+ stateManager: mockStateManager,
385
+ })
386
+ );
387
+
388
+ await result.current.bootstrap();
389
+
390
+ await waitFor(() => {
391
+ expect(mockOnError).toHaveBeenCalledWith('Widget URL is missing in the response');
392
+ expect(mockOpen).not.toHaveBeenCalled();
393
+ });
394
+ });
395
+ });
396
+
397
+ describe('bootstrap - Error Handling', () => {
398
+ it('should call onError when widget data is missing', async () => {
399
+ const { result } = renderHook(() =>
400
+ useWidgetBootstrap({
401
+ ...defaultParams,
402
+ data: undefined as any,
403
+ callbacks: {
404
+ onError: mockOnError,
405
+ },
406
+ open: mockOpen,
407
+ hide: mockHide,
408
+ validationService: mockValidationService,
409
+ stateManager: mockStateManager,
410
+ })
411
+ );
412
+
413
+ await result.current.bootstrap();
414
+
415
+ await waitFor(() => {
416
+ expect(mockOnError).toHaveBeenCalledWith('Widget data is required but was not provided');
417
+ expect(mockOpen).not.toHaveBeenCalled();
418
+ });
419
+ });
420
+
421
+ it('should call onError when requestWidgetUrl throws error', async () => {
422
+ mockRequestWidgetUrl.mockRejectedValue(new Error('Network error'));
423
+
424
+ const { result } = renderHook(() =>
425
+ useWidgetBootstrap({
426
+ ...defaultParams,
427
+ callbacks: {
428
+ onError: mockOnError,
429
+ },
430
+ open: mockOpen,
431
+ hide: mockHide,
432
+ validationService: mockValidationService,
433
+ stateManager: mockStateManager,
434
+ })
435
+ );
436
+
437
+ await result.current.bootstrap();
438
+
439
+ await waitFor(() => {
440
+ expect(mockOnError).toHaveBeenCalledWith('Network error');
441
+ expect(mockHide).toHaveBeenCalled();
442
+ });
443
+ });
444
+
445
+ it('should call onError when error is a string', async () => {
446
+ mockRequestWidgetUrl.mockRejectedValue('Connection timeout');
447
+
448
+ const { result } = renderHook(() =>
449
+ useWidgetBootstrap({
450
+ ...defaultParams,
451
+ callbacks: {
452
+ onError: mockOnError,
453
+ },
454
+ open: mockOpen,
455
+ hide: mockHide,
456
+ validationService: mockValidationService,
457
+ stateManager: mockStateManager,
458
+ })
459
+ );
460
+
461
+ await result.current.bootstrap();
462
+
463
+ await waitFor(() => {
464
+ expect(mockOnError).toHaveBeenCalledWith('Connection timeout');
465
+ });
466
+ });
467
+
468
+ it('should call onError when error is an object', async () => {
469
+ mockRequestWidgetUrl.mockRejectedValue({ code: 500, message: 'Server error' });
470
+
471
+ const { result } = renderHook(() =>
472
+ useWidgetBootstrap({
473
+ ...defaultParams,
474
+ callbacks: {
475
+ onError: mockOnError,
476
+ },
477
+ open: mockOpen,
478
+ hide: mockHide,
479
+ validationService: mockValidationService,
480
+ stateManager: mockStateManager,
481
+ })
482
+ );
483
+
484
+ await result.current.bootstrap();
485
+
486
+ await waitFor(() => {
487
+ expect(mockOnError).toHaveBeenCalledWith('{"code":500,"message":"Server error"}');
488
+ });
489
+ });
490
+
491
+ it('should call onError when error type is unknown', async () => {
492
+ mockRequestWidgetUrl.mockRejectedValue(null);
493
+
494
+ const { result } = renderHook(() =>
495
+ useWidgetBootstrap({
496
+ ...defaultParams,
497
+ callbacks: {
498
+ onError: mockOnError,
499
+ },
500
+ open: mockOpen,
501
+ hide: mockHide,
502
+ validationService: mockValidationService,
503
+ stateManager: mockStateManager,
504
+ })
505
+ );
506
+
507
+ await result.current.bootstrap();
508
+
509
+ await waitFor(() => {
510
+ expect(mockOnError).toHaveBeenCalledWith('Unknown error');
511
+ });
512
+ });
513
+
514
+ it('should call onError when saving display timestamps fails', async () => {
515
+ mockStateManager.incrementAttempt.mockRejectedValue(new Error('Storage full'));
516
+
517
+ const { result } = renderHook(() =>
518
+ useWidgetBootstrap({
519
+ ...defaultParams,
520
+ callbacks: {
521
+ onError: mockOnError,
522
+ onOpened: mockOnOpened,
523
+ },
524
+ open: mockOpen,
525
+ hide: mockHide,
526
+ validationService: mockValidationService,
527
+ stateManager: mockStateManager,
528
+ })
529
+ );
530
+
531
+ await result.current.bootstrap();
532
+
533
+ await waitFor(() => {
534
+ expect(mockOnError).toHaveBeenCalledWith('Error saving display timestamps');
535
+ // O widget ainda deve abrir mesmo com erro no timestamp
536
+ expect(mockOnOpened).toHaveBeenCalled();
537
+ });
538
+ });
539
+ });
540
+
541
+ describe('bootstrap - State Management', () => {
542
+ it('should reset widgetUri to null when bootstrap starts', async () => {
543
+ const { result, rerender } = renderHook(() =>
544
+ useWidgetBootstrap({
545
+ ...defaultParams,
546
+ open: mockOpen,
547
+ hide: mockHide,
548
+ validationService: mockValidationService,
549
+ stateManager: mockStateManager,
550
+ })
551
+ );
552
+
553
+ // Primeiro bootstrap
554
+ await result.current.bootstrap();
555
+ await waitFor(() => {
556
+ expect(result.current.widgetUri).toBe('https://widget.test.com/survey?token=abc123');
557
+ });
558
+
559
+ // Segundo bootstrap - deve limpar URI primeiro
560
+ mockRequestWidgetUrl.mockResolvedValue({
561
+ available: true,
562
+ url: 'https://widget.test.com/survey?token=xyz789',
563
+ });
564
+
565
+ await result.current.bootstrap();
566
+
567
+ // Verificar que foi resetado e depois atualizado
568
+ await waitFor(() => {
569
+ expect(result.current.widgetUri).toBe('https://widget.test.com/survey?token=xyz789');
570
+ });
571
+ });
572
+
573
+ it('should pass undefined as experienceId when transaction_id is not provided', async () => {
574
+ const { result } = renderHook(() =>
575
+ useWidgetBootstrap({
576
+ ...defaultParams,
577
+ data: {
578
+ form_id: 'form123',
579
+ user_id: 'user123',
580
+ },
581
+ open: mockOpen,
582
+ hide: mockHide,
583
+ validationService: mockValidationService,
584
+ stateManager: mockStateManager,
585
+ })
586
+ );
587
+
588
+ await result.current.bootstrap();
589
+
590
+ await waitFor(() => {
591
+ expect(mockValidationService.shouldDisplayWidget).toHaveBeenCalledWith(
592
+ expect.anything(),
593
+ undefined
594
+ );
595
+ });
596
+ });
597
+ });
598
+
599
+ describe('bootstrap - Validation Before Preflight', () => {
600
+ it('should validate before preflight when bootstrap is called', async () => {
601
+ const callOrder: string[] = [];
602
+
603
+ mockValidationService.shouldDisplayWidget.mockImplementation(async () => {
604
+ callOrder.push('validation');
605
+ return { canDisplay: false, blockReason: 'BLOCKED_BY_DISABLED' };
606
+ });
607
+
608
+ mockRequestWidgetUrl.mockImplementation(async () => {
609
+ callOrder.push('preflight');
610
+ return { available: true, url: 'test' };
611
+ });
612
+
613
+ const { result } = renderHook(() =>
614
+ useWidgetBootstrap({
615
+ ...defaultParams,
616
+ callbacks: {
617
+ onBlocked: mockOnBlocked,
618
+ },
619
+ open: mockOpen,
620
+ hide: mockHide,
621
+ validationService: mockValidationService,
622
+ stateManager: mockStateManager,
623
+ })
624
+ );
625
+
626
+ await result.current.bootstrap();
627
+
628
+ await waitFor(() => {
629
+ expect(callOrder).toEqual(['validation']);
630
+ expect(mockRequestWidgetUrl).not.toHaveBeenCalled();
631
+ });
632
+ });
633
+ });
634
+ });