@eeacms/volto-editing-progress 0.4.0 → 2.0.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.
- package/.eslintrc.js +65 -0
- package/CHANGELOG.md +21 -0
- package/jest-addon.config.js +19 -4
- package/jest.setup.js +65 -0
- package/locales/de/LC_MESSAGES/volto.po +3 -3
- package/locales/en/LC_MESSAGES/volto.po +3 -3
- package/locales/it/LC_MESSAGES/volto.po +3 -3
- package/locales/ro/LC_MESSAGES/volto.po +3 -3
- package/locales/volto.pot +5 -5
- package/package.json +2 -1
- package/src/VisualJSONWidget.jsx +89 -12
- package/src/VisualWidget.test.js +1459 -5
- package/src/WidgetDataComponent.jsx +97 -0
- package/src/less/editor.less +10 -1
- package/.project.eslintrc.js +0 -48
package/src/VisualWidget.test.js
CHANGED
|
@@ -2,12 +2,22 @@ import { makeFirstLetterCapital } from './WidgetDataComponent';
|
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { Provider } from 'react-intl-redux';
|
|
4
4
|
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
-
import
|
|
5
|
+
import { render, screen, fireEvent, act } from '@testing-library/react';
|
|
6
6
|
import configureStore from 'redux-mock-store';
|
|
7
|
+
import SidebarComponent, { backgroundColor } from './WidgetSidebar';
|
|
8
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
9
|
+
|
|
7
10
|
import VisualJSONWidget from './VisualJSONWidget';
|
|
8
|
-
import
|
|
11
|
+
import EditDataComponent from './WidgetDataComponent';
|
|
12
|
+
import ScrollIntoView from './ScrollIntoView';
|
|
13
|
+
import TextareaJSONWidget from './TextareaJSONWidget';
|
|
14
|
+
import { getEditingProgress, getRawContent } from './actions';
|
|
15
|
+
import { editingProgress, rawdata } from './reducers';
|
|
16
|
+
import { JSONSchema } from './schema';
|
|
17
|
+
|
|
9
18
|
const mockStore = configureStore();
|
|
10
19
|
const propsEmpty = {};
|
|
20
|
+
|
|
11
21
|
describe('Widget Data Component', () => {
|
|
12
22
|
it('should make first letter capital', () => {
|
|
13
23
|
const testString = 'this is a test string';
|
|
@@ -16,6 +26,7 @@ describe('Widget Data Component', () => {
|
|
|
16
26
|
);
|
|
17
27
|
});
|
|
18
28
|
});
|
|
29
|
+
|
|
19
30
|
describe('Widget Sidebar', () => {
|
|
20
31
|
it('should return a background lightblue', () => {
|
|
21
32
|
expect(backgroundColor(true, false)).toEqual('lightblue');
|
|
@@ -24,7 +35,111 @@ describe('Widget Sidebar', () => {
|
|
|
24
35
|
it('should return a background lightpink', () => {
|
|
25
36
|
expect(backgroundColor(false, true)).toEqual('lightpink');
|
|
26
37
|
});
|
|
38
|
+
it('should return undefined when no conditions match', () => {
|
|
39
|
+
expect(backgroundColor(false, false)).toEqual(undefined);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('renders SidebarComponent and filters types on input change', () => {
|
|
43
|
+
const types = {
|
|
44
|
+
loaded: true,
|
|
45
|
+
loading: false,
|
|
46
|
+
types: [
|
|
47
|
+
{ id: 'document', title: 'Document' },
|
|
48
|
+
{ id: 'news-item', title: 'News Item' },
|
|
49
|
+
{ id: 'event', title: 'Event' },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const handleChange = jest.fn();
|
|
53
|
+
|
|
54
|
+
render(
|
|
55
|
+
<SidebarComponent
|
|
56
|
+
types={types}
|
|
57
|
+
currentContentType={{ id: 'document', title: 'Document' }}
|
|
58
|
+
handleChangeSelectedContentType={handleChange}
|
|
59
|
+
value={{}}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// All types should be visible initially
|
|
64
|
+
expect(screen.getByText('Document')).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText('News Item')).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText('Event')).toBeInTheDocument();
|
|
67
|
+
|
|
68
|
+
// Type in search input to filter
|
|
69
|
+
const input = document.querySelector('input[placeholder="Search... "]');
|
|
70
|
+
fireEvent.change(input, { target: { value: 'News' } });
|
|
71
|
+
|
|
72
|
+
// Only News Item should match
|
|
73
|
+
expect(screen.getByText('News Item')).toBeInTheDocument();
|
|
74
|
+
expect(screen.queryByText('Event')).not.toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('handles click on list item', () => {
|
|
78
|
+
const types = {
|
|
79
|
+
loaded: true,
|
|
80
|
+
loading: false,
|
|
81
|
+
types: [{ id: 'document', title: 'Document' }],
|
|
82
|
+
};
|
|
83
|
+
const handleChange = jest.fn();
|
|
84
|
+
|
|
85
|
+
render(
|
|
86
|
+
<SidebarComponent
|
|
87
|
+
types={types}
|
|
88
|
+
currentContentType={null}
|
|
89
|
+
handleChangeSelectedContentType={handleChange}
|
|
90
|
+
value={{ document: [] }}
|
|
91
|
+
/>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
fireEvent.click(screen.getByText('Document'));
|
|
95
|
+
expect(handleChange).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles null input value', () => {
|
|
99
|
+
const types = {
|
|
100
|
+
loaded: true,
|
|
101
|
+
loading: false,
|
|
102
|
+
types: [{ id: 'document', title: 'Document' }],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
render(
|
|
106
|
+
<SidebarComponent
|
|
107
|
+
types={types}
|
|
108
|
+
currentContentType={null}
|
|
109
|
+
handleChangeSelectedContentType={jest.fn()}
|
|
110
|
+
value={{}}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const input = document.querySelector('input[placeholder="Search... "]');
|
|
115
|
+
fireEvent.change(input, { target: { value: null } });
|
|
116
|
+
|
|
117
|
+
// Should still show all types
|
|
118
|
+
expect(screen.getByText('Document')).toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('handles types not loaded', () => {
|
|
122
|
+
const types = {
|
|
123
|
+
loaded: false,
|
|
124
|
+
loading: true,
|
|
125
|
+
types: [],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
render(
|
|
129
|
+
<SidebarComponent
|
|
130
|
+
types={types}
|
|
131
|
+
currentContentType={null}
|
|
132
|
+
handleChangeSelectedContentType={jest.fn()}
|
|
133
|
+
value={{}}
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(
|
|
138
|
+
document.querySelector('input[placeholder="Search... "]'),
|
|
139
|
+
).toBeInTheDocument();
|
|
140
|
+
});
|
|
27
141
|
});
|
|
142
|
+
|
|
28
143
|
describe('Visual widget', () => {
|
|
29
144
|
it('renders the VisualJSONWidget component without breaking if props and progressEditing are empty', () => {
|
|
30
145
|
const store = mockStore({
|
|
@@ -33,19 +148,1358 @@ describe('Visual widget', () => {
|
|
|
33
148
|
messages: {},
|
|
34
149
|
},
|
|
35
150
|
progressEditing: {},
|
|
151
|
+
rawdata: {},
|
|
152
|
+
types: {
|
|
153
|
+
loaded: true,
|
|
154
|
+
loading: false,
|
|
155
|
+
types: [
|
|
156
|
+
{
|
|
157
|
+
id: 'content-type-1',
|
|
158
|
+
title: 'Content Type 1',
|
|
159
|
+
'@id': '/content-type-1',
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
},
|
|
36
163
|
});
|
|
37
|
-
const
|
|
164
|
+
const { container } = render(
|
|
38
165
|
<Provider store={store}>
|
|
39
166
|
<MemoryRouter>
|
|
40
167
|
<VisualJSONWidget
|
|
41
168
|
pathname="/test"
|
|
42
169
|
{...propsEmpty}
|
|
43
170
|
hasToolbar={true}
|
|
171
|
+
value={{}}
|
|
172
|
+
onChange={jest.fn()}
|
|
173
|
+
/>
|
|
174
|
+
</MemoryRouter>
|
|
175
|
+
</Provider>,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(screen.getByText('Edit JSON')).toBeInTheDocument();
|
|
179
|
+
expect(screen.getByText('Add Property')).toBeInTheDocument();
|
|
180
|
+
expect(
|
|
181
|
+
container.querySelector('input[placeholder="Search... "]'),
|
|
182
|
+
).toBeInTheDocument();
|
|
183
|
+
expect(screen.getByText('Content Type 1')).toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('renders the VisualJSONWidget with enforceCharLimits option in dropdown', () => {
|
|
187
|
+
const store = mockStore({
|
|
188
|
+
intl: {
|
|
189
|
+
locale: 'en',
|
|
190
|
+
messages: {},
|
|
191
|
+
},
|
|
192
|
+
progressEditing: {},
|
|
193
|
+
rawdata: {
|
|
194
|
+
'/content-type-1': {
|
|
195
|
+
loaded: true,
|
|
196
|
+
loading: false,
|
|
197
|
+
data: {
|
|
198
|
+
fieldsets: [{ fields: ['title', 'description'] }],
|
|
199
|
+
required: ['title'],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
types: {
|
|
204
|
+
loaded: true,
|
|
205
|
+
loading: false,
|
|
206
|
+
types: [
|
|
207
|
+
{
|
|
208
|
+
id: 'content-type-1',
|
|
209
|
+
title: 'Content Type 1',
|
|
210
|
+
'@id': '/content-type-1',
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
render(
|
|
216
|
+
<Provider store={store}>
|
|
217
|
+
<MemoryRouter>
|
|
218
|
+
<VisualJSONWidget
|
|
219
|
+
pathname="/test"
|
|
220
|
+
hasToolbar={true}
|
|
221
|
+
value={{}}
|
|
222
|
+
onChange={jest.fn()}
|
|
223
|
+
/>
|
|
224
|
+
</MemoryRouter>
|
|
225
|
+
</Provider>,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
expect(screen.getByText('Add Property')).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('handles enforceCharLimits rule in value', () => {
|
|
232
|
+
const store = mockStore({
|
|
233
|
+
intl: {
|
|
234
|
+
locale: 'en',
|
|
235
|
+
messages: {},
|
|
236
|
+
},
|
|
237
|
+
progressEditing: {},
|
|
238
|
+
rawdata: {
|
|
239
|
+
'/content-type-1': {
|
|
240
|
+
loaded: true,
|
|
241
|
+
loading: false,
|
|
242
|
+
data: {
|
|
243
|
+
fieldsets: [{ fields: ['title', 'description'] }],
|
|
244
|
+
required: ['title'],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
types: {
|
|
249
|
+
loaded: true,
|
|
250
|
+
loading: false,
|
|
251
|
+
types: [
|
|
252
|
+
{
|
|
253
|
+
id: 'content-type-1',
|
|
254
|
+
title: 'Content Type 1',
|
|
255
|
+
'@id': '/content-type-1',
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
const valueWithCharLimits = {
|
|
261
|
+
'content-type-1': [
|
|
262
|
+
{
|
|
263
|
+
type: 'enforceCharLimits',
|
|
264
|
+
states: ['all'],
|
|
265
|
+
linkLabel: 'Fix {title}',
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
render(
|
|
270
|
+
<Provider store={store}>
|
|
271
|
+
<MemoryRouter>
|
|
272
|
+
<VisualJSONWidget
|
|
273
|
+
pathname="/test"
|
|
274
|
+
hasToolbar={true}
|
|
275
|
+
value={valueWithCharLimits}
|
|
276
|
+
onChange={jest.fn()}
|
|
277
|
+
/>
|
|
278
|
+
</MemoryRouter>
|
|
279
|
+
</Provider>,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
expect(screen.getByText('Add Property')).toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('adds enforceCharLimits when dropdown option is selected', () => {
|
|
286
|
+
const mockOnChange = jest.fn();
|
|
287
|
+
const store = mockStore({
|
|
288
|
+
intl: {
|
|
289
|
+
locale: 'en',
|
|
290
|
+
messages: {},
|
|
291
|
+
},
|
|
292
|
+
progressEditing: {},
|
|
293
|
+
rawdata: {
|
|
294
|
+
'/content-type-1': {
|
|
295
|
+
loaded: true,
|
|
296
|
+
loading: false,
|
|
297
|
+
data: {
|
|
298
|
+
fieldsets: [{ fields: ['description'] }],
|
|
299
|
+
required: [],
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
types: {
|
|
304
|
+
loaded: true,
|
|
305
|
+
loading: false,
|
|
306
|
+
types: [
|
|
307
|
+
{
|
|
308
|
+
id: 'content-type-1',
|
|
309
|
+
title: 'Content Type 1',
|
|
310
|
+
'@id': '/content-type-1',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
render(
|
|
316
|
+
<Provider store={store}>
|
|
317
|
+
<MemoryRouter>
|
|
318
|
+
<VisualJSONWidget
|
|
319
|
+
pathname="/test"
|
|
320
|
+
hasToolbar={true}
|
|
321
|
+
value={{}}
|
|
322
|
+
onChange={mockOnChange}
|
|
323
|
+
/>
|
|
324
|
+
</MemoryRouter>
|
|
325
|
+
</Provider>,
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
// Click on dropdown to see options
|
|
329
|
+
const dropdown = screen.getByText('Add Property');
|
|
330
|
+
fireEvent.click(dropdown);
|
|
331
|
+
|
|
332
|
+
expect(screen.getByText('Add Property')).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('opens JSON editor modal when Edit JSON button is clicked', () => {
|
|
336
|
+
const store = mockStore({
|
|
337
|
+
intl: {
|
|
338
|
+
locale: 'en',
|
|
339
|
+
messages: {},
|
|
340
|
+
},
|
|
341
|
+
progressEditing: {},
|
|
342
|
+
rawdata: {},
|
|
343
|
+
types: {
|
|
344
|
+
loaded: true,
|
|
345
|
+
loading: false,
|
|
346
|
+
types: [
|
|
347
|
+
{
|
|
348
|
+
id: 'content-type-1',
|
|
349
|
+
title: 'Content Type 1',
|
|
350
|
+
'@id': '/content-type-1',
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
render(
|
|
356
|
+
<Provider store={store}>
|
|
357
|
+
<MemoryRouter>
|
|
358
|
+
<VisualJSONWidget
|
|
359
|
+
pathname="/test"
|
|
360
|
+
hasToolbar={true}
|
|
361
|
+
value={{}}
|
|
362
|
+
onChange={jest.fn()}
|
|
363
|
+
/>
|
|
364
|
+
</MemoryRouter>
|
|
365
|
+
</Provider>,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const editJsonButton = screen.getByText('Edit JSON');
|
|
369
|
+
fireEvent.click(editJsonButton);
|
|
370
|
+
|
|
371
|
+
// Modal should be visible after clicking
|
|
372
|
+
expect(editJsonButton).toBeInTheDocument();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('handles types not loaded state', () => {
|
|
376
|
+
const store = mockStore({
|
|
377
|
+
intl: {
|
|
378
|
+
locale: 'en',
|
|
379
|
+
messages: {},
|
|
380
|
+
},
|
|
381
|
+
progressEditing: {},
|
|
382
|
+
rawdata: {},
|
|
383
|
+
types: {
|
|
384
|
+
loaded: false,
|
|
385
|
+
loading: true,
|
|
386
|
+
types: [],
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
render(
|
|
390
|
+
<Provider store={store}>
|
|
391
|
+
<MemoryRouter>
|
|
392
|
+
<VisualJSONWidget
|
|
393
|
+
pathname="/test"
|
|
394
|
+
hasToolbar={true}
|
|
395
|
+
value={{}}
|
|
396
|
+
onChange={jest.fn()}
|
|
397
|
+
/>
|
|
398
|
+
</MemoryRouter>
|
|
399
|
+
</Provider>,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(screen.getByText('Edit JSON')).toBeInTheDocument();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('updates existing field rule when dropdown changes for existing field', () => {
|
|
406
|
+
const mockOnChange = jest.fn();
|
|
407
|
+
const existingValue = {
|
|
408
|
+
'content-type-1': [
|
|
409
|
+
{
|
|
410
|
+
prefix: 'description',
|
|
411
|
+
states: ['all'],
|
|
412
|
+
linkLabel: 'Add description',
|
|
413
|
+
condition: 'python:value',
|
|
414
|
+
link: 'edit#description',
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
const store = mockStore({
|
|
419
|
+
intl: {
|
|
420
|
+
locale: 'en',
|
|
421
|
+
messages: {},
|
|
422
|
+
},
|
|
423
|
+
progressEditing: {},
|
|
424
|
+
rawdata: {
|
|
425
|
+
'/content-type-1': {
|
|
426
|
+
loaded: true,
|
|
427
|
+
loading: false,
|
|
428
|
+
data: {
|
|
429
|
+
fieldsets: [{ fields: ['title', 'description'] }],
|
|
430
|
+
required: ['title'],
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
434
|
+
loaded: true,
|
|
435
|
+
loading: false,
|
|
436
|
+
data: {
|
|
437
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
types: {
|
|
442
|
+
loaded: true,
|
|
443
|
+
loading: false,
|
|
444
|
+
types: [
|
|
445
|
+
{
|
|
446
|
+
id: 'content-type-1',
|
|
447
|
+
title: 'Content Type 1',
|
|
448
|
+
'@id': '/content-type-1',
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
render(
|
|
454
|
+
<Provider store={store}>
|
|
455
|
+
<MemoryRouter>
|
|
456
|
+
<VisualJSONWidget
|
|
457
|
+
pathname="/test"
|
|
458
|
+
hasToolbar={true}
|
|
459
|
+
value={existingValue}
|
|
460
|
+
onChange={mockOnChange}
|
|
461
|
+
/>
|
|
462
|
+
</MemoryRouter>
|
|
463
|
+
</Provider>,
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// The description field should be visible
|
|
467
|
+
expect(screen.getByText('description')).toBeInTheDocument();
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('handles field with multiple rules where prefix does not match', () => {
|
|
471
|
+
const mockOnChange = jest.fn();
|
|
472
|
+
const existingValue = {
|
|
473
|
+
'content-type-1': [
|
|
474
|
+
{
|
|
475
|
+
prefix: 'other_field',
|
|
476
|
+
states: ['all'],
|
|
477
|
+
linkLabel: 'Other',
|
|
478
|
+
condition: 'python:value',
|
|
479
|
+
link: 'edit#other',
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
prefix: 'description',
|
|
483
|
+
states: ['published'],
|
|
484
|
+
linkLabel: 'Add description',
|
|
485
|
+
condition: 'python:value',
|
|
486
|
+
link: 'edit#description',
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
};
|
|
490
|
+
const store = mockStore({
|
|
491
|
+
intl: {
|
|
492
|
+
locale: 'en',
|
|
493
|
+
messages: {},
|
|
494
|
+
},
|
|
495
|
+
progressEditing: {},
|
|
496
|
+
rawdata: {
|
|
497
|
+
'/content-type-1': {
|
|
498
|
+
loaded: true,
|
|
499
|
+
loading: false,
|
|
500
|
+
data: {
|
|
501
|
+
fieldsets: [{ fields: ['title', 'description', 'other_field'] }],
|
|
502
|
+
required: ['title'],
|
|
503
|
+
},
|
|
504
|
+
},
|
|
505
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
506
|
+
loaded: true,
|
|
507
|
+
loading: false,
|
|
508
|
+
data: {
|
|
509
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
510
|
+
},
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
types: {
|
|
514
|
+
loaded: true,
|
|
515
|
+
loading: false,
|
|
516
|
+
types: [
|
|
517
|
+
{
|
|
518
|
+
id: 'content-type-1',
|
|
519
|
+
title: 'Content Type 1',
|
|
520
|
+
'@id': '/content-type-1',
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
render(
|
|
526
|
+
<Provider store={store}>
|
|
527
|
+
<MemoryRouter>
|
|
528
|
+
<VisualJSONWidget
|
|
529
|
+
pathname="/test"
|
|
530
|
+
hasToolbar={true}
|
|
531
|
+
value={existingValue}
|
|
532
|
+
onChange={mockOnChange}
|
|
533
|
+
/>
|
|
534
|
+
</MemoryRouter>
|
|
535
|
+
</Provider>,
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// Both fields should be visible
|
|
539
|
+
expect(screen.getByText('description')).toBeInTheDocument();
|
|
540
|
+
expect(screen.getByText('other_field')).toBeInTheDocument();
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
describe('EditDataComponent with enforceCharLimits', () => {
|
|
545
|
+
it('renders enforceCharLimits section when rule exists', () => {
|
|
546
|
+
const store = mockStore({
|
|
547
|
+
intl: {
|
|
548
|
+
locale: 'en',
|
|
549
|
+
messages: {},
|
|
550
|
+
},
|
|
551
|
+
rawdata: {
|
|
552
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
553
|
+
loaded: true,
|
|
554
|
+
loading: false,
|
|
555
|
+
data: {
|
|
556
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
557
|
+
},
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const value = {
|
|
563
|
+
'content-type-1': [
|
|
564
|
+
{
|
|
565
|
+
type: 'enforceCharLimits',
|
|
566
|
+
states: ['all'],
|
|
567
|
+
linkLabel: 'Fix {title}',
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
render(
|
|
573
|
+
<Provider store={store}>
|
|
574
|
+
<MemoryRouter>
|
|
575
|
+
<EditDataComponent
|
|
576
|
+
request={{
|
|
577
|
+
loaded: true,
|
|
578
|
+
loading: false,
|
|
579
|
+
data: { fieldsets: [{ fields: [] }], required: [] },
|
|
580
|
+
}}
|
|
581
|
+
handleOnDropdownChange={jest.fn()}
|
|
582
|
+
currentContentType={{
|
|
583
|
+
id: 'content-type-1',
|
|
584
|
+
title: 'Content Type 1',
|
|
585
|
+
}}
|
|
586
|
+
value={value}
|
|
587
|
+
fields={[]}
|
|
588
|
+
getDropdownValues={jest.fn()}
|
|
589
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
590
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
591
|
+
/>
|
|
592
|
+
</MemoryRouter>
|
|
593
|
+
</Provider>,
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
expect(screen.getByText('Enforce character limits')).toBeInTheDocument();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('does not render enforceCharLimits section when rule does not exist', () => {
|
|
600
|
+
const store = mockStore({
|
|
601
|
+
intl: {
|
|
602
|
+
locale: 'en',
|
|
603
|
+
messages: {},
|
|
604
|
+
},
|
|
605
|
+
rawdata: {
|
|
606
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
607
|
+
loaded: true,
|
|
608
|
+
loading: false,
|
|
609
|
+
data: {
|
|
610
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
render(
|
|
617
|
+
<Provider store={store}>
|
|
618
|
+
<MemoryRouter>
|
|
619
|
+
<EditDataComponent
|
|
620
|
+
request={{
|
|
621
|
+
loaded: true,
|
|
622
|
+
loading: false,
|
|
623
|
+
data: { fieldsets: [{ fields: [] }], required: [] },
|
|
624
|
+
}}
|
|
625
|
+
handleOnDropdownChange={jest.fn()}
|
|
626
|
+
currentContentType={{
|
|
627
|
+
id: 'content-type-1',
|
|
628
|
+
title: 'Content Type 1',
|
|
629
|
+
}}
|
|
630
|
+
value={{}}
|
|
631
|
+
fields={[]}
|
|
632
|
+
getDropdownValues={jest.fn()}
|
|
633
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
634
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
635
|
+
/>
|
|
636
|
+
</MemoryRouter>
|
|
637
|
+
</Provider>,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
expect(
|
|
641
|
+
screen.queryByText('Enforce character limits'),
|
|
642
|
+
).not.toBeInTheDocument();
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('calls handleRemoveEnforceCharLimits when cancel icon is clicked', () => {
|
|
646
|
+
const store = mockStore({
|
|
647
|
+
intl: {
|
|
648
|
+
locale: 'en',
|
|
649
|
+
messages: {},
|
|
650
|
+
},
|
|
651
|
+
rawdata: {
|
|
652
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
653
|
+
loaded: true,
|
|
654
|
+
loading: false,
|
|
655
|
+
data: {
|
|
656
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
const value = {
|
|
663
|
+
'content-type-1': [
|
|
664
|
+
{
|
|
665
|
+
type: 'enforceCharLimits',
|
|
666
|
+
states: ['all'],
|
|
667
|
+
linkLabel: 'Fix {title}',
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const handleRemove = jest.fn();
|
|
673
|
+
|
|
674
|
+
render(
|
|
675
|
+
<Provider store={store}>
|
|
676
|
+
<MemoryRouter>
|
|
677
|
+
<EditDataComponent
|
|
678
|
+
request={{
|
|
679
|
+
loaded: true,
|
|
680
|
+
loading: false,
|
|
681
|
+
data: { fieldsets: [{ fields: [] }], required: [] },
|
|
682
|
+
}}
|
|
683
|
+
handleOnDropdownChange={jest.fn()}
|
|
684
|
+
currentContentType={{
|
|
685
|
+
id: 'content-type-1',
|
|
686
|
+
title: 'Content Type 1',
|
|
687
|
+
}}
|
|
688
|
+
value={value}
|
|
689
|
+
fields={[]}
|
|
690
|
+
getDropdownValues={jest.fn()}
|
|
691
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
692
|
+
handleRemoveEnforceCharLimits={handleRemove}
|
|
693
|
+
/>
|
|
694
|
+
</MemoryRouter>
|
|
695
|
+
</Provider>,
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const cancelIcon = screen
|
|
699
|
+
.getByText('Enforce character limits')
|
|
700
|
+
.parentElement.parentElement.querySelector('.cancel');
|
|
701
|
+
fireEvent.click(cancelIcon);
|
|
702
|
+
expect(handleRemove).toHaveBeenCalled();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('calls handleUpdateEnforceCharLimits when linkLabel input changes', () => {
|
|
706
|
+
const store = mockStore({
|
|
707
|
+
intl: {
|
|
708
|
+
locale: 'en',
|
|
709
|
+
messages: {},
|
|
710
|
+
},
|
|
711
|
+
rawdata: {
|
|
712
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
713
|
+
loaded: true,
|
|
714
|
+
loading: false,
|
|
715
|
+
data: {
|
|
716
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
const value = {
|
|
723
|
+
'content-type-1': [
|
|
724
|
+
{
|
|
725
|
+
type: 'enforceCharLimits',
|
|
726
|
+
states: ['all'],
|
|
727
|
+
linkLabel: 'Fix {title}',
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const handleUpdate = jest.fn();
|
|
733
|
+
|
|
734
|
+
render(
|
|
735
|
+
<Provider store={store}>
|
|
736
|
+
<MemoryRouter>
|
|
737
|
+
<EditDataComponent
|
|
738
|
+
request={{
|
|
739
|
+
loaded: true,
|
|
740
|
+
loading: false,
|
|
741
|
+
data: { fieldsets: [{ fields: [] }], required: [] },
|
|
742
|
+
}}
|
|
743
|
+
handleOnDropdownChange={jest.fn()}
|
|
744
|
+
currentContentType={{
|
|
745
|
+
id: 'content-type-1',
|
|
746
|
+
title: 'Content Type 1',
|
|
747
|
+
}}
|
|
748
|
+
value={value}
|
|
749
|
+
fields={[]}
|
|
750
|
+
getDropdownValues={jest.fn()}
|
|
751
|
+
handleUpdateEnforceCharLimits={handleUpdate}
|
|
752
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
753
|
+
/>
|
|
754
|
+
</MemoryRouter>
|
|
755
|
+
</Provider>,
|
|
756
|
+
);
|
|
757
|
+
|
|
758
|
+
// Click on accordion to expand it
|
|
759
|
+
fireEvent.click(screen.getByText('Enforce character limits'));
|
|
760
|
+
|
|
761
|
+
// Find and change the input
|
|
762
|
+
const input = screen.getByPlaceholderText('Fix {title}');
|
|
763
|
+
fireEvent.change(input, { target: { value: 'New label' } });
|
|
764
|
+
|
|
765
|
+
expect(handleUpdate).toHaveBeenCalledWith('linkLabel', 'New label');
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('renders with empty currentContentType', () => {
|
|
769
|
+
const store = mockStore({
|
|
770
|
+
intl: {
|
|
771
|
+
locale: 'en',
|
|
772
|
+
messages: {},
|
|
773
|
+
},
|
|
774
|
+
rawdata: {
|
|
775
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
776
|
+
loaded: true,
|
|
777
|
+
loading: false,
|
|
778
|
+
data: {
|
|
779
|
+
items: [],
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
render(
|
|
786
|
+
<Provider store={store}>
|
|
787
|
+
<MemoryRouter>
|
|
788
|
+
<EditDataComponent
|
|
789
|
+
request={{
|
|
790
|
+
loaded: true,
|
|
791
|
+
loading: false,
|
|
792
|
+
data: { fieldsets: [{ fields: [] }], required: [] },
|
|
793
|
+
}}
|
|
794
|
+
handleOnDropdownChange={jest.fn()}
|
|
795
|
+
currentContentType={null}
|
|
796
|
+
value={{}}
|
|
797
|
+
fields={[]}
|
|
798
|
+
getDropdownValues={jest.fn()}
|
|
799
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
800
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
801
|
+
/>
|
|
802
|
+
</MemoryRouter>
|
|
803
|
+
</Provider>,
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
expect(
|
|
807
|
+
screen.queryByText('Enforce character limits'),
|
|
808
|
+
).not.toBeInTheDocument();
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('renders with fields and handles field accordion', () => {
|
|
812
|
+
const store = mockStore({
|
|
813
|
+
intl: {
|
|
814
|
+
locale: 'en',
|
|
815
|
+
messages: {},
|
|
816
|
+
},
|
|
817
|
+
rawdata: {
|
|
818
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
819
|
+
loaded: true,
|
|
820
|
+
loading: false,
|
|
821
|
+
data: {
|
|
822
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
const value = {
|
|
829
|
+
'content-type-1': [
|
|
830
|
+
{
|
|
831
|
+
prefix: 'description',
|
|
832
|
+
states: ['all'],
|
|
833
|
+
linkLabel: 'Add description',
|
|
834
|
+
condition: 'python:value',
|
|
835
|
+
link: 'edit#description',
|
|
836
|
+
},
|
|
837
|
+
],
|
|
838
|
+
};
|
|
839
|
+
|
|
840
|
+
render(
|
|
841
|
+
<Provider store={store}>
|
|
842
|
+
<MemoryRouter>
|
|
843
|
+
<EditDataComponent
|
|
844
|
+
request={{
|
|
845
|
+
loaded: true,
|
|
846
|
+
loading: false,
|
|
847
|
+
data: {
|
|
848
|
+
fieldsets: [{ fields: ['title', 'description'] }],
|
|
849
|
+
required: ['title'],
|
|
850
|
+
},
|
|
851
|
+
}}
|
|
852
|
+
handleOnDropdownChange={jest.fn()}
|
|
853
|
+
currentContentType={{
|
|
854
|
+
id: 'content-type-1',
|
|
855
|
+
title: 'Content Type 1',
|
|
856
|
+
}}
|
|
857
|
+
value={value}
|
|
858
|
+
fields={['title', 'description']}
|
|
859
|
+
getDropdownValues={(field) =>
|
|
860
|
+
field === 'description' ? ['All'] : undefined
|
|
861
|
+
}
|
|
862
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
863
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
864
|
+
/>
|
|
865
|
+
</MemoryRouter>
|
|
866
|
+
</Provider>,
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
expect(screen.getByText('description')).toBeInTheDocument();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('handles request loading state', () => {
|
|
873
|
+
const store = mockStore({
|
|
874
|
+
intl: {
|
|
875
|
+
locale: 'en',
|
|
876
|
+
messages: {},
|
|
877
|
+
},
|
|
878
|
+
rawdata: {
|
|
879
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
880
|
+
loaded: false,
|
|
881
|
+
loading: true,
|
|
882
|
+
data: null,
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
render(
|
|
888
|
+
<Provider store={store}>
|
|
889
|
+
<MemoryRouter>
|
|
890
|
+
<EditDataComponent
|
|
891
|
+
request={{ loaded: false, loading: true, data: null }}
|
|
892
|
+
handleOnDropdownChange={jest.fn()}
|
|
893
|
+
currentContentType={{
|
|
894
|
+
id: 'content-type-1',
|
|
895
|
+
title: 'Content Type 1',
|
|
896
|
+
}}
|
|
897
|
+
value={{}}
|
|
898
|
+
fields={[]}
|
|
899
|
+
getDropdownValues={jest.fn()}
|
|
900
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
901
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
902
|
+
/>
|
|
903
|
+
</MemoryRouter>
|
|
904
|
+
</Provider>,
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
expect(
|
|
908
|
+
screen.queryByText('Enforce character limits'),
|
|
909
|
+
).not.toBeInTheDocument();
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
it('handles message input change for field', () => {
|
|
913
|
+
const mockHandleOnDropdownChange = jest.fn();
|
|
914
|
+
const store = mockStore({
|
|
915
|
+
intl: {
|
|
916
|
+
locale: 'en',
|
|
917
|
+
messages: {},
|
|
918
|
+
},
|
|
919
|
+
rawdata: {
|
|
920
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
921
|
+
loaded: true,
|
|
922
|
+
loading: false,
|
|
923
|
+
data: {
|
|
924
|
+
items: [{ token: 'published' }, { token: 'private' }],
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const value = {
|
|
931
|
+
'content-type-1': [
|
|
932
|
+
{
|
|
933
|
+
prefix: 'description',
|
|
934
|
+
states: ['all'],
|
|
935
|
+
linkLabel: 'Add description',
|
|
936
|
+
condition: 'python:value',
|
|
937
|
+
link: 'edit#description',
|
|
938
|
+
},
|
|
939
|
+
],
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
render(
|
|
943
|
+
<Provider store={store}>
|
|
944
|
+
<MemoryRouter>
|
|
945
|
+
<EditDataComponent
|
|
946
|
+
request={{
|
|
947
|
+
loaded: true,
|
|
948
|
+
loading: false,
|
|
949
|
+
data: {
|
|
950
|
+
fieldsets: [{ fields: ['description'] }],
|
|
951
|
+
required: [],
|
|
952
|
+
},
|
|
953
|
+
}}
|
|
954
|
+
handleOnDropdownChange={mockHandleOnDropdownChange}
|
|
955
|
+
currentContentType={{
|
|
956
|
+
id: 'content-type-1',
|
|
957
|
+
title: 'Content Type 1',
|
|
958
|
+
}}
|
|
959
|
+
value={value}
|
|
960
|
+
fields={['description']}
|
|
961
|
+
getDropdownValues={(field) =>
|
|
962
|
+
field === 'description' ? ['All'] : undefined
|
|
963
|
+
}
|
|
964
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
965
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
966
|
+
/>
|
|
967
|
+
</MemoryRouter>
|
|
968
|
+
</Provider>,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Click on field accordion to expand
|
|
972
|
+
fireEvent.click(screen.getByText('description'));
|
|
973
|
+
|
|
974
|
+
// Change message input
|
|
975
|
+
const messageInput = document.querySelector('input[name="message"]');
|
|
976
|
+
fireEvent.change(messageInput, { target: { value: 'New message' } });
|
|
977
|
+
|
|
978
|
+
expect(mockHandleOnDropdownChange).toHaveBeenCalled();
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('handles link input change for field', () => {
|
|
982
|
+
const mockHandleOnDropdownChange = jest.fn();
|
|
983
|
+
const store = mockStore({
|
|
984
|
+
intl: {
|
|
985
|
+
locale: 'en',
|
|
986
|
+
messages: {},
|
|
987
|
+
},
|
|
988
|
+
rawdata: {
|
|
989
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
990
|
+
loaded: true,
|
|
991
|
+
loading: false,
|
|
992
|
+
data: {
|
|
993
|
+
items: [{ token: 'published' }],
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
const value = {
|
|
1000
|
+
'content-type-1': [
|
|
1001
|
+
{
|
|
1002
|
+
prefix: 'description',
|
|
1003
|
+
states: ['all'],
|
|
1004
|
+
linkLabel: 'Add',
|
|
1005
|
+
condition: 'python:value',
|
|
1006
|
+
link: 'edit#desc',
|
|
1007
|
+
},
|
|
1008
|
+
],
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
render(
|
|
1012
|
+
<Provider store={store}>
|
|
1013
|
+
<MemoryRouter>
|
|
1014
|
+
<EditDataComponent
|
|
1015
|
+
request={{
|
|
1016
|
+
loaded: true,
|
|
1017
|
+
loading: false,
|
|
1018
|
+
data: {
|
|
1019
|
+
fieldsets: [{ fields: ['description'] }],
|
|
1020
|
+
required: [],
|
|
1021
|
+
},
|
|
1022
|
+
}}
|
|
1023
|
+
handleOnDropdownChange={mockHandleOnDropdownChange}
|
|
1024
|
+
currentContentType={{
|
|
1025
|
+
id: 'content-type-1',
|
|
1026
|
+
title: 'Content Type 1',
|
|
1027
|
+
}}
|
|
1028
|
+
value={value}
|
|
1029
|
+
fields={['description']}
|
|
1030
|
+
getDropdownValues={(field) =>
|
|
1031
|
+
field === 'description' ? ['All'] : undefined
|
|
1032
|
+
}
|
|
1033
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
1034
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
1035
|
+
/>
|
|
1036
|
+
</MemoryRouter>
|
|
1037
|
+
</Provider>,
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
// Click on field accordion to expand
|
|
1041
|
+
fireEvent.click(screen.getByText('description'));
|
|
1042
|
+
|
|
1043
|
+
// Change link input
|
|
1044
|
+
const linkInput = document.querySelector('input[name="link"]');
|
|
1045
|
+
fireEvent.change(linkInput, { target: { value: 'new-link' } });
|
|
1046
|
+
|
|
1047
|
+
expect(mockHandleOnDropdownChange).toHaveBeenCalled();
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it('handles condition input change for field', () => {
|
|
1051
|
+
const mockHandleOnDropdownChange = jest.fn();
|
|
1052
|
+
const store = mockStore({
|
|
1053
|
+
intl: {
|
|
1054
|
+
locale: 'en',
|
|
1055
|
+
messages: {},
|
|
1056
|
+
},
|
|
1057
|
+
rawdata: {
|
|
1058
|
+
'/@vocabularies/plone.app.vocabularies.WorkflowStates': {
|
|
1059
|
+
loaded: true,
|
|
1060
|
+
loading: false,
|
|
1061
|
+
data: {
|
|
1062
|
+
items: [{ token: 'published' }],
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
const value = {
|
|
1069
|
+
'content-type-1': [
|
|
1070
|
+
{
|
|
1071
|
+
prefix: 'description',
|
|
1072
|
+
states: ['all'],
|
|
1073
|
+
linkLabel: 'Add',
|
|
1074
|
+
condition: 'python:value',
|
|
1075
|
+
link: 'edit#desc',
|
|
1076
|
+
},
|
|
1077
|
+
],
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
render(
|
|
1081
|
+
<Provider store={store}>
|
|
1082
|
+
<MemoryRouter>
|
|
1083
|
+
<EditDataComponent
|
|
1084
|
+
request={{
|
|
1085
|
+
loaded: true,
|
|
1086
|
+
loading: false,
|
|
1087
|
+
data: {
|
|
1088
|
+
fieldsets: [{ fields: ['description'] }],
|
|
1089
|
+
required: [],
|
|
1090
|
+
},
|
|
1091
|
+
}}
|
|
1092
|
+
handleOnDropdownChange={mockHandleOnDropdownChange}
|
|
1093
|
+
currentContentType={{
|
|
1094
|
+
id: 'content-type-1',
|
|
1095
|
+
title: 'Content Type 1',
|
|
1096
|
+
}}
|
|
1097
|
+
value={value}
|
|
1098
|
+
fields={['description']}
|
|
1099
|
+
getDropdownValues={(field) =>
|
|
1100
|
+
field === 'description' ? ['All'] : undefined
|
|
1101
|
+
}
|
|
1102
|
+
handleUpdateEnforceCharLimits={jest.fn()}
|
|
1103
|
+
handleRemoveEnforceCharLimits={jest.fn()}
|
|
44
1104
|
/>
|
|
45
1105
|
</MemoryRouter>
|
|
46
1106
|
</Provider>,
|
|
47
1107
|
);
|
|
48
|
-
|
|
49
|
-
|
|
1108
|
+
|
|
1109
|
+
// Click on field accordion to expand
|
|
1110
|
+
fireEvent.click(screen.getByText('description'));
|
|
1111
|
+
|
|
1112
|
+
// Change condition input
|
|
1113
|
+
const conditionInput = document.querySelector('input[name="condition"]');
|
|
1114
|
+
fireEvent.change(conditionInput, { target: { value: 'python:True' } });
|
|
1115
|
+
|
|
1116
|
+
expect(mockHandleOnDropdownChange).toHaveBeenCalled();
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
describe('ScrollIntoView', () => {
|
|
1121
|
+
beforeEach(() => {
|
|
1122
|
+
jest.useFakeTimers();
|
|
1123
|
+
global.__CLIENT__ = true;
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
afterEach(() => {
|
|
1127
|
+
jest.useRealTimers();
|
|
1128
|
+
delete global.__CLIENT__;
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
it('renders null', () => {
|
|
1132
|
+
const { container } = render(
|
|
1133
|
+
<ScrollIntoView location={{ hash: '', pathname: '/test' }} />,
|
|
1134
|
+
);
|
|
1135
|
+
expect(container.firstChild).toBeNull();
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
it('does nothing when no hash', () => {
|
|
1139
|
+
const { container } = render(
|
|
1140
|
+
<ScrollIntoView location={{ hash: '', pathname: '/test' }} />,
|
|
1141
|
+
);
|
|
1142
|
+
expect(container.firstChild).toBeNull();
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it('scrolls to element when hash is present', () => {
|
|
1146
|
+
const mockElement = document.createElement('div');
|
|
1147
|
+
mockElement.id = 'test-element';
|
|
1148
|
+
mockElement.scrollIntoView = jest.fn();
|
|
1149
|
+
mockElement.classList = { add: jest.fn(), remove: jest.fn() };
|
|
1150
|
+
mockElement.closest = jest.fn().mockReturnValue(null);
|
|
1151
|
+
document.body.appendChild(mockElement);
|
|
1152
|
+
|
|
1153
|
+
const originalGetElementById = document.getElementById;
|
|
1154
|
+
document.getElementById = jest.fn().mockReturnValue(mockElement);
|
|
1155
|
+
|
|
1156
|
+
render(
|
|
1157
|
+
<ScrollIntoView
|
|
1158
|
+
location={{ hash: '#test-element', pathname: '/test' }}
|
|
1159
|
+
/>,
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
jest.advanceTimersByTime(250);
|
|
1163
|
+
|
|
1164
|
+
expect(document.getElementById).toHaveBeenCalledWith('test-element');
|
|
1165
|
+
expect(mockElement.scrollIntoView).toHaveBeenCalledWith({
|
|
1166
|
+
behavior: 'smooth',
|
|
1167
|
+
block: 'center',
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
document.getElementById = originalGetElementById;
|
|
1171
|
+
document.body.removeChild(mockElement);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it('clears interval after 40 attempts', () => {
|
|
1175
|
+
const originalGetElementById = document.getElementById;
|
|
1176
|
+
document.getElementById = jest.fn().mockReturnValue(null);
|
|
1177
|
+
const clearIntervalSpy = jest.spyOn(window, 'clearInterval');
|
|
1178
|
+
|
|
1179
|
+
render(
|
|
1180
|
+
<ScrollIntoView location={{ hash: '#nonexistent', pathname: '/test' }} />,
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
// Run 41 intervals (250ms each)
|
|
1184
|
+
jest.advanceTimersByTime(250 * 41);
|
|
1185
|
+
|
|
1186
|
+
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
1187
|
+
|
|
1188
|
+
document.getElementById = originalGetElementById;
|
|
1189
|
+
clearIntervalSpy.mockRestore();
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
it('clicks first tab on edit page with fieldset hash', () => {
|
|
1193
|
+
const mockElement = document.createElement('div');
|
|
1194
|
+
mockElement.id = 'fieldset-test';
|
|
1195
|
+
mockElement.scrollIntoView = jest.fn();
|
|
1196
|
+
mockElement.classList = { add: jest.fn(), remove: jest.fn() };
|
|
1197
|
+
mockElement.closest = jest.fn().mockReturnValue(null);
|
|
1198
|
+
|
|
1199
|
+
const mockTab = document.createElement('div');
|
|
1200
|
+
mockTab.click = jest.fn();
|
|
1201
|
+
mockTab.classList = { contains: jest.fn().mockReturnValue(false) };
|
|
1202
|
+
|
|
1203
|
+
const mockFormTabs = document.createElement('div');
|
|
1204
|
+
mockFormTabs.className = 'formtabs';
|
|
1205
|
+
Object.defineProperty(mockFormTabs, 'firstElementChild', {
|
|
1206
|
+
get: () => mockTab,
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
const mockSidebar = document.createElement('div');
|
|
1210
|
+
mockSidebar.className = 'sidebar-container';
|
|
1211
|
+
mockSidebar.appendChild(mockFormTabs);
|
|
1212
|
+
document.body.appendChild(mockSidebar);
|
|
1213
|
+
|
|
1214
|
+
const originalGetElementById = document.getElementById;
|
|
1215
|
+
const originalQuerySelector = document.querySelector;
|
|
1216
|
+
|
|
1217
|
+
document.getElementById = jest.fn().mockReturnValue(mockElement);
|
|
1218
|
+
document.querySelector = jest.fn().mockImplementation((selector) => {
|
|
1219
|
+
if (selector === '.sidebar-container .formtabs') {
|
|
1220
|
+
return mockFormTabs;
|
|
1221
|
+
}
|
|
1222
|
+
return null;
|
|
1223
|
+
});
|
|
1224
|
+
document.querySelectorAll = jest.fn().mockReturnValue([]);
|
|
1225
|
+
|
|
1226
|
+
render(
|
|
1227
|
+
<ScrollIntoView
|
|
1228
|
+
location={{ hash: '#fieldset-test', pathname: '/test/edit' }}
|
|
1229
|
+
/>,
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
jest.advanceTimersByTime(250);
|
|
1233
|
+
|
|
1234
|
+
expect(mockTab.click).toHaveBeenCalled();
|
|
1235
|
+
|
|
1236
|
+
document.getElementById = originalGetElementById;
|
|
1237
|
+
document.querySelector = originalQuerySelector;
|
|
1238
|
+
document.body.removeChild(mockSidebar);
|
|
1239
|
+
});
|
|
1240
|
+
|
|
1241
|
+
it('does nothing on server side', () => {
|
|
1242
|
+
global.__CLIENT__ = false;
|
|
1243
|
+
|
|
1244
|
+
render(<ScrollIntoView location={{ hash: '#test', pathname: '/test' }} />);
|
|
1245
|
+
|
|
1246
|
+
// Should not throw or do anything
|
|
1247
|
+
expect(true).toBe(true);
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
describe('Actions', () => {
|
|
1252
|
+
it('getEditingProgress returns correct action', () => {
|
|
1253
|
+
const result = getEditingProgress('/test/item');
|
|
1254
|
+
expect(result.type).toBe('EDITING_PROGRESS');
|
|
1255
|
+
expect(result.request.path).toBe('/test/item/@editing.progress');
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
it('getRawContent returns correct action', () => {
|
|
1259
|
+
const result = getRawContent('/test/url');
|
|
1260
|
+
expect(result.type).toBe('GET_RAW_CONTENT');
|
|
1261
|
+
expect(result.request.path).toBe('/test/url');
|
|
1262
|
+
expect(result.url).toBe('/test/url');
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
it('getRawContent accepts custom headers', () => {
|
|
1266
|
+
const result = getRawContent('/test/url', { 'X-Custom': 'value' });
|
|
1267
|
+
expect(result.request.headers).toEqual({ 'X-Custom': 'value' });
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
describe('Reducers', () => {
|
|
1272
|
+
describe('editingProgress', () => {
|
|
1273
|
+
it('returns initial state', () => {
|
|
1274
|
+
const result = editingProgress(undefined, {});
|
|
1275
|
+
expect(result.get.loaded).toBe(false);
|
|
1276
|
+
expect(result.get.loading).toBe(false);
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
it('handles EDITING_PROGRESS_PENDING', () => {
|
|
1280
|
+
const result = editingProgress(undefined, {
|
|
1281
|
+
type: 'EDITING_PROGRESS_PENDING',
|
|
1282
|
+
});
|
|
1283
|
+
expect(result.editing.loading).toBe(true);
|
|
1284
|
+
expect(result.editing.loaded).toBe(false);
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
it('handles EDITING_PROGRESS_SUCCESS', () => {
|
|
1288
|
+
const result = editingProgress(undefined, {
|
|
1289
|
+
type: 'EDITING_PROGRESS_SUCCESS',
|
|
1290
|
+
result: { data: 'test' },
|
|
1291
|
+
});
|
|
1292
|
+
expect(result.editing.loading).toBe(false);
|
|
1293
|
+
expect(result.editing.loaded).toBe(true);
|
|
1294
|
+
expect(result.result).toEqual({ data: 'test' });
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
it('handles EDITING_PROGRESS_FAIL', () => {
|
|
1298
|
+
const result = editingProgress(undefined, {
|
|
1299
|
+
type: 'EDITING_PROGRESS_FAIL',
|
|
1300
|
+
error: 'error message',
|
|
1301
|
+
});
|
|
1302
|
+
expect(result.editing.loading).toBe(false);
|
|
1303
|
+
expect(result.editing.loaded).toBe(false);
|
|
1304
|
+
expect(result.editing.error).toBe('error message');
|
|
1305
|
+
});
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
describe('rawdata', () => {
|
|
1309
|
+
it('returns initial state', () => {
|
|
1310
|
+
const result = rawdata(undefined, {});
|
|
1311
|
+
expect(result).toEqual({});
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
it('handles GET_RAW_CONTENT_PENDING', () => {
|
|
1315
|
+
const result = rawdata(
|
|
1316
|
+
{},
|
|
1317
|
+
{
|
|
1318
|
+
type: 'GET_RAW_CONTENT_PENDING',
|
|
1319
|
+
url: '/test',
|
|
1320
|
+
},
|
|
1321
|
+
);
|
|
1322
|
+
expect(result['/test'].loading).toBe(true);
|
|
1323
|
+
expect(result['/test'].loaded).toBe(false);
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
it('handles GET_RAW_CONTENT_SUCCESS', () => {
|
|
1327
|
+
const result = rawdata(
|
|
1328
|
+
{},
|
|
1329
|
+
{
|
|
1330
|
+
type: 'GET_RAW_CONTENT_SUCCESS',
|
|
1331
|
+
url: '/test',
|
|
1332
|
+
result: { items: [] },
|
|
1333
|
+
},
|
|
1334
|
+
);
|
|
1335
|
+
expect(result['/test'].loading).toBe(false);
|
|
1336
|
+
expect(result['/test'].loaded).toBe(true);
|
|
1337
|
+
expect(result['/test'].data).toEqual({ items: [] });
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
it('handles GET_RAW_CONTENT_FAIL', () => {
|
|
1341
|
+
const result = rawdata(
|
|
1342
|
+
{},
|
|
1343
|
+
{
|
|
1344
|
+
type: 'GET_RAW_CONTENT_FAIL',
|
|
1345
|
+
url: '/test',
|
|
1346
|
+
error: 'error',
|
|
1347
|
+
},
|
|
1348
|
+
);
|
|
1349
|
+
expect(result['/test'].loading).toBe(false);
|
|
1350
|
+
expect(result['/test'].loaded).toBe(false);
|
|
1351
|
+
expect(result['/test'].error).toBe('error');
|
|
1352
|
+
});
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
describe('JSONSchema', () => {
|
|
1357
|
+
it('returns schema with json field', () => {
|
|
1358
|
+
const mockIntl = {
|
|
1359
|
+
formatMessage: jest.fn().mockReturnValue('JSON code'),
|
|
1360
|
+
};
|
|
1361
|
+
const result = JSONSchema({ intl: mockIntl });
|
|
1362
|
+
expect(result.required).toContain('json');
|
|
1363
|
+
expect(result.fieldsets[0].fields).toContain('json');
|
|
1364
|
+
expect(result.properties.json.widget).toBe('jsonTextarea');
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
describe('TextareaJSONWidget', () => {
|
|
1369
|
+
const store = mockStore({
|
|
1370
|
+
intl: {
|
|
1371
|
+
locale: 'en',
|
|
1372
|
+
messages: {},
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
it('renders with initial value', () => {
|
|
1377
|
+
render(
|
|
1378
|
+
<Provider store={store}>
|
|
1379
|
+
<TextareaJSONWidget
|
|
1380
|
+
id="test-json"
|
|
1381
|
+
title="Test JSON"
|
|
1382
|
+
value={{ key: 'value' }}
|
|
1383
|
+
onChange={jest.fn()}
|
|
1384
|
+
/>
|
|
1385
|
+
</Provider>,
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
const textarea = document.querySelector('textarea');
|
|
1389
|
+
expect(textarea).toBeInTheDocument();
|
|
1390
|
+
expect(textarea.value).toContain('"key"');
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
it('handles valid JSON input', () => {
|
|
1394
|
+
const mockOnChange = jest.fn();
|
|
1395
|
+
render(
|
|
1396
|
+
<Provider store={store}>
|
|
1397
|
+
<TextareaJSONWidget
|
|
1398
|
+
id="test-json"
|
|
1399
|
+
title="Test JSON"
|
|
1400
|
+
value={{ initial: 'value' }}
|
|
1401
|
+
onChange={mockOnChange}
|
|
1402
|
+
/>
|
|
1403
|
+
</Provider>,
|
|
1404
|
+
);
|
|
1405
|
+
|
|
1406
|
+
const textarea = document.querySelector('textarea');
|
|
1407
|
+
fireEvent.change(textarea, {
|
|
1408
|
+
target: { value: '{"new": "json"}' },
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
expect(mockOnChange).toHaveBeenCalledWith('test-json', { new: 'json' });
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
it('handles invalid JSON input and shows error', () => {
|
|
1415
|
+
jest.useFakeTimers();
|
|
1416
|
+
const mockOnChange = jest.fn();
|
|
1417
|
+
render(
|
|
1418
|
+
<Provider store={store}>
|
|
1419
|
+
<TextareaJSONWidget
|
|
1420
|
+
id="test-json"
|
|
1421
|
+
title="Test JSON"
|
|
1422
|
+
value={{ initial: 'value' }}
|
|
1423
|
+
onChange={mockOnChange}
|
|
1424
|
+
/>
|
|
1425
|
+
</Provider>,
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
const textarea = document.querySelector('textarea');
|
|
1429
|
+
fireEvent.change(textarea, {
|
|
1430
|
+
target: { value: 'invalid json {' },
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
// Should call onChange with previous value
|
|
1434
|
+
expect(mockOnChange).toHaveBeenCalledWith('test-json', {
|
|
1435
|
+
initial: 'value',
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
// Error message should appear
|
|
1439
|
+
expect(screen.getByText('Please enter valid JSON!')).toBeInTheDocument();
|
|
1440
|
+
|
|
1441
|
+
// Error should disappear after 1.5 seconds
|
|
1442
|
+
act(() => {
|
|
1443
|
+
jest.advanceTimersByTime(1500);
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
jest.useRealTimers();
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it('handles empty string input as undefined', () => {
|
|
1450
|
+
const mockOnChange = jest.fn();
|
|
1451
|
+
render(
|
|
1452
|
+
<Provider store={store}>
|
|
1453
|
+
<TextareaJSONWidget
|
|
1454
|
+
id="test-json"
|
|
1455
|
+
title="Test JSON"
|
|
1456
|
+
value={{ initial: 'value' }}
|
|
1457
|
+
onChange={mockOnChange}
|
|
1458
|
+
/>
|
|
1459
|
+
</Provider>,
|
|
1460
|
+
);
|
|
1461
|
+
|
|
1462
|
+
const textarea = document.querySelector('textarea');
|
|
1463
|
+
fireEvent.change(textarea, {
|
|
1464
|
+
target: { value: '' },
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// Empty string triggers invalid JSON path with previous value
|
|
1468
|
+
expect(mockOnChange).toHaveBeenCalledWith('test-json', {
|
|
1469
|
+
initial: 'value',
|
|
1470
|
+
});
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
it('handles string value prop', () => {
|
|
1474
|
+
render(
|
|
1475
|
+
<Provider store={store}>
|
|
1476
|
+
<TextareaJSONWidget
|
|
1477
|
+
id="test-json"
|
|
1478
|
+
title="Test JSON"
|
|
1479
|
+
value='{"stringified": "value"}'
|
|
1480
|
+
onChange={jest.fn()}
|
|
1481
|
+
/>
|
|
1482
|
+
</Provider>,
|
|
1483
|
+
);
|
|
1484
|
+
|
|
1485
|
+
const textarea = document.querySelector('textarea');
|
|
1486
|
+
expect(textarea.value).toContain('"stringified"');
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it('renders disabled state', () => {
|
|
1490
|
+
render(
|
|
1491
|
+
<Provider store={store}>
|
|
1492
|
+
<TextareaJSONWidget
|
|
1493
|
+
id="test-json"
|
|
1494
|
+
title="Test JSON"
|
|
1495
|
+
value={{ key: 'value' }}
|
|
1496
|
+
onChange={jest.fn()}
|
|
1497
|
+
isDisabled={true}
|
|
1498
|
+
/>
|
|
1499
|
+
</Provider>,
|
|
1500
|
+
);
|
|
1501
|
+
|
|
1502
|
+
const textarea = document.querySelector('textarea');
|
|
1503
|
+
expect(textarea).toBeDisabled();
|
|
50
1504
|
});
|
|
51
1505
|
});
|