@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,7 +1,8 @@
1
1
  import React from 'react';
2
+ import { vi, describe, it, expect, test, beforeEach, type Mock } from 'vitest';
2
3
  import userEvent from '@testing-library/user-event';
3
- import { screen } from '@testing-library/react';
4
- import { Formik, Form, useFormikContext } from 'formik';
4
+ import { screen, waitFor, fireEvent } from '@testing-library/react';
5
+ import { Formik, Form } from 'formik';
5
6
  import { renderWithContext } from 'tools';
6
7
  import { type Resources } from '../../../offline.resources';
7
8
  import { PatientRegistrationContextProvider } from '../../patient-registration-context';
@@ -10,27 +11,18 @@ import { useAddressHierarchy, useOrderedAddressHierarchyLevels } from './address
10
11
  import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../../config-schema';
11
12
  import { mockedAddressTemplate, mockedAddressOptions, mockedOrderedFields } from '__mocks__';
12
13
  import { ResourcesContextProvider } from '../../../resources-context';
14
+ import { initialFormValues } from '../../patient-registration.component';
15
+ import { type FormValues } from '../../patient-registration.types';
13
16
  import AddressSearchComponent from './address-search.component';
14
17
 
15
- const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
16
- const mockUseAddressHierarchy = jest.mocked(useAddressHierarchy);
17
- const mockUseOrderedAddressHierarchyLevels = jest.mocked(useOrderedAddressHierarchyLevels);
18
- const mockUseFormikContext = useFormikContext as jest.Mock;
18
+ const mockUseConfig = vi.mocked(useConfig<RegistrationConfig>);
19
+ const mockUseAddressHierarchy = vi.mocked(useAddressHierarchy);
20
+ const mockUseOrderedAddressHierarchyLevels = vi.mocked(useOrderedAddressHierarchyLevels);
19
21
 
