@capillarytech/creatives-library 8.0.241 → 8.0.242-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/package.json +1 -1
  2. package/sagas/__tests__/assetPolling.test.js +607 -0
  3. package/sagas/assetPolling.js +156 -0
  4. package/services/api.js +16 -0
  5. package/services/tests/api.test.js +124 -0
  6. package/translations/en.json +1 -0
  7. package/utils/assetStatusConstants.js +12 -0
  8. package/utils/asyncAssetUpload.js +161 -0
  9. package/utils/tests/asyncAssetUpload.test.js +292 -0
  10. package/utils/transformerUtils.js +42 -0
  11. package/v2Components/CapImageUpload/constants.js +2 -0
  12. package/v2Components/CapImageUpload/index.js +54 -14
  13. package/v2Components/CapImageUpload/index.scss +4 -1
  14. package/v2Components/CapImageUpload/messages.js +4 -0
  15. package/v2Components/CapImageUrlUpload/constants.js +19 -0
  16. package/v2Components/CapImageUrlUpload/index.js +455 -0
  17. package/v2Components/CapImageUrlUpload/index.scss +35 -0
  18. package/v2Components/CapImageUrlUpload/messages.js +47 -0
  19. package/v2Containers/App/constants.js +5 -0
  20. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +1 -0
  21. package/v2Containers/CreativesContainer/SlideBoxContent.js +57 -2
  22. package/v2Containers/CreativesContainer/SlideBoxHeader.js +1 -0
  23. package/v2Containers/CreativesContainer/constants.js +2 -0
  24. package/v2Containers/CreativesContainer/index.js +152 -0
  25. package/v2Containers/CreativesContainer/messages.js +4 -0
  26. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  27. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  28. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +25 -0
  29. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +18 -0
  30. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +46 -0
  31. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +4 -0
  32. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +8 -0
  33. package/v2Containers/Templates/ChannelTypeIllustration.js +13 -1
  34. package/v2Containers/Templates/_templates.scss +203 -0
  35. package/v2Containers/Templates/actions.js +2 -1
  36. package/v2Containers/Templates/constants.js +1 -0
  37. package/v2Containers/Templates/index.js +273 -30
  38. package/v2Containers/Templates/messages.js +24 -0
  39. package/v2Containers/Templates/reducer.js +2 -0
  40. package/v2Containers/Templates/tests/index.test.js +10 -0
  41. package/v2Containers/TemplatesV2/index.js +3 -2
  42. package/v2Containers/TemplatesV2/messages.js +4 -0
  43. package/v2Containers/WebPush/Create/components/ButtonForm.js +175 -0
  44. package/v2Containers/WebPush/Create/components/ButtonItem.js +101 -0
  45. package/v2Containers/WebPush/Create/components/ButtonList.js +144 -0
  46. package/v2Containers/WebPush/Create/components/_buttons.scss +246 -0
  47. package/v2Containers/WebPush/Create/components/tests/ButtonForm.test.js +554 -0
  48. package/v2Containers/WebPush/Create/components/tests/ButtonItem.test.js +607 -0
  49. package/v2Containers/WebPush/Create/components/tests/ButtonList.test.js +633 -0
  50. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonForm.test.js.snap +666 -0
  51. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonItem.test.js.snap +74 -0
  52. package/v2Containers/WebPush/Create/components/tests/__snapshots__/ButtonList.test.js.snap +80 -0
  53. package/v2Containers/WebPush/Create/index.js +1755 -0
  54. package/v2Containers/WebPush/Create/index.scss +123 -0
  55. package/v2Containers/WebPush/Create/messages.js +199 -0
  56. package/v2Containers/WebPush/Create/preview/DevicePreviewContent.js +241 -0
  57. package/v2Containers/WebPush/Create/preview/NotificationContainer.js +290 -0
  58. package/v2Containers/WebPush/Create/preview/PreviewContent.js +81 -0
  59. package/v2Containers/WebPush/Create/preview/PreviewControls.js +240 -0
  60. package/v2Containers/WebPush/Create/preview/PreviewDisclaimer.js +23 -0
  61. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +144 -0
  62. package/v2Containers/WebPush/Create/preview/assets/Light.svg +53 -0
  63. package/v2Containers/WebPush/Create/preview/assets/Top.svg +5 -0
  64. package/v2Containers/WebPush/Create/preview/assets/chrome-icon.png +0 -0
  65. package/v2Containers/WebPush/Create/preview/assets/edge-icon.png +0 -0
  66. package/v2Containers/WebPush/Create/preview/assets/firefox-icon.svg +106 -0
  67. package/v2Containers/WebPush/Create/preview/assets/iOS.svg +26 -0
  68. package/v2Containers/WebPush/Create/preview/assets/opera-icon.svg +18 -0
  69. package/v2Containers/WebPush/Create/preview/assets/safari-icon.svg +29 -0
  70. package/v2Containers/WebPush/Create/preview/components/AndroidMobileChromeHeader.js +44 -0
  71. package/v2Containers/WebPush/Create/preview/components/AndroidMobileExpanded.js +110 -0
  72. package/v2Containers/WebPush/Create/preview/components/IOSHeader.js +45 -0
  73. package/v2Containers/WebPush/Create/preview/components/NotificationExpandedContent.js +72 -0
  74. package/v2Containers/WebPush/Create/preview/components/NotificationHeader.js +55 -0
  75. package/v2Containers/WebPush/Create/preview/components/WindowsChromeExpanded.js +70 -0
  76. package/v2Containers/WebPush/Create/preview/components/tests/AndroidMobileExpanded.test.js +512 -0
  77. package/v2Containers/WebPush/Create/preview/components/tests/__snapshots__/AndroidMobileExpanded.test.js.snap +77 -0
  78. package/v2Containers/WebPush/Create/preview/config/notificationMappings.js +527 -0
  79. package/v2Containers/WebPush/Create/preview/constants.js +162 -0
  80. package/v2Containers/WebPush/Create/preview/notification-container.scss +104 -0
  81. package/v2Containers/WebPush/Create/preview/preview.scss +409 -0
  82. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-chrome.scss +300 -0
  83. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-edge.scss +12 -0
  84. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-firefox.scss +12 -0
  85. package/v2Containers/WebPush/Create/preview/styles/_android-mobile-opera.scss +12 -0
  86. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-chrome.scss +303 -0
  87. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-edge.scss +11 -0
  88. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-firefox.scss +11 -0
  89. package/v2Containers/WebPush/Create/preview/styles/_android-tablet-opera.scss +11 -0
  90. package/v2Containers/WebPush/Create/preview/styles/_base.scss +188 -0
  91. package/v2Containers/WebPush/Create/preview/styles/_ios.scss +106 -0
  92. package/v2Containers/WebPush/Create/preview/styles/_ipados.scss +107 -0
  93. package/v2Containers/WebPush/Create/preview/styles/_macos-chrome.scss +75 -0
  94. package/v2Containers/WebPush/Create/preview/styles/_windows-chrome.scss +174 -0
  95. package/v2Containers/WebPush/Create/preview/tests/DevicePreviewContent.test.js +909 -0
  96. package/v2Containers/WebPush/Create/preview/tests/NotificationContainer.test.js +1077 -0
  97. package/v2Containers/WebPush/Create/preview/tests/PreviewControls.test.js +723 -0
  98. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +943 -0
  99. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/DevicePreviewContent.test.js.snap +128 -0
  100. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/NotificationContainer.test.js.snap +121 -0
  101. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/PreviewControls.test.js.snap +144 -0
  102. package/v2Containers/WebPush/Create/preview/tests/__snapshots__/WebPushPreview.test.js.snap +127 -0
  103. package/v2Containers/WebPush/Create/utils/urlValidation.js +116 -0
  104. package/v2Containers/WebPush/Create/utils/urlValidation.test.js +449 -0
  105. package/v2Containers/WebPush/actions.js +60 -0
  106. package/v2Containers/WebPush/constants.js +108 -0
  107. package/v2Containers/WebPush/index.js +2 -0
  108. package/v2Containers/WebPush/reducer.js +104 -0
  109. package/v2Containers/WebPush/sagas.js +119 -0
  110. package/v2Containers/WebPush/selectors.js +65 -0
  111. package/v2Containers/WebPush/tests/reducer.test.js +863 -0
  112. package/v2Containers/WebPush/tests/sagas.test.js +566 -0
  113. package/v2Containers/WebPush/tests/selectors.test.js +960 -0
  114. package/v2Containers/Whatsapp/constants.js +9 -0
  115. package/v2Containers/Whatsapp/reducer.js +34 -5
  116. package/v2Containers/Whatsapp/sagas.js +61 -10
  117. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +132 -0
  118. package/v2Containers/Whatsapp/tests/reducer.test.js +188 -0
  119. package/v2Containers/Whatsapp/tests/saga.test.js +420 -7
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.241",
4
+ "version": "8.0.242-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -0,0 +1,607 @@
1
+ import { expectSaga, testSaga } from 'redux-saga-test-plan';
2
+ import * as matchers from 'redux-saga-test-plan/matchers';
3
+ import { throwError } from 'redux-saga-test-plan/providers';
4
+ import { call, put } from 'redux-saga/effects';
5
+ import { pollAssetStatus } from '../assetPolling';
6
+ import { getAssetStatus } from '../../services/api';
7
+
8
+ // Dynamic provider for delay calls - matches call effects with a single number argument
9
+ // This is needed because function references don't match between modules
10
+ // The effect structure: { '@@redux-saga/IO': true, CALL: { context: null, fn: delay, args: [ms] } }
11
+ const delayProvider = (effect, next) => {
12
+ // Check if this is a call effect
13
+ if (effect && effect.CALL) {
14
+ const args = effect.CALL.args;
15
+ // Match any call with a single number argument (delay calls)
16
+ // delay(ms) calls have exactly one numeric argument
17
+ if (Array.isArray(args) && args.length === 1 && typeof args[0] === 'number' && args[0] >= 0) {
18
+ // Return resolved promise immediately to skip actual delay
19
+ return Promise.resolve();
20
+ }
21
+ }
22
+ // Not a delay call, pass to next provider/matcher
23
+ return next();
24
+ };
25
+
26
+ // Increase timeout for tests that involve delays
27
+ jest.setTimeout(20000);
28
+
29
+ describe('assetPolling saga', () => {
30
+ describe('pollAssetStatus', () => {
31
+ const mockAssetId = 'asset-123';
32
+ const mockAssetType = 'image';
33
+ const mockAsset = { _id: mockAssetId, type: 'IMAGE', url: 'https://example.com/image.jpg' };
34
+
35
+ // Mock Date.now() to return predictable values for duration calculation
36
+ const mockStartTime = 1000;
37
+ let originalDateNow;
38
+
39
+ beforeEach(() => {
40
+ originalDateNow = Date.now;
41
+ // Mock Date.now() to always return the same value so duration = 0
42
+ Date.now = jest.fn(() => mockStartTime);
43
+ });
44
+
45
+ afterEach(() => {
46
+ Date.now = originalDateNow;
47
+ });
48
+
49
+ const createMockConfig = (overrides = {}) => ({
50
+ type: mockAssetType,
51
+ assetId: mockAssetId,
52
+ onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
53
+ onFailed: (data) => ({ type: 'ASSET_FAILED', payload: data }),
54
+ onTimeout: (data) => ({ type: 'ASSET_TIMEOUT', payload: data }),
55
+ maxDuration: 50000, // Longer duration to avoid timeout issues
56
+ initialDelay: 0, // No delay for faster tests
57
+ pollInterval: 0, // No interval for faster tests
58
+ ...overrides,
59
+ });
60
+
61
+ it('should complete successfully when asset status is completed on first poll', () => {
62
+ const config = createMockConfig();
63
+
64
+ return expectSaga(pollAssetStatus, config)
65
+ .provide({
66
+ call: delayProvider,
67
+ })
68
+ .provide([
69
+ [
70
+ matchers.call.fn(getAssetStatus),
71
+ {
72
+ response: {
73
+ status: 'completed',
74
+ asset: mockAsset,
75
+ },
76
+ },
77
+ ],
78
+ ])
79
+ .put({
80
+ type: 'ASSET_COMPLETED',
81
+ payload: {
82
+ assetId: mockAssetId,
83
+ asset: mockAsset,
84
+ duration: 0, // Date.now() - startTime = 0 when mocked
85
+ },
86
+ })
87
+ .run({ timeout: 15000 });
88
+ });
89
+
90
+ it('should handle failed status immediately', () => {
91
+ const config = createMockConfig();
92
+ const errorMessage = 'Processing failed';
93
+
94
+ return expectSaga(pollAssetStatus, config)
95
+ .provide([
96
+ [
97
+ matchers.call.fn(getAssetStatus),
98
+ {
99
+ response: {
100
+ status: 'failed',
101
+ error: errorMessage,
102
+ },
103
+ },
104
+ ],
105
+ ])
106
+ .put({
107
+ type: 'ASSET_FAILED',
108
+ payload: {
109
+ assetId: mockAssetId,
110
+ error: errorMessage,
111
+ duration: 0, // Date.now() - startTime = 0 when mocked
112
+ },
113
+ })
114
+ .run({ timeout: 10000 });
115
+ });
116
+
117
+ it('should timeout when maxDuration is exceeded', () => {
118
+ const config = createMockConfig({
119
+ maxDuration: 10,
120
+ initialDelay: 0,
121
+ pollInterval: 5,
122
+ });
123
+
124
+ // Mock Date.now() to simulate time passing - override the beforeEach mock
125
+ let callCount = 0;
126
+ const originalDateNow = Date.now;
127
+ Date.now = jest.fn(() => {
128
+ callCount++;
129
+ // First call sets startTime, second call checks timeout (should exceed maxDuration)
130
+ if (callCount === 1) {
131
+ return mockStartTime; // startTime
132
+ }
133
+ return mockStartTime + 11; // Exceeds maxDuration of 10
134
+ });
135
+
136
+ return expectSaga(pollAssetStatus, config)
137
+ .provide([
138
+ // Delay provider handles all delay calls
139
+ {
140
+ call: delayProvider,
141
+ },
142
+ // Keep returning processing status
143
+ [
144
+ matchers.call.fn(getAssetStatus),
145
+ {
146
+ response: {
147
+ status: 'processing',
148
+ },
149
+ },
150
+ ],
151
+ ])
152
+ .put({
153
+ type: 'ASSET_TIMEOUT',
154
+ payload: {
155
+ assetId: mockAssetId,
156
+ message: 'Asset processing is taking longer than expected. Please check back later.',
157
+ duration: 11, // mockStartTime + 11 - mockStartTime = 11
158
+ },
159
+ })
160
+ .run({ timeout: 15000 })
161
+ .finally(() => {
162
+ Date.now = originalDateNow;
163
+ callCount = 0;
164
+ });
165
+ });
166
+
167
+ it('should use default values when config options are not provided', () => {
168
+ const config = {
169
+ type: mockAssetType,
170
+ assetId: mockAssetId,
171
+ onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
172
+ };
173
+
174
+ return expectSaga(pollAssetStatus, config)
175
+ .provide([
176
+ [
177
+ matchers.call.fn(getAssetStatus),
178
+ {
179
+ response: {
180
+ status: 'completed',
181
+ asset: mockAsset,
182
+ },
183
+ },
184
+ ],
185
+ ])
186
+ .put({
187
+ type: 'ASSET_COMPLETED',
188
+ payload: {
189
+ assetId: mockAssetId,
190
+ asset: mockAsset,
191
+ duration: 0, // Date.now() - startTime = 0 when mocked
192
+ },
193
+ })
194
+ .run({ timeout: 15000 });
195
+ });
196
+
197
+ it('should handle missing onCompleted callback', () => {
198
+ const config = {
199
+ type: mockAssetType,
200
+ assetId: mockAssetId,
201
+ onFailed: (data) => ({ type: 'ASSET_FAILED', payload: data }),
202
+ };
203
+
204
+ return expectSaga(pollAssetStatus, config)
205
+ .provide([
206
+ [
207
+ matchers.call.fn(getAssetStatus),
208
+ {
209
+ response: {
210
+ status: 'completed',
211
+ asset: mockAsset,
212
+ },
213
+ },
214
+ ],
215
+ ])
216
+ .not.put({ type: 'ASSET_COMPLETED' })
217
+ .run({ timeout: 15000 });
218
+ });
219
+
220
+ it('should handle missing onTimeout callback', () => {
221
+ const config = {
222
+ type: mockAssetType,
223
+ assetId: mockAssetId,
224
+ maxDuration: 10,
225
+ initialDelay: 0,
226
+ pollInterval: 5,
227
+ onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
228
+ };
229
+
230
+ return expectSaga(pollAssetStatus, config)
231
+ .provide([
232
+ [
233
+ matchers.call.fn(getAssetStatus),
234
+ {
235
+ response: {
236
+ status: 'processing',
237
+ },
238
+ },
239
+ ],
240
+ ])
241
+ .not.put({ type: 'ASSET_TIMEOUT' })
242
+ .run({ timeout: 15000 });
243
+ });
244
+
245
+ it('should use custom initialDelay and pollInterval', () => {
246
+ const config = createMockConfig({
247
+ initialDelay: 0,
248
+ pollInterval: 0,
249
+ });
250
+
251
+ return expectSaga(pollAssetStatus, config)
252
+ .provide([
253
+ [
254
+ matchers.call.fn(getAssetStatus),
255
+ {
256
+ response: {
257
+ status: 'completed',
258
+ asset: mockAsset,
259
+ },
260
+ },
261
+ ],
262
+ ])
263
+ .put({
264
+ type: 'ASSET_COMPLETED',
265
+ payload: {
266
+ assetId: mockAssetId,
267
+ asset: mockAsset,
268
+ duration: 0, // Date.now() - startTime = 0 when mocked
269
+ },
270
+ })
271
+ .run({ timeout: 15000 });
272
+ });
273
+
274
+ it('should handle failed status with default error message', () => {
275
+ const config = createMockConfig();
276
+
277
+ return expectSaga(pollAssetStatus, config)
278
+ .provide([
279
+ [
280
+ matchers.call.fn(getAssetStatus),
281
+ {
282
+ response: {
283
+ status: 'failed',
284
+ // No error field
285
+ },
286
+ },
287
+ ],
288
+ ])
289
+ .put({
290
+ type: 'ASSET_FAILED',
291
+ payload: {
292
+ assetId: mockAssetId,
293
+ error: 'Processing failed',
294
+ duration: 0, // Date.now() - startTime = 0 when mocked
295
+ },
296
+ })
297
+ .run({ timeout: 15000 });
298
+ });
299
+
300
+ it('should handle different asset types', () => {
301
+ const videoConfig = createMockConfig({ type: 'video' });
302
+
303
+ return expectSaga(pollAssetStatus, videoConfig)
304
+ .provide([
305
+ [
306
+ matchers.call.fn(getAssetStatus),
307
+ {
308
+ response: {
309
+ status: 'completed',
310
+ asset: { ...mockAsset, type: 'VIDEO' },
311
+ },
312
+ },
313
+ ],
314
+ ])
315
+ .put({
316
+ type: 'ASSET_COMPLETED',
317
+ payload: {
318
+ assetId: mockAssetId,
319
+ asset: { ...mockAsset, type: 'VIDEO' },
320
+ duration: 0, // Date.now() - startTime = 0 when mocked
321
+ },
322
+ })
323
+ .run({ timeout: 15000 });
324
+ });
325
+
326
+ it('should handle API response with success: false', () => {
327
+ const config = createMockConfig();
328
+ const errorMessage = 'API request failed';
329
+
330
+ return expectSaga(pollAssetStatus, config)
331
+ .provide({
332
+ call: delayProvider,
333
+ })
334
+ .provide([
335
+ [
336
+ matchers.call.fn(getAssetStatus),
337
+ {
338
+ success: false,
339
+ message: errorMessage,
340
+ },
341
+ ],
342
+ ])
343
+ .put({
344
+ type: 'ASSET_FAILED',
345
+ payload: {
346
+ assetId: mockAssetId,
347
+ error: errorMessage,
348
+ duration: 0,
349
+ },
350
+ })
351
+ .run({ timeout: 15000 });
352
+ });
353
+
354
+ it('should handle API response with error property', () => {
355
+ const config = createMockConfig();
356
+ const errorMessage = 'Network error occurred';
357
+
358
+ return expectSaga(pollAssetStatus, config)
359
+ .provide({
360
+ call: delayProvider,
361
+ })
362
+ .provide([
363
+ [
364
+ matchers.call.fn(getAssetStatus),
365
+ {
366
+ error: errorMessage,
367
+ },
368
+ ],
369
+ ])
370
+ .put({
371
+ type: 'ASSET_FAILED',
372
+ payload: {
373
+ assetId: mockAssetId,
374
+ error: errorMessage,
375
+ duration: 0,
376
+ },
377
+ })
378
+ .run({ timeout: 15000 });
379
+ });
380
+
381
+ it('should handle null or undefined API response', () => {
382
+ const config = createMockConfig();
383
+
384
+ return expectSaga(pollAssetStatus, config)
385
+ .provide({
386
+ call: delayProvider,
387
+ })
388
+ .provide([
389
+ [
390
+ matchers.call.fn(getAssetStatus),
391
+ null,
392
+ ],
393
+ ])
394
+ .put({
395
+ type: 'ASSET_FAILED',
396
+ payload: {
397
+ assetId: mockAssetId,
398
+ error: 'Failed to fetch asset status: Invalid API response',
399
+ duration: 0,
400
+ },
401
+ })
402
+ .run({ timeout: 15000 });
403
+ });
404
+
405
+ it('should handle API response with missing response property', () => {
406
+ const config = createMockConfig();
407
+
408
+ return expectSaga(pollAssetStatus, config)
409
+ .provide({
410
+ call: delayProvider,
411
+ })
412
+ .provide([
413
+ [
414
+ matchers.call.fn(getAssetStatus),
415
+ {
416
+ status: 200,
417
+ // Missing response property
418
+ },
419
+ ],
420
+ ])
421
+ .put({
422
+ type: 'ASSET_FAILED',
423
+ payload: {
424
+ assetId: mockAssetId,
425
+ error: 'Invalid asset status response: Missing or invalid status field',
426
+ duration: 0,
427
+ },
428
+ })
429
+ .run({ timeout: 15000 });
430
+ });
431
+
432
+ it('should handle API response with empty response object', () => {
433
+ const config = createMockConfig();
434
+
435
+ return expectSaga(pollAssetStatus, config)
436
+ .provide({
437
+ call: delayProvider,
438
+ })
439
+ .provide([
440
+ [
441
+ matchers.call.fn(getAssetStatus),
442
+ {
443
+ response: {},
444
+ // Empty response object without status
445
+ },
446
+ ],
447
+ ])
448
+ .put({
449
+ type: 'ASSET_FAILED',
450
+ payload: {
451
+ assetId: mockAssetId,
452
+ error: 'Invalid asset status response: Missing or invalid status field',
453
+ duration: 0,
454
+ },
455
+ })
456
+ .run({ timeout: 15000 });
457
+ });
458
+
459
+ it('should handle API response with invalid status field (not a string)', () => {
460
+ const config = createMockConfig();
461
+
462
+ return expectSaga(pollAssetStatus, config)
463
+ .provide({
464
+ call: delayProvider,
465
+ })
466
+ .provide([
467
+ [
468
+ matchers.call.fn(getAssetStatus),
469
+ {
470
+ response: {
471
+ status: 200, // Invalid: status should be a string, not a number
472
+ },
473
+ },
474
+ ],
475
+ ])
476
+ .put({
477
+ type: 'ASSET_FAILED',
478
+ payload: {
479
+ assetId: mockAssetId,
480
+ error: 'Invalid asset status response: Missing or invalid status field',
481
+ duration: 0,
482
+ },
483
+ })
484
+ .run({ timeout: 15000 });
485
+ });
486
+
487
+ it('should handle API response with error message property', () => {
488
+ const config = createMockConfig();
489
+ const errorMessage = 'Custom error message';
490
+
491
+ return expectSaga(pollAssetStatus, config)
492
+ .provide({
493
+ call: delayProvider,
494
+ })
495
+ .provide([
496
+ [
497
+ matchers.call.fn(getAssetStatus),
498
+ {
499
+ success: false,
500
+ message: errorMessage,
501
+ },
502
+ ],
503
+ ])
504
+ .put({
505
+ type: 'ASSET_FAILED',
506
+ payload: {
507
+ assetId: mockAssetId,
508
+ error: errorMessage,
509
+ duration: 0,
510
+ },
511
+ })
512
+ .run({ timeout: 15000 });
513
+ });
514
+
515
+ it('should handle timeout without onTimeout callback', () => {
516
+ const config = {
517
+ type: mockAssetType,
518
+ assetId: mockAssetId,
519
+ maxDuration: 10,
520
+ initialDelay: 0,
521
+ pollInterval: 5,
522
+ onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
523
+ onFailed: (data) => ({ type: 'ASSET_FAILED', payload: data }),
524
+ // No onTimeout callback
525
+ };
526
+
527
+ // Mock Date.now() to simulate time passing
528
+ let callCount = 0;
529
+ const originalDateNow = Date.now;
530
+ Date.now = jest.fn(() => {
531
+ callCount++;
532
+ if (callCount === 1) {
533
+ return mockStartTime; // startTime
534
+ }
535
+ return mockStartTime + 11; // Exceeds maxDuration of 10
536
+ });
537
+
538
+ return expectSaga(pollAssetStatus, config)
539
+ .provide({
540
+ call: delayProvider,
541
+ })
542
+ .provide([
543
+ [
544
+ matchers.call.fn(getAssetStatus),
545
+ {
546
+ response: {
547
+ status: 'processing',
548
+ },
549
+ },
550
+ ],
551
+ ])
552
+ .not.put({ type: 'ASSET_TIMEOUT' })
553
+ .run({ timeout: 15000 })
554
+ .finally(() => {
555
+ Date.now = originalDateNow;
556
+ callCount = 0;
557
+ });
558
+ });
559
+
560
+ it('should handle catch block error when getAssetStatus throws', () => {
561
+ const config = createMockConfig();
562
+ const networkError = new Error('Network request failed');
563
+
564
+ return expectSaga(pollAssetStatus, config)
565
+ .provide({
566
+ call: delayProvider,
567
+ })
568
+ .provide([
569
+ [
570
+ matchers.call.fn(getAssetStatus),
571
+ throwError(networkError),
572
+ ],
573
+ ])
574
+ .put({
575
+ type: 'ASSET_FAILED',
576
+ payload: {
577
+ assetId: mockAssetId,
578
+ error: 'Failed to check asset status: Network request failed',
579
+ },
580
+ })
581
+ .run({ timeout: 15000 });
582
+ });
583
+
584
+ it('should handle catch block error without onFailed callback', () => {
585
+ const config = {
586
+ type: mockAssetType,
587
+ assetId: mockAssetId,
588
+ onCompleted: (data) => ({ type: 'ASSET_COMPLETED', payload: data }),
589
+ // No onFailed callback
590
+ };
591
+ const networkError = new Error('Network request failed');
592
+
593
+ return expectSaga(pollAssetStatus, config)
594
+ .provide({
595
+ call: delayProvider,
596
+ })
597
+ .provide([
598
+ [
599
+ matchers.call.fn(getAssetStatus),
600
+ throwError(networkError),
601
+ ],
602
+ ])
603
+ .not.put({ type: 'ASSET_FAILED' })
604
+ .run({ timeout: 15000 });
605
+ });
606
+ });
607
+ });