@evoke-platform/ui-components 1.10.0-dev.30 → 1.10.0-dev.32

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.
@@ -1,5 +1,6 @@
1
1
  import { ExpandMore, InfoRounded } from '@mui/icons-material';
2
2
  import { InputLabel, Autocomplete as MUIAutocomplete } from '@mui/material';
3
+ import { omit } from 'lodash';
3
4
  import React from 'react';
4
5
  import UIThemeProvider from '../../../theme';
5
6
  import FieldError from '../FieldError';
@@ -37,6 +38,7 @@ const Autocomplete = (props) => {
37
38
  return props.instructionText;
38
39
  }
39
40
  };
41
+ const muiAutocompleteProps = omit(props, 'sortBy', 'error', 'errorMessage', 'labelPlacement', 'instructionText');
40
42
  if (!!props.label && props.labelPlacement === 'outside-top') {
41
43
  return (React.createElement(UIThemeProvider, null,
42
44
  React.createElement(InputLabel, { htmlFor: props.id ?? '', sx: { display: 'flex', paddingBottom: '0px', fontSize: '14px' } },
@@ -50,7 +52,7 @@ const Autocomplete = (props) => {
50
52
  color: (theme) => theme.palette.text.secondary,
51
53
  } })))),
52
54
  renderInstructionText(),
