@capillarytech/creatives-library 8.0.342 → 8.0.343-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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.342",
4
+ "version": "8.0.343-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -109,8 +109,7 @@ function emailReducer(state = initialState, action) {
109
109
  .set('fetchingCmsData', false)
110
110
  .set('fetchingCmsDataFailed', true);
111
111
  case types.GET_CMS_ACCOUNTS_REQUEST:
112
- return state
113
- .set('isBeeEnabled', false); // default to false
112
+ return state; // preserve null — "still pending", only update on SUCCESS/FAILURE
114
113
  case types.GET_CMS_ACCOUNTS_SUCCESS:
115
114
  return state
116
115
  .set('isBeeEnabled', action.isBeeEnabled);
@@ -16,15 +16,15 @@ describe('emailReducer', () => {
16
16
  expect(emailReducer(undefined, action)).toMatchSnapshot();
17
17
  });
18
18
 
19
- it.concurrent('it handles GET_CMS_ACCOUNTS_REQUEST action (line 111-113)', () => {
19
+ it.concurrent('it handles GET_CMS_ACCOUNTS_REQUEST action (does not change isBeeEnabled)', () => {
20
20
  const initialState = fromJS({
21
- isBeeEnabled: true, // Start with true to verify it gets set to false
21
+ isBeeEnabled: true, // preserved during request "still pending"
22
22
  });
23
23
  const action = {
24
24
  type: types.GET_CMS_ACCOUNTS_REQUEST,
25
25
  };
26
26
  const result = emailReducer(initialState, action);
27
- expect(result.get('isBeeEnabled')).toBe(false);
27
+ expect(result.get('isBeeEnabled')).toBe(true);
28
28
  });
29
29
 
30
30
  it.concurrent('it handles GET_CMS_ACCOUNTS_SUCCESS action (line 114-116)', () => {
@@ -200,7 +200,9 @@ const useEmailWrapper = ({
200
200
  if (!isEditEmail) {
201
201
  return;
202
202
  }
203
- const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
203
+ const explicitId = params?.id || location?.query?.id || location?.params?.id;
204
+ const pathnameId = !explicitId && location?.pathname?.match(/\/edit\/([^/]+)/)?.[1];
205
+ const hasParamsId = explicitId || (pathnameId && pathnameId !== 'undefined');
204
206
  const hasTemplateDetails = Email?.templateDetails && !isEmpty(Email.templateDetails);
205
207
  const hasTemplateDataProp = templateData && !isEmpty(templateData);
206
208
  const isTemplateLoading = Email?.getTemplateDetailsInProgress;
@@ -230,13 +232,18 @@ const useEmailWrapper = ({
230
232
  // New flow: When template details are loaded and it's a BEE template, set it in Templates.BEETemplate
231
233
  // This allows Email component to properly initialize and call getCmsSetting
232
234
  const hasTemplateDetails = Email?.templateDetails && !isEmpty(Email.templateDetails);
235
+ const hasTemplateProp = templateData && !isEmpty(templateData);
233
236
  // Note: We check Email?.BEETemplate as a proxy, but the actual BEETemplate is in Templates reducer
234
237
  // The Email component will detect it via this.props.Templates.BEETemplate
235
238
  const hasBEETemplate = Email?.BEETemplate && !isEmpty(Email.BEETemplate);
236
239
 
237
- if (hasTemplateDetails && !hasBEETemplate && templatesActions?.setBEETemplate && !beeTemplateSetRef.current) {
240
+ // Use Email.templateDetails when available (edit-by-ID flow), otherwise fall back to
241
+ // the templateData prop (BEE free template flow — template passed directly by consumer)
242
+ const editTemplateSource = hasTemplateDetails ? Email.templateDetails : (hasTemplateProp ? templateData : null);
243
+
244
+ if (editTemplateSource && !hasBEETemplate && templatesActions?.setBEETemplate && !beeTemplateSetRef.current) {
238
245
  // Check if it's a BEE template
239
- const editTemplateData = Email.templateDetails;
246
+ const editTemplateData = editTemplateSource;
240
247
  const getIsDragDrop = (data) => {
241
248
  if (!data) return false;
242
249
  const baseDragDrop = get(data, 'versions.base.is_drag_drop', false);
@@ -265,13 +272,17 @@ const useEmailWrapper = ({
265
272
  || activeTabData.id
266
273
  || get(editTemplateData, 'versions.base.drag_drop_id')
267
274
  || get(editTemplateData, 'versions.base.id')
275
+ || get(editTemplateData, 'base.drag_drop_id')
276
+ || get(editTemplateData, 'base.id')
268
277
  || editTemplateData._id;
269
278
 
270
279
  const isBEESupport = (location?.query?.isBEESupport !== "false") || false;
271
280
  // IMPORTANT: isBEEAppEnable should be consistent across full mode and library mode
272
281
  // It represents whether BEE is enabled for the organization, not the mode
273
282
  // This ensures the same template behaves the same way in both modes
274
- const isBEEAppEnable = checkBeeEditorEnabled();
283
+ // isDragDrop is always true here — pass true directly rather than relying on
284
+ // checkBeeEditorEnabled() which may return false while getCmsAccounts is pending
285
+ const isBEEAppEnable = isDragDrop || checkBeeEditorEnabled();
275
286
  // Check if we're in edit mode - check multiple sources for id
276
287
  const hasParamsId = params?.id
277
288
  || location?.query?.id
@@ -291,11 +302,11 @@ const useEmailWrapper = ({
291
302
  }
292
303
  }
293
304
 
294
- // Reset ref when template changes (template details cleared)
295
- if (!hasTemplateDetails && beeTemplateSetRef.current) {
305
+ // Reset ref when all template sources are gone (prevents duplicate calls on re-render)
306
+ if (!hasTemplateDetails && !hasTemplateProp && beeTemplateSetRef.current) {
296
307
  beeTemplateSetRef.current = false;
297
308
  }
298
- }, [Email?.templateDetails, Email?.BEETemplate, Email?.getTemplateDetailsInProgress, Email?.fetchingCmsSettings, templatesActions, emailActions, params?.id, location?.query, checkBeeEditorEnabled, isFullMode]);
309
+ }, [Email?.templateDetails, Email?.BEETemplate, Email?.getTemplateDetailsInProgress, Email?.fetchingCmsSettings, templatesActions, emailActions, params?.id, location?.query, checkBeeEditorEnabled, isFullMode, templateData]);
299
310
 
300
311
  const onChange = useCallback((e) => {
301
312
  const { target: { value } } = e;
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import CapHeading from "@capillarytech/cap-ui-library/CapHeading";
4
4
  import CapLabel from "@capillarytech/cap-ui-library/CapLabel";
5
5
  import messages from "./messages";
6
+ import { createAsyncAssetUploadConstants } from '../../utils/asyncAssetUpload';
6
7
 
7
8
  export const DEFAULT_ACTION = 'app/v2Containers/Viber/DEFAULT_ACTION';
8
9
  export const ALLOWED_EXTENSIONS_REGEX = (/\.(jpe?g|png)$/i);
@@ -16,6 +17,13 @@ export const UPLOAD_VIBER_ASSET_REQUEST = 'app/v2Containers/Viber/UPLOAD_ASSET_R
16
17
  export const UPLOAD_VIBER_ASSET_SUCCESS = 'app/v2Containers/Viber/UPLOAD_ASSET_SUCCESS';
17
18
  export const UPLOAD_VIBER_ASSET_FAILURE = 'app/v2Containers/Viber/UPLOAD_ASSET_FAILURE';
18
19
 
20
+ const viberAsyncPrefix = 'app/v2Containers/Viber';
21
+ const asyncUploadConstants = createAsyncAssetUploadConstants('VIBER', viberAsyncPrefix);
22
+ export const UPLOAD_VIBER_ASSET_PROCESSING = asyncUploadConstants.UPLOAD_VIBER_ASSET_PROCESSING;
23
+ export const UPLOAD_VIBER_ASSET_COMPLETED = asyncUploadConstants.UPLOAD_VIBER_ASSET_COMPLETED;
24
+ export const UPLOAD_VIBER_ASSET_FAILED = asyncUploadConstants.UPLOAD_VIBER_ASSET_FAILED;
25
+ export const UPLOAD_VIBER_ASSET_TIMEOUT = asyncUploadConstants.UPLOAD_VIBER_ASSET_TIMEOUT;
26
+
19
27
 
20
28
  export const CREATE_VIBER_TEMPLATE_REQUEST = 'app/v2Containers/Viber/CREATE_VIBER_TEMPLATE_REQUEST';
21
29
  export const CREATE_VIBER_TEMPLATE_SUCCESS = 'app/v2Containers/Viber/CREATE_VIBER_TEMPLATE_SUCCESS';
@@ -419,6 +419,7 @@ export const Viber = (props) => {
419
419
  channel={VIBER}
420
420
  errorMessage={formatMessage(messages.videoErrorMessage)}
421
421
  showVideoNameAndDuration
422
+ assetUploading={viber?.assetUploading}
422
423
  />
423
424
  );
424
425
 
@@ -795,6 +796,10 @@ export const Viber = (props) => {
795
796
  ) {
796
797
  return true;
797
798
  }
799
+ // block submit while a media asset upload is still in progress
800
+ if ((isMediaTypeImage || isMediaTypeVideo) && viber?.assetUploading) {
801
+ return true;
802
+ }
798
803
  if (isBtnTypeCta && !isCtaSaved) {
799
804
  return true;
800
805
  }
@@ -210,4 +210,8 @@ export default defineMessages({
210
210
  id: `${scope}.cancel`,
211
211
  defaultMessage: 'Cancel',
212
212
  },
213
+ assetIdMissingError: {
214
+ id: `${scope}.assetIdMissingError`,
215
+ defaultMessage: 'Asset upload initiated but no asset ID was returned from the server. Unable to track processing status.',
216
+ },
213
217
  });
@@ -6,10 +6,19 @@
6
6
 
7
7
  import { fromJS } from 'immutable';
8
8
  import * as types from './constants';
9
+ import { createAsyncAssetUploadReducerCases } from '../../utils/asyncAssetUpload';
10
+
11
+ const asyncUploadCases = createAsyncAssetUploadReducerCases({
12
+ PROCESSING: types.UPLOAD_VIBER_ASSET_PROCESSING,
13
+ COMPLETED: types.UPLOAD_VIBER_ASSET_COMPLETED,
14
+ FAILED: types.UPLOAD_VIBER_ASSET_FAILED,
15
+ TIMEOUT: types.UPLOAD_VIBER_ASSET_TIMEOUT,
16
+ });
9
17
 
10
18
  const initialState = fromJS({
11
19
  uploadedAssetData: {},
12
20
  createTemplateInProgress: false,
21
+ assetProcessing: {},
13
22
  });
14
23
 
15
24
  function viberReducer(state = initialState, action) {
@@ -37,14 +46,25 @@ function viberReducer(state = initialState, action) {
37
46
  return state
38
47
  .set('uploadAssetSuccess', (action.statusCode !== undefined && action.statusCode !== '' && action.statusCode < 300))
39
48
  .set('assetUploading', false)
40
- .set(action.templateType !== undefined ? `uploadedAssetData${action.templateType}` : 'uploadedAssetData', action.data);
49
+ .set(action.templateType !== undefined ? `uploadedAssetData${action.templateType}` : 'uploadedAssetData', fromJS(action.data));
41
50
  case types.UPLOAD_VIBER_ASSET_FAILURE:
42
51
  return state
43
52
  .set('uploadAssetSuccess', false)
44
53
  .set('assetUploading', false);
54
+ case asyncUploadCases.PROCESSING:
55
+ return asyncUploadCases.handleProcessing(state, action);
56
+ case asyncUploadCases.COMPLETED:
57
+ return asyncUploadCases.handleCompleted(state, action);
58
+ case asyncUploadCases.FAILED:
59
+ return asyncUploadCases.handleFailed(state, action);
60
+ case asyncUploadCases.TIMEOUT:
61
+ return asyncUploadCases.handleTimeout(state, action);
45
62
  case types.CLEAR_VIBER_ASSET:
46
63
  return state
47
- .delete(action.templateType !== undefined ? `uploadedAssetData${action.templateType}` : 'uploadedAssetData');
64
+ .delete(action.templateType !== undefined ? `uploadedAssetData${action.templateType}` : 'uploadedAssetData')
65
+ .set('assetProcessing', fromJS({}))
66
+ .set('assetUploading', false)
67
+ .set('uploadAssetSuccess', false);
48
68
  // FOR EDIT
49
69
  case types.EDIT_VIBER_TEMPLATE_REQUEST:
50
70
  return state
@@ -75,7 +95,10 @@ function viberReducer(state = initialState, action) {
75
95
  return state
76
96
  .delete('uploadedAssetData')
77
97
  .delete('metaEntities')
78
- .delete('templateDetails');
98
+ .delete('templateDetails')
99
+ .set('assetProcessing', fromJS({}))
100
+ .set('assetUploading', false)
101
+ .set('uploadAssetSuccess', false);
79
102
  default:
80
103
  return state;
81
104
  }
@@ -2,17 +2,59 @@ import {
2
2
  call, put, takeLatest, all,
3
3
  } from 'redux-saga/effects';
4
4
  import * as Api from '../../services/api';
5
+ import { pollAssetStatus } from '../../sagas/assetPolling';
6
+ import { createPollingConfig } from '../../utils/asyncAssetUpload';
7
+ import { ASSET_STATUS } from '../../utils/assetStatusConstants';
5
8
  import * as types from './constants';
9
+ import messages from './messages';
6
10
 
7
- export function* uploadViberAsset(file, assetType, fileParams ) {
11
+ export function* uploadViberAsset(params) {
8
12
  try {
9
- const result = yield call(Api.uploadFile, file, assetType, fileParams);
10
- yield put({
11
- type: types.UPLOAD_VIBER_ASSET_SUCCESS,
12
- data: result.response.asset,
13
- statusCode: result.status ? result.status.code : '',
14
- templateType: file.templateType,
15
- });
13
+ const result = yield call(Api.uploadFile, params);
14
+ const responseData = result?.response || {};
15
+ const statusCode = result?.status?.code || result?.statusCode;
16
+ const asset = responseData?.asset || responseData;
17
+
18
+ if (statusCode === 202 || responseData?.processingStatus === ASSET_STATUS.PROCESSING) {
19
+ const assetId = asset?._id || responseData?.assetId;
20
+ const { templateType } = params || {};
21
+ const assetType = params?.assetType || asset?.type?.toLowerCase() || 'image';
22
+
23
+ if (assetId) {
24
+ yield put({
25
+ type: types.UPLOAD_VIBER_ASSET_PROCESSING,
26
+ payload: {
27
+ assetId,
28
+ asset,
29
+ processingStatus: ASSET_STATUS.PROCESSING,
30
+ },
31
+ });
32
+
33
+ const actionTypes = {
34
+ COMPLETED: types.UPLOAD_VIBER_ASSET_COMPLETED,
35
+ FAILED: types.UPLOAD_VIBER_ASSET_FAILED,
36
+ TIMEOUT: types.UPLOAD_VIBER_ASSET_TIMEOUT,
37
+ };
38
+ const pollingConfig = createPollingConfig(assetType, assetId, actionTypes, templateType);
39
+ yield call(pollAssetStatus, pollingConfig);
40
+ } else {
41
+ yield put({
42
+ type: types.UPLOAD_VIBER_ASSET_FAILED,
43
+ payload: {
44
+ assetId: undefined,
45
+ error: messages.assetIdMissingError,
46
+ },
47
+ templateType,
48
+ });
49
+ }
50
+ } else {
51
+ yield put({
52
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
53
+ data: asset,
54
+ statusCode,
55
+ templateType: params?.templateType,
56
+ });
57
+ }
16
58
  } catch (error) {
17
59
  yield put({ type: types.UPLOAD_VIBER_ASSET_FAILURE, error });
18
60
  }
@@ -303,4 +303,84 @@ describe('Test Viber container', () => {
303
303
  });
304
304
  expect(doneBtn).toBeDisabled();
305
305
  });
306
+
307
+ it('disables Done while a replacement video asset is uploading (finding 1)', async () => {
308
+ renderComponent({
309
+ actions: mockActions,
310
+ globalActions: mockGlobalActions,
311
+ templateData: { mode: 'create' },
312
+ viber: {
313
+ uploadedAssetData: {},
314
+ createTemplateInProgress: false,
315
+ templateDetails: templateDetailsVideo,
316
+ // a new upload has started after the template was loaded
317
+ assetUploading: true,
318
+ },
319
+ location: {
320
+ pathname: '/sms/edit',
321
+ query: { type: false, module: 'default' },
322
+ search: '',
323
+ },
324
+ isFullMode: true,
325
+ params: { id: 'test' },
326
+ handleClose: jest.fn(),
327
+ metaEntities,
328
+ getDefaultTags,
329
+ injectedTags,
330
+ });
331
+ const doneBtn = screen.getByRole('button', { name: /done/i });
332
+ expect(doneBtn).toBeDisabled();
333
+ });
334
+
335
+ it('disables Done while an image asset is uploading (finding 1)', async () => {
336
+ renderComponent({
337
+ actions: mockActions,
338
+ globalActions: mockGlobalActions,
339
+ templateData: { mode: 'create' },
340
+ viber: {
341
+ uploadedAssetData: {},
342
+ createTemplateInProgress: false,
343
+ templateDetails: templateDetailsImage,
344
+ assetUploading: true,
345
+ },
346
+ location: {
347
+ pathname: '/sms/edit',
348
+ query: { type: false, module: 'default' },
349
+ search: '',
350
+ },
351
+ isFullMode: true,
352
+ params: { id: 'test' },
353
+ handleClose: jest.fn(),
354
+ });
355
+ const doneBtn = screen.getByRole('button', { name: /done/i });
356
+ expect(doneBtn).toBeDisabled();
357
+ });
358
+
359
+ it('keeps Done enabled when assetUploading is true but media type is text', async () => {
360
+ renderComponent({
361
+ actions: mockActions,
362
+ globalActions: mockGlobalActions,
363
+ templateData: { mode: 'create' },
364
+ viber: {
365
+ uploadedAssetData: {},
366
+ createTemplateInProgress: false,
367
+ templateDetails: templateDetailsText,
368
+ // stale upload flag, but the form has no media so submission is safe
369
+ assetUploading: true,
370
+ },
371
+ location: {
372
+ pathname: '/sms/edit',
373
+ query: { type: false, module: 'default' },
374
+ search: '',
375
+ },
376
+ isFullMode: true,
377
+ params: { id: 'test' },
378
+ handleClose: jest.fn(),
379
+ metaEntities,
380
+ getDefaultTags,
381
+ injectedTags,
382
+ });
383
+ const doneBtn = screen.getByRole('button', { name: /done/i });
384
+ expect(doneBtn).toBeEnabled();
385
+ });
306
386
  });
@@ -0,0 +1,297 @@
1
+ import { fromJS } from 'immutable';
2
+ import viberReducer from '../reducer';
3
+ import * as types from '../constants';
4
+ import { ASSET_STATUS } from '../../../utils/assetStatusConstants';
5
+
6
+ const initialState = fromJS({
7
+ uploadedAssetData: {},
8
+ createTemplateInProgress: false,
9
+ assetProcessing: {},
10
+ });
11
+
12
+ describe('viberReducer', () => {
13
+ it('returns initial state for unknown action', () => {
14
+ expect(viberReducer(undefined, { type: '@@INIT' })).toEqual(initialState);
15
+ });
16
+
17
+ describe('CREATE_VIBER_TEMPLATE_*', () => {
18
+ it('handles CREATE_VIBER_TEMPLATE_REQUEST', () => {
19
+ const next = viberReducer(initialState, {
20
+ type: types.CREATE_VIBER_TEMPLATE_REQUEST,
21
+ });
22
+ expect(next.get('createTemplateInProgress')).toBe(true);
23
+ expect(next.get('createTemplateError')).toBe(false);
24
+ expect(next.get('createTemplateErrorMessage')).toEqual(fromJS(''));
25
+ });
26
+
27
+ it('handles CREATE_VIBER_TEMPLATE_SUCCESS with status > 300', () => {
28
+ const next = viberReducer(initialState, {
29
+ type: types.CREATE_VIBER_TEMPLATE_SUCCESS,
30
+ data: { id: 'x' },
31
+ statusCode: 400,
32
+ errorMsg: 'oops',
33
+ });
34
+ expect(next.get('createTemplateInProgress')).toBe(false);
35
+ expect(next.get('response')).toEqual({ id: 'x' });
36
+ expect(next.get('createTemplateError')).toBe(true);
37
+ expect(next.get('createTemplateErrorMessage')).toEqual(fromJS('oops'));
38
+ });
39
+
40
+ it('handles CREATE_VIBER_TEMPLATE_SUCCESS with status <= 300', () => {
41
+ const next = viberReducer(initialState, {
42
+ type: types.CREATE_VIBER_TEMPLATE_SUCCESS,
43
+ data: { id: 'x' },
44
+ statusCode: 200,
45
+ errorMsg: '',
46
+ });
47
+ expect(next.get('createTemplateError')).toBe(false);
48
+ });
49
+
50
+ it('handles CREATE_VIBER_TEMPLATE_FAILURE', () => {
51
+ const next = viberReducer(initialState, {
52
+ type: types.CREATE_VIBER_TEMPLATE_FAILURE,
53
+ errorMsg: 'bad',
54
+ });
55
+ expect(next.get('createTemplateInProgress')).toBe(false);
56
+ expect(next.get('createTemplateError')).toBe(true);
57
+ expect(next.get('createTemplateErrorMessage')).toEqual(fromJS('bad'));
58
+ });
59
+ });
60
+
61
+ describe('UPLOAD_VIBER_ASSET_*', () => {
62
+ it('handles UPLOAD_VIBER_ASSET_REQUEST', () => {
63
+ const next = viberReducer(initialState, {
64
+ type: types.UPLOAD_VIBER_ASSET_REQUEST,
65
+ });
66
+ expect(next.get('uploadAssetSuccess')).toBe(false);
67
+ expect(next.get('assetUploading')).toBe(true);
68
+ });
69
+
70
+ it('handles UPLOAD_VIBER_ASSET_SUCCESS and wraps data with fromJS (finding 2)', () => {
71
+ const payload = { url: 'https://cdn/x.png', meta: { size: 100 } };
72
+ const next = viberReducer(initialState, {
73
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
74
+ data: payload,
75
+ statusCode: 200,
76
+ });
77
+ expect(next.get('assetUploading')).toBe(false);
78
+ expect(next.get('uploadAssetSuccess')).toBe(true);
79
+ // must be wrapped in Immutable (same as asyncUploadCases.handleCompleted)
80
+ const stored = next.get('uploadedAssetData');
81
+ expect(stored).toEqual(fromJS(payload));
82
+ // verify it's actually an Immutable Map, not a plain JS object
83
+ expect(stored.toJS).toBeDefined();
84
+ expect(typeof stored.toJS).toBe('function');
85
+ expect(stored.get('url')).toBe(payload.url);
86
+ });
87
+
88
+ it('handles UPLOAD_VIBER_ASSET_SUCCESS with templateType key wrapped in fromJS', () => {
89
+ const payload = { url: 'https://cdn/y.mp4' };
90
+ const next = viberReducer(initialState, {
91
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
92
+ data: payload,
93
+ statusCode: 200,
94
+ templateType: 3,
95
+ });
96
+ const stored = next.get('uploadedAssetData3');
97
+ expect(stored).toEqual(fromJS(payload));
98
+ expect(stored.get('url')).toBe(payload.url);
99
+ });
100
+
101
+ it('UPLOAD_VIBER_ASSET_SUCCESS sets uploadAssetSuccess false when status >= 300', () => {
102
+ const next = viberReducer(initialState, {
103
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
104
+ data: {},
105
+ statusCode: 500,
106
+ });
107
+ expect(next.get('uploadAssetSuccess')).toBe(false);
108
+ });
109
+
110
+ it('UPLOAD_VIBER_ASSET_SUCCESS sync and async produce same shape', () => {
111
+ const asset = { url: 'https://cdn/same.png' };
112
+ const sync = viberReducer(initialState, {
113
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
114
+ data: asset,
115
+ statusCode: 200,
116
+ });
117
+ const async_ = viberReducer(initialState, {
118
+ type: types.UPLOAD_VIBER_ASSET_COMPLETED,
119
+ payload: { assetId: 'a1', asset },
120
+ });
121
+ // Both paths should store uploadedAssetData as Immutable fromJS(asset)
122
+ expect(sync.get('uploadedAssetData')).toEqual(async_.get('uploadedAssetData'));
123
+ });
124
+
125
+ it('handles UPLOAD_VIBER_ASSET_FAILURE', () => {
126
+ const next = viberReducer(initialState, {
127
+ type: types.UPLOAD_VIBER_ASSET_FAILURE,
128
+ });
129
+ expect(next.get('uploadAssetSuccess')).toBe(false);
130
+ expect(next.get('assetUploading')).toBe(false);
131
+ });
132
+ });
133
+
134
+ describe('async polling cases', () => {
135
+ it('handles UPLOAD_VIBER_ASSET_PROCESSING', () => {
136
+ const next = viberReducer(initialState, {
137
+ type: types.UPLOAD_VIBER_ASSET_PROCESSING,
138
+ payload: { assetId: 'a1', asset: { id: 'a1' } },
139
+ });
140
+ expect(next.get('assetUploading')).toBe(true);
141
+ expect(next.get('uploadAssetSuccess')).toBe(false);
142
+ expect(next.getIn(['assetProcessing', 'a1', 'status'])).toBe(ASSET_STATUS.PROCESSING);
143
+ });
144
+
145
+ it('handles UPLOAD_VIBER_ASSET_COMPLETED', () => {
146
+ const next = viberReducer(initialState, {
147
+ type: types.UPLOAD_VIBER_ASSET_COMPLETED,
148
+ payload: { assetId: 'a1', asset: { url: 'https://cdn/x.png' } },
149
+ });
150
+ expect(next.get('assetUploading')).toBe(false);
151
+ expect(next.get('uploadAssetSuccess')).toBe(true);
152
+ expect(next.getIn(['assetProcessing', 'a1', 'status'])).toBe(ASSET_STATUS.COMPLETED);
153
+ expect(next.get('uploadedAssetData')).toEqual(fromJS({ url: 'https://cdn/x.png' }));
154
+ });
155
+
156
+ it('handles UPLOAD_VIBER_ASSET_FAILED', () => {
157
+ const next = viberReducer(initialState, {
158
+ type: types.UPLOAD_VIBER_ASSET_FAILED,
159
+ payload: { assetId: 'a1', error: 'boom' },
160
+ });
161
+ expect(next.get('assetUploading')).toBe(false);
162
+ expect(next.get('uploadAssetSuccess')).toBe(false);
163
+ expect(next.getIn(['assetProcessing', 'a1', 'status'])).toBe(ASSET_STATUS.FAILED);
164
+ expect(next.getIn(['assetProcessing', 'a1', 'error'])).toBe('boom');
165
+ });
166
+
167
+ it('handles UPLOAD_VIBER_ASSET_TIMEOUT', () => {
168
+ const next = viberReducer(initialState, {
169
+ type: types.UPLOAD_VIBER_ASSET_TIMEOUT,
170
+ payload: { assetId: 'a1', message: 'timed out' },
171
+ });
172
+ expect(next.get('assetUploading')).toBe(false);
173
+ expect(next.getIn(['assetProcessing', 'a1', 'status'])).toBe(ASSET_STATUS.TIMEOUT);
174
+ expect(next.getIn(['assetProcessing', 'a1', 'error'])).toBe('timed out');
175
+ });
176
+ });
177
+
178
+ describe('CLEAR_VIBER_ASSET (finding 3)', () => {
179
+ const dirtyState = initialState
180
+ .set('assetUploading', true)
181
+ .set('uploadAssetSuccess', true)
182
+ .set('uploadedAssetData', fromJS({ url: 'x' }))
183
+ .set('uploadedAssetData2', fromJS({ url: 'y' }))
184
+ .setIn(['assetProcessing', 'a1'], fromJS({ status: 'processing' }));
185
+
186
+ it('clears default uploadedAssetData and resets upload flags', () => {
187
+ const next = viberReducer(dirtyState, { type: types.CLEAR_VIBER_ASSET });
188
+ expect(next.has('uploadedAssetData')).toBe(false);
189
+ expect(next.get('assetProcessing')).toEqual(fromJS({}));
190
+ expect(next.get('assetUploading')).toBe(false);
191
+ expect(next.get('uploadAssetSuccess')).toBe(false);
192
+ });
193
+
194
+ it('clears templateType-specific uploadedAssetData and resets upload flags', () => {
195
+ const next = viberReducer(dirtyState, {
196
+ type: types.CLEAR_VIBER_ASSET,
197
+ templateType: 2,
198
+ });
199
+ expect(next.has('uploadedAssetData2')).toBe(false);
200
+ // default key untouched by specific clear
201
+ expect(next.has('uploadedAssetData')).toBe(true);
202
+ expect(next.get('assetProcessing')).toEqual(fromJS({}));
203
+ expect(next.get('assetUploading')).toBe(false);
204
+ expect(next.get('uploadAssetSuccess')).toBe(false);
205
+ });
206
+ });
207
+
208
+ describe('CLEAR_VIBER_DATA (finding 3 mirror)', () => {
209
+ it('clears assetProcessing, assetUploading, uploadAssetSuccess', () => {
210
+ const dirtyState = initialState
211
+ .set('assetUploading', true)
212
+ .set('uploadAssetSuccess', true)
213
+ .set('metaEntities', fromJS({ some: 'data' }))
214
+ .set('templateDetails', fromJS({ id: 'x' }))
215
+ .setIn(['assetProcessing', 'a1'], fromJS({ status: 'processing' }));
216
+ const next = viberReducer(dirtyState, { type: types.CLEAR_VIBER_DATA });
217
+ expect(next.has('uploadedAssetData')).toBe(false);
218
+ expect(next.has('metaEntities')).toBe(false);
219
+ expect(next.has('templateDetails')).toBe(false);
220
+ expect(next.get('assetProcessing')).toEqual(fromJS({}));
221
+ expect(next.get('assetUploading')).toBe(false);
222
+ expect(next.get('uploadAssetSuccess')).toBe(false);
223
+ });
224
+ });
225
+
226
+ describe('EDIT_VIBER_TEMPLATE_*', () => {
227
+ it('handles EDIT_VIBER_TEMPLATE_REQUEST', () => {
228
+ const next = viberReducer(initialState, {
229
+ type: types.EDIT_VIBER_TEMPLATE_REQUEST,
230
+ });
231
+ expect(next.get('editTemplateInProgress')).toBe(true);
232
+ expect(next.get('editTemplateError')).toBe(false);
233
+ });
234
+
235
+ it('handles EDIT_VIBER_TEMPLATE_SUCCESS', () => {
236
+ const next = viberReducer(initialState, {
237
+ type: types.EDIT_VIBER_TEMPLATE_SUCCESS,
238
+ data: { id: 'e' },
239
+ statusCode: 200,
240
+ errorMsg: '',
241
+ });
242
+ expect(next.get('editTemplateInProgress')).toBe(false);
243
+ expect(next.get('editResponse')).toEqual({ id: 'e' });
244
+ expect(next.get('editTemplateError')).toBe(false);
245
+ });
246
+
247
+ it('handles EDIT_VIBER_TEMPLATE_SUCCESS error status', () => {
248
+ const next = viberReducer(initialState, {
249
+ type: types.EDIT_VIBER_TEMPLATE_SUCCESS,
250
+ data: {},
251
+ statusCode: 500,
252
+ errorMsg: 'err',
253
+ });
254
+ expect(next.get('editTemplateError')).toBe(true);
255
+ });
256
+
257
+ it('handles EDIT_VIBER_TEMPLATE_FAILURE', () => {
258
+ const next = viberReducer(initialState, {
259
+ type: types.EDIT_VIBER_TEMPLATE_FAILURE,
260
+ errorMsg: 'nope',
261
+ });
262
+ expect(next.get('editTemplateError')).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe('GET/CLEAR branches', () => {
267
+ it('handles GET_VIBER_TEMPLATE_DETAILS_REQUEST', () => {
268
+ const next = viberReducer(initialState, {
269
+ type: types.GET_VIBER_TEMPLATE_DETAILS_REQUEST,
270
+ });
271
+ expect(next.get('getTemplateDetailsInProgress')).toBe(true);
272
+ });
273
+
274
+ it('handles GET_VIBER_TEMPLATE_DETAILS_SUCCESS', () => {
275
+ const next = viberReducer(initialState, {
276
+ type: types.GET_VIBER_TEMPLATE_DETAILS_SUCCESS,
277
+ data: { id: 'd' },
278
+ });
279
+ expect(next.get('getTemplateDetailsInProgress')).toBe(false);
280
+ expect(next.get('templateDetails')).toEqual({ id: 'd' });
281
+ });
282
+
283
+ it('handles GET_VIBER_TEMPLATE_DETAILS_FAILURE / CLEAR_VIBER_EDIT_RESPONSE_REQUEST', () => {
284
+ const next = viberReducer(initialState, {
285
+ type: types.CLEAR_VIBER_EDIT_RESPONSE_REQUEST,
286
+ });
287
+ expect(next.get('editResponse')).toEqual({});
288
+ });
289
+
290
+ it('handles CLEAR_VIBER_CREATE_RESPONSE_REQUEST', () => {
291
+ const next = viberReducer(initialState, {
292
+ type: types.CLEAR_VIBER_CREATE_RESPONSE_REQUEST,
293
+ });
294
+ expect(next.get('response')).toEqual({});
295
+ });
296
+ });
297
+ });
@@ -1,50 +1,305 @@
1
1
  import { expectSaga } from 'redux-saga-test-plan';
2
- import { call, put } from 'redux-saga/effects';
2
+ import * as matchers from 'redux-saga-test-plan/matchers';
3
+ import { call } from 'redux-saga/effects';
3
4
  import { throwError } from 'redux-saga-test-plan/providers';
4
5
  import * as Api from '../../../services/api';
5
6
  import * as sagas from '../sagas';
6
- import { v2ViberSagas } from '../sagas';
7
7
  import * as types from '../constants';
8
+ import { pollAssetStatus } from '../../../sagas/assetPolling';
9
+ import { ASSET_STATUS } from '../../../utils/assetStatusConstants';
10
+ import messages from '../messages';
8
11
 
9
12
  describe('Viber Sagas', () => {
10
-
11
13
  describe('uploadViberAsset Saga', () => {
12
- const file = new Blob(['file contents'], { type: 'text/plain' });
13
- const assetType = 'image';
14
- const fileParams = { directory: 'profile', templateType: 'viber' };
14
+ const mockAsset = {
15
+ _id: 'asset-123',
16
+ type: 'IMAGE',
17
+ url: 'https://example.com/image.jpg',
18
+ };
15
19
 
16
- it('handles uploading asset successfully', () => {
17
- const fakeResponse = {
18
- response: { asset: { id: 1, url: 'http://example.com/image.png' } },
19
- status: { code: 200 }
20
+ const mockParams = {
21
+ file: new File(['test'], 'test.jpg', { type: 'image/jpeg' }),
22
+ assetType: 'image',
23
+ fileParams: {},
24
+ templateType: 0,
25
+ };
26
+
27
+ it('handles async upload (202) with polling', () => {
28
+ const mockResponse = {
29
+ status: { code: 202 },
30
+ response: {
31
+ assetId: 'asset-123',
32
+ asset: mockAsset,
33
+ processingStatus: ASSET_STATUS.PROCESSING,
34
+ },
35
+ };
36
+
37
+ return expectSaga(sagas.uploadViberAsset, mockParams)
38
+ .provide([
39
+ [matchers.call.fn(Api.uploadFile), mockResponse],
40
+ [matchers.call.fn(pollAssetStatus), undefined],
41
+ ])
42
+ .put({
43
+ type: types.UPLOAD_VIBER_ASSET_PROCESSING,
44
+ payload: {
45
+ assetId: 'asset-123',
46
+ asset: mockAsset,
47
+ processingStatus: ASSET_STATUS.PROCESSING,
48
+ },
49
+ })
50
+ .call.fn(Api.uploadFile)
51
+ .call.fn(pollAssetStatus)
52
+ .run();
53
+ });
54
+
55
+ it('handles async when processingStatus is processing with 200 status', () => {
56
+ const mockResponse = {
57
+ status: { code: 200 },
58
+ response: {
59
+ assetId: 'asset-123',
60
+ asset: mockAsset,
61
+ processingStatus: ASSET_STATUS.PROCESSING,
62
+ },
20
63
  };
21
64
 
22
- return expectSaga(sagas.uploadViberAsset, file, assetType, fileParams)
65
+ return expectSaga(sagas.uploadViberAsset, mockParams)
23
66
  .provide([
24
- [call(Api.uploadFile, file, assetType, fileParams), fakeResponse]
67
+ [matchers.call.fn(Api.uploadFile), mockResponse],
68
+ [matchers.call.fn(pollAssetStatus), undefined],
69
+ ])
70
+ .put({
71
+ type: types.UPLOAD_VIBER_ASSET_PROCESSING,
72
+ payload: {
73
+ assetId: 'asset-123',
74
+ asset: mockAsset,
75
+ processingStatus: ASSET_STATUS.PROCESSING,
76
+ },
77
+ })
78
+ .call.fn(pollAssetStatus)
79
+ .run();
80
+ });
81
+
82
+ it('handles sync upload (201) without polling', () => {
83
+ const mockResponse = {
84
+ status: { code: 201 },
85
+ response: {
86
+ asset: mockAsset,
87
+ },
88
+ };
89
+
90
+ return expectSaga(sagas.uploadViberAsset, mockParams)
91
+ .provide([
92
+ [matchers.call.fn(Api.uploadFile), mockResponse],
25
93
  ])
26
94
  .put({
27
95
  type: types.UPLOAD_VIBER_ASSET_SUCCESS,
28
- data: fakeResponse.response.asset,
29
- statusCode: fakeResponse.status.code,
30
- templateType: undefined
96
+ data: mockAsset,
97
+ statusCode: 201,
98
+ templateType: 0,
31
99
  })
100
+ .not.call(pollAssetStatus)
32
101
  .run();
33
102
  });
34
103
 
35
- it('handles asset upload failure', () => {
36
- const error = new Error('Upload failed');
104
+ it('extracts assetId from asset._id when assetId omitted', () => {
105
+ const mockResponse = {
106
+ status: { code: 202 },
107
+ response: {
108
+ asset: { ...mockAsset, _id: 'asset-456' },
109
+ processingStatus: ASSET_STATUS.PROCESSING,
110
+ },
111
+ };
37
112
 
38
- return expectSaga(sagas.uploadViberAsset, file, assetType, fileParams)
113
+ return expectSaga(sagas.uploadViberAsset, mockParams)
39
114
  .provide([
40
- [call(Api.uploadFile, file, assetType, fileParams), throwError(error)]
115
+ [matchers.call.fn(Api.uploadFile), mockResponse],
116
+ [matchers.call.fn(pollAssetStatus), undefined],
117
+ ])
118
+ .put({
119
+ type: types.UPLOAD_VIBER_ASSET_PROCESSING,
120
+ payload: {
121
+ assetId: 'asset-456',
122
+ asset: { ...mockAsset, _id: 'asset-456' },
123
+ processingStatus: ASSET_STATUS.PROCESSING,
124
+ },
125
+ })
126
+ .run();
127
+ });
128
+
129
+ it('dispatches FAILED when assetId missing on async response', () => {
130
+ const mockResponse = {
131
+ status: { code: 202 },
132
+ response: {
133
+ processingStatus: ASSET_STATUS.PROCESSING,
134
+ },
135
+ };
136
+
137
+ return expectSaga(sagas.uploadViberAsset, mockParams)
138
+ .provide([
139
+ [matchers.call.fn(Api.uploadFile), mockResponse],
140
+ ])
141
+ .put({
142
+ type: types.UPLOAD_VIBER_ASSET_FAILED,
143
+ payload: {
144
+ assetId: undefined,
145
+ error: messages.assetIdMissingError,
146
+ },
147
+ templateType: 0,
148
+ })
149
+ .not.call(pollAssetStatus)
150
+ .run();
151
+ });
152
+
153
+ it('handles upload error', () => {
154
+ const uploadError = new Error('Upload failed');
155
+
156
+ return expectSaga(sagas.uploadViberAsset, mockParams)
157
+ .provide([
158
+ [matchers.call.fn(Api.uploadFile), throwError(uploadError)],
41
159
  ])
42
160
  .put({
43
161
  type: types.UPLOAD_VIBER_ASSET_FAILURE,
44
- error
162
+ error: uploadError,
163
+ })
164
+ .run();
165
+ });
166
+
167
+ it('falls back to statusCode on response when status object missing', () => {
168
+ const mockResponse = {
169
+ statusCode: 201,
170
+ response: { asset: mockAsset },
171
+ };
172
+ return expectSaga(sagas.uploadViberAsset, mockParams)
173
+ .provide([
174
+ [matchers.call.fn(Api.uploadFile), mockResponse],
175
+ ])
176
+ .put({
177
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
178
+ data: mockAsset,
179
+ statusCode: 201,
180
+ templateType: 0,
181
+ })
182
+ .run();
183
+ });
184
+
185
+ it('passes undefined statusCode through when no status info present (no default)', () => {
186
+ const mockResponse = { response: { asset: mockAsset } };
187
+ return expectSaga(sagas.uploadViberAsset, mockParams)
188
+ .provide([
189
+ [matchers.call.fn(Api.uploadFile), mockResponse],
190
+ ])
191
+ .put({
192
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
193
+ data: mockAsset,
194
+ statusCode: undefined,
195
+ templateType: 0,
45
196
  })
46
197
  .run();
47
198
  });
199
+
200
+ it('handles empty response object', () => {
201
+ const mockResponse = { status: { code: 201 } };
202
+ return expectSaga(sagas.uploadViberAsset, mockParams)
203
+ .provide([
204
+ [matchers.call.fn(Api.uploadFile), mockResponse],
205
+ ])
206
+ .put({
207
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
208
+ data: {},
209
+ statusCode: 201,
210
+ templateType: 0,
211
+ })
212
+ .run();
213
+ });
214
+
215
+ it('normalizes a flat async response (asset fields at top level) for polling', () => {
216
+ // API returns a flat asset object instead of { asset: {...} }
217
+ const flatResponse = {
218
+ status: { code: 202 },
219
+ response: {
220
+ _id: 'flat-asset-789',
221
+ type: 'VIDEO',
222
+ processingStatus: ASSET_STATUS.PROCESSING,
223
+ },
224
+ };
225
+ return expectSaga(sagas.uploadViberAsset, { ...mockParams, assetType: undefined })
226
+ .provide([
227
+ [matchers.call.fn(Api.uploadFile), flatResponse],
228
+ [matchers.call.fn(pollAssetStatus), undefined],
229
+ ])
230
+ .put({
231
+ type: types.UPLOAD_VIBER_ASSET_PROCESSING,
232
+ payload: {
233
+ assetId: 'flat-asset-789',
234
+ asset: flatResponse.response,
235
+ processingStatus: ASSET_STATUS.PROCESSING,
236
+ },
237
+ })
238
+ .call.fn(pollAssetStatus)
239
+ .run();
240
+ });
241
+
242
+ it('normalizes a flat sync response (asset fields at top level)', () => {
243
+ const flatAsset = { _id: 'flat-sync-1', url: 'https://cdn/x.jpg' };
244
+ const flatResponse = {
245
+ status: { code: 201 },
246
+ response: flatAsset,
247
+ };
248
+ return expectSaga(sagas.uploadViberAsset, mockParams)
249
+ .provide([
250
+ [matchers.call.fn(Api.uploadFile), flatResponse],
251
+ ])
252
+ .put({
253
+ type: types.UPLOAD_VIBER_ASSET_SUCCESS,
254
+ data: flatAsset,
255
+ statusCode: 201,
256
+ templateType: 0,
257
+ })
258
+ .run();
259
+ });
260
+
261
+ it('derives assetType from asset.type when params.assetType omitted', () => {
262
+ const paramsWithoutType = {
263
+ file: new File(['v'], 'v.mp4', { type: 'video/mp4' }),
264
+ fileParams: {},
265
+ templateType: 1,
266
+ };
267
+ const mockResponse = {
268
+ status: { code: 202 },
269
+ response: {
270
+ assetId: 'asset-vid',
271
+ asset: { _id: 'asset-vid', type: 'VIDEO' },
272
+ },
273
+ };
274
+ return expectSaga(sagas.uploadViberAsset, paramsWithoutType)
275
+ .provide([
276
+ [matchers.call.fn(Api.uploadFile), mockResponse],
277
+ [matchers.call.fn(pollAssetStatus), undefined],
278
+ ])
279
+ .call.fn(pollAssetStatus)
280
+ .run();
281
+ });
282
+
283
+ it('defaults assetType to image when no type info available', () => {
284
+ const paramsWithoutType = {
285
+ file: new File(['x'], 'x', { type: '' }),
286
+ fileParams: {},
287
+ templateType: 0,
288
+ };
289
+ const mockResponse = {
290
+ status: { code: 202 },
291
+ response: {
292
+ assetId: 'asset-noop',
293
+ },
294
+ };
295
+ return expectSaga(sagas.uploadViberAsset, paramsWithoutType)
296
+ .provide([
297
+ [matchers.call.fn(Api.uploadFile), mockResponse],
298
+ [matchers.call.fn(pollAssetStatus), undefined],
299
+ ])
300
+ .call.fn(pollAssetStatus)
301
+ .run();
302
+ });
48
303
  });
49
304
 
50
305
  describe('createViberTemplate Saga', () => {
@@ -54,18 +309,18 @@ describe('Viber Sagas', () => {
54
309
  it('handles creating template successfully', () => {
55
310
  const fakeResponse = {
56
311
  response: { id: 2, content: template.content },
57
- status: { code: 201 }
312
+ status: { code: 201 },
58
313
  };
59
314
 
60
315
  return expectSaga(sagas.createViberTemplate, { template, callback })
61
316
  .provide([
62
- [call(Api.createViberTemplate, { template }), fakeResponse]
317
+ [call(Api.createViberTemplate, { template }), fakeResponse],
63
318
  ])
64
319
  .put({
65
320
  type: types.CREATE_VIBER_TEMPLATE_SUCCESS,
66
321
  data: fakeResponse.response,
67
322
  statusCode: fakeResponse.status.code,
68
- errorMsg: undefined
323
+ errorMsg: undefined,
69
324
  })
70
325
  .run()
71
326
  .then(() => {
@@ -76,21 +331,74 @@ describe('Viber Sagas', () => {
76
331
  it('handles failure in creating template', () => {
77
332
  const error = new Error({ message: 'Creation failed', status: { code: 400 } });
78
333
 
79
- const errorMsg = 'Creation failed';
80
334
  return expectSaga(sagas.createViberTemplate, { template, callback })
81
335
  .provide([
82
- [call(Api.createViberTemplate, { template }), throwError(error)]
336
+ [call(Api.createViberTemplate, { template }), throwError(error)],
83
337
  ])
84
338
  .put({
85
339
  type: types.CREATE_VIBER_TEMPLATE_FAILURE,
86
340
  error,
87
- errorMsg : undefined
341
+ errorMsg: undefined,
88
342
  })
89
343
  .run()
90
344
  .then(() => {
91
345
  expect(callback).toHaveBeenCalledWith(null, undefined);
92
346
  });
93
347
  });
348
+
349
+ it('handles 4xx API response (treated as failure)', () => {
350
+ const cb = jest.fn();
351
+ const fakeResponse = {
352
+ response: {},
353
+ status: { code: 400 },
354
+ message: 'bad request',
355
+ };
356
+ return expectSaga(sagas.createViberTemplate, { template, callback: cb })
357
+ .provide([
358
+ [call(Api.createViberTemplate, { template }), fakeResponse],
359
+ ])
360
+ .put({
361
+ type: types.CREATE_VIBER_TEMPLATE_FAILURE,
362
+ error: 'bad request',
363
+ errorMsg: 'bad request',
364
+ })
365
+ .run()
366
+ .then(() => {
367
+ expect(cb).toHaveBeenCalledWith(null, 'bad request');
368
+ });
369
+ });
370
+
371
+ it('handles success when no callback is supplied', () => {
372
+ const fakeResponse = {
373
+ response: { id: 42 },
374
+ status: { code: 201 },
375
+ };
376
+ return expectSaga(sagas.createViberTemplate, { template })
377
+ .provide([
378
+ [call(Api.createViberTemplate, { template }), fakeResponse],
379
+ ])
380
+ .put({
381
+ type: types.CREATE_VIBER_TEMPLATE_SUCCESS,
382
+ data: fakeResponse.response,
383
+ statusCode: 201,
384
+ errorMsg: undefined,
385
+ })
386
+ .run();
387
+ });
388
+
389
+ it('handles failure when no callback is supplied', () => {
390
+ const error = new Error('Creation failed');
391
+ return expectSaga(sagas.createViberTemplate, { template })
392
+ .provide([
393
+ [call(Api.createViberTemplate, { template }), throwError(error)],
394
+ ])
395
+ .put({
396
+ type: types.CREATE_VIBER_TEMPLATE_FAILURE,
397
+ error,
398
+ errorMsg: undefined,
399
+ })
400
+ .run();
401
+ });
94
402
  });
95
403
 
96
404
  describe('editTemplate Saga', () => {
@@ -100,18 +408,18 @@ describe('Viber Sagas', () => {
100
408
  it('handles editing template successfully', () => {
101
409
  const fakeResponse = {
102
410
  response: { updated: true },
103
- status: { code: 200 }
411
+ status: { code: 200 },
104
412
  };
105
413
 
106
414
  return expectSaga(sagas.editTemplate, { template, callback })
107
415
  .provide([
108
- [call(Api.createViberTemplate, { template }), fakeResponse]
416
+ [call(Api.createViberTemplate, { template }), fakeResponse],
109
417
  ])
110
418
  .put({
111
419
  type: types.EDIT_VIBER_TEMPLATE_SUCCESS,
112
420
  data: fakeResponse.response,
113
421
  statusCode: fakeResponse.status.code,
114
- errorMsg: undefined
422
+ errorMsg: undefined,
115
423
  })
116
424
  .run()
117
425
  .then(() => {
@@ -123,7 +431,7 @@ describe('Viber Sagas', () => {
123
431
  const errorMsg = 'Error in editing template';
124
432
  return expectSaga(sagas.editTemplate, { template, callback })
125
433
  .provide([
126
- [call(Api.createViberTemplate, { template }), throwError({ message: errorMsg, status: { code: 400 } })]
434
+ [call(Api.createViberTemplate, { template }), throwError({ message: errorMsg, status: { code: 400 } })],
127
435
  ])
128
436
  .put({
129
437
  type: types.EDIT_VIBER_TEMPLATE_FAILURE,
@@ -135,6 +443,60 @@ describe('Viber Sagas', () => {
135
443
  expect(callback).toHaveBeenCalledWith(null, undefined);
136
444
  });
137
445
  });
446
+
447
+ it('handles 4xx API response (treated as failure)', () => {
448
+ const cb = jest.fn();
449
+ const fakeResponse = {
450
+ response: {},
451
+ status: { code: 422 },
452
+ message: 'invalid payload',
453
+ };
454
+ return expectSaga(sagas.editTemplate, { template, callback: cb })
455
+ .provide([
456
+ [call(Api.createViberTemplate, { template }), fakeResponse],
457
+ ])
458
+ .put({
459
+ type: types.EDIT_VIBER_TEMPLATE_FAILURE,
460
+ error: 'invalid payload',
461
+ errorMsg: 'invalid payload',
462
+ })
463
+ .run()
464
+ .then(() => {
465
+ expect(cb).toHaveBeenCalledWith(null, 'invalid payload');
466
+ });
467
+ });
468
+
469
+ it('handles success when no callback is supplied', () => {
470
+ const fakeResponse = {
471
+ response: { updated: true },
472
+ status: { code: 200 },
473
+ };
474
+ return expectSaga(sagas.editTemplate, { template })
475
+ .provide([
476
+ [call(Api.createViberTemplate, { template }), fakeResponse],
477
+ ])
478
+ .put({
479
+ type: types.EDIT_VIBER_TEMPLATE_SUCCESS,
480
+ data: fakeResponse.response,
481
+ statusCode: 200,
482
+ errorMsg: undefined,
483
+ })
484
+ .run();
485
+ });
486
+
487
+ it('handles failure when no callback is supplied', () => {
488
+ const error = new Error('Edit failed');
489
+ return expectSaga(sagas.editTemplate, { template })
490
+ .provide([
491
+ [call(Api.createViberTemplate, { template }), throwError(error)],
492
+ ])
493
+ .put({
494
+ type: types.EDIT_VIBER_TEMPLATE_FAILURE,
495
+ error,
496
+ errorMsg: undefined,
497
+ })
498
+ .run();
499
+ });
138
500
  });
139
501
 
140
502
  describe('getTemplateDetails Saga', () => {
@@ -143,16 +505,16 @@ describe('Viber Sagas', () => {
143
505
 
144
506
  it('handles fetching template details successfully', () => {
145
507
  const fakeResponse = {
146
- response: { id: id, name: 'Detailed Template' }
508
+ response: { id, name: 'Detailed Template' },
147
509
  };
148
510
 
149
511
  return expectSaga(sagas.getTemplateDetails, { id, callback })
150
512
  .provide([
151
- [call(Api.getTemplateDetails, { id, channel: 'VIBER' }), fakeResponse]
513
+ [call(Api.getTemplateDetails, { id, channel: 'VIBER' }), fakeResponse],
152
514
  ])
153
515
  .put({
154
516
  type: types.GET_VIBER_TEMPLATE_DETAILS_SUCCESS,
155
- data: fakeResponse.response
517
+ data: fakeResponse.response,
156
518
  })
157
519
  .run()
158
520
  .then(() => {
@@ -165,23 +527,33 @@ describe('Viber Sagas', () => {
165
527
 
166
528
  return expectSaga(sagas.getTemplateDetails, { id, callback })
167
529
  .provide([
168
- [call(Api.getTemplateDetails, { id, channel: 'VIBER' }), throwError(error)]
530
+ [call(Api.getTemplateDetails, { id, channel: 'VIBER' }), throwError(error)],
169
531
  ])
170
532
  .put({
171
533
  type: types.GET_VIBER_TEMPLATE_DETAILS_FAILURE,
172
- error
534
+ error,
173
535
  })
174
536
  .run()
175
537
  .then(() => {
176
538
  expect(callback).not.toHaveBeenCalledWith();
177
539
  });
178
540
  });
179
- });
180
541
 
181
- describe('v2ViberSagas Combined', () => {
182
- it('should initialize all Viber-related watcher sagas without error', () => {
183
- return expectSaga(v2ViberSagas).run();
542
+ it('handles success without a callback', () => {
543
+ const fakeResponse = { response: { id, name: 'No CB' } };
544
+ return expectSaga(sagas.getTemplateDetails, { id })
545
+ .provide([
546
+ [call(Api.getTemplateDetails, { id, channel: 'VIBER' }), fakeResponse],
547
+ ])
548
+ .put({
549
+ type: types.GET_VIBER_TEMPLATE_DETAILS_SUCCESS,
550
+ data: fakeResponse.response,
551
+ })
552
+ .run();
184
553
  });
185
554
  });
186
555
 
187
- });
556
+ describe('v2ViberSagas Combined', () => {
557
+ it('should initialize all Viber-related watcher sagas without error', () => expectSaga(sagas.v2ViberSagas).run());
558
+ });
559
+ });