@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,448 +0,0 @@
1
- /**
2
- * MOBILEPUSH field renderers — presentational mirrors of Classic's render cases.
3
- * Prop contract: { field, value, error, resolvedError, onChange, onBlur, onEvent, renderContext }.
4
- * ButtonField/TagListField are reused from the SMS set; the rest are MPUSH variants
5
- * where Classic's behavior differs by channel (each notes its Classic lines).
6
- */
7
-
8
- import React from 'react';
9
- import PropTypes from 'prop-types';
10
- import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
11
- import CapInput from '@capillarytech/cap-ui-library/CapInput';
12
- import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
13
- import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
14
- import CapButton from '@capillarytech/cap-ui-library/CapButton';
15
- import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
16
- import CapImage from '@capillarytech/cap-ui-library/CapImage';
17
- import CapRadio from '@capillarytech/cap-ui-library/CapRadio';
18
- import CapRadioGroup from '@capillarytech/cap-ui-library/CapRadioGroup';
19
- import CapSelect from '@capillarytech/cap-ui-library/CapSelect';
20
- import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
21
- import LabelHOC from '@capillarytech/cap-ui-library/assets/HOCs/ComponentWithLabelHOC';
22
- import { FONT_COLOR_04, FONT_COLOR_05 } from '@capillarytech/cap-ui-library/styled/variables';
23
- import UnifiedPreview from '../../../CommonTestAndPreview/UnifiedPreview';
24
- import { ANDROID } from '../../../CommonTestAndPreview/constants';
25
- import { MOBILE_PUSH } from '../../../../v2Containers/CreativesContainer/constants';
26
- import globalMessages from '../../../../v2Containers/Cap/messages';
27
- import formMessages from '../../messages';
28
- import { isAiContentBotDisabled } from '../../../../utils/common';
29
- import { hasPersonalizationTags } from '../../../../utils/commonUtils';
30
- import { createFieldRegistry } from '../core/schema/fieldRegistry';
31
- import { ALLOWED_IMAGE_EXTENSIONS, FIELD_TYPE } from '../constants';
32
- import { ButtonField, TagListField } from './smsRenderers';
33
- import { ERROR_VALUE } from '../channels/mobilepush/config';
34
-
35
- const { TextArea } = CapInput;
36
-
37
-
38
- const dataFieldPropTypes = {
39
- field: PropTypes.object.isRequired,
40
- value: PropTypes.any,
41
- error: PropTypes.any,
42
- onChange: PropTypes.func.isRequired,
43
- renderContext: PropTypes.object.isRequired,
44
- };
45
- const dataFieldDefaultProps = {
46
- value: undefined,
47
- error: undefined,
48
- };
49
-
50
- const formatSafe = (intl, descriptor) => (intl?.formatMessage ? intl.formatMessage(descriptor) : '');
51
-
52
- // Classic gates every field-error display on checkValidation (pre-save silence).
53
- const shouldShowError = (renderContext, error) => Boolean(renderContext?.checkValidation && error);
54
-
55
- /** Classic input-case error message: personalization > brace > schema errorMessage. */
56
- const resolveInputMessage = (field, value, error, renderContext) => {
57
- const { intl, restrictPersonalization } = renderContext || {};
58
- if (restrictPersonalization && value && hasPersonalizationTags(String(value))) {
59
- return formatSafe(intl, formMessages.personalizationTagsErrorMessage);
60
- }
61
- if (error === ERROR_VALUE.BRACKET) {
62
- return formatSafe(intl, globalMessages.unbalanacedCurlyBraces);
63
- }
64
- return field.errorMessage || '';
65
- };
66
-
67
- export const MpushInputField = ({
68
- field, value, error, onChange, onBlur, renderContext,
69
- }) => {
70
- const showError = shouldShowError(renderContext, error);
71
- const message = showError ? resolveInputMessage(field, value, error, renderContext) : '';
72
- return (
73
- <CapColumn key={field.id} span={field.width} offset={field.offset} style={field.style || {}}>
74
- <CapInput
75
- id={field.id}
76
- label={field.label}
77
- placeholder={field.placeholder}
78
- errorMessage={message}
79
- className={`input-primary chart-name-input${showError ? ' error' : ''}`}
80
- value={value || ''}
81
- onChange={(e) => onChange(e.target.value)}
82
- onBlur={onBlur}
83
- disabled={field.disabled}
84
- size={field.size || 'default'}
85
- />
86
- </CapColumn>
87
- );
88
- };
89
-
90
- MpushInputField.propTypes = { ...dataFieldPropTypes, onBlur: PropTypes.func };
91
- MpushInputField.defaultProps = { ...dataFieldDefaultProps, onBlur: undefined };
92
-
93
- export const MpushTextAreaField = ({
94
- field, value, error, onChange, renderContext,
95
- }) => {
96
- let aiDisabled = true;
97
- try { aiDisabled = isAiContentBotDisabled(); } catch (e) { console.error(e); aiDisabled = true; }
98
- // Classic-observable behavior: errorData is only ever computed under
99
- // startValidation (Classic.js:334-345), so NO editor error — empty or tag —
100
- // is visible before a save attempt. checkValidation is true exactly in the
101
- // states where Classic can hold a non-false errorType, so gating display on it
102
- // reproduces Classic's typing silence and also keeps the initial-hydrate
103
- // errorData (computed for the parent validity emission) from leaking into the UI.
104
- const showError = shouldShowError(renderContext, error);
105
- // MPUSH keeps the inline message even though the channel is liquid-supported
106
- // (Classic.js:2908-2915). Personalization gets its own message (2877-2879).
107
- const inlineMessage = showError ? resolveInputMessage(field, value, error, renderContext) : '';
108
- return (
109
- <CapColumn key={field.id} span={field.width} offset={field.offset}>
110
- <TextArea
111
- id={field.id}
112
- label={field.label}
113
- placeholder={field.placeholder || ''}
114
- className={`${showError ? 'error-form-builder' : ''}`}
115
- errorMessage={inlineMessage}
116
- autosize={field.autosize ? field.autosizeParams : false}
117
- value={value || ''}
118
- onChange={(e) => onChange(e.target.value)}
119
- style={field.style || {}}
120
- disabled={field.disabled}
121
- />
122
- {!aiDisabled && (
123
- // AskAira content bot is enabled for MOBILEPUSH textareas (Classic.js:2927).
124
- <CapAskAira.ContentGenerationBot
125
- text={value || ''}
126
- setText={(x) => onChange(x)}
127
- iconPlacement="float-br"
128
- rootStyle={{ bottom: 'calc(1rem + 0.2rem)', right: '0.2rem' }}
129
- />
130
- )}
131
- </CapColumn>
132
- );
133
- };
134
-
135
- MpushTextAreaField.propTypes = { ...dataFieldPropTypes };
136
- MpushTextAreaField.defaultProps = { ...dataFieldDefaultProps };
137
-
138
- export const MpushCheckboxField = ({
139
- field, value, error, onChange, onEvent, renderContext,
140
- }) => {
141
- const showError = shouldShowError(renderContext, error);
142
- const checkbox = (
143
- <CapCheckbox
144
- key={field.id}
145
- className={`${showError ? 'error' : ''}`}
146
- errorMessage={field.errorMessage && showError ? field.errorMessage : ''}
147
- onChange={(e) => {
148
- const { checked } = e.target;
149
- onChange(checked);
150
- // CTA toggles route through the injected submit-action with the
151
- // performFormDataUpdate commit signature (Classic.js:3567 -> 228).
152
- if (field.submitAction && onEvent) onEvent(field.submitAction, checked);
153
- }}
154
- checked={Boolean(value)}
155
- style={field.style || {}}
156
- disabled={field.disabled}
157
- inductiveText={field.inductiveText}
158
- >
159
- {field.label}
160
- </CapCheckbox>
161
- );
162
- return (
163
- <CapColumn key={field.id} span={field.width} offset={field.offset}>
164
- {field.disabled && field.hoverText
165
- ? <CapTooltip title={field.hoverText}>{checkbox}</CapTooltip>
166
- : checkbox}
167
- </CapColumn>
168
- );
169
- };
170
-
171
- MpushCheckboxField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func };
172
- MpushCheckboxField.defaultProps = { ...dataFieldDefaultProps, onEvent: undefined };
173
-
174
- // Direct renderer (needs the full legacy formData for the value-resolution chain).
175
- export const RadioGroupField = ({
176
- field, error, onChange, onEvent, renderContext,
177
- }) => {
178
- const showError = shouldShowError(renderContext, error);
179
- // Classic handleSetRadioValue (2729-2736): tab value -> root value -> schema default.
180
- const legacy = renderContext?.legacyFormData || {};
181
- const tab = legacy[(renderContext?.paneTabIndex ?? renderContext?.activeTabIndex) || 0] || {};
182
- const resolvedValue = tab[field.id] !== undefined && tab[field.id] !== ''
183
- ? tab[field.id]
184
- : (legacy[field.id] !== undefined && legacy[field.id] !== '' ? legacy[field.id] : field.value);
185
- return (
186
- <CapColumn key={`input-${field.id}`} span={field.width} offset={field.offset}>
187
- <CapRadioGroup
188
- key={`${field.id}-radio-group`}
189
- className={`form-builder-radio-group ${showError ? 'error' : ''}`}
190
- errorMessage={field.errorMessage && showError ? field.errorMessage : ''}
191
- onChange={(e) => {
192
- const nextValue = e.target.value;
193
- onChange(nextValue);
194
- onEvent('onChange', nextValue); // -> injected onLinkTypeChange (commit signature)
195
- }}
196
- style={field.style || {}}
197
- value={resolvedValue}
198
- name={field.name || `${field.id}-name`}
199
- disabled={field.disabled}
200
- >
201
- {(field.options || []).map((option, index) => (
202
- <CapRadio
203
- key={`${field.id}-radio-${index}`}
204
- value={option}
205
- inductiveText={field.inductiveText && field.inductiveText[index]}
206
- >
207
- {option}
208
- </CapRadio>
209
- ))}
210
- </CapRadioGroup>
211
- </CapColumn>
212
- );
213
- };
214
-
215
- RadioGroupField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func.isRequired };
216
- RadioGroupField.defaultProps = { ...dataFieldDefaultProps };
217
-
218
- // Direct renderer (options may be overridden through `${id}-options` in the form data).
219
- export const SelectField = ({
220
- field, value, error, onChange, onEvent, renderContext,
221
- }) => {
222
- const showError = shouldShowError(renderContext, error);
223
- const options = renderContext?.legacyFormData?.[`${field.id}-options`] || field.options;
224
- return (
225
- <CapColumn key={`select-${field.id}`} style={field.colStyle || {}} span={field.width} offset={field.offset}>
226
- <CapSelect
227
- id={field.id}
228
- options={options}
229
- placeholder={field.placeholder || ''}
230
- style={field.style || {}}
231
- onSelect={(data) => {
232
- onChange(data);
233
- onEvent('onSelect', data); // -> injected showCtaKeys / onTemplateChange (commit signature)
234
- }}
235
- value={value}
236
- disabled={field.disabled}
237
- label={field.label}
238
- />
239
- {showError && field.errorMessage && <span className="error">{field.errorMessage}</span>}
240
- </CapColumn>
241
- );
242
- };
243
-
244
- SelectField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func.isRequired };
245
- SelectField.defaultProps = { ...dataFieldDefaultProps };
246
-
247
- // Image upload (imageSchema only), Classic.js:3657-3690 + 2737-2764: the 5 MB check
248
- // and dimension read happen here; the upload + formData write stay container-owned.
249
- export const UploadField = ({
250
- field, value, error, onEvent, renderContext,
251
- }) => {
252
- // The only field whose error display also honors startValidation (Classic.js:3666).
253
- const showError = Boolean(
254
- (renderContext?.checkValidation || renderContext?.startValidation) && error,
255
- );
256
- const isImage = Boolean(value);
257
-
258
- const openFileDialog = (e) => {
259
- if (e) e.preventDefault();
260
- const fileInput = document.querySelector(`#${field.id} #fileName`);
261
- if (fileInput) fileInput.click();
262
- };
263
-
264
- const onFileChange = (e) => {
265
- const files = e?.target?.files;
266
- const file = files && files[0];
267
- if (!file) return;
268
- if (field.supportedExtensions && !ALLOWED_IMAGE_EXTENSIONS.exec(file.name)) {
269
- // Classic fires the wrong-file event and CONTINUES (no early return) — preserved.
270
- onEvent(field.submitAction, { file, type: 'wrong file' });
271
- }
272
- const urlApi = window.URL || window.webkitURL;
273
- const objectUrl = urlApi.createObjectURL(file);
274
- const img = new Image();
275
- img.src = objectUrl;
276
- img.onload = () => {
277
- const fileParams = {
278
- width: img?.width,
279
- height: img?.height,
280
- error: Boolean(file && (file.size / 1e6 > 5)), // > 5 MB (Classic.js:2753)
281
- };
282
- urlApi.revokeObjectURL(objectUrl); // dimension read done — free the blob URL
283
- onEvent(field.submitAction, { file, type: 'image', fileParams });
284
- };
285
- // Classic dispatches nothing on a failed image load — only release the URL.
286
- img.onerror = () => urlApi.revokeObjectURL(objectUrl);
287
- e.target.value = null;
288
- };
289
-
290
- const ImageComponent = (imageProps) => (
291
- <div key={`${field.id}-preview`}>
292
- <div className={`image-container ${imageProps.ifError ? 'error' : ''}`}>
293
- {isImage
294
- ? <CapImage src={value} alt={imageProps.alt} style={imageProps.style} />
295
- : (
296
- <div className="fb-upload-placeholder">
297
- <span className="image-placeholder">{imageProps.placeholder}</span>
298
- </div>
299
- )}
300
- </div>
301
- </div>
302
- );
303
- const WithLabel = LabelHOC(ImageComponent);
304
- const { errorMessage, ...previewRest } = field.previewProps || {};
305
-
306
- return (
307
- <CapColumn span={field.width} offset={field.offset} style={field.style}>
308
- {field.showPreview && (
309
- <WithLabel
310
- key={`${field.id}-with-label`}
311
- {...previewRest}
312
- errorMessage={showError && errorMessage}
313
- ifError={showError}
314
- />
315
- )}
316
- <form encType="multipart/form-data" id={field.id}>
317
- {/* Native input on purpose: openFileDialog clicks it via #fileName, and CapInput
318
- (antd text Input + affix wrapper) can't host type="file". */}
319
- <input
320
- key={field.id}
321
- className="fb-upload-file-input"
322
- id="fileName"
323
- type="file"
324
- onChange={onFileChange}
325
- accept={field.supportedExtensions ? field.supportedExtensions : 'image/*'}
326
- />
327
- <CapButton
328
- disabled={field.disabled || false}
329
- type="link"
330
- prefix={<CapIcon size="s" type="add-photo" />}
331
- onClick={openFileDialog}
332
- // color is state-dependent and must beat CapButton's own runtime styling —
333
- // kept inline (Classic.js:3399 verbatim).
334
- style={{ float: 'right', color: `${field.disabled ? FONT_COLOR_04 : FONT_COLOR_05}` }}
335
- >
336
- {field.label}
337
- </CapButton>
338
- </form>
339
- </CapColumn>
340
- );
341
- };
342
-
343
- UploadField.propTypes = { ...dataFieldPropTypes, onEvent: PropTypes.func.isRequired };
344
- UploadField.defaultProps = { ...dataFieldDefaultProps };
345
-
346
- // Primitive div (tab headers, copy links, key labels), Classic.js:3304-3328:
347
- // value resolves from the ACTIVE tab; DOM id gets the legacy pane suffix (`${id}2`).
348
- export const MpushDivField = ({ field, onEvent, renderContext }) => {
349
- const paneTabIndex = renderContext?.paneTabIndex ?? renderContext?.activeTabIndex ?? 0;
350
- const activeTab = renderContext?.legacyFormData?.[renderContext?.activeTabIndex ?? 0] || {};
351
- const children = activeTab[field.id] || field.value || '';
352
- const domId = `${field.id}${paneTabIndex > 0 ? paneTabIndex + 1 : ''}`;
353
- return (
354
- <div
355
- className={field.className || ''}
356
- id={domId}
357
- onClick={(data) => field.submitAction && onEvent(field.submitAction, data)}
358
- style={field.style || {}}
359
- // Classic fires the injected 'onChange' via callChildEvent's TAIL signature
360
- // (data, field.id) — the shell's commit-signature branch is type-guarded to
361
- // checkbox/radioGroup/select, so a div's onChange takes the tail path.
362
- onInput={(e) => { e.stopPropagation(); onEvent('onChange', e.target.textContent); }}
363
- >
364
- {children}
365
- </div>
366
- );
367
- };
368
-
369
- MpushDivField.propTypes = {
370
- field: PropTypes.object.isRequired,
371
- onEvent: PropTypes.func.isRequired,
372
- renderContext: PropTypes.object.isRequired,
373
- };
374
-
375
- // Direct renderer — CTA delete icons.
376
- export const IconField = ({ field, onEvent }) => (
377
- <CapColumn
378
- key={`icon-${field.id}`}
379
- id={field.id}
380
- offset={field.offset}
381
- width={field.width || 1}
382
- onClick={(data) => field.submitAction && onEvent(field.submitAction, data)}
383
- className={field.className || ''}
384
- style={field.colStyle || {}}
385
- >
386
- <i style={field.style || {}} className="material-icons">{field.value || ''}</i>
387
- </CapColumn>
388
- );
389
-
390
- IconField.propTypes = {
391
- field: PropTypes.object.isRequired,
392
- onEvent: PropTypes.func.isRequired,
393
- };
394
-
395
- // Device preview, Classic.js:3897-3943 quirks preserved: both panes show the ACTIVE
396
- // tab's content and the frame is hardcoded to ANDROID with the toggle hidden.
397
- export const MobilePushPreviewField = ({ field, renderContext }) => {
398
- const activeTab = renderContext?.legacyFormData?.[renderContext?.activeTabIndex ?? 0];
399
- if (!activeTab) return null;
400
- const header = activeTab[field.content?.title] || '';
401
- const bodyText = activeTab[field.content?.message] || '';
402
- const bodyImage = activeTab.image || '';
403
- const actions = [];
404
- if (activeTab[field.content?.secondaryCta1]) actions.push({ label: activeTab[field.content.secondaryCta1] });
405
- if (activeTab[field.content?.secondaryCta2]) actions.push({ label: activeTab[field.content.secondaryCta2] });
406
- const platformContent = {
407
- header, bodyText, bodyImage, actions, appName: field.content?.appName || '',
408
- };
409
- const mobilePushContent = { androidContent: platformContent, iosContent: platformContent };
410
- return (
411
- <CapColumn key="input" span={23} offset={1}>
412
- <UnifiedPreview
413
- key={field.id}
414
- style={field.customStyling || {}}
415
- channel={field.channel || MOBILE_PUSH}
416
- content={mobilePushContent}
417
- device={ANDROID}
418
- showDeviceToggle={false}
419
- showHeader={false}
420
- formatMessage={renderContext?.intl?.formatMessage}
421
- />
422
- </CapColumn>
423
- );
424
- };
425
-
426
- MobilePushPreviewField.propTypes = {
427
- field: PropTypes.object.isRequired,
428
- renderContext: PropTypes.object.isRequired,
429
- };
430
-
431
- /** Build a registry pre-loaded with the MOBILEPUSH field renderers. */
432
- export const createMpushRegistry = () => createFieldRegistry({
433
- [FIELD_TYPE.INPUT]: MpushInputField,
434
- [FIELD_TYPE.TEXTAREA]: MpushTextAreaField,
435
- [FIELD_TYPE.CHECKBOX]: MpushCheckboxField,
436
- [FIELD_TYPE.BUTTON]: ButtonField,
437
- // TagListField mirrors Classic's SEMANTIC tag-list path (Classic.js:3730-3769):
438
- // span = field.width || '', field.style applied — the CTA splices rely on it.
439
- [FIELD_TYPE.TAG_LIST]: TagListField,
440
- [FIELD_TYPE.RADIO_GROUP]: RadioGroupField,
441
- [FIELD_TYPE.SELECT]: SelectField,
442
- [FIELD_TYPE.UPLOAD]: UploadField,
443
- [FIELD_TYPE.DIV]: MpushDivField,
444
- [FIELD_TYPE.ICON]: IconField,
445
- [FIELD_TYPE.MOBILE_PUSH_PREVIEW]: MobilePushPreviewField,
446
- });
447
-
448
- export default createMpushRegistry;
@@ -1,135 +0,0 @@
1
- /**
2
- * MANUAL perf probe (skipped by default): per-keystroke JS cost, Classic vs
3
- * Functional, over the real embedded Create container. jsdom has no paint, so
4
- * this measures the render+effects side of typing lag as an A/B comparison.
5
- * Run: RUN_PERF=1 TZ=UTC npx jest --config internals/testing/jest.unit.config.js manual.typingPerf
6
- */
7
- import React from 'react';
8
- import '@testing-library/jest-dom';
9
- import _ from 'lodash';
10
- import { Router } from 'react-router-dom';
11
- import { render, act, fireEvent } from '../../../../utils/test-utils';
12
- import history from '../../../../utils/history';
13
- import { response as mpushSchemaResponse } from '../../../../v2Containers/MobilePush/initialSchema';
14
- import { Create } from '../../../../v2Containers/MobilePush/Create';
15
-
16
- jest.mock('redux-auth-wrapper/history4/redirect', () => ({
17
- connectedRouterRedirect: jest.fn(() => (Component) => Component),
18
- }));
19
- jest.mock('../../../../services/api', () => ({
20
- ...jest.requireActual('../../../../services/api'),
21
- getUnsubscribeUrl: () => Promise.resolve({ response: { response: '' } }),
22
- }));
23
-
24
- let mockMpushFlag = false;
25
- jest.mock('../../../../utils/common', () => ({
26
- ...jest.requireActual('../../../../utils/common'),
27
- hasNewFormBuilderEnabledForMpush: () => mockMpushFlag,
28
- }));
29
-
30
- const fullDefinition = () => _.cloneDeep(mpushSchemaResponse.metaEntities[0].definition);
31
- const intl = { formatMessage: (d) => (d && (d.defaultMessage || d.id)) || '', locale: 'en' };
32
- const DEEPLINKS = JSON.stringify([{ name: 'Home Page', link: 'app://home', keys: ['user_id'] }]);
33
- const selectedAccount = {
34
- id: 'acc-1', name: 'Acc', sourceTypeName: 'SOME_SDK', sourceAccountIdentifier: 'lic-1',
35
- configs: { android: '1', ios: '1', deeplink: DEEPLINKS },
36
- };
37
-
38
- const buildProps = () => ({
39
- intl,
40
- params: { mode: 'text' },
41
- route: { name: 'create' },
42
- location: { pathname: '/mobilepush/create/text', query: { module: 'default', type: 'embedded' } },
43
- router: { push: jest.fn() },
44
- isFullMode: false,
45
- isGetFormData: false,
46
- isLoadingMetaEntities: false,
47
- metaEntities: {},
48
- Create: { createTemplateInProgress: false },
49
- Edit: {},
50
- Templates: { selectedWeChatAccount: selectedAccount },
51
- actions: new Proxy({}, { get: (t, n) => { if (!t[n]) t[n] = jest.fn(); return t[n]; } }), // eslint-disable-line no-param-reassign
52
- globalActions: { fetchSchemaForEntity: jest.fn(), addMessageToQueue: jest.fn(), setInjectedTags: jest.fn() },
53
- getFormLibraryData: jest.fn(),
54
- onValidationFail: jest.fn(),
55
- showLiquidErrorInFooter: jest.fn(),
56
- onPersonalizationTokensChange: jest.fn(),
57
- onContentValidityChange: jest.fn(),
58
- getLiquidTags: jest.fn(),
59
- injectedTags: {},
60
- });
61
-
62
- const setNativeValue = (el, value) => {
63
- Object.getOwnPropertyDescriptor(Object.getPrototypeOf(el), 'value').set.call(el, value);
64
- };
65
-
66
- const measureFlow = (flag, { withCta = false, longContent = 0 } = {}) => {
67
- mockMpushFlag = flag;
68
- const props = buildProps();
69
- const ui = (p) => (
70
- <Router history={history}>
71
- <Create {...p} />
72
- </Router>
73
- );
74
- const utils = render(ui(props));
75
- const meta = { layouts: [{ definition: fullDefinition() }], tags: { standard: [] } };
76
- act(() => { utils.rerender(ui({ ...props, metaEntities: meta })); });
77
-
78
- if (withCta) {
79
- const pane = document.querySelectorAll('.ant-tabs-tabpane')[0] || document;
80
- const label = Array.from(pane.querySelectorAll('label')).find((l) => /action link/i.test(l.textContent || ''));
81
- const cb = (label && label.querySelector('input[type="checkbox"]')) || pane.querySelector('input[type="checkbox"]');
82
- act(() => { fireEvent.click(cb); });
83
- }
84
-
85
- const editor = document.getElementById('message-editor');
86
- let text = 'The quick brown fox jumped over the lazy dog and kept typing more content ';
87
- if (longContent) {
88
- // long-message scenario incl. personalization tags — catches superlinear
89
- // per-keystroke validation/render costs that short content hides.
90
- text = 'Hello {{first_name}}, your offer {{offer_id}} is waiting. '.repeat(Math.ceil(longContent / 58));
91
- }
92
- // warm-up keystrokes (JIT/styles)
93
- for (let i = 0; i < 5; i++) {
94
- text += 'w';
95
- setNativeValue(editor, text);
96
- act(() => { fireEvent.change(editor, { target: { value: text } }); });
97
- }
98
- const N = 30;
99
- const t0 = performance.now();
100
- for (let i = 0; i < N; i++) {
101
- text += String.fromCharCode(97 + (i % 26));
102
- setNativeValue(editor, text);
103
- act(() => { fireEvent.change(editor, { target: { value: text } }); });
104
- }
105
- const perKey = (performance.now() - t0) / N;
106
- utils.unmount();
107
- mockMpushFlag = false;
108
- return perKey;
109
- };
110
-
111
- const describePerf = process.env.RUN_PERF ? describe : describe.skip;
112
- describePerf('MANUAL typing perf (RUN_PERF=1 to enable)', () => {
113
- it('per-keystroke JS cost, Classic vs Functional', () => {
114
- const classicPlain = measureFlow(false);
115
- const functionalPlain = measureFlow(true);
116
- const classicCta = measureFlow(false, { withCta: true });
117
- const functionalCta = measureFlow(true, { withCta: true });
118
- const classicLong5k = measureFlow(false, { longContent: 5000 });
119
- const functionalLong5k = measureFlow(true, { longContent: 5000 });
120
- const classicLong20k = measureFlow(false, { longContent: 20000 });
121
- const functionalLong20k = measureFlow(true, { longContent: 20000 });
122
- // eslint-disable-next-line no-console
123
- console.log(JSON.stringify({
124
- classicPlain: `${classicPlain.toFixed(1)} ms/key`,
125
- functionalPlain: `${functionalPlain.toFixed(1)} ms/key`,
126
- classicWithCta: `${classicCta.toFixed(1)} ms/key`,
127
- functionalWithCta: `${functionalCta.toFixed(1)} ms/key`,
128
- classicLong5k: `${classicLong5k.toFixed(1)} ms/key`,
129
- functionalLong5k: `${functionalLong5k.toFixed(1)} ms/key`,
130
- classicLong20k: `${classicLong20k.toFixed(1)} ms/key`,
131
- functionalLong20k: `${functionalLong20k.toFixed(1)} ms/key`,
132
- }, null, 1));
133
- expect(true).toBe(true);
134
- });
135
- });