@openmrs/esm-form-engine-lib 2.1.0-pre.1362

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 (272) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +58 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +4 -0
  7. package/LICENSE.txt +401 -0
  8. package/README.md +136 -0
  9. package/__mocks__/concepts.mock.json +140 -0
  10. package/__mocks__/forms/afe-forms/component_art.json +38 -0
  11. package/__mocks__/forms/afe-forms/component_preclinic-review.json +38 -0
  12. package/__mocks__/forms/afe-forms/demo_hts-form.json +62 -0
  13. package/__mocks__/forms/afe-forms/form-component.json +38 -0
  14. package/__mocks__/forms/afe-forms/mini-form.json +31 -0
  15. package/__mocks__/forms/afe-forms/nested-form1.json +38 -0
  16. package/__mocks__/forms/afe-forms/nested-form2.json +38 -0
  17. package/__mocks__/forms/afe-forms/test-orders.json +72 -0
  18. package/__mocks__/forms/afe-forms/test-schema-transformer-form.json +88 -0
  19. package/__mocks__/forms/rfe-forms/age-validation-form.json +58 -0
  20. package/__mocks__/forms/rfe-forms/bmi-test-form.json +69 -0
  21. package/__mocks__/forms/rfe-forms/bsa-test-form.json +69 -0
  22. package/__mocks__/forms/rfe-forms/component_art.json +1705 -0
  23. package/__mocks__/forms/rfe-forms/component_preclinic-review.json +480 -0
  24. package/__mocks__/forms/rfe-forms/conditional-answered-form.json +97 -0
  25. package/__mocks__/forms/rfe-forms/conditional-required-form.json +281 -0
  26. package/__mocks__/forms/rfe-forms/demo_hts-form.json +346 -0
  27. package/__mocks__/forms/rfe-forms/edd-test-form.json +88 -0
  28. package/__mocks__/forms/rfe-forms/external_data_source_form.json +43 -0
  29. package/__mocks__/forms/rfe-forms/filter-answer-options-test-form.json +87 -0
  30. package/__mocks__/forms/rfe-forms/form-component.json +43 -0
  31. package/__mocks__/forms/rfe-forms/forms-loader.test.schema.ts +209 -0
  32. package/__mocks__/forms/rfe-forms/historical-expressions-form.json +170 -0
  33. package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +374 -0
  34. package/__mocks__/forms/rfe-forms/mini-form.json +29 -0
  35. package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +89 -0
  36. package/__mocks__/forms/rfe-forms/months-on-art-form.json +90 -0
  37. package/__mocks__/forms/rfe-forms/multi-select-form.json +86 -0
  38. package/__mocks__/forms/rfe-forms/nested-form1.json +43 -0
  39. package/__mocks__/forms/rfe-forms/nested-form2.json +43 -0
  40. package/__mocks__/forms/rfe-forms/next-visit-test-form.json +78 -0
  41. package/__mocks__/forms/rfe-forms/obs-group-test_form.json +137 -0
  42. package/__mocks__/forms/rfe-forms/obs-list-data.ts +37 -0
  43. package/__mocks__/forms/rfe-forms/post-submission-test-form.json +116 -0
  44. package/__mocks__/forms/rfe-forms/reference-by-mapping-form.json +54 -0
  45. package/__mocks__/forms/rfe-forms/required-form.json +50 -0
  46. package/__mocks__/forms/rfe-forms/sample_fields.json +36 -0
  47. package/__mocks__/forms/rfe-forms/test-enrolment-form.json +241 -0
  48. package/__mocks__/forms/rfe-forms/treatment-end-date-test-form.json +121 -0
  49. package/__mocks__/forms/rfe-forms/viral-load-status-form.json +75 -0
  50. package/__mocks__/forms/rfe-forms/zscore-bmi-for-age-form.json +79 -0
  51. package/__mocks__/forms/rfe-forms/zscore-height-for-age-form.json +79 -0
  52. package/__mocks__/forms/rfe-forms/zscore-weight-height-form.json +77 -0
  53. package/__mocks__/packages/hiv/forms/hts_poc/1.0.json +8 -0
  54. package/__mocks__/packages/hiv/forms/hts_poc/1.1.json +91 -0
  55. package/__mocks__/packages/test-forms-registry.ts +12 -0
  56. package/__mocks__/patient.mock.ts +173 -0
  57. package/__mocks__/react-i18next.js +49 -0
  58. package/__mocks__/react-markdown.tsx +5 -0
  59. package/__mocks__/session.mock.ts +117 -0
  60. package/__mocks__/single-spa-react.js +11 -0
  61. package/__mocks__/use-initial-values/encounter.mock.json +963 -0
  62. package/__mocks__/use-initial-values/patient.mock.json +73 -0
  63. package/__mocks__/visit.mock.ts +19 -0
  64. package/dist/openmrs-esm-form-engine-lib.js +1 -0
  65. package/jest.config.js +30 -0
  66. package/package.json +104 -0
  67. package/prettier.config.js +8 -0
  68. package/readme/form-engine.jpeg +0 -0
  69. package/src/adapters/control-adapter.ts +29 -0
  70. package/src/adapters/encounter-datetime-adapter.ts +38 -0
  71. package/src/adapters/encounter-location-adapter.ts +39 -0
  72. package/src/adapters/encounter-provider-adapter.ts +48 -0
  73. package/src/adapters/encounter-role-adapter.ts +54 -0
  74. package/src/adapters/inline-date-adapter.ts +58 -0
  75. package/src/adapters/obs-adapter.ts +280 -0
  76. package/src/adapters/obs-comment-adapter.ts +60 -0
  77. package/src/adapters/orders-adapter.ts +75 -0
  78. package/src/adapters/patient-identifier-adapter.ts +40 -0
  79. package/src/adapters/program-state-adapter.ts +52 -0
  80. package/src/api/index.ts +178 -0
  81. package/src/components/error/error-modal.component.tsx +37 -0
  82. package/src/components/error/error.scss +4 -0
  83. package/src/components/extension/extension-parcel.component.tsx +32 -0
  84. package/src/components/field-label/field-label.component.tsx +32 -0
  85. package/src/components/field-label/field-label.scss +11 -0
  86. package/src/components/group/obs-group.component.tsx +29 -0
  87. package/src/components/group/obs-group.scss +12 -0
  88. package/src/components/inputs/content-switcher/content-switcher.component.tsx +71 -0
  89. package/src/components/inputs/content-switcher/content-switcher.scss +55 -0
  90. package/src/components/inputs/date/date.component.tsx +149 -0
  91. package/src/components/inputs/date/date.scss +36 -0
  92. package/src/components/inputs/file/camera/camera.component.tsx +34 -0
  93. package/src/components/inputs/file/camera/camera.scss +3 -0
  94. package/src/components/inputs/file/file.component.tsx +159 -0
  95. package/src/components/inputs/file/file.scss +101 -0
  96. package/src/components/inputs/fixed-value/fixed-value.component.tsx +19 -0
  97. package/src/components/inputs/markdown/markdown-wrapper.component.tsx +14 -0
  98. package/src/components/inputs/markdown/markdown.component.tsx +8 -0
  99. package/src/components/inputs/multi-select/multi-select.component.tsx +151 -0
  100. package/src/components/inputs/multi-select/multi-select.scss +25 -0
  101. package/src/components/inputs/multi-select/multi-select.test.tsx +90 -0
  102. package/src/components/inputs/number/number.component.tsx +69 -0
  103. package/src/components/inputs/number/number.scss +15 -0
  104. package/src/components/inputs/radio/radio.component.tsx +79 -0
  105. package/src/components/inputs/radio/radio.scss +36 -0
  106. package/src/components/inputs/select/dropdown.component.tsx +73 -0
  107. package/src/components/inputs/select/dropdown.scss +11 -0
  108. package/src/components/inputs/select/dropdown.test.tsx +120 -0
  109. package/src/components/inputs/text/text.component.tsx +65 -0
  110. package/src/components/inputs/text/text.scss +15 -0
  111. package/src/components/inputs/text/text.test.tsx +104 -0
  112. package/src/components/inputs/text-area/text-area.component.tsx +63 -0
  113. package/src/components/inputs/text-area/text-area.scss +11 -0
  114. package/src/components/inputs/toggle/toggle.component.tsx +66 -0
  115. package/src/components/inputs/toggle/toggle.scss +12 -0
  116. package/src/components/inputs/tooltip/tooltip.component.tsx +23 -0
  117. package/src/components/inputs/tooltip/tooltip.scss +8 -0
  118. package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +187 -0
  119. package/src/components/inputs/ui-select-extended/ui-select-extended.scss +15 -0
  120. package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +211 -0
  121. package/src/components/inputs/unspecified/unspecified.component.tsx +74 -0
  122. package/src/components/inputs/unspecified/unspecified.scss +7 -0
  123. package/src/components/inputs/unspecified/unspecified.test.tsx +95 -0
  124. package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +35 -0
  125. package/src/components/inputs/workspace-launcher/workspace-launcher.scss +15 -0
  126. package/src/components/label/label.component.tsx +20 -0
  127. package/src/components/label/label.scss +11 -0
  128. package/src/components/loaders/loader.component.tsx +16 -0
  129. package/src/components/loaders/loader.scss +20 -0
  130. package/src/components/patient-banner/patient-banner.component.tsx +20 -0
  131. package/src/components/patient-banner/patient-banner.scss +12 -0
  132. package/src/components/previous-value-review/previous-value-review.component.tsx +49 -0
  133. package/src/components/previous-value-review/previous-value-review.scss +36 -0
  134. package/src/components/processor-factory/form-processor-factory.component.tsx +127 -0
  135. package/src/components/renderer/custom-hooks-renderer.component.tsx +30 -0
  136. package/src/components/renderer/field/fieldLogic.ts +214 -0
  137. package/src/components/renderer/field/form-field-renderer.component.tsx +281 -0
  138. package/src/components/renderer/field/form-field-renderer.scss +5 -0
  139. package/src/components/renderer/form/form-renderer.component.tsx +89 -0
  140. package/src/components/renderer/form/state.ts +54 -0
  141. package/src/components/renderer/page/page.renderer.component.tsx +50 -0
  142. package/src/components/renderer/page/page.renderer.scss +36 -0
  143. package/src/components/renderer/section/section-renderer.component.tsx +21 -0
  144. package/src/components/renderer/section/section-renderer.scss +19 -0
  145. package/src/components/repeat/helpers.test.ts +29 -0
  146. package/src/components/repeat/helpers.ts +68 -0
  147. package/src/components/repeat/repeat-controls.component.tsx +38 -0
  148. package/src/components/repeat/repeat-controls.scss +7 -0
  149. package/src/components/repeat/repeat.component.tsx +201 -0
  150. package/src/components/repeat/repeat.scss +30 -0
  151. package/src/components/repeat/repeat.test.ts +29 -0
  152. package/src/components/sidebar/sidebar.component.tsx +134 -0
  153. package/src/components/sidebar/sidebar.scss +121 -0
  154. package/src/components/value/value.component.tsx +27 -0
  155. package/src/components/value/value.scss +17 -0
  156. package/src/components/value/view/field-value-view.component.tsx +33 -0
  157. package/src/components/value/view/field-value-view.scss +31 -0
  158. package/src/constants.ts +12 -0
  159. package/src/datasources/concept-data-source.ts +42 -0
  160. package/src/datasources/data-source.ts +23 -0
  161. package/src/datasources/encounter-role-datasource.ts +15 -0
  162. package/src/datasources/historical-data-source.ts +11 -0
  163. package/src/datasources/location-data-source.ts +27 -0
  164. package/src/datasources/provider-datasource.ts +15 -0
  165. package/src/datasources/select-concept-answers-datasource.ts +15 -0
  166. package/src/declarations.d.ts +4 -0
  167. package/src/external-function-context.tsx +8 -0
  168. package/src/form-context.tsx +42 -0
  169. package/src/form-engine.component.tsx +178 -0
  170. package/src/form-engine.scss +140 -0
  171. package/src/form-engine.test.tsx +817 -0
  172. package/src/globals.ts +1 -0
  173. package/src/hooks/useClobData.tsx +21 -0
  174. package/src/hooks/useConcepts.tsx +55 -0
  175. package/src/hooks/useDatasourceDependentValue.ts +16 -0
  176. package/src/hooks/useEncounter.tsx +32 -0
  177. package/src/hooks/useEncounterRole.tsx +15 -0
  178. package/src/hooks/useEvaluateFormFieldExpressions.ts +138 -0
  179. package/src/hooks/useFieldValidationResults.ts +18 -0
  180. package/src/hooks/useFormCollapse.tsx +36 -0
  181. package/src/hooks/useFormFieldValidators.ts +22 -0
  182. package/src/hooks/useFormFieldValueAdapters.ts +24 -0
  183. package/src/hooks/useFormFields.ts +37 -0
  184. package/src/hooks/useFormFieldsMeta.ts +48 -0
  185. package/src/hooks/useFormJson.test.tsx +173 -0
  186. package/src/hooks/useFormJson.tsx +237 -0
  187. package/src/hooks/useFormStateHelpers.ts +50 -0
  188. package/src/hooks/useFormsConfig.tsx +27 -0
  189. package/src/hooks/useInitialValues.ts +38 -0
  190. package/src/hooks/usePatientData.tsx +32 -0
  191. package/src/hooks/usePatientPrograms.ts +32 -0
  192. package/src/hooks/usePostSubmissionActions.test.tsx +42 -0
  193. package/src/hooks/usePostSubmissionActions.ts +31 -0
  194. package/src/hooks/useProcessorDependencies.ts +30 -0
  195. package/src/hooks/useRestMaxResultsCount.ts +5 -0
  196. package/src/hooks/useSystemSetting.ts +36 -0
  197. package/src/hooks/useWorkspaceLayout.ts +29 -0
  198. package/src/index.ts +12 -0
  199. package/src/lifecycle.ts +33 -0
  200. package/src/post-submission-actions/program-enrollment-action.ts +138 -0
  201. package/src/processors/encounter/encounter-form-processor.ts +337 -0
  202. package/src/processors/encounter/encounter-processor-helper.ts +320 -0
  203. package/src/processors/form-processor.ts +41 -0
  204. package/src/provider/form-factory-helper.ts +100 -0
  205. package/src/provider/form-factory-provider.tsx +169 -0
  206. package/src/provider/form-provider.tsx +37 -0
  207. package/src/registry/inbuilt-components/InbuiltPostSubmissionActions.ts +9 -0
  208. package/src/registry/inbuilt-components/control-templates.ts +57 -0
  209. package/src/registry/inbuilt-components/inbuiltControls.ts +99 -0
  210. package/src/registry/inbuilt-components/inbuiltDataSources.ts +41 -0
  211. package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +64 -0
  212. package/src/registry/inbuilt-components/inbuiltTransformers.ts +10 -0
  213. package/src/registry/inbuilt-components/inbuiltValidators.ts +33 -0
  214. package/src/registry/inbuilt-components/template-component-map.ts +28 -0
  215. package/src/registry/registry.test.ts +20 -0
  216. package/src/registry/registry.ts +261 -0
  217. package/src/routes.json +1 -0
  218. package/src/setupI18n.ts +16 -0
  219. package/src/setupTests.ts +5 -0
  220. package/src/transformers/default-schema-transformer.test.ts +155 -0
  221. package/src/transformers/default-schema-transformer.ts +239 -0
  222. package/src/types/domain.ts +129 -0
  223. package/src/types/index.ts +130 -0
  224. package/src/types/schema.ts +238 -0
  225. package/src/typings.d.ts +9 -0
  226. package/src/utils/boolean-utils.ts +25 -0
  227. package/src/utils/common-expression-helpers.ts +503 -0
  228. package/src/utils/common-utils.test.ts +136 -0
  229. package/src/utils/common-utils.ts +55 -0
  230. package/src/utils/error-utils.ts +37 -0
  231. package/src/utils/expression-parser.test.ts +308 -0
  232. package/src/utils/expression-parser.ts +158 -0
  233. package/src/utils/expression-runner.test.ts +387 -0
  234. package/src/utils/expression-runner.ts +219 -0
  235. package/src/utils/form-helper.test.ts +482 -0
  236. package/src/utils/form-helper.ts +210 -0
  237. package/src/utils/form-page-utils.ts +13 -0
  238. package/src/utils/forms-loader.test.ts +323 -0
  239. package/src/utils/forms-loader.ts +306 -0
  240. package/src/utils/post-submission-action-helper.ts +71 -0
  241. package/src/utils/test-utils.ts +54 -0
  242. package/src/utils/zscore-service.ts +59 -0
  243. package/src/validators/conditional-answered-validator.test.ts +61 -0
  244. package/src/validators/conditional-answered-validator.ts +17 -0
  245. package/src/validators/date-validator.test.ts +46 -0
  246. package/src/validators/date-validator.ts +19 -0
  247. package/src/validators/default-value-validator.test.ts +90 -0
  248. package/src/validators/default-value-validator.ts +36 -0
  249. package/src/validators/form-validator.test.ts +188 -0
  250. package/src/validators/form-validator.ts +95 -0
  251. package/src/validators/js-expression-validator.test.ts +118 -0
  252. package/src/validators/js-expression-validator.ts +44 -0
  253. package/src/validators/schema.ts +34 -0
  254. package/src/zscore/bfa_boys_5_above.json +2522 -0
  255. package/src/zscore/bfa_girls_5_above.json +2522 -0
  256. package/src/zscore/hfa_boys_5_above.json +2186 -0
  257. package/src/zscore/hfa_boys_below5.json +22286 -0
  258. package/src/zscore/hfa_girls_5_above.json +2186 -0
  259. package/src/zscore/hfa_girls_below5.json +22286 -0
  260. package/src/zscore/wfl_boys_below5.json +7814 -0
  261. package/src/zscore/wfl_girls_below5.json +7814 -0
  262. package/src/zscore-tests/bmi-age.test.tsx +88 -0
  263. package/src/zscore-tests/height-age.test.tsx +96 -0
  264. package/src/zscore-tests/weight-height.test.tsx +87 -0
  265. package/tools/i18next-parser.config.js +93 -0
  266. package/translations/en.json +47 -0
  267. package/translations/es.json +38 -0
  268. package/translations/fr.json +38 -0
  269. package/translations/km.json +38 -0
  270. package/tsconfig.json +19 -0
  271. package/turbo.json +15 -0
  272. package/webpack.config.js +1 -0
