@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,376 +0,0 @@
1
- /**
2
- * Unit tests for the MPUSH field renderers — each renderer is invoked as a plain
3
- * function and its element tree inspected, exercising the conditional branches
4
- * (error gating, message resolution, value-resolution chains, upload handlers)
5
- * without mounting the heavy subtrees.
6
- */
7
- import {
8
- MpushInputField, MpushTextAreaField, MpushCheckboxField, RadioGroupField,
9
- SelectField, UploadField, MpushDivField, IconField, MobilePushPreviewField,
10
- createMpushRegistry,
11
- } from '../renderers/mpushRenderers';
12
- import { TagListField } from '../renderers/smsRenderers';
13
- import { ERROR_VALUE } from '../channels/mobilepush/config';
14
- import { isAiContentBotDisabled } from '../../../../utils/common';
15
-
16
- jest.mock('../../../../utils/common', () => ({ isAiContentBotDisabled: jest.fn(() => true) }));
17
-
18
- const intl = { formatMessage: (d) => d.defaultMessage || d.id };
19
-
20
- const kids = (el) => {
21
- const c = el.props.children;
22
- return (Array.isArray(c) ? c : [c]).filter(Boolean);
23
- };
24
-
25
- describe('MpushInputField', () => {
26
- const field = { id: 'secondary-cta-0-label', width: 18, errorMessage: 'Label required' };
27
-
28
- it('message resolution: personalization > brace > schema errorMessage; hidden without checkValidation', () => {
29
- const base = { field, onChange: () => {}, renderContext: { checkValidation: true, intl } };
30
- expect(kids(MpushInputField({ ...base, value: '', error: true }))[0].props.errorMessage)
31
- .toBe('Label required');
32
- expect(kids(MpushInputField({ ...base, value: 'a {{b', error: ERROR_VALUE.BRACKET }))[0].props.errorMessage)
33
- .toContain('curly braces');
34
- expect(kids(MpushInputField({
35
- ...base,
36
- value: 'hi {{first_name}}',
37
- error: true,
38
- renderContext: { checkValidation: true, intl, restrictPersonalization: true },
39
- }))[0].props.errorMessage).toContain('Personalization');
40
- expect(kids(MpushInputField({
41
- ...base, value: '', error: true, renderContext: { checkValidation: false, intl },
42
- }))[0].props.errorMessage).toBe('');
43
- });
44
- });
45
-
46
- describe('MpushTextAreaField', () => {
47
- const field = { id: 'message-editor', width: 18, errorMessage: 'Message required' };
48
-
49
- it('inline message shows only under checkValidation; AskAira renders when AI enabled', () => {
50
- const base = { field, onChange: jest.fn(), renderContext: { checkValidation: true, intl } };
51
- expect(kids(MpushTextAreaField({ ...base, value: '', error: true }))[0].props.errorMessage)
52
- .toBe('Message required');
53
- isAiContentBotDisabled.mockReturnValueOnce(false);
54
- const el = MpushTextAreaField({ ...base, value: 'hi', error: false });
55
- const bot = kids(el).find((c) => typeof c.props?.setText === 'function');
56
- expect(bot).toBeTruthy();
57
- bot.props.setText('generated');
58
- expect(base.onChange).toHaveBeenCalledWith('generated');
59
- });
60
-
61
- it('falls back to AI-disabled when the flag reader throws', () => {
62
- isAiContentBotDisabled.mockImplementationOnce(() => { throw new Error('no auth'); });
63
- const el = MpushTextAreaField({
64
- field, value: 'hi', error: false, onChange: () => {}, renderContext: { checkValidation: false, intl },
65
- });
66
- expect(kids(el).length).toBe(1); // textarea only, no bot
67
- });
68
- });
69
-
70
- describe('MpushCheckboxField', () => {
71
- const field = {
72
- id: 'add-pri-cta', width: 18, submitAction: 'addPrimaryCta', errorMessage: 'err',
73
- };
74
-
75
- it('toggle commits the value AND fires the injected submit-action', () => {
76
- const onChange = jest.fn();
77
- const onEvent = jest.fn();
78
- const el = MpushCheckboxField({
79
- field, value: false, error: false, onChange, onEvent, renderContext: {},
80
- });
81
- kids(el)[0].props.onChange({ target: { checked: true } });
82
- expect(onChange).toHaveBeenCalledWith(true);
83
- expect(onEvent).toHaveBeenCalledWith('addPrimaryCta', true);
84
- });
85
-
86
- it('no submit-action => only the value commit; disabled + hoverText wraps in a tooltip', () => {
87
- const onEvent = jest.fn();
88
- const plain = MpushCheckboxField({
89
- field: { id: 'x' }, value: false, error: false, onChange: () => {}, onEvent, renderContext: {},
90
- });
91
- kids(plain)[0].props.onChange({ target: { checked: true } });
92
- expect(onEvent).not.toHaveBeenCalled();
93
-
94
- const wrapped = MpushCheckboxField({
95
- field: { ...field, disabled: true, hoverText: 'not configured' },
96
- value: false,
97
- error: false,
98
- onChange: () => {},
99
- renderContext: {},
100
- });
101
- expect(kids(wrapped)[0].props.title).toBe('not configured');
102
- });
103
- });
104
-
105
- describe('RadioGroupField', () => {
106
- const field = {
107
- id: 'cta-deeplink', width: 18, options: ['Deeplink', 'External Link'], value: 'Deeplink', errorMessage: 'pick one',
108
- };
109
- const build = (legacyFormData, overrides = {}) => RadioGroupField({
110
- field,
111
- error: false,
112
- onChange: () => {},
113
- onEvent: () => {},
114
- renderContext: { legacyFormData, activeTabIndex: 0, ...overrides },
115
- ...overrides,
116
- });
117
-
118
- it('value resolution chain: tab value -> root value -> schema default (Classic handleSetRadioValue)', () => {
119
- expect(kids(build({ 0: { 'cta-deeplink': 'External Link' } }))[0].props.value).toBe('External Link');
120
- expect(kids(build({ 0: {}, 'cta-deeplink': 'External Link' }))[0].props.value).toBe('External Link');
121
- expect(kids(build({ 0: { 'cta-deeplink': '' } }))[0].props.value).toBe('Deeplink'); // '' falls through
122
- });
123
-
124
- it('change commits the value and fires the injected onChange with the commit signature', () => {
125
- const onChange = jest.fn();
126
- const onEvent = jest.fn();
127
- const el = RadioGroupField({
128
- field, error: false, onChange, onEvent, renderContext: { legacyFormData: { 0: {} } },
129
- });
130
- kids(el)[0].props.onChange({ target: { value: 'External Link' } });
131
- expect(onChange).toHaveBeenCalledWith('External Link');
132
- expect(onEvent).toHaveBeenCalledWith('onChange', 'External Link');
133
- });
134
- });
135
-
136
- describe('SelectField', () => {
137
- const field = {
138
- id: 'cta-deeplink-select', width: 18, options: [{ label: 'A', value: 'a' }], errorMessage: 'Select a deeplink',
139
- };
140
-
141
- it('formData `${id}-options` overrides the schema options; selection fires both callbacks', () => {
142
- const onChange = jest.fn();
143
- const onEvent = jest.fn();
144
- const override = [{ label: 'B', value: 'b' }];
145
- const el = SelectField({
146
- field,
147
- value: '',
148
- error: false,
149
- onChange,
150
- onEvent,
151
- renderContext: { legacyFormData: { 'cta-deeplink-select-options': override } },
152
- });
153
- const [select] = kids(el);
154
- expect(select.props.options).toBe(override);
155
- select.props.onSelect('b');
156
- expect(onChange).toHaveBeenCalledWith('b');
157
- expect(onEvent).toHaveBeenCalledWith('onSelect', 'b');
158
- });
159
-
160
- it('error span shows only under checkValidation', () => {
161
- const base = { field, value: '', error: true, onChange: () => {}, onEvent: () => {} };
162
- expect(kids(SelectField({ ...base, renderContext: { checkValidation: true } })).length).toBe(2);
163
- expect(kids(SelectField({ ...base, renderContext: { checkValidation: false } })).length).toBe(1);
164
- });
165
- });
166
-
167
- describe('UploadField', () => {
168
- const field = {
169
- id: 'image-upload', width: 6, label: 'Add image', showPreview: true,
170
- supportedExtensions: '.png,.jpg', submitAction: 'onUpload',
171
- previewProps: { placeholder: 'No image', errorMessage: 'Image required' },
172
- };
173
- // CapColumn > [WithLabel(preview), form > [input, CapButton]]
174
- const parts = (el) => {
175
- const [preview, form] = kids(el);
176
- const [input, button] = kids(form);
177
- return { preview, input, button };
178
- };
179
-
180
- it('error display honors checkValidation OR startValidation (the one Classic exception)', () => {
181
- const base = { field, value: '', error: true, onEvent: () => {} };
182
- expect(parts(UploadField({ ...base, renderContext: { startValidation: true } })).preview.props.ifError).toBe(true);
183
- expect(parts(UploadField({ ...base, renderContext: { checkValidation: true } })).preview.props.ifError).toBe(true);
184
- expect(parts(UploadField({ ...base, renderContext: {} })).preview.props.ifError).toBe(false);
185
- });
186
-
187
- it('file change: no file => no event; wrong extension fires the wrong-file event and CONTINUES (Classic parity)', () => {
188
- const createObjectURL = jest.fn(() => 'blob:x');
189
- window.URL.createObjectURL = createObjectURL;
190
- window.URL.revokeObjectURL = jest.fn();
191
- const onEvent = jest.fn();
192
- const { input } = parts(UploadField({
193
- field, value: '', error: false, onEvent, renderContext: {},
194
- }));
195
-
196
- input.props.onChange({ target: { files: [] } });
197
- expect(onEvent).not.toHaveBeenCalled();
198
-
199
- const file = { name: 'notes.txt', size: 100 };
200
- input.props.onChange({ target: { files: [file], value: null } });
201
- expect(onEvent).toHaveBeenCalledWith('onUpload', { file, type: 'wrong file' });
202
- expect(createObjectURL).toHaveBeenCalledWith(file); // no early return
203
- });
204
-
205
- it('image load reports dimensions and the >5MB error flag, then releases the blob URL', () => {
206
- window.URL.createObjectURL = jest.fn(() => 'blob:x');
207
- const revokeObjectURL = jest.fn();
208
- window.URL.revokeObjectURL = revokeObjectURL;
209
- const images = [];
210
- const RealImage = global.Image;
211
- global.Image = class { constructor() { images.push(this); } };
212
- try {
213
- const onEvent = jest.fn();
214
- const { input } = parts(UploadField({
215
- field, value: '', error: false, onEvent, renderContext: {},
216
- }));
217
- const file = { name: 'big.png', size: 6e6 };
218
- input.props.onChange({ target: { files: [file], value: null } });
219
- const img = images[images.length - 1];
220
- img.width = 300;
221
- img.height = 200;
222
- img.onload();
223
- expect(onEvent).toHaveBeenCalledWith('onUpload', {
224
- file, type: 'image', fileParams: { width: 300, height: 200, error: true },
225
- });
226
- expect(revokeObjectURL).toHaveBeenCalledWith('blob:x');
227
- // failed image load: URL released, no event (Classic dispatches nothing)
228
- input.props.onChange({ target: { files: [file], value: null } });
229
- images[images.length - 1].onerror();
230
- expect(revokeObjectURL).toHaveBeenCalledTimes(2);
231
- } finally {
232
- global.Image = RealImage;
233
- }
234
- });
235
-
236
- it('the add-photo button opens the hidden file input (and no-ops when it is absent)', () => {
237
- const { button } = parts(UploadField({
238
- field, value: '', error: false, onEvent: () => {}, renderContext: {},
239
- }));
240
- const preventDefault = jest.fn();
241
- button.props.onClick({ preventDefault }); // no #image-upload in DOM -> no-op branch
242
- expect(preventDefault).toHaveBeenCalled();
243
-
244
- const host = document.createElement('form');
245
- host.id = 'image-upload';
246
- const fileInput = document.createElement('input');
247
- fileInput.id = 'fileName';
248
- host.appendChild(fileInput);
249
- document.body.appendChild(host);
250
- const click = jest.spyOn(fileInput, 'click');
251
- try {
252
- button.props.onClick({ preventDefault: () => {} });
253
- expect(click).toHaveBeenCalled();
254
- button.props.onClick(); // the no-event guard branch
255
- expect(click).toHaveBeenCalledTimes(2);
256
- } finally {
257
- document.body.removeChild(host);
258
- }
259
- });
260
-
261
- it('renders the image when a value exists, the placeholder otherwise', () => {
262
- const withImage = parts(UploadField({
263
- field, value: 'https://cdn/img.png', error: false, onEvent: () => {}, renderContext: {},
264
- }));
265
- const withoutImage = parts(UploadField({
266
- field, value: '', error: false, onEvent: () => {}, renderContext: {},
267
- }));
268
- expect(withImage.preview).toBeTruthy();
269
- expect(withoutImage.preview).toBeTruthy();
270
- });
271
- });
272
-
273
- describe('MpushDivField', () => {
274
- const field = { id: 'copy-android-content', value: 'Copy content', submitAction: 'onClick' };
275
-
276
- it('value resolves from the ACTIVE tab first; DOM id gets the pane suffix on pane 2', () => {
277
- const el = MpushDivField({
278
- field,
279
- onEvent: () => {},
280
- renderContext: { legacyFormData: { 0: { 'copy-android-content': 'from tab' } }, activeTabIndex: 0, paneTabIndex: 1 },
281
- });
282
- expect(el.props.children).toBe('from tab');
283
- expect(el.props.id).toBe('copy-android-content2');
284
- const androidPane = MpushDivField({
285
- field, onEvent: () => {}, renderContext: { legacyFormData: {}, activeTabIndex: 0, paneTabIndex: 0 },
286
- });
287
- expect(androidPane.props.id).toBe('copy-android-content');
288
- expect(androidPane.props.children).toBe('Copy content'); // schema fallback
289
- });
290
-
291
- it('click fires the submit-action; onInput takes the tail onChange path', () => {
292
- const onEvent = jest.fn();
293
- const el = MpushDivField({ field, onEvent, renderContext: {} });
294
- el.props.onClick('payload');
295
- expect(onEvent).toHaveBeenCalledWith('onClick', 'payload');
296
- el.props.onInput({ stopPropagation: () => {}, target: { textContent: 'typed' } });
297
- expect(onEvent).toHaveBeenCalledWith('onChange', 'typed');
298
- });
299
- });
300
-
301
- describe('IconField', () => {
302
- it('click fires the submit-action; no action => no event', () => {
303
- const onEvent = jest.fn();
304
- IconField({ field: { id: 'cta-deeplink-delete', submitAction: 'onDelete' }, onEvent })
305
- .props.onClick('x');
306
- expect(onEvent).toHaveBeenCalledWith('onDelete', 'x');
307
- IconField({ field: { id: 'plain-icon' }, onEvent: jest.fn() }).props.onClick('x');
308
- expect(onEvent).toHaveBeenCalledTimes(1);
309
- });
310
- });
311
-
312
- describe('tag-list rendering (shared TagListField)', () => {
313
- it('CLASSIC PARITY PIN: column takes field.width and field.style (semantic path, Classic.js:3730-3769)', () => {
314
- // the container-spliced CTA tagLists position "Add label" via style
315
- // (marginRight 24%/10%) — Classic's semantic tag-list case honors both.
316
- const field = {
317
- id: 'title-tagList',
318
- label: 'Add label',
319
- offset: 3,
320
- style: { marginRight: '10%', display: 'flex', justifyContent: 'end', width: '100%' },
321
- };
322
- const el = TagListField({ field, onEvent: jest.fn(), renderContext: {} });
323
- expect(el.props.span).toBe(''); // no width -> '' exactly like Classic
324
- expect(el.props.offset).toBe(3);
325
- expect(el.props.style).toEqual(field.style);
326
- expect(el.props.children.props.label).toBe('Add label');
327
- });
328
-
329
- it('bridges onTagSelect through onEvent', () => {
330
- const onEvent = jest.fn();
331
- const el = TagListField({ field: { id: 'title-tagList' }, onEvent, renderContext: {} });
332
- el.props.children.props.onTagSelect({ definition: { value: 'x' } });
333
- expect(onEvent).toHaveBeenCalledWith('onTagSelect', { definition: { value: 'x' } });
334
- });
335
- });
336
-
337
- describe('MobilePushPreviewField', () => {
338
- const field = {
339
- id: 'mobile-push-preview',
340
- content: {
341
- title: 'message-title', message: 'message-editor', secondaryCta1: 'secondary-cta-0-label', secondaryCta2: 'secondary-cta-1-label', appName: 'My App',
342
- },
343
- };
344
-
345
- it('builds both platform contents from the ACTIVE tab (Classic quirk) with CTA labels', () => {
346
- const el = MobilePushPreviewField({
347
- field,
348
- renderContext: {
349
- intl,
350
- activeTabIndex: 0,
351
- legacyFormData: {
352
- 0: {
353
- 'message-title': 'T', 'message-editor': 'B', image: 'img.png', 'secondary-cta-0-label': 'Buy', 'secondary-cta-1-label': 'Skip',
354
- },
355
- },
356
- },
357
- });
358
- const [preview] = kids(el);
359
- expect(preview.props.content.androidContent).toEqual(preview.props.content.iosContent);
360
- expect(preview.props.content.androidContent.header).toBe('T');
361
- expect(preview.props.content.androidContent.actions).toEqual([{ label: 'Buy' }, { label: 'Skip' }]);
362
- expect(preview.props.showDeviceToggle).toBe(false);
363
- });
364
-
365
- it('returns null when the active tab is absent', () => {
366
- expect(MobilePushPreviewField({ field, renderContext: { legacyFormData: {} } })).toBeNull();
367
- });
368
- });
369
-
370
- describe('createMpushRegistry', () => {
371
- it('resolves every MPUSH field type to a renderer', () => {
372
- const registry = createMpushRegistry();
373
- ['input', 'textarea', 'checkbox', 'radioGroup', 'select', 'upload', 'div', 'icon', 'mobile-push-preview']
374
- .forEach((type) => expect(registry.resolve(type)).toBeTruthy());
375
- });
376
- });
@@ -1,110 +0,0 @@
1
- /**
2
- * Unit tests for TabsContainer — pane/key binding, unsupported-pane skipping,
3
- * locale-independent headers, and the tab-switch bridge.
4
- */
5
- import React from 'react';
6
- import { render, fireEvent } from '@testing-library/react';
7
- import TabsContainer from '../layout/TabsContainer';
8
-
9
- const Stub = () => <span data-testid="stub" />;
10
- const registry = { resolve: () => Stub };
11
-
12
- const buildContext = (overrides = {}) => ({
13
- registry,
14
- state: { root: {}, tabs: [{ fields: {}, tabKey: 'kA' }, { fields: {}, tabKey: 'kI' }] },
15
- errorData: {},
16
- activeTabIndex: 0,
17
- onFieldChange: () => {},
18
- onFieldBlur: () => {},
19
- onEvent: () => {},
20
- onTabSwitch: jest.fn(),
21
- intl: { locale: 'en', formatMessage: (d) => d.defaultMessage || d.id },
22
- ...overrides,
23
- });
24
-
25
- const headerSection = (label) => ({
26
- type: 'col-label',
27
- inputFields: [{ id: `hdr-${label}`, type: 'div', value: label }],
28
- });
29
-
30
- const buildContainer = (overrides = {}) => ({
31
- id: 'pane',
32
- type: 'tabs',
33
- panes: [
34
- { sectionsHeaders: [headerSection('Android')], sections: [] },
35
- { sectionsHeaders: [headerSection('iOS')], sections: [] },
36
- ],
37
- ...overrides,
38
- });
39
-
40
- describe('TabsContainer', () => {
41
- it('renders nothing without panes', () => {
42
- const { container } = render(<TabsContainer container={null} renderContext={buildContext()} />);
43
- expect(container.textContent).toBe('');
44
- const { container: c2 } = render(
45
- <TabsContainer container={{ id: 'pane', type: 'tabs', panes: [] }} renderContext={buildContext()} />,
46
- );
47
- expect(c2.textContent).toBe('');
48
- });
49
-
50
- it('skips isSupported:false panes WITHOUT reserving a tab index (pane->tab binding shifts)', () => {
51
- const container = buildContainer();
52
- container.panes[0].isSupported = false;
53
- const { container: dom } = render(
54
- <TabsContainer container={container} renderContext={buildContext()} />,
55
- );
56
- // only the iOS pane renders, bound to tab 0 (key 'kA' — the shifted binding)
57
- expect(dom.querySelectorAll('.ant-tabs-tab').length).toBe(1);
58
- });
59
-
60
- it('renders the tab-header sections for every locale (Classic has no locale gating)', () => {
61
- const context = buildContext({ intl: { locale: 'ja-JP', formatMessage: (d) => d.id } });
62
- const { queryAllByTestId } = render(
63
- <TabsContainer container={buildContainer()} renderContext={context} />,
64
- );
65
- expect(queryAllByTestId('stub').length).toBeGreaterThan(0);
66
- });
67
-
68
- it('falls back to container.tabContent.sections when a pane has no sectionsHeaders', () => {
69
- const container = buildContainer({
70
- panes: [{ sections: [] }, { sections: [] }],
71
- tabContent: { sections: [headerSection('shared')] },
72
- });
73
- const { getAllByTestId } = render(
74
- <TabsContainer container={container} renderContext={buildContext()} />,
75
- );
76
- expect(getAllByTestId('stub').length).toBeGreaterThan(0);
77
- });
78
-
79
- it('a tab-header click routes through onTabSwitch with the clicked tabKey', () => {
80
- const context = buildContext();
81
- const { container: dom } = render(
82
- <TabsContainer container={buildContainer()} renderContext={context} />,
83
- );
84
- fireEvent.click(dom.querySelectorAll('.ant-tabs-tab')[1]);
85
- expect(context.onTabSwitch).toHaveBeenCalledWith(expect.objectContaining({ id: 'pane' }), 'kI');
86
- });
87
-
88
- it('an out-of-range active index falls back to the first pane key', () => {
89
- const context = buildContext({
90
- activeTabIndex: 5,
91
- state: { root: {}, tabs: [{ fields: {} }, { fields: {} }] },
92
- });
93
- const { container: dom } = render(
94
- <TabsContainer container={buildContainer()} renderContext={context} />,
95
- );
96
- expect(dom.querySelectorAll('.ant-tabs-tab-active').length).toBe(1);
97
- });
98
-
99
- it('key-less tabs fall back to the pane index for both pane keys and the active key', () => {
100
- const context = buildContext({
101
- state: { root: {}, tabs: [{ fields: {} }, { fields: {} }] },
102
- });
103
- const { container: dom } = render(
104
- <TabsContainer container={buildContainer()} renderContext={context} />,
105
- );
106
- // both panes render and one is active — the fallback chains agree
107
- expect(dom.querySelectorAll('.ant-tabs-tab').length).toBe(2);
108
- expect(dom.querySelectorAll('.ant-tabs-tab-active').length).toBe(1);
109
- });
110
- });