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