@capillarytech/creatives-library 9.0.61 → 9.0.63-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/constants/unified.js +8 -2
  2. package/package.json +1 -1
  3. package/services/api.js +5 -0
  4. package/services/tests/api.test.js +44 -0
  5. package/utils/common.js +8 -0
  6. package/utils/downloadFile.js +57 -0
  7. package/utils/tests/downloadFile.test.js +219 -0
  8. package/v2Components/FormBuilder/Classic.js +43 -9
  9. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +324 -69
  10. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +10 -0
  11. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +75 -0
  12. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +74 -0
  13. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +28 -0
  14. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +21 -0
  15. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +45 -0
  16. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +162 -0
  17. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +37 -0
  18. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +70 -0
  19. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +274 -0
  20. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +251 -0
  21. package/v2Components/FormBuilder/Functional/channels/registry.js +2 -0
  22. package/v2Components/FormBuilder/Functional/constants.js +46 -8
  23. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +175 -28
  24. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +75 -6
  25. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +10 -20
  26. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +9 -1
  27. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +20 -3
  28. package/v2Components/FormBuilder/Functional/layout/Section.js +6 -1
  29. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +85 -0
  30. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +451 -0
  31. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +9 -2
  32. package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +135 -0
  33. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +430 -0
  34. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +320 -0
  35. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +148 -0
  36. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +306 -0
  37. package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +152 -0
  38. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +200 -0
  39. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +382 -0
  40. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +17 -0
  41. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +112 -0
  42. package/v2Components/FormBuilder/_formBuilder.scss +11 -0
  43. package/v2Components/FormBuilder/index.js +27 -17
  44. package/v2Components/FormBuilder/tests/entryGate.test.js +90 -7
  45. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +459 -0
  46. package/v2Containers/BeeEditor/index.js +8 -0
  47. package/v2Containers/Email/index.js +118 -16
  48. package/v2Containers/Email/initialSchema.js +25 -3
  49. package/v2Containers/Email/messages.js +16 -0
  50. package/v2Containers/Email/tests/index.test.js +597 -0
  51. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +55 -1
  52. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +141 -0
  53. package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +11 -0
  54. package/v2Containers/Templates/index.js +53 -0
  55. package/v2Containers/Templates/messages.js +8 -0
  56. package/v2Containers/Templates/tests/index.test.js +170 -0
