@capillarytech/creatives-library 9.0.13-alpha.0 → 9.0.13-alpha.1

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 (60) hide show
  1. package/constants/unified.js +0 -3
  2. package/package.json +1 -1
  3. package/utils/common.js +0 -8
  4. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +132 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +108 -15
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +141 -1
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +132 -0
  8. package/v2Components/CommonTestAndPreview/index.js +244 -26
  9. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +364 -0
  10. package/v2Components/FormBuilder/_formBuilder.scss +0 -8
  11. package/v2Components/FormBuilder/index.js +4479 -41
  12. package/v2Containers/Templates/_templates.scss +83 -0
  13. package/v2Containers/Templates/index.js +90 -10
  14. package/v2Containers/Viber/constants.js +21 -0
  15. package/v2Containers/Viber/index.js +719 -49
  16. package/v2Containers/Viber/index.scss +175 -0
  17. package/v2Containers/Viber/messages.js +121 -0
  18. package/v2Containers/Viber/tests/index.test.js +80 -0
  19. package/v2Components/FormBuilder/Classic.js +0 -4487
  20. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +0 -369
  21. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -17
  22. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +0 -9
  23. package/v2Components/FormBuilder/Functional/channels/sms/config.js +0 -30
  24. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +0 -46
  25. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +0 -13
  26. package/v2Components/FormBuilder/Functional/channels/sms/index.js +0 -22
  27. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +0 -52
  28. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +0 -25
  29. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +0 -87
  30. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +0 -89
  31. package/v2Components/FormBuilder/Functional/constants.js +0 -39
  32. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +0 -38
  33. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +0 -85
  34. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +0 -81
  35. package/v2Components/FormBuilder/Functional/core/store/selectors.js +0 -30
  36. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +0 -91
  37. package/v2Components/FormBuilder/Functional/index.js +0 -26
  38. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +0 -59
  39. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +0 -32
  40. package/v2Components/FormBuilder/Functional/layout/Section.js +0 -118
  41. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +0 -265
  42. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +0 -21
  43. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +0 -65
  44. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +0 -97
  45. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +0 -192
  46. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +0 -129
  47. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +0 -132
  48. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -40
  49. package/v2Components/FormBuilder/Functional/tests/section.test.js +0 -99
  50. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +0 -67
  51. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +0 -155
  52. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +0 -172
  53. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +0 -122
  54. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +0 -329
  55. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +0 -162
  56. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +0 -95
  57. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +0 -114
  58. package/v2Components/FormBuilder/tests/entryGate.test.js +0 -106
  59. package/v2Components/FormBuilder/tests/sms.characterization.test.js +0 -336
  60. package/v2Components/TemplatePreview/coderabbits_comments +0 -171
