@capillarytech/creatives-library 9.0.56-betaa.0 → 9.0.56

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 (116) hide show
  1. package/constants/unified.js +9 -4
  2. package/package.json +1 -1
  3. package/services/api.js +25 -4
  4. package/utils/common.js +0 -12
  5. package/utils/templateVarUtils.js +16 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/index.js +73 -60
  8. package/v2Components/CapActionButton/index.scss +44 -28
  9. package/v2Components/CapActionButton/messages.js +7 -3
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  11. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  12. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  13. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  15. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  16. package/v2Components/CommonTestAndPreview/constants.js +8 -0
  17. package/v2Components/CommonTestAndPreview/index.js +55 -3
  18. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  19. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  20. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  22. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  26. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  27. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  28. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +46 -320
  29. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  30. package/v2Components/FormBuilder/Functional/constants.js +8 -43
  31. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  32. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  33. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  34. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  35. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  36. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  37. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  38. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  39. package/v2Components/FormBuilder/index.js +12 -34
  40. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -94
  41. package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
  42. package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
  43. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
  44. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
  45. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
  46. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
  47. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  48. package/v2Containers/CommunicationFlow/constants.js +65 -4
  49. package/v2Containers/CommunicationFlow/index.js +16 -23
  50. package/v2Containers/CommunicationFlow/messages.js +11 -3
  51. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
  52. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  53. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
  54. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
  55. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
  56. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  57. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
  58. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  59. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  60. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  61. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  62. package/v2Containers/CreativesContainer/index.js +29 -3
  63. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  64. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
  65. package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
  66. package/v2Containers/MobilePush/Create/index.js +18 -1
  67. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  68. package/v2Containers/MobilePush/Edit/index.js +18 -1
  69. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  70. package/v2Containers/MobilePush/commonMethods.js +49 -1
  71. package/v2Containers/MobilePushNew/index.js +29 -4
  72. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  73. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  74. package/v2Containers/MobilePushNew/utils.js +34 -1
  75. package/v2Containers/MobilepushWrapper/index.js +3 -1
  76. package/v2Containers/Rcs/index.js +34 -0
  77. package/v2Containers/Rcs/index.scss +15 -0
  78. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
  79. package/v2Containers/Rcs/tests/index.test.js +67 -0
  80. package/v2Containers/Sms/Create/index.js +3 -1
  81. package/v2Containers/Sms/Edit/index.js +25 -0
  82. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  83. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
  84. package/v2Containers/Viber/index.js +24 -1
  85. package/v2Containers/Viber/tests/index.test.js +103 -0
  86. package/v2Containers/WebPush/Create/index.js +19 -0
  87. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  88. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  89. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  90. package/v2Containers/Whatsapp/index.js +25 -4
  91. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
  92. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  93. package/v2Containers/Zalo/index.js +28 -1
  94. package/v2Containers/Zalo/tests/index.test.js +99 -0
  95. package/v2Containers/mockdata.js +25 -0
  96. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  97. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -82
  98. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  99. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  100. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -19
  101. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  102. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  103. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  104. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  105. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  106. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  107. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -89
  108. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -440
  109. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  110. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -282
  111. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -141
  112. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  113. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -151
  114. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -342
  115. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  116. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -1,162 +0,0 @@
