@evoke-platform/ui-components 1.10.0-dev.6 → 1.10.0-dev.8
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/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +1 -1
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.d.ts +1 -0
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +430 -0
- package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +19 -6
- package/dist/published/components/custom/FormV2/FormRenderer.js +13 -3
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +6 -1
- package/dist/published/components/custom/FormV2/components/Header.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Header.js +5 -2
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +227 -8
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +202 -12
- package/package.json +3 -1
|
@@ -248,7 +248,7 @@ const customDelete = (props) => {
|
|
|
248
248
|
'&:hover': { backgroundColor: 'transparent', boxShadow: 'none' },
|
|
249
249
|
color: '#212B36',
|
|
250
250
|
fontWeight: '400',
|
|
251
|
-
} }, "Delete group")) : (React.createElement(IconButton, { onClick: handleOnClick, size: "small" },
|
|
251
|
+
} }, "Delete group")) : (React.createElement(IconButton, { onClick: handleOnClick, size: "small", "aria-label": "Delete rule" },
|
|
252
252
|
React.createElement(TrashCan, { sx: { ':hover': { color: '#637381' } } })))) : (React.createElement(React.Fragment, null));
|
|
253
253
|
};
|
|
254
254
|
export const valueEditor = (props) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import * as matchers from '@testing-library/jest-dom/matchers';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { expect, it } from 'vitest';
|
|
6
|
+
import CriteriaBuilder from './CriteriaBuilder';
|
|
7
|
+
expect.extend(matchers);
|
|
8
|
+
const mockProperties = [
|
|
9
|
+
{
|
|
10
|
+
id: 'name',
|
|
11
|
+
name: 'Name',
|
|
12
|
+
type: 'string',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'age',
|
|
16
|
+
name: 'Age',
|
|
17
|
+
type: 'integer',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'birthDate',
|
|
21
|
+
name: 'Birth Date',
|
|
22
|
+
type: 'date',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'createdTime',
|
|
26
|
+
name: 'Created Time',
|
|
27
|
+
type: 'time',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'tags',
|
|
31
|
+
name: 'Tags',
|
|
32
|
+
type: 'array',
|
|
33
|
+
enum: ['tag1', 'tag2', 'tag3'],
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'status',
|
|
37
|
+
name: 'Status',
|
|
38
|
+
type: 'string',
|
|
39
|
+
enum: ['active', 'inactive', 'pending'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'profilePic',
|
|
43
|
+
name: 'Profile Picture',
|
|
44
|
+
type: 'image',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'metadata',
|
|
48
|
+
name: 'Metadata',
|
|
49
|
+
type: 'document',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'percentCompleted',
|
|
53
|
+
name: 'Percent Completed',
|
|
54
|
+
type: 'number',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'boolean',
|
|
58
|
+
name: 'Boolean',
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
describe('CriteriaBuilder', () => {
|
|
63
|
+
// Mock function for setCriteria
|
|
64
|
+
const setCriteriaMock = vi.fn();
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
// Reset the mock before each test
|
|
67
|
+
setCriteriaMock.mockReset();
|
|
68
|
+
});
|
|
69
|
+
describe('when passed single-select fields', () => {
|
|
70
|
+
it('should render the field name', () => {
|
|
71
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
72
|
+
status: 'active',
|
|
73
|
+
}, setCriteria: setCriteriaMock }));
|
|
74
|
+
expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Status');
|
|
75
|
+
});
|
|
76
|
+
it('should render the "is" operator to represent equality', () => {
|
|
77
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
78
|
+
status: 'active',
|
|
79
|
+
}, setCriteria: setCriteriaMock }));
|
|
80
|
+
expect(screen.getByRole('combobox', { name: /select operator/i })).toHaveValue('Is');
|
|
81
|
+
});
|
|
82
|
+
['In', 'Is', 'Is empty', 'Is not', 'Is not empty', 'Not in'].forEach((operator) => {
|
|
83
|
+
it(`should offer the ${operator} operator`, async () => {
|
|
84
|
+
const user = userEvent.setup();
|
|
85
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
86
|
+
status: 'active',
|
|
87
|
+
}, setCriteria: setCriteriaMock }));
|
|
88
|
+
await user.click(screen.getByRole('combobox', { name: /select operator/i }));
|
|
89
|
+
await screen.findByRole('option', { name: operator });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('when utilizing the "is" operator', () => {
|
|
93
|
+
it('should display a single preloaded value', async () => {
|
|
94
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
95
|
+
status: 'active',
|
|
96
|
+
}, setCriteria: setCriteriaMock }));
|
|
97
|
+
expect(screen.getByRole('combobox', { name: 'Select or enter a value' })).toHaveValue('active');
|
|
98
|
+
});
|
|
99
|
+
it('should hide list of values after selecting a value', async () => {
|
|
100
|
+
const user = userEvent.setup();
|
|
101
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
102
|
+
status: '',
|
|
103
|
+
}, setCriteria: setCriteriaMock }));
|
|
104
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
105
|
+
await user.click(valueInput);
|
|
106
|
+
const listbox = await screen.findByRole('listbox');
|
|
107
|
+
await user.click(screen.getByRole('option', { name: 'active' }));
|
|
108
|
+
expect(listbox).not.toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
it('should mark the selected value in the list', async () => {
|
|
111
|
+
const user = userEvent.setup();
|
|
112
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
113
|
+
status: 'active',
|
|
114
|
+
}, setCriteria: setCriteriaMock }));
|
|
115
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
116
|
+
await user.click(valueInput);
|
|
117
|
+
const selectedOption = await screen.findByRole('option', { name: 'active' });
|
|
118
|
+
expect(selectedOption).toHaveAttribute('aria-selected', 'true');
|
|
119
|
+
});
|
|
120
|
+
it('should allow values to be deleted using the backspace key', async () => {
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
123
|
+
status: 'active',
|
|
124
|
+
}, setCriteria: setCriteriaMock }));
|
|
125
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
126
|
+
await user.click(valueInput);
|
|
127
|
+
for (let i = 0; i < 6; i++) {
|
|
128
|
+
await user.type(valueInput, '{Backspace}');
|
|
129
|
+
}
|
|
130
|
+
// Clear focus away from input
|
|
131
|
+
await user.click(screen.getByRole('combobox', { name: /select operator/i }));
|
|
132
|
+
expect(screen.getByRole('combobox', { name: /select or enter a value/i })).toHaveValue('');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('when passed multi-select (array) fields', () => {
|
|
137
|
+
it('should render the field name', () => {
|
|
138
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
139
|
+
tags: { $in: ['tag1', 'tag2'] },
|
|
140
|
+
}, setCriteria: setCriteriaMock }));
|
|
141
|
+
expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Tags');
|
|
142
|
+
});
|
|
143
|
+
it('should load the correct operator for pre-loaded data', () => {
|
|
144
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
145
|
+
tags: { $in: ['tag1', 'tag2'] },
|
|
146
|
+
}, setCriteria: setCriteriaMock }));
|
|
147
|
+
expect(screen.getByRole('combobox', { name: /select operator/i })).toHaveValue('In');
|
|
148
|
+
});
|
|
149
|
+
['In', 'Is empty', 'Is not empty'].forEach((operator) => {
|
|
150
|
+
it(`should offer the ${operator} operator`, async () => {
|
|
151
|
+
const user = userEvent.setup();
|
|
152
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
153
|
+
tags: { $in: ['tag1', 'tag2'] },
|
|
154
|
+
}, setCriteria: setCriteriaMock }));
|
|
155
|
+
await user.click(screen.getByRole('combobox', { name: /select operator/i }));
|
|
156
|
+
await screen.findByRole('option', { name: operator });
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
it('should display a single preloaded value', async () => {
|
|
160
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
161
|
+
tags: { $in: ['tag1'] },
|
|
162
|
+
}, setCriteria: setCriteriaMock }));
|
|
163
|
+
screen.getByRole('button', { name: 'tag1' });
|
|
164
|
+
});
|
|
165
|
+
it('should display multiple preloaded values', async () => {
|
|
166
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
167
|
+
tags: { $in: ['tag1', 'tag2'] },
|
|
168
|
+
}, setCriteria: setCriteriaMock }));
|
|
169
|
+
screen.getByRole('button', { name: 'tag1' });
|
|
170
|
+
screen.getByRole('button', { name: 'tag2' });
|
|
171
|
+
});
|
|
172
|
+
it('should hide list of values open after selecting a value', async () => {
|
|
173
|
+
const user = userEvent.setup();
|
|
174
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
175
|
+
tags: { $in: [] },
|
|
176
|
+
}, setCriteria: setCriteriaMock }));
|
|
177
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
178
|
+
await user.click(valueInput);
|
|
179
|
+
const listbox = await screen.findByRole('listbox');
|
|
180
|
+
await user.click(screen.getByRole('option', { name: 'tag1' }));
|
|
181
|
+
expect(listbox).not.toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
it('should mark the selected values in the list', async () => {
|
|
184
|
+
const user = userEvent.setup();
|
|
185
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
186
|
+
tags: { $in: ['tag1', 'tag2'] },
|
|
187
|
+
}, setCriteria: setCriteriaMock }));
|
|
188
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
189
|
+
await user.click(valueInput);
|
|
190
|
+
const selectedOption1 = await screen.findByRole('option', { name: 'tag1' });
|
|
191
|
+
const selectedOption2 = await screen.findByRole('option', { name: 'tag2' });
|
|
192
|
+
expect(selectedOption1).toHaveAttribute('aria-selected', 'true');
|
|
193
|
+
expect(selectedOption2).toHaveAttribute('aria-selected', 'true');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('when passed (decimal) number fields', () => {
|
|
197
|
+
it('should render the field name', () => {
|
|
198
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
199
|
+
percentCompleted: 30.5,
|
|
200
|
+
}, setCriteria: setCriteriaMock }));
|
|
201
|
+
expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Percent Completed');
|
|
202
|
+
});
|
|
203
|
+
[
|
|
204
|
+
'Greater than',
|
|
205
|
+
'Greater than or equal to',
|
|
206
|
+
'In',
|
|
207
|
+
'Is',
|
|
208
|
+
'Is empty',
|
|
209
|
+
'Is not empty',
|
|
210
|
+
'Less than',
|
|
211
|
+
'Less than or equal to',
|
|
212
|
+
'Not in',
|
|
213
|
+
'Is not',
|
|
214
|
+
].forEach((operator) => {
|
|
215
|
+
it(`should offer the ${operator} operator`, async () => {
|
|
216
|
+
const user = userEvent.setup();
|
|
217
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
218
|
+
percentCompleted: 25.5,
|
|
219
|
+
}, setCriteria: setCriteriaMock }));
|
|
220
|
+
await user.click(screen.getByRole('combobox', { name: /select operator/i }));
|
|
221
|
+
await screen.findByRole('option', { name: operator });
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
describe('when utilizing the "in" operator', () => {
|
|
225
|
+
it('should allow numbers to be entered', async () => {
|
|
226
|
+
const user = userEvent.setup();
|
|
227
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
228
|
+
percentCompleted: { $in: [] },
|
|
229
|
+
}, setCriteria: setCriteriaMock }));
|
|
230
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
231
|
+
await user.click(valueInput);
|
|
232
|
+
await user.type(valueInput, '4{Enter}');
|
|
233
|
+
screen.getByRole('button', { name: '4' });
|
|
234
|
+
});
|
|
235
|
+
it('should not allow non-numeric values to be entered', async () => {
|
|
236
|
+
const user = userEvent.setup();
|
|
237
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
238
|
+
percentCompleted: { $in: [] },
|
|
239
|
+
}, setCriteria: setCriteriaMock }));
|
|
240
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
241
|
+
await user.click(valueInput);
|
|
242
|
+
await user.type(valueInput, 'nonNumericValue{Enter}');
|
|
243
|
+
expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
describe('when utilizing the "not in" operator', () => {
|
|
247
|
+
it('should allow numbers to be entered', async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
250
|
+
age: { $nin: [] },
|
|
251
|
+
}, setCriteria: setCriteriaMock }));
|
|
252
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
253
|
+
await user.click(valueInput);
|
|
254
|
+
await user.type(valueInput, '4{Enter}');
|
|
255
|
+
screen.getByRole('button', { name: '4' });
|
|
256
|
+
});
|
|
257
|
+
it('should not allow non-numeric values to be entered', async () => {
|
|
258
|
+
const user = userEvent.setup();
|
|
259
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
260
|
+
age: { $nin: [] },
|
|
261
|
+
}, setCriteria: setCriteriaMock }));
|
|
262
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
263
|
+
await user.click(valueInput);
|
|
264
|
+
await user.type(valueInput, 'nonNumericValue{Enter}');
|
|
265
|
+
expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe('when passed integer fields', () => {
|
|
270
|
+
it('should render the field name', () => {
|
|
271
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
272
|
+
age: 25,
|
|
273
|
+
}, setCriteria: setCriteriaMock }));
|
|
274
|
+
expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Age');
|
|
275
|
+
});
|
|
276
|
+
[
|
|
277
|
+
'Greater than',
|
|
278
|
+
'Greater than or equal to',
|
|
279
|
+
'In',
|
|
280
|
+
'Is',
|
|
281
|
+
'Is empty',
|
|
282
|
+
'Is not empty',
|
|
283
|
+
'Less than',
|
|
284
|
+
'Less than or equal to',
|
|
285
|
+
'Not in',
|
|
286
|
+
'Is not',
|
|
287
|
+
].forEach((operator) => {
|
|
288
|
+
it(`should offer the ${operator} operator`, async () => {
|
|
289
|
+
const user = userEvent.setup();
|
|
290
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
291
|
+
age: 25,
|
|
292
|
+
}, setCriteria: setCriteriaMock }));
|
|
293
|
+
await user.click(screen.getByRole('combobox', { name: /select operator/i }));
|
|
294
|
+
await screen.findByRole('option', { name: operator });
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
describe('when utilizing the "in" operator', () => {
|
|
298
|
+
it('should allow numbers to be entered', async () => {
|
|
299
|
+
const user = userEvent.setup();
|
|
300
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
301
|
+
age: { $in: [] },
|
|
302
|
+
}, setCriteria: setCriteriaMock }));
|
|
303
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
304
|
+
await user.click(valueInput);
|
|
305
|
+
await user.type(valueInput, '4{Enter}');
|
|
306
|
+
screen.getByRole('button', { name: '4' });
|
|
307
|
+
});
|
|
308
|
+
it('should not allow non-numeric values to be entered', async () => {
|
|
309
|
+
const user = userEvent.setup();
|
|
310
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
311
|
+
age: { $in: [] },
|
|
312
|
+
}, setCriteria: setCriteriaMock }));
|
|
313
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
314
|
+
await user.click(valueInput);
|
|
315
|
+
await user.type(valueInput, 'nonNumericValue{Enter}');
|
|
316
|
+
expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe('when utilizing the "not in" operator', () => {
|
|
320
|
+
it('should allow numbers to be entered', async () => {
|
|
321
|
+
const user = userEvent.setup();
|
|
322
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
323
|
+
age: { $nin: [] },
|
|
324
|
+
}, setCriteria: setCriteriaMock }));
|
|
325
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
326
|
+
await user.click(valueInput);
|
|
327
|
+
await user.type(valueInput, '4{Enter}');
|
|
328
|
+
screen.getByRole('button', { name: '4' });
|
|
329
|
+
});
|
|
330
|
+
it('should not allow non-numeric values to be entered', async () => {
|
|
331
|
+
const user = userEvent.setup();
|
|
332
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
333
|
+
age: { $nin: [] },
|
|
334
|
+
}, setCriteria: setCriteriaMock }));
|
|
335
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
336
|
+
await user.click(valueInput);
|
|
337
|
+
await user.type(valueInput, 'nonNumericValue{Enter}');
|
|
338
|
+
expect(screen.queryByText('nonNumericValue')).not.toBeInTheDocument();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
describe('when passed text fields', () => {
|
|
343
|
+
it('should render the field name', () => {
|
|
344
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
345
|
+
name: 'John Doe',
|
|
346
|
+
}, setCriteria: setCriteriaMock }));
|
|
347
|
+
expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Name');
|
|
348
|
+
});
|
|
349
|
+
describe('when utilizing the "in" operator', () => {
|
|
350
|
+
it('should allow custom values to be entered', async () => {
|
|
351
|
+
const user = userEvent.setup();
|
|
352
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
353
|
+
name: { $in: [] },
|
|
354
|
+
}, setCriteria: setCriteriaMock }));
|
|
355
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
356
|
+
await user.click(valueInput);
|
|
357
|
+
await user.type(valueInput, 'customStatus{Enter}');
|
|
358
|
+
expect(screen.getByRole('button', { name: 'customStatus' })).toBeInTheDocument();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
describe('when utilizing the "Not in" operator', () => {
|
|
362
|
+
it('should allow custom values to be entered', async () => {
|
|
363
|
+
const user = userEvent.setup();
|
|
364
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
365
|
+
name: { $nin: [] },
|
|
366
|
+
}, setCriteria: setCriteriaMock }));
|
|
367
|
+
const valueInput = screen.getByRole('combobox', { name: /select or enter a value/i });
|
|
368
|
+
await user.click(valueInput);
|
|
369
|
+
await user.type(valueInput, 'customStatus{Enter}');
|
|
370
|
+
expect(screen.getByRole('button', { name: 'customStatus' })).toBeInTheDocument();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
describe('when passed boolean fields', () => {
|
|
375
|
+
it('should render the field name', () => {
|
|
376
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
377
|
+
boolean: true,
|
|
378
|
+
}, setCriteria: setCriteriaMock }));
|
|
379
|
+
expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Boolean');
|
|
380
|
+
});
|
|
381
|
+
['Is', 'Is not', 'Is empty', 'Is not empty'].forEach((operator) => {
|
|
382
|
+
it(`should offer the ${operator} operator`, async () => {
|
|
383
|
+
const user = userEvent.setup();
|
|
384
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
385
|
+
boolean: true,
|
|
386
|
+
}, setCriteria: setCriteriaMock }));
|
|
387
|
+
await user.click(screen.getByRole('combobox', { name: /select operator/i }));
|
|
388
|
+
await screen.findByRole('option', { name: operator });
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
it('should display preloaded value', () => {
|
|
392
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
393
|
+
boolean: true,
|
|
394
|
+
}, setCriteria: setCriteriaMock }));
|
|
395
|
+
expect(screen.getByRole('combobox', { name: /select or enter a value/i })).toHaveValue('True');
|
|
396
|
+
});
|
|
397
|
+
['True', 'False'].forEach((value) => {
|
|
398
|
+
it(`should offer the value ${value}`, async () => {
|
|
399
|
+
const user = userEvent.setup();
|
|
400
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
401
|
+
boolean: false,
|
|
402
|
+
}, setCriteria: setCriteriaMock }));
|
|
403
|
+
await user.click(screen.getByRole('combobox', { name: /select or enter a value/i }));
|
|
404
|
+
await screen.findByRole('option', { name: value });
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
describe('when deleting rules', () => {
|
|
409
|
+
it('should allow individual rules to be deleted if the delete rule button is pressed', async () => {
|
|
410
|
+
const user = userEvent.setup();
|
|
411
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
412
|
+
status: 'active',
|
|
413
|
+
}, setCriteria: setCriteriaMock }));
|
|
414
|
+
const propertySelectBox = screen.getByRole('combobox', { name: /select property/i });
|
|
415
|
+
const deleteButton = screen.getByRole('button', { name: /delete rule/i });
|
|
416
|
+
await user.click(deleteButton);
|
|
417
|
+
expect(propertySelectBox).not.toBeInTheDocument();
|
|
418
|
+
});
|
|
419
|
+
it('should remove an entire rule group if the delete group button is pressed', async () => {
|
|
420
|
+
const user = userEvent.setup();
|
|
421
|
+
render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
|
|
422
|
+
$and: [{ $and: [{ status: 'active' }] }],
|
|
423
|
+
}, setCriteria: setCriteriaMock }));
|
|
424
|
+
const propertySelectBox = screen.getByRole('combobox', { name: /select property/i });
|
|
425
|
+
const deleteButton = screen.getByRole('button', { name: /delete group/i });
|
|
426
|
+
await user.click(deleteButton);
|
|
427
|
+
expect(propertySelectBox).not.toBeInTheDocument();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
});
|
|
@@ -221,7 +221,10 @@ const ValueEditor = (props) => {
|
|
|
221
221
|
})
|
|
222
222
|
.filter((item) => item !== '');
|
|
223
223
|
handleOnChange(uniqueSelections.length ? Array.from(new Set(uniqueSelections)) : '');
|
|
224
|
-
}, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, {
|
|
224
|
+
}, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, ...params, size: "small", inputProps: {
|
|
225
|
+
...params.inputProps,
|
|
226
|
+
'aria-label': 'Select or enter a value',
|
|
227
|
+
} })), groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sx: styles.input, readOnly: readOnly }));
|
|
225
228
|
}
|
|
226
229
|
else {
|
|
227
230
|
return (React.createElement(TextField, { inputRef: inputRef, value: ['null', 'notNull'].includes(operator) ? '' : value, disabled: disabled || ['null', 'notNull'].includes(operator), onChange: (e) => {
|
|
@@ -240,11 +243,14 @@ const ValueEditor = (props) => {
|
|
|
240
243
|
const options = [{ label: 'True', value: true }, { label: 'False', value: false }, ...presetValues];
|
|
241
244
|
return (React.createElement(Autocomplete, { options: options, value: options.find((opt) => opt.value === value) ?? value, onChange: (event, newValue) => {
|
|
242
245
|
handleOnChange(newValue ? newValue.value : '');
|
|
243
|
-
}, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef,
|
|
246
|
+
}, isOptionEqualToValue: (option, value) => option.value === value.value, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, size: "small", ...params, inputProps: {
|
|
247
|
+
'aria-label': 'Select or enter a value',
|
|
248
|
+
...params.inputProps,
|
|
249
|
+
} })), groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sortBy: "NONE", sx: styles.input, readOnly: readOnly }));
|
|
244
250
|
}
|
|
245
251
|
else {
|
|
246
|
-
const isMultiple = inputType === 'array' || isMultipleOperator
|
|
247
|
-
if (isMultiple) {
|
|
252
|
+
const isMultiple = inputType === 'array' || isMultipleOperator;
|
|
253
|
+
if (isMultiple || values?.length) {
|
|
248
254
|
const options = [...values, ...presetValues];
|
|
249
255
|
return (React.createElement(Autocomplete, { freeSolo: inputType !== 'array' && fieldData.valueEditorType !== 'select', multiple: isMultiple, options: options, value: isMultiple
|
|
250
256
|
? Array.isArray(value)
|
|
@@ -263,7 +269,11 @@ const ValueEditor = (props) => {
|
|
|
263
269
|
}
|
|
264
270
|
else {
|
|
265
271
|
value =
|
|
266
|
-
typeof newValue === 'string'
|
|
272
|
+
typeof newValue === 'string'
|
|
273
|
+
? newValue
|
|
274
|
+
: !newValue
|
|
275
|
+
? newValue
|
|
276
|
+
: newValue.value;
|
|
267
277
|
}
|
|
268
278
|
handleOnChange(value);
|
|
269
279
|
}, onBlur: () => {
|
|
@@ -285,7 +295,10 @@ const ValueEditor = (props) => {
|
|
|
285
295
|
}
|
|
286
296
|
}, onInputChange: (event, newInputValue) => {
|
|
287
297
|
setInputValue(newInputValue);
|
|
288
|
-
}, inputValue: inputValue, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef,
|
|
298
|
+
}, inputValue: inputValue, renderInput: (params) => (React.createElement(TextField, { inputRef: inputRef, ...params, size: "small", inputProps: {
|
|
299
|
+
...params.inputProps,
|
|
300
|
+
'aria-label': 'Select or enter a value',
|
|
301
|
+
} })), isOptionEqualToValue: (option, value) => typeof value === 'string' ? option?.value === value : option?.value === value.value, groupBy: (option) => isPresetValue(option.value) ? context.presetGroupLabel || 'Preset Values' : 'Options', renderGroup: groupRenderGroup, sortBy: "NONE", sx: styles.input, readOnly: readOnly }));
|
|
289
302
|
}
|
|
290
303
|
else {
|
|
291
304
|
return (React.createElement(TextField, { inputRef: inputRef, value: ['null', 'notNull'].includes(operator) ? '' : value, disabled: ['null', 'notNull'].includes(operator), onChange: (e) => handleOnChange(e.target.value), onClick: onClick, placeholder: "Value", size: "small", sx: styles.input, readOnly: readOnly }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useObject } from '@evoke-platform/context';
|
|
2
2
|
import { isEmpty, isEqual, omit } from 'lodash';
|
|
3
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useForm } from 'react-hook-form';
|
|
5
5
|
import { useWidgetSize } from '../../../theme';
|
|
6
6
|
import { Box } from '../../layout';
|
|
@@ -12,7 +12,7 @@ import { assignIdsToSectionsAndRichText, convertDocToParameters, convertProperti
|
|
|
12
12
|
import { handleValidation } from './components/ValidationFiles/Validation';
|
|
13
13
|
import ValidationErrors from './components/ValidationFiles/ValidationErrors';
|
|
14
14
|
const FormRendererInternal = (props) => {
|
|
15
|
-
const { onSubmit, onDiscardChanges, onSubmitError, value, fieldHeight, richTextEditor, form, instance, onChange, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
15
|
+
const { onSubmit, onDiscardChanges, onSubmitError: onSubmitErrorOverride, value, fieldHeight, richTextEditor, form, instance, onChange, associatedObject, renderHeader, renderBody, renderFooter, } = props;
|
|
16
16
|
const { entries, name: title, objectId, actionId, display } = form;
|
|
17
17
|
const { register, unregister, setValue, reset, handleSubmit, formState: { errors, isSubmitted }, getValues, } = useForm({
|
|
18
18
|
defaultValues: value,
|
|
@@ -32,6 +32,7 @@ const FormRendererInternal = (props) => {
|
|
|
32
32
|
const [isInitializing, setIsInitializing] = useState(true);
|
|
33
33
|
const [parameters, setParameters] = useState();
|
|
34
34
|
const objectStore = useObject(objectId);
|
|
35
|
+
const validationErrorsRef = useRef(null);
|
|
35
36
|
const updateFetchedOptions = (newData) => {
|
|
36
37
|
setFetchedOptions((prev) => ({
|
|
37
38
|
...prev,
|
|
@@ -134,9 +135,17 @@ const FormRendererInternal = (props) => {
|
|
|
134
135
|
unregister(fieldId);
|
|
135
136
|
}
|
|
136
137
|
};
|
|
138
|
+
const onSubmitError = (errors) => {
|
|
139
|
+
if (onSubmitErrorOverride) {
|
|
140
|
+
onSubmitErrorOverride(errors);
|
|
141
|
+
}
|
|
142
|
+
else if (validationErrorsRef.current) {
|
|
143
|
+
validationErrorsRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
137
146
|
async function unregisterHiddenFieldsAndSubmit() {
|
|
138
147
|
unregisterHiddenFields(entries ?? []);
|
|
139
|
-
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError
|
|
148
|
+
await handleSubmit((data) => onSubmit && onSubmit(action?.type === 'delete' ? {} : data), (errors) => onSubmitError(errors))();
|
|
140
149
|
}
|
|
141
150
|
const headerProps = {
|
|
142
151
|
title,
|
|
@@ -148,6 +157,7 @@ const FormRendererInternal = (props) => {
|
|
|
148
157
|
shouldShowValidationErrors: isSubmitted,
|
|
149
158
|
form,
|
|
150
159
|
action,
|
|
160
|
+
validationErrorsRef: validationErrorsRef,
|
|
151
161
|
};
|
|
152
162
|
const footerProps = {
|
|
153
163
|
onSubmit: unregisterHiddenFieldsAndSubmit,
|
|
@@ -59,6 +59,10 @@ function FormRendererContainer(props) {
|
|
|
59
59
|
const action = object?.actions?.find((a) => a.id === (form?.actionId || actionId));
|
|
60
60
|
if (action && (instanceId || action.type === 'create')) {
|
|
61
61
|
setAction(action);
|
|
62
|
+
// Clear error if action is found after being missing
|
|
63
|
+
// TODO: This entire effect should take place after form is fetched to avoid an error flickering
|
|
64
|
+
// That is, this effect should be merged with the one below that fetches the form
|
|
65
|
+
setError((prevError) => prevError === 'Action could not be found' ? undefined : prevError);
|
|
62
66
|
}
|
|
63
67
|
else {
|
|
64
68
|
setError('Action could not be found');
|
|
@@ -99,7 +103,8 @@ function FormRendererContainer(props) {
|
|
|
99
103
|
apiServices
|
|
100
104
|
.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
|
|
101
105
|
.then((evokeForm) => {
|
|
102
|
-
|
|
106
|
+
// If an actionId is provided, ensure it matches the form's actionId
|
|
107
|
+
if (!actionId || evokeForm?.actionId === actionId) {
|
|
103
108
|
const form = evokeForm;
|
|
104
109
|
setForm(form);
|
|
105
110
|
}
|
|
@@ -6,6 +6,7 @@ import { ExpandedSection } from './types';
|
|
|
6
6
|
export type HeaderProps = {
|
|
7
7
|
hasAccordions: boolean;
|
|
8
8
|
shouldShowValidationErrors: boolean;
|
|
9
|
+
validationErrorsRef?: React.Ref<HTMLDivElement>;
|
|
9
10
|
title?: string;
|
|
10
11
|
expandedSections?: ExpandedSection[];
|
|
11
12
|
onExpandAll?: () => void;
|
|
@@ -6,7 +6,7 @@ import { Typography } from '../../../core/Typography';
|
|
|
6
6
|
import Box from '../../../layout/Box/Box';
|
|
7
7
|
import ValidationErrors from './ValidationFiles/ValidationErrors';
|
|
8
8
|
const Header = (props) => {
|
|
9
|
-
const { title, errors, hasAccordions, shouldShowValidationErrors, form, sx } = props;
|
|
9
|
+
const { title, errors, hasAccordions, shouldShowValidationErrors, validationErrorsRef, form, sx } = props;
|
|
10
10
|
const { width } = useFormContext();
|
|
11
11
|
const { breakpoints, isBelow } = useWidgetSize({
|
|
12
12
|
scroll: false,
|
|
@@ -25,12 +25,15 @@ const Header = (props) => {
|
|
|
25
25
|
borderBottom: !form.id ? undefined : '1px solid #e9ecef',
|
|
26
26
|
gap: isSm || isXs ? 2 : 3,
|
|
27
27
|
...sx,
|
|
28
|
+
'.evoke-form-renderer-header': {
|
|
29
|
+
flex: 1,
|
|
30
|
+
},
|
|
28
31
|
} },
|
|
29
32
|
title && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
|
|
30
33
|
React.createElement(Title, { ...props }))),
|
|
31
34
|
hasAccordions && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
|
|
32
35
|
React.createElement(AccordionActions, { ...props }))),
|
|
33
|
-
shouldShowValidationErrors && !isEmpty(errors) ? React.createElement(ValidationErrors, { errors: errors }) : null));
|
|
36
|
+
React.createElement("div", { ref: validationErrorsRef, className: 'evoke-form-renderer-header' }, shouldShowValidationErrors && !isEmpty(errors) ? React.createElement(ValidationErrors, { errors: errors }) : null)));
|
|
34
37
|
};
|
|
35
38
|
// Default slot components for convenience
|
|
36
39
|
export const Title = ({ title }) => (React.createElement(Typography, { sx: {
|
|
@@ -20,7 +20,7 @@ const WithProviders = ({ children }) => {
|
|
|
20
20
|
return React.createElement(MemoryRouter, null, children);
|
|
21
21
|
};
|
|
22
22
|
const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
|
|
23
|
-
describe('
|
|
23
|
+
describe('FormRenderer', () => {
|
|
24
24
|
let server;
|
|
25
25
|
beforeAll(() => {
|
|
26
26
|
server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
|
|
@@ -59,9 +59,9 @@ describe('Form component', () => {
|
|
|
59
59
|
npSpecialtyType1,
|
|
60
60
|
npSpecialtyType2,
|
|
61
61
|
]);
|
|
62
|
-
else if (isEqual(whereFilter, {
|
|
62
|
+
else if (isEqual(whereFilter, { 'licenseType.id': 'rnLicenseType' }))
|
|
63
63
|
return HttpResponse.json([rnSpecialtyType1, rnSpecialtyType2]);
|
|
64
|
-
else if (isEqual(whereFilter, {
|
|
64
|
+
else if (isEqual(whereFilter, { 'licenseType.id': 'npLicenseType' }))
|
|
65
65
|
return HttpResponse.json([npSpecialtyType1, npSpecialtyType2]);
|
|
66
66
|
}
|
|
67
67
|
}), http.get('/api/accessManagement/users', () => HttpResponse.json(users)));
|
|
@@ -657,7 +657,12 @@ describe('Form component', () => {
|
|
|
657
657
|
actionId: '_update',
|
|
658
658
|
objectId: 'relatedObjectTestForm',
|
|
659
659
|
};
|
|
660
|
+
let scrollIntoViewMock;
|
|
661
|
+
let originalScrollIntoView;
|
|
660
662
|
beforeEach(async () => {
|
|
663
|
+
scrollIntoViewMock = vitest.fn();
|
|
664
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
665
|
+
Element.prototype.scrollIntoView = scrollIntoViewMock;
|
|
661
666
|
const relatedObjectTestFormObject = {
|
|
662
667
|
id: 'relatedObjectTestForm',
|
|
663
668
|
name: 'Related Object Test Form',
|
|
@@ -694,9 +699,20 @@ describe('Form component', () => {
|
|
|
694
699
|
type: 'content',
|
|
695
700
|
html: '<div>Specialty Type Form Content</div>',
|
|
696
701
|
},
|
|
702
|
+
{
|
|
703
|
+
type: 'input',
|
|
704
|
+
parameterId: 'requiredField',
|
|
705
|
+
display: {
|
|
706
|
+
label: 'Required Field',
|
|
707
|
+
required: true,
|
|
708
|
+
},
|
|
709
|
+
},
|
|
697
710
|
],
|
|
698
711
|
actionId: '_create',
|
|
699
712
|
objectId: 'specialtyType',
|
|
713
|
+
display: {
|
|
714
|
+
submitLabel: 'Create Specialty Type',
|
|
715
|
+
},
|
|
700
716
|
};
|
|
701
717
|
const specialtyTypeObject = {
|
|
702
718
|
id: 'specialtyType',
|
|
@@ -706,7 +722,14 @@ describe('Form component', () => {
|
|
|
706
722
|
id: '_create',
|
|
707
723
|
name: 'Create',
|
|
708
724
|
type: 'create',
|
|
709
|
-
parameters: [
|
|
725
|
+
parameters: [
|
|
726
|
+
{
|
|
727
|
+
id: 'requiredField',
|
|
728
|
+
name: 'Required Field',
|
|
729
|
+
type: 'string',
|
|
730
|
+
required: true,
|
|
731
|
+
},
|
|
732
|
+
],
|
|
710
733
|
outputEvent: 'created',
|
|
711
734
|
defaultFormId: 'specialtyTypeForm',
|
|
712
735
|
},
|
|
@@ -722,6 +745,9 @@ describe('Form component', () => {
|
|
|
722
745
|
};
|
|
723
746
|
setupTestMocks(specialtyTypeObject, specialtyTypeForm);
|
|
724
747
|
});
|
|
748
|
+
afterEach(() => {
|
|
749
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
750
|
+
});
|
|
725
751
|
it('displays an add button for related object fields', async () => {
|
|
726
752
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
727
753
|
await screen.findByRole('button', { name: 'Add' });
|
|
@@ -814,6 +840,63 @@ describe('Form component', () => {
|
|
|
814
840
|
await user.click(newRecordButton);
|
|
815
841
|
await screen.findByText(/not found/i);
|
|
816
842
|
});
|
|
843
|
+
it('should show validation errors in record creation mode', async () => {
|
|
844
|
+
const user = userEvent.setup();
|
|
845
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
846
|
+
await user.click(await screen.findByRole('button', { name: 'Add' }));
|
|
847
|
+
const newRecordButton = await screen.findByRole('radio', { name: /new/i });
|
|
848
|
+
await user.click(newRecordButton);
|
|
849
|
+
const createSpecialtyTypeButton = await screen.findByRole('button', {
|
|
850
|
+
name: /create specialty type/i,
|
|
851
|
+
});
|
|
852
|
+
await user.click(createSpecialtyTypeButton);
|
|
853
|
+
const errorMessage = await screen.findByRole('listitem');
|
|
854
|
+
expect(errorMessage).toHaveTextContent('Required Field is required');
|
|
855
|
+
});
|
|
856
|
+
it('should clear validation errors after they have been resolved', async () => {
|
|
857
|
+
const user = userEvent.setup();
|
|
858
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
859
|
+
await user.click(await screen.findByRole('button', { name: 'Add' }));
|
|
860
|
+
const newRecordButton = await screen.findByRole('radio', { name: /new/i });
|
|
861
|
+
await user.click(newRecordButton);
|
|
862
|
+
const createSpecialtyTypeButton = await screen.findByRole('button', {
|
|
863
|
+
name: /create specialty type/i,
|
|
864
|
+
});
|
|
865
|
+
await user.click(createSpecialtyTypeButton);
|
|
866
|
+
// Make sure error elements appear
|
|
867
|
+
screen.getByRole('listitem');
|
|
868
|
+
const requiredField = screen.getByRole('textbox', { name: /Required Field */i });
|
|
869
|
+
await user.type(requiredField, 'Some content here...');
|
|
870
|
+
expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
|
|
871
|
+
});
|
|
872
|
+
it('should scroll to validation errors after submission', async () => {
|
|
873
|
+
const user = userEvent.setup();
|
|
874
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
875
|
+
await user.click(await screen.findByRole('button', { name: 'Add' }));
|
|
876
|
+
const newRecordButton = await screen.findByRole('radio', { name: /new/i });
|
|
877
|
+
await user.click(newRecordButton);
|
|
878
|
+
const createSpecialtyTypeButton = await screen.findByRole('button', {
|
|
879
|
+
name: /create specialty type/i,
|
|
880
|
+
});
|
|
881
|
+
await user.click(createSpecialtyTypeButton);
|
|
882
|
+
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
883
|
+
});
|
|
884
|
+
it('should not scroll to validation errors if there are none', async () => async () => {
|
|
885
|
+
const user = userEvent.setup();
|
|
886
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
887
|
+
await user.click(await screen.findByRole('button', { name: 'Add' }));
|
|
888
|
+
const newRecordButton = await screen.findByRole('radio', { name: /new/i });
|
|
889
|
+
await user.click(newRecordButton);
|
|
890
|
+
// Make sure error elements appear
|
|
891
|
+
screen.getByRole('listitem');
|
|
892
|
+
const requiredField = await screen.findByRole('textbox', { name: /Required Field */i });
|
|
893
|
+
await user.type(requiredField, 'Some content here...');
|
|
894
|
+
const createSpecialtyTypeButton = await screen.findByRole('button', {
|
|
895
|
+
name: /create specialty type/i,
|
|
896
|
+
});
|
|
897
|
+
await user.click(createSpecialtyTypeButton);
|
|
898
|
+
expect(scrollIntoViewMock).not.toHaveBeenCalled();
|
|
899
|
+
});
|
|
817
900
|
});
|
|
818
901
|
});
|
|
819
902
|
describe('when in dropdown view', () => {
|
|
@@ -1050,7 +1133,12 @@ describe('Form component', () => {
|
|
|
1050
1133
|
actionId: '_update',
|
|
1051
1134
|
objectId: 'testObjectForCollections',
|
|
1052
1135
|
};
|
|
1136
|
+
let scrollIntoViewMock;
|
|
1137
|
+
let originalScrollIntoView;
|
|
1053
1138
|
beforeEach(() => {
|
|
1139
|
+
scrollIntoViewMock = vitest.fn();
|
|
1140
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
1141
|
+
Element.prototype.scrollIntoView = scrollIntoViewMock;
|
|
1054
1142
|
const collectionFormObject = {
|
|
1055
1143
|
id: 'testObjectForCollections',
|
|
1056
1144
|
name: 'Object for one-to-many collections tests',
|
|
@@ -1120,6 +1208,7 @@ describe('Form component', () => {
|
|
|
1120
1208
|
parameterId: 'name',
|
|
1121
1209
|
display: {
|
|
1122
1210
|
label: 'Name',
|
|
1211
|
+
required: true,
|
|
1123
1212
|
},
|
|
1124
1213
|
},
|
|
1125
1214
|
{
|
|
@@ -1133,9 +1222,15 @@ describe('Form component', () => {
|
|
|
1133
1222
|
],
|
|
1134
1223
|
actionId: '_create',
|
|
1135
1224
|
objectId: 'collectionObject',
|
|
1225
|
+
display: {
|
|
1226
|
+
submitLabel: 'Create Collection Item',
|
|
1227
|
+
},
|
|
1136
1228
|
};
|
|
1137
1229
|
setupTestMocks(collectionObject, collectionObjectForm);
|
|
1138
1230
|
});
|
|
1231
|
+
afterEach(() => {
|
|
1232
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
1233
|
+
});
|
|
1139
1234
|
it('should render collection field', async () => {
|
|
1140
1235
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1141
1236
|
await screen.findByText('Collection');
|
|
@@ -1194,7 +1289,7 @@ describe('Form component', () => {
|
|
|
1194
1289
|
const user = userEvent.setup();
|
|
1195
1290
|
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1196
1291
|
await user.click(await screen.findByRole('button', { name: 'Add' }));
|
|
1197
|
-
await screen.findByRole('button', { name: '
|
|
1292
|
+
await screen.findByRole('button', { name: 'Create Collection Item' });
|
|
1198
1293
|
});
|
|
1199
1294
|
it('should hide related object field in collection item form', async () => {
|
|
1200
1295
|
const user = userEvent.setup();
|
|
@@ -1203,7 +1298,7 @@ describe('Form component', () => {
|
|
|
1203
1298
|
await user.click(addButton);
|
|
1204
1299
|
await screen.findByRole('dialog');
|
|
1205
1300
|
// Make sure other form entry is present
|
|
1206
|
-
await screen.findByRole('textbox', { name: 'Name' });
|
|
1301
|
+
await screen.findByRole('textbox', { name: 'Name *' });
|
|
1207
1302
|
const relatedObjectField = screen.queryByRole('textbox', { name: 'Related Object' });
|
|
1208
1303
|
expect(relatedObjectField).not.toBeInTheDocument();
|
|
1209
1304
|
});
|
|
@@ -1222,12 +1317,136 @@ describe('Form component', () => {
|
|
|
1222
1317
|
const addButton = await screen.findByRole('button', { name: /add/i });
|
|
1223
1318
|
await user.click(addButton);
|
|
1224
1319
|
await screen.findByRole('dialog');
|
|
1225
|
-
const nameField = screen.getByRole('textbox', { name: 'Name' });
|
|
1320
|
+
const nameField = screen.getByRole('textbox', { name: 'Name *' });
|
|
1226
1321
|
await user.type(nameField, 'New Collection Item');
|
|
1227
|
-
const submitButton = screen.getByRole('button', { name: '
|
|
1322
|
+
const submitButton = screen.getByRole('button', { name: 'Create Collection Item' });
|
|
1228
1323
|
await user.click(submitButton);
|
|
1229
1324
|
await screen.findByRole('columnheader', { name: 'Name' });
|
|
1230
1325
|
screen.getByRole('cell', { name: 'New Collection Item' });
|
|
1231
1326
|
});
|
|
1327
|
+
it('should show validation errors', async () => {
|
|
1328
|
+
const user = userEvent.setup();
|
|
1329
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1330
|
+
const addButton = await screen.findByRole('button', { name: /add/i });
|
|
1331
|
+
await user.click(addButton);
|
|
1332
|
+
await screen.findByRole('dialog');
|
|
1333
|
+
const submitButton = screen.getByRole('button', { name: 'Create Collection Item' });
|
|
1334
|
+
await user.click(submitButton);
|
|
1335
|
+
const errorMessage = await screen.findByRole('listitem');
|
|
1336
|
+
expect(errorMessage).toHaveTextContent('Name is required');
|
|
1337
|
+
});
|
|
1338
|
+
it('should hide validation errors after they have been resolved', async () => {
|
|
1339
|
+
const user = userEvent.setup();
|
|
1340
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1341
|
+
const addButton = await screen.findByRole('button', { name: /add/i });
|
|
1342
|
+
await user.click(addButton);
|
|
1343
|
+
await screen.findByRole('dialog');
|
|
1344
|
+
const submitButton = screen.getByRole('button', { name: 'Create Collection Item' });
|
|
1345
|
+
await user.click(submitButton);
|
|
1346
|
+
// Make sure error elements appear
|
|
1347
|
+
screen.getByRole('listitem');
|
|
1348
|
+
const requiredField = screen.getByRole('textbox', { name: /Name */i });
|
|
1349
|
+
await user.type(requiredField, 'Some content here...');
|
|
1350
|
+
expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
|
|
1351
|
+
});
|
|
1352
|
+
it('should scroll to validation errors on submit', async () => {
|
|
1353
|
+
const user = userEvent.setup();
|
|
1354
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1355
|
+
const addButton = await screen.findByRole('button', { name: /add/i });
|
|
1356
|
+
await user.click(addButton);
|
|
1357
|
+
await screen.findByRole('dialog');
|
|
1358
|
+
const submitButton = screen.getByRole('button', { name: 'Create Collection Item' });
|
|
1359
|
+
await user.click(submitButton);
|
|
1360
|
+
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
1361
|
+
});
|
|
1362
|
+
it('should not scroll to validation errors if there are none', async () => async () => {
|
|
1363
|
+
const user = userEvent.setup();
|
|
1364
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1365
|
+
const addButton = await screen.findByRole('button', { name: /add/i });
|
|
1366
|
+
await user.click(addButton);
|
|
1367
|
+
await screen.findByRole('dialog');
|
|
1368
|
+
const requiredField = await screen.findByRole('textbox', { name: /Name */i });
|
|
1369
|
+
await user.type(requiredField, 'Some content here...');
|
|
1370
|
+
const submitButton = screen.getByRole('button', { name: 'Create Collection Item' });
|
|
1371
|
+
await user.click(submitButton);
|
|
1372
|
+
expect(scrollIntoViewMock).not.toHaveBeenCalled();
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
describe('when passed a text field entry', () => {
|
|
1376
|
+
it('should render text field', async () => {
|
|
1377
|
+
const form = {
|
|
1378
|
+
id: 'textFieldTestForm',
|
|
1379
|
+
name: 'Text Field Test Form',
|
|
1380
|
+
entries: [
|
|
1381
|
+
{
|
|
1382
|
+
type: 'inputField',
|
|
1383
|
+
input: {
|
|
1384
|
+
id: 'textField',
|
|
1385
|
+
type: 'string',
|
|
1386
|
+
},
|
|
1387
|
+
display: {
|
|
1388
|
+
label: 'Text Field',
|
|
1389
|
+
},
|
|
1390
|
+
},
|
|
1391
|
+
],
|
|
1392
|
+
actionId: '_update',
|
|
1393
|
+
objectId: 'textFieldTestObject',
|
|
1394
|
+
};
|
|
1395
|
+
const textFieldTestObject = {
|
|
1396
|
+
id: 'textFieldTestObject',
|
|
1397
|
+
name: 'Text Field Test Object',
|
|
1398
|
+
actions: [
|
|
1399
|
+
{
|
|
1400
|
+
id: '_update',
|
|
1401
|
+
name: 'Update',
|
|
1402
|
+
type: 'update',
|
|
1403
|
+
outputEvent: 'updated',
|
|
1404
|
+
},
|
|
1405
|
+
],
|
|
1406
|
+
properties: [],
|
|
1407
|
+
};
|
|
1408
|
+
server.use(http.get(`/api/data/objects/${textFieldTestObject.id}/effective`, () => HttpResponse.json(textFieldTestObject)));
|
|
1409
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1410
|
+
await screen.findByRole('textbox', { name: 'Text Field' });
|
|
1411
|
+
});
|
|
1412
|
+
it('should allow text input in text field', async () => {
|
|
1413
|
+
const user = userEvent.setup();
|
|
1414
|
+
const form = {
|
|
1415
|
+
id: 'textFieldTestForm',
|
|
1416
|
+
name: 'Text Field Test Form',
|
|
1417
|
+
entries: [
|
|
1418
|
+
{
|
|
1419
|
+
type: 'inputField',
|
|
1420
|
+
input: {
|
|
1421
|
+
id: 'textField',
|
|
1422
|
+
type: 'string',
|
|
1423
|
+
},
|
|
1424
|
+
display: {
|
|
1425
|
+
label: 'Text Field',
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
],
|
|
1429
|
+
actionId: '_update',
|
|
1430
|
+
objectId: 'textFieldTestObject',
|
|
1431
|
+
};
|
|
1432
|
+
const textFieldTestObject = {
|
|
1433
|
+
id: 'textFieldTestObject',
|
|
1434
|
+
name: 'Text Field Test Object',
|
|
1435
|
+
actions: [
|
|
1436
|
+
{
|
|
1437
|
+
id: '_update',
|
|
1438
|
+
name: 'Update',
|
|
1439
|
+
type: 'update',
|
|
1440
|
+
outputEvent: 'updated',
|
|
1441
|
+
},
|
|
1442
|
+
],
|
|
1443
|
+
properties: [],
|
|
1444
|
+
};
|
|
1445
|
+
server.use(http.get(`/api/data/objects/${textFieldTestObject.id}/effective`, () => HttpResponse.json(textFieldTestObject)));
|
|
1446
|
+
render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
|
|
1447
|
+
const textField = await screen.findByRole('textbox', { name: 'Text Field' });
|
|
1448
|
+
await user.type(textField, 'Test Input');
|
|
1449
|
+
expect(textField).toHaveValue('Test Input');
|
|
1450
|
+
});
|
|
1232
1451
|
});
|
|
1233
1452
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as matchers from '@testing-library/jest-dom/matchers';
|
|
2
|
-
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
2
|
+
import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
4
|
import { isEqual } from 'lodash';
|
|
5
5
|
import { http, HttpResponse } from 'msw';
|
|
@@ -16,14 +16,11 @@ global.ResizeObserver = class ResizeObserver {
|
|
|
16
16
|
unobserve() { }
|
|
17
17
|
disconnect() { }
|
|
18
18
|
};
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
portalSelectors.forEach((selector) => {
|
|
22
|
-
// eslint-disable-next-line testing-library/no-node-access
|
|
23
|
-
document.querySelectorAll(selector).forEach((el) => el.remove());
|
|
24
|
-
});
|
|
19
|
+
const WithProviders = ({ children }) => {
|
|
20
|
+
return React.createElement(MemoryRouter, null, children);
|
|
25
21
|
};
|
|
26
|
-
|
|
22
|
+
const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
|
|
23
|
+
describe('FormRendererContainer', () => {
|
|
27
24
|
let server;
|
|
28
25
|
beforeAll(() => {
|
|
29
26
|
server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
|
|
@@ -71,7 +68,6 @@ describe('Form component', () => {
|
|
|
71
68
|
});
|
|
72
69
|
afterEach(() => {
|
|
73
70
|
server.resetHandlers();
|
|
74
|
-
removePoppers();
|
|
75
71
|
});
|
|
76
72
|
describe('validation criteria', () => {
|
|
77
73
|
it(`filters related object field with validation criteria that references a defaulted related object's nested data`, async () => {
|
|
@@ -81,13 +77,11 @@ describe('Form component', () => {
|
|
|
81
77
|
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
82
78
|
return HttpResponse.json(createSpecialtyForm);
|
|
83
79
|
}));
|
|
84
|
-
render(React.createElement(
|
|
85
|
-
React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_create', associatedObject: { propertyId: 'license', instanceId: 'rnLicense' } })));
|
|
80
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_create', associatedObject: { propertyId: 'license', instanceId: 'rnLicense' } }));
|
|
86
81
|
// Validate that the license field is hidden
|
|
87
82
|
await waitFor(() => expect(screen.queryByRole('combobox', { name: 'License' })).not.toBeInTheDocument());
|
|
88
83
|
// Validate that specialty type dropdown is only rendering specialty types that are associated with the selected license.
|
|
89
84
|
const specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' });
|
|
90
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
91
85
|
await user.click(specialtyType);
|
|
92
86
|
const openAutocomplete = await screen.findByRole('listbox');
|
|
93
87
|
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
|
|
@@ -100,4 +94,200 @@ describe('Form component', () => {
|
|
|
100
94
|
});
|
|
101
95
|
});
|
|
102
96
|
});
|
|
97
|
+
it('should display a submit button', async () => {
|
|
98
|
+
const form = {
|
|
99
|
+
id: 'simpleForm',
|
|
100
|
+
name: 'Simple Form',
|
|
101
|
+
entries: [],
|
|
102
|
+
actionId: '_create',
|
|
103
|
+
objectId: 'simpleObject',
|
|
104
|
+
};
|
|
105
|
+
const simpleObject = {
|
|
106
|
+
id: 'simpleObject',
|
|
107
|
+
name: 'Simple Object',
|
|
108
|
+
actions: [
|
|
109
|
+
{
|
|
110
|
+
id: '_create',
|
|
111
|
+
name: 'Create',
|
|
112
|
+
type: 'create',
|
|
113
|
+
parameters: [],
|
|
114
|
+
outputEvent: 'created',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
properties: [],
|
|
118
|
+
};
|
|
119
|
+
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
120
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
121
|
+
await screen.findByRole('button', { name: 'Submit' });
|
|
122
|
+
});
|
|
123
|
+
it('should display a button to discard changes', async () => {
|
|
124
|
+
const form = {
|
|
125
|
+
id: 'simpleForm',
|
|
126
|
+
name: 'Simple Form',
|
|
127
|
+
entries: [],
|
|
128
|
+
actionId: '_create',
|
|
129
|
+
objectId: 'simpleObject',
|
|
130
|
+
};
|
|
131
|
+
const simpleObject = {
|
|
132
|
+
id: 'simpleObject',
|
|
133
|
+
name: 'Simple Object',
|
|
134
|
+
actions: [
|
|
135
|
+
{
|
|
136
|
+
id: '_create',
|
|
137
|
+
name: 'Create',
|
|
138
|
+
type: 'create',
|
|
139
|
+
parameters: [],
|
|
140
|
+
outputEvent: 'created',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
properties: [],
|
|
144
|
+
};
|
|
145
|
+
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
146
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
147
|
+
await screen.findByRole('button', { name: 'Discard Changes' });
|
|
148
|
+
});
|
|
149
|
+
it('should reset the form when discarding changes', async () => {
|
|
150
|
+
const form = {
|
|
151
|
+
id: 'simpleForm2',
|
|
152
|
+
name: 'Simple Form',
|
|
153
|
+
entries: [
|
|
154
|
+
{
|
|
155
|
+
type: 'inputField',
|
|
156
|
+
input: {
|
|
157
|
+
id: 'firstName',
|
|
158
|
+
type: 'string',
|
|
159
|
+
},
|
|
160
|
+
display: {
|
|
161
|
+
label: 'First Name',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
actionId: '_create',
|
|
166
|
+
objectId: 'simpleObject2',
|
|
167
|
+
};
|
|
168
|
+
const simpleObject = {
|
|
169
|
+
id: 'simpleObject2',
|
|
170
|
+
name: 'Simple Object',
|
|
171
|
+
actions: [
|
|
172
|
+
{
|
|
173
|
+
id: '_create',
|
|
174
|
+
name: 'Create',
|
|
175
|
+
type: 'create',
|
|
176
|
+
outputEvent: 'created',
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
properties: [
|
|
180
|
+
{
|
|
181
|
+
id: 'firstName',
|
|
182
|
+
name: 'First Name',
|
|
183
|
+
type: 'string',
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
188
|
+
const user = userEvent.setup();
|
|
189
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
190
|
+
const firstNameInput = await screen.findByRole('textbox', { name: 'First Name' });
|
|
191
|
+
await user.type(firstNameInput, 'John');
|
|
192
|
+
const discardButton = await screen.findByRole('button', { name: 'Discard Changes' });
|
|
193
|
+
await user.click(discardButton);
|
|
194
|
+
await waitFor(() => expect(firstNameInput).toHaveValue(''));
|
|
195
|
+
});
|
|
196
|
+
describe('when submitting a form with validation', () => {
|
|
197
|
+
const form = {
|
|
198
|
+
id: 'validationTestForm',
|
|
199
|
+
name: 'Validation Test Form',
|
|
200
|
+
entries: [
|
|
201
|
+
{
|
|
202
|
+
type: 'input',
|
|
203
|
+
parameterId: 'requiredField',
|
|
204
|
+
display: {
|
|
205
|
+
label: 'Required Field',
|
|
206
|
+
required: true,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
actionId: '_create',
|
|
211
|
+
objectId: 'validationTestObject',
|
|
212
|
+
};
|
|
213
|
+
let scrollIntoViewMock;
|
|
214
|
+
let originalScrollIntoView;
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
scrollIntoViewMock = vitest.fn();
|
|
217
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
218
|
+
Element.prototype.scrollIntoView = scrollIntoViewMock;
|
|
219
|
+
});
|
|
220
|
+
afterEach(() => {
|
|
221
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
222
|
+
});
|
|
223
|
+
const validationTestObject = {
|
|
224
|
+
id: 'validationTestObject',
|
|
225
|
+
name: 'Validation Test Object',
|
|
226
|
+
actions: [
|
|
227
|
+
{
|
|
228
|
+
id: '_create',
|
|
229
|
+
name: 'Create',
|
|
230
|
+
type: 'create',
|
|
231
|
+
parameters: [
|
|
232
|
+
{
|
|
233
|
+
id: 'requiredField',
|
|
234
|
+
name: 'Required Field',
|
|
235
|
+
type: 'string',
|
|
236
|
+
required: true,
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
outputEvent: 'created',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
properties: [
|
|
243
|
+
{
|
|
244
|
+
id: 'requiredField',
|
|
245
|
+
name: 'Required Field',
|
|
246
|
+
type: 'string',
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
beforeEach(() => {
|
|
251
|
+
server.use(http.get(`/api/data/objects/${validationTestObject.id}/effective`, () => HttpResponse.json(validationTestObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
252
|
+
});
|
|
253
|
+
it('should display validation errors after trying to submit the form', async () => {
|
|
254
|
+
const user = userEvent.setup();
|
|
255
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
256
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
257
|
+
await user.click(submitButton);
|
|
258
|
+
// List items are named by author, but they don't
|
|
259
|
+
// need to be given an accessible name here because their text content is clear enough.
|
|
260
|
+
// As such, we use getByRole and ensure it has the correct text
|
|
261
|
+
const errorMessage = await screen.findByRole('listitem');
|
|
262
|
+
expect(errorMessage).toHaveTextContent('Required Field is required');
|
|
263
|
+
});
|
|
264
|
+
it('should clear validation errors after they have been resolved', async () => {
|
|
265
|
+
const user = userEvent.setup();
|
|
266
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
267
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
268
|
+
await user.click(submitButton);
|
|
269
|
+
// Make sure error elements appear
|
|
270
|
+
screen.getByRole('listitem');
|
|
271
|
+
const requiredField = screen.getByRole('textbox', { name: /Required Field */i });
|
|
272
|
+
await user.type(requiredField, 'Some content here...');
|
|
273
|
+
expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
|
|
274
|
+
});
|
|
275
|
+
it('should scroll to validation errors after submission', async () => {
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
278
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
279
|
+
await user.click(submitButton);
|
|
280
|
+
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
it('should not scroll to validation errors after submission if there are none', async () => {
|
|
283
|
+
const user = userEvent.setup();
|
|
284
|
+
server.use(http.post(`/api/data/objects/${validationTestObject.id}/instances/actions`, () => HttpResponse.json({}, { status: 200 })));
|
|
285
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
286
|
+
const requiredField = await screen.findByRole('textbox', { name: /Required Field */i });
|
|
287
|
+
await user.type(requiredField, 'Some content here...');
|
|
288
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
289
|
+
await user.click(submitButton);
|
|
290
|
+
expect(scrollIntoViewMock).not.toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
103
293
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evoke-platform/ui-components",
|
|
3
|
-
"version": "1.10.0-dev.
|
|
3
|
+
"version": "1.10.0-dev.8",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/published/index.js",
|
|
6
6
|
"module": "dist/published/index.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "vitest",
|
|
22
22
|
"test:ui": "vitest --ui",
|
|
23
|
+
"test:preview": "vitest-preview",
|
|
23
24
|
"copy-styles": "copyfiles -u 1 src/styles/*.css dist/published/",
|
|
24
25
|
"build": "rm -rf ./dist && tsc && npm run copy-styles",
|
|
25
26
|
"build:cjs": "tsc --module CommonJS --outDir dist/cjs",
|
|
@@ -88,6 +89,7 @@
|
|
|
88
89
|
"storybook": "^7.6.20",
|
|
89
90
|
"typescript": "^4.7.3",
|
|
90
91
|
"vitest": "^1.6.0",
|
|
92
|
+
"vitest-preview": "^0.0.3",
|
|
91
93
|
"webpack": "^5.74.0",
|
|
92
94
|
"yalc": "^1.0.0-pre.53"
|
|
93
95
|
},
|