@capillarytech/creatives-library 9.0.59 → 9.0.60

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 (44) hide show
  1. package/constants/unified.js +2 -3
  2. package/package.json +1 -1
  3. package/utils/common.js +0 -8
  4. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +69 -324
  5. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
  6. package/v2Components/FormBuilder/Functional/constants.js +8 -46
  7. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
  8. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
  9. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
  10. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
  11. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
  12. package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
  13. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
  14. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
  15. package/v2Components/FormBuilder/_formBuilder.scss +0 -11
  16. package/v2Components/FormBuilder/index.js +13 -20
  17. package/v2Components/FormBuilder/tests/entryGate.test.js +7 -87
  18. package/v2Containers/CommunicationFlow/CommunicationFlow.js +12 -25
  19. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +9 -28
  20. package/v2Containers/CommunicationFlow/hooks/Tests/useFetchCommDefinition.test.js +118 -0
  21. package/v2Containers/CommunicationFlow/hooks/useFetchCommDefinition.js +49 -0
  22. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  23. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
  24. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  25. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  26. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
  27. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  28. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  29. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  30. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  31. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  32. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  33. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
  34. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
  35. package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
  36. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  37. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
  38. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
  39. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  40. package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
  41. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
  42. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
  43. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  44. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -1,28 +0,0 @@
1
- /**
2
- * MOBILEPUSH channel adapter — the isolation unit for MPUSH. Bundles config + the
3
- * pure validate + the identity payload builder + the platform-specific submit
4
- * pipeline + the single-platform modal semantics + the channel's renderer
5
- * registry. The shell looks this up via channels/registry by `schema.channel`;
6
- * it imports nothing from other channels.
7
- */
8
- import { MOBILE_PUSH } from '../../../../../v2Containers/CreativesContainer/constants';
9
- import config from './config';
10
- import validate from './validate';
11
- import buildSubmitPayload from './buildSubmitPayload';
12
- import runSubmitPipeline from './runSubmitPipeline';
13
- import modals from './modals';
14
- import getEditorErrorDescriptor from './getEditorErrorDescriptor';
15
- import { createMpushRegistry } from '../../renderers/mpushRenderers';
16
-
17
- const mpushAdapter = {
18
- channel: MOBILE_PUSH,
19
- config,
20
- validate,
21
- buildSubmitPayload,
22
- runSubmitPipeline,
23
- modals,
24
- getEditorErrorDescriptor,
25
- createRegistry: createMpushRegistry,
26
- };
27
-
28
- export default mpushAdapter;
@@ -1,21 +0,0 @@
1
- /**
2
- * Single-platform save confirmation (Classic handleOk, Classic.js:2674-2709).
3
- * The OK button's id ('android'|'ios') is the dispatch key: confirming saves that
4
- * platform alone, but only if its validity flag is true — a falsy flag silently
5
- * closes the modal (Classic parity). Modal CONTENT stays container-owned.
6
- */
7
-
8
- import { ANDROID, IOS } from '../../../../../constants/unified';
9
-
10
- const mpushModals = {
11
- handleOk: (id, { platform, submitSingleTab } = {}) => {
12
- if (!platform || typeof submitSingleTab !== 'function') return;
13
- if (id === ANDROID && platform.androidValid) {
14
- submitSingleTab(ANDROID);
15
- } else if (id === IOS && platform.iosValid) {
16
- submitSingleTab(IOS);
17
- }
18
- },
19
- };
20
-
21
- export default mpushModals;
@@ -1,45 +0,0 @@
1
- /**
2
- * MOBILEPUSH submit pipeline (Classic.js:1397-1405, 1473-1522). Full mode submits
3
- * the raw legacy formData (no liquid, no getChannelData); library mode runs
4
- * validateMobilePushContent over [androidTab, iosTab]. Classic quirks preserved:
5
- * the util's nested per-platform error objects go to the footer AS-IS, its initial
6
- * empty "clear" call still triggers stopValidation + onFormValidityChange(false),
7
- * its onSuccess contentToSubmit is IGNORED (the full formData is submitted), and
8
- * singleTab from the single-platform modal skips the other platform (1519).
9
- */
10
-
11
- import { validateMobilePushContent } from '../../../../../utils/commonUtils';
12
-
13
- export default function runSubmitPipeline(legacyFormData, ctx = {}, callbacks = {}) {
14
- const {
15
- isFullMode, singleTab, currentTab, getLiquidTags, formatMessage, messages,
16
- } = ctx;
17
- const {
18
- onSubmit, pushFooter, stopValidation, onFormValidityChange,
19
- } = callbacks;
20
-
21
- const runLiquid = !isFullMode && typeof getLiquidTags === 'function';
22
- if (!runLiquid) {
23
- if (onSubmit) onSubmit(legacyFormData);
24
- return;
25
- }
26
-
27
- validateMobilePushContent(legacyFormData, {
28
- currentTab,
29
- onError: ({ standardErrors, liquidErrors }) => {
30
- if (pushFooter) pushFooter({ STANDARD_ERROR_MSG: standardErrors, LIQUID_ERROR_MSG: liquidErrors });
31
- if (stopValidation) stopValidation();
32
- // Block save: tell the parent the form is invalid (Classic passes the last
33
- // sync errorData — the shell's callback supplies it).
34
- if (onFormValidityChange) onFormValidityChange(false);
35
- },
36
- onSuccess: () => {
37
- if (pushFooter) pushFooter({ STANDARD_ERROR_MSG: [], LIQUID_ERROR_MSG: [] });
38
- if (onSubmit) onSubmit(legacyFormData);
39
- },
40
- getLiquidTags,
41
- formatMessage,
42
- messages,
43
- singleTab: singleTab ? `${singleTab}`.toUpperCase() : undefined,
44
- });
45
- }
@@ -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
- });