@capillarytech/creatives-library 9.0.59 → 9.0.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/constants/unified.js +2 -3
- package/package.json +1 -1
- package/utils/common.js +0 -8
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +69 -324
- package/v2Components/FormBuilder/Functional/channels/registry.js +0 -2
- package/v2Components/FormBuilder/Functional/constants.js +8 -46
- package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +28 -175
- package/v2Components/FormBuilder/Functional/core/store/formReducer.js +6 -75
- package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +20 -10
- package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +1 -9
- package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +3 -20
- package/v2Components/FormBuilder/Functional/layout/Section.js +1 -6
- package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +2 -9
- package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -17
- package/v2Components/FormBuilder/_formBuilder.scss +0 -11
- package/v2Components/FormBuilder/index.js +13 -20
- package/v2Components/FormBuilder/tests/entryGate.test.js +7 -87
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +12 -25
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +9 -28
- package/v2Containers/CommunicationFlow/hooks/Tests/useFetchCommDefinition.test.js +118 -0
- package/v2Containers/CommunicationFlow/hooks/useFetchCommDefinition.js +49 -0
- package/v2Components/FormBuilder/Functional/channels/mobilepush/buildSubmitPayload.js +0 -10
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +0 -75
- package/v2Components/FormBuilder/Functional/channels/mobilepush/getEditorErrorDescriptor.js +0 -74
- package/v2Components/FormBuilder/Functional/channels/mobilepush/index.js +0 -28
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +0 -21
- package/v2Components/FormBuilder/Functional/channels/mobilepush/runSubmitPipeline.js +0 -45
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/getEditorErrorDescriptor.test.js +0 -162
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/modals.test.js +0 -37
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/runSubmitPipeline.test.js +0 -70
- package/v2Components/FormBuilder/Functional/channels/mobilepush/tests/validate.test.js +0 -274
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +0 -251
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +0 -85
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +0 -448
- package/v2Components/FormBuilder/Functional/tests/manual.typingPerf.test.js +0 -135
- package/v2Components/FormBuilder/Functional/tests/mpush.crossFlowParity.test.js +0 -430
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +0 -320
- package/v2Components/FormBuilder/Functional/tests/mpush.editContainer.parity.test.js +0 -148
- package/v2Components/FormBuilder/Functional/tests/mpush.engine.test.js +0 -306
- package/v2Components/FormBuilder/Functional/tests/mpush.libraryValidity.parity.test.js +0 -147
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +0 -200
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +0 -376
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +0 -110
- 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
|
-
});
|