@@ -0,0 +1,320 @@
1
+ /**
2
+ * MPUSH CTA flows parity — the REAL (unconnected) MobilePush Create container
3
+ * over the REAL gated FormBuilder, driving the CTA surface end-to-end through
4
+ * the DOM the way a user does:
5
+ *
6
+ * fill both platforms -> "Add action link to content" (add-pri-cta)
7
+ * -> deeplink option with/without params | External Link radio switch
8
+ * -> press Create (isGetFormData transition, full mode)
9
+ *
10
+ * Pins manual-QA fix #5: switching the link-type radio Deeplink <-> External
11
+ * Link REPLACES the `cta-deeplink-select` schema row with `cta-deeplink-text`.
12
+ * Classic's initialiseForm PRUNES formData keys that left the schema walk
13
+ * (Classic.js:1889-1897); before reconcileFormState ported that prune, the
14
+ * orphaned seeded-'' `cta-deeplink-select` failed validateMobilePush's empty
15
+ * rule and the container toasted "Validation error / Validation failed for
16
+ * android" on a form Classic saves.
17
+ *
18
+ * Outcome contract per flow: NO validation stop (onValidationFail), and the
19
+ * save proceeds — createTemplate fires, or the single-platform confirm modal
20
+ * opens (a legitimate Classic outcome when the iOS tab stayed empty).
21
+ */
22
+ import React from 'react';
23
+ import '@testing-library/jest-dom';
24
+ import _ from 'lodash';
25
+ import { notification } from 'antd';
26
+ import { Router } from 'react-router-dom';
27
+ import { render, act, fireEvent } from '../../../../utils/test-utils';
28
+ import history from '../../../../utils/history';
29
+ import { response as mpushSchemaResponse } from '../../../../v2Containers/MobilePush/initialSchema';
30
+ import { Create } from '../../../../v2Containers/MobilePush/Create';
31
+
32
+ jest.mock('redux-auth-wrapper/history4/redirect', () => ({
33
+ connectedRouterRedirect: jest.fn(() => (Component) => Component),
34
+ }));
35
+ jest.mock('../../../../services/api', () => ({
36
+ ...jest.requireActual('../../../../services/api'),
37
+ getUnsubscribeUrl: () => Promise.resolve({ response: { response: '' } }),
38
+ }));
39
+
40
+ // The gate is purely org-flag controlled — drive the flow via the flag reader.
41
+ let mockMpushFlag = false;
42
+ jest.mock('../../../../utils/common', () => ({
43
+ ...jest.requireActual('../../../../utils/common'),
44
+ hasNewFormBuilderEnabledForMpush: () => mockMpushFlag,
45
+ }));
46
+
47
+ const fullDefinition = () => _.cloneDeep(mpushSchemaResponse.metaEntities[0].definition);
48
+
49
+ const intl = {
50
+ formatMessage: (descriptor) => (descriptor && (descriptor.defaultMessage || descriptor.id)) || '',
51
+ locale: 'en',
52
+ };
53
+
54
+ const DEEPLINKS = JSON.stringify([
55
+ { name: 'Home Page', link: 'app://home', keys: ['user_id'] },
56
+ { name: 'Offers Page', link: 'app://offers', keys: [] },
57
+ ]);
58
+
59
+ const selectedAccount = {
60
+ id: 'acc-1',
61
+ name: 'Acc',
62
+ sourceTypeName: 'SOME_SDK',
63
+ sourceAccountIdentifier: 'lic-1',
64
+ configs: { android: '1', ios: '1', deeplink: DEEPLINKS },
65
+ };
66
+
67
+ const buildProps = (overrides = {}) => ({
68
+ intl,
69
+ params: { mode: 'text' },
70
+ route: { name: 'create' },
71
+ location: { pathname: '/mobilepush/create/text', query: { module: 'default' } },
72
+ router: { push: jest.fn() },
73
+ isFullMode: true,
74
+ isGetFormData: false,
75
+ isLoadingMetaEntities: false,
76
+ metaEntities: {},
77
+ Create: { createTemplateInProgress: false },
78
+ Edit: {},
79
+ Templates: { selectedWeChatAccount: selectedAccount },
80
+ // Any missing action becomes a jest.fn on demand (the container calls several).
81
+ actions: new Proxy({}, { get: (target, name) => {
82
+ if (!target[name]) target[name] = jest.fn(); // eslint-disable-line no-param-reassign
83
+ return target[name];
84
+ } }),
85
+ globalActions: {
86
+ fetchSchemaForEntity: jest.fn(),
87
+ addMessageToQueue: jest.fn(),
88
+ setInjectedTags: jest.fn(),
89
+ },
90
+ showTemplateName: jest.fn(),
91
+ getFormLibraryData: jest.fn(),
92
+ onValidationFail: jest.fn(),
93
+ showLiquidErrorInFooter: jest.fn(),
94
+ onPersonalizationTokensChange: jest.fn(),
95
+ injectedTags: {},
96
+ defaultData: { 'template-name': 'cta_test_1' },
97
+ ...overrides,
98
+ });
99
+
100
+ const setNativeValue = (el, value) => {
101
+ const proto = Object.getPrototypeOf(el);
102
+ const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
103
+ setter.call(el, value);
104
+ };
105
+
106
+ const typeInto = (id, value) => {
107
+ const el = document.getElementById(id);
108
+ if (!el) throw new Error(`no #${id} in DOM`);
109
+ setNativeValue(el, value);
110
+ fireEvent.change(el, { target: { value } });
111
+ fireEvent.blur(el);
112
+ };
113
+
114
+ const runCreateFlow = ({
115
+ optionLabel = 'Offers Page', fillCustomValue = false, useCopy = false, iosCta = false,
116
+ externalLink = false,
117
+ } = {}) => {
118
+ const props = buildProps();
119
+ let containerRef = null;
120
+ const ui = (p) => (
121
+ <Router history={history}>
122
+ <Create ref={(r) => { if (r) containerRef = r; }} {...p} />
123
+ </Router>
124
+ );
125
+ const utils = render(ui(props));
126
+ const meta = { layouts: [{ definition: fullDefinition() }], tags: { standard: [] } };
127
+ act(() => {
128
+ utils.rerender(ui({ ...props, metaEntities: meta }));
129
+ });
130
+
131
+ // 1. fill mandatory fields (template-name comes via defaultData in full mode).
132
+ act(() => { typeInto('message-title', 'Hello title'); });
133
+ act(() => { typeInto('message-editor', 'Hello body'); });
134
+ const tabHeaders = () => Array.from(document.querySelectorAll('.ant-tabs-tab'));
135
+ if (useCopy) {
136
+ // the user's habitual step: switch to iOS, "copy content from Android"
137
+ act(() => { fireEvent.click(tabHeaders()[1]); });
138
+ const copyLink = document.getElementById('copy-android-content2')
139
+ || document.getElementById('copy-android-content');
140
+ if (!copyLink) throw new Error('no copy-android-content link found');
141
+ act(() => { fireEvent.click(copyLink); });
142
+ act(() => { fireEvent.click(tabHeaders()[0]); });
143
+ } else {
144
+ act(() => { fireEvent.click(tabHeaders()[1]); }); // switch to iOS
145
+ act(() => { typeInto('message-title2', 'Hello ios title'); });
146
+ act(() => { typeInto('message-editor2', 'Hello ios body'); });
147
+ act(() => { fireEvent.click(tabHeaders()[0]); }); // back to Android
148
+ }
149
+
150
+ if (iosCta) {
151
+ // run the whole CTA flow on the iOS pane instead (suffixed ids)
152
+ act(() => { fireEvent.click(tabHeaders()[1]); });
153
+ }
154
+ const ctaCheckboxId = iosCta ? 'add-pri-cta-ios' : 'add-pri-cta';
155
+ const selectId = iosCta ? 'cta-deeplink2-select' : 'cta-deeplink-select';
156
+ const customValueId = iosCta
157
+ ? 'custom-value-user_id-cta-deeplink2-select'
158
+ : 'custom-value-user_id-cta-deeplink-select';
159
+
160
+ // 2. click "Add action link to content" — the checkbox carries no DOM id in
161
+ // either flow, so find it by label text inside the right platform pane.
162
+ const pane = document.querySelectorAll('.ant-tabs-tabpane')[iosCta ? 1 : 0] || document;
163
+ const label = Array.from(pane.querySelectorAll('label'))
164
+ .find((l) => /action link/i.test(l.textContent || ''));
165
+ const cb = (label && label.querySelector('input[type="checkbox"]'))
166
+ || pane.querySelector('input[type="checkbox"]');
167
+ if (!cb) throw new Error(`no ${ctaCheckboxId} checkbox found`);
168
+ act(() => { fireEvent.click(cb); });
169
+
170
+ // the deeplink dropdown must have been spliced in
171
+ let selectHost = document.getElementById(selectId);
172
+ const info = { selectRendered: Boolean(selectHost) };
173
+
174
+ if (externalLink) {
175
+ // switch the link-type radio to "External Link": the dropdown row must be
176
+ // REPLACED by a text input (container onLinkTypeChange splice)
177
+ const radios = Array.from(pane.querySelectorAll('input[type="radio"]'));
178
+ const external = radios.find((rd) => rd.value === 'External Link');
179
+ if (!external) throw new Error(`no External Link radio (found ${radios.map((rd) => rd.value)})`);
180
+ act(() => { fireEvent.click(external); });
181
+ selectHost = document.getElementById(selectId);
182
+ info.selectStillRendered = Boolean(selectHost);
183
+ const textId = iosCta ? 'cta-deeplink2-text' : 'cta-deeplink-text';
184
+ info.externalInputRendered = Boolean(document.getElementById(textId));
185
+ if (info.externalInputRendered) {
186
+ act(() => { typeInto(textId, 'https://example.com/offer'); });
187
+ }
188
+ }
189
+
190
+ // 3. open the CapSelect and choose the requested option
191
+ if (selectHost && !externalLink) {
192
+ const trigger = selectHost.querySelector('.ant-select-selector')
193
+ || selectHost.closest('.ant-select')?.querySelector('.ant-select-selector')
194
+ || selectHost;
195
+ act(() => { fireEvent.mouseDown(trigger); });
196
+ const options = Array.from(document.querySelectorAll('.ant-select-item-option'));
197
+ const wanted = options.find((o) => o.textContent.includes(optionLabel));
198
+ if (wanted) act(() => { fireEvent.click(wanted); });
199
+ info.pickedOption = Boolean(wanted);
200
+ }
201
+
202
+ // For a deeplink with params, the containers splice in a custom-value input.
203
+ const customInput = document.getElementById(customValueId);
204
+ info.customInputRendered = Boolean(customInput);
205
+ if (customInput && fillCustomValue) {
206
+ act(() => { typeInto(customValueId, '42'); });
207
+ }
208
+
209
+ // 4. press Create: isGetFormData transition in full mode -> startValidation
210
+ act(() => {
211
+ utils.rerender(ui({ ...props, metaEntities: meta, isGetFormData: true }));
212
+ });
213
+
214
+ const result = {
215
+ ...info,
216
+ createCalled: props.actions.createTemplate.mock.calls.length,
217
+ validationFailCalled: props.onValidationFail.mock.calls.length,
218
+ modalShown: containerRef ? containerRef.state.showModal : null,
219
+ errorTab0: containerRef
220
+ ? Object.entries(containerRef.state.errorData?.[0] || {}).filter(([, v]) => v === true).map(([k]) => k)
221
+ : null,
222
+ };
223
+ utils.unmount();
224
+ return result;
225
+ };
226
+
227
+ // Success = the save flow proceeds: either createTemplate fired, or the
228
+ // single-platform confirm modal opened. A validation STOP (onValidationFail)
229
+ // is the bug class this suite pins.
230
+ const expectSaveProceeds = (r) => {
231
+ expect(r.validationFailCalled).toBe(0);
232
+ expect(r.createCalled + (r.modalShown ? 1 : 0)).toBeGreaterThanOrEqual(1);
233
+ };
234
+
235
+ const expectDeeplinkPicked = (r) => {
236
+ expect(r.selectRendered).toBe(true);
237
+ expect(r.pickedOption).toBe(true);
238
+ };
239
+
240
+ describe.each([
241
+ ['Classic', 'false'],
242
+ ['Functional', 'true'],
243
+ ])('CTA flows parity — %s', (flow, flagValue) => {
244
+ beforeEach(() => {
245
+ mockMpushFlag = flagValue === 'true';
246
+ });
247
+ afterEach(() => {
248
+ mockMpushFlag = false;
249
+ });
250
+
251
+ it('deeplink option WITHOUT params: no extra input, save proceeds', () => {
252
+ const r = runCreateFlow({ optionLabel: 'Offers Page' });
253
+ expectDeeplinkPicked(r);
254
+ expect(r.customInputRendered).toBe(false);
255
+ expectSaveProceeds(r);
256
+ });
257
+
258
+ it('deeplink option WITH params: custom-value input renders, save proceeds', () => {
259
+ const r = runCreateFlow({ optionLabel: 'Home Page', fillCustomValue: true });
260
+ expectDeeplinkPicked(r);
261
+ expect(r.customInputRendered).toBe(true);
262
+ expectSaveProceeds(r);
263
+ });
264
+
265
+ it('copy-content flow + CTA: save proceeds', () => {
266
+ const r = runCreateFlow({ optionLabel: 'Offers Page', useCopy: true });
267
+ expectDeeplinkPicked(r);
268
+ expectSaveProceeds(r);
269
+ });
270
+
271
+ it('External Link radio: dropdown replaced by text input, orphaned select key does not fail validation', () => {
272
+ const r = runCreateFlow({ externalLink: true });
273
+ expect(r.selectRendered).toBe(true);
274
+ expect(r.selectStillRendered).toBe(false);
275
+ expect(r.externalInputRendered).toBe(true);
276
+ expectSaveProceeds(r);
277
+ });
278
+
279
+ // iOS-pane CTA runs only on the Functional flow: Classic's tab switching scans
280
+ // the real DOM (Classic.js:2526-2551), which a jsdom tab-header click does not
281
+ // drive, so its container-side splice targets the wrong pane in this harness
282
+ // (artifact, not a product behavior).
283
+ if (flow === 'Functional') {
284
+ it('iOS-pane CTA (suffixed ids): save proceeds', () => {
285
+ const r = runCreateFlow({ optionLabel: 'Offers Page', iosCta: true });
286
+ expectDeeplinkPicked(r);
287
+ expectSaveProceeds(r);
288
+ });
289
+ }
290
+
291
+ // Copy with EMPTY source fields must warn ("Cannot copy empty field values."):
292
+ // the container handler reads its own formData copy (Create/index.js:1456-1465),
293
+ // seeded by the builder's init onChange push (Classic.js:1906-1911 -> 740-748).
294
+ it('copy from Android with empty fields shows the cannot-copy warning', () => {
295
+ const warnSpy = jest.spyOn(notification, 'warning').mockImplementation(() => {});
296
+ try {
297
+ const props = buildProps();
298
+ const ui = (p) => (
299
+ <Router history={history}>
300
+ <Create {...p} />
301
+ </Router>
302
+ );
303
+ const utils = render(ui(props));
304
+ const meta = { layouts: [{ definition: fullDefinition() }], tags: { standard: [] } };
305
+ act(() => {
306
+ utils.rerender(ui({ ...props, metaEntities: meta }));
307
+ });
308
+ act(() => { fireEvent.click(document.querySelectorAll('.ant-tabs-tab')[1]); });
309
+ const copyLink = document.getElementById('copy-android-content2')
310
+ || document.getElementById('copy-android-content');
311
+ if (!copyLink) throw new Error('no copy-android-content link found');
312
+ act(() => { fireEvent.click(copyLink); });
313
+ expect(warnSpy).toHaveBeenCalledTimes(1);
314
+ expect(warnSpy.mock.calls[0][0].description).toBe('Cannot copy empty field values.');
315
+ utils.unmount();
316
+ } finally {
317
+ warnSpy.mockRestore();
318
+ }
319
+ });
320
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * MPUSH edit parity — the REAL (unconnected) MobilePush Edit container driving
3
+ * the REAL gated FormBuilder, with prop transitions exactly like production:
4
+ * mount (account selected, no metaEntities yet)
5
+ * -> metaEntities arrive (fullSchema + schema set, Edit CWRP 227-249)
6
+ * -> templateDetails arrive (handleEditSchemaOnPropsChange -> setEditState
7
+ * -> getSchemaForFormData -> injectEvents -> formData/tabCount setStates)
8
+ *
9
+ * This pins the bug the synthetic shell-level harnesses could NOT see: the
10
+ * container's internal setState cascade commits the shell BEFORE a prior
11
+ * dispatch has rendered, so a shell-side reconcile computed from stateRef was
12
+ * STALE and clobbered the just-hydrated template (fields rendered but empty).
13
+ * Fixed by running reconciliation inside the reducer (reconcileSchema action)
14
+ * and emitting background validity from the settled state.
15
+ */
16
+ import React from 'react';
17
+ import '@testing-library/jest-dom';
18
+ import _ from 'lodash';
19
+ import { Router } from 'react-router-dom';
20
+ import { render, act } from '../../../../utils/test-utils';
21
+ import history from '../../../../utils/history';
22
+ import { response as mpushSchemaResponse } from '../../../../v2Containers/MobilePush/initialSchema';
23
+ import { Edit } from '../../../../v2Containers/MobilePush/Edit';
24
+
25
+ jest.mock('redux-auth-wrapper/history4/redirect', () => ({
26
+ connectedRouterRedirect: jest.fn(() => (Component) => Component),
27
+ }));
28
+ jest.mock('../../../../services/api', () => ({
29
+ ...jest.requireActual('../../../../services/api'),
30
+ getUnsubscribeUrl: () => Promise.resolve({ response: { response: '' } }),
31
+ }));
32
+
33
+ // The gate is purely org-flag controlled — drive the flow via the flag reader.
34
+ let mockMpushFlag = false;
35
+ jest.mock('../../../../utils/common', () => ({
36
+ ...jest.requireActual('../../../../utils/common'),
37
+ hasNewFormBuilderEnabledForMpush: () => mockMpushFlag,
38
+ }));
39
+
40
+ const fullDefinition = () => _.cloneDeep(mpushSchemaResponse.metaEntities[0].definition);
41
+
42
+ const intl = {
43
+ formatMessage: (descriptor) => (descriptor && (descriptor.defaultMessage || descriptor.id)) || '',
44
+ locale: 'en',
45
+ };
46
+
47
+ const templateDetails = () => ({
48
+ name: 'test_push_12',
49
+ mode: 'text',
50
+ definition: { mode: 'text' },
51
+ versions: {
52
+ base: {
53
+ ANDROID: { title: 'Saved A title', message: 'Saved A body' },
54
+ IOS: { title: 'Saved I title', message: 'Saved I body' },
55
+ },
56
+ },
57
+ });
58
+
59
+ const selectedAccount = {
60
+ id: 'acc-1',
61
+ name: 'Acc',
62
+ configs: { android: '1', ios: '1', deeplink: '[]' },
63
+ };
64
+
65
+ const buildProps = (overrides = {}) => ({
66
+ intl,
67
+ params: { id: '123' },
68
+ location: { pathname: '/mobilepush/edit/123', query: { module: 'default' } },
69
+ router: { push: jest.fn() },
70
+ isFullMode: true,
71
+ isLoadingMetaEntities: false,
72
+ metaEntities: {},
73
+ templateDetails: null,
74
+ Edit: { weCrmAccounts: [] },
75
+ Create: {},
76
+ Templates: { selectedWeChatAccount: selectedAccount },
77
+ // Any missing action becomes a jest.fn on demand (the container calls several).
78
+ actions: new Proxy({}, { get: (target, name) => {
79
+ if (!target[name]) target[name] = jest.fn(); // eslint-disable-line no-param-reassign
80
+ return target[name];
81
+ } }),
82
+ globalActions: {
83
+ fetchSchemaForEntity: jest.fn(),
84
+ addMessageToQueue: jest.fn(),
85
+ setInjectedTags: jest.fn(),
86
+ },
87
+ showTemplateName: jest.fn(),
88
+ getFormLibraryData: jest.fn(),
89
+ onValidationFail: jest.fn(),
90
+ showLiquidErrorInFooter: jest.fn(),
91
+ onPersonalizationTokensChange: jest.fn(),
92
+ injectedTags: {},
93
+ ...overrides,
94
+ });
95
+
96
+ const read = () => {
97
+ const editor = document.getElementById('message-editor');
98
+ const title = document.getElementById('message-title');
99
+ return {
100
+ titleValue: title ? title.value : '(no #message-title in DOM)',
101
+ editorValue: editor ? editor.value : '(no #message-editor in DOM)',
102
+ };
103
+ };
104
+
105
+ const runEditContainer = () => {
106
+ const props = buildProps();
107
+ const ui = (p) => (
108
+ <Router history={history}>
109
+ <Edit {...p} />
110
+ </Router>
111
+ );
112
+ const utils = render(ui(props));
113
+ // metaEntities arrive (bundled schema)
114
+ act(() => {
115
+ utils.rerender(ui({ ...props, metaEntities: { layouts: [{ definition: fullDefinition() }], tags: { standard: [] } } }));
116
+ });
117
+ // template details arrive
118
+ act(() => {
119
+ utils.rerender(ui({
120
+ ...props,
121
+ metaEntities: { layouts: [{ definition: fullDefinition() }], tags: { standard: [] } },
122
+ templateDetails: templateDetails(),
123
+ }));
124
+ });
125
+ const result = read();
126
+ utils.unmount();
127
+ return result;
128
+ };
129
+
130
+ describe('SCRATCH v3: REAL Edit container over the gated FormBuilder', () => {
131
+ afterEach(() => {
132
+ mockMpushFlag = false;
133
+ });
134
+
135
+ it('Functional path (flag on) populates the fields', () => {
136
+ mockMpushFlag = true;
137
+ const r = runEditContainer();
138
+ expect(r.titleValue).toBe('Saved A title');
139
+ expect(r.editorValue).toBe('Saved A body');
140
+ });
141
+
142
+ it('Classic path (flag off) populates the fields', () => {
143
+ mockMpushFlag = false;
144
+ const r = runEditContainer();
145
+ expect(r.titleValue).toBe('Saved A title');
146
+ expect(r.editorValue).toBe('Saved A body');
147
+ });
148
+ });