1
- /**
2
- * MPUSH getEditorErrorDescriptor — the library-mode footer echo of the active
3
- * tab's editor error (mirror of Classic's render-phase push, Classic.js:2885-2895).
4
- */
5
- import getEditorErrorDescriptor from '../getEditorErrorDescriptor';
6
- import { ERROR_VALUE } from '../config';
7
- import { EDITOR_ERROR_KIND } from '../../../constants';
8
- import formMessages from '../../../../messages';
9
-
10
- const EDITOR_ERROR_TEXT = 'Template message has unsupported/missing tags!';
11
-
12
- const schemaWithEditor = (fieldId = 'message-editor', paneIndex = 0) => {
13
- const panes = [{ sections: [] }, { sections: [] }];
14
- panes[paneIndex] = {
15
- sections: [{
16
- type: 'multicols',
17
- inputFields: [{ cols: [{ id: fieldId, type: 'textarea', errorMessage: EDITOR_ERROR_TEXT }] }],
18
- }],
19
- };
20
- return { containers: [{ id: 'pane', type: 'tabs', panes }] };
21
- };
22
-
23
- // Same editor field, but reached through parent -> col-label sections (the other
24
- // findPaneField walk branches).
25
- const colLabelSchema = (fieldId = 'message-editor') => ({
26
- containers: [{
27
- id: 'pane',
28
- type: 'tabs',
29
- panes: [{
30
- sections: [{
31
- type: 'parent',
32
- childSections: [{
33
- type: 'col-label',
34
- inputFields: [{ id: fieldId, type: 'textarea', errorMessage: EDITOR_ERROR_TEXT }],
35
- }],
36
- }],
37
- }, { sections: [] }],
38
- }],
39
- });
40
-
41
- const legacyWith = (content, tabIndex = 0, fieldId = 'message-editor') => ({
42
- [tabIndex]: { [fieldId]: content, tabKey: `k${tabIndex}` },
43
- });
44
-
45
- const baseCtx = (overrides = {}) => ({
46
- isFullMode: false,
47
- currentTab: 1,
48
- schema: schemaWithEditor(),
49
- checkValidation: true,
50
- restrictPersonalization: false,
51
- ...overrides,
52
- });
53
-
54
- describe('channels/mobilepush/getEditorErrorDescriptor', () => {
55
- it('returns null in full mode (MPUSH errors show inline there, never as an inline-brace override)', () => {
56
- const ctx = baseCtx({ isFullMode: true, errorData: { 0: { 'message-editor': true } } });
57
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
58
- });
59
-
60
- it('returns null when the active tab has no editor error', () => {
61
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': false } } });
62
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
63
- });
64
-
65
- it('echoes the schema errorMessage for a generic editor error', () => {
66
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': true } } });
67
- expect(getEditorErrorDescriptor(legacyWith('{{unknown}}'), ctx)).toEqual({
68
- text: EDITOR_ERROR_TEXT,
69
- kind: EDITOR_ERROR_KIND.MISSING,
70
- });
71
- });
72
-
73
- it('finds the editor field through parent -> col-label pane sections too', () => {
74
- const ctx = baseCtx({ schema: colLabelSchema(), errorData: { 0: { 'message-editor': true } } });
75
- expect(getEditorErrorDescriptor(legacyWith('{{unknown}}'), ctx)).toEqual({
76
- text: EDITOR_ERROR_TEXT,
77
- kind: EDITOR_ERROR_KIND.MISSING,
78
- });
79
- });
80
-
81
- it('defaults: no arguments => null; missing currentTab resolves to tab 0', () => {
82
- expect(getEditorErrorDescriptor()).toBeNull();
83
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': true } } });
84
- delete ctx.currentTab;
85
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toEqual({
86
- text: EDITOR_ERROR_TEXT,
87
- kind: EDITOR_ERROR_KIND.MISSING,
88
- });
89
- });
90
-
91
- it('an inactive tabs container hides the field => no echo (null)', () => {
92
- const schema = schemaWithEditor();
93
- schema.containers[0].isActive = false;
94
- const ctx = baseCtx({ schema, errorData: { 0: { 'message-editor': true } } });
95
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
96
- });
97
-
98
- it('also locates the editor field among a multicols row\'s actionFields', () => {
99
- const schema = schemaWithEditor();
100
- const section = schema.containers[0].panes[0].sections[0];
101
- section.actionFields = section.inputFields;
102
- section.inputFields = [];
103
- const ctx = baseCtx({ schema, errorData: { 0: { 'message-editor': true } } });
104
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toEqual({
105
- text: EDITOR_ERROR_TEXT,
106
- kind: EDITOR_ERROR_KIND.MISSING,
107
- });
108
- });
109
-
110
- it('marks a brace error with the BRACE kind', () => {
111
- const ctx = baseCtx({ errorData: { 0: { 'message-editor': ERROR_VALUE.BRACKET } } });
112
- expect(getEditorErrorDescriptor(legacyWith('abc {{full_name'), ctx)).toEqual({
113
- text: EDITOR_ERROR_TEXT,
114
- kind: EDITOR_ERROR_KIND.BRACE,
115
- });
116
- });
117
-
118
- it('gates EVERY error on checkValidation — no footer echo before a save attempt (Classic parity: errorData is only computed under startValidation, Classic.js:334-345)', () => {
119
- const generic = { 0: { 'message-editor': true } };
120
- expect(getEditorErrorDescriptor(legacyWith('{{unknown}}'), baseCtx({ errorData: generic, checkValidation: false }))).toBeNull();
121
- const brace = { 0: { 'message-editor': ERROR_VALUE.BRACKET } };
122
- expect(getEditorErrorDescriptor(legacyWith('abc {{full_name'), baseCtx({ errorData: brace, checkValidation: false }))).toBeNull();
123
- });
124
-
125
- it('gates the EMPTY-content error on checkValidation (hidden until Save, like the inline message)', () => {
126
- const errorData = { 0: { 'message-editor': true } };
127
- expect(getEditorErrorDescriptor(legacyWith(''), baseCtx({ errorData, checkValidation: false }))).toBeNull();
128
- expect(getEditorErrorDescriptor(legacyWith(' '), baseCtx({ errorData, checkValidation: false }))).toBeNull();
129
- expect(getEditorErrorDescriptor(legacyWith(''), baseCtx({ errorData, checkValidation: true }))).toEqual({
130
- text: EDITOR_ERROR_TEXT,
131
- kind: EDITOR_ERROR_KIND.MISSING,
132
- });
133
- });
134
-
135
- it('returns the personalization intl descriptor when restricted tokens are present', () => {
136
- const ctx = baseCtx({
137
- restrictPersonalization: true,
138
- errorData: { 0: { 'message-editor': true } },
139
- });
140
- expect(getEditorErrorDescriptor(legacyWith('hi [eventAttr]'), ctx)).toEqual({
141
- message: formMessages.personalizationTagsErrorMessage,
142
- kind: EDITOR_ERROR_KIND.MISSING,
143
- });
144
- });
145
-
146
- it('resolves the iOS tab through the suffixed field id (currentTab=2 => message-editor2)', () => {
147
- const ctx = baseCtx({
148
- currentTab: 2,
149
- schema: schemaWithEditor('message-editor2', 1),
150
- errorData: { 1: { 'message-editor2': true } },
151
- });
152
- expect(getEditorErrorDescriptor(legacyWith('{{x}}', 1, 'message-editor2'), ctx)).toEqual({
153
- text: EDITOR_ERROR_TEXT,
154
- kind: EDITOR_ERROR_KIND.MISSING,
155
- });
156
- });
157
-
158
- it('returns null when the schema pane has no errorMessage to echo', () => {
159
- const ctx = baseCtx({ schema: { containers: [] }, errorData: { 0: { 'message-editor': true } } });
160
- expect(getEditorErrorDescriptor(legacyWith('abc'), ctx)).toBeNull();
161
- });
162
- });
@@ -1,37 +0,0 @@
1
- /**
2
- * MPUSH modal semantics — Classic's single-platform handleOk (Classic.js:2674-2709):
3
- * the OK id dispatches a single-platform save only when that platform's last
4
- * validation flag is true; anything else silently closes the modal.
5
- */
6
- import mpushModals from '../modals';
7
-
8
- describe('channels/mobilepush/modals.handleOk', () => {
9
- const run = (id, platform) => {
10
- const submitSingleTab = jest.fn();
11
- mpushModals.handleOk(id, { platform, submitSingleTab });
12
- return submitSingleTab;
13
- };
14
-
15
- it('android OK + androidValid => saves android alone', () => {
16
- expect(run('android', { androidValid: true, iosValid: false }))
17
- .toHaveBeenCalledWith('android');
18
- });
19
-
20
- it('ios OK + iosValid => saves ios alone', () => {
21
- expect(run('ios', { androidValid: false, iosValid: true }))
22
- .toHaveBeenCalledWith('ios');
23
- });
24
-
25
- it('a falsy platform flag silently closes (no submit) — Classic parity', () => {
26
- expect(run('android', { androidValid: false, iosValid: true })).not.toHaveBeenCalled();
27
- expect(run('ios', { androidValid: true, iosValid: false })).not.toHaveBeenCalled();
28
- });
29
-
30
- it('unknown id / missing platform / missing callback are all no-ops', () => {
31
- expect(run('template-back-confirm-modal', { androidValid: true, iosValid: true }))
32
- .not.toHaveBeenCalled();
33
- expect(run('android', null)).not.toHaveBeenCalled();
34
- expect(() => mpushModals.handleOk('android', { platform: { androidValid: true } })).not.toThrow();
35
- expect(() => mpushModals.handleOk('android')).not.toThrow();
36
- });
37
- });
@@ -1,70 +0,0 @@
1
- /**
2
- * MPUSH submit pipeline — Classic's onSubmitWrapper short-circuit +
3
- * validateFormBuilderMPush contract (Classic.js:1397-1405, 1473-1522).
4
- */
5
- import runSubmitPipeline from '../runSubmitPipeline';
6
- import { validateMobilePushContent } from '../../../../../../utils/commonUtils';
7
-
8
- jest.mock('../../../../../../utils/commonUtils', () => ({
9
- validateMobilePushContent: jest.fn(),
10
- }));
11
-
12
- const legacy = { 'template-name': 'p', 0: { 'message-editor': 'a' }, 1: {} };
13
-
14
- const buildCallbacks = () => ({
15
- onSubmit: jest.fn(),
16
- pushFooter: jest.fn(),
17
- stopValidation: jest.fn(),
18
- onFormValidityChange: jest.fn(),
19
- });
20
-
21
- describe('channels/mobilepush/runSubmitPipeline', () => {
22
- beforeEach(() => jest.clearAllMocks());
23
-
24
- it('full mode submits the raw legacy formData directly — no liquid validation', () => {
25
- const callbacks = buildCallbacks();
26
- runSubmitPipeline(legacy, { isFullMode: true, getLiquidTags: jest.fn() }, callbacks);
27
- expect(callbacks.onSubmit).toHaveBeenCalledWith(legacy);
28
- expect(validateMobilePushContent).not.toHaveBeenCalled();
29
- });
30
-
31
- it('library mode without getLiquidTags also submits directly', () => {
32
- const callbacks = buildCallbacks();
33
- runSubmitPipeline(legacy, { isFullMode: false }, callbacks);
34
- expect(callbacks.onSubmit).toHaveBeenCalledWith(legacy);
35
- expect(validateMobilePushContent).not.toHaveBeenCalled();
36
- });
37
-
38
- it('library mode runs the liquid pipeline; singleTab is upper-cased; success submits the FULL formData', () => {
39
- const callbacks = buildCallbacks();
40
- runSubmitPipeline(legacy, {
41
- isFullMode: false, getLiquidTags: jest.fn(), singleTab: 'android', currentTab: 1,
42
- }, callbacks);
43
- const options = validateMobilePushContent.mock.calls[0][1];
44
- expect(options.singleTab).toBe('ANDROID');
45
- options.onSuccess('{"ignored":"single-tab json"}', 'android');
46
- expect(callbacks.onSubmit).toHaveBeenCalledWith(legacy); // contentToSubmit IGNORED
47
- expect(callbacks.pushFooter).toHaveBeenCalledWith({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
48
- });
49
-
50
- it('error callback forwards the nested per-platform objects AS-IS, stops validation, and blocks the save', () => {
51
- const callbacks = buildCallbacks();
52
- runSubmitPipeline(legacy, { isFullMode: false, getLiquidTags: jest.fn() }, callbacks);
53
- const options = validateMobilePushContent.mock.calls[0][1];
54
- const standardErrors = { ANDROID: ['bad tag'], IOS: [], generic: [] };
55
- options.onError({ standardErrors, liquidErrors: [] });
56
- expect(callbacks.pushFooter).toHaveBeenCalledWith({ STANDARD_ERROR_MSG: standardErrors, LIQUID_ERROR_MSG: [] });
57
- expect(callbacks.stopValidation).toHaveBeenCalled();
58
- expect(callbacks.onFormValidityChange).toHaveBeenCalledWith(false);
59
- expect(callbacks.onSubmit).not.toHaveBeenCalled();
60
- expect(options.singleTab).toBeUndefined(); // no modal => both platforms validate
61
- });
62
-
63
- it('tolerates missing callbacks (every callback is optional)', () => {
64
- runSubmitPipeline(legacy, { isFullMode: false, getLiquidTags: jest.fn() }, {});
65
- const options = validateMobilePushContent.mock.calls[0][1];
66
- expect(() => options.onError({ standardErrors: [], liquidErrors: [] })).not.toThrow();
67
- expect(() => options.onSuccess('x')).not.toThrow();
68
- expect(() => runSubmitPipeline(legacy, { isFullMode: true }, {})).not.toThrow();
69
- });
70
- });
@@ -1,274 +0,0 @@
1
- /**
2
- * MOBILEPUSH validate — unit tests for the line-faithful port of Classic's
3
- * validateForm MPUSH block (Classic.js:935-1218), including the quirks the
4
- * design doc pins (FormBuilder-V3-MPUSH-Technical-Design.md §6.1/§6.2).
5
- */
6
- import validateMobilePush, { isValidExternalLink } from '../validate';
7
- import { ERROR_VALUE } from '../config';
8
-
9
- const baseOptions = { tags: [], isFullMode: true, isEmbedded: false };
10
-
11
- const bothPlatforms = (overrides = {}) => ({
12
- 'template-name': 'my push',
13
- "0": {
14
- 'message-title': 'hello', 'message-editor': 'world', "base": true, "tabKey": 'k0',
15
- },
16
- "1": { 'message-title2': 'bonjour', 'message-editor2': 'monde', "tabKey": 'k1' },
17
- ...overrides,
18
- });
19
-
20
- describe('channels/mobilepush/validate', () => {
21
- it('valid when both platforms carry title + message and template-name is set', () => {
22
- const result = validateMobilePush(bothPlatforms(), baseOptions);
23
- expect(result.isValid).toBe(true);
24
- expect(result.errorData[0]['message-editor']).toBe(false);
25
- expect(result.errorData[1]['message-editor2']).toBe(false);
26
- expect(result.errorData['template-name']).toBe(false);
27
- expect(result.platform).toEqual({
28
- androidValid: true, iosValid: true, isOnlyAndroid: false, isOnlyIos: false,
29
- });
30
- expect(result.singlePlatformPrompt).toBeNull();
31
- });
32
-
33
- it('QUIRK: absent tab 0 skips ALL validation (template-name included) and stays valid', () => {
34
- const result = validateMobilePush({ 'template-name': '' }, baseOptions);
35
- expect(result.isValid).toBe(true);
36
- expect(result.errorData).toEqual({});
37
- expect(result.singlePlatformPrompt).toBeNull();
38
- });
39
-
40
- it('missing message => error true; missing title clears the tab flag, missing message does NOT (asymmetry)', () => {
41
- // Android has a title but no message: isValid false, but androidValid STAYS true.
42
- const onlyTitle = validateMobilePush(bothPlatforms({
43
- 0: { 'message-title': 'hello', "base": true, "tabKey": 'k0' },
44
- }), baseOptions);
45
- expect(onlyTitle.isValid).toBe(false);
46
- expect(onlyTitle.errorData[0]['message-editor']).toBe(true);
47
- expect(onlyTitle.platform.androidValid).toBe(true); // Classic parity: not cleared
48
-
49
- // Android has a message but no title: androidValid IS cleared.
50
- const onlyMessage = validateMobilePush(bothPlatforms({
51
- 0: { 'message-editor': 'world', "base": true, "tabKey": 'k0' },
52
- }), baseOptions);
53
- expect(onlyMessage.isValid).toBe(false);
54
- expect(onlyMessage.errorData[0]['message-title']).toBe(true);
55
- expect(onlyMessage.platform.androidValid).toBe(false);
56
- });
57
-
58
- it('brace mismatch writes the STRING error value + bracket-error, and does NOT clear the tab flag', () => {
59
- const result = validateMobilePush(bothPlatforms({
60
- 0: {
61
- 'message-title': 'hello', 'message-editor': 'abc {{full_name', "base": true, "tabKey": 'k0',
62
- },
63
- }), baseOptions);
64
- expect(result.isValid).toBe(false);
65
- expect(result.errorData[0]['message-editor']).toBe(ERROR_VALUE.BRACKET);
66
- expect(result.errorData[0]['bracket-error']).toBe(ERROR_VALUE.BRACKET);
67
- // The single-platform modal stays reachable on tag errors (Classic parity).
68
- expect(result.platform.androidValid).toBe(true);
69
- });
70
-
71
- it('template-name: required in full mode (and suppresses the platform flags); skipped when embedded', () => {
72
- const full = validateMobilePush(bothPlatforms({ 'template-name': '' }), baseOptions);
73
- expect(full.isValid).toBe(false);
74
- expect(full.errorData['template-name']).toBe(true);
75
- expect(full.platform.androidValid).toBe(false);
76
- expect(full.platform.iosValid).toBe(false);
77
- expect(full.singlePlatformPrompt).toBeNull(); // both flags false => no prompt
78
-
79
- const embedded = validateMobilePush(
80
- bothPlatforms({ 'template-name': '' }),
81
- { ...baseOptions, isEmbedded: true },
82
- );
83
- expect(embedded.errorData['template-name']).toBe(false);
84
- expect(embedded.isValid).toBe(true);
85
- });
86
-
87
- it('single-platform prompt: iOS empty + saving from the Android tab => "android"', () => {
88
- const result = validateMobilePush(bothPlatforms({
89
- 1: { tabKey: 'k1' },
90
- }), {
91
- ...baseOptions, isSave: true, currentTab: 1,
92
- });
93
- expect(result.singlePlatformPrompt).toBe('android');
94
- expect(result.platform.isOnlyAndroid).toBe(true);
95
- });
96
-
97
- it('single-platform prompt: Android empty + saving from the iOS tab => "ios"', () => {
98
- const result = validateMobilePush(bothPlatforms({
99
- 0: { base: true, tabKey: 'k0' },
100
- }), {
101
- ...baseOptions, isSave: true, currentTab: 2,
102
- });
103
- expect(result.singlePlatformPrompt).toBe('ios');
104
- expect(result.platform.isOnlyIos).toBe(true);
105
- });
106
-
107
- it('no prompt when the empty platform does not match the current tab (Classic co-condition)', () => {
108
- // iOS empty but the user is ON the iOS tab (currentTab 2): no prompt.
109
- const result = validateMobilePush(bothPlatforms({
110
- 1: { tabKey: 'k1' },
111
- }), {
112
- ...baseOptions, isSave: true, currentTab: 2,
113
- });
114
- expect(result.singlePlatformPrompt).toBeNull();
115
- });
116
-
117
- it('no prompt without isSave', () => {
118
- const result = validateMobilePush(bothPlatforms({ 1: { tabKey: 'k1' } }), {
119
- ...baseOptions, isSave: false, currentTab: 1,
120
- });
121
- expect(result.singlePlatformPrompt).toBeNull();
122
- });
123
-
124
- it('embedded mode skips validating the tab that is not open (empty other platform)', () => {
125
- const result = validateMobilePush(bothPlatforms({
126
- "1": { tabKey: 'k1' }, 'template-name': '',
127
- }), {
128
- ...baseOptions, isEmbedded: true,
129
- });
130
- // The empty iOS tab (isOnlyAndroid) is skipped entirely: no message-editor2 error.
131
- expect(result.errorData[1]).toBeUndefined();
132
- expect(result.isValid).toBe(true);
133
- });
134
-
135
- it('CTA rules: empty-string select fails, ABSENT key passes; labels required when present-empty', () => {
136
- const failing = validateMobilePush(bothPlatforms({
137
- 0: {
138
- 'message-title': 'a',
139
- 'message-editor': 'b',
140
- 'cta-deeplink-select': '',
141
- 'secondary-cta-0-label': '',
142
- "base": true,
143
- "tabKey": 'k0',
144
- },
145
- }), baseOptions);
146
- expect(failing.isValid).toBe(false);
147
- expect(failing.errorData[0]['cta-deeplink-select']).toBe(true);
148
- expect(failing.errorData[0]['secondary-cta-0-label']).toBe(true);
149
- expect(failing.platform.androidValid).toBe(false);
150
-
151
- const absent = validateMobilePush(bothPlatforms(), baseOptions);
152
- expect(absent.errorData[0]['cta-deeplink-select']).toBe(false); // absent key => rule passes
153
- });
154
-
155
- it('external-link rules use isValidExternalLink semantics (scheme required)', () => {
156
- const badUrl = validateMobilePush(bothPlatforms({
157
- 0: {
158
- 'message-title': 'a', 'message-editor': 'b', 'cta-deeplink-text': 'www.no-scheme.com', "base": true, "tabKey": 'k0',
159
- },
160
- }), baseOptions);
161
- expect(badUrl.errorData[0]['cta-deeplink-text']).toBe(true);
162
- expect(badUrl.isValid).toBe(false);
163
-
164
- const goodUrl = validateMobilePush(bothPlatforms({
165
- 0: {
166
- 'message-title': 'a', 'message-editor': 'b', 'cta-deeplink-text': 'https://capillarytech.com/x', "base": true, "tabKey": 'k0',
167
- },
168
- }), baseOptions);
169
- expect(goodUrl.errorData[0]['cta-deeplink-text']).toBe(false);
170
- expect(goodUrl.isValid).toBe(true);
171
- });
172
-
173
- it('image: required only when the key exists (imageSchema); custom-value-* keys required when present', () => {
174
- const result = validateMobilePush(bothPlatforms({
175
- 0: {
176
- 'message-title': 'a',
177
- 'message-editor': 'b',
178
- "image": '',
179
- 'custom-value-userId-cta-deeplink-select': '',
180
- "base": true,
181
- "tabKey": 'k0',
182
- },
183
- }), baseOptions);
184
- expect(result.errorData[0].image).toBe(true);
185
- expect(result.errorData[0]['custom-value-userId-cta-deeplink-select']).toBe(true);
186
- expect(result.isValid).toBe(false);
187
-
188
- const textSchema = validateMobilePush(bothPlatforms(), baseOptions);
189
- expect(textSchema.errorData[0].image).toBe(false); // always written, false when absent
190
- });
191
-
192
- it('personalization tokens are rejected when restrictPersonalization is on', () => {
193
- const result = validateMobilePush(bothPlatforms({
194
- 0: {
195
- 'message-title': 'hi [eventAttr]', 'message-editor': 'b', "base": true, "tabKey": 'k0',
196
- },
197
- }), { ...baseOptions, restrictPersonalization: true });
198
- expect(result.errorData[0]['message-title']).toBe(true);
199
- expect(result.platform.androidValid).toBe(false);
200
- expect(result.isValid).toBe(false);
201
- });
202
-
203
- it('personalization tokens in the EDITOR are also rejected (clears the tab flag)', () => {
204
- const result = validateMobilePush(bothPlatforms({
205
- 0: {
206
- 'message-title': 'hi', 'message-editor': 'go [eventAttr]', "base": true, "tabKey": 'k0',
207
- },
208
- }), { ...baseOptions, restrictPersonalization: true });
209
- expect(result.errorData[0]['message-editor']).toBe(true);
210
- expect(result.platform.androidValid).toBe(false);
211
- });
212
-
213
- it('whitespace-only editor/title trim to empty and fail (both clear the tab flag)', () => {
214
- const result = validateMobilePush(bothPlatforms({
215
- 0: {
216
- 'message-title': ' ', 'message-editor': ' ', "base": true, "tabKey": 'k0',
217
- },
218
- }), baseOptions);
219
- expect(result.errorData[0]['message-editor']).toBe(true);
220
- expect(result.errorData[0]['message-title']).toBe(true);
221
- expect(result.platform.androidValid).toBe(false);
222
- expect(result.isValid).toBe(false);
223
- });
224
-
225
- it('defaults: no arguments at all => valid (the tab-0 gate short-circuits everything)', () => {
226
- const result = validateMobilePush();
227
- expect(result.isValid).toBe(true);
228
- expect(result.errorData).toEqual({});
229
- });
230
-
231
- it('seedTabErrors skips tabKey/base/selectedLanguages from the tab-0 key seed', () => {
232
- const result = validateMobilePush(bothPlatforms({
233
- 0: {
234
- 'message-title': 'hello', 'message-editor': 'world', 'selectedLanguages': ['en'], "base": true, "tabKey": 'k0',
235
- },
236
- }), baseOptions);
237
- expect(result.errorData[1].tabKey).toBeUndefined();
238
- expect(result.errorData[1].selectedLanguages).toBeUndefined();
239
- expect(result.errorData[1].base).toBeUndefined();
240
- });
241
-
242
- it('embedded with BOTH platforms empty skips nothing (isOnlyAndroid === isOnlyIos)', () => {
243
- const result = validateMobilePush({
244
- 'template-name': 'p',
245
- '0': { base: true, tabKey: 'k0' },
246
- '1': { tabKey: 'k1' },
247
- }, { ...baseOptions, isEmbedded: true });
248
- // both tabs validated: both flagged for the missing editor/title
249
- expect(result.errorData[0]['message-editor']).toBe(true);
250
- expect(result.errorData[1]['message-editor2']).toBe(true);
251
- });
252
-
253
- it('brace mismatch in the TITLE writes the string error value and keeps the tab flag (Classic asymmetry)', () => {
254
- const result = validateMobilePush(bothPlatforms({
255
- 0: {
256
- 'message-title': 'hi {{name', 'message-editor': 'body', "base": true, "tabKey": 'k0',
257
- },
258
- }), baseOptions);
259
- expect(result.errorData[0]['message-title']).toBe(ERROR_VALUE.BRACKET);
260
- expect(result.errorData[0]['bracket-error']).toBe(ERROR_VALUE.BRACKET);
261
- expect(result.platform.androidValid).toBe(true); // NOT cleared, like the editor
262
- expect(result.isValid).toBe(false);
263
- });
264
- });
265
-
266
- describe('isValidExternalLink', () => {
267
- const tab = { present: 'https://a.co/x', empty: '', invalid: 'not a url' };
268
- it('absent key => valid; present-but-empty or invalid => invalid; valid url => valid', () => {
269
- expect(isValidExternalLink(tab, 'missing-key')).toBe(true);
270
- expect(isValidExternalLink(tab, 'empty')).toBe(false);
271
- expect(isValidExternalLink(tab, 'invalid')).toBe(false);
272
- expect(isValidExternalLink(tab, 'present')).toBe(true);
273
- });
274
- });