20
- jest.mock('./address-hierarchy.resource', () => ({
21
- ...(jest.requireActual('./address-hierarchy.resource') as jest.Mock),
22
- useAddressHierarchy: jest.fn(),
23
- useOrderedAddressHierarchyLevels: jest.fn(),
24
- }));
25
-
26
- jest.mock('../../patient-registration.resource', () => ({
27
- ...(jest.requireActual('../../../patient-registration.resource') as jest.Mock),
28
- useAddressHierarchy: jest.fn(),
29
- }));
30
-
31
- jest.mock('formik', () => ({
32
- ...(jest.requireActual('formik') as jest.Mock),
33
- useFormikContext: jest.fn(() => ({})),
22
+ vi.mock('./address-hierarchy.resource', async () => ({
23
+ ...((await vi.importActual('./address-hierarchy.resource')) as object),
24
+ useAddressHierarchy: vi.fn(),
25
+ useOrderedAddressHierarchyLevels: vi.fn(),
34
26
  }));
35
27
 
36
28
  const allFields = mockedAddressTemplate.lines
@@ -44,23 +36,55 @@ const allFields = mockedAddressTemplate.lines
44
36
  const orderMap = Object.fromEntries(mockedOrderedFields.map((field, indx) => [field, indx]));
45
37
  allFields.sort((existingField1, existingField2) => orderMap[existingField1.name] - orderMap[existingField2.name]);
46
38
 
47
- async function renderAddressHierarchy(addressTemplate = mockedAddressTemplate) {
39
+ /**
40
+ * Helper to render AddressSearchComponent with Formik for state-dependent tests.
41
+ */
42
+ async function renderAddressSearchWithFormik(
43
+ initialValues: Partial<FormValues> = {},
44
+ addressTemplate = mockedAddressTemplate,
45
+ ) {
46
+ const defaultValues = {
47
+ address: {},
48
+ ...initialValues,
49
+ };
50
+
51
+ let formValuesRef: FormValues = { ...initialFormValues, ...defaultValues } as FormValues;
48
52
  const mockResourcesContextValue = { addressTemplate } as unknown as Resources;
49
53
 
50
- await renderWithContext(
51
- <Formik initialValues={{}} onSubmit={null}>
52
- <Form>
53
- <PatientRegistrationContextProvider value={{ setFieldValue: jest.fn() } as any}>
54
- <AddressSearchComponent addressLayout={allFields} />
55
- </PatientRegistrationContextProvider>
56
- </Form>
54
+ const utils = await renderWithContext(
55
+ <Formik initialValues={defaultValues} onSubmit={() => {}}>
56
+ {({ setFieldValue, values }) => {
57
+ formValuesRef = { ...initialFormValues, ...values } as FormValues;
58
+ return (
59
+ <Form>
60
+ <PatientRegistrationContextProvider
61
+ value={{
62
+ identifierTypes: [],
63
+ values: formValuesRef,
64
+ validationSchema: null,
65
+ inEditMode: false,
66
+ setFieldValue: setFieldValue as any,
67
+ setCapturePhotoProps: vi.fn(),
68
+ setFieldTouched: vi.fn().mockResolvedValue(undefined),
69
+ currentPhoto: '',
70
+ isOffline: false,
71
+ initialFormValues: formValuesRef,
72
+ }}>
73
+ <AddressSearchComponent addressLayout={allFields} />
74
+ </PatientRegistrationContextProvider>
75
+ </Form>
76
+ );
77
+ }}
57
78
  </Formik>,
58
79
  ResourcesContextProvider,
59
80
  mockResourcesContextValue,
60
81
  );
61
- }
62
82
 
63
- const setFieldValue = jest.fn();
83
+ return {
84
+ ...utils,
85
+ getFormValues: () => formValuesRef,
86
+ };
87
+ }
64
88
 
65
89
  describe('Testing address search bar', () => {
66
90
  beforeEach(() => {
@@ -73,7 +97,6 @@ describe('Testing address search bar', () => {
73
97
  useQuickSearch: true,
74
98
  searchAddressByLevel: false,
75
99
  },
76
- requiredAddressFields: [],
77
100
  },
78
101
  } as RegistrationConfig['fieldConfigurations'],
79
102
  });
@@ -82,60 +105,229 @@ describe('Testing address search bar', () => {
82
105
  isLoadingFieldOrder: false,
83
106
  errorFetchingFieldOrder: null,
84
107
  });
85
- mockUseFormikContext.mockReturnValue({
86
- setFieldValue,
87
- });
88
108
  });
89
109
 
90
- it('should render the search bar', () => {
110
+ it('renders the search bar', async () => {
91
111
  mockUseAddressHierarchy.mockReturnValue({
92
112
  addresses: [],
93
113
  error: null,
94
114
  isLoading: false,
95
115
  });
96
116
 
97
- renderAddressHierarchy();
117
+ await renderAddressSearchWithFormik();
98
118
 
99
- const searchbox = screen.getByRole('searchbox');
119
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
100
120
  expect(searchbox).toBeInTheDocument();
101
121
 
102
122
  const ul = screen.queryByRole('list');
103
123
  expect(ul).not.toBeInTheDocument();
104
124
  });
105
125
 
106
- it("should render only the results for the search term matched address' parents", async () => {
126
+ it('displays search results when user types a search term', async () => {
107
127
  const user = userEvent.setup();
128
+ mockUseAddressHierarchy.mockReturnValue({
129
+ addresses: mockedAddressOptions,
130
+ error: null,
131
+ isLoading: false,
132
+ });
108
133
 
134
+ await renderAddressSearchWithFormik();
135
+
136
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
137
+ await user.type(searchbox, 'nea');
138
+
139
+ await waitFor(() => {
140
+ expect(screen.getByRole('list')).toBeInTheDocument();
141
+ });
142
+
143
+ await waitFor(() => {
144
+ expect(screen.getAllByRole('listitem').length).toBeGreaterThan(0);
145
+ });
146
+ });
147
+
148
+ it('updates form values when user selects an address option', async () => {
149
+ const user = userEvent.setup();
109
150
  mockUseAddressHierarchy.mockReturnValue({
110
151
  addresses: mockedAddressOptions,
111
152
  error: null,
112
153
  isLoading: false,
113
154
  });
114
155
 
115
- renderAddressHierarchy();
156
+ const { getFormValues } = await renderAddressSearchWithFormik();
116
157
 
117
- const searchString = 'nea';
118
- const separator = ' > ';
119
- const options: Set<string> = new Set();
158
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
159
+ await user.type(searchbox, 'nea');
120
160
 
121
- mockedAddressOptions.forEach((address) => {
122
- const values = address.split(separator);
123
- values.forEach((val, index) => {
124
- if (val.toLowerCase().includes(searchString.toLowerCase())) {
125
- options.add(values.slice(0, index + 1).join(separator));
126
- }
127
- });
161
+ // Wait for results to appear
162
+ await waitFor(() => {
163
+ expect(screen.getByRole('list')).toBeInTheDocument();
128
164
  });
129
165
 
130
- const addressOptions = [...options];
131
- addressOptions.forEach(async (address) => {
132
- const optionElement = screen.getByText(address);
133
- expect(optionElement).toBeInTheDocument();
134
- await user.click(optionElement);
135
- const values = address.split(separator);
136
- allFields.map(({ name }, index) => {
137
- expect(setFieldValue).toHaveBeenCalledWith(`address.${name}`, values?.[index]);
166
+ // Find and click an address option
167
+ const addressOptions = screen.getAllByRole('listitem');
168
+ const firstOption = addressOptions[0];
169
+ const addressText = firstOption.textContent || '';
170
+
171
+ await user.click(firstOption);
172
+
173
+ // Verify form values were updated
174
+ const addressValues = addressText.split(' > ');
175
+ for (let index = 0; index < allFields.length; index++) {
176
+ const { name } = allFields[index];
177
+ await waitFor(() => {
178
+ const formValues = getFormValues();
179
+ expect(formValues.address[name]).toBe(addressValues[index] || '');
138
180
  });
181
+ }
182
+ });
183
+
184
+ it('clears search results when user clicks outside the component', async () => {
185
+ const user = userEvent.setup();
186
+ mockUseAddressHierarchy.mockReturnValue({
187
+ addresses: mockedAddressOptions,
188
+ error: null,
189
+ isLoading: false,
190
+ });
191
+
192
+ await renderAddressSearchWithFormik();
193
+
194
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
195
+ await user.type(searchbox, 'nea');
196
+
197
+ await waitFor(() => {
198
+ expect(screen.getByRole('list')).toBeInTheDocument();
199
+ });
200
+
201
+ // Mock empty addresses when searchString is cleared
202
+ mockUseAddressHierarchy.mockReturnValue({
203
+ addresses: [],
204
+ error: null,
205
+ isLoading: false,
206
+ });
207
+
208
+ // Click outside - component listens for mousedown events
209
+ fireEvent.mouseDown(document.body);
210
+
211
+ await waitFor(() => {
212
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
213
+ });
214
+ });
215
+
216
+ it('clears search input when an address is selected', async () => {
217
+ const user = userEvent.setup();
218
+ mockUseAddressHierarchy.mockReturnValue({
219
+ addresses: mockedAddressOptions,
220
+ error: null,
221
+ isLoading: false,
139
222
  });
223
+
224
+ await renderAddressSearchWithFormik();
225
+
226
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i }) as HTMLInputElement;
227
+ await user.type(searchbox, 'nea');
228
+
229
+ await waitFor(() => {
230
+ expect(screen.getByRole('list')).toBeInTheDocument();
231
+ });
232
+
233
+ const addressOptions = screen.getAllByRole('listitem');
234
+ await user.click(addressOptions[0]);
235
+
236
+ // Search input should be cleared
237
+ await waitFor(() => {
238
+ expect(searchbox.value).toBe('');
239
+ });
240
+ });
241
+
242
+ it('shows a loading message while search results are being fetched', async () => {
243
+ const user = userEvent.setup();
244
+ mockUseAddressHierarchy.mockReturnValue({
245
+ addresses: [],
246
+ error: null,
247
+ isLoading: true,
248
+ });
249
+
250
+ await renderAddressSearchWithFormik();
251
+
252
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
253
+ await user.type(searchbox, 'nea');
254
+
255
+ expect(screen.getByText(/searching/i)).toBeInTheDocument();
256
+ });
257
+
258
+ it('shows an error message when the address search fails', async () => {
259
+ const user = userEvent.setup();
260
+ mockUseAddressHierarchy.mockReturnValue({
261
+ addresses: [],
262
+ error: new Error('Network error'),
263
+ isLoading: false,
264
+ });
265
+
266
+ await renderAddressSearchWithFormik();
267
+
268
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
269
+ await user.type(searchbox, 'nea');
270
+
271
+ expect(screen.getByText(/error fetching address results/i)).toBeInTheDocument();
272
+ });
273
+
274
+ it('shows a no results message when the search returns no matches', async () => {
275
+ const user = userEvent.setup();
276
+ mockUseAddressHierarchy.mockReturnValue({
277
+ addresses: [],
278
+ error: null,
279
+ isLoading: false,
280
+ });
281
+
282
+ await renderAddressSearchWithFormik();
283
+
284
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
285
+ await user.type(searchbox, 'nonexistent');
286
+
287
+ expect(screen.getByText(/no matching addresses found/i)).toBeInTheDocument();
288
+ });
289
+
290
+ it('does not submit the form when pressing Enter in the search field', async () => {
291
+ const user = userEvent.setup();
292
+ const onSubmit = vi.fn();
293
+
294
+ mockUseAddressHierarchy.mockReturnValue({
295
+ addresses: [],
296
+ error: null,
297
+ isLoading: false,
298
+ });
299
+
300
+ const defaultValues = { address: {} };
301
+ const formValuesRef = { ...initialFormValues, ...defaultValues } as FormValues;
302
+ const mockResourcesContextValue = { addressTemplate: mockedAddressTemplate } as unknown as Resources;
303
+
304
+ await renderWithContext(
305
+ <Formik initialValues={defaultValues} onSubmit={onSubmit}>
306
+ <Form>
307
+ <PatientRegistrationContextProvider
308
+ value={{
309
+ identifierTypes: [],
310
+ values: formValuesRef,
311
+ validationSchema: null,
312
+ inEditMode: false,
313
+ setFieldValue: vi.fn() as any,
314
+ setCapturePhotoProps: vi.fn(),
315
+ setFieldTouched: vi.fn().mockResolvedValue(undefined),
316
+ currentPhoto: '',
317
+ isOffline: false,
318
+ initialFormValues: formValuesRef,
319
+ }}>
320
+ <AddressSearchComponent addressLayout={allFields} />
321
+ </PatientRegistrationContextProvider>
322
+ </Form>
323
+ </Formik>,
324
+ ResourcesContextProvider,
325
+ mockResourcesContextValue,
326
+ );
327
+
328
+ const searchbox = screen.getByRole('searchbox', { name: /search address/i });
329
+ await user.type(searchbox, 'test{Enter}');
330
+
331
+ expect(onSubmit).not.toHaveBeenCalled();
140
332
  });
141
333
  });
@@ -3,8 +3,8 @@ import classNames from 'classnames';
3
3
  import { Field } from 'formik';
4
4
  import { useTranslation } from 'react-i18next';
5
5
  import { Input } from '../../input/basic-input/input/input.component';
6
- import styles from '../field.scss';
7
6
  import { type FieldDefinition } from '../../../config-schema';
7
+ import styles from '../field.scss';
8
8
 
9
9
  export interface AddressFieldProps {
10
10
  fieldDefinition: FieldDefinition;
@@ -59,7 +59,7 @@ export const CauseOfDeathField: React.FC = () => {
59
59
  id="deathCause"
60
60
  invalid={errors.deathCause && touched.deathCause}
61
61
  invalidText={errors.deathCause?.message}
62
- labelText={t('causeOfDeathInputLabel', 'Cause of Death')}
62
+ labelText={t('causeOfDeathInputLabel', 'Cause of death')}
63
63
  name="deathCause"
64
64
  required={required}>
65
65
  <SelectItem id="empty-default-option" value={null} text={t('selectAnOption', 'Select an option')} />
@@ -0,0 +1,251 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { Formik, Form } from 'formik';
5
+ import { render, screen, waitFor } from '@testing-library/react';
6
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
7
+ import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../../config-schema';
8
+ import { useConceptAnswers } from '../field.resource';
9
+ import { CauseOfDeathField } from './cause-of-death.component';
10
+ import { PatientRegistrationContextProvider } from '../../patient-registration-context';
11
+ import { initialFormValues } from '../../patient-registration.component';
12
+ import { type FormValues } from '../../patient-registration.types';
13
+
14
+ const mockUseConfig = vi.mocked(useConfig<RegistrationConfig>);
15
+ const mockUseConceptAnswers = vi.mocked(useConceptAnswers);
16
+
17
+ vi.mock('../field.resource', async () => ({
18
+ ...((await vi.importActual('../field.resource')) as object),
19
+ useConceptAnswers: vi.fn(),
20
+ }));
21
+
22
+ const mockConceptAnswers = [
23
+ { display: 'Cardiac Arrest', uuid: 'cardiac-uuid' },
24
+ { display: 'Pneumonia', uuid: 'pneumonia-uuid' },
25
+ { display: 'Other', uuid: 'other-uuid' },
26
+ ];
27
+
28
+ /**
29
+ * Helper to render CauseOfDeathField with Formik render props for state-dependent tests.
30
+ */
31
+ const renderCauseOfDeathFieldWithFormik = (
32
+ initialValues: Partial<FormValues> = {},
33
+ options?: { enableReinitialize?: boolean },
34
+ ) => {
35
+ const defaultValues = {
36
+ deathCause: '',
37
+ nonCodedCauseOfDeath: '',
38
+ ...initialValues,
39
+ };
40
+
41
+ let formValuesRef: FormValues = { ...initialFormValues, ...defaultValues } as FormValues;
42
+
43
+ const utils = render(
44
+ <Formik initialValues={defaultValues} onSubmit={() => {}} enableReinitialize={options?.enableReinitialize}>
45
+ {({ setFieldValue, values, setFieldTouched }) => {
46
+ formValuesRef = { ...initialFormValues, ...values } as FormValues;
47
+ return (
48
+ <Form>
49
+ <PatientRegistrationContextProvider
50
+ value={{
51
+ identifierTypes: [],
52
+ values: formValuesRef,
53
+ validationSchema: null,
54
+ inEditMode: false,
55
+ setFieldValue: setFieldValue as any,
56
+ setCapturePhotoProps: vi.fn(),
57
+ setFieldTouched: setFieldTouched as any,
58
+ currentPhoto: '',
59
+ isOffline: false,
60
+ initialFormValues: formValuesRef,
61
+ }}>
62
+ <CauseOfDeathField />
63
+ </PatientRegistrationContextProvider>
64
+ </Form>
65
+ );
66
+ }}
67
+ </Formik>,
68
+ );
69
+
70
+ return {
71
+ ...utils,
72
+ getFormValues: () => formValuesRef,
73
+ };
74
+ };
75
+
76
+ describe('CauseOfDeathField', () => {
77
+ beforeEach(() => {
78
+ mockUseConfig.mockReturnValue({
79
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
80
+ fieldConfigurations: {
81
+ causeOfDeath: {
82
+ conceptUuid: 'cause-of-death-uuid',
83
+ required: false,
84
+ },
85
+ } as RegistrationConfig['fieldConfigurations'],
86
+ freeTextFieldConceptUuid: 'other-uuid',
87
+ } as RegistrationConfig);
88
+ mockUseConceptAnswers.mockReturnValue({
89
+ data: mockConceptAnswers,
90
+ isLoading: false,
91
+ error: null,
92
+ });
93
+ });
94
+
95
+ it('renders the cause of death field with label', () => {
96
+ renderCauseOfDeathFieldWithFormik();
97
+
98
+ expect(screen.getByRole('heading', { name: /cause of death/i })).toBeInTheDocument();
99
+ expect(screen.getByRole('combobox', { name: /cause of death/i })).toBeInTheDocument();
100
+ });
101
+
102
+ it('displays loading skeleton when concept answers are loading', () => {
103
+ mockUseConceptAnswers.mockReturnValue({
104
+ data: null,
105
+ isLoading: true,
106
+ error: null,
107
+ });
108
+
109
+ renderCauseOfDeathFieldWithFormik();
110
+
111
+ // SelectSkeleton renders as a skeleton element, check for the skeleton class
112
+ expect(screen.getByText(/cause of death/i)).toBeInTheDocument();
113
+ expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
114
+ });
115
+
116
+ it('displays error notification when concept UUID is not configured', () => {
117
+ mockUseConfig.mockReturnValue({
118
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
119
+ fieldConfigurations: {
120
+ causeOfDeath: {
121
+ conceptUuid: null,
122
+ required: false,
123
+ },
124
+ } as RegistrationConfig['fieldConfigurations'],
125
+ });
126
+
127
+ renderCauseOfDeathFieldWithFormik();
128
+
129
+ expect(screen.getByText(/error fetching coded causes of death/i)).toBeInTheDocument();
130
+ });
131
+
132
+ it('displays error notification when there is an error loading concept answers', () => {
133
+ mockUseConceptAnswers.mockReturnValue({
134
+ data: null,
135
+ isLoading: false,
136
+ error: new Error('Failed to load'),
137
+ });
138
+
139
+ renderCauseOfDeathFieldWithFormik();
140
+
141
+ expect(screen.getByText(/error fetching coded causes of death/i)).toBeInTheDocument();
142
+ });
143
+
144
+ describe('Cause of death selection', () => {
145
+ it('renders all available cause of death options', () => {
146
+ renderCauseOfDeathFieldWithFormik();
147
+
148
+ expect(screen.getByRole('option', { name: /cardiac arrest/i })).toBeInTheDocument();
149
+ expect(screen.getByRole('option', { name: /pneumonia/i })).toBeInTheDocument();
150
+ expect(screen.getByRole('option', { name: /other/i })).toBeInTheDocument();
151
+ });
152
+
153
+ it('updates form value when user selects a cause of death', async () => {
154
+ const user = userEvent.setup();
155
+ const { getFormValues } = renderCauseOfDeathFieldWithFormik();
156
+
157
+ const select = screen.getByRole('combobox', { name: /cause of death/i }) as HTMLSelectElement;
158
+ await user.selectOptions(select, 'cardiac-uuid');
159
+
160
+ await waitFor(() => {
161
+ expect(getFormValues().deathCause).toBe('cardiac-uuid');
162
+ });
163
+ expect(select.value).toBe('cardiac-uuid');
164
+ });
165
+
166
+ it('shows non-coded cause of death input when "Other" is selected', async () => {
167
+ const user = userEvent.setup();
168
+ const { getFormValues } = renderCauseOfDeathFieldWithFormik();
169
+
170
+ const select = screen.getByRole('combobox', { name: /cause of death/i }) as HTMLSelectElement;
171
+ await user.selectOptions(select, 'other-uuid');
172
+
173
+ await waitFor(() => {
174
+ expect(getFormValues().deathCause).toBe('other-uuid');
175
+ });
176
+
177
+ await waitFor(() => {
178
+ expect(screen.getByRole('textbox', { name: /non-coded cause of death/i })).toBeInTheDocument();
179
+ });
180
+ });
181
+
182
+ it('hides non-coded cause of death input when non-other option is selected', async () => {
183
+ const user = userEvent.setup();
184
+ const { getFormValues } = renderCauseOfDeathFieldWithFormik(
185
+ { deathCause: 'other-uuid' },
186
+ { enableReinitialize: true },
187
+ );
188
+
189
+ // Wait for non-coded input to appear (component checks deathCause.value)
190
+ await waitFor(() => {
191
+ expect(screen.getByRole('textbox', { name: /non-coded cause of death/i })).toBeInTheDocument();
192
+ });
193
+
194
+ // Select a different option
195
+ const select = screen.getByRole('combobox', { name: /cause of death/i }) as HTMLSelectElement;
196
+ await user.selectOptions(select, 'cardiac-uuid');
197
+
198
+ await waitFor(() => {
199
+ expect(getFormValues().deathCause).toBe('cardiac-uuid');
200
+ });
201
+
202
+ // Non-coded input should be hidden
203
+ await waitFor(() => {
204
+ expect(screen.queryByRole('textbox', { name: /non-coded cause of death/i })).not.toBeInTheDocument();
205
+ });
206
+ });
207
+
208
+ it('allows user to enter non-coded cause of death text', async () => {
209
+ const user = userEvent.setup();
210
+ const { getFormValues } = renderCauseOfDeathFieldWithFormik(
211
+ { deathCause: 'other-uuid' },
212
+ { enableReinitialize: true },
213
+ );
214
+
215
+ // Wait for non-coded input to appear
216
+ const textInput = await screen.findByRole('textbox', { name: /non-coded cause of death/i });
217
+ await user.type(textInput, 'Unknown cause');
218
+
219
+ await waitFor(() => {
220
+ expect(getFormValues().nonCodedCauseOfDeath).toBe('Unknown cause');
221
+ });
222
+ });
223
+ });
224
+
225
+ describe('Required field', () => {
226
+ it('renders as required when configured', () => {
227
+ mockUseConfig.mockReturnValue({
228
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
229
+ fieldConfigurations: {
230
+ causeOfDeath: {
231
+ conceptUuid: 'cause-of-death-uuid',
232
+ required: true,
233
+ },
234
+ } as RegistrationConfig['fieldConfigurations'],
235
+ freeTextFieldConceptUuid: 'other-uuid',
236
+ } as RegistrationConfig);
237
+
238
+ renderCauseOfDeathFieldWithFormik();
239
+
240
+ const select = screen.getByRole('combobox', { name: /cause of death/i });
241
+ expect(select).toBeRequired();
242
+ });
243
+
244
+ it('renders as optional when not required', () => {
245
+ renderCauseOfDeathFieldWithFormik();
246
+
247
+ const select = screen.getByRole('combobox', { name: /cause of death/i });
248
+ expect(select).not.toBeRequired();
249
+ });
250
+ });
251
+ });
@@ -11,7 +11,7 @@ export interface CustomFieldProps {
11
11
 
12
12
  export function CustomField({ name }: CustomFieldProps) {
13
13
  const config = useConfig<RegistrationConfig>();
14
- const fieldDefinition = config.fieldDefinitions.filter((def) => def.id == name)[0];
14
+ const fieldDefinition = config.fieldDefinitions.filter((def) => def.id === name)[0];
15
15
 
16
16
  if (fieldDefinition.type === 'person attribute') {
17
17
  return <PersonAttributeField fieldDefinition={fieldDefinition} />;
@@ -14,7 +14,7 @@ export const DateAndTimeOfDeathField: React.FC = () => {
14
14
 
15
15
  return (
16
16
  <div className={classNames(styles.dodField, styles.halfWidthInDesktopView)}>
17
- <h4 className={styles.productiveHeading02Light}>{t('deathDateInputLabel', 'Date of Death')}</h4>
17
+ <h4 className={styles.productiveHeading02Light}>{t('deathDateInputLabel', 'Date of death')}</h4>
18
18
  <span>
19
19
  <DeathDateField />
20
20
  <DeathTimeField />