@evoke-platform/ui-components 1.10.0-testing.9 → 1.10.1-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
  2. package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
  3. package/dist/published/components/core/TextField/TextField.js +1 -1
  4. package/dist/published/components/core/TextField/TextField.test.js +0 -2
  5. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
  7. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
  8. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  9. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  10. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
  11. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  12. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  13. package/dist/published/components/custom/FormField/FormField.js +17 -5
  14. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  15. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  16. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  17. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
  21. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  22. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  23. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  24. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  25. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  26. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  27. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  28. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +9 -23
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +127 -92
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
  42. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  43. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  44. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  45. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  46. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
  47. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  48. package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
  49. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  50. package/dist/published/components/custom/FormV2/components/utils.js +83 -13
  51. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +411 -44
  52. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
  53. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  54. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  55. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  56. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
  57. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  58. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  59. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  60. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  61. package/dist/published/components/custom/index.d.ts +2 -0
  62. package/dist/published/components/custom/index.js +1 -0
  63. package/dist/published/index.d.ts +6 -6
  64. package/dist/published/index.js +1 -1
  65. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  66. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  67. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  68. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  69. package/dist/published/stories/FormRendererData.d.ts +12 -0
  70. package/dist/published/stories/FormRendererData.js +26 -1
  71. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  72. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  73. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  74. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  75. package/dist/published/stories/sharedMswHandlers.js +49 -10
  76. package/dist/published/theme/hooks.d.ts +4 -3
  77. package/dist/published/types.d.ts +3 -0
  78. package/package.json +10 -8
@@ -1,5 +1,4 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
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 removePoppers = () => {
20
- const portalSelectors = ['.MuiAutocomplete-popper'];
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
- describe('Form component', () => {
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(MemoryRouter, null,
85
- React.createElement(FormRendererContainer, { objectId: 'specialty', formId: 'specialtyForm', dataType: 'objectInstances', actionId: '_create', associatedObject: { propertyId: 'license', instanceId: 'rnLicense' } })));
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
  });