@olaboot/esm-patient-registration-app 9.2.0 → 10.0.2

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 (346) hide show
  1. package/dist/1339.js +1 -0
  2. package/dist/1339.js.map +1 -0
  3. package/dist/1480.js +1 -0
  4. package/dist/1480.js.map +1 -0
  5. package/dist/1646.js +1 -0
  6. package/dist/1646.js.map +1 -0
  7. package/dist/1789.js +1 -0
  8. package/dist/1789.js.map +1 -0
  9. package/dist/1869.js +1 -0
  10. package/dist/1869.js.map +1 -0
  11. package/dist/1877.js +1 -0
  12. package/dist/1877.js.map +1 -0
  13. package/dist/2317.js +1 -0
  14. package/dist/2317.js.map +1 -0
  15. package/dist/2416.js +1 -0
  16. package/dist/2416.js.map +1 -0
  17. package/dist/2747.js +1 -0
  18. package/dist/2747.js.map +1 -0
  19. package/dist/282.js +1 -0
  20. package/dist/282.js.map +1 -0
  21. package/dist/2881.js +1 -0
  22. package/dist/2881.js.map +1 -0
  23. package/dist/3378.js +1 -0
  24. package/dist/3378.js.map +1 -0
  25. package/dist/3720.js +1 -0
  26. package/dist/3720.js.map +1 -0
  27. package/dist/3906.js +1 -0
  28. package/dist/3906.js.map +1 -0
  29. package/dist/3963.js +1 -0
  30. package/dist/3963.js.map +1 -0
  31. package/dist/3989.js +1 -0
  32. package/dist/3989.js.map +1 -0
  33. package/dist/4106.js +1 -0
  34. package/dist/4106.js.map +1 -0
  35. package/dist/4111.js +1 -0
  36. package/dist/4111.js.map +1 -0
  37. package/dist/434.js +1 -0
  38. package/dist/434.js.map +1 -0
  39. package/dist/4348.js +1 -0
  40. package/dist/4348.js.map +1 -0
  41. package/dist/4383.js +1 -0
  42. package/dist/4383.js.map +1 -0
  43. package/dist/4658.js +1 -0
  44. package/dist/4658.js.map +1 -0
  45. package/dist/466.js +1 -0
  46. package/dist/466.js.map +1 -0
  47. package/dist/4928.js +1 -0
  48. package/dist/4928.js.map +1 -0
  49. package/dist/5117.js +1 -0
  50. package/dist/5117.js.map +1 -0
  51. package/dist/5132.js +1 -0
  52. package/dist/5132.js.map +1 -0
  53. package/dist/5145.js +1 -0
  54. package/dist/5145.js.map +1 -0
  55. package/dist/5208.js +43 -0
  56. package/dist/5208.js.map +1 -0
  57. package/dist/527.js +1 -0
  58. package/dist/527.js.map +1 -0
  59. package/dist/5280.js +1 -0
  60. package/dist/5280.js.map +1 -0
  61. package/dist/5338.js +6 -0
  62. package/dist/5338.js.map +1 -0
  63. package/dist/5503.js +1 -0
  64. package/dist/5503.js.map +1 -0
  65. package/dist/555.js +1 -0
  66. package/dist/555.js.map +1 -0
  67. package/dist/556.js +1 -0
  68. package/dist/556.js.map +1 -0
  69. package/dist/5644.js +1 -0
  70. package/dist/5644.js.map +1 -0
  71. package/dist/5697.js +1 -0
  72. package/dist/{4024.js.map → 5697.js.map} +1 -1
  73. package/dist/5940.js +1 -0
  74. package/dist/5940.js.map +1 -0
  75. package/dist/6047.js +1 -0
  76. package/dist/6047.js.map +1 -0
  77. package/dist/6371.js +1 -0
  78. package/dist/6371.js.map +1 -0
  79. package/dist/6377.js +1 -0
  80. package/dist/6377.js.map +1 -0
  81. package/dist/6388.js +1 -0
  82. package/dist/6388.js.map +1 -0
  83. package/dist/6444.js +1 -0
  84. package/dist/6444.js.map +1 -0
  85. package/dist/6508.js +1 -0
  86. package/dist/6508.js.map +1 -0
  87. package/dist/6724.js +1 -0
  88. package/dist/6724.js.map +1 -0
  89. package/dist/689.js +1 -0
  90. package/dist/689.js.map +1 -0
  91. package/dist/6904.js +1 -0
  92. package/dist/6904.js.map +1 -0
  93. package/dist/7045.js +1 -0
  94. package/dist/7045.js.map +1 -0
  95. package/dist/7175.js +1 -0
  96. package/dist/7175.js.map +1 -0
  97. package/dist/7182.js +1 -0
  98. package/dist/7182.js.map +1 -0
  99. package/dist/7649.js +1 -0
  100. package/dist/7649.js.map +1 -0
  101. package/dist/7742.js +1 -0
  102. package/dist/7742.js.map +1 -0
  103. package/dist/7912.js +1 -0
  104. package/dist/7912.js.map +1 -0
  105. package/dist/8358.js +1 -0
  106. package/dist/8358.js.map +1 -0
  107. package/dist/8359.js +1 -0
  108. package/dist/8359.js.map +1 -0
  109. package/dist/8695.js +1 -0
  110. package/dist/8695.js.map +1 -0
  111. package/dist/903.js +1 -0
  112. package/dist/903.js.map +1 -0
  113. package/dist/9061.js +1 -0
  114. package/dist/9061.js.map +1 -0
  115. package/dist/9072.js +1 -0
  116. package/dist/9072.js.map +1 -0
  117. package/dist/9397.js +1 -0
  118. package/dist/9397.js.map +1 -0
  119. package/dist/9712.js +1 -0
  120. package/dist/9712.js.map +1 -0
  121. package/dist/9771.js +1 -0
  122. package/dist/9771.js.map +1 -0
  123. package/dist/9806.js +1 -0
  124. package/dist/9806.js.map +1 -0
  125. package/dist/9816.js +1 -0
  126. package/dist/9816.js.map +1 -0
  127. package/dist/main.js +7 -6
  128. package/dist/main.js.map +1 -1
  129. package/dist/openmrs-esm-patient-registration-app.js +6 -0
  130. package/dist/{olaboot-esm-patient-registration-app.js.buildmanifest.json → openmrs-esm-patient-registration-app.js.buildmanifest.json} +540 -455
  131. package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
  132. package/dist/routes.json +1 -1
  133. package/package.json +8 -9
  134. package/src/add-patient-link.extension.tsx +3 -2
  135. package/src/add-patient-link.test.tsx +2 -1
  136. package/src/config-schema.ts +1 -1
  137. package/src/index.ts +2 -24
  138. package/src/nav-link.test.tsx +1 -0
  139. package/src/offline.resources.ts +97 -31
  140. package/src/patient-registration/before-save-prompt.test.tsx +199 -0
  141. package/src/patient-registration/field/__mocks__/field.resource.ts +8 -7
  142. package/src/patient-registration/field/address/address-field.component.tsx +10 -13
  143. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +6 -1
  144. package/src/patient-registration/field/address/address-hierarchy.test.tsx +191 -198
  145. package/src/patient-registration/field/address/address-search.component.tsx +20 -8
  146. package/src/patient-registration/field/address/address-search.scss +19 -2
  147. package/src/patient-registration/field/address/address-search.test.tsx +249 -57
  148. package/src/patient-registration/field/address/custom-address-field.component.tsx +1 -1
  149. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +1 -1
  150. package/src/patient-registration/field/cause-of-death/cause-of-death.test.tsx +251 -0
  151. package/src/patient-registration/field/custom-field.component.tsx +1 -1
  152. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +1 -1
  153. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.test.tsx +144 -0
  154. package/src/patient-registration/field/dob/dob.component.tsx +2 -2
  155. package/src/patient-registration/field/dob/dob.test.tsx +370 -54
  156. package/src/patient-registration/field/field.component.tsx +1 -1
  157. package/src/patient-registration/field/field.resource.ts +2 -2
  158. package/src/patient-registration/field/field.test.tsx +25 -22
  159. package/src/patient-registration/field/gender/gender-field.test.tsx +240 -54
  160. package/src/patient-registration/field/id/id-field.component.tsx +15 -5
  161. package/src/patient-registration/field/id/id-field.test.tsx +103 -47
  162. package/src/patient-registration/field/id/identifier-selection-overlay.test.tsx +346 -0
  163. package/src/patient-registration/field/name/name-field.component.tsx +2 -2
  164. package/src/patient-registration/field/name/name-field.test.tsx +282 -0
  165. package/src/patient-registration/field/obs/obs-field.test.tsx +294 -118
  166. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +172 -108
  167. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +3 -3
  168. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +2 -5
  169. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +2 -2
  170. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +249 -131
  171. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +1 -1
  172. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +98 -70
  173. package/src/patient-registration/field/phone/phone-field.test.tsx +100 -0
  174. package/src/patient-registration/form-manager.test.ts +6 -5
  175. package/src/patient-registration/form-manager.ts +5 -2
  176. package/src/patient-registration/input/basic-input/input/input.component.tsx +3 -121
  177. package/src/patient-registration/input/basic-input/input/input.test.tsx +151 -51
  178. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +113 -33
  179. package/src/patient-registration/input/combo-input/combo-input.component.tsx +60 -24
  180. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +10 -101
  181. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +144 -108
  182. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +241 -177
  183. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +47 -8
  184. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +12 -12
  185. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +52 -20
  186. package/src/patient-registration/input/input.scss +1 -2
  187. package/src/patient-registration/patient-registration-context.ts +5 -3
  188. package/src/patient-registration/patient-registration-hooks.ts +4 -12
  189. package/src/patient-registration/patient-registration-utils.test.ts +2 -1
  190. package/src/patient-registration/patient-registration-utils.ts +2 -98
  191. package/src/patient-registration/patient-registration.component.tsx +50 -46
  192. package/src/patient-registration/patient-registration.resource.test.tsx +4 -7
  193. package/src/patient-registration/patient-registration.resource.ts +1 -4
  194. package/src/patient-registration/patient-registration.scss +16 -3
  195. package/src/patient-registration/patient-registration.test.tsx +99 -65
  196. package/src/patient-registration/patient-registration.types.ts +17 -28
  197. package/src/patient-registration/section/death-info/death-info-section.test.tsx +130 -34
  198. package/src/patient-registration/section/demographics/demographics-section.test.tsx +122 -68
  199. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +15 -15
  200. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +278 -84
  201. package/src/patient-registration/section/section-wrapper.component.tsx +1 -1
  202. package/src/patient-registration/ui-components/overlay/overlay.test.tsx +104 -0
  203. package/src/patient-registration/validation/patient-registration-validation.test.ts +2 -1
  204. package/src/patient-registration/validation/patient-registration-validation.ts +9 -3
  205. package/src/root.component.tsx +2 -5
  206. package/src/widgets/cancel-patient-edit.test.tsx +48 -11
  207. package/src/widgets/delete-identifier-confirmation.test.tsx +77 -24
  208. package/src/widgets/edit-patient-details-button.component.tsx +14 -18
  209. package/src/widgets/edit-patient-details-button.scss +2 -2
  210. package/src/widgets/edit-patient-details-button.test.tsx +11 -13
  211. package/translations/am.json +9 -4
  212. package/translations/ar.json +9 -4
  213. package/translations/ar_SY.json +9 -4
  214. package/translations/bn.json +9 -4
  215. package/translations/cs.json +9 -4
  216. package/translations/de.json +120 -115
  217. package/translations/en.json +9 -4
  218. package/translations/en_US.json +9 -4
  219. package/translations/es.json +9 -4
  220. package/translations/es_MX.json +9 -4
  221. package/translations/fr.json +9 -4
  222. package/translations/he.json +9 -4
  223. package/translations/hi.json +9 -4
  224. package/translations/hi_IN.json +9 -4
  225. package/translations/id.json +9 -4
  226. package/translations/it.json +9 -4
  227. package/translations/ka.json +9 -4
  228. package/translations/km.json +9 -4
  229. package/translations/ku.json +9 -4
  230. package/translations/ky.json +9 -4
  231. package/translations/lg.json +9 -4
  232. package/translations/ne.json +9 -4
  233. package/translations/pl.json +9 -4
  234. package/translations/pt.json +9 -4
  235. package/translations/pt_BR.json +10 -5
  236. package/translations/qu.json +9 -4
  237. package/translations/ro_RO.json +9 -4
  238. package/translations/ru_RU.json +9 -4
  239. package/translations/si.json +9 -4
  240. package/translations/sq.json +9 -4
  241. package/translations/sw.json +9 -4
  242. package/translations/sw_KE.json +9 -4
  243. package/translations/tr.json +9 -4
  244. package/translations/tr_TR.json +9 -4
  245. package/translations/uk.json +9 -4
  246. package/translations/uz.json +9 -4
  247. package/translations/uz@Latn.json +9 -4
  248. package/translations/uz_UZ.json +9 -4
  249. package/translations/vi.json +9 -4
  250. package/translations/zh.json +50 -45
  251. package/translations/zh_CN.json +9 -4
  252. package/translations/zh_TW.json +9 -4
  253. package/vitest.config.ts +4 -0
  254. package/ADDRESS_CONFIGURATION.md +0 -152
  255. package/IDENTIFIER_CONFIGURATION.md +0 -142
  256. package/IMPLEMENTATION_SUMMARY.md +0 -111
  257. package/QUICK_START.md +0 -95
  258. package/address-required-fields-config.json +0 -26
  259. package/dist/126.js +0 -1
  260. package/dist/15.js +0 -1
  261. package/dist/1564.js +0 -1
  262. package/dist/1567.js +0 -1
  263. package/dist/1845.js +0 -1
  264. package/dist/1953.js +0 -1
  265. package/dist/200.js +0 -1
  266. package/dist/200.js.map +0 -1
  267. package/dist/215.js +0 -1
  268. package/dist/2178.js +0 -1
  269. package/dist/250.js +0 -1
  270. package/dist/250.js.map +0 -1
  271. package/dist/2523.js +0 -1
  272. package/dist/2523.js.map +0 -1
  273. package/dist/2566.js +0 -1
  274. package/dist/2586.js +0 -1
  275. package/dist/2586.js.map +0 -1
  276. package/dist/2716.js +0 -1
  277. package/dist/2716.js.map +0 -1
  278. package/dist/2759.js +0 -1
  279. package/dist/2821.js +0 -6
  280. package/dist/2821.js.map +0 -1
  281. package/dist/3089.js +0 -1
  282. package/dist/3089.js.map +0 -1
  283. package/dist/3230.js +0 -1
  284. package/dist/3441.js +0 -1
  285. package/dist/3565.js +0 -1
  286. package/dist/3571.js +0 -1
  287. package/dist/3571.js.map +0 -1
  288. package/dist/3746.js +0 -1
  289. package/dist/3925.js +0 -1
  290. package/dist/3946.js +0 -1
  291. package/dist/4024.js +0 -1
  292. package/dist/4744.js +0 -1
  293. package/dist/4744.js.map +0 -1
  294. package/dist/4809.js +0 -1
  295. package/dist/4894.js +0 -1
  296. package/dist/4970.js +0 -1
  297. package/dist/4970.js.map +0 -1
  298. package/dist/5130.js +0 -1
  299. package/dist/5187.js +0 -1
  300. package/dist/5491.js +0 -1
  301. package/dist/5491.js.map +0 -1
  302. package/dist/5595.js +0 -1
  303. package/dist/5961.js +0 -1
  304. package/dist/6133.js +0 -1
  305. package/dist/634.js +0 -1
  306. package/dist/634.js.map +0 -1
  307. package/dist/6456.js +0 -1
  308. package/dist/6466.js +0 -1
  309. package/dist/6613.js +0 -1
  310. package/dist/6783.js +0 -1
  311. package/dist/7073.js +0 -38
  312. package/dist/7073.js.map +0 -1
  313. package/dist/7154.js +0 -1
  314. package/dist/7154.js.map +0 -1
  315. package/dist/7348.js +0 -1
  316. package/dist/7439.js +0 -1
  317. package/dist/7439.js.map +0 -1
  318. package/dist/7543.js +0 -1
  319. package/dist/7607.js +0 -1
  320. package/dist/772.js +0 -1
  321. package/dist/7984.js +0 -1
  322. package/dist/7984.js.map +0 -1
  323. package/dist/8538.js +0 -1
  324. package/dist/8538.js.map +0 -1
  325. package/dist/8599.js +0 -1
  326. package/dist/8727.js +0 -1
  327. package/dist/8847.js +0 -1
  328. package/dist/9015.js +0 -1
  329. package/dist/906.js +0 -1
  330. package/dist/9065.js +0 -1
  331. package/dist/9182.js +0 -1
  332. package/dist/9339.js +0 -1
  333. package/dist/9453.js +0 -1
  334. package/dist/9833.js +0 -1
  335. package/dist/9833.js.map +0 -1
  336. package/dist/9856.js +0 -1
  337. package/dist/9856.js.map +0 -1
  338. package/dist/9920.js +0 -1
  339. package/dist/9938.js +0 -1
  340. package/dist/9943.js +0 -1
  341. package/dist/9943.js.map +0 -1
  342. package/dist/olaboot-esm-patient-registration-app.js +0 -5
  343. package/dist/olaboot-esm-patient-registration-app.js.map +0 -1
  344. package/example-config.json +0 -14
  345. package/jest.config.js +0 -3
  346. package/src/resource.ts +0 -12
