@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,430 +0,0 @@
1
- /**
2
- * MOBILEPUSH cross-flow parity — the legacy (Classic) and new (Functional)
3
- * FormBuilders are rendered through the SAME edit-hydrated save flows and we
4
- * assert Classic ≡ Functional ≡ the documented `expected`.
5
- *
6
- * The flows hydrate both platform tabs via the formData prop (like the Edit
7
- * container / a fresh host push), then raise startValidation — the container's
8
- * "Done/Save" contract. Compared per case: validity, the error shapes the
9
- * containers pattern on (string vs boolean values included), the single-platform
10
- * modal prompt, the submitted payload shape, and the footer's tab argument.
11
- */
12
-
13
- import React from 'react';
14
- import '@testing-library/jest-dom';
15
- import _ from 'lodash';
16
- import { Router } from 'react-router-dom';
17
- import { render, act } from '../../../../utils/test-utils';
18
- import history from '../../../../utils/history';
19
- import { response as mpushSchemaResponse } from '../../../../v2Containers/MobilePush/initialSchema';
20
-
21
- import ClassicFormBuilder from '../../Classic';
22
- import FunctionalFormBuilder from '../index';
23
-
24
- // TagList (rendered for the tag-list fields) is auth-guarded; neutralize the route guard.
25
- jest.mock('redux-auth-wrapper/history4/redirect', () => ({
26
- connectedRouterRedirect: jest.fn(() => (Component) => Component),
27
- }));
28
-
29
- // Stub the impure unsubscribe-url call (plain fn so resetMocks doesn't wipe it).
30
- jest.mock('../../../../services/api', () => ({
31
- ...jest.requireActual('../../../../services/api'),
32
- getUnsubscribeUrl: () => Promise.resolve({ response: { response: '' } }),
33
- }));
34
-
35
- // Make the MPUSH liquid pipeline resolve synchronously and identically for both
36
- // flows (Classic's validateFormBuilderMPush and the adapter's runSubmitPipeline
37
- // both import validateMobilePushContent from commonUtils). It reproduces the
38
- // util's observable contract: an initial clearing onError, then onSuccess with a
39
- // single tab's JSON — which BOTH implementations must ignore in favor of the
40
- // full formData.
41
- jest.mock('../../../../utils/commonUtils', () => ({
42
- ...jest.requireActual('../../../../utils/commonUtils'),
43
- validateMobilePushContent: (formData, options = {}) => {
44
- if (typeof options.onError === 'function') options.onError({ standardErrors: [], liquidErrors: [] });
45
- if (typeof options.onSuccess === 'function') options.onSuccess(JSON.stringify(formData[0] || {}), 'android');
46
- return true;
47
- },
48
- }));
49
-
50
- const definitionFor = (variant) => _.cloneDeep(
51
- mpushSchemaResponse.metaEntities[0].definition[variant],
52
- );
53
-
54
- // Production-faithful dynamic-CTA splice: when a template carries CTA fields, the
55
- // containers have ALSO spliced the matching schema rows (getSchemaForFormData /
56
- // commonMethods). Hydrating the formData without the schema rows is a state no
57
- // container produces — Classic's mount re-init drops schema-unknown tab fields.
58
- const spliceCtaRows = (schema) => {
59
- const inputFields = schema.containers[0].panes[0]
60
- .sections[0].childSections[0].childSections[0].inputFields;
61
- inputFields.push(
62
- {
63
- identifier: 'cta-deeplink-select',
64
- cols: [{
65
- id: 'cta-deeplink-select', type: 'select', options: [], width: 12, supportedEvents: ['onSelect'],
66
- }],
67
- },
68
- {
69
- cols: [{
70
- id: 'custom-value-userId-cta-deeplink-select', type: 'input', width: 12, supportedEvents: [],
71
- }],
72
- },
73
- );
74
- return schema;
75
- };
76
-
77
- const buildFormData = ({
78
- android = {}, ios = {}, name = 'My Push', omitIos = false,
79
- }) => {
80
- const tab0 = { ...android, base: true, tabKey: 'tab-android' };
81
- const tab1 = omitIos ? { tabKey: 'tab-ios' } : { ...ios, tabKey: 'tab-ios' };
82
- return {
83
- 'template-name': name, 0: tab0, 1: tab1, base: tab0,
84
- };
85
- };
86
-
87
- const buildProps = (flowCase, mode) => {
88
- const onSubmit = jest.fn();
89
- const onFormValidityChange = jest.fn();
90
- const setModalContent = jest.fn();
91
- const showLiquidErrorInFooter = jest.fn();
92
- const schema = definitionFor(flowCase.variant || 'textSchema');
93
- return {
94
- schema: flowCase.mutateSchema ? flowCase.mutateSchema(schema) : schema,
95
- location: {
96
- pathname: '/mobilepush/create/text',
97
- query: { type: flowCase.embedded ? 'embedded' : false, module: 'default' },
98
- },
99
- usingTabContainer: true,
100
- isFullMode: mode === 'full',
101
- isEdit: true,
102
- currentTab: 1,
103
- tabCount: 2,
104
- tabKey: '',
105
- formData: buildFormData(flowCase),
106
- startValidation: false,
107
- checkValidation: false,
108
- tags: [],
109
- injectedTags: {},
110
- actions: { getLiquidTags: jest.fn() },
111
- onChange: jest.fn(),
112
- onSubmit,
113
- onFormValidityChange,
114
- stopValidation: jest.fn(),
115
- showLiquidErrorInFooter,
116
- setModalContent,
117
- handleCancelModal: jest.fn(),
118
- parent: {},
119
- _onSubmit: onSubmit,
120
- _onFormValidityChange: onFormValidityChange,
121
- _setModalContent: setModalContent,
122
- _showLiquidErrorInFooter: showLiquidErrorInFooter,
123
- };
124
- };
125
-
126
- const runFlow = (FlowComponent, flowCase, mode) => {
127
- const props = buildProps(flowCase, mode);
128
- const ui = (p) => (
129
- <Router history={history}>
130
- <FlowComponent {...p} />
131
- </Router>
132
- );
133
- const utils = render(ui(props));
134
- act(() => {
135
- utils.rerender(ui({ ...props, startValidation: true }));
136
- });
137
- utils.unmount();
138
-
139
- const vCalls = props._onFormValidityChange.mock.calls;
140
- const lastValidity = vCalls[vCalls.length - 1] || [];
141
- const errorData = lastValidity[1] || {};
142
- const submitted = props._onSubmit.mock.calls[0] ? props._onSubmit.mock.calls[0][0] : null;
143
- const modalCalls = props._setModalContent.mock.calls;
144
- const footerCalls = props._showLiquidErrorInFooter.mock.calls;
145
- const aErr = errorData[0] || {};
146
- const iErr = errorData[1] || {};
147
- return {
148
- isValid: Boolean(lastValidity[0]),
149
- nameError: Boolean(errorData['template-name']),
150
- aEditorError: aErr['message-editor'] === undefined ? false : aErr['message-editor'],
151
- aTitleError: aErr['message-title'] === undefined ? false : aErr['message-title'],
152
- aBracketError: aErr['bracket-error'] || false,
153
- iEditorError: iErr['message-editor2'] === undefined ? false : iErr['message-editor2'],
154
- imageError: Boolean(aErr.image),
155
- ctaSelectError: Boolean(aErr['cta-deeplink-select']),
156
- customValueError: Boolean(aErr['custom-value-userId-cta-deeplink-select']),
157
- modalPrompt: modalCalls.length ? modalCalls[modalCalls.length - 1][0] : null,
158
- submits: Boolean(submitted),
159
- submittedName: submitted ? submitted['template-name'] : null,
160
- submittedAEditor: submitted ? submitted[0]['message-editor'] : null,
161
- submittedIEditor: submitted && submitted[1] ? submitted[1]['message-editor2'] : null,
162
- footerTabArg: footerCalls.length ? footerCalls[footerCalls.length - 1][1] : null,
163
- };
164
- };
165
-
166
- const PARITY_CASES = [
167
- {
168
- title: 'both platforms valid',
169
- modes: ['full', 'library'],
170
- android: { 'message-title': 'A title', 'message-editor': 'A body' },
171
- ios: { 'message-title2': 'I title', 'message-editor2': 'I body' },
172
- expected: {
173
- submits: true, submittedName: 'My Push', submittedAEditor: 'A body', submittedIEditor: 'I body', modalPrompt: null,
174
- },
175
- // GOLDEN QUIRK: in library mode the liquid pipeline's initial CLEARING
176
- // onError fires onFormValidityChange(false) before onSuccess submits — so the
177
- // LAST validity emission is false even though the save goes through. Full
178
- // mode has no pipeline and stays true.
179
- expectedByMode: {
180
- full: { isValid: true },
181
- library: { isValid: false },
182
- },
183
- },
184
- {
185
- title: 'android only, saving from the Android tab => single-platform prompt, nothing else',
186
- modes: ['full', 'library'],
187
- android: { 'message-title': 'A title', 'message-editor': 'A body' },
188
- omitIos: true,
189
- expected: { modalPrompt: 'android', submits: false },
190
- },
191
- {
192
- title: 'brace mismatch in the Android editor (both platforms present)',
193
- modes: ['full', 'library'],
194
- android: { 'message-title': 'A title', 'message-editor': 'abc {{full_name' },
195
- ios: { 'message-title2': 'I title', 'message-editor2': 'I body' },
196
- expected: {
197
- isValid: false,
198
- submits: false,
199
- aEditorError: 'tagBracketCountMismatchError',
200
- aBracketError: 'tagBracketCountMismatchError',
201
- modalPrompt: null,
202
- },
203
- },
204
- {
205
- title: 'missing Android title clears platform validity; missing template name (full) flags the root',
206
- modes: ['full'],
207
- name: '',
208
- android: { 'message-editor': 'A body' },
209
- ios: { 'message-title2': 'I title', 'message-editor2': 'I body' },
210
- expected: {
211
- isValid: false, nameError: true, aTitleError: true, submits: false, modalPrompt: null,
212
- },
213
- },
214
- {
215
- title: 'embedded android-only: template name not required, closed iOS tab skipped — but the single-platform prompt STILL fires',
216
- modes: ['full'],
217
- embedded: true,
218
- name: '',
219
- android: { 'message-title': 'A title', 'message-editor': 'A body' },
220
- omitIos: true,
221
- // GOLDEN QUIRK: the embedded validation skip does NOT suppress the
222
- // single-platform modal (Classic.js:1171-1204 runs regardless of mode), so an
223
- // embedded android-only save prompts instead of submitting directly. The last
224
- // validity emission is the mount-time one (valid — the empty tab is skipped).
225
- expected: {
226
- isValid: true, nameError: false, submits: false, modalPrompt: 'android',
227
- },
228
- },
229
- {
230
- title: 'empty CTA deeplink select + empty custom-value key block the save',
231
- modes: ['full'],
232
- mutateSchema: spliceCtaRows,
233
- android: {
234
- 'message-title': 'A title',
235
- 'message-editor': 'A body',
236
- 'cta-deeplink-select': '',
237
- 'custom-value-userId-cta-deeplink-select': '',
238
- },
239
- ios: { 'message-title2': 'I title', 'message-editor2': 'I body' },
240
- expected: {
241
- isValid: false, ctaSelectError: true, customValueError: true, submits: false, modalPrompt: null,
242
- },
243
- },
244
- {
245
- title: 'imageSchema: empty image on a filled template blocks the save',
246
- modes: ['full'],
247
- variant: 'imageSchema',
248
- android: { 'message-title': 'A title', 'message-editor': 'A body', image: '' },
249
- ios: { 'message-title2': 'I title', 'message-editor2': 'I body', image: '' },
250
- expected: {
251
- isValid: false, imageError: true, submits: false, modalPrompt: null,
252
- },
253
- },
254
- ];
255
-
256
- describe('MPUSH cross-flow parity (Classic ≡ Functional ≡ expected)', () => {
257
- PARITY_CASES.forEach((flowCase) => {
258
- flowCase.modes.forEach((mode) => {
259
- it(`${flowCase.title} [${mode} mode]`, () => {
260
- const classic = runFlow(ClassicFormBuilder, flowCase, mode);
261
- const functional = runFlow(FunctionalFormBuilder, flowCase, mode);
262
-
263
- // Functional must match Classic on every compared field…
264
- expect(functional).toEqual(classic);
265
- // …and both must match the documented expectation.
266
- const expected = { ...flowCase.expected, ...(flowCase.expectedByMode || {})[mode] };
267
- Object.keys(expected).forEach((key) => {
268
- expect({ key, value: classic[key] }).toEqual({ key, value: expected[key] });
269
- });
270
- });
271
- });
272
- });
273
- });
274
-
275
- // ---------------------------------------------------------------------------
276
- // Background validity maintenance — the Create/Done gate.
277
- //
278
- // Classic's validateForm tail ALWAYS emits onFormValidityChange (Classic.js:1361)
279
- // and validateForm runs on every deep-unequal parent formData push
280
- // (Classic.js:516-537, MOBILEPUSH unconditionally) — with checkValidation still
281
- // false, so nothing displays. The MobilePush containers gate their save flows on
282
- // the validity maintained this way (Create/index.js:101 returns early on
283
- // !state.isFormValid in library mode), so a missing emission makes the Create
284
- // button do nothing. This block reproduces the reported user flow: fill Android,
285
- // copy the content to iOS (the container's copyAndroidContent clone-and-push),
286
- // then Create.
287
- // ---------------------------------------------------------------------------
288
- describe('MPUSH cross-flow parity — background validity on parent pushes (Create gate)', () => {
289
- const androidOnly = {
290
- name: 'My Push',
291
- android: { 'message-title': 'A title', 'message-editor': 'A body' },
292
- ios: {},
293
- };
294
- const bothFilled = {
295
- name: 'My Push',
296
- android: { 'message-title': 'A title', 'message-editor': 'A body' },
297
- ios: { 'message-title2': 'A title', 'message-editor2': 'A body' },
298
- };
299
-
300
- const runCopyPushFlow = (FlowComponent, { save }) => {
301
- const props = buildProps(androidOnly, 'full');
302
- const ui = (p) => (
303
- <Router history={history}>
304
- <FlowComponent {...p} />
305
- </Router>
306
- );
307
- const utils = render(ui(props));
308
- const emissionsBeforePush = props._onFormValidityChange.mock.calls.length;
309
- // copyAndroidContent: the container clones its formData, writes the iOS
310
- // values, and setStates -> a deep-unequal formData prop push. No save yet.
311
- act(() => {
312
- utils.rerender(ui({ ...props, formData: buildFormData(bothFilled) }));
313
- });
314
- const vCalls = props._onFormValidityChange.mock.calls;
315
- const result = {
316
- emittedOnPush: vCalls.length > emissionsBeforePush,
317
- validityAfterPush: vCalls.length ? Boolean(vCalls[vCalls.length - 1][0]) : null,
318
- submits: false,
319
- submitted: null,
320
- };
321
- if (save) {
322
- act(() => {
323
- utils.rerender(ui({ ...props, formData: buildFormData(bothFilled), startValidation: true }));
324
- });
325
- result.submits = props._onSubmit.mock.calls.length > 0;
326
- result.submitted = props._onSubmit.mock.calls[0] ? props._onSubmit.mock.calls[0][0] : null;
327
- }
328
- utils.unmount();
329
- return result;
330
- };
331
-
332
- it('a deep-unequal parent push (copy-content) re-validates and emits validity(true) BEFORE any save', () => {
333
- const classic = runCopyPushFlow(ClassicFormBuilder, { save: false });
334
- const functional = runCopyPushFlow(FunctionalFormBuilder, { save: false });
335
- expect(classic.emittedOnPush).toBe(true);
336
- expect(functional.emittedOnPush).toBe(true);
337
- expect(classic.validityAfterPush).toBe(true);
338
- expect(functional.validityAfterPush).toBe(true);
339
- });
340
-
341
- it('the reported user flow — fill Android, copy to iOS, Create — submits in both implementations', () => {
342
- const classic = runCopyPushFlow(ClassicFormBuilder, { save: true });
343
- const functional = runCopyPushFlow(FunctionalFormBuilder, { save: true });
344
- expect(classic.submits).toBe(true);
345
- expect(functional.submits).toBe(true);
346
- expect(classic.submitted[1]['message-editor2']).toBe('A body');
347
- expect(functional.submitted[1]['message-editor2']).toBe('A body');
348
- expect(functional.submitted['template-name']).toBe(classic.submitted['template-name']);
349
- });
350
- });
351
-
352
- // ---------------------------------------------------------------------------
353
- // Edit hydration — asynchronous template arrival.
354
- //
355
- // The real Edit container mounts the FormBuilder BEFORE the template details
356
- // arrive (empty formData), then pushes the setEditState-shaped formData:
357
- // base alias, base:true on tab 0, and NO tabKeys (Edit/index.js:983-986 — the
358
- // tabKey lines are commented out). Classic survives because resetTabKeys
359
- // assigns fresh keys on the push (Classic.js:453-461); the engine's HYDRATE
360
- // must guarantee the same (CapTab panes are keyed by tabKey — a key-less
361
- // hydrate would leave no pane active and the fields would simply not render).
362
- // ---------------------------------------------------------------------------
363
- describe('MPUSH cross-flow parity — edit hydration (async template arrival)', () => {
364
- const editFormData = () => {
365
- const android = {
366
- 'message-title': 'Saved A title',
367
- 'message-editor': 'Saved A body',
368
- base: true,
369
- };
370
- const ios = {
371
- 'message-title2': 'Saved I title',
372
- 'message-editor2': 'Saved I body',
373
- };
374
- return {
375
- 'template-name': 'Saved push', 0: android, 1: ios, base: android,
376
- };
377
- };
378
-
379
- const runEditSequence = (FlowComponent, { save }) => {
380
- const props = buildProps({ android: {}, ios: {}, name: '' }, 'full');
381
- props.formData = {}; // mount BEFORE the template details arrive
382
- const ui = (p) => (
383
- <Router history={history}>
384
- <FlowComponent {...p} />
385
- </Router>
386
- );
387
- const utils = render(ui(props));
388
- act(() => {
389
- utils.rerender(ui({ ...props, formData: editFormData() }));
390
- });
391
- const editor = document.getElementById('message-editor');
392
- const title = document.getElementById('message-title');
393
- const result = {
394
- titleValue: title ? title.value : '(no #message-title in DOM)',
395
- editorValue: editor ? editor.value : '(no #message-editor in DOM)',
396
- submitted: null,
397
- };
398
- if (save) {
399
- act(() => {
400
- utils.rerender(ui({ ...props, formData: editFormData(), startValidation: true }));
401
- });
402
- result.submitted = props._onSubmit.mock.calls[0] ? props._onSubmit.mock.calls[0][0] : null;
403
- }
404
- utils.unmount();
405
- return result;
406
- };
407
-
408
- it('the pushed template populates the visible fields in both implementations', () => {
409
- const classic = runEditSequence(ClassicFormBuilder, { save: false });
410
- const functional = runEditSequence(FunctionalFormBuilder, { save: false });
411
- expect(classic.titleValue).toBe('Saved A title');
412
- expect(functional.titleValue).toBe('Saved A title');
413
- expect(classic.editorValue).toBe('Saved A body');
414
- expect(functional.editorValue).toBe('Saved A body');
415
- });
416
-
417
- it('edit-then-save round-trips the hydrated values in both implementations', () => {
418
- const classic = runEditSequence(ClassicFormBuilder, { save: true });
419
- const functional = runEditSequence(FunctionalFormBuilder, { save: true });
420
- expect(classic.submitted).toBeTruthy();
421
- expect(functional.submitted).toBeTruthy();
422
- ['template-name'].forEach((key) => {
423
- expect(functional.submitted[key]).toEqual(classic.submitted[key]);
424
- });
425
- expect(functional.submitted[0]['message-editor']).toBe('Saved A body');
426
- expect(classic.submitted[0]['message-editor']).toBe('Saved A body');
427
- expect(functional.submitted[1]['message-editor2']).toBe('Saved I body');
428
- expect(classic.submitted[1]['message-editor2']).toBe('Saved I body');
429
- });
430
- });