53
- React.createElement(MUIAutocomplete, { ...props, sx: {
55
+ React.createElement(MUIAutocomplete, { ...muiAutocompleteProps, sx: {
54
56
  '& fieldset': { borderRadius: '8px', borderColor: props.error ? 'red' : undefined },
55
57
  '& .MuiOutlinedInput-notchedOutline': {
56
58
  border: props.readOnly ? 'none' : 'auto',
@@ -67,7 +69,7 @@ const Autocomplete = (props) => {
67
69
  }
68
70
  else {
69
71
  return (React.createElement(UIThemeProvider, null,
70
- React.createElement(MUIAutocomplete, { ...props, sx: {
72
+ React.createElement(MUIAutocomplete, { ...muiAutocompleteProps, sx: {
71
73
  '& fieldset': { borderRadius: '8px', borderColor: props.error ? 'red' : undefined },
72
74
  '& .MuiOutlinedInput-notchedOutline': {
73
75
  border: props.readOnly ? 'none' : 'auto',
@@ -1,11 +1,120 @@
1
1
  import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import React from 'react';
3
4
  import { it } from 'vitest';
4
5
  import TextField from '../TextField';
5
6
  import Autocomplete from './Autocomplete';
6
- const renderInput = (params) => React.createElement(TextField, { ...params });
7
+ const renderInputFactory = (label) => (params) => React.createElement(TextField, { ...params, label: label });
7
8
  const options = [];
8
- it('renders with label outside outline', () => {
9
- render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", renderInput: renderInput, options: options }));
9
+ it('should render with label outside outline', () => {
10
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", renderInput: renderInputFactory(), options: options }));
10
11
  screen.getByRole('combobox', { name: 'Title' });
11
12
  });
13
+ it('should render label outside outline with asterisk when required', () => {
14
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", renderInput: renderInputFactory(), options: options, required: true }));
15
+ screen.getByRole('combobox', { name: 'Title *' });
16
+ });
17
+ it('should render instruction text when provided when there is an outside-top label', () => {
18
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", instructionText: "This is an instruction", renderInput: renderInputFactory(), options: options }));
19
+ screen.getByText('This is an instruction');
20
+ });
21
+ it('should not render instruction text when there is not an outside-top label', () => {
22
+ render(React.createElement(Autocomplete, { id: "testinput", instructionText: "This is an instruction", renderInput: renderInputFactory(), options: options }));
23
+ const instructionText = screen.queryByText('This is an instruction');
24
+ expect(instructionText).not.toBeInTheDocument();
25
+ });
26
+ it('should render a tooltip if one is provided and there is an outside-top label', async () => {
27
+ const user = userEvent.setup();
28
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", tooltip: "This is a tooltip", renderInput: renderInputFactory(), options: options }));
29
+ expect(screen.queryByText('This is a tooltip')).not.toBeInTheDocument();
30
+ await user.hover(screen.getByLabelText('This is a tooltip'));
31
+ await screen.findByText('This is a tooltip');
32
+ });
33
+ it('should sort options in ascending order if sortBy is ASC', async () => {
34
+ const user = userEvent.setup();
35
+ const unsortedOptions = [
36
+ { label: 'Banana', value: 1 },
37
+ { label: 'Apple', value: 2 },
38
+ { label: 'Cherry', value: 3 },
39
+ ];
40
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "ASC", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
41
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
42
+ await user.click(comboBox);
43
+ const options = screen.getAllByRole('option');
44
+ const labels = options.map((option) => option.textContent);
45
+ expect(labels).toEqual(['Apple', 'Banana', 'Cherry']);
46
+ });
47
+ it('should sort options in descending order if sortBy is DESC', async () => {
48
+ const user = userEvent.setup();
49
+ const unsortedOptions = [
50
+ { label: 'Banana', value: 1 },
51
+ { label: 'Apple', value: 2 },
52
+ { label: 'Cherry', value: 3 },
53
+ ];
54
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "DESC", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
55
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
56
+ await user.click(comboBox);
57
+ const options = screen.getAllByRole('option');
58
+ const labels = options.map((option) => option.textContent);
59
+ expect(labels).toEqual(['Cherry', 'Banana', 'Apple']);
60
+ });
61
+ it('should not sort options if sortBy is NONE', async () => {
62
+ const user = userEvent.setup();
63
+ const unsortedOptions = [
64
+ { label: 'Banana', value: 1 },
65
+ { label: 'Apple', value: 2 },
66
+ { label: 'Cherry', value: 3 },
67
+ ];
68
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "NONE", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
69
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
70
+ await user.click(comboBox);
71
+ const options = screen.getAllByRole('option');
72
+ const labels = options.map((option) => option.textContent);
73
+ expect(labels).toEqual(['Banana', 'Apple', 'Cherry']);
74
+ });
75
+ it('should sort options in ascending order by default', async () => {
76
+ const user = userEvent.setup();
77
+ const unsortedOptions = [
78
+ { label: 'Banana', value: 1 },
79
+ { label: 'Apple', value: 2 },
80
+ { label: 'Cherry', value: 3 },
81
+ ];
82
+ render(React.createElement(Autocomplete, { id: "testinput", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
83
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
84
+ await user.click(comboBox);
85
+ const options = screen.getAllByRole('option');
86
+ const labels = options.map((option) => option.textContent);
87
+ expect(labels).toEqual(['Apple', 'Banana', 'Cherry']);
88
+ });
89
+ it('should sort string options', async () => {
90
+ const user = userEvent.setup();
91
+ const unsortedOptions = ['Banana', 'Apple', 'Cherry'];
92
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "ASC", renderInput: renderInputFactory('Fruit Options'), options: unsortedOptions }));
93
+ const comboBox = screen.getByRole('combobox', { name: 'Fruit Options' });
94
+ await user.click(comboBox);
95
+ const options = screen.getAllByRole('option');
96
+ const labels = options.map((option) => option.textContent);
97
+ expect(labels).toEqual(['Apple', 'Banana', 'Cherry']);
98
+ });
99
+ it('should render an error when error prop is true', () => {
100
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", error: true, errorMessage: "This is an error", labelPlacement: "outside-top", renderInput: renderInputFactory(), options: options }));
101
+ screen.getByText('This is an error');
102
+ });
103
+ it('should show popupIcon when not disabled', () => {
104
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", disabled: false, renderInput: renderInputFactory(), options: options }));
105
+ screen.getByTestId('ExpandMoreIcon');
106
+ });
107
+ it('should hide popupIcon when disabled', () => {
108
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", disabled: true, renderInput: renderInputFactory(), options: options }));
109
+ const popupIcon = screen.queryByTestId('ExpandMoreIcon');
110
+ expect(popupIcon).not.toBeInTheDocument();
111
+ });
112
+ it('should hide popupIcon when readOnly', () => {
113
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", readOnly: true, renderInput: renderInputFactory(), options: options }));
114
+ const popupIcon = screen.queryByTestId('ExpandMoreIcon');
115
+ expect(popupIcon).not.toBeInTheDocument();
116
+ });
117
+ it('should show custom popupIcon when provided', () => {
118
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", popupIcon: React.createElement("span", { "data-testid": "custom-icon" }, "^"), renderInput: renderInputFactory(), options: options }));
119
+ screen.getByTestId('custom-icon');
120
+ });
@@ -36,7 +36,7 @@ const TextField = (props) => {
36
36
  ...props.sx,
37
37
  } }),
38
38
  error && React.createElement(FieldError, { required: required, label: errorMessage }))) : (React.createElement(React.Fragment, null,
39
- React.createElement(MUITextField, { inputProps: { readOnly: readOnly, 'aria-readonly': !!readOnly, 'data-testid': 'label-inside' }, ...props, sx: readOnly
39
+ React.createElement(MUITextField, { inputProps: { readOnly: readOnly, 'aria-readonly': !!readOnly, 'data-testid': 'label-inside' }, ...muiProps, sx: readOnly
40
40
  ? { ...readOnlyStyles, ...props.sx }
41
41
  : { '& fieldset': { borderRadius: '8px' }, ...props.sx } }),
42
42
  error && React.createElement(FieldError, { required: required, label: errorMessage })))));
@@ -1,9 +1,7 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import React from 'react';
4
3
  import { expect, it } from 'vitest';
5
4
  import TextField from './index';
6
- expect.extend(matchers);
7
5
  it('render TextField and check for data-testid === label-outside when labelPlacement === outside-top && variant === outlined', () => {
8
6
  render(React.createElement(TextField, { id: "testinput", labelPlacement: "outside-top", variant: "outlined", label: "Title" }));
9
7
  const textField = screen.getByRole('textbox', { name: 'Title' });
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { expect, it } from 'vitest';
6
5
  import CriteriaBuilder from './CriteriaBuilder';
7
- expect.extend(matchers);
8
6
  const mockProperties = [
9
7
  {
10
8
  id: 'name',
@@ -42,7 +42,8 @@ export const Document = (props) => {
42
42
  if (canUpdateProperty) {
43
43
  apiServices
44
44
  .get(getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=update`))
45
- .then((accessCheck) => setHasUpdatePermission(accessCheck.result));
45
+ .then((accessCheck) => setHasUpdatePermission(accessCheck.result))
46
+ .catch(() => setHasUpdatePermission(false));
46
47
  }
47
48
  };
48
49
  const handleUpload = async (files) => {
@@ -1,5 +1,4 @@
1
1
  import { ApiServices } from '@evoke-platform/context';
2
- import * as matchers from '@testing-library/jest-dom/matchers';
3
2
  import { render, screen, waitFor, within } from '@testing-library/react';
4
3
  import userEvent from '@testing-library/user-event';
5
4
  import axios from 'axios';
@@ -10,7 +9,6 @@ import React from 'react';
10
9
  import { expect, it } from 'vitest';
11
10
  import Form from '../Common/Form';
12
11
  import { accessibility508Object, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, users, } from './test-data';
13
- expect.extend(matchers);
14
12
  const removePoppers = () => {
15
13
  const portalSelectors = ['.MuiAutocomplete-popper'];
16
14
  portalSelectors.forEach((selector) => {
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import { userEvent } from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { describe, expect, it, vi } from 'vitest';
6
5
  import InputField from './InputFieldComponent';
7
- expect.extend(matchers);
8
6
  describe('Free-text input', () => {
9
7
  // Right now an object property is required for this to function, but eventually this should go
10
8
  // away.
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import { userEvent } from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { describe, expect, it, vi } from 'vitest';
6
5
  import Select from './Select';
7
- expect.extend(matchers);
8
6
  describe('Single select', () => {
9
7
  // Right now an object property is required for this to function, but eventually this should go
10
8
  // away.
@@ -70,9 +70,8 @@ function FormRendererContainer(props) {
70
70
  }
71
71
  else {
72
72
  if (instanceId) {
73
- objectStore.getInstance(instanceId).then((instance) => {
74
- setInstance(instance);
75
- });
73
+ const instance = await objectStore.getInstance(instanceId);
74
+ setInstance(instance);
76
75
  }
77
76
  const object = await apiServices.get(getPrefixedUrl(`/objects/${form?.objectId || objectId}${instanceId ? `/instances/${instanceId}/object` : '/effective'}`), { params: { sanitizedVersion: true } });
78
77
  setSanitizedObject(object);
@@ -42,6 +42,12 @@ export const Document = (props) => {
42
42
  [`${id}UpdatePermission`]: accessCheck.result,
43
43
  });
44
44
  setHasUpdatePermission(accessCheck.result);
45
+ })
46
+ .catch(() => {
47
+ setFetchedOptions({
48
+ [`${id}UpdatePermission`]: false,
49
+ });
50
+ setHasUpdatePermission(false);
45
51
  });
46
52
  }
47
53
  }, [canUpdateProperty, fetchedOptions, instance, object]);
@@ -1,4 +1,3 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
3
  import { isEmpty, isEqual, set } from 'lodash';
@@ -9,7 +8,6 @@ import { MemoryRouter } from 'react-router-dom';
9
8
  import { expect, it } from 'vitest';
10
9
  import FormRenderer from '../FormRenderer';
11
10
  import { accessibility508Object, createSpecialtyForm, jsonLogicDisplayTestSpecialtyForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, simpleConditionDisplayTestSpecialtyForm, specialtyObject, specialtyTypeObject, UpdateAccessibilityFormOne, UpdateAccessibilityFormTwo, users, } from './test-data';
12
- expect.extend(matchers);
13
11
  // Mock ResizeObserver
14
12
  global.ResizeObserver = class ResizeObserver {
15
13
  observe() { }
@@ -997,25 +995,25 @@ describe('FormRenderer', () => {
997
995
  });
998
996
  describe('when mode is default (allows both new and existing)', () => {
999
997
  const form = {
1000
- id: 'relatedObjectTestForm',
998
+ id: 'dropdownRelatedObjectTestForm',
1001
999
  name: 'Related Object Test Form',
1002
1000
  entries: [
1003
1001
  {
1004
1002
  type: 'input',
1005
- parameterId: 'specialtyType',
1003
+ parameterId: 'licenseType',
1006
1004
  display: {
1007
- label: 'Speciality Type',
1005
+ label: 'License Type',
1008
1006
  relatedObjectDisplay: 'dropdown',
1009
1007
  createActionId: '_create',
1010
1008
  },
1011
1009
  },
1012
1010
  ],
1013
1011
  actionId: '_update',
1014
- objectId: 'relatedObjectTestForm',
1012
+ objectId: 'dropdownRelatedTestObject',
1015
1013
  };
1016
1014
  beforeEach(() => {
1017
- const relatedObjectTestFormObject = {
1018
- id: 'relatedObjectTestForm',
1015
+ const dropdownRelatedTestObject = {
1016
+ id: 'dropdownRelatedTestObject',
1019
1017
  name: 'Related Object Test Form',
1020
1018
  actions: [
1021
1019
  {
@@ -1024,10 +1022,10 @@ describe('FormRenderer', () => {
1024
1022
  type: 'update',
1025
1023
  parameters: [
1026
1024
  {
1027
- id: 'specialtyType',
1025
+ id: 'licenseType',
1028
1026
  name: 'Related Object',
1029
1027
  type: 'object',
1030
- objectId: 'specialtyType',
1028
+ objectId: 'licenseType',
1031
1029
  },
1032
1030
  ],
1033
1031
  outputEvent: 'updated',
@@ -1035,45 +1033,61 @@ describe('FormRenderer', () => {
1035
1033
  ],
1036
1034
  properties: [
1037
1035
  {
1038
- id: 'specialtyType',
1036
+ id: 'licenseType',
1039
1037
  name: 'Related Object',
1040
1038
  type: 'object',
1039
+ objectId: 'licenseType',
1041
1040
  },
1042
1041
  ],
1043
1042
  };
1044
- setupTestMocks(relatedObjectTestFormObject, form);
1045
- const specialtyTypeObject = {
1046
- id: 'specialtyType',
1047
- name: 'Specialty Type',
1043
+ setupTestMocks(dropdownRelatedTestObject, form);
1044
+ const licenseTypeObject = {
1045
+ id: 'licenseType',
1046
+ name: 'License Type',
1048
1047
  actions: [
1049
1048
  {
1050
1049
  id: '_create',
1051
1050
  name: 'Create',
1052
1051
  type: 'create',
1053
- parameters: [],
1052
+ parameters: [
1053
+ {
1054
+ type: 'string',
1055
+ id: 'name',
1056
+ name: 'License Type Name',
1057
+ },
1058
+ ],
1054
1059
  outputEvent: 'created',
1055
- defaultFormId: 'specialtyTypeForm',
1060
+ defaultFormId: 'licenseTypeForm',
1061
+ },
1062
+ ],
1063
+ properties: [
1064
+ {
1065
+ type: 'string',
1066
+ id: 'name',
1067
+ name: 'License Type Name',
1056
1068
  },
1057
1069
  ],
1058
- properties: [],
1059
1070
  };
1060
- const specialtyTypeForm = {
1061
- id: 'specialtyTypeForm',
1062
- name: 'Specialty Type Form',
1071
+ const licenseTypeForm = {
1072
+ id: 'licenseTypeForm',
1073
+ name: 'License Type Form',
1063
1074
  entries: [
1064
1075
  {
1065
- type: 'content',
1066
- html: '<div>Specialty Type Form Content</div>',
1076
+ type: 'input',
1077
+ parameterId: 'name',
1078
+ display: {
1079
+ label: 'License Type Name',
1080
+ },
1067
1081
  },
1068
1082
  ],
1069
1083
  actionId: '_create',
1070
- objectId: 'specialtyType',
1084
+ objectId: 'licenseType',
1071
1085
  };
1072
- setupTestMocks(specialtyTypeObject, specialtyTypeForm, [
1086
+ setupTestMocks(licenseTypeObject, licenseTypeForm, [
1073
1087
  {
1074
- id: 'rnSpecialtyType1',
1075
- name: 'RN Specialty Type #1',
1076
- objectId: 'specialtyType',
1088
+ id: 'licenseType1',
1089
+ name: 'License Type #1',
1090
+ objectId: 'licenseType',
1077
1091
  },
1078
1092
  ]);
1079
1093
  });
@@ -1081,27 +1095,34 @@ describe('FormRenderer', () => {
1081
1095
  const user = userEvent.setup();
1082
1096
  render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1083
1097
  // Navigate to and open dropdown
1084
- const dropdown = await screen.findByRole('combobox', { name: 'Speciality Type' });
1098
+ const dropdown = await screen.findByRole('combobox', { name: 'License Type' });
1085
1099
  await user.click(dropdown);
1086
- await waitFor(() => {
1087
- expect(screen.getByText('+ Add New')).toBeInTheDocument();
1088
- });
1089
- await user.click(screen.getByText('+ Add New'));
1100
+ await screen.findByRole('listbox');
1101
+ await user.click(await screen.findByRole('option', { name: '+ Add New' }));
1102
+ });
1103
+ it('opens create related object instance form in dialog when add new is clicked', async () => {
1104
+ const user = userEvent.setup();
1105
+ render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1106
+ // Navigate to and open dropdown
1107
+ const dropdown = await screen.findByRole('combobox', { name: 'License Type' });
1108
+ await user.click(dropdown);
1109
+ await screen.findByRole('listbox');
1110
+ await user.click(await screen.findByRole('option', { name: '+ Add New' }));
1090
1111
  await screen.findByRole('dialog');
1091
- await screen.findByText('Specialty Type Form Content');
1112
+ await screen.findByRole('textbox', { name: 'License Type Name' });
1092
1113
  });
1093
1114
  it('allows related object instance selection', async () => {
1094
1115
  const user = userEvent.setup();
1095
1116
  render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1096
1117
  // Navigate to and open dropdown
1097
- const dropdown = await screen.findByRole('combobox', { name: 'Speciality Type' });
1118
+ const dropdown = await screen.findByRole('combobox', { name: 'License Type' });
1098
1119
  await user.click(dropdown);
1099
- const option = await screen.findByRole('option', { name: 'RN Specialty Type #1' });
1120
+ const option = await screen.findByRole('option', { name: 'License Type #1' });
1100
1121
  await user.click(option);
1101
1122
  // Verify option has been removed from the document
1102
1123
  expect(option).not.toBeInTheDocument();
1103
1124
  // Verify selection
1104
- screen.getByText('RN Specialty Type #1');
1125
+ screen.getByText('License Type #1');
1105
1126
  });
1106
1127
  });
1107
1128
  });
@@ -1,4 +1,3 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
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';
@@ -9,7 +8,6 @@ import { MemoryRouter } from 'react-router-dom';
9
8
  import { expect, it } from 'vitest';
10
9
  import FormRendererContainer from '../FormRendererContainer';
11
10
  import { createSpecialtyForm, licenseForm, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, } from './test-data';
12
- expect.extend(matchers);
13
11
  // Mock ResizeObserver
14
12
  global.ResizeObserver = class ResizeObserver {
15
13
  observe() { }
@@ -78,10 +76,11 @@ describe('FormRendererContainer', () => {
78
76
  return HttpResponse.json(createSpecialtyForm);
79
77
  }));
80
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 });
81
81
  // Validate that the license field is hidden
82
82
  await waitFor(() => expect(screen.queryByRole('combobox', { name: 'License' })).not.toBeInTheDocument());
83
83
  // Validate that specialty type dropdown is only rendering specialty types that are associated with the selected license.
84
- const specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' });
85
84
  await user.click(specialtyType);
86
85
  const openAutocomplete = await screen.findByRole('listbox');
87
86
  await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
@@ -331,7 +330,7 @@ describe('FormRendererContainer', () => {
331
330
  await waitFor(() => {
332
331
  expect(autosaveActionSpy).toHaveBeenCalled();
333
332
  });
334
- const lastCall = autosaveActionSpy.mock.lastCall[0];
333
+ const lastCall = autosaveActionSpy.mock.lastCall?.[0];
335
334
  expect(lastCall).toEqual(expect.objectContaining({
336
335
  input: expect.objectContaining({
337
336
  address: expect.objectContaining({
@@ -456,7 +455,7 @@ describe('FormRendererContainer', () => {
456
455
  // The autosave is triggered twice when selecting the autocomplete option,
457
456
  // once by the selection and once by the onBlur event. We want to verify the last call
458
457
  // has the correct data.
459
- const lastCall = autosaveActionSpy.mock.lastCall[0];
458
+ const lastCall = autosaveActionSpy.mock.lastCall?.[0];
460
459
  expect(lastCall).toEqual(expect.objectContaining({
461
460
  input: expect.objectContaining({
462
461
  address: expect.objectContaining({
@@ -632,6 +631,325 @@ describe('FormRendererContainer', () => {
632
631
  await user.click(discardButton);
633
632
  await waitFor(() => expect(firstNameInput).toHaveValue(''));
634
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
+ });
635
953
  describe('when submitting a form with validation', () => {
636
954
  const form = {
637
955
  id: 'validationTestForm',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.10.0-dev.30",
3
+ "version": "1.10.0-dev.32",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -21,6 +21,7 @@
21
21
  "test": "vitest",
22
22
  "test:ui": "vitest --ui",
23
23
  "test:preview": "vitest-preview",
24
+ "coverage": "vitest run --coverage",
24
25
  "copy-styles": "copyfiles -u 1 src/styles/*.css dist/published/",
25
26
  "build": "rm -rf ./dist && tsc && npm run copy-styles",
26
27
  "build:cjs": "tsc --module CommonJS --outDir dist/cjs",
@@ -52,22 +53,23 @@
52
53
  "@storybook/react": "^7.6.20",
53
54
  "@storybook/react-webpack5": "^7.6.20",
54
55
  "@storybook/testing-library": "^0.0.11",
55
- "@testing-library/jest-dom": "^6.8.0",
56
- "@testing-library/react": "^14.3.1",
57
- "@testing-library/user-event": "^14.5.2",
56
+ "@testing-library/jest-dom": "^6.9.1",
57
+ "@testing-library/react": "^16.3.0",
58
+ "@testing-library/user-event": "^14.6.1",
58
59
  "@types/flat": "^5.0.5",
59
60
  "@types/jest": "^28.1.4",
60
61
  "@types/json-logic-js": "^2.0.8",
61
62
  "@types/luxon": "^3.4.2",
62
63
  "@types/nanoid-dictionary": "^4.2.3",
63
- "@types/node": "^18.0.0",
64
+ "@types/node": "^24.10.0",
64
65
  "@types/react": "^18.0.17",
65
66
  "@types/react-dom": "^18.0.5",
66
67
  "@types/react-input-mask": "^2.0.4",
67
68
  "@typescript-eslint/eslint-plugin": "^5.52.0",
68
69
  "@typescript-eslint/parser": "^5.35.1",
69
70
  "@typescript-eslint/typescript-estree": "^5.35.1",
70
- "@vitest/ui": "^1.6.0",
71
+ "@vitest/coverage-v8": "^3.2.4",
72
+ "@vitest/ui": "^3.2.4",
71
73
  "babel-jest": "^28.1.2",
72
74
  "babel-loader": "^8.2.5",
73
75
  "copyfiles": "^2.4.1",
@@ -88,7 +90,7 @@
88
90
  "rimraf": "^3.0.2",
89
91
  "storybook": "^7.6.20",
90
92
  "typescript": "^4.7.3",
91
- "vitest": "^1.6.0",
93
+ "vitest": "^3.2.4",
92
94
  "vitest-preview": "^0.0.3",
93
95
  "webpack": "^5.74.0",
94
96
  "yalc": "^1.0.0-pre.53"