@capillarytech/creatives-library 9.0.13-beta.0 → 9.0.13

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 (55) 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/ErrorInfoNote/index.js +1 -1
  5. package/v2Components/ErrorInfoNote/style.scss +3 -0
  6. package/v2Components/FormBuilder/_formBuilder.scss +0 -5
  7. package/v2Components/FormBuilder/index.js +4479 -41
  8. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +216 -96
  9. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +37 -17
  10. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +77 -37
  11. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +530 -250
  12. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +3 -3
  13. package/v2Containers/Templates/_templates.scss +1 -1
  14. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +2645 -11346
  15. package/v2Components/FormBuilder/Classic.js +0 -4487
  16. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +0 -369
  17. package/v2Components/FormBuilder/Functional/channels/registry.js +0 -17
  18. package/v2Components/FormBuilder/Functional/channels/sms/buildSubmitPayload.js +0 -9
  19. package/v2Components/FormBuilder/Functional/channels/sms/config.js +0 -30
  20. package/v2Components/FormBuilder/Functional/channels/sms/getEditorErrorDescriptor.js +0 -46
  21. package/v2Components/FormBuilder/Functional/channels/sms/getLiquidContent.js +0 -13
  22. package/v2Components/FormBuilder/Functional/channels/sms/index.js +0 -22
  23. package/v2Components/FormBuilder/Functional/channels/sms/tests/getEditorErrorDescriptor.test.js +0 -52
  24. package/v2Components/FormBuilder/Functional/channels/sms/tests/getLiquidContent.test.js +0 -25
  25. package/v2Components/FormBuilder/Functional/channels/sms/tests/validate.test.js +0 -87
  26. package/v2Components/FormBuilder/Functional/channels/sms/validate.js +0 -89
  27. package/v2Components/FormBuilder/Functional/constants.js +0 -42
  28. package/v2Components/FormBuilder/Functional/core/schema/fieldRegistry.js +0 -38
  29. package/v2Components/FormBuilder/Functional/core/schema/initializeFormState.js +0 -85
  30. package/v2Components/FormBuilder/Functional/core/store/formReducer.js +0 -81
  31. package/v2Components/FormBuilder/Functional/core/store/selectors.js +0 -30
  32. package/v2Components/FormBuilder/Functional/core/store/toLegacyFormData.js +0 -91
  33. package/v2Components/FormBuilder/Functional/index.js +0 -26
  34. package/v2Components/FormBuilder/Functional/layout/FieldSlot.js +0 -59
  35. package/v2Components/FormBuilder/Functional/layout/SchemaForm.js +0 -32
  36. package/v2Components/FormBuilder/Functional/layout/Section.js +0 -116
  37. package/v2Components/FormBuilder/Functional/renderers/smsRenderers.js +0 -258
  38. package/v2Components/FormBuilder/Functional/tests/channelRegistry.test.js +0 -21
  39. package/v2Components/FormBuilder/Functional/tests/fieldRegistry.test.js +0 -65
  40. package/v2Components/FormBuilder/Functional/tests/fieldSlot.test.js +0 -97
  41. package/v2Components/FormBuilder/Functional/tests/fixtures/smsParityCases.js +0 -192
  42. package/v2Components/FormBuilder/Functional/tests/formReducer.test.js +0 -129
  43. package/v2Components/FormBuilder/Functional/tests/initializeFormState.test.js +0 -132
  44. package/v2Components/FormBuilder/Functional/tests/schemaForm.test.js +0 -40
  45. package/v2Components/FormBuilder/Functional/tests/section.test.js +0 -99
  46. package/v2Components/FormBuilder/Functional/tests/selectors.test.js +0 -67
  47. package/v2Components/FormBuilder/Functional/tests/sms.crossFlowParity.test.js +0 -155
  48. package/v2Components/FormBuilder/Functional/tests/sms.liquid.test.js +0 -172
  49. package/v2Components/FormBuilder/Functional/tests/sms.rollout.test.js +0 -122
  50. package/v2Components/FormBuilder/Functional/tests/sms.shell.parity.test.js +0 -329
  51. package/v2Components/FormBuilder/Functional/tests/smsRenderers.test.js +0 -160
  52. package/v2Components/FormBuilder/Functional/tests/toLegacyFormData.test.js +0 -95
  53. package/v2Components/FormBuilder/tests/__snapshots__/sms.characterization.test.js.snap +0 -114
  54. package/v2Components/FormBuilder/tests/entryGate.test.js +0 -106
  55. package/v2Components/FormBuilder/tests/sms.characterization.test.js +0 -336