@@ -0,0 +1,817 @@
1
+ import React from 'react';
2
+ import dayjs from 'dayjs';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { act, cleanup, render, screen, within, fireEvent, waitFor } from '@testing-library/react';
5
+ import { restBaseUrl } from '@openmrs/esm-framework';
6
+ import { parseDate } from '@internationalized/date';
7
+ import { when } from 'jest-when';
8
+ import * as api from './api';
9
+ import { assertFormHasAllFields, findMultiSelectInput, findSelectInput } from './utils/test-utils';
10
+ import { evaluatePostSubmissionExpression } from './utils/post-submission-action-helper';
11
+ import { mockPatient } from '__mocks__/patient.mock';
12
+ import { mockSessionDataResponse } from '__mocks__/session.mock';
13
+ import { mockVisit } from '__mocks__/visit.mock';
14
+ import ageValidationForm from '__mocks__/forms/rfe-forms/age-validation-form.json';
15
+ import bmiForm from '__mocks__/forms/rfe-forms/bmi-test-form.json';
16
+ import bsaForm from '__mocks__/forms/rfe-forms/bsa-test-form.json';
17
+ import demoHtsForm from '__mocks__/forms/rfe-forms/demo_hts-form.json';
18
+ import demoHtsOpenmrsForm from '__mocks__/forms/afe-forms/demo_hts-form.json';
19
+ import eddForm from '__mocks__/forms/rfe-forms/edd-test-form.json';
20
+ import externalDataSourceForm from '__mocks__/forms/rfe-forms/external_data_source_form.json';
21
+ import filterAnswerOptionsTestForm from '__mocks__/forms/rfe-forms/filter-answer-options-test-form.json';
22
+ import htsPocForm from '__mocks__/packages/hiv/forms/hts_poc/1.1.json';
23
+ import labourAndDeliveryTestForm from '__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json';
24
+ import mockConceptsForm from '__mocks__/concepts.mock.json';
25
+ import monthsOnArtForm from '__mocks__/forms/rfe-forms/months-on-art-form.json';
26
+ import nextVisitForm from '__mocks__/forms/rfe-forms/next-visit-test-form.json';
27
+ import obsGroupTestForm from '__mocks__/forms/rfe-forms/obs-group-test_form.json';
28
+ import postSubmissionTestForm from '__mocks__/forms/rfe-forms/post-submission-test-form.json';
29
+ import referenceByMappingForm from '__mocks__/forms/rfe-forms/reference-by-mapping-form.json';
30
+ import sampleFieldsForm from '__mocks__/forms/rfe-forms/sample_fields.json';
31
+ import testEnrolmentForm from '__mocks__/forms/rfe-forms/test-enrolment-form.json';
32
+ import viralLoadStatusForm from '__mocks__/forms/rfe-forms/viral-load-status-form.json';
33
+ import historicalExpressionsForm from '__mocks__/forms/rfe-forms/historical-expressions-form.json';
34
+ import mockHxpEncounter from '__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json';
35
+ import requiredTestForm from '__mocks__/forms/rfe-forms/required-form.json';
36
+ import conditionalRequiredTestForm from '__mocks__/forms/rfe-forms/conditional-required-form.json';
37
+ import conditionalAnsweredForm from '__mocks__/forms/rfe-forms/conditional-answered-form.json';
38
+ import FormEngine from './form-engine.component';
39
+
40
+ const patientUUID = '8673ee4f-e2ab-4077-ba55-4980f408773e';
41
+ const visit = mockVisit;
42
+ const mockOpenmrsFetch = jest.fn();
43
+ const formsResourcePath = when((url: string) => url.includes(`${restBaseUrl}/form/`));
44
+ const clobdataResourcePath = when((url: string) => url.includes(`${restBaseUrl}/clobdata/`));
45
+ global.ResizeObserver = require('resize-observer-polyfill');
46
+
47
+ when(mockOpenmrsFetch).calledWith(formsResourcePath).mockReturnValue({ data: demoHtsOpenmrsForm });
48
+ when(mockOpenmrsFetch).calledWith(clobdataResourcePath).mockReturnValue({ data: demoHtsForm });
49
+
50
+ const locale = window.i18next.language == 'en' ? 'en-GB' : window.i18next.language;
51
+
52
+ // jest.mock('@openmrs/esm-framework', () => {
53
+ // const originalModule = jest.requireActual('@openmrs/esm-framework');
54
+
55
+ // return {
56
+ // ...originalModule,
57
+ // createErrorHandler: jest.fn(),
58
+ // showNotification: jest.fn(),
59
+ // showToast: jest.fn(),
60
+ // getAsyncLifecycle: jest.fn(),
61
+ // usePatient: jest.fn().mockImplementation(() => ({ patient: mockPatient })),
62
+ // registerExtension: jest.fn(),
63
+ // useSession: jest.fn().mockImplementation(() => mockSessionDataResponse.data),
64
+ // openmrsFetch: jest.fn().mockImplementation((args) => mockOpenmrsFetch(args)),
65
+ // OpenmrsDatePicker: jest.fn().mockImplementation(({ id, labelText, value, onChange, isInvalid, invalidText }) => {
66
+ // return (
67
+ // <>
68
+ // <label htmlFor={id}>{labelText}</label>
69
+ // <input
70
+ // id={id}
71
+ // value={value ? dayjs(value).format('DD/MM/YYYY') : undefined}
72
+ // onChange={(evt) => onChange(parseDate(dayjs(evt.target.value).format('YYYY-MM-DD')))}
73
+ // />
74
+ // {isInvalid && invalidText && <span>{invalidText}</span>}
75
+ // </>
76
+ // );
77
+ // }),
78
+ // };
79
+ // });
80
+
81
+ jest.mock('../src/api', () => {
82
+ const originalModule = jest.requireActual('../src/api');
83
+
84
+ return {
85
+ ...originalModule,
86
+ getPreviousEncounter: jest.fn().mockImplementation(() => Promise.resolve(mockHxpEncounter)),
87
+ getConcept: jest.fn().mockImplementation(() => Promise.resolve(null)),
88
+ getLatestObs: jest.fn().mockImplementation(() => Promise.resolve({ valueNumeric: 60 })),
89
+ saveEncounter: jest.fn(),
90
+ createProgramEnrollment: jest.fn(),
91
+ };
92
+ });
93
+
94
+ jest.mock('./hooks/useRestMaxResultsCount', () => jest.fn().mockReturnValue({ systemSetting: { value: '50' } }));
95
+
96
+ xdescribe('Form engine component', () => {
97
+ const user = userEvent.setup();
98
+
99
+ afterEach(() => {
100
+ jest.useRealTimers();
101
+ });
102
+
103
+ it('should render the form schema without dying', async () => {
104
+ await act(async () => renderForm(null, htsPocForm));
105
+
106
+ await assertFormHasAllFields(screen, [{ fieldName: 'When was the HIV test conducted? *', fieldType: 'date' }]);
107
+ });
108
+
109
+ it('should render by the form UUID without dying', async () => {
110
+ await act(async () => {
111
+ renderForm('955ab92f-f93e-4dc0-9c68-b7b2346def55', null);
112
+ });
113
+
114
+ await assertFormHasAllFields(screen, [
115
+ { fieldName: 'When was the HIV test conducted? *', fieldType: 'date' },
116
+ { fieldName: 'Community service delivery point', fieldType: 'select' },
117
+ { fieldName: 'TB screening', fieldType: 'combobox' },
118
+ ]);
119
+ });
120
+
121
+ it('should demonstrate behaviour driven by form intents', async () => {
122
+ await act(async () => {
123
+ renderForm('955ab92f-f93e-4dc0-9c68-b7b2346def55', null, 'HTS_INTENT_A');
124
+ });
125
+
126
+ await assertFormHasAllFields(screen, [
127
+ { fieldName: 'When was the HIV test conducted? *', fieldType: 'date' },
128
+ { fieldName: 'TB screening', fieldType: 'combobox' },
129
+ ]);
130
+
131
+ try {
132
+ await findSelectInput(screen, 'Community service delivery point');
133
+ fail("Field with title 'Community service delivery point' should not be found");
134
+ } catch (err) {
135
+ expect(
136
+ err.message.includes('Unable to find role="combobox" and name "Community service delivery point"'),
137
+ ).toBeTruthy();
138
+ }
139
+
140
+ // cleanup
141
+ cleanup();
142
+
143
+ // HTS_INTENT_B
144
+ await act(async () => {
145
+ renderForm('955ab92f-f93e-4dc0-9c68-b7b2346def55', null, 'HTS_INTENT_B');
146
+ });
147
+
148
+ await assertFormHasAllFields(screen, [
149
+ { fieldName: 'When was the HIV test conducted? *', fieldType: 'date' },
150
+ { fieldName: 'Community service delivery point', fieldType: 'combobox' },
151
+ ]);
152
+
153
+ try {
154
+ await findMultiSelectInput(screen, 'TB screening');
155
+ fail("Field with title 'TB screening' should not be found");
156
+ } catch (err) {
157
+ expect(err.message.includes('Unable to find role="combobox" and name `/TB screening/i`')).toBeTruthy();
158
+ }
159
+ });
160
+
161
+ describe('Question info', () => {
162
+ it('should ascertain that each field with questionInfo passed will display a tooltip', async () => {
163
+ await act(async () => {
164
+ renderForm(null, sampleFieldsForm);
165
+ });
166
+
167
+ screen.findByRole('textbox', { name: /text question/i });
168
+
169
+ const textFieldTooltip = screen.getByTestId('id_text');
170
+ expect(textFieldTooltip).toBeInTheDocument();
171
+
172
+ await user.hover(textFieldTooltip);
173
+ await screen.findByText(/sample tooltip info for text/i);
174
+ });
175
+ });
176
+
177
+ describe('conditional answered validation', () => {
178
+ it('should fail if the referenced field has a value that does not exist on the referenced answers array', async () => {
179
+ await act(async () => {
180
+ renderForm(null, conditionalAnsweredForm);
181
+ });
182
+
183
+ const hospitalizationHistoryDropdown = screen.getByRole('combobox', {
184
+ name: /was the patient hospitalized since last visit\?/i,
185
+ });
186
+ const hospitalizationReasonDropdown = screen.getByRole('combobox', {
187
+ name: /reason for hospitalization:/i,
188
+ });
189
+
190
+ expect(hospitalizationHistoryDropdown);
191
+ expect(hospitalizationReasonDropdown);
192
+
193
+ fireEvent.click(hospitalizationHistoryDropdown);
194
+
195
+ expect(screen.getByText(/yes/i)).toBeInTheDocument();
196
+ expect(screen.getByText(/no/i)).toBeInTheDocument();
197
+
198
+ fireEvent.click(screen.getByText(/no/i));
199
+
200
+ fireEvent.click(hospitalizationReasonDropdown);
201
+
202
+ expect(screen.getByText(/Maternal Visit/i)).toBeInTheDocument();
203
+ expect(screen.getByText(/Emergency Visit/i)).toBeInTheDocument();
204
+ expect(screen.getByText(/Unscheduled visit late/i)).toBeInTheDocument();
205
+
206
+ fireEvent.click(screen.getByText(/Maternal Visit/i));
207
+
208
+ const errorMessage = screen.getByText(
209
+ /Providing diagnosis but didn't answer that patient was hospitalized in question/i,
210
+ );
211
+
212
+ expect(errorMessage).toBeInTheDocument();
213
+
214
+ fireEvent.click(hospitalizationHistoryDropdown);
215
+ fireEvent.click(screen.getByText(/yes/i));
216
+
217
+ expect(errorMessage).not.toBeInTheDocument();
218
+ });
219
+ });
220
+
221
+ describe('historical expressions', () => {
222
+ it('should ascertain getPreviousEncounter() returns an encounter and the historical expression displays on the UI', async () => {
223
+ const user = userEvent.setup();
224
+
225
+ renderForm(null, historicalExpressionsForm, 'COVID Assessment');
226
+
227
+ //ascertain form has rendered
228
+ await screen.findByRole('combobox', { name: /Reasons for assessment/i });
229
+
230
+ //ascertain function fetching the encounter has been called
231
+ expect(api.getPreviousEncounter).toHaveBeenCalled();
232
+ expect(api.getPreviousEncounter).toHaveReturnedWith(Promise.resolve(mockHxpEncounter));
233
+
234
+ const reuseValueButton = screen.getByRole('button', { name: /reuse value/i });
235
+ const evaluatedHistoricalValue = screen.getByText(/Entry into a country/i);
236
+
237
+ expect(reuseValueButton).toBeInTheDocument;
238
+ expect(evaluatedHistoricalValue).toBeInTheDocument;
239
+ });
240
+ });
241
+
242
+ describe('Form submission', () => {
243
+ it('should validate required field on form submission', async () => {
244
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
245
+
246
+ await act(async () => {
247
+ renderForm(null, requiredTestForm);
248
+ });
249
+
250
+ await user.click(screen.getByRole('button', { name: /save/i }));
251
+
252
+ const labels = screen.getAllByText(/Text question/i);
253
+ expect(labels).toHaveLength(2);
254
+
255
+ const requiredAsterisks = screen.getAllByText('*');
256
+ expect(requiredAsterisks).toHaveLength(2);
257
+
258
+ const inputFields = screen.getAllByRole('textbox', { name: /Text question/i });
259
+ expect(inputFields).toHaveLength(2);
260
+
261
+ inputFields.forEach((inputField) => {
262
+ expect(inputField).toHaveClass('cds--text-input--invalid');
263
+ });
264
+
265
+ const errorMessages = screen.getAllByText('Field is mandatory');
266
+ expect(errorMessages).toHaveLength(2);
267
+ errorMessages.forEach((errorMessage, index) => {
268
+ expect(errorMessage).toBeInTheDocument();
269
+ });
270
+ expect(saveEncounterMock).toHaveBeenCalledTimes(0);
271
+ });
272
+
273
+ it('should validate conditional required field on form submission', async () => {
274
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
275
+
276
+ await act(async () => {
277
+ renderForm(null, conditionalRequiredTestForm);
278
+ });
279
+
280
+ const visitScheduledDropdown = screen.getByRole('combobox', { name: /Was this visit scheduled?/i });
281
+ await user.click(visitScheduledDropdown);
282
+
283
+ expect(screen.queryByRole('option', { name: /Unscheduled Visit Early/i })).toBeInTheDocument();
284
+ expect(screen.queryByRole('option', { name: /Unscheduled Visit Late/i })).toBeInTheDocument();
285
+ expect(screen.getByRole('option', { name: 'Scheduled visit' })).toBeInTheDocument();
286
+
287
+ const options = screen.getAllByRole('option');
288
+ await user.click(options[2]);
289
+ await user.click(screen.getByRole('button', { name: /save/i }));
290
+
291
+ await assertFormHasAllFields(screen, [
292
+ { fieldName: 'Was this visit scheduled?', fieldType: 'combobox' },
293
+ { fieldName: 'If Unscheduled, actual text scheduled date *', fieldType: 'text' },
294
+ { fieldName: 'If Unscheduled, actual scheduled date *', fieldType: 'date' },
295
+ { fieldName: 'If Unscheduled, actual number scheduled date *', fieldType: 'number' },
296
+ { fieldName: 'If Unscheduled, actual text area scheduled date *', fieldType: 'textarea' },
297
+ { fieldName: 'Not required actual text area scheduled date', fieldType: 'textarea' },
298
+ { fieldName: 'If Unscheduled, actual scheduled reason select *', fieldType: 'select' },
299
+ { fieldName: 'If Unscheduled, actual scheduled reason multi-select *', fieldType: 'combobox' },
300
+ { fieldName: 'If Unscheduled, actual scheduled reason radio *', fieldType: 'radio' },
301
+ ]);
302
+
303
+ // TODO: Temporarily disabling this until the core date picker mock gets fixed
304
+ // Issue - https://openmrs.atlassian.net/browse/O3-3479
305
+ // Validate date field
306
+ // const dateInputField = await screen.getByLabelText(/If Unscheduled, actual scheduled date/i);
307
+ // expect(dateInputField).toHaveClass('cds--date-picker__input--invalid');
308
+ const errorMessage = await screen.findByText(
309
+ 'Patient visit marked as unscheduled. Please provide the scheduled date.',
310
+ );
311
+ expect(errorMessage).toBeInTheDocument();
312
+
313
+ // Validate text field
314
+ const textInputField = screen.getByLabelText(/If Unscheduled, actual text scheduled date/i);
315
+ expect(textInputField).toHaveClass('cds--text-input--invalid');
316
+ const textErrorMessage = screen.getByText(
317
+ 'Patient visit marked as unscheduled. Please provide the scheduled text date.',
318
+ );
319
+ expect(textErrorMessage).toBeInTheDocument();
320
+
321
+ // Validate number field
322
+ const numberInputField = screen.getByLabelText(/If Unscheduled, actual number scheduled date/i);
323
+ const dataInvalidValue = numberInputField.getAttribute('data-invalid');
324
+ expect(dataInvalidValue).toBe('true');
325
+ const numberErrorMessage = screen.getByText(
326
+ 'Patient visit marked as unscheduled. Please provide the scheduled number',
327
+ );
328
+ expect(numberErrorMessage).toBeInTheDocument();
329
+
330
+ // Validate text area field
331
+ const textAreaInputField = screen.getByLabelText(/If Unscheduled, actual text area scheduled date/i);
332
+ expect(textAreaInputField).toHaveClass('cds--text-area cds--text-area--invalid');
333
+ const textAreaErrorMessage = screen.getByText(
334
+ 'Patient visit marked as unscheduled. Please provide the scheduled text area date.',
335
+ );
336
+ expect(textAreaErrorMessage).toBeInTheDocument();
337
+
338
+ // Validate Select field
339
+ const selectInputField = screen.getByText('If Unscheduled, actual scheduled reason select', {
340
+ selector: 'span',
341
+ });
342
+ expect(selectInputField).toBeInTheDocument();
343
+ const selectErrorMessage = screen.getByText(
344
+ 'Patient visit marked as unscheduled. Please provide the scheduled reason select',
345
+ );
346
+ expect(selectErrorMessage).toBeInTheDocument();
347
+
348
+ // Validate multi-select field
349
+ const multiSelectInputField = screen.getByLabelText(/If Unscheduled, actual scheduled reason multi-select/i);
350
+ expect(multiSelectInputField).toBeInTheDocument();
351
+ const multiSelectErrorMessage = screen.getByText(
352
+ 'Patient visit marked as unscheduled. Please provide the scheduled multi-select reason.',
353
+ );
354
+ expect(multiSelectErrorMessage).toBeInTheDocument();
355
+
356
+ // Validate radio field
357
+ const radioInputField = screen.getByText('If Unscheduled, actual scheduled reason radio', {
358
+ selector: 'span',
359
+ });
360
+ expect(radioInputField).toBeInTheDocument();
361
+ const radioErrorMessage = screen.getByText(
362
+ 'Patient visit marked as unscheduled. Please provide the scheduled radio reason.',
363
+ );
364
+ expect(radioErrorMessage).toBeInTheDocument();
365
+ expect(saveEncounterMock).toHaveBeenCalledTimes(0);
366
+ });
367
+
368
+ it('should validate form submission', async () => {
369
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
370
+
371
+ await act(async () => {
372
+ renderForm(null, testEnrolmentForm);
373
+ });
374
+
375
+ screen.queryByRole('textbox', { name: /enrolment date/i });
376
+
377
+ const enrolmentDateField = screen.getByRole('textbox', { name: /enrolment date/i });
378
+ const uniqueIdField = screen.getByRole('textbox', { name: /unique id/i });
379
+ const motherEnrolledField = screen.getByRole('radio', { name: /mother enrolled in pmtct program/i });
380
+ const generalPopulationField = screen.getByRole('radio', { name: /general population/i });
381
+
382
+ await user.click(enrolmentDateField);
383
+ await user.paste('2023-09-09T00:00:00.000Z');
384
+ await user.type(uniqueIdField, 'U0-001109');
385
+ await user.click(motherEnrolledField);
386
+ await user.click(generalPopulationField);
387
+ await user.click(screen.getByRole('button', { name: /save/i }));
388
+
389
+ expect(saveEncounterMock).toHaveBeenCalledTimes(1);
390
+ expect(saveEncounterMock).toHaveBeenCalledWith(expect.any(AbortController), expect.any(Object), undefined);
391
+ expect(saveEncounterMock).toHaveReturned();
392
+ });
393
+
394
+ it('should validate transient fields', async () => {
395
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
396
+
397
+ await act(async () => {
398
+ renderForm(null, testEnrolmentForm);
399
+ });
400
+
401
+ const enrolmentDateField = screen.getByRole('textbox', { name: /enrolment date/i });
402
+ const uniqueIdField = screen.getByRole('textbox', { name: /unique id/i });
403
+ const motherEnrolledField = screen.getByRole('radio', { name: /mother enrolled in pmtct program/i });
404
+ const generalPopulationField = screen.getByRole('radio', { name: /general population/i });
405
+
406
+ await user.click(enrolmentDateField);
407
+ await user.paste('2023-09-09T00:00:00.000Z');
408
+ await user.type(uniqueIdField, 'U0-001109');
409
+ await user.click(motherEnrolledField);
410
+ await user.click(generalPopulationField);
411
+
412
+ await user.click(screen.getByRole('button', { name: /save/i }));
413
+
414
+ expect(enrolmentDateField).toHaveValue('09/09/2023');
415
+
416
+ const [abortController, encounter, encounterUuid] = saveEncounterMock.mock.calls[0];
417
+ expect(encounter.obs.length).toEqual(3);
418
+ expect(encounter.obs.find((obs) => obs.formFieldPath === 'rfe-forms-hivEnrolmentDate')).toBeUndefined();
419
+ });
420
+
421
+ it('should evaluate post submission enabled flag expression', () => {
422
+ const encounters = [
423
+ {
424
+ uuid: '47cfe95b-357a-48f8-aa70-63eb5ae51916',
425
+ obs: [
426
+ {
427
+ formFieldPath: 'rfe-forms-tbProgramType',
428
+ value: {
429
+ display: 'Tuberculosis treatment program',
430
+ uuid: '160541AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
431
+ },
432
+ },
433
+ {
434
+ formFieldPath: 'rfe-forms-tbRegDate',
435
+ value: '2023-12-05T00:00:00.000+0000',
436
+ },
437
+ ],
438
+ },
439
+ ];
440
+
441
+ const expression1 = "tbProgramType === '160541AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'";
442
+ const expression2 = "tbProgramType === '160052AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'";
443
+ let enabled = evaluatePostSubmissionExpression(expression1, encounters);
444
+ expect(enabled).toEqual(true);
445
+
446
+ enabled = evaluatePostSubmissionExpression(expression2, encounters);
447
+ expect(enabled).toEqual(false);
448
+ });
449
+
450
+ it('should test post submission actions', async () => {
451
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
452
+ saveEncounterMock.mockResolvedValue({
453
+ headers: null,
454
+ ok: true,
455
+ redirected: false,
456
+ status: 200,
457
+ statusText: 'ok',
458
+ type: 'default',
459
+ url: '',
460
+ clone: null,
461
+ body: null,
462
+ bodyUsed: null,
463
+ arrayBuffer: null,
464
+ blob: null,
465
+ formData: null,
466
+ json: null,
467
+ text: jest.fn(),
468
+ data: [
469
+ {
470
+ uuid: '47cfe95b-357a-48f8-aa70-63eb5ae51916',
471
+ obs: [
472
+ {
473
+ formFieldPath: 'rfe-forms-tbProgramType',
474
+ value: {
475
+ display: 'Tuberculosis treatment program',
476
+ uuid: '160541AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
477
+ },
478
+ },
479
+ {
480
+ formFieldPath: 'rfe-forms-tbRegDate',
481
+ value: '2023-12-05T00:00:00.000+0000',
482
+ },
483
+ ],
484
+ },
485
+ ],
486
+ });
487
+
488
+ await act(async () => renderForm(null, postSubmissionTestForm));
489
+
490
+ const drugSensitiveProgramField = screen.getByRole('radio', { name: /drug-susceptible \(DS\) tb program/i });
491
+ const treatmentNumber = screen.getByRole('spinbutton', { name: /ds tb treatment number/i });
492
+
493
+ await user.click(drugSensitiveProgramField);
494
+ await user.click(screen.getByRole('textbox', { name: /date enrolled in tuberculosis \(TB\) care/i }));
495
+ await user.paste('2023-12-12');
496
+ await user.click(treatmentNumber);
497
+ await user.paste('11200');
498
+ await user.click(screen.getByRole('button', { name: /save/i }));
499
+
500
+ expect(saveEncounterMock).toHaveBeenCalled();
501
+ });
502
+
503
+ it('should save on form submission on initial state', async () => {
504
+ const saveEncounterMock = jest.spyOn(api, 'saveEncounter');
505
+
506
+ await act(async () => {
507
+ renderForm(null, conditionalRequiredTestForm);
508
+ });
509
+ await assertFormHasAllFields(screen, [{ fieldName: 'Was this visit scheduled?', fieldType: 'combobox' }]);
510
+ await user.click(screen.getByRole('button', { name: /save/i }));
511
+ expect(saveEncounterMock).toHaveBeenCalled();
512
+ expect(saveEncounterMock).toHaveBeenCalledWith(expect.any(AbortController), expect.any(Object), undefined);
513
+ expect(saveEncounterMock).toHaveReturned();
514
+ });
515
+ });
516
+
517
+ describe('Obs group count validation', () => {
518
+ it('should limit number of repeatable obs groups based on configured repeat limit', async () => {
519
+ await act(async () => renderForm(null, labourAndDeliveryTestForm));
520
+
521
+ const birthCount = screen.getByRole('spinbutton', { name: /number of babies born from this pregnancy/i });
522
+ expect(birthCount).toBeInTheDocument();
523
+
524
+ await user.type(birthCount, '2');
525
+ expect(birthCount).toHaveValue(2);
526
+
527
+ // Male radio button in 'sex at birth' field
528
+ const maleSexLabel = screen.getByRole('radio', { name: /^male$/i });
529
+ expect(maleSexLabel).toBeInTheDocument();
530
+
531
+ await user.click(maleSexLabel);
532
+ expect(maleSexLabel).toBeChecked();
533
+
534
+ // Missing radio button in "infant status" field
535
+ const infantStatus = screen.getByRole('group', { name: /infant status at birth/i });
536
+ const infantStatusMissingLabel = within(infantStatus).getByRole('radio', { name: 'Missing' });
537
+ expect(infantStatusMissingLabel).toBeInTheDocument();
538
+
539
+ await user.click(infantStatusMissingLabel);
540
+ expect(infantStatusMissingLabel).toBeChecked();
541
+
542
+ const dateOfBirth = screen.getByRole('textbox', { name: /date of birth/i });
543
+ expect(dateOfBirth).toBeInTheDocument();
544
+
545
+ await user.click(dateOfBirth);
546
+ await user.paste('2022-03-11T00:00:00.000Z');
547
+ await user.tab();
548
+
549
+ expect(dateOfBirth).toHaveValue('11/03/2022');
550
+
551
+ await user.click(screen.getByRole('button', { name: 'Add' }));
552
+
553
+ expect(screen.getByRole('button', { name: 'Add' })).toBeDisabled();
554
+ });
555
+ });
556
+
557
+ describe('Filter answer options', () => {
558
+ it('should filter dropdown options based on value in count input field', async () => {
559
+ await act(async () => renderForm(null, filterAnswerOptionsTestForm));
560
+
561
+ const recommendationDropdown = screen.getByRole('combobox', { name: /Testing Recommendations/i });
562
+ const testCountField = screen.getByRole('spinbutton', { name: 'How many times have you tested in the past?' });
563
+
564
+ await user.click(recommendationDropdown);
565
+
566
+ expect(screen.queryByRole('option', { name: /Perfect testing/i })).toBeInTheDocument();
567
+ expect(screen.queryByRole('option', { name: /Minimal testing/i })).toBeInTheDocument();
568
+ expect(screen.queryByRole('option', { name: /Un-decisive/i })).toBeInTheDocument();
569
+ expect(screen.queryByRole('option', { name: /Not ideal/i })).toBeInTheDocument();
570
+
571
+ await user.click(recommendationDropdown);
572
+ await user.type(testCountField, '6');
573
+ await user.click(recommendationDropdown);
574
+
575
+ expect(testCountField).toHaveValue(6);
576
+ expect(screen.queryByRole('option', { name: /Perfect testing/i })).toBeNull();
577
+ expect(screen.queryByRole('option', { name: /Minimal testing/i })).toBeNull();
578
+ expect(screen.queryByRole('option', { name: /Un-decisive/i })).toBeInTheDocument();
579
+ expect(screen.queryByRole('option', { name: /Not ideal/i })).toBeInTheDocument();
580
+ });
581
+ });
582
+
583
+ describe('Calculated values', () => {
584
+ it('should evaluate BMI', async () => {
585
+ await act(async () => renderForm(null, bmiForm));
586
+
587
+ const bmiField = screen.getByRole('textbox', { name: /bmi/i });
588
+ const heightField = screen.getByLabelText(/height/i);
589
+ const weightField = screen.getByLabelText(/weight/i);
590
+
591
+ await user.type(weightField, '50');
592
+ await user.type(heightField, '150');
593
+ await user.tab();
594
+
595
+ expect(heightField).toHaveValue(150);
596
+ expect(weightField).toHaveValue(50);
597
+ expect(bmiField).toHaveValue('22.2');
598
+ });
599
+
600
+ it('should evaluate BSA', async () => {
601
+ await act(async () => renderForm(null, bsaForm));
602
+
603
+ const bsaField = screen.getByRole('textbox', { name: /bsa/i });
604
+ const heightField = screen.getByRole('spinbutton', { name: /height/i });
605
+ const weightField = screen.getByRole('spinbutton', { name: /weight/i });
606
+
607
+ await user.type(heightField, '190.5');
608
+ await user.type(weightField, '95');
609
+ await user.tab();
610
+
611
+ expect(heightField).toHaveValue(190.5);
612
+ expect(weightField).toHaveValue(95);
613
+ expect(bsaField).toHaveValue('2.24');
614
+ });
615
+
616
+ it('should evaluate EDD', async () => {
617
+ await act(async () => renderForm(null, eddForm));
618
+
619
+ const eddField = screen.getByRole('textbox', { name: /edd/i });
620
+ const lmpField = screen.getByRole('textbox', { name: /lmp/i });
621
+
622
+ await user.click(lmpField);
623
+ await user.paste('2022-07-06T00:00:00.000Z');
624
+ await user.tab();
625
+
626
+ expect(lmpField).toHaveValue(dayjs('2022-07-06').toDate().toLocaleDateString(locale));
627
+ expect(eddField).toHaveValue(dayjs('2023-04-12').toDate().toLocaleDateString(locale));
628
+ });
629
+
630
+ it('should evaluate months on ART', async () => {
631
+ await act(async () => renderForm(null, monthsOnArtForm));
632
+
633
+ jest.useFakeTimers();
634
+ jest.setSystemTime(new Date(2022, 9, 1));
635
+
636
+ let artStartDateField = screen.getByRole('textbox', {
637
+ name: /antiretroviral treatment start date/i,
638
+ });
639
+ let monthsOnArtField = screen.getByRole('spinbutton', {
640
+ name: /months on art/i,
641
+ });
642
+
643
+ expect(artStartDateField).not.toHaveValue();
644
+ expect(monthsOnArtField).not.toHaveValue();
645
+
646
+ fireEvent.change(artStartDateField, { target: { value: '02/05/2022' } });
647
+ fireEvent.blur(artStartDateField, { target: { value: '02/05/2022' } });
648
+
649
+ await waitFor(() => {
650
+ expect(monthsOnArtField).toHaveValue(7);
651
+ });
652
+ });
653
+
654
+ it('should evaluate viral load status', async () => {
655
+ renderForm(null, viralLoadStatusForm);
656
+
657
+ let viralLoadCountField = await screen.findByRole('spinbutton', {
658
+ name: /viral load count/i,
659
+ });
660
+ let suppressedField = await screen.findByRole('radio', {
661
+ name: /^suppressed$/i,
662
+ });
663
+ let unsuppressedField = await screen.findByRole('radio', {
664
+ name: /unsuppressed/i,
665
+ });
666
+
667
+ fireEvent.blur(viralLoadCountField, { target: { value: 30 } });
668
+
669
+ await waitFor(() => {
670
+ expect(viralLoadCountField).toHaveValue(30);
671
+ expect(suppressedField).toBeChecked();
672
+ expect(unsuppressedField).not.toBeChecked();
673
+ });
674
+ });
675
+
676
+ it('should only show question when age is under 5', async () => {
677
+ await act(async () => renderForm(null, ageValidationForm));
678
+
679
+ let enrollmentDate = screen.getByRole('textbox', {
680
+ name: /enrollmentDate/,
681
+ });
682
+
683
+ expect(enrollmentDate).not.toHaveValue();
684
+ await user.click(enrollmentDate);
685
+ await user.paste('1975-07-06T00:00:00.000Z');
686
+ await user.tab();
687
+
688
+ let mrn = screen.getByRole('textbox', {
689
+ name: /mrn/i,
690
+ });
691
+
692
+ expect(enrollmentDate).toHaveValue(new Date('1975-07-06T00:00:00.000Z').toLocaleDateString(locale));
693
+
694
+ expect(mrn).toBeVisible();
695
+ });
696
+
697
+ it('should load initial value from external arbitrary data source', async () => {
698
+ await act(async () => renderForm(null, externalDataSourceForm));
699
+
700
+ const bodyWeightField = screen.getByRole('spinbutton', {
701
+ name: /body weight/i,
702
+ });
703
+
704
+ await waitFor(() => expect(bodyWeightField).toHaveValue(60));
705
+ });
706
+
707
+ it('should evaluate next visit date', async () => {
708
+ await act(async () => renderForm(null, nextVisitForm));
709
+
710
+ const followupDateField = screen.getByRole('textbox', {
711
+ name: /followup date/i,
712
+ });
713
+ const nextVisitDateField = screen.getByRole('textbox', {
714
+ name: /next visit date/i,
715
+ });
716
+ const arvDispensedInDaysField = screen.getByRole('spinbutton', {
717
+ name: /arv dispensed in days/i,
718
+ });
719
+
720
+ await user.click(followupDateField);
721
+ await user.paste('2022-07-06T00:00:00.000Z');
722
+ await user.tab();
723
+
724
+ await user.click(arvDispensedInDaysField);
725
+ await user.type(arvDispensedInDaysField, '120');
726
+ await user.tab();
727
+
728
+ expect(arvDispensedInDaysField).toHaveValue(120);
729
+ expect(nextVisitDateField).toHaveValue('03/11/2022');
730
+ });
731
+ });
732
+
733
+ describe('Concept references', () => {
734
+ const conceptResourcePath = when((url: string) =>
735
+ url.includes(`${restBaseUrl}/concept?references=PIH:Occurrence of trauma,PIH:Yes,PIH:No,PIH:COUGH`),
736
+ );
737
+
738
+ when(mockOpenmrsFetch).calledWith(conceptResourcePath).mockReturnValue({ data: mockConceptsForm });
739
+
740
+ it('should add default labels based on concept display and substitute mapping references with uuids', async () => {
741
+ await act(async () => renderForm(null, referenceByMappingForm));
742
+
743
+ const yes = (await screen.findAllByRole('radio', {
744
+ name: 'Yes',
745
+ })) as Array<HTMLInputElement>;
746
+ const no = (await screen.findAllByRole('radio', {
747
+ name: 'No',
748
+ })) as Array<HTMLInputElement>;
749
+
750
+ await assertFormHasAllFields(screen, [
751
+ { fieldName: 'Cough', fieldType: 'radio' },
752
+ { fieldName: 'Occurrence of trauma', fieldType: 'radio' },
753
+ ]);
754
+
755
+ expect(no[0].value).toBe('3cd6f86c-26fe-102b-80cb-0017a47871b2');
756
+ expect(no[1].value).toBe('3cd6f86c-26fe-102b-80cb-0017a47871b2');
757
+ expect(yes[0].value).toBe('3cd6f600-26fe-102b-80cb-0017a47871b2');
758
+ expect(yes[1].value).toBe('3cd6f600-26fe-102b-80cb-0017a47871b2');
759
+ });
760
+ });
761
+
762
+ describe('Obs group', () => {
763
+ it('should test addition of a repeating group', async () => {
764
+ await act(async () => {
765
+ renderForm(null, obsGroupTestForm);
766
+ });
767
+ const addButton = screen.getByRole('button', { name: 'Add' });
768
+ expect(addButton).toBeInTheDocument();
769
+ expect(screen.getByRole('textbox', { name: /date of birth/i })).toBeInTheDocument();
770
+ expect(screen.getByRole('radio', { name: /^male$/i })).toBeInTheDocument();
771
+ expect(screen.getByRole('radio', { name: /female/i })).toBeInTheDocument();
772
+
773
+ await user.click(addButton);
774
+
775
+ expect(screen.getByRole('button', { name: /Remove/i })).toBeInTheDocument();
776
+ expect(screen.getAllByRole('radio', { name: /^male$/i }).length).toEqual(2);
777
+ expect(screen.getAllByRole('radio', { name: /^female$/i }).length).toEqual(2);
778
+ expect(screen.getAllByRole('textbox', { name: /date of birth/i }).length).toEqual(2);
779
+ });
780
+
781
+ it('should test deletion of a group', async () => {
782
+ await act(async () => {
783
+ renderForm(null, obsGroupTestForm);
784
+ });
785
+
786
+ const addButton = screen.getByRole('button', { name: 'Add' });
787
+ expect(addButton).toBeInTheDocument();
788
+ expect(screen.queryByRole('textbox', { name: /date of birth/i })).toBeInTheDocument();
789
+ expect(screen.getByRole('radio', { name: /^male$/i })).toBeInTheDocument();
790
+ expect(screen.getByRole('radio', { name: /female/i })).toBeInTheDocument();
791
+
792
+ const groups = screen.getAllByRole('group', { name: /my group/i });
793
+ expect(groups.length).toBe(1);
794
+
795
+ await user.click(addButton);
796
+
797
+ const removeGroupButton = screen.getByRole('button', { name: /Remove/i });
798
+ expect(removeGroupButton).toBeInTheDocument();
799
+
800
+ await user.click(removeGroupButton);
801
+
802
+ expect(removeGroupButton).not.toBeInTheDocument();
803
+ });
804
+ });
805
+
806
+ function renderForm(formUUID, formJson, intent?: string) {
807
+ render(
808
+ <FormEngine
809
+ formJson={formJson}
810
+ formUUID={formUUID}
811
+ patientUUID={patientUUID}
812
+ formSessionIntent={intent}
813
+ visit={visit}
814
+ />,
815
+ );
816
+ }
817
+ });