@@ -1,118 +0,0 @@
1
- /**
2
- * Section — channel-agnostic recursive renderer for the schema's structural
3
- * section types (parent / multicols / col-label). It resolves each leaf field's
4
- * renderer from the registry and feeds it the field's primitive value + error +
5
- * handlers (computed here from the normalized state + the legacy errorData).
6
- */
7
-
8
- import React from 'react';
9
- import PropTypes from 'prop-types';
10
- import CapRow from '@capillarytech/cap-ui-library/CapRow';
11
- import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
12
- import { selectFieldValue } from '../core/store/selectors';
13
- import { MEMOIZED_TYPES, SECTION_TYPE } from '../constants';
14
- import FieldSlot from './FieldSlot';
15
-
16
- const legacyError = (errorData, field, tabIndex) => {
17
- if (!errorData) return undefined;
18
- if (tabIndex == null) return errorData[field?.id];
19
- return errorData[tabIndex]?.[field?.id];
20
- };
21
-
22
- export const renderField = (field, renderContext) => {
23
- if (!field?.type) return null;
24
- const Renderer = renderContext?.registry?.resolve(field.type);
25
- if (!Renderer) return null; // unknown/unsupported type -> nothing (dead branches don't exist here)
26
-
27
- const tabIndex = field.standalone ? null : renderContext?.activeTabIndex;
28
- const value = selectFieldValue(renderContext?.state, { fieldId: field?.id, tabIndex });
29
- const error = legacyError(renderContext?.errorData, field, tabIndex);
30
-
31
- if (MEMOIZED_TYPES.has(field.type)) {
32
- return (
33
- <FieldSlot
34
- key={field.id}
35
- Renderer={Renderer}
36
- field={field}
37
- value={value}
38
- error={error}
39
- resolvedError={renderContext?.resolvedFieldErrors?.[field?.id]}
40
- checkValidation={renderContext?.checkValidation}
41
- channel={renderContext?.channel}
42
- onFieldChange={renderContext?.onFieldChange}
43
- onFieldBlur={renderContext?.onFieldBlur}
44
- onEvent={renderContext?.onEvent}
45
- />
46
- );
47
- }
48
-
49
- return (
50
- <Renderer
51
- key={field.id}
52
- field={field}
53
- value={value}
54
- error={error}
55
- onChange={(newValue) => renderContext.onFieldChange(field, newValue)}
56
- onBlur={() => renderContext.onFieldBlur?.(field)}
57
- onEvent={(eventName, data) => renderContext.onEvent(field, eventName, data)}
58
- renderContext={renderContext}
59
- />
60
- );
61
- };
62
-
63
- const Section = ({ section, renderContext }) => {
64
- if (!section) return null;
65
-
66
- if (section.type === SECTION_TYPE.PARENT) {
67
- return (
68
- <CapRow useLegacy style={section?.rowStyle || {}}>
69
- {section?.childSections?.map((childSection, index) => (
70
- // Schema layout is a frozen, never-reordered descriptor -> index is a stable key.
71
- // eslint-disable-next-line react/no-array-index-key
72
- <CapColumn key={index} span={childSection?.width} offset={childSection?.offset} style={childSection?.colStyle || {}}>
73
- <Section section={childSection} renderContext={renderContext} />
74
- </CapColumn>
75
- ))}
76
- </CapRow>
77
- );
78
- }
79
-
80
- if (section.type === SECTION_TYPE.MULTICOLS) {
81
- return (
82
- <>
83
- {section?.inputFields?.map((row, index) => (
84
- <CapRow useLegacy key={`in-${index}`} className={row?.rowClassName} style={row?.rowStyle || {}}>
85
- {row?.cols?.map((col) => renderField(col, renderContext))}
86
- </CapRow>
87
- ))}
88
- {section?.actionFields?.map((row, index) => (
89
- <CapRow useLegacy key={`act-${index}`} className={row?.rowClassName} style={row?.rowStyle || {}}>
90
- {row?.cols?.map((col) => renderField(col, renderContext))}
91
- </CapRow>
92
- ))}
93
- </>
94
- );
95
- }
96
-
97
- if (section.type === SECTION_TYPE.COL_LABEL) {
98
- return (
99
- <>
100
- {section?.inputFields?.map((field) => renderField(field, renderContext))}
101
- {section?.actionFields?.map((field) => renderField(field, renderContext))}
102
- </>
103
- );
104
- }
105
-
106
- return null;
107
- };
108
-
109
- Section.propTypes = {
110
- section: PropTypes.object,
111
- renderContext: PropTypes.object.isRequired,
112
- };
113
-
114
- Section.defaultProps = {
115
- section: null,
116
- };
117
-
118
- export default Section;
@@ -1,265 +0,0 @@
1
- /**
2
- * SMS field renderers — pure presentational components for the SMS schema field
3
- * types, reusing the cap-ui-library components for visual parity with the class
4
- * component. Prop contract: { field, value, error, resolvedError, onChange, onBlur, onEvent, renderContext }.
5
- */
6
-
7
- import React from 'react';
8
- import PropTypes from 'prop-types';
9
- import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
10
- import CapInput from '@capillarytech/cap-ui-library/CapInput';
11
- import CapCheckbox from '@capillarytech/cap-ui-library/CapCheckbox';
12
- import CapButton from '@capillarytech/cap-ui-library/CapButton';
13
- import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
14
- import UnifiedPreview from '../../../CommonTestAndPreview/UnifiedPreview';
15
- import { ANDROID } from '../../../CommonTestAndPreview/constants';
16
- import TagList from '../../../../v2Containers/TagList';
17
- import { isAiContentBotDisabled } from '../../../../utils/common';
18
- import { SMS } from '../../../../v2Containers/CreativesContainer/constants';
19
- import { EMBEDDED } from '../../../../constants/unified';
20
- import { createFieldRegistry } from '../core/schema/fieldRegistry';
21
- import { FIELD_TYPE } from '../constants';
22
- import { fieldIds, SMS_TAB } from '../channels/sms/config';
23
-
24
- const { TextArea } = CapInput;
25
-
26
- // Shared prop shapes — every renderer receives the frozen field schema and the
27
- // render context; data renderers also get value/error + an onChange handler.
28
- const dataFieldPropTypes = {
29
- field: PropTypes.object.isRequired,
30
- value: PropTypes.any,
31
- error: PropTypes.any,
32
- onChange: PropTypes.func.isRequired,
33
- renderContext: PropTypes.object.isRequired,
34
- };
35
- const dataFieldDefaultProps = {
36
- value: undefined,
37
- error: undefined,
38
- };
39
-
40
- export const InputField = ({
41
- field, value, error, onChange, onBlur, renderContext,
42
- }) => {
43
- // Errors show only after validation is triggered (checkValidation). We drive the
44
- // border via `status` and render the message in our own span rather than passing
45
- // CapInput's errorMessage: toggling its antd suffix remounts the <input> and drops
46
- // focus mid-keystroke.
47
- const showError = Boolean(renderContext?.checkValidation && error);
48
- return (
49
- <CapColumn key={field.id} span={field.width} offset={field.offset} style={field.style || {}}>
50
- <CapInput
51
- id={field.id}
52
- label={field.label}
53
- placeholder={field.placeholder}
54
- status={showError ? 'error' : undefined}
55
- className={`input-primary chart-name-input${showError ? ' error' : ''}`}
56
- value={value || ''}
57
- onChange={(e) => onChange(e.target.value)}
58
- onBlur={onBlur}
59
- disabled={field.disabled}
60
- size={field.size || 'default'}
61
- />
62
- {showError && field.errorMessage ? (
63
- <span className="component-with-label-error-message">{field.errorMessage}</span>
64
- ) : null}
65
- </CapColumn>
66
- );
67
- };
68
-
69
- InputField.propTypes = {
70
- ...dataFieldPropTypes,
71
- onBlur: PropTypes.func,
72
- };
73
- InputField.defaultProps = {
74
- ...dataFieldDefaultProps,
75
- onBlur: undefined,
76
- };
77
-
78
- export const TextAreaField = ({
79
- field, value, error, resolvedError, onChange, renderContext,
80
- }) => {
81
- let aiDisabled = true;
82
- try { aiDisabled = isAiContentBotDisabled(); } catch (e) { aiDisabled = true; }
83
- // Match renderTextAreaContent: an EMPTY/required error shows only after
84
- // validation is triggered; other errors (brace/tag) show live while typing.
85
- const isContentEmpty = !value || !String(value).trim();
86
- const showError = Boolean(error) && (isContentEmpty ? Boolean(renderContext?.checkValidation) : true);
87
- // Inline (below-the-box) message: only the brace error is shown inline, passed in
88
- // as `resolvedError` by the Shell (full mode only — the single intentional
89
- // difference from Classic). Missing-tag/empty/generic body errors stay border-only
90
- // here; the footer carries them.
91
- const inlineMessage = resolvedError || '';
92
- return (
93
- <CapColumn key={field.id} span={field.width} offset={field.offset}>
94
- <TextArea
95
- id={field.id}
96
- label={field.label}
97
- placeholder={field.placeholder || ''}
98
- className={`${showError ? 'error-form-builder' : ''}`}
99
- errorMessage={showError && inlineMessage ? inlineMessage : ''}
100
- autosize={field.autosize ? field.autosizeParams : false}
101
- value={value || ''}
102
- onChange={(e) => onChange(e.target.value)}
103
- style={field.style || {}}
104
- disabled={field.disabled}
105
- />
106
- {renderContext?.channel === SMS && !aiDisabled && (
107
- <CapAskAira.ContentGenerationBot
108
- text={value || ''}
109
- setText={(x) => onChange(x)}
110
- iconPlacement="float-br"
111
- rootStyle={{ bottom: 'calc(1rem + 0.2rem)', right: '0.2rem' }}
112
- />
113
- )}
114
- </CapColumn>
115
- );
116
- };
117
-
118
- TextAreaField.propTypes = {
119
- ...dataFieldPropTypes,
120
- resolvedError: PropTypes.string,
121
- };
122
- TextAreaField.defaultProps = {
123
- ...dataFieldDefaultProps,
124
- resolvedError: '',
125
- };
126
-
127
- export const CheckboxField = ({
128
- field, value, error, onChange, renderContext,
129
- }) => {
130
- const showError = Boolean(renderContext?.checkValidation && error);
131
- return (
132
- <CapColumn key={field.id} span={field.width} offset={field.offset}>
133
- <CapCheckbox
134
- key={field.id}
135
- className={`${showError ? 'error' : ''}`}
136
- errorMessage={field.errorMessage && showError ? field.errorMessage : ''}
137
- onChange={(e) => onChange(e.target.checked)}
138
- checked={Boolean(value)}
139
- style={field.style || {}}
140
- disabled={field.disabled}
141
- inductiveText={field.inductiveText}
142
- >
143
- {field.label}
144
- </CapCheckbox>
145
- </CapColumn>
146
- );
147
- };
148
-
149
- CheckboxField.propTypes = { ...dataFieldPropTypes };
150
- CheckboxField.defaultProps = { ...dataFieldDefaultProps };
151
-
152
- export const ButtonField = ({ field, onEvent }) => {
153
- if (field.metaType !== 'submit-button') return null;
154
- return (
155
- <CapColumn style={field.colStyle || {}} span={field.width} offset={field.offset}>
156
- <CapButton
157
- type={field.buttonType}
158
- icon={field.icon}
159
- disabled={field.disabled || false}
160
- onClick={(data) => onEvent(field.submitAction, data)}
161
- >
162
- {field.value}
163
- </CapButton>
164
- </CapColumn>
165
- );
166
- };
167
-
168
- ButtonField.propTypes = {
169
- field: PropTypes.object.isRequired,
170
- onEvent: PropTypes.func.isRequired,
171
- };
172
-
173
- export const TagListField = ({ field, onEvent, renderContext }) => (
174
- <CapColumn key={`input-${field.id}`} offset={field.offset} span={field.width || ''} style={field.style || { marginBottom: '16px' }}>
175
- <TagList
176
- key={`input-${field.id}`}
177
- moduleFilterEnabled={renderContext?.location?.query?.type !== EMBEDDED}
178
- label={field.label || ''}
179
- onTagSelect={(data) => onEvent('onTagSelect', data)}
180
- onContextChange={renderContext?.onContextChange}
181
- location={renderContext?.location}
182
- tags={renderContext?.tags || []}
183
- injectedTags={renderContext?.injectedTags || {}}
184
- className={field.className || ''}
185
- id={field.id}
186
- userLocale={renderContext?.userLocale}
187
- selectedOfferDetails={renderContext?.selectedOfferDetails}
188
- channel={renderContext?.channel}
189
- eventContextTags={renderContext?.eventContextTags}
190
- restrictPersonalization={renderContext?.restrictPersonalization}
191
- waitEventContextTags={renderContext?.waitEventContextTags}
192
- />
193
- </CapColumn>
194
- );
195
-
196
- TagListField.propTypes = {
197
- field: PropTypes.object.isRequired,
198
- onEvent: PropTypes.func.isRequired,
199
- renderContext: PropTypes.object.isRequired,
200
- };
201
-
202
- export const SmsPreviewField = ({ field, renderContext }) => {
203
- const tab = renderContext?.legacyFormData?.[SMS_TAB] || {};
204
- const content = tab[fieldIds.editor];
205
- const unicodeEnabled = tab[fieldIds.unicode];
206
- return (
207
- <CapColumn key="input" span={23} offset={1}>
208
- <UnifiedPreview
209
- key={field.id}
210
- style={field.customStyling || {}}
211
- channel={field.channel || SMS}
212
- content={content}
213
- device={ANDROID}
214
- showDeviceToggle={false}
215
- showHeader={false}
216
- formatMessage={renderContext?.intl?.formatMessage}
217
- senderId={unicodeEnabled ? 'Unicode' : 'ASCII'}
218
- />
219
- </CapColumn>
220
- );
221
- };
222
-
223
- SmsPreviewField.propTypes = {
224
- field: PropTypes.object.isRequired,
225
- renderContext: PropTypes.object.isRequired,
226
- };
227
-
228
- // sms-count: a display-only div whose text comes from the field value/derived state.
229
- // Matches the class component's renderPrimitiveElement: the field `style`
230
- // (e.g. float:right) goes on the inner <div>; the CapColumn gets `colStyle`
231
- // (not `style`) — applying the float to the column shifts the layout.
232
- export const DivField = ({ field, renderContext }) => {
233
- const tab = renderContext?.legacyFormData?.[SMS_TAB] || {};
234
- const children = tab[field.id] || field.value || '';
235
- return (
236
- <CapColumn key={field.id} span={field.width} offset={field.offset} style={field.colStyle || {}}>
237
- <div
238
- id={field.id}
239
- className={field.className || ''}
240
- ref={renderContext?.refs?.[field.id]}
241
- style={field.style || {}}
242
- >
243
- {children}
244
- </div>
245
- </CapColumn>
246
- );
247
- };
248
-
249
- DivField.propTypes = {
250
- field: PropTypes.object.isRequired,
251
- renderContext: PropTypes.object.isRequired,
252
- };
253
-
254
- /** Build a registry pre-loaded with the SMS field renderers. */
255
- export const createSmsRegistry = () => createFieldRegistry({
256
- [FIELD_TYPE.INPUT]: InputField,
257
- [FIELD_TYPE.TEXTAREA]: TextAreaField,
258
- [FIELD_TYPE.CHECKBOX]: CheckboxField,
259
- [FIELD_TYPE.BUTTON]: ButtonField,
260
- [FIELD_TYPE.TAG_LIST]: TagListField,
261
- [FIELD_TYPE.SMS_PREVIEW]: SmsPreviewField,
262
- [FIELD_TYPE.DIV]: DivField,
263
- });
264
-
265
- export default createSmsRegistry;
@@ -1,21 +0,0 @@
1
- /**
2
- * Unit tests for the channel adapter registry lookup.
3
- */
4
- import { getChannelAdapter } from '../channels/registry';
5
- import smsAdapter from '../channels/sms';
6
-
7
- describe('getChannelAdapter', () => {
8
- it('resolves the SMS adapter (case-insensitive)', () => {
9
- expect(getChannelAdapter('SMS')).toBe(smsAdapter);
10
- expect(getChannelAdapter('sms')).toBe(smsAdapter);
11
- });
12
-
13
- it('returns null for an unknown channel', () => {
14
- expect(getChannelAdapter('EMAIL')).toBeNull();
15
- });
16
-
17
- it('returns null for undefined/empty channel', () => {
18
- expect(getChannelAdapter(undefined)).toBeNull();
19
- expect(getChannelAdapter('')).toBeNull();
20
- });
21
- });
@@ -1,65 +0,0 @@
1
- /**
2
- * Unit tests for the field renderer registry (factory).
3
- */
4
-
5
- import { createFieldRegistry } from '../core/schema/fieldRegistry';
6
-
7
- const Input = () => null;
8
- const TextArea = () => null;
9
- const EmailCk = () => null;
10
-
11
- describe('createFieldRegistry', () => {
12
- it('registers and resolves a renderer by type', () => {
13
- const r = createFieldRegistry();
14
- r.register('input', Input);
15
- expect(r.resolve('input')).toBe(Input);
16
- expect(r.has('input')).toBe(true);
17
- });
18
-
19
- it('returns null for an unregistered type', () => {
20
- const r = createFieldRegistry();
21
- expect(r.resolve('nope')).toBeNull();
22
- expect(r.has('nope')).toBe(false);
23
- });
24
-
25
- it('registerAll adds many at once and is chainable', () => {
26
- const r = createFieldRegistry();
27
- const ret = r.registerAll({ input: Input, textarea: TextArea });
28
- expect(ret).toBe(r);
29
- expect(r.resolve('input')).toBe(Input);
30
- expect(r.resolve('textarea')).toBe(TextArea);
31
- });
32
-
33
- it('registerAll() with no args is a chainable no-op (default {})', () => {
34
- const r = createFieldRegistry();
35
- expect(r.registerAll()).toBe(r);
36
- });
37
-
38
- it('register() is chainable', () => {
39
- const r = createFieldRegistry();
40
- expect(r.register('input', Input)).toBe(r);
41
- });
42
-
43
- it('accepts an initial map in the factory', () => {
44
- const r = createFieldRegistry({ input: Input });
45
- expect(r.resolve('input')).toBe(Input);
46
- });
47
-
48
- it('extend() shadows the base registry without mutating it (channel overrides)', () => {
49
- const base = createFieldRegistry({ input: Input, ckeditor: TextArea });
50
- const emailScoped = base.extend({ ckeditor: EmailCk });
51
-
52
- // override applies only to the extended registry
53
- expect(emailScoped.resolve('ckeditor')).toBe(EmailCk);
54
- expect(emailScoped.resolve('input')).toBe(Input); // inherited
55
- // base is untouched
56
- expect(base.resolve('ckeditor')).toBe(TextArea);
57
- });
58
-
59
- it('register on the child does not leak into the parent', () => {
60
- const base = createFieldRegistry({ input: Input });
61
- const child = base.extend();
62
- child.register('textarea', TextArea);
63
- expect(base.has('textarea')).toBe(false);
64
- });
65
- });
@@ -1,97 +0,0 @@
1
- /**
2
- * FieldSlot memo-boundary tests. Proves the perf contract: a field re-renders
3
- * only when its own primitive props (value/error/checkValidation) change, not
4
- * when an unrelated re-render happens or a sibling field changes.
5
- */
6
- import React from 'react';
7
- import { render } from '@testing-library/react';
8
- import FieldSlot from '../layout/FieldSlot';
9
-
10
- const field = { id: 'template-name', type: 'input', width: 18 };
11
- const noop = () => {};
12
-
13
- const renderSlot = (overrides = {}) => {
14
- const Renderer = jest.fn(() => <div>field</div>);
15
- const props = {
16
- Renderer,
17
- field,
18
- value: 'a',
19
- error: false,
20
- checkValidation: false,
21
- channel: 'SMS',
22
- onFieldChange: noop,
23
- onFieldBlur: noop,
24
- onEvent: noop,
25
- ...overrides,
26
- };
27
- const utils = render(<FieldSlot {...props} />);
28
- return { Renderer, props, rerender: (next) => utils.rerender(<FieldSlot {...{ ...props, ...next }} />) };
29
- };
30
-
31
- describe('FieldSlot memoization', () => {
32
- it('does NOT re-render the field when props are unchanged (stable handlers)', () => {
33
- const { Renderer, rerender } = renderSlot();
34
- expect(Renderer).toHaveBeenCalledTimes(1);
35
- rerender({}); // identical props -> memo bails out
36
- expect(Renderer).toHaveBeenCalledTimes(1);
37
- });
38
-
39
- it('re-renders when its own value changes', () => {
40
- const { Renderer, rerender } = renderSlot();
41
- rerender({ value: 'ab' });
42
- expect(Renderer).toHaveBeenCalledTimes(2);
43
- });
44
-
45
- it('re-renders when its error changes', () => {
46
- const { Renderer, rerender } = renderSlot();
47
- rerender({ error: 'tagBracketCountMismatchError' });
48
- expect(Renderer).toHaveBeenCalledTimes(2);
49
- });
50
-
51
- it('re-renders when checkValidation flips (error reveal on Save)', () => {
52
- const { Renderer, rerender } = renderSlot();
53
- rerender({ checkValidation: true });
54
- expect(Renderer).toHaveBeenCalledTimes(2);
55
- });
56
-
57
- it('passes a minimal renderContext ({ checkValidation, channel }) and wires onChange to its field', () => {
58
- const onFieldChange = jest.fn();
59
- let captured;
60
- const Renderer = jest.fn((p) => { captured = p; return <div>f</div>; });
61
- render(
62
- <FieldSlot
63
- Renderer={Renderer}
64
- field={field}
65
- value="a"
66
- error={false}
67
- checkValidation
68
- channel="SMS"
69
- onFieldChange={onFieldChange}
70
- onFieldBlur={noop}
71
- onEvent={noop}
72
- />,
73
- );
74
- expect(captured.renderContext).toEqual({ checkValidation: true, channel: 'SMS' });
75
- captured.onChange('hello');
76
- expect(onFieldChange).toHaveBeenCalledWith(field, 'hello');
77
- });
78
-
79
- it('wires onBlur and onEvent to the field handlers', () => {
80
- const onFieldBlur = jest.fn();
81
- const onEvent = jest.fn();
82
- let captured;
83
- const Renderer = jest.fn((p) => { captured = p; return <div>f</div>; });
84
- render(<FieldSlot Renderer={Renderer} field={field} onFieldChange={noop} onFieldBlur={onFieldBlur} onEvent={onEvent} />);
85
- captured.onBlur();
86
- captured.onEvent('onTagSelect', { x: 1 });
87
- expect(onFieldBlur).toHaveBeenCalledWith(field);
88
- expect(onEvent).toHaveBeenCalledWith(field, 'onTagSelect', { x: 1 });
89
- });
90
-
91
- it('onBlur / onEvent are safe no-ops when those handlers are omitted (optional-chaining branch)', () => {
92
- let captured;
93
- const Renderer = jest.fn((p) => { captured = p; return <div>f</div>; });
94
- render(<FieldSlot Renderer={Renderer} field={field} onFieldChange={noop} />);
95
- expect(() => { captured.onBlur(); captured.onEvent('e', {}); }).not.toThrow();
96
- });
97
- });