@@ -1,116 +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
- <CapColumn key={`${childSection}-${index}`} span={childSection?.width} offset={childSection?.offset} style={childSection?.colStyle || {}}>
71
- <Section section={childSection} renderContext={renderContext} />
72
- </CapColumn>
73
- ))}
74
- </CapRow>
75
- );
76
- }
77
-
78
- if (section.type === SECTION_TYPE.MULTICOLS) {
79
- return (
80
- <>
81
- {section?.inputFields?.map((row, index) => (
82
- <CapRow useLegacy key={`in-${index}`} className={row?.rowClassName} style={row?.rowStyle || {}}>
83
- {row?.cols?.map((col) => renderField(col, renderContext))}
84
- </CapRow>
85
- ))}
86
- {section?.actionFields?.map((row, index) => (
87
- <CapRow useLegacy key={`act-${index}`} className={row?.rowClassName} style={row?.rowStyle || {}}>
88
- {row?.cols?.map((col) => renderField(col, renderContext))}
89
- </CapRow>
90
- ))}
91
- </>
92
- );
93
- }
94
-
95
- if (section.type === SECTION_TYPE.COL_LABEL) {
96
- return (
97
- <>
98
- {section?.inputFields?.map((field) => renderField(field, renderContext))}
99
- {section?.actionFields?.map((field) => renderField(field, renderContext))}
100
- </>
101
- );
102
- }
103
-
104
- return null;
105
- };
106
-
107
- Section.propTypes = {
108
- section: PropTypes.object,
109
- renderContext: PropTypes.object.isRequired,
110
- };
111
-
112
- Section.defaultProps = {
113
- section: null,
114
- };
115
-
116
- export default Section;
@@ -1,258 +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 { ASCII, FIELD_TYPE, UNICODE } 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
- const showError = Boolean(renderContext?.checkValidation && error);
44
- return (
45
- <CapColumn key={field.id} span={field.width} offset={field.offset} style={field.style || {}}>
46
- <CapInput
47
- id={field.id}
48
- label={field.label}
49
- placeholder={field.placeholder}
50
- errorMessage={showError && field.errorMessage ? field.errorMessage : ''}
51
- className={`input-primary chart-name-input${showError ? ' error' : ''}`}
52
- value={value || ''}
53
- onChange={(e) => onChange(e.target.value)}
54
- onBlur={onBlur}
55
- disabled={field.disabled}
56
- size={field.size || 'default'}
57
- />
58
- </CapColumn>
59
- );
60
- };
61
-
62
- InputField.propTypes = {
63
- ...dataFieldPropTypes,
64
- onBlur: PropTypes.func,
65
- };
66
- InputField.defaultProps = {
67
- ...dataFieldDefaultProps,
68
- onBlur: undefined,
69
- };
70
-
71
- export const TextAreaField = ({
72
- field, value, error, resolvedError, onChange, renderContext,
73
- }) => {
74
- let aiDisabled = true;
75
- try { aiDisabled = isAiContentBotDisabled(); } catch (e) { aiDisabled = true; }
76
- // Match renderTextAreaContent: an EMPTY/required error shows only after
77
- // validation is triggered; other errors (brace/tag) show live while typing.
78
- const isContentEmpty = !value || !String(value).trim();
79
- const showError = Boolean(error) && (isContentEmpty ? Boolean(renderContext?.checkValidation) : true);
80
- // Inline (below-the-box) message: only the brace error is shown inline, passed in
81
- // as `resolvedError` by the Shell (full mode only — the single intentional
82
- // difference from Classic). Missing-tag/empty/generic body errors stay border-only
83
- // here; the footer carries them.
84
- const inlineMessage = resolvedError || '';
85
- return (
86
- <CapColumn key={field.id} span={field.width} offset={field.offset}>
87
- <TextArea
88
- id={field.id}
89
- label={field.label}
90
- placeholder={field.placeholder || ''}
91
- className={`${showError ? 'error-form-builder' : ''}`}
92
- errorMessage={showError && inlineMessage ? inlineMessage : ''}
93
- autosize={field.autosize ? field.autosizeParams : false}
94
- value={value || ''}
95
- onChange={(e) => onChange(e.target.value)}
96
- style={field.style || {}}
97
- disabled={field.disabled}
98
- />
99
- {renderContext?.channel === SMS && !aiDisabled && (
100
- <CapAskAira.ContentGenerationBot
101
- text={value || ''}
102
- setText={(x) => onChange(x)}
103
- iconPlacement="float-br"
104
- rootStyle={{ bottom: 'calc(1rem + 0.2rem)', right: '0.2rem' }}
105
- />
106
- )}
107
- </CapColumn>
108
- );
109
- };
110
-
111
- TextAreaField.propTypes = {
112
- ...dataFieldPropTypes,
113
- resolvedError: PropTypes.string,
114
- };
115
- TextAreaField.defaultProps = {
116
- ...dataFieldDefaultProps,
117
- resolvedError: '',
118
- };
119
-
120
- export const CheckboxField = ({
121
- field, value, error, onChange, renderContext,
122
- }) => {
123
- const showError = Boolean(renderContext?.checkValidation && error);
124
- return (
125
- <CapColumn key={field.id} span={field.width} offset={field.offset}>
126
- <CapCheckbox
127
- key={field.id}
128
- className={`${showError ? 'error' : ''}`}
129
- errorMessage={field.errorMessage && showError ? field.errorMessage : ''}
130
- onChange={(e) => onChange(e.target.checked)}
131
- checked={Boolean(value)}
132
- style={field.style || {}}
133
- disabled={field.disabled}
134
- inductiveText={field.inductiveText}
135
- >
136
- {field.label}
137
- </CapCheckbox>
138
- </CapColumn>
139
- );
140
- };
141
-
142
- CheckboxField.propTypes = { ...dataFieldPropTypes };
143
- CheckboxField.defaultProps = { ...dataFieldDefaultProps };
144
-
145
- export const ButtonField = ({ field, onEvent }) => {
146
- if (field.metaType !== 'submit-button') return null;
147
- return (
148
- <CapColumn style={field.colStyle || {}} span={field.width} offset={field.offset}>
149
- <CapButton
150
- type={field.buttonType}
151
- icon={field.icon}
152
- disabled={field.disabled || false}
153
- onClick={(data) => onEvent(field.submitAction, data)}
154
- >
155
- {field.value}
156
- </CapButton>
157
- </CapColumn>
158
- );
159
- };
160
-
161
- ButtonField.propTypes = {
162
- field: PropTypes.object.isRequired,
163
- onEvent: PropTypes.func.isRequired,
164
- };
165
-
166
- export const TagListField = ({ field, onEvent, renderContext }) => (
167
- <CapColumn key={`input-${field.id}`} offset={field.offset} span={field.width || ''} style={field.style || { marginBottom: '16px' }}>
168
- <TagList
169
- key={`input-${field.id}`}
170
- moduleFilterEnabled={renderContext?.location?.query?.type !== EMBEDDED}
171
- label={field.label || ''}
172
- onTagSelect={(data) => onEvent('onTagSelect', data)}
173
- onContextChange={renderContext?.onContextChange}
174
- location={renderContext?.location}
175
- tags={renderContext?.tags || []}
176
- injectedTags={renderContext?.injectedTags || {}}
177
- className={field.className || ''}
178
- id={field.id}
179
- userLocale={renderContext?.userLocale}
180
- selectedOfferDetails={renderContext?.selectedOfferDetails}
181
- channel={renderContext?.channel}
182
- eventContextTags={renderContext?.eventContextTags}
183
- restrictPersonalization={renderContext?.restrictPersonalization}
184
- waitEventContextTags={renderContext?.waitEventContextTags}
185
- />
186
- </CapColumn>
187
- );
188
-
189
- TagListField.propTypes = {
190
- field: PropTypes.object.isRequired,
191
- onEvent: PropTypes.func.isRequired,
192
- renderContext: PropTypes.object.isRequired,
193
- };
194
-
195
- export const SmsPreviewField = ({ field, renderContext }) => {
196
- const tab = renderContext?.legacyFormData?.[SMS_TAB] || {};
197
- const content = tab[fieldIds.editor];
198
- const unicodeEnabled = tab[fieldIds.unicode];
199
- return (
200
- <CapColumn key="input" span={23} offset={1}>
201
- <UnifiedPreview
202
- key={field.id}
203
- style={field.customStyling || {}}
204
- channel={field.channel || SMS}
205
- content={content}
206
- device={ANDROID}
207
- showDeviceToggle={false}
208
- showHeader={false}
209
- formatMessage={renderContext?.intl?.formatMessage}
210
- senderId={unicodeEnabled ? UNICODE : ASCII}
211
- />
212
- </CapColumn>
213
- );
214
- };
215
-
216
- SmsPreviewField.propTypes = {
217
- field: PropTypes.object.isRequired,
218
- renderContext: PropTypes.object.isRequired,
219
- };
220
-
221
- // sms-count: a display-only div whose text comes from the field value/derived state.
222
- // Matches the class component's renderPrimitiveElement: the field `style`
223
- // (e.g. float:right) goes on the inner <div>; the CapColumn gets `colStyle`
224
- // (not `style`) — applying the float to the column shifts the layout.
225
- export const DivField = ({ field, renderContext }) => {
226
- const tab = renderContext?.legacyFormData?.[SMS_TAB] || {};
227
- const children = tab[field.id] || field.value || '';
228
- return (
229
- <CapColumn key={field.id} span={field.width} offset={field.offset} style={field.colStyle || {}}>
230
- <div
231
- id={field.id}
232
- className={field.className || ''}
233
- ref={renderContext?.refs?.[field.id]}
234
- style={field.style || {}}
235
- >
236
- {children}
237
- </div>
238
- </CapColumn>
239
- );
240
- };
241
-
242
- DivField.propTypes = {
243
- field: PropTypes.object.isRequired,
244
- renderContext: PropTypes.object.isRequired,
245
- };
246
-
247
- /** Build a registry pre-loaded with the SMS field renderers. */
248
- export const createSmsRegistry = () => createFieldRegistry({
249
- [FIELD_TYPE.INPUT]: InputField,
250
- [FIELD_TYPE.TEXTAREA]: TextAreaField,
251
- [FIELD_TYPE.CHECKBOX]: CheckboxField,
252
- [FIELD_TYPE.BUTTON]: ButtonField,
253
- [FIELD_TYPE.TAG_LIST]: TagListField,
254
- [FIELD_TYPE.SMS_PREVIEW]: SmsPreviewField,
255
- [FIELD_TYPE.DIV]: DivField,
256
- });
257
-
258
- 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
- });