@capillarytech/creatives-library 9.0.61-alpha.3 → 9.0.62
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/constants/unified.js +8 -2
- package/package.json +1 -1
- package/services/api.js +5 -0
- package/services/tests/api.test.js +44 -0
- package/utils/common.js +8 -0
- package/utils/downloadFile.js +57 -0
- package/utils/tests/downloadFile.test.js +219 -0
- package/v2Components/FormBuilder/Classic.js +43 -9
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +324 -69
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +10 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +75 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +74 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +28 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +21 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +45 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +162 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +37 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +70 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +274 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +251 -0
- package/v2Components/FormBuilder/Functional/channels/registry.js +2 -0
- package/v2Components/FormBuilder/Functional/constants.js +46 -8
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +175 -28
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +75 -6
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +10 -20
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +9 -1
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +20 -3
- package/v2Components/FormBuilder/Functional/layout/Section.js +6 -1
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +85 -0
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +451 -0
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +9 -2
- package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +135 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +430 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +320 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +148 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +306 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +152 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +200 -0
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +382 -0
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +17 -0
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +112 -0
- package/v2Components/FormBuilder/_formBuilder.scss +11 -0
- package/v2Components/FormBuilder/index.js +27 -17
- package/v2Components/FormBuilder/tests/entryGate.test.js +90 -7
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +459 -0
- package/v2Containers/BeeEditor/index.js +8 -0
- package/v2Containers/Email/index.js +118 -16
- package/v2Containers/Email/initialSchema.js +25 -3
- package/v2Containers/Email/messages.js +16 -0
- package/v2Containers/Email/tests/index.test.js +597 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +55 -1
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +141 -0
- package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +11 -0
- package/v2Containers/Templates/index.js +53 -0
- package/v2Containers/Templates/messages.js +8 -0
- package/v2Containers/Templates/tests/index.test.js +170 -0
- package/v2Containers/Viber/constants.js +0 -3
- package/v2Containers/Viber/sagas.js +3 -1
- package/v2Containers/Viber/tests/saga.test.js +21 -0
|
@@ -0,0 +1,430 @@
|
|
|
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
|
+
});
|