@evoke-platform/ui-components 1.10.0-testing.9 → 1.10.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/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
- package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
- package/dist/published/components/core/TextField/TextField.js +1 -1
- package/dist/published/components/core/TextField/TextField.test.js +0 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
- package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
- package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
- package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
- package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
- package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
- package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
- package/dist/published/components/custom/FormField/FormField.js +17 -5
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
- package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
- package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
- package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
- package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
- package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
- package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
- package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
- package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
- package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
- package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +6 -23
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +115 -87
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
- package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
- package/dist/published/components/custom/FormV2/components/Header.js +47 -9
- package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
- package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
- package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
- package/dist/published/components/custom/FormV2/components/utils.js +83 -13
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +411 -44
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
- package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
- package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
- package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
- package/dist/published/components/custom/index.d.ts +2 -0
- package/dist/published/components/custom/index.js +1 -0
- package/dist/published/index.d.ts +6 -6
- package/dist/published/index.js +1 -1
- package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
- package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
- package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
- package/dist/published/stories/FormRendererContainer.stories.js +5 -0
- package/dist/published/stories/FormRendererData.d.ts +12 -0
- package/dist/published/stories/FormRendererData.js +26 -1
- package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
- package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
- package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
- package/dist/published/stories/ViewDetailsV2Data.js +203 -0
- package/dist/published/stories/sharedMswHandlers.js +49 -10
- package/dist/published/theme/hooks.d.ts +4 -3
- package/dist/published/types.d.ts +3 -0
- package/package.json +10 -8
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
1
|
+
import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
|
|
3
2
|
import userEvent from '@testing-library/user-event';
|
|
4
3
|
import { isEqual } from 'lodash';
|
|
5
4
|
import { http, HttpResponse } from 'msw';
|
|
@@ -8,22 +7,18 @@ import React from 'react';
|
|
|
8
7
|
import { MemoryRouter } from 'react-router-dom';
|
|
9
8
|
import { expect, it } from 'vitest';
|
|
10
9
|
import FormRendererContainer from '../FormRendererContainer';
|
|
11
|
-
import { createSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, } from './test-data';
|
|
12
|
-
expect.extend(matchers);
|
|
10
|
+
import { createSpecialtyForm, licenseForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, } from './test-data';
|
|
13
11
|
// Mock ResizeObserver
|
|
14
12
|
global.ResizeObserver = class ResizeObserver {
|
|
15
13
|
observe() { }
|
|
16
14
|
unobserve() { }
|
|
17
15
|
disconnect() { }
|
|
18
16
|
};
|
|
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
|
-
});
|
|
17
|
+
const WithProviders = ({ children }) => {
|
|
18
|
+
return React.createElement(MemoryRouter, null, children);
|
|
25
19
|
};
|
|
26
|
-
|
|
20
|
+
const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
|
|
21
|
+
describe('FormRendererContainer', () => {
|
|
27
22
|
let server;
|
|
28
23
|
beforeAll(() => {
|
|
29
24
|
server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
|
|
@@ -71,7 +66,6 @@ describe('Form component', () => {
|
|
|
71
66
|
});
|
|
72
67
|
afterEach(() => {
|
|
73
68
|
server.resetHandlers();
|
|
74
|
-
removePoppers();
|
|
75
69
|
});
|
|
76
70
|
describe('validation criteria', () => {
|
|
77
71
|
it(`filters related object field with validation criteria that references a defaulted related object's nested data`, async () => {
|
|
@@ -81,13 +75,12 @@ describe('Form component', () => {
|
|
|
81
75
|
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
82
76
|
return HttpResponse.json(createSpecialtyForm);
|
|
83
77
|
}));
|
|
84
|
-
render(React.createElement(
|
|
85
|
-
|
|
78
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_create', associatedObject: { propertyId: 'license', instanceId: 'rnLicense' } }));
|
|
79
|
+
// Give the form renderer some time to load
|
|
80
|
+
const specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' }, { timeout: 3000 });
|
|
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
|
-
const specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' });
|
|
90
|
-
await new Promise((r) => setTimeout(r, 5000));
|
|
91
84
|
await user.click(specialtyType);
|
|
92
85
|
const openAutocomplete = await screen.findByRole('listbox');
|
|
93
86
|
await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
|
|
@@ -100,4 +93,978 @@ describe('Form component', () => {
|
|
|
100
93
|
});
|
|
101
94
|
});
|
|
102
95
|
});
|
|
96
|
+
describe('autosave functionality', () => {
|
|
97
|
+
it('should trigger autosave when field loses focus', async () => {
|
|
98
|
+
const user = userEvent.setup();
|
|
99
|
+
const autosaveActionSpy = vi.fn();
|
|
100
|
+
server.use(http.get('/api/data/objects/specialty/instances/test-instance', () => {
|
|
101
|
+
return HttpResponse.json({
|
|
102
|
+
id: 'test-instance',
|
|
103
|
+
name: 'Original Name',
|
|
104
|
+
specialtyType: null,
|
|
105
|
+
license: null,
|
|
106
|
+
});
|
|
107
|
+
}), http.get('/api/data/objects/specialty/instances/test-instance/object', () => {
|
|
108
|
+
return HttpResponse.json(specialtyObject);
|
|
109
|
+
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
110
|
+
return HttpResponse.json({
|
|
111
|
+
...createSpecialtyForm,
|
|
112
|
+
actionId: '_update',
|
|
113
|
+
autosaveActionId: '_autosave',
|
|
114
|
+
});
|
|
115
|
+
}), http.post('/api/data/objects/specialty/instances/test-instance/actions', async ({ request }) => {
|
|
116
|
+
const body = (await request.json());
|
|
117
|
+
autosaveActionSpy(body);
|
|
118
|
+
return HttpResponse.json({
|
|
119
|
+
id: 'test-instance',
|
|
120
|
+
name: body.input.name,
|
|
121
|
+
specialtyType: null,
|
|
122
|
+
license: null,
|
|
123
|
+
});
|
|
124
|
+
}));
|
|
125
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
126
|
+
await waitFor(() => {
|
|
127
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
128
|
+
});
|
|
129
|
+
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
130
|
+
// Clear the existing value and type new value
|
|
131
|
+
await user.clear(nameField);
|
|
132
|
+
await user.type(nameField, 'Test Specialty');
|
|
133
|
+
await user.tab(); // Blur the field
|
|
134
|
+
// Verify the data being saved
|
|
135
|
+
expect(autosaveActionSpy).toHaveBeenCalledWith(expect.objectContaining({
|
|
136
|
+
actionId: '_autosave',
|
|
137
|
+
input: expect.objectContaining({
|
|
138
|
+
name: 'Test Specialty',
|
|
139
|
+
}),
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
it('should show saving indicator during autosave', async () => {
|
|
143
|
+
const user = userEvent.setup();
|
|
144
|
+
let resolveSave;
|
|
145
|
+
const savePromise = new Promise((resolve) => {
|
|
146
|
+
resolveSave = resolve;
|
|
147
|
+
});
|
|
148
|
+
const autosaveActionSpy = vi.fn();
|
|
149
|
+
server.use(http.get('/api/data/objects/specialty/instances/test-instance', () => {
|
|
150
|
+
return HttpResponse.json({
|
|
151
|
+
id: 'test-instance',
|
|
152
|
+
name: '',
|
|
153
|
+
specialtyType: null,
|
|
154
|
+
license: null,
|
|
155
|
+
});
|
|
156
|
+
}), http.get('/api/data/objects/specialty/instances/test-instance/object', () => {
|
|
157
|
+
return HttpResponse.json(specialtyObject);
|
|
158
|
+
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
159
|
+
return HttpResponse.json({
|
|
160
|
+
...createSpecialtyForm,
|
|
161
|
+
actionId: '_update',
|
|
162
|
+
autosaveActionId: '_autosave',
|
|
163
|
+
});
|
|
164
|
+
}), http.post('/api/data/objects/specialty/instances/test-instance/actions', async ({ request }) => {
|
|
165
|
+
const body = (await request.json());
|
|
166
|
+
autosaveActionSpy(body);
|
|
167
|
+
await savePromise; // Wait for manual resolution
|
|
168
|
+
return HttpResponse.json({
|
|
169
|
+
id: 'test-instance',
|
|
170
|
+
name: body.input.name,
|
|
171
|
+
specialtyType: null,
|
|
172
|
+
license: null,
|
|
173
|
+
});
|
|
174
|
+
}));
|
|
175
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
178
|
+
});
|
|
179
|
+
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
180
|
+
await user.type(nameField, 'Test Specialty');
|
|
181
|
+
await user.tab();
|
|
182
|
+
// Should show saving indicator
|
|
183
|
+
await waitFor(() => {
|
|
184
|
+
expect(screen.getByText('Saving')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
// Verify the API was called
|
|
187
|
+
expect(autosaveActionSpy).toHaveBeenCalled();
|
|
188
|
+
// Complete the save
|
|
189
|
+
resolveSave({
|
|
190
|
+
id: 'test-instance',
|
|
191
|
+
name: 'Test Specialty',
|
|
192
|
+
specialtyType: null,
|
|
193
|
+
license: null,
|
|
194
|
+
});
|
|
195
|
+
// Saving indicator should disappear
|
|
196
|
+
await waitFor(() => {
|
|
197
|
+
expect(screen.queryByText('Saving')).not.toBeInTheDocument();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
it('should hide saving indicator after autosave fails', async () => {
|
|
201
|
+
const user = userEvent.setup();
|
|
202
|
+
const autosaveActionSpy = vi.fn();
|
|
203
|
+
server.use(http.get('/api/data/objects/specialty/instances/test-instance', () => {
|
|
204
|
+
return HttpResponse.json({
|
|
205
|
+
id: 'test-instance',
|
|
206
|
+
name: '',
|
|
207
|
+
specialtyType: null,
|
|
208
|
+
license: null,
|
|
209
|
+
});
|
|
210
|
+
}), http.get('/api/data/objects/specialty/instances/test-instance/object', () => {
|
|
211
|
+
return HttpResponse.json(specialtyObject);
|
|
212
|
+
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
213
|
+
return HttpResponse.json({
|
|
214
|
+
...createSpecialtyForm,
|
|
215
|
+
actionId: '_update',
|
|
216
|
+
autosaveActionId: '_autosave',
|
|
217
|
+
});
|
|
218
|
+
}), http.post('/api/data/objects/specialty/instances/test-instance/actions', () => {
|
|
219
|
+
autosaveActionSpy();
|
|
220
|
+
return HttpResponse.json({ error: 'Save failed' }, { status: 500 });
|
|
221
|
+
}));
|
|
222
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
225
|
+
});
|
|
226
|
+
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
227
|
+
await user.type(nameField, 'Test Specialty');
|
|
228
|
+
await user.tab();
|
|
229
|
+
// Wait for autosave to complete
|
|
230
|
+
await waitFor(() => {
|
|
231
|
+
expect(autosaveActionSpy).toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
// Saving indicator should be hidden after error
|
|
234
|
+
await waitFor(() => {
|
|
235
|
+
expect(screen.queryByText('Saving')).not.toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
it('should not trigger autosave when field value has not changed', async () => {
|
|
239
|
+
const user = userEvent.setup();
|
|
240
|
+
const autosaveActionSpy = vi.fn();
|
|
241
|
+
server.use(http.get('/api/data/objects/specialty/instances/test-instance', () => {
|
|
242
|
+
return HttpResponse.json({
|
|
243
|
+
id: 'test-instance',
|
|
244
|
+
name: 'Original Name',
|
|
245
|
+
specialtyType: null,
|
|
246
|
+
license: null,
|
|
247
|
+
});
|
|
248
|
+
}), http.get('/api/data/objects/specialty/instances/test-instance/object', () => {
|
|
249
|
+
return HttpResponse.json(specialtyObject);
|
|
250
|
+
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
251
|
+
return HttpResponse.json({
|
|
252
|
+
...createSpecialtyForm,
|
|
253
|
+
actionId: '_update',
|
|
254
|
+
autosaveActionId: '_autosave',
|
|
255
|
+
});
|
|
256
|
+
}), http.post('/api/data/objects/specialty/instances/test-instance/actions', async ({ request }) => {
|
|
257
|
+
const body = (await request.json());
|
|
258
|
+
autosaveActionSpy(body);
|
|
259
|
+
return HttpResponse.json({
|
|
260
|
+
id: 'test-instance',
|
|
261
|
+
name: body.input.name,
|
|
262
|
+
specialtyType: null,
|
|
263
|
+
license: null,
|
|
264
|
+
});
|
|
265
|
+
}));
|
|
266
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
267
|
+
await waitFor(() => {
|
|
268
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
const nameField = await screen.findByRole('textbox', { name: 'Name' });
|
|
271
|
+
// Click into the field and blur it without changing value
|
|
272
|
+
await user.click(nameField);
|
|
273
|
+
await user.tab(); // Blur the field
|
|
274
|
+
await waitFor(() => {
|
|
275
|
+
expect(nameField).not.toHaveFocus();
|
|
276
|
+
});
|
|
277
|
+
// Wait a bit to ensure no autosave is triggered
|
|
278
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
279
|
+
// Verify autosave was NOT called since value didn't change
|
|
280
|
+
expect(autosaveActionSpy).not.toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
it('should trigger autosave when address field loses focus after change', async () => {
|
|
283
|
+
const user = userEvent.setup();
|
|
284
|
+
const autosaveActionSpy = vi.fn();
|
|
285
|
+
server.use(http.get('/api/data/objects/license/instances/test-license', () => {
|
|
286
|
+
return HttpResponse.json({
|
|
287
|
+
id: 'test-license',
|
|
288
|
+
name: 'RN-123456',
|
|
289
|
+
address: {
|
|
290
|
+
line1: '123 Main St',
|
|
291
|
+
city: 'Boston',
|
|
292
|
+
state: 'MA',
|
|
293
|
+
zipCode: '02101',
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}), http.get('/api/data/objects/license/instances/test-license/object', () => {
|
|
297
|
+
return HttpResponse.json(licenseObject);
|
|
298
|
+
}), http.get('/api/data/forms/licenseForm', () => {
|
|
299
|
+
return HttpResponse.json({
|
|
300
|
+
...licenseForm,
|
|
301
|
+
autosaveActionId: '_autosave',
|
|
302
|
+
});
|
|
303
|
+
}), http.get('/api/data/locations/search', () => {
|
|
304
|
+
return HttpResponse.json([]);
|
|
305
|
+
}), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
|
|
306
|
+
const body = (await request.json());
|
|
307
|
+
autosaveActionSpy(body);
|
|
308
|
+
return HttpResponse.json({
|
|
309
|
+
id: 'test-license',
|
|
310
|
+
name: body.input.name || 'RN-123456',
|
|
311
|
+
address: body.input.address || {
|
|
312
|
+
line1: '123 Main St',
|
|
313
|
+
city: 'Boston',
|
|
314
|
+
state: 'MA',
|
|
315
|
+
zipCode: '02101',
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
}));
|
|
319
|
+
render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
|
|
320
|
+
await waitFor(() => {
|
|
321
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
322
|
+
});
|
|
323
|
+
// Find the city field
|
|
324
|
+
const cityField = await screen.findByRole('textbox', { name: 'City' });
|
|
325
|
+
// Clear and type new value
|
|
326
|
+
await user.clear(cityField);
|
|
327
|
+
await user.type(cityField, 'Cambridge');
|
|
328
|
+
await user.tab(); // Blur the field
|
|
329
|
+
// Verify autosave was eventually called and the final call contains the updated city
|
|
330
|
+
await waitFor(() => {
|
|
331
|
+
expect(autosaveActionSpy).toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
const lastCall = autosaveActionSpy.mock.lastCall?.[0];
|
|
334
|
+
expect(lastCall).toEqual(expect.objectContaining({
|
|
335
|
+
input: expect.objectContaining({
|
|
336
|
+
address: expect.objectContaining({
|
|
337
|
+
city: 'Cambridge',
|
|
338
|
+
}),
|
|
339
|
+
}),
|
|
340
|
+
}));
|
|
341
|
+
});
|
|
342
|
+
it('should not trigger autosave when address field loses focus without changes', async () => {
|
|
343
|
+
const user = userEvent.setup();
|
|
344
|
+
const autosaveActionSpy = vi.fn();
|
|
345
|
+
server.use(http.get('/api/data/objects/license/instances/test-license', () => {
|
|
346
|
+
return HttpResponse.json({
|
|
347
|
+
id: 'test-license',
|
|
348
|
+
name: 'RN-123456',
|
|
349
|
+
address: {
|
|
350
|
+
line1: '123 Main St',
|
|
351
|
+
city: 'Boston',
|
|
352
|
+
state: 'MA',
|
|
353
|
+
zipCode: '02101',
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}), http.get('/api/data/objects/license/instances/test-license/object', () => {
|
|
357
|
+
return HttpResponse.json(licenseObject);
|
|
358
|
+
}), http.get('/api/data/objects/license/effective', (req) => {
|
|
359
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
360
|
+
if (sanitizedVersion === 'true') {
|
|
361
|
+
return HttpResponse.json(licenseObject);
|
|
362
|
+
}
|
|
363
|
+
}), http.get('/api/data/forms/licenseForm', () => {
|
|
364
|
+
return HttpResponse.json({
|
|
365
|
+
...licenseForm,
|
|
366
|
+
autosaveActionId: '_autosave',
|
|
367
|
+
});
|
|
368
|
+
}), http.get('/api/data/locations/search', () => {
|
|
369
|
+
return HttpResponse.json([]);
|
|
370
|
+
}), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
|
|
371
|
+
const body = (await request.json());
|
|
372
|
+
autosaveActionSpy(body);
|
|
373
|
+
return HttpResponse.json({
|
|
374
|
+
id: 'test-license',
|
|
375
|
+
name: 'RN-123456',
|
|
376
|
+
address: body.input.address,
|
|
377
|
+
});
|
|
378
|
+
}));
|
|
379
|
+
render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
|
|
380
|
+
await waitFor(() => {
|
|
381
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
382
|
+
});
|
|
383
|
+
// Find the city field
|
|
384
|
+
const cityField = await screen.findByRole('textbox', { name: 'City' });
|
|
385
|
+
// Click into field and blur without changing
|
|
386
|
+
await user.click(cityField);
|
|
387
|
+
await user.tab();
|
|
388
|
+
// Wait to ensure no autosave is triggered
|
|
389
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
390
|
+
// Verify autosave was NOT called since value didn't change
|
|
391
|
+
expect(autosaveActionSpy).not.toHaveBeenCalled();
|
|
392
|
+
});
|
|
393
|
+
it('should trigger autosave for multiple address fields when line1 autocompletes', async () => {
|
|
394
|
+
const user = userEvent.setup();
|
|
395
|
+
const autosaveActionSpy = vi.fn();
|
|
396
|
+
server.use(http.get('/api/data/objects/license/instances/test-license', () => {
|
|
397
|
+
return HttpResponse.json({
|
|
398
|
+
id: 'test-license',
|
|
399
|
+
name: 'RN-123456',
|
|
400
|
+
address: {
|
|
401
|
+
line1: '',
|
|
402
|
+
city: '',
|
|
403
|
+
state: '',
|
|
404
|
+
zipCode: '',
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
}), http.get('/api/data/objects/license/instances/test-license/object', () => {
|
|
408
|
+
return HttpResponse.json(licenseObject);
|
|
409
|
+
}), http.get('/api/data/objects/license/effective', (req) => {
|
|
410
|
+
const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
|
|
411
|
+
if (sanitizedVersion === 'true') {
|
|
412
|
+
return HttpResponse.json(licenseObject);
|
|
413
|
+
}
|
|
414
|
+
}), http.get('/api/data/forms/licenseForm', () => {
|
|
415
|
+
return HttpResponse.json({
|
|
416
|
+
...licenseForm,
|
|
417
|
+
autosaveActionId: '_autosave',
|
|
418
|
+
});
|
|
419
|
+
}), http.get('/api/data/locations/search', () => {
|
|
420
|
+
return HttpResponse.json([
|
|
421
|
+
{
|
|
422
|
+
address: {
|
|
423
|
+
line1: '456 Oak Street',
|
|
424
|
+
city: 'Springfield',
|
|
425
|
+
state: 'MA',
|
|
426
|
+
zipCode: '01101',
|
|
427
|
+
county: 'Hampden',
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
]);
|
|
431
|
+
}), http.post('/api/data/objects/license/instances/test-license/actions', async ({ request }) => {
|
|
432
|
+
const body = (await request.json());
|
|
433
|
+
autosaveActionSpy(body);
|
|
434
|
+
return HttpResponse.json({
|
|
435
|
+
id: 'test-license',
|
|
436
|
+
name: 'RN-123456',
|
|
437
|
+
address: body.input.address,
|
|
438
|
+
});
|
|
439
|
+
}));
|
|
440
|
+
render(React.createElement(FormRendererContainer, { objectId: 'license', formId: 'licenseForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-license' }));
|
|
441
|
+
await waitFor(() => {
|
|
442
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
443
|
+
});
|
|
444
|
+
// Find the line1 field (it's a searchbox because of address autocomplete)
|
|
445
|
+
const line1Field = await screen.findByRole('searchbox', { name: 'Address Line 1' });
|
|
446
|
+
// Type to trigger autocomplete
|
|
447
|
+
await user.type(line1Field, '456');
|
|
448
|
+
// Wait for and select the autocomplete option
|
|
449
|
+
const autocompleteOption = await screen.findByText('456 Oak Street');
|
|
450
|
+
await user.click(autocompleteOption);
|
|
451
|
+
// Verify autosave was eventually called and the final call contains the expected address values
|
|
452
|
+
await waitFor(() => {
|
|
453
|
+
expect(autosaveActionSpy).toHaveBeenCalled();
|
|
454
|
+
});
|
|
455
|
+
// The autosave is triggered twice when selecting the autocomplete option,
|
|
456
|
+
// once by the selection and once by the onBlur event. We want to verify the last call
|
|
457
|
+
// has the correct data.
|
|
458
|
+
const lastCall = autosaveActionSpy.mock.lastCall?.[0];
|
|
459
|
+
expect(lastCall).toEqual(expect.objectContaining({
|
|
460
|
+
input: expect.objectContaining({
|
|
461
|
+
address: expect.objectContaining({
|
|
462
|
+
line1: '456 Oak Street',
|
|
463
|
+
city: 'Springfield',
|
|
464
|
+
state: 'MA',
|
|
465
|
+
zipCode: '01101',
|
|
466
|
+
}),
|
|
467
|
+
}),
|
|
468
|
+
}));
|
|
469
|
+
});
|
|
470
|
+
it('should hide discard changes button when autosave is configured', async () => {
|
|
471
|
+
server.use(http.get('/api/data/objects/specialty/instances/test-instance', () => {
|
|
472
|
+
return HttpResponse.json({
|
|
473
|
+
id: 'test-instance',
|
|
474
|
+
name: '',
|
|
475
|
+
specialtyType: null,
|
|
476
|
+
license: null,
|
|
477
|
+
});
|
|
478
|
+
}), http.get('/api/data/objects/specialty/instances/test-instance/object', () => {
|
|
479
|
+
return HttpResponse.json(specialtyObject);
|
|
480
|
+
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
481
|
+
return HttpResponse.json({
|
|
482
|
+
...createSpecialtyForm,
|
|
483
|
+
actionId: '_update',
|
|
484
|
+
autosaveActionId: '_autosave',
|
|
485
|
+
});
|
|
486
|
+
}));
|
|
487
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
488
|
+
await waitFor(() => {
|
|
489
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
490
|
+
});
|
|
491
|
+
// When autosaveActionId is present the discard button should be hidden
|
|
492
|
+
expect(screen.queryByRole('button', { name: /discard/i })).not.toBeInTheDocument();
|
|
493
|
+
});
|
|
494
|
+
it('should not trigger autosave when field changes but auto save is not configured', async () => {
|
|
495
|
+
server.use(http.get('/api/data/objects/specialty/instances/test-instance', () => {
|
|
496
|
+
return HttpResponse.json({
|
|
497
|
+
id: 'test-instance',
|
|
498
|
+
name: '',
|
|
499
|
+
specialtyType: null,
|
|
500
|
+
license: null,
|
|
501
|
+
});
|
|
502
|
+
}), http.get('/api/data/objects/specialty/instances/test-instance/object', () => {
|
|
503
|
+
return HttpResponse.json(specialtyObject);
|
|
504
|
+
}), http.get('/api/data/forms/specialtyForm', () => {
|
|
505
|
+
return HttpResponse.json({
|
|
506
|
+
...createSpecialtyForm,
|
|
507
|
+
actionId: '_update',
|
|
508
|
+
});
|
|
509
|
+
}));
|
|
510
|
+
const autosaveActionSpy = vi.fn();
|
|
511
|
+
server.use(http.post('/api/data/objects/specialty/instances/test-instance/actions', async ({ request }) => {
|
|
512
|
+
const body = (await request.json());
|
|
513
|
+
autosaveActionSpy(body);
|
|
514
|
+
return HttpResponse.json({
|
|
515
|
+
id: 'test-instance',
|
|
516
|
+
...body.input,
|
|
517
|
+
});
|
|
518
|
+
}));
|
|
519
|
+
const user = userEvent.setup();
|
|
520
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_update', instanceId: 'test-instance' }));
|
|
521
|
+
await waitFor(() => {
|
|
522
|
+
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
|
523
|
+
});
|
|
524
|
+
// Change a field value
|
|
525
|
+
const nameInput = screen.getByRole('textbox', { name: /name/i });
|
|
526
|
+
await user.clear(nameInput);
|
|
527
|
+
await user.type(nameInput, 'Test Specialty');
|
|
528
|
+
await user.tab();
|
|
529
|
+
// Wait a bit to ensure no autosave happens
|
|
530
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
531
|
+
// Verify autosave was not triggered
|
|
532
|
+
expect(autosaveActionSpy).not.toHaveBeenCalled();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
it('should display a submit button', async () => {
|
|
536
|
+
const form = {
|
|
537
|
+
id: 'simpleForm',
|
|
538
|
+
name: 'Simple Form',
|
|
539
|
+
entries: [],
|
|
540
|
+
actionId: '_create',
|
|
541
|
+
objectId: 'simpleObject',
|
|
542
|
+
};
|
|
543
|
+
const simpleObject = {
|
|
544
|
+
id: 'simpleObject',
|
|
545
|
+
name: 'Simple Object',
|
|
546
|
+
actions: [
|
|
547
|
+
{
|
|
548
|
+
id: '_create',
|
|
549
|
+
name: 'Create',
|
|
550
|
+
type: 'create',
|
|
551
|
+
parameters: [],
|
|
552
|
+
outputEvent: 'created',
|
|
553
|
+
},
|
|
554
|
+
],
|
|
555
|
+
properties: [],
|
|
556
|
+
};
|
|
557
|
+
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
558
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
559
|
+
await screen.findByRole('button', { name: 'Submit' });
|
|
560
|
+
});
|
|
561
|
+
it('should display a button to discard changes', async () => {
|
|
562
|
+
const form = {
|
|
563
|
+
id: 'simpleForm',
|
|
564
|
+
name: 'Simple Form',
|
|
565
|
+
entries: [],
|
|
566
|
+
actionId: '_create',
|
|
567
|
+
objectId: 'simpleObject',
|
|
568
|
+
};
|
|
569
|
+
const simpleObject = {
|
|
570
|
+
id: 'simpleObject',
|
|
571
|
+
name: 'Simple Object',
|
|
572
|
+
actions: [
|
|
573
|
+
{
|
|
574
|
+
id: '_create',
|
|
575
|
+
name: 'Create',
|
|
576
|
+
type: 'create',
|
|
577
|
+
parameters: [],
|
|
578
|
+
outputEvent: 'created',
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
properties: [],
|
|
582
|
+
};
|
|
583
|
+
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
584
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
585
|
+
await screen.findByRole('button', { name: 'Discard Changes' });
|
|
586
|
+
});
|
|
587
|
+
it('should reset the form when discarding changes', async () => {
|
|
588
|
+
const form = {
|
|
589
|
+
id: 'simpleForm2',
|
|
590
|
+
name: 'Simple Form',
|
|
591
|
+
entries: [
|
|
592
|
+
{
|
|
593
|
+
type: 'inputField',
|
|
594
|
+
input: {
|
|
595
|
+
id: 'firstName',
|
|
596
|
+
type: 'string',
|
|
597
|
+
},
|
|
598
|
+
display: {
|
|
599
|
+
label: 'First Name',
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
actionId: '_create',
|
|
604
|
+
objectId: 'simpleObject2',
|
|
605
|
+
};
|
|
606
|
+
const simpleObject = {
|
|
607
|
+
id: 'simpleObject2',
|
|
608
|
+
name: 'Simple Object',
|
|
609
|
+
actions: [
|
|
610
|
+
{
|
|
611
|
+
id: '_create',
|
|
612
|
+
name: 'Create',
|
|
613
|
+
type: 'create',
|
|
614
|
+
outputEvent: 'created',
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
properties: [
|
|
618
|
+
{
|
|
619
|
+
id: 'firstName',
|
|
620
|
+
name: 'First Name',
|
|
621
|
+
type: 'string',
|
|
622
|
+
},
|
|
623
|
+
],
|
|
624
|
+
};
|
|
625
|
+
server.use(http.get(`/api/data/objects/${simpleObject.id}/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
626
|
+
const user = userEvent.setup();
|
|
627
|
+
render(React.createElement(FormRendererContainer, { objectId: simpleObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
628
|
+
const firstNameInput = await screen.findByRole('textbox', { name: 'First Name' });
|
|
629
|
+
await user.type(firstNameInput, 'John');
|
|
630
|
+
const discardButton = await screen.findByRole('button', { name: 'Discard Changes' });
|
|
631
|
+
await user.click(discardButton);
|
|
632
|
+
await waitFor(() => expect(firstNameInput).toHaveValue(''));
|
|
633
|
+
});
|
|
634
|
+
it('should show a not found error if the instance cannot be found', async () => {
|
|
635
|
+
const form = {
|
|
636
|
+
id: 'simpleForm',
|
|
637
|
+
name: 'Simple Form',
|
|
638
|
+
entries: [
|
|
639
|
+
{
|
|
640
|
+
type: 'inputField',
|
|
641
|
+
input: {
|
|
642
|
+
id: 'name',
|
|
643
|
+
type: 'string',
|
|
644
|
+
},
|
|
645
|
+
display: {
|
|
646
|
+
label: 'Name',
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
actionId: '_update',
|
|
651
|
+
objectId: 'simpleObject',
|
|
652
|
+
};
|
|
653
|
+
const simpleObject = {
|
|
654
|
+
id: 'simpleObject',
|
|
655
|
+
name: 'Simple Object',
|
|
656
|
+
actions: [
|
|
657
|
+
{
|
|
658
|
+
id: '_update',
|
|
659
|
+
name: 'Update',
|
|
660
|
+
type: 'update',
|
|
661
|
+
parameters: [
|
|
662
|
+
{
|
|
663
|
+
id: 'name',
|
|
664
|
+
name: 'Name',
|
|
665
|
+
type: 'string',
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
outputEvent: 'updated',
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
properties: [
|
|
672
|
+
{
|
|
673
|
+
id: 'name',
|
|
674
|
+
name: 'Name',
|
|
675
|
+
type: 'string',
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
};
|
|
679
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)), http.get('/api/data/objects/simpleObject/instances/123', () => HttpResponse.json({
|
|
680
|
+
message: 'Not Found',
|
|
681
|
+
}, { status: 404 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json({
|
|
682
|
+
message: 'Not Found',
|
|
683
|
+
}, { status: 404 })));
|
|
684
|
+
render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_update", objectId: "simpleObject", instanceId: '123', dataType: "objectInstances" }));
|
|
685
|
+
await screen.findByText('The requested content could not be found.');
|
|
686
|
+
});
|
|
687
|
+
it('should show an unauthorized error if the instance access is unauthorized', async () => {
|
|
688
|
+
const form = {
|
|
689
|
+
id: 'simpleForm',
|
|
690
|
+
name: 'Simple Form',
|
|
691
|
+
entries: [
|
|
692
|
+
{
|
|
693
|
+
type: 'inputField',
|
|
694
|
+
input: {
|
|
695
|
+
id: 'name',
|
|
696
|
+
type: 'string',
|
|
697
|
+
},
|
|
698
|
+
display: {
|
|
699
|
+
label: 'Name',
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
],
|
|
703
|
+
actionId: '_create',
|
|
704
|
+
objectId: 'simpleObject',
|
|
705
|
+
};
|
|
706
|
+
const simpleObject = {
|
|
707
|
+
id: 'simpleObject',
|
|
708
|
+
name: 'Simple Object',
|
|
709
|
+
actions: [
|
|
710
|
+
{
|
|
711
|
+
id: '_create',
|
|
712
|
+
name: 'Create',
|
|
713
|
+
type: 'create',
|
|
714
|
+
parameters: [
|
|
715
|
+
{
|
|
716
|
+
id: 'name',
|
|
717
|
+
name: 'Name',
|
|
718
|
+
type: 'string',
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
outputEvent: 'created',
|
|
722
|
+
},
|
|
723
|
+
],
|
|
724
|
+
properties: [
|
|
725
|
+
{
|
|
726
|
+
id: 'name',
|
|
727
|
+
name: 'Name',
|
|
728
|
+
type: 'string',
|
|
729
|
+
},
|
|
730
|
+
],
|
|
731
|
+
};
|
|
732
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)), http.get('/api/data/objects/simpleObject/instances/123', () => HttpResponse.json({
|
|
733
|
+
message: 'Unauthorized',
|
|
734
|
+
}, { status: 403 })), http.get('/api/data/objects/simpleObject/instances/123/object', () => HttpResponse.json({
|
|
735
|
+
message: 'Unauthorized',
|
|
736
|
+
}, { status: 403 })));
|
|
737
|
+
render(React.createElement(FormRendererContainer, { formId: form.id, actionId: "_create", objectId: "simpleObject", instanceId: '123', dataType: "objectInstances" }));
|
|
738
|
+
await screen.findByText('You do not have permission to view this content.');
|
|
739
|
+
});
|
|
740
|
+
it('should show a misconfiguration error when action with actionId does not exist', async () => {
|
|
741
|
+
const simpleObject = {
|
|
742
|
+
id: 'simpleObject',
|
|
743
|
+
name: 'Simple Object',
|
|
744
|
+
actions: [],
|
|
745
|
+
properties: [],
|
|
746
|
+
};
|
|
747
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)));
|
|
748
|
+
render(React.createElement(FormRendererContainer, { objectId: "simpleObject", actionId: "_create", dataType: "objectInstances" }));
|
|
749
|
+
await screen.findByText('It looks like something is missing.');
|
|
750
|
+
});
|
|
751
|
+
it('should show a not found error when object cannot be found', async () => {
|
|
752
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json({ error: 'Not Found' }, { status: 404 })));
|
|
753
|
+
render(React.createElement(FormRendererContainer, { objectId: "simpleObject", dataType: "objectInstances" }));
|
|
754
|
+
await screen.findByText('The requested content could not be found.');
|
|
755
|
+
});
|
|
756
|
+
describe('when trying to show a specific form', () => {
|
|
757
|
+
it("should not show the action's default form", async () => {
|
|
758
|
+
const form = {
|
|
759
|
+
id: 'simpleForm',
|
|
760
|
+
name: 'Simple Form',
|
|
761
|
+
entries: [],
|
|
762
|
+
actionId: '_create',
|
|
763
|
+
objectId: 'simpleObject',
|
|
764
|
+
};
|
|
765
|
+
const simpleObject = {
|
|
766
|
+
id: 'simpleObject',
|
|
767
|
+
name: 'Simple Object',
|
|
768
|
+
actions: [
|
|
769
|
+
{
|
|
770
|
+
id: '_create',
|
|
771
|
+
name: 'Create',
|
|
772
|
+
type: 'create',
|
|
773
|
+
parameters: [],
|
|
774
|
+
outputEvent: 'created',
|
|
775
|
+
defaultFormId: 'notSimpleForm',
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
properties: [],
|
|
779
|
+
};
|
|
780
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
|
|
781
|
+
render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", dataType: "objectInstances" }));
|
|
782
|
+
await screen.findByText('Simple Form');
|
|
783
|
+
});
|
|
784
|
+
it('should show a not found error when the form cannot be found', async () => {
|
|
785
|
+
const simpleObject = {
|
|
786
|
+
id: 'simpleObject',
|
|
787
|
+
name: 'Simple Object',
|
|
788
|
+
actions: [
|
|
789
|
+
{
|
|
790
|
+
id: '_create',
|
|
791
|
+
name: 'Create',
|
|
792
|
+
type: 'create',
|
|
793
|
+
parameters: [],
|
|
794
|
+
outputEvent: 'created',
|
|
795
|
+
},
|
|
796
|
+
],
|
|
797
|
+
properties: [],
|
|
798
|
+
};
|
|
799
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/notAForm`, () => HttpResponse.json({ error: 'Not Found' }, { status: 404 })));
|
|
800
|
+
render(React.createElement(FormRendererContainer, { formId: 'notAForm', objectId: "simpleObject", dataType: "objectInstances" }));
|
|
801
|
+
await screen.findByText('The requested content could not be found.');
|
|
802
|
+
});
|
|
803
|
+
it("should show a misconfiguration error when the form's action does not exist", async () => {
|
|
804
|
+
const form = {
|
|
805
|
+
id: 'simpleForm',
|
|
806
|
+
name: 'Simple Form',
|
|
807
|
+
entries: [],
|
|
808
|
+
actionId: '_create',
|
|
809
|
+
objectId: 'simpleObject',
|
|
810
|
+
};
|
|
811
|
+
const simpleObject = {
|
|
812
|
+
id: 'simpleObject',
|
|
813
|
+
name: 'Simple Object',
|
|
814
|
+
actions: [
|
|
815
|
+
{
|
|
816
|
+
id: 'notTheRightAction',
|
|
817
|
+
name: 'Create',
|
|
818
|
+
type: 'create',
|
|
819
|
+
parameters: [],
|
|
820
|
+
outputEvent: 'created',
|
|
821
|
+
},
|
|
822
|
+
],
|
|
823
|
+
properties: [],
|
|
824
|
+
};
|
|
825
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
|
|
826
|
+
render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", dataType: "objectInstances" }));
|
|
827
|
+
await screen.findByText('It looks like something is missing.');
|
|
828
|
+
});
|
|
829
|
+
it("should show a misconfiguration error when actionId doesn't match form's action id", async () => {
|
|
830
|
+
const form = {
|
|
831
|
+
id: 'simpleForm',
|
|
832
|
+
name: 'Simple Form',
|
|
833
|
+
entries: [],
|
|
834
|
+
actionId: '_notCreate',
|
|
835
|
+
objectId: 'simpleObject',
|
|
836
|
+
};
|
|
837
|
+
const simpleObject = {
|
|
838
|
+
id: 'simpleObject',
|
|
839
|
+
name: 'Simple Object',
|
|
840
|
+
actions: [
|
|
841
|
+
{
|
|
842
|
+
id: '_create',
|
|
843
|
+
name: 'Create',
|
|
844
|
+
type: 'create',
|
|
845
|
+
parameters: [],
|
|
846
|
+
outputEvent: 'created',
|
|
847
|
+
},
|
|
848
|
+
],
|
|
849
|
+
properties: [],
|
|
850
|
+
};
|
|
851
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
|
|
852
|
+
render(React.createElement(FormRendererContainer, { formId: form.id, objectId: "simpleObject", actionId: "_create", dataType: "objectInstances" }));
|
|
853
|
+
await screen.findByText('It looks like something is missing.');
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
describe('when trying to show a default form', () => {
|
|
857
|
+
// object id and action id are provided, but form id is not => use default form
|
|
858
|
+
it('should use the default form when provided with an object and action with a default form id', async () => {
|
|
859
|
+
const form = {
|
|
860
|
+
id: 'simpleForm',
|
|
861
|
+
name: 'Simple Form',
|
|
862
|
+
entries: [],
|
|
863
|
+
actionId: '_create',
|
|
864
|
+
objectId: 'simpleObject',
|
|
865
|
+
};
|
|
866
|
+
const simpleObject = {
|
|
867
|
+
id: 'simpleObject',
|
|
868
|
+
name: 'Simple Object',
|
|
869
|
+
actions: [
|
|
870
|
+
{
|
|
871
|
+
id: '_create',
|
|
872
|
+
name: 'Create',
|
|
873
|
+
type: 'create',
|
|
874
|
+
parameters: [],
|
|
875
|
+
outputEvent: 'created',
|
|
876
|
+
defaultFormId: 'simpleForm',
|
|
877
|
+
},
|
|
878
|
+
],
|
|
879
|
+
properties: [],
|
|
880
|
+
};
|
|
881
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
|
|
882
|
+
render(React.createElement(FormRendererContainer, { objectId: "simpleObject", actionId: "_create", dataType: "objectInstances" }));
|
|
883
|
+
await screen.findByText('Simple Form');
|
|
884
|
+
});
|
|
885
|
+
// object id and action id are provided, and defaultFormId is defined but the form doesn't exist
|
|
886
|
+
it('should show a not found error when the default form cannot be found', async () => {
|
|
887
|
+
const simpleObject = {
|
|
888
|
+
id: 'simpleObject',
|
|
889
|
+
name: 'Simple Object',
|
|
890
|
+
actions: [
|
|
891
|
+
{
|
|
892
|
+
id: '_create',
|
|
893
|
+
name: 'Create',
|
|
894
|
+
type: 'create',
|
|
895
|
+
parameters: [],
|
|
896
|
+
outputEvent: 'created',
|
|
897
|
+
defaultFormId: 'notAForm',
|
|
898
|
+
},
|
|
899
|
+
],
|
|
900
|
+
properties: [],
|
|
901
|
+
};
|
|
902
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/notAForm`, () => HttpResponse.json({ error: 'Not Found' }, { status: 404 })));
|
|
903
|
+
render(React.createElement(FormRendererContainer, { objectId: "simpleObject", actionId: "_create", dataType: "objectInstances" }));
|
|
904
|
+
await screen.findByText('The requested content could not be found.');
|
|
905
|
+
});
|
|
906
|
+
it('should show a misconfiguration error when the default form is not defined', async () => {
|
|
907
|
+
const simpleObject = {
|
|
908
|
+
id: 'simpleObject',
|
|
909
|
+
name: 'Simple Object',
|
|
910
|
+
actions: [
|
|
911
|
+
{
|
|
912
|
+
id: '_create',
|
|
913
|
+
name: 'Create',
|
|
914
|
+
type: 'create',
|
|
915
|
+
parameters: [],
|
|
916
|
+
outputEvent: 'created',
|
|
917
|
+
},
|
|
918
|
+
],
|
|
919
|
+
properties: [],
|
|
920
|
+
};
|
|
921
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)));
|
|
922
|
+
render(React.createElement(FormRendererContainer, { objectId: "simpleObject", actionId: "_create", dataType: "objectInstances" }));
|
|
923
|
+
await screen.findByText('It looks like something is missing.');
|
|
924
|
+
});
|
|
925
|
+
it("should show a misconfiguration error if actionId doesn't match the form's actionId", async () => {
|
|
926
|
+
const form = {
|
|
927
|
+
id: 'simpleForm',
|
|
928
|
+
name: 'Simple Form',
|
|
929
|
+
entries: [],
|
|
930
|
+
actionId: 'notCreate',
|
|
931
|
+
objectId: 'simpleObject',
|
|
932
|
+
};
|
|
933
|
+
const simpleObject = {
|
|
934
|
+
id: 'simpleObject',
|
|
935
|
+
name: 'Simple Object',
|
|
936
|
+
actions: [
|
|
937
|
+
{
|
|
938
|
+
id: '_create',
|
|
939
|
+
name: 'Create',
|
|
940
|
+
type: 'create',
|
|
941
|
+
parameters: [],
|
|
942
|
+
outputEvent: 'created',
|
|
943
|
+
defaultFormId: 'simpleForm',
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
properties: [],
|
|
947
|
+
};
|
|
948
|
+
server.use(http.get(`/api/data/objects/simpleObject/effective`, () => HttpResponse.json(simpleObject)), http.get(`/api/data/forms/simpleForm`, () => HttpResponse.json(form)));
|
|
949
|
+
render(React.createElement(FormRendererContainer, { objectId: "simpleObject", actionId: "_create", dataType: "objectInstances" }));
|
|
950
|
+
await screen.findByText('It looks like something is missing.');
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
describe('when submitting a form with validation', () => {
|
|
954
|
+
const form = {
|
|
955
|
+
id: 'validationTestForm',
|
|
956
|
+
name: 'Validation Test Form',
|
|
957
|
+
entries: [
|
|
958
|
+
{
|
|
959
|
+
type: 'input',
|
|
960
|
+
parameterId: 'requiredField',
|
|
961
|
+
display: {
|
|
962
|
+
label: 'Required Field',
|
|
963
|
+
required: true,
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
],
|
|
967
|
+
actionId: '_create',
|
|
968
|
+
objectId: 'validationTestObject',
|
|
969
|
+
};
|
|
970
|
+
let scrollIntoViewMock;
|
|
971
|
+
let originalScrollIntoView;
|
|
972
|
+
beforeEach(() => {
|
|
973
|
+
scrollIntoViewMock = vitest.fn();
|
|
974
|
+
originalScrollIntoView = Element.prototype.scrollIntoView;
|
|
975
|
+
Element.prototype.scrollIntoView = scrollIntoViewMock;
|
|
976
|
+
});
|
|
977
|
+
afterEach(() => {
|
|
978
|
+
Element.prototype.scrollIntoView = originalScrollIntoView;
|
|
979
|
+
});
|
|
980
|
+
const validationTestObject = {
|
|
981
|
+
id: 'validationTestObject',
|
|
982
|
+
name: 'Validation Test Object',
|
|
983
|
+
actions: [
|
|
984
|
+
{
|
|
985
|
+
id: '_create',
|
|
986
|
+
name: 'Create',
|
|
987
|
+
type: 'create',
|
|
988
|
+
parameters: [
|
|
989
|
+
{
|
|
990
|
+
id: 'requiredField',
|
|
991
|
+
name: 'Required Field',
|
|
992
|
+
type: 'string',
|
|
993
|
+
required: true,
|
|
994
|
+
},
|
|
995
|
+
],
|
|
996
|
+
outputEvent: 'created',
|
|
997
|
+
},
|
|
998
|
+
],
|
|
999
|
+
properties: [
|
|
1000
|
+
{
|
|
1001
|
+
id: 'requiredField',
|
|
1002
|
+
name: 'Required Field',
|
|
1003
|
+
type: 'string',
|
|
1004
|
+
},
|
|
1005
|
+
],
|
|
1006
|
+
};
|
|
1007
|
+
beforeEach(() => {
|
|
1008
|
+
server.use(http.get(`/api/data/objects/${validationTestObject.id}/effective`, () => HttpResponse.json(validationTestObject)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)));
|
|
1009
|
+
});
|
|
1010
|
+
it('should display validation errors after trying to submit the form', async () => {
|
|
1011
|
+
const user = userEvent.setup();
|
|
1012
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
1013
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
1014
|
+
await user.click(submitButton);
|
|
1015
|
+
// List items are named by author, but they don't
|
|
1016
|
+
// need to be given an accessible name here because their text content is clear enough.
|
|
1017
|
+
// As such, we use getByRole and ensure it has the correct text
|
|
1018
|
+
const errorMessage = await screen.findByRole('listitem');
|
|
1019
|
+
expect(errorMessage).toHaveTextContent('Required Field is required');
|
|
1020
|
+
});
|
|
1021
|
+
it('should clear validation errors after they have been resolved', async () => {
|
|
1022
|
+
const user = userEvent.setup();
|
|
1023
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
1024
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
1025
|
+
await user.click(submitButton);
|
|
1026
|
+
// Make sure error elements appear
|
|
1027
|
+
screen.getByRole('listitem');
|
|
1028
|
+
const requiredField = screen.getByRole('textbox', { name: /Required Field */i });
|
|
1029
|
+
await user.type(requiredField, 'Some content here...');
|
|
1030
|
+
expect(screen.queryByRole('listitem')).not.toBeInTheDocument();
|
|
1031
|
+
});
|
|
1032
|
+
it('should scroll to validation errors after submission', async () => {
|
|
1033
|
+
const user = userEvent.setup();
|
|
1034
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
1035
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
1036
|
+
await user.click(submitButton);
|
|
1037
|
+
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
1038
|
+
});
|
|
1039
|
+
it('should not scroll to validation errors after submission if there are none', async () => {
|
|
1040
|
+
const user = userEvent.setup();
|
|
1041
|
+
server.use(http.post(`/api/data/objects/${validationTestObject.id}/instances/actions`, () => HttpResponse.json({}, { status: 200 })));
|
|
1042
|
+
render(React.createElement(FormRendererContainer, { objectId: validationTestObject.id, formId: form.id, dataType: "objectInstances" }));
|
|
1043
|
+
const requiredField = await screen.findByRole('textbox', { name: /Required Field */i });
|
|
1044
|
+
await user.type(requiredField, 'Some content here...');
|
|
1045
|
+
const submitButton = await screen.findByRole('button', { name: 'Submit' });
|
|
1046
|
+
await user.click(submitButton);
|
|
1047
|
+
expect(scrollIntoViewMock).not.toHaveBeenCalled();
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
it('renders the auto-generated delete confirmation form when formId is "_auto_"', async () => {
|
|
1051
|
+
server.use(http.get('/api/data/objects/specialty/instances/test-instance', () => {
|
|
1052
|
+
return HttpResponse.json({
|
|
1053
|
+
id: 'test-instance',
|
|
1054
|
+
name: 'Persons Name',
|
|
1055
|
+
specialtyType: null,
|
|
1056
|
+
license: null,
|
|
1057
|
+
});
|
|
1058
|
+
}), http.get('/api/data/objects/specialty/instances/test-instance/object', () => {
|
|
1059
|
+
return HttpResponse.json(specialtyObject);
|
|
1060
|
+
}));
|
|
1061
|
+
render(React.createElement(FormRendererContainer, { objectId: 'specialty', formId: '_auto_', dataType: 'objectInstances', actionId: '_delete', instanceId: 'test-instance' }));
|
|
1062
|
+
// Wait for the delete confirmation message to appear
|
|
1063
|
+
const confirmation = await screen.findByText(/you are about to delete/i);
|
|
1064
|
+
expect(confirmation).toBeInTheDocument();
|
|
1065
|
+
// Validate that the message includes the instance name
|
|
1066
|
+
expect(confirmation).toHaveTextContent(/Persons Name/);
|
|
1067
|
+
// Ensure the "Delete" button is rendered
|
|
1068
|
+
await screen.findByRole('button', { name: /delete/i });
|
|
1069
|
+
});
|
|
103
1070
|
});
|