@capillarytech/creatives-library 9.0.59 → 9.0.61-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 (48) 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/v2Containers/TagList/index.js +3 -3
  23. package/v2Containers/TagList/tests/TagList.test.js +16 -0
  24. package/v2Containers/Viber/constants.js +6 -0
  25. package/v2Containers/Viber/index.js +22 -16
  26. package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
  27. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
  28. package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
  29. package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
  30. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
  31. package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
  32. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
  33. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
  34. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
  35. package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
  36. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
  37. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
  38. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
  39. package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
  40. package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
  41. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
  42. package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
  43. package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
  44. package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
  45. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
  46. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
  47. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
  48. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +0 -459
@@ -1,459 +0,0 @@
1
- /**
2
- * MOBILEPUSH characterization (golden) tests — FormBuilder V3 migration, PR6a.
3
- *
4
- * These tests pin the CURRENT behavior of the FormBuilder monolith
5
- * (app/v2Components/FormBuilder/Classic.js) for the MOBILEPUSH channel, using
6
- * the real bundled layout schemas (app/v2Containers/MobilePush/initialSchema.js,
7
- * textSchema + imageSchema). They are the parity gate for the functional
8
- * rewrite: the new implementation must keep every assertion here green.
9
- *
10
- * Behavior is pinned as-is, including quirks (the single-platform modal's early
11
- * return that skips validity emission AND stopValidation; the string-typed
12
- * 'tagBracketCountMismatchError' error value; writes scoping to currentTab).
13
- * Do not "fix" expectations here — if the monolith changes, that is the regression.
14
- */
15
-
16
- import React from 'react';
17
- import { injectIntl } from 'react-intl';
18
- import '@testing-library/jest-dom';
19
- import _ from 'lodash';
20
- import { Provider } from 'react-redux';
21
- import { Router } from 'react-router-dom';
22
- import { configureStore } from '@capillarytech/vulcan-react-sdk/utils';
23
- // Import Classic directly, NOT the entry-gated ../index (the gate could route
24
- // MOBILEPUSH to the Functional build based on feature access).
25
- import FormBuilder from '../Classic';
26
- import history from '../../../utils/history';
27
- import { initialReducer } from '../../../initialReducer';
28
- import {
29
- render, screen, fireEvent, waitFor, act,
30
- } from '../../../utils/test-utils';
31
- import { response as mpushSchemaResponse } from '../../../v2Containers/MobilePush/initialSchema';
32
-
33
- // Same harness facts as the SMS suite: neutralize the auth-guarded TagList wrapper…
34
- jest.mock('redux-auth-wrapper/history4/redirect', () => ({
35
- connectedRouterRedirect: jest.fn(() => (Component) => Component),
36
- }));
37
- // …and stub the impure unsubscribe-url call with a plain function (resetMocks-safe).
38
- jest.mock('../../../services/api', () => ({
39
- ...jest.requireActual('../../../services/api'),
40
- getUnsubscribeUrl: () => Promise.resolve({ response: { response: '' } }),
41
- }));
42
-
43
- const ComponentToRender = injectIntl(FormBuilder);
44
-
45
- /**
46
- * Mimics the MobilePush containers' injectEvents step: walks the standalone
47
- * sections AND the tabs container (panes + headers), gluing capture handlers
48
- * onto every node with supportedEvents — including the container itself
49
- * (onTabChange). Handlers record args + `this` (the monolith rebinds to
50
- * props.parent), which the rewrite's parent bridge must preserve.
51
- */
52
- const injectHandlers = (schema) => {
53
- const registry = {};
54
- const attach = (field) => {
55
- if (!field || !field.id || !field.supportedEvents) return;
56
- field.injectedEvents = {};
57
- field.supportedEvents.forEach((event) => {
58
- const record = { calls: [], contexts: [] };
59
- registry[`${field.id}:${event}`] = record;
60
- field.injectedEvents[event] = function capture(...args) {
61
- record.calls.push(args);
62
- record.contexts.push(this);
63
- };
64
- });
65
- };
66
- const walkSection = (section) => {
67
- if (!section) return;
68
- if (section.type === 'parent') {
69
- (section.childSections || []).forEach(walkSection);
70
- } else if (section.type === 'multicols') {
71
- (section.inputFields || []).forEach((row) => (row.cols || []).forEach(attach));
72
- (section.actionFields || []).forEach((row) => (row.cols || []).forEach(attach));
73
- } else if (section.type === 'col-label') {
74
- (section.inputFields || []).forEach(attach);
75
- (section.actionFields || []).forEach(attach);
76
- }
77
- };
78
- ((schema.standalone && schema.standalone.sections) || []).forEach(walkSection);
79
- (schema.containers || []).forEach((container) => {
80
- attach(container);
81
- (container.panes || []).forEach((pane) => {
82
- (pane.sections || []).forEach(walkSection);
83
- (pane.sectionsHeaders || []).forEach(walkSection);
84
- });
85
- });
86
- return registry;
87
- };
88
-
89
- const buildMpushSchema = (variant = 'textSchema') => {
90
- const schema = _.cloneDeep(mpushSchemaResponse.metaEntities[0].definition[variant]);
91
- const registry = injectHandlers(schema);
92
- return { schema, registry };
93
- };
94
-
95
- const buildProps = (overrides = {}) => ({
96
- formData: {},
97
- location: { pathname: '/mobilepush/create/text', query: { type: false, module: 'default' } },
98
- usingTabContainer: true,
99
- isFullMode: true,
100
- isEdit: false,
101
- startValidation: false,
102
- checkValidation: false,
103
- currentTab: 1,
104
- tabCount: 2,
105
- tabKey: '',
106
- tags: [],
107
- injectedTags: {},
108
- onChange: jest.fn(),
109
- onSubmit: jest.fn(),
110
- onFormValidityChange: jest.fn(),
111
- stopValidation: jest.fn(),
112
- showLiquidErrorInFooter: jest.fn(),
113
- setModalContent: jest.fn(),
114
- handleCancelModal: jest.fn(),
115
- ...overrides,
116
- });
117
-
118
- const renderMpush = (props) => {
119
- const store = configureStore({}, initialReducer, history);
120
- const ui = (p) => (
121
- <Router history={history}>
122
- <Provider store={store}>
123
- <div className="mobilepush-wrapper">
124
- <ComponentToRender {...p} />
125
- </div>
126
- </Provider>
127
- </Router>
128
- );
129
- const utils = render(ui(props));
130
- return { ...utils, rerenderWith: (next) => utils.rerender(ui(next)) };
131
- };
132
-
133
- const lastCall = (mockFn) => mockFn.mock.calls[mockFn.mock.calls.length - 1];
134
-
135
- // GOLDEN (pinned by these tests): despite the pane objects carrying
136
- // forceRender:true, CapTab keeps ONLY the ACTIVE pane's fields in the DOM.
137
- // Android and iOS share the same placeholder strings, so on the Android tab
138
- // these queries resolve to the Android inputs; after switching, to the iOS ones.
139
- const titleInput = () => screen.getByPlaceholderText('Please input message title name.');
140
- const editorInput = () => screen.getByPlaceholderText('Please input mobile push template content.');
141
-
142
- const fillAndroid = ({ title = 'A title', message = 'A message' } = {}) => {
143
- fireEvent.change(titleInput(), { target: { value: title } });
144
- fireEvent.change(editorInput(), { target: { value: message } });
145
- };
146
-
147
- // Switch to the iOS pane the way a user does: click the tab header. Classic then
148
- // runs its setTimeout(0) DOM scan (.mobilepush-wrapper .ant-tabs-tab-active) to
149
- // derive the 1-based index and notify the container's injected onTabChange.
150
- //
151
- // GOLDEN QUIRK (pinned by seedTabs below): pane keys are `formData[i].tabKey ||
152
- // index`. In the pure create flow only tab 0 gets a tabKey (_.uniqueId()), and in
153
- // a fresh module uniqueId() returns "1" while the iOS pane falls back to index 1 —
154
- // the keys COLLIDE and antd treats the iOS tab as already-active, so clicking it
155
- // does nothing. Production only escapes this because the app-wide uniqueId counter
156
- // has advanced. Tests (and real hydration flows) must supply distinct tabKeys.
157
- const seedTabs = { 0: { base: true, tabKey: 'tab-android' }, 1: { tabKey: 'tab-ios' } };
158
-
159
- const clickIosTab = async () => {
160
- fireEvent.click(screen.getByText('iOS'));
161
- await waitFor(
162
- () => expect(document.getElementById('message-editor2')).toBeInTheDocument(),
163
- { timeout: 3000 },
164
- );
165
- };
166
-
167
- const typeTemplateName = (name = 'My Push', placeholder = 'Enter template name*') => {
168
- jest.useFakeTimers();
169
- fireEvent.change(screen.getByPlaceholderText(placeholder), { target: { value: name } });
170
- act(() => { jest.advanceTimersByTime(350); });
171
- jest.useRealTimers();
172
- };
173
-
174
- describe('MPUSH characterization — mount & rendering census (textSchema)', () => {
175
- it('renders the standalone fields, both tab headers, and ONLY the active (Android) pane fields', () => {
176
- const { schema } = buildMpushSchema();
177
- renderMpush(buildProps({ schema }));
178
-
179
- expect(screen.getByPlaceholderText('Enter template name*')).toBeInTheDocument();
180
- expect(screen.getByText('Save')).toBeInTheDocument();
181
- // Both tab HEADERS exist…
182
- expect(screen.getByText('Android')).toBeInTheDocument();
183
- expect(screen.getByText('iOS')).toBeInTheDocument();
184
- // …but only the ACTIVE pane's fields are in the DOM (pinned Classic behavior:
185
- // the pane objects say forceRender:true, yet CapTab mounts the active pane only).
186
- expect(screen.getAllByPlaceholderText('Please input message title name.')).toHaveLength(1);
187
- expect(document.getElementById('message-editor')).toBeInTheDocument();
188
- expect(document.getElementById('message-editor2')).toBeNull();
189
- // Android pane extras: copy-from-iOS link + CTA checkbox
190
- expect(screen.getByText('Copy Title and Content From IOS')).toBeInTheDocument();
191
- expect(screen.getByText('Add action link to content')).toBeInTheDocument();
192
- // The iOS pane's copy link is NOT mounted while Android is active
193
- expect(screen.queryByText('Copy Title and Content From Android')).toBeNull();
194
- });
195
-
196
- it('clicking the iOS tab header swaps the mounted pane and notifies the injected onTabChange with the 1-based index', async () => {
197
- const { schema, registry } = buildMpushSchema();
198
- const parentSentinel = { iAmTheContainer: true };
199
- renderMpush(buildProps({ schema, parent: parentSentinel, formData: _.cloneDeep(seedTabs) }));
200
-
201
- await clickIosTab();
202
-
203
- // GOLDEN: panes are lazy-mounted but PERSIST once visited — after switching,
204
- // the Android editor is still in the DOM (hidden), alongside the iOS pane.
205
- expect(document.getElementById('message-editor')).toBeInTheDocument();
206
- expect(screen.getByText('Copy Title and Content From Android')).toBeInTheDocument();
207
- const record = registry['pane:onTabChange'];
208
- await waitFor(() => expect(record.calls.length).toBeGreaterThan(0));
209
- expect(record.calls[record.calls.length - 1][0]).toBe(2); // 1-based: 2 = iOS
210
- expect(record.contexts[0]).toBe(parentSentinel);
211
- });
212
-
213
- it('mount validation reports invalid: template-name at ROOT, message errors under tab indices', () => {
214
- const { schema } = buildMpushSchema();
215
- const props = buildProps({ schema });
216
- renderMpush(props);
217
-
218
- expect(props.onFormValidityChange).toHaveBeenCalled();
219
- const [isValid, errorData] = lastCall(props.onFormValidityChange);
220
- expect(isValid).toBe(false);
221
- expect(errorData['template-name']).toBe(true);
222
- expect(errorData[0]['message-editor']).toBe(true);
223
- expect(errorData[0]['message-title']).toBe(true);
224
- });
225
- });
226
-
227
- describe('MPUSH characterization — onChange contract', () => {
228
- it('typing in the Android editor writes formData[0], mirrors the base alias, and emits onChange(formData, tabCount, 1, field)', () => {
229
- const { schema } = buildMpushSchema();
230
- const props = buildProps({ schema });
231
- renderMpush(props);
232
-
233
- fireEvent.change(editorInput(), { target: { value: 'hello android' } });
234
-
235
- expect(props.onChange).toHaveBeenCalled();
236
- const [formData, tabCount, currentTab, fieldSchema] = lastCall(props.onChange);
237
- // QUIRK (pinned): Classic emits this.state.tabCount, which starts at 1 in the
238
- // create flow and is only mirrored from the prop in edit-mode formData syncs —
239
- // so create-flow MPUSH emits tabCount 1 even though the containers pass tabCount={2}.
240
- expect(tabCount).toBe(1);
241
- expect(currentTab).toBe(1);
242
- expect(fieldSchema.id).toBe('message-editor');
243
- expect(formData[0]['message-editor']).toBe('hello android');
244
- // base is an ALIAS of tab 0 and mirrors the write
245
- expect(formData[0].base).toBe(true);
246
- expect(formData.base['message-editor']).toBe('hello android');
247
- });
248
-
249
- it('after switching to the iOS tab, writes land in tab 1 and onChange carries currentTab=2', async () => {
250
- const { schema } = buildMpushSchema();
251
- const props = buildProps({ schema, formData: _.cloneDeep(seedTabs) });
252
- renderMpush(props);
253
-
254
- await clickIosTab();
255
- // Classic derives currentTab from a setTimeout(0) DOM scan; give it a tick.
256
- await waitFor(() => {
257
- fireEvent.change(document.getElementById('message-editor2'), { target: { value: 'ios body' } });
258
- const [formData, , currentTab] = lastCall(props.onChange);
259
- expect(currentTab).toBe(2);
260
- expect(formData[1]['message-editor2']).toBe('ios body');
261
- });
262
- });
263
- });
264
-
265
- describe('MPUSH characterization — save flow (startValidation rising edge, full mode)', () => {
266
- it('both platforms filled + template name => valid, submits the RAW full formData (no getChannelData)', async () => {
267
- const { schema } = buildMpushSchema();
268
- const props = buildProps({ schema, formData: _.cloneDeep(seedTabs) });
269
- const { rerenderWith } = renderMpush(props);
270
-
271
- typeTemplateName('My Push');
272
- fillAndroid();
273
- await clickIosTab();
274
- await waitFor(() => {
275
- fireEvent.change(document.getElementById('message-title2'), { target: { value: 'I title' } });
276
- fireEvent.change(document.getElementById('message-editor2'), { target: { value: 'I message' } });
277
- const [formData] = lastCall(props.onChange);
278
- expect(formData[1]['message-editor2']).toBe('I message');
279
- });
280
-
281
- // GOLDEN QUIRK: Classic re-mirrors ANY non-empty formData prop that differs
282
- // from its state (componentWillReceiveProps) — a stale parent copy clobbers
283
- // typed values. Real containers survive only because they refresh their copy
284
- // from every onChange; the test must do the same when re-rendering.
285
- const latestEmitted = lastCall(props.onChange)[0];
286
- rerenderWith({ ...props, formData: latestEmitted, startValidation: true });
287
-
288
- const [isValid] = lastCall(props.onFormValidityChange);
289
- expect(isValid).toBe(true);
290
- expect(props.onSubmit).toHaveBeenCalledTimes(1);
291
- const [submitted] = lastCall(props.onSubmit);
292
- expect(submitted['template-name']).toBe('My Push');
293
- expect(submitted[0]['message-editor']).toBe('A message');
294
- expect(submitted[1]['message-editor2']).toBe('I message');
295
- // GOLDEN: base is created as a reference alias of tab 0 at init, but Classic's
296
- // update path lets the two diverge into separate objects with MIRRORED content.
297
- // The container-observable contract is value equality, not reference identity.
298
- expect(submitted.base).toStrictEqual(submitted[0]);
299
- });
300
-
301
- it('QUIRK: iOS empty + saving from the Android tab => setModalContent("android"), and NOTHING else runs (no onSubmit, no stopValidation, no validity emission)', () => {
302
- const { schema } = buildMpushSchema();
303
- const props = buildProps({ schema });
304
- const { rerenderWith } = renderMpush(props);
305
-
306
- typeTemplateName('My Push');
307
- fillAndroid();
308
- props.onFormValidityChange.mockClear();
309
-
310
- rerenderWith({ ...props, startValidation: true });
311
-
312
- expect(props.setModalContent).toHaveBeenCalledWith('android');
313
- expect(props.onSubmit).not.toHaveBeenCalled();
314
- // the early return skips BOTH the save callback and stopValidation
315
- expect(props.stopValidation).not.toHaveBeenCalled();
316
- expect(props.onFormValidityChange).not.toHaveBeenCalled();
317
- });
318
-
319
- it('confirming the single-platform modal (Yes, id=android) submits the full formData', () => {
320
- const { schema } = buildMpushSchema();
321
- const props = buildProps({ schema });
322
- const { rerenderWith } = renderMpush(props);
323
-
324
- typeTemplateName('My Push');
325
- fillAndroid();
326
- rerenderWith({ ...props, startValidation: true });
327
- expect(props.setModalContent).toHaveBeenCalledWith('android');
328
-
329
- // The container reacts to setModalContent by passing the modal back down.
330
- rerenderWith({
331
- ...props,
332
- startValidation: true,
333
- showModal: true,
334
- modal: {
335
- title: 'Alert!', body: 'IOS template is not configured', type: 'confirm', id: 'android',
336
- },
337
- });
338
- fireEvent.click(screen.getByText('Yes'));
339
-
340
- expect(props.onSubmit).toHaveBeenCalledTimes(1);
341
- const [submitted] = lastCall(props.onSubmit);
342
- expect(submitted[0]['message-editor']).toBe('A message');
343
- });
344
-
345
- it('brace mismatch in the Android editor blocks save with the STRING error value + bracket-error (both platforms present => no modal detour)', () => {
346
- // Hydrate BOTH tabs via the formData prop (edit flow) so the single-platform
347
- // modal path cannot swallow the validity emission.
348
- const { schema } = buildMpushSchema();
349
- const tab0 = {
350
- 'message-title': 'A title', 'message-editor': 'abc {{full_name', base: true, tabKey: 'ka',
351
- };
352
- const tab1 = { 'message-title2': 'I title', 'message-editor2': 'I message', tabKey: 'ki' };
353
- const props = buildProps({
354
- schema,
355
- isEdit: true,
356
- formData: {
357
- 'template-name': 'My Push', 0: tab0, 1: tab1, base: tab0,
358
- },
359
- });
360
- const { rerenderWith } = renderMpush(props);
361
-
362
- rerenderWith({ ...props, startValidation: true });
363
-
364
- const [isValid, errorData] = lastCall(props.onFormValidityChange);
365
- expect(isValid).toBe(false);
366
- expect(errorData[0]['message-editor']).toBe('tagBracketCountMismatchError');
367
- expect(errorData[0]['bracket-error']).toBe('tagBracketCountMismatchError');
368
- expect(props.onSubmit).not.toHaveBeenCalled();
369
- expect(props.stopValidation).toHaveBeenCalled();
370
- });
371
-
372
- it('embedded mode drops the template-name requirement', () => {
373
- const { schema } = buildMpushSchema();
374
- const props = buildProps({
375
- schema,
376
- location: { pathname: '/mobilepush/create/text', query: { type: 'embedded', module: 'campaigns' } },
377
- });
378
- renderMpush(props);
379
-
380
- const [, errorData] = lastCall(props.onFormValidityChange);
381
- expect(errorData['template-name']).toBe(false);
382
- });
383
- });
384
-
385
- describe('MPUSH characterization — container handler contract', () => {
386
- it('clicking Save invokes the injected saveFormData handler with `this` rebound to props.parent and val.id as second arg', () => {
387
- const { schema, registry } = buildMpushSchema();
388
- const parentSentinel = { iAmTheContainer: true };
389
- renderMpush(buildProps({ schema, parent: parentSentinel }));
390
-
391
- fireEvent.click(screen.getByText('Save'));
392
-
393
- const record = registry['save-button:saveFormData'];
394
- expect(record.calls).toHaveLength(1);
395
- expect(record.calls[0][1]).toBe('save-button');
396
- expect(record.contexts[0]).toBe(parentSentinel);
397
- });
398
-
399
- it('clicking the copy-content div invokes its injected onClick with `this` rebound to props.parent', () => {
400
- const { schema, registry } = buildMpushSchema();
401
- const parentSentinel = { iAmTheContainer: true };
402
- renderMpush(buildProps({ schema, parent: parentSentinel }));
403
-
404
- fireEvent.click(screen.getByText('Copy Title and Content From IOS'));
405
-
406
- const record = registry['copy-iphone-content:onClick'];
407
- expect(record.calls).toHaveLength(1);
408
- expect(record.calls[0][1]).toBe('copy-iphone-content');
409
- expect(record.contexts[0]).toBe(parentSentinel);
410
- });
411
- });
412
-
413
- describe('MPUSH characterization — edit-mode hydration', () => {
414
- it('renders saved template data passed via the formData prop without overwriting it', async () => {
415
- const { schema } = buildMpushSchema();
416
- const tab0 = {
417
- 'message-title': 'Saved A title', 'message-editor': 'Saved A body', base: true, tabKey: 'ka',
418
- };
419
- const tab1 = { 'message-title2': 'Saved I title', 'message-editor2': 'Saved I body', tabKey: 'ki' };
420
- const props = buildProps({
421
- schema,
422
- isEdit: true,
423
- formData: {
424
- 'template-name': 'Saved name', 0: tab0, 1: tab1, base: tab0,
425
- },
426
- });
427
- renderMpush(props);
428
-
429
- expect(screen.getByPlaceholderText('Enter template name*')).toHaveValue('Saved name');
430
- expect(titleInput()).toHaveValue('Saved A title');
431
- expect(editorInput()).toHaveValue('Saved A body');
432
- // iOS pane values render after switching tabs; GOLDEN: the field values bind
433
- // one tick AFTER the pane mounts, because Classic derives currentTab from its
434
- // setTimeout(0) DOM scan and only then re-renders values from formData[1].
435
- await clickIosTab();
436
- await waitFor(() => {
437
- expect(document.getElementById('message-title2')).toHaveValue('Saved I title');
438
- expect(document.getElementById('message-editor2')).toHaveValue('Saved I body');
439
- });
440
- });
441
- });
442
-
443
- describe('MPUSH characterization — imageSchema', () => {
444
- it('seeds the image field and REQUIRES it: filled platforms still fail with errorData[0].image', () => {
445
- const { schema } = buildMpushSchema('imageSchema');
446
- const props = buildProps({ schema, location: { pathname: '/mobilepush/create/image', query: { type: false, module: 'default' } } });
447
- const { rerenderWith } = renderMpush(props);
448
-
449
- typeTemplateName('My Push', 'Enter template name'); // imageSchema has no asterisk
450
- fillAndroid();
451
-
452
- rerenderWith({ ...props, startValidation: true });
453
-
454
- const [isValid, errorData] = lastCall(props.onFormValidityChange);
455
- expect(isValid).toBe(false);
456
- expect(errorData[0].image).toBe(true);
457
- expect(props.onSubmit).not.toHaveBeenCalled();
458
- });
459
- });