@@ -1,23 +1,27 @@
1
1
  import React from 'react';
2
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
3
+ import userEvent from '@testing-library/user-event';
2
4
  import { Form, Formik } from 'formik';
3
- import { render, screen } from '@testing-library/react';
5
+ import { render, screen, waitFor } from '@testing-library/react';
4
6
  import { type FieldDefinition } from '../../../config-schema';
5
7
  import { usePersonAttributeType } from './person-attributes.resource';
6
8
  import { useConceptAnswers } from '../field.resource';
7
9
  import { PersonAttributeField } from './person-attribute-field.component';
10
+ import { initialFormValues } from '../../patient-registration.component';
11
+ import { type FormValues } from '../../patient-registration.types';
8
12
 
9
- jest.mock('./person-attributes.resource', () => ({
10
- ...jest.requireActual('./person-attributes.resource'),
11
- usePersonAttributeType: jest.fn(),
13
+ vi.mock('./person-attributes.resource', async () => ({
14
+ ...((await vi.importActual('./person-attributes.resource')) as object),
15
+ usePersonAttributeType: vi.fn(),
12
16
  }));
13
17
 
14
- jest.mock('../field.resource', () => ({
15
- ...jest.requireActual('../field.resource'),
16
- useConceptAnswers: jest.fn(),
18
+ vi.mock('../field.resource', async () => ({
19
+ ...((await vi.importActual('../field.resource')) as object),
20
+ useConceptAnswers: vi.fn(),
17
21
  }));
18
22
 
19
- const mockUsePersonAttributeType = jest.mocked(usePersonAttributeType);
20
- const mockUseConceptAnswers = jest.mocked(useConceptAnswers);
23
+ const mockUsePersonAttributeType = vi.mocked(usePersonAttributeType);
24
+ const mockUseConceptAnswers = vi.mocked(useConceptAnswers);
21
25
 
22
26
  const mockPersonAttributeType = {
23
27
  format: 'java.lang.String',
@@ -27,17 +31,38 @@ const mockPersonAttributeType = {
27
31
  description: 'The person who referred the patient',
28
32
  };
29
33
 
30
- let fieldDefinition: FieldDefinition = {
31
- id: 'referredby',
32
- label: 'Referred by',
33
- type: 'person attribute',
34
- uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
35
- answerConceptSetUuid: '6682d17f-0777-45e4-a39b-93f77eb3531c',
36
- validation: {
37
- matches: '',
38
- required: true,
39
- },
40
- showHeading: true,
34
+ /**
35
+ * Helper to render PersonAttributeField with Formik render props for state-dependent tests.
36
+ */
37
+ const renderPersonAttributeFieldWithFormik = (
38
+ fieldDefinition: FieldDefinition,
39
+ initialValues: Partial<FormValues> = {},
40
+ options?: { enableReinitialize?: boolean },
41
+ ) => {
42
+ const defaultValues = {
43
+ attributes: {},
44
+ ...initialValues,
45
+ };
46
+
47
+ let formValuesRef: FormValues = { ...initialFormValues, ...defaultValues } as FormValues;
48
+
49
+ const utils = render(
50
+ <Formik initialValues={defaultValues} onSubmit={() => {}} enableReinitialize={options?.enableReinitialize}>
51
+ {({ setFieldValue, values, setFieldTouched }) => {
52
+ formValuesRef = { ...initialFormValues, ...values } as FormValues;
53
+ return (
54
+ <Form>
55
+ <PersonAttributeField fieldDefinition={fieldDefinition} />
56
+ </Form>
57
+ );
58
+ }}
59
+ </Formik>,
60
+ );
61
+
62
+ return {
63
+ ...utils,
64
+ getFormValues: () => formValuesRef,
65
+ };
41
66
  };
42
67
 
43
68
  describe('PersonAttributeField', () => {
@@ -49,145 +74,238 @@ describe('PersonAttributeField', () => {
49
74
  });
50
75
  });
51
76
 
52
- it('renders the text input field for String format', () => {
53
- render(
54
- <Formik initialValues={{}} onSubmit={() => {}}>
55
- <Form>
56
- <PersonAttributeField fieldDefinition={fieldDefinition} />
57
- </Form>
58
- </Formik>,
59
- );
60
-
61
- const input = screen.getByLabelText(/Referred by/i) as HTMLInputElement;
62
- expect(screen.getByRole('heading')).toBeInTheDocument();
63
- expect(input).toBeInTheDocument();
64
- expect(input.type).toBe('text');
77
+ describe('Loading and error states', () => {
78
+ it('renders a skeleton if attribute type is loading', async () => {
79
+ mockUsePersonAttributeType.mockReturnValue({
80
+ data: null,
81
+ isLoading: true,
82
+ error: null,
83
+ });
84
+
85
+ const fieldDefinition: FieldDefinition = {
86
+ id: 'referredBy',
87
+ uuid: 'attribute-uuid',
88
+ label: 'Attribute',
89
+ showHeading: true,
90
+ type: 'person attribute',
91
+ };
92
+
93
+ renderPersonAttributeFieldWithFormik(fieldDefinition);
94
+
95
+ await screen.findByRole('heading', { name: /attribute/i });
96
+ expect(screen.queryByLabelText(/referred by/i)).not.toBeInTheDocument();
97
+ });
98
+
99
+ it('renders an error notification if unable to fetch attribute type', () => {
100
+ mockUsePersonAttributeType.mockReturnValue({
101
+ data: null,
102
+ isLoading: false,
103
+ error: new Error('Failed to fetch attribute type'),
104
+ });
105
+
106
+ const fieldDefinition: FieldDefinition = {
107
+ id: 'referredBy',
108
+ uuid: 'attribute-uuid',
109
+ label: 'Attribute',
110
+ showHeading: false,
111
+ type: 'person attribute',
112
+ };
113
+
114
+ renderPersonAttributeFieldWithFormik(fieldDefinition);
115
+
116
+ // Check for the specific error message text
117
+ expect(screen.getByText(/Unable to fetch person attribute type/i)).toBeInTheDocument();
118
+ });
119
+
120
+ it('renders an error notification if attribute type has unknown format', () => {
121
+ mockUsePersonAttributeType.mockReturnValue({
122
+ data: { ...mockPersonAttributeType, format: 'unknown' },
123
+ isLoading: false,
124
+ error: null,
125
+ });
126
+
127
+ const fieldDefinition: FieldDefinition = {
128
+ id: 'referredby',
129
+ label: 'Referred by',
130
+ type: 'person attribute',
131
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
132
+ validation: {
133
+ matches: '',
134
+ required: true,
135
+ },
136
+ showHeading: true,
137
+ };
138
+
139
+ renderPersonAttributeFieldWithFormik(fieldDefinition);
140
+
141
+ // Check for the specific error message text
142
+ expect(screen.getByText(/Patient attribute type has unknown format/i)).toBeInTheDocument();
143
+ expect(screen.getByText(/unknown/i)).toBeInTheDocument();
144
+ });
65
145
  });
66
146
 
67
- it('should not show heading if showHeading is false', () => {
68
- fieldDefinition = {
69
- ...fieldDefinition,
70
- showHeading: false,
71
- };
147
+ describe('Heading display', () => {
148
+ it('shows heading when showHeading is true', () => {
149
+ const fieldDefinition: FieldDefinition = {
150
+ id: 'referredby',
151
+ label: 'Referred by',
152
+ type: 'person attribute',
153
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
154
+ validation: {
155
+ matches: '',
156
+ required: true,
157
+ },
158
+ showHeading: true,
159
+ };
160
+
161
+ renderPersonAttributeFieldWithFormik(fieldDefinition);
72
162
 
73
- render(
74
- <Formik initialValues={{}} onSubmit={() => {}}>
75
- <Form>
76
- <PersonAttributeField fieldDefinition={fieldDefinition} />
77
- </Form>
78
- </Formik>,
79
- );
163
+ expect(screen.getByRole('heading', { name: /referred by/i })).toBeInTheDocument();
164
+ });
165
+
166
+ it('does not show heading when showHeading is false', () => {
167
+ const fieldDefinition: FieldDefinition = {
168
+ id: 'referredby',
169
+ label: 'Referred by',
170
+ type: 'person attribute',
171
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
172
+ validation: {
173
+ matches: '',
174
+ required: true,
175
+ },
176
+ showHeading: false,
177
+ };
80
178
 
81
- expect(screen.queryByRole('heading')).not.toBeInTheDocument();
179
+ renderPersonAttributeFieldWithFormik(fieldDefinition);
180
+
181
+ expect(screen.queryByRole('heading')).not.toBeInTheDocument();
182
+ });
82
183
  });
83
184
 
84
- it('renders the coded attribute field for Concept format', () => {
85
- fieldDefinition = {
185
+ describe('String format (text input)', () => {
186
+ const textFieldDefinition: FieldDefinition = {
86
187
  id: 'referredby',
87
- ...fieldDefinition,
88
188
  label: 'Referred by',
189
+ type: 'person attribute',
190
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
191
+ validation: {
192
+ matches: '',
193
+ required: false,
194
+ },
195
+ showHeading: true,
89
196
  };
90
197
 
91
- mockUsePersonAttributeType.mockReturnValue({
92
- data: { ...mockPersonAttributeType, format: 'org.openmrs.Concept' },
93
- isLoading: false,
94
- error: null,
95
- });
198
+ it('renders a text input field for String format', () => {
199
+ renderPersonAttributeFieldWithFormik(textFieldDefinition);
96
200
 
97
- mockUseConceptAnswers.mockReturnValueOnce({
98
- data: [
99
- { uuid: '1', display: 'Option 1' },
100
- { uuid: '2', display: 'Option 2' },
101
- ],
102
- error: null,
103
- isLoading: false,
201
+ const input = screen.getByRole('textbox', { name: /referred by/i });
202
+ expect(input).toBeInTheDocument();
104
203
  });
105
204
 
106
- render(
107
- <Formik initialValues={{}} onSubmit={() => {}}>
108
- <Form>
109
- <PersonAttributeField fieldDefinition={fieldDefinition} />
110
- </Form>
111
- </Formik>,
112
- );
113
-
114
- const input = screen.getByLabelText(/Referred by/i) as HTMLInputElement;
115
- expect(input).toBeInTheDocument();
116
- expect(input.type).toBe('select-one');
117
- expect(screen.getByText('Option 1')).toBeInTheDocument();
118
- expect(screen.getByText('Option 2')).toBeInTheDocument();
119
- });
205
+ it('allows user to enter text', async () => {
206
+ const user = userEvent.setup();
207
+ const { getFormValues } = renderPersonAttributeFieldWithFormik(textFieldDefinition);
120
208
 
121
- it('renders an error notification if attribute type has unknown format', () => {
122
- mockUsePersonAttributeType.mockReturnValue({
123
- data: { ...mockPersonAttributeType, format: 'unknown' },
124
- isLoading: false,
125
- error: null,
209
+ const input = screen.getByRole('textbox', { name: /referred by/i }) as HTMLInputElement;
210
+ await user.type(input, 'Dr. Smith');
211
+
212
+ await waitFor(() => {
213
+ expect(getFormValues().attributes['4dd56a75-14ab-4148-8700-1f4f704dc5b0']).toBe('Dr. Smith');
214
+ });
126
215
  });
127
216
 
128
- render(
129
- <Formik initialValues={{}} onSubmit={() => {}}>
130
- <Form>
131
- <PersonAttributeField fieldDefinition={fieldDefinition} />
132
- </Form>
133
- </Formik>,
134
- );
217
+ it('renders as required when configured', () => {
218
+ renderPersonAttributeFieldWithFormik({
219
+ ...textFieldDefinition,
220
+ validation: {
221
+ matches: '',
222
+ required: true,
223
+ },
224
+ });
135
225
 
136
- expect(screen.getByText('Error')).toBeInTheDocument();
137
- expect(screen.getByText(/Patient attribute type has unknown format/i)).toBeInTheDocument();
138
- });
226
+ const input = screen.getByRole('textbox', { name: /referred by/i });
227
+ expect(input).toBeRequired();
228
+ });
139
229
 
140
- it('renders an error notification if unable to fetch attribute type', () => {
141
- mockUsePersonAttributeType.mockReturnValue({
142
- data: null,
143
- isLoading: false,
144
- error: new Error('Failed to fetch attribute type'),
230
+ it('renders as optional when not required', () => {
231
+ renderPersonAttributeFieldWithFormik(textFieldDefinition);
232
+
233
+ const input = screen.getByRole('textbox', { name: /referred by/i });
234
+ expect(input).not.toBeRequired();
145
235
  });
236
+ });
146
237
 
147
- fieldDefinition = {
148
- id: 'referredBy',
149
- uuid: 'attribute-uuid',
150
- label: 'Attribute',
151
- showHeading: false,
238
+ describe('Concept format (coded select)', () => {
239
+ const codedFieldDefinition: FieldDefinition = {
240
+ id: 'referredby',
241
+ label: 'Referred by',
152
242
  type: 'person attribute',
243
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
244
+ answerConceptSetUuid: '6682d17f-0777-45e4-a39b-93f77eb3531c',
245
+ validation: {
246
+ matches: '',
247
+ required: false,
248
+ },
249
+ showHeading: true,
153
250
  };
154
251
 
155
- render(
156
- <Formik initialValues={{}} onSubmit={() => {}}>
157
- <Form>
158
- <PersonAttributeField fieldDefinition={fieldDefinition} />
159
- </Form>
160
- </Formik>,
161
- );
252
+ beforeEach(() => {
253
+ mockUsePersonAttributeType.mockReturnValue({
254
+ data: { ...mockPersonAttributeType, format: 'org.openmrs.Concept' },
255
+ isLoading: false,
256
+ error: null,
257
+ });
162
258
 
163
- expect(screen.getByText('Error')).toBeInTheDocument();
164
- expect(screen.getByText(/Unable to fetch person attribute type/i)).toBeInTheDocument();
165
- });
259
+ mockUseConceptAnswers.mockReturnValue({
260
+ data: [
261
+ { uuid: '1', display: 'Option 1' },
262
+ { uuid: '2', display: 'Option 2' },
263
+ ],
264
+ error: null,
265
+ isLoading: false,
266
+ });
267
+ });
166
268
 
167
- it('renders a skeleton if attribute type is loading', async () => {
168
- mockUsePersonAttributeType.mockReturnValue({
169
- data: null,
170
- isLoading: true,
171
- error: null,
269
+ it('renders a select field for Concept format', () => {
270
+ renderPersonAttributeFieldWithFormik(codedFieldDefinition);
271
+
272
+ const select = screen.getByRole('combobox', { name: /referred by/i });
273
+ expect(select).toBeInTheDocument();
274
+ expect(screen.getByText('Option 1')).toBeInTheDocument();
275
+ expect(screen.getByText('Option 2')).toBeInTheDocument();
172
276
  });
173
277
 
174
- fieldDefinition = {
175
- id: 'referredBy',
176
- uuid: 'attribute-uuid',
177
- label: 'Attribute',
178
- showHeading: true,
179
- type: 'person attribute',
180
- };
278
+ it('allows user to select an option', async () => {
279
+ const user = userEvent.setup();
280
+ const { getFormValues } = renderPersonAttributeFieldWithFormik(codedFieldDefinition);
181
281
 
182
- render(
183
- <Formik initialValues={{}} onSubmit={() => {}}>
184
- <Form>
185
- <PersonAttributeField fieldDefinition={fieldDefinition} />
186
- </Form>
187
- </Formik>,
188
- );
282
+ const select = screen.getByRole('combobox', { name: /referred by/i }) as HTMLSelectElement;
283
+ await user.selectOptions(select, '1');
189
284
 
190
- await screen.findByRole('heading', { name: /attribute/i });
191
- expect(screen.queryByLabelText(/referred by/i)).not.toBeInTheDocument();
285
+ await waitFor(() => {
286
+ expect(getFormValues().attributes['4dd56a75-14ab-4148-8700-1f4f704dc5b0']).toBe('1');
287
+ });
288
+ expect(select.value).toBe('1');
289
+ });
290
+
291
+ it('renders as required when configured', () => {
292
+ renderPersonAttributeFieldWithFormik({
293
+ ...codedFieldDefinition,
294
+ validation: {
295
+ matches: '',
296
+ required: true,
297
+ },
298
+ });
299
+
300
+ const select = screen.getByRole('combobox', { name: /referred by/i });
301
+ expect(select).toBeRequired();
302
+ });
303
+
304
+ it('renders as optional when not required', () => {
305
+ renderPersonAttributeFieldWithFormik(codedFieldDefinition);
306
+
307
+ const select = screen.getByRole('combobox', { name: /referred by/i });
308
+ expect(select).not.toBeRequired();
309
+ });
192
310
  });
193
311
  });
@@ -5,7 +5,7 @@ import { type PersonAttributeTypeResponse } from '../../patient-registration.typ
5
5
  export function usePersonAttributeType(personAttributeTypeUuid: string): {
6
6
  data: PersonAttributeTypeResponse;
7
7
  isLoading: boolean;
8
- error: any;
8
+ error: Error | undefined;
9
9
  } {
10
10
  const { data, error, isLoading } = useSWRImmutable<FetchResponse<PersonAttributeTypeResponse>>(
11
11
  `${restBaseUrl}/personattributetype/${personAttributeTypeUuid}`,
@@ -1,90 +1,118 @@
1
1
  import React from 'react';
2
- import { render, screen } from '@testing-library/react';
2
+ import { vi, describe, it, expect } from 'vitest';
3
+ import { render, screen, waitFor } from '@testing-library/react';
3
4
  import userEvent from '@testing-library/user-event';
4
5
  import { Form, Formik } from 'formik';
5
6
  import { TextPersonAttributeField } from './text-person-attribute-field.component';
6
7
 
7
- describe('TextPersonAttributeField', () => {
8
- const mockPersonAttributeType = {
9
- format: 'java.lang.String',
10
- display: 'Referred by',
11
- uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
12
- description: 'Referred by',
13
- name: 'Referred by',
14
- };
8
+ const mockPersonAttributeType = {
9
+ format: 'java.lang.String',
10
+ display: 'Referred by',
11
+ uuid: '4dd56a75-14ab-4148-8700-1f4f704dc5b0',
12
+ description: 'Referred by',
13
+ name: 'Referred by',
14
+ };
15
+
16
+ /**
17
+ * Helper to render TextPersonAttributeField with Formik for state-dependent tests.
18
+ */
19
+ const renderTextPersonAttributeField = (
20
+ props: {
21
+ id?: string;
22
+ label?: string;
23
+ validationRegex?: string;
24
+ required?: boolean;
25
+ } = {},
26
+ ) => {
27
+ const utils = render(
28
+ <Formik initialValues={{}} onSubmit={() => {}}>
29
+ <Form>
30
+ <TextPersonAttributeField
31
+ id={props.id || 'attributeId'}
32
+ personAttributeType={mockPersonAttributeType}
33
+ label={props.label}
34
+ validationRegex={props.validationRegex}
35
+ required={props.required}
36
+ />
37
+ </Form>
38
+ </Formik>,
39
+ );
40
+
41
+ return utils;
42
+ };
15
43
 
44
+ describe('TextPersonAttributeField', () => {
16
45
  it('renders the input field with a label', () => {
17
- render(
18
- <Formik initialValues={{}} onSubmit={() => {}}>
19
- <Form>
20
- <TextPersonAttributeField
21
- id="attributeId"
22
- personAttributeType={mockPersonAttributeType}
23
- label="Custom Label"
24
- />
25
- </Form>
26
- </Formik>,
27
- );
46
+ renderTextPersonAttributeField({ label: 'Custom Label' });
28
47
 
29
48
  expect(screen.getByRole('textbox', { name: /custom label \(optional\)/i })).toBeInTheDocument();
30
49
  });
31
50
 
32
51
  it('renders the input field with the default label if label prop is not provided', () => {
33
- render(
34
- <Formik initialValues={{}} onSubmit={() => {}}>
35
- <Form>
36
- <TextPersonAttributeField id="attributeId" personAttributeType={mockPersonAttributeType} />
37
- </Form>
38
- </Formik>,
39
- );
52
+ renderTextPersonAttributeField();
40
53
 
41
54
  expect(screen.getByRole('textbox', { name: /referred by \(optional\)/i })).toBeInTheDocument();
42
55
  });
43
56
 
44
- it('validates the input with the provided validationRegex', async () => {
45
- const user = userEvent.setup();
46
- const validationRegex = '^[A-Z]+$'; // Accepts only uppercase letters
47
-
48
- render(
49
- <Formik initialValues={{}} onSubmit={() => {}}>
50
- <Form>
51
- <TextPersonAttributeField
52
- id="attributeId"
53
- personAttributeType={mockPersonAttributeType}
54
- validationRegex={validationRegex}
55
- />
56
- </Form>
57
- </Formik>,
58
- );
59
-
60
- const textbox = screen.getByRole('textbox', { name: /referred by \(optional\)/i });
61
- expect(textbox).toBeInTheDocument();
62
-
63
- // Valid input: "ABC"
64
- await user.type(textbox, 'ABC');
65
- await user.tab();
66
-
67
- expect(screen.queryByText(/invalid input/i)).not.toBeInTheDocument();
68
- await user.clear(textbox);
69
-
70
- // // Invalid input: "abc" (contains lowercase letters)
71
- await user.type(textbox, 'abc');
72
- await user.tab();
73
- expect(screen.getByText(/invalid input/i)).toBeInTheDocument();
57
+ describe('Input validation', () => {
58
+ it('validates the input with the provided validationRegex', async () => {
59
+ const user = userEvent.setup();
60
+ const validationRegex = '^[A-Z]+$'; // Accepts only uppercase letters
61
+
62
+ renderTextPersonAttributeField({ validationRegex });
63
+
64
+ const textbox = screen.getByRole('textbox', { name: /referred by \(optional\)/i });
65
+ expect(textbox).toBeInTheDocument();
66
+
67
+ // Valid input: "ABC"
68
+ await user.type(textbox, 'ABC');
69
+ await user.tab();
70
+
71
+ await waitFor(() => {
72
+ expect(screen.queryByText(/invalid input/i)).not.toBeInTheDocument();
73
+ });
74
+ await user.clear(textbox);
75
+
76
+ // Invalid input: "abc" (contains lowercase letters)
77
+ await user.type(textbox, 'abc');
78
+ await user.tab();
79
+
80
+ await waitFor(() => {
81
+ expect(screen.getByText(/invalid input/i)).toBeInTheDocument();
82
+ });
83
+ });
84
+
85
+ it('does not show error for valid input matching regex', async () => {
86
+ const user = userEvent.setup();
87
+ const validationRegex = '^[A-Z]+$';
88
+
89
+ renderTextPersonAttributeField({ validationRegex });
90
+
91
+ const textbox = screen.getByRole('textbox', { name: /referred by \(optional\)/i });
92
+ await user.type(textbox, 'VALID');
93
+ await user.tab();
94
+
95
+ await waitFor(() => {
96
+ expect(screen.queryByText(/invalid input/i)).not.toBeInTheDocument();
97
+ });
98
+ });
74
99
  });
75
100
 
76
- it('renders the input field as required when required prop is true', () => {
77
- render(
78
- <Formik initialValues={{}} onSubmit={() => {}}>
79
- <Form>
80
- <TextPersonAttributeField id="attributeId" personAttributeType={mockPersonAttributeType} required />
81
- </Form>
82
- </Formik>,
83
- );
84
- const textbox = screen.getByRole('textbox', { name: /referred by/i });
85
-
86
- // Required attribute should be truthy on the input element
87
- expect(textbox).toBeInTheDocument();
88
- expect(textbox).toBeRequired();
101
+ describe('Required field', () => {
102
+ it('renders the input field as required when required prop is true', () => {
103
+ renderTextPersonAttributeField({ required: true });
104
+ const textbox = screen.getByRole('textbox', { name: /referred by/i });
105
+
106
+ expect(textbox).toBeInTheDocument();
107
+ expect(textbox).toBeRequired();
108
+ });
109
+
110
+ it('renders as optional when required prop is false', () => {
111
+ renderTextPersonAttributeField({ required: false });
112
+ const textbox = screen.getByRole('textbox', { name: /referred by \(optional\)/i });
113
+
114
+ expect(textbox).toBeInTheDocument();
115
+ expect(textbox).not.toBeRequired();
116
+ });
89
117
  });
90
118
  });