@olaboot/esm-patient-registration-app 9.2.1 → 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 (342) 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 +507 -507
  137. package/src/constants.ts +1 -1
  138. package/src/index.ts +2 -24
  139. package/src/nav-link.test.tsx +1 -0
  140. package/src/offline.resources.ts +97 -31
  141. package/src/patient-registration/before-save-prompt.test.tsx +199 -0
  142. package/src/patient-registration/field/__mocks__/field.resource.ts +8 -7
  143. package/src/patient-registration/field/address/address-field.component.tsx +183 -186
  144. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +6 -1
  145. package/src/patient-registration/field/address/address-hierarchy.test.tsx +289 -296
  146. package/src/patient-registration/field/address/address-search.component.tsx +20 -8
  147. package/src/patient-registration/field/address/address-search.scss +19 -2
  148. package/src/patient-registration/field/address/address-search.test.tsx +333 -141
  149. package/src/patient-registration/field/address/custom-address-field.component.tsx +1 -1
  150. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +1 -1
  151. package/src/patient-registration/field/cause-of-death/cause-of-death.test.tsx +251 -0
  152. package/src/patient-registration/field/custom-field.component.tsx +1 -1
  153. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +1 -1
  154. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.test.tsx +144 -0
  155. package/src/patient-registration/field/dob/dob.component.tsx +2 -2
  156. package/src/patient-registration/field/dob/dob.test.tsx +370 -54
  157. package/src/patient-registration/field/field.component.tsx +1 -1
  158. package/src/patient-registration/field/field.resource.ts +2 -2
  159. package/src/patient-registration/field/field.test.tsx +333 -330
  160. package/src/patient-registration/field/gender/gender-field.test.tsx +240 -54
  161. package/src/patient-registration/field/id/id-field.component.tsx +146 -144
  162. package/src/patient-registration/field/id/id-field.test.tsx +177 -121
  163. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +200 -200
  164. package/src/patient-registration/field/id/identifier-selection-overlay.test.tsx +346 -0
  165. package/src/patient-registration/field/name/name-field.component.tsx +2 -2
  166. package/src/patient-registration/field/name/name-field.test.tsx +282 -0
  167. package/src/patient-registration/field/obs/obs-field.test.tsx +294 -118
  168. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +172 -108
  169. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +3 -3
  170. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +2 -5
  171. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +2 -2
  172. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +249 -131
  173. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +1 -1
  174. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +98 -70
  175. package/src/patient-registration/field/phone/phone-field.test.tsx +100 -0
  176. package/src/patient-registration/form-manager.test.ts +6 -5
  177. package/src/patient-registration/form-manager.ts +5 -2
  178. package/src/patient-registration/input/basic-input/input/input.component.tsx +3 -121
  179. package/src/patient-registration/input/basic-input/input/input.test.tsx +151 -51
  180. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +113 -33
  181. package/src/patient-registration/input/combo-input/combo-input.component.tsx +60 -24
  182. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +10 -101
  183. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +144 -108
  184. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +241 -177
  185. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +47 -8
  186. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +12 -12
  187. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +52 -20
  188. package/src/patient-registration/input/input.scss +1 -2
  189. package/src/patient-registration/patient-registration-context.ts +5 -3
  190. package/src/patient-registration/patient-registration-hooks.ts +4 -12
  191. package/src/patient-registration/patient-registration-utils.test.ts +2 -1
  192. package/src/patient-registration/patient-registration-utils.ts +2 -98
  193. package/src/patient-registration/patient-registration.component.tsx +50 -46
  194. package/src/patient-registration/patient-registration.resource.test.tsx +4 -7
  195. package/src/patient-registration/patient-registration.resource.ts +1 -4
  196. package/src/patient-registration/patient-registration.scss +16 -3
  197. package/src/patient-registration/patient-registration.test.tsx +614 -580
  198. package/src/patient-registration/patient-registration.types.ts +17 -28
  199. package/src/patient-registration/section/death-info/death-info-section.test.tsx +130 -34
  200. package/src/patient-registration/section/demographics/demographics-section.test.tsx +122 -68
  201. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +15 -15
  202. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +278 -84
  203. package/src/patient-registration/section/section-wrapper.component.tsx +1 -1
  204. package/src/patient-registration/ui-components/overlay/overlay.test.tsx +104 -0
  205. package/src/patient-registration/validation/patient-registration-validation.test.ts +2 -1
  206. package/src/patient-registration/validation/patient-registration-validation.ts +9 -3
  207. package/src/root.component.tsx +2 -5
  208. package/src/widgets/cancel-patient-edit.test.tsx +48 -11
  209. package/src/widgets/delete-identifier-confirmation.test.tsx +77 -24
  210. package/src/widgets/edit-patient-details-button.component.tsx +14 -18
  211. package/src/widgets/edit-patient-details-button.scss +2 -2
  212. package/src/widgets/edit-patient-details-button.test.tsx +11 -13
  213. package/translations/am.json +9 -4
  214. package/translations/ar.json +9 -4
  215. package/translations/ar_SY.json +9 -4
  216. package/translations/bn.json +9 -4
  217. package/translations/cs.json +9 -4
  218. package/translations/de.json +120 -115
  219. package/translations/en.json +10 -4
  220. package/translations/en_US.json +9 -4
  221. package/translations/es.json +9 -4
  222. package/translations/es_MX.json +9 -4
  223. package/translations/fr.json +9 -4
  224. package/translations/he.json +9 -4
  225. package/translations/hi.json +9 -4
  226. package/translations/hi_IN.json +9 -4
  227. package/translations/id.json +9 -4
  228. package/translations/it.json +9 -4
  229. package/translations/ka.json +9 -4
  230. package/translations/km.json +9 -4
  231. package/translations/ku.json +9 -4
  232. package/translations/ky.json +9 -4
  233. package/translations/lg.json +9 -4
  234. package/translations/ne.json +9 -4
  235. package/translations/pl.json +9 -4
  236. package/translations/pt.json +9 -4
  237. package/translations/pt_BR.json +10 -5
  238. package/translations/qu.json +9 -4
  239. package/translations/ro_RO.json +9 -4
  240. package/translations/ru_RU.json +9 -4
  241. package/translations/si.json +9 -4
  242. package/translations/sq.json +9 -4
  243. package/translations/sw.json +9 -4
  244. package/translations/sw_KE.json +9 -4
  245. package/translations/tr.json +9 -4
  246. package/translations/tr_TR.json +9 -4
  247. package/translations/uk.json +9 -4
  248. package/translations/uz.json +9 -4
  249. package/translations/uz@Latn.json +9 -4
  250. package/translations/uz_UZ.json +9 -4
  251. package/translations/vi.json +9 -4
  252. package/translations/zh.json +50 -45
  253. package/translations/zh_CN.json +9 -4
  254. package/translations/zh_TW.json +9 -4
  255. package/vitest.config.ts +4 -0
  256. package/dist/126.js +0 -1
  257. package/dist/15.js +0 -1
  258. package/dist/1564.js +0 -1
  259. package/dist/1567.js +0 -1
  260. package/dist/1845.js +0 -1
  261. package/dist/1953.js +0 -1
  262. package/dist/200.js +0 -1
  263. package/dist/200.js.map +0 -1
  264. package/dist/215.js +0 -1
  265. package/dist/2178.js +0 -1
  266. package/dist/250.js +0 -1
  267. package/dist/250.js.map +0 -1
  268. package/dist/2523.js +0 -1
  269. package/dist/2523.js.map +0 -1
  270. package/dist/2566.js +0 -1
  271. package/dist/2586.js +0 -1
  272. package/dist/2586.js.map +0 -1
  273. package/dist/2716.js +0 -1
  274. package/dist/2716.js.map +0 -1
  275. package/dist/2759.js +0 -1
  276. package/dist/2821.js +0 -6
  277. package/dist/2821.js.map +0 -1
  278. package/dist/3089.js +0 -1
  279. package/dist/3089.js.map +0 -1
  280. package/dist/3230.js +0 -1
  281. package/dist/3441.js +0 -1
  282. package/dist/3565.js +0 -1
  283. package/dist/3571.js +0 -1
  284. package/dist/3571.js.map +0 -1
  285. package/dist/3746.js +0 -1
  286. package/dist/3925.js +0 -1
  287. package/dist/3946.js +0 -1
  288. package/dist/4024.js +0 -1
  289. package/dist/4744.js +0 -1
  290. package/dist/4744.js.map +0 -1
  291. package/dist/4809.js +0 -1
  292. package/dist/4894.js +0 -1
  293. package/dist/4970.js +0 -1
  294. package/dist/4970.js.map +0 -1
  295. package/dist/5130.js +0 -1
  296. package/dist/5187.js +0 -1
  297. package/dist/5491.js +0 -1
  298. package/dist/5491.js.map +0 -1
  299. package/dist/5595.js +0 -1
  300. package/dist/5961.js +0 -1
  301. package/dist/6133.js +0 -1
  302. package/dist/634.js +0 -1
  303. package/dist/634.js.map +0 -1
  304. package/dist/6456.js +0 -1
  305. package/dist/6466.js +0 -1
  306. package/dist/6613.js +0 -1
  307. package/dist/6783.js +0 -1
  308. package/dist/7073.js +0 -38
  309. package/dist/7073.js.map +0 -1
  310. package/dist/7154.js +0 -1
  311. package/dist/7154.js.map +0 -1
  312. package/dist/7348.js +0 -1
  313. package/dist/7439.js +0 -1
  314. package/dist/7439.js.map +0 -1
  315. package/dist/7543.js +0 -1
  316. package/dist/7607.js +0 -1
  317. package/dist/772.js +0 -1
  318. package/dist/7984.js +0 -1
  319. package/dist/7984.js.map +0 -1
  320. package/dist/8538.js +0 -1
  321. package/dist/8538.js.map +0 -1
  322. package/dist/8599.js +0 -1
  323. package/dist/8727.js +0 -1
  324. package/dist/8847.js +0 -1
  325. package/dist/9015.js +0 -1
  326. package/dist/906.js +0 -1
  327. package/dist/9065.js +0 -1
  328. package/dist/9182.js +0 -1
  329. package/dist/9339.js +0 -1
  330. package/dist/9453.js +0 -1
  331. package/dist/9833.js +0 -1
  332. package/dist/9833.js.map +0 -1
  333. package/dist/9856.js +0 -1
  334. package/dist/9856.js.map +0 -1
  335. package/dist/9920.js +0 -1
  336. package/dist/9938.js +0 -1
  337. package/dist/9943.js +0 -1
  338. package/dist/9943.js.map +0 -1
  339. package/dist/olaboot-esm-patient-registration-app.js +0 -5
  340. package/dist/olaboot-esm-patient-registration-app.js.map +0 -1
  341. package/jest.config.js +0 -3
  342. package/src/resource.ts +0 -12
@@ -1,580 +1,614 @@
1
- import React from 'react';
2
- import userEvent from '@testing-library/user-event';
3
- import { useParams } from 'react-router-dom';
4
- import { screen, within } from '@testing-library/react';
5
- import {
6
- type FetchResponse,
7
- getDefaultsFromConfigSchema,
8
- showSnackbar,
9
- useConfig,
10
- usePatient,
11
- } from '@openmrs/esm-framework';
12
- import type { AddressTemplate, Encounter, FormValues } from './patient-registration.types';
13
- import { mockedAddressTemplate } from '__mocks__';
14
- import { mockPatient, renderWithContext } from 'tools';
15
- import { saveEncounter, savePatient } from './patient-registration.resource';
16
- import { esmPatientRegistrationSchema, type RegistrationConfig } from '../config-schema';
17
- import { FormManager } from './form-manager';
18
- import { PatientRegistration } from './patient-registration.component';
19
- import { useInitialFormValues } from './patient-registration-hooks';
20
- import { ResourcesContextProvider } from '../resources-context';
21
-
22
- const mockSaveEncounter = jest.mocked(saveEncounter);
23
- const mockSavePatient = savePatient as jest.Mock;
24
- const mockShowSnackbar = jest.mocked(showSnackbar);
25
- const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
26
- const mockUsePatient = jest.mocked(usePatient);
27
- const mockUseParams = useParams as jest.Mock;
28
- const mockUseInitialFormValues = jest.mocked(useInitialFormValues);
29
-
30
- jest.mock('./field/field.resource', () => ({
31
- useConcept: jest.fn().mockImplementation((uuid: string) => {
32
- let data;
33
- if (uuid == 'weight-uuid') {
34
- data = {
35
- uuid: 'weight-uuid',
36
- display: 'Weight (kg)',
37
- datatype: { display: 'Numeric', uuid: 'num' },
38
- answers: [],
39
- setMembers: [],
40
- };
41
- } else if (uuid == 'chief-complaint-uuid') {
42
- data = {
43
- uuid: 'chief-complaint-uuid',
44
- display: 'Chief Complaint',
45
- datatype: { display: 'Text', uuid: 'txt' },
46
- answers: [],
47
- setMembers: [],
48
- };
49
- } else if (uuid == 'nationality-uuid') {
50
- data = {
51
- uuid: 'nationality-uuid',
52
- display: 'Nationality',
53
- datatype: { display: 'Coded', uuid: 'cdd' },
54
- answers: [
55
- { display: 'USA', uuid: 'usa' },
56
- { display: 'Mexico', uuid: 'mex' },
57
- ],
58
- setMembers: [],
59
- };
60
- }
61
- return {
62
- data: data ?? null,
63
- isLoading: !data,
64
- };
65
- }),
66
- useConceptAnswers: jest.fn().mockImplementation((uuid: string) => {
67
- if (uuid == 'nationality-uuid') {
68
- return {
69
- data: [
70
- { display: 'USA', uuid: 'usa' },
71
- { display: 'Mexico', uuid: 'mex' },
72
- ],
73
- isLoading: false,
74
- };
75
- } else if (uuid == 'other-countries-uuid') {
76
- return {
77
- data: [
78
- { display: 'Kenya', uuid: 'ke' },
79
- { display: 'Uganda', uuid: 'ug' },
80
- ],
81
- isLoading: false,
82
- };
83
- }
84
- }),
85
- }));
86
-
87
- jest.mock('react-router-dom', () => ({
88
- ...jest.requireActual('react-router-dom'),
89
- useLocation: () => ({
90
- pathname: 'openmrs/spa/patient-registration',
91
- }),
92
- useHistory: () => [],
93
- useParams: jest.fn().mockReturnValue({ patientUuid: undefined }),
94
- }));
95
-
96
- jest.mock('./patient-registration.resource', () => ({
97
- ...jest.requireActual('./patient-registration.resource'),
98
- saveEncounter: jest.fn(),
99
- savePatient: jest.fn(),
100
- }));
101
-
102
- jest.mock('./patient-registration-hooks', () => ({
103
- ...jest.requireActual('./patient-registration-hooks'),
104
- useInitialFormValues: jest.fn().mockReturnValue([{}, jest.fn()]),
105
- useInitialAddressFieldValues: jest.fn().mockReturnValue([{}, jest.fn()]),
106
- usePatientUuidMap: jest.fn().mockReturnValue([{}, jest.fn()]),
107
- }));
108
-
109
- const mockResourcesContextValue = {
110
- addressTemplate: mockedAddressTemplate as AddressTemplate,
111
- currentSession: {
112
- authenticated: true,
113
- sessionId: 'JSESSION',
114
- currentProvider: { uuid: 'provider-uuid', identifier: 'PRO-123' },
115
- },
116
- relationshipTypes: [],
117
- identifierTypes: [],
118
- };
119
-
120
- const mockOpenmrsConfig: RegistrationConfig = {
121
- sections: ['demographics', 'contact'],
122
- sectionDefinitions: [
123
- { id: 'demographics', name: 'Demographics', fields: ['name', 'gender', 'dob'] },
124
- { id: 'contact', name: 'Contact Info', fields: ['address'] },
125
- { id: 'relationships', name: 'Relationships', fields: ['relationship'] },
126
- ],
127
- fieldDefinitions: [],
128
- fieldConfigurations: {
129
- phone: {
130
- personAttributeUuid: '14d4f066-15f5-102d-96e4-000c29c2a5d7',
131
- },
132
- dateOfBirth: {
133
- allowEstimatedDateOfBirth: true,
134
- useEstimatedDateOfBirth: {
135
- enabled: true,
136
- dayOfMonth: new Date().getDate(),
137
- month: new Date().getMonth(),
138
- },
139
- },
140
- name: {
141
- displayMiddleName: true,
142
- allowUnidentifiedPatients: true,
143
- defaultUnknownGivenName: 'UNKNOWN',
144
- defaultUnknownFamilyName: 'UNKNOWN',
145
- displayReverseFieldOrder: false,
146
- displayCapturePhoto: true,
147
- },
148
- gender: [
149
- {
150
- value: 'male',
151
- label: 'Male',
152
- },
153
- {
154
- value: 'female',
155
- label: 'Female',
156
- },
157
- ],
158
- address: {
159
- useAddressHierarchy: {
160
- enabled: true,
161
- useQuickSearch: true,
162
- searchAddressByLevel: true,
163
- },
164
- requiredAddressFields: [],
165
- },
166
- causeOfDeath: {
167
- conceptUuid: 'cause-of-death-concept-uuid',
168
- },
169
- },
170
- links: {
171
- submitButton: '#',
172
- },
173
- defaultPatientIdentifierTypes: [],
174
- registrationObs: {
175
- encounterTypeUuid: null,
176
- encounterProviderRoleUuid: 'asdf',
177
- registrationFormUuid: null,
178
- },
179
- freeTextFieldConceptUuid: '',
180
- };
181
- const configWithObs = JSON.parse(JSON.stringify(mockOpenmrsConfig));
182
-
183
- configWithObs.fieldDefinitions = [
184
- {
185
- id: 'weight',
186
- type: 'obs',
187
- label: null,
188
- uuid: 'weight-uuid',
189
- placeholder: '',
190
- validation: { required: false, matches: null },
191
- answerConceptSetUuid: null,
192
- customConceptAnswers: [],
193
- },
194
- {
195
- id: 'chief complaint',
196
- type: 'obs',
197
- label: null,
198
- uuid: 'chief-complaint-uuid',
199
- placeholder: '',
200
- validation: { required: false, matches: null },
201
- answerConceptSetUuid: null,
202
- customConceptAnswers: [],
203
- },
204
- {
205
- id: 'nationality',
206
- type: 'obs',
207
- label: null,
208
- uuid: 'nationality-uuid',
209
- placeholder: '',
210
- validation: { required: false, matches: null },
211
- answerConceptSetUuid: null,
212
- customConceptAnswers: [],
213
- },
214
- ];
215
- configWithObs.sectionDefinitions?.push({
216
- id: 'custom',
217
- name: 'Custom',
218
- fields: ['weight', 'chief complaint', 'nationality'],
219
- });
220
- configWithObs.sections.push('custom');
221
- configWithObs.registrationObs.encounterTypeUuid = 'reg-enc-uuid';
222
-
223
- const fillRequiredFields = async () => {
224
- const user = userEvent.setup();
225
-
226
- const demographicsSection = await screen.findByLabelText('Demographics Section');
227
- const givenNameInput = within(demographicsSection).getByLabelText(/first/i) as HTMLInputElement;
228
- const familyNameInput = within(demographicsSection).getByLabelText(/family/i) as HTMLInputElement;
229
- const dateInput = within(demographicsSection).getByRole('spinbutton', {
230
- name: /day, date of birth/i,
231
- }) as HTMLInputElement;
232
- const monthInput = within(demographicsSection).getByRole('spinbutton', {
233
- name: /month, date of birth/i,
234
- }) as HTMLInputElement;
235
- const yearInput = within(demographicsSection).getByRole('spinbutton', {
236
- name: /year, date of birth/i,
237
- }) as HTMLInputElement;
238
- const genderInput = within(demographicsSection).getByLabelText(/Male/) as HTMLSelectElement;
239
- await user.type(givenNameInput, 'Paul');
240
- await user.type(familyNameInput, 'Gaihre');
241
- await user.clear(dateInput);
242
- await user.type(dateInput, '02');
243
- await user.clear(monthInput);
244
- await user.type(monthInput, '08');
245
- await user.clear(yearInput);
246
- await user.type(yearInput, '1993');
247
- await user.click(genderInput);
248
- };
249
-
250
- describe('Registering a new patient', () => {
251
- beforeEach(() => {
252
- mockUseConfig.mockReturnValue({
253
- ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
254
- ...mockOpenmrsConfig,
255
- });
256
- mockSavePatient.mockReturnValue({ data: { uuid: 'new-pt-uuid' }, ok: true });
257
- });
258
-
259
- it('should render all the required fields and sections', async () => {
260
- renderWithContext(
261
- <PatientRegistration isOffline={false} savePatientForm={jest.fn()} />,
262
- ResourcesContextProvider,
263
- mockResourcesContextValue,
264
- );
265
-
266
- await screen.findByRole('heading', { name: /create new patient/i });
267
-
268
- const demographicSection = screen.getByRole('region', { name: /demographics section/i });
269
- const contactSection = screen.getByRole('region', { name: /contact info section/i });
270
-
271
- expect(demographicSection).toBeInTheDocument();
272
- expect(contactSection).toBeInTheDocument();
273
- expect(screen.getByText(/jump to/i)).toBeInTheDocument();
274
- expect(within(demographicSection).getByLabelText(/first name/i)).toBeInTheDocument();
275
- expect(within(demographicSection).getByLabelText(/middle name \(optional\)/i)).toBeInTheDocument();
276
- expect(within(demographicSection).getByLabelText(/family name/i)).toBeInTheDocument();
277
- const dateOfBirthInput = within(demographicSection).getByLabelText(/date of birth/i);
278
- expect(dateOfBirthInput).toBeInTheDocument();
279
- expect(within(demographicSection).getByRole('radio', { name: /^male$/i })).toBeInTheDocument();
280
- expect(within(demographicSection).getByRole('radio', { name: /^female$/i })).toBeInTheDocument();
281
- expect(within(demographicSection).getByText(/date of birth known\?/i)).toBeInTheDocument();
282
-
283
- expect(within(contactSection).getByRole('heading', { name: /address/i })).toBeInTheDocument();
284
-
285
- expect(screen.getByRole('button', { name: /register patient/i })).toBeInTheDocument();
286
- expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
287
- });
288
-
289
- // FIXME the register patient button is missing
290
- it.skip('saves the patient without extra info', async () => {
291
- const user = userEvent.setup();
292
-
293
- renderWithContext(
294
- <PatientRegistration isOffline={false} savePatientForm={FormManager.savePatientFormOnline} />,
295
- ResourcesContextProvider,
296
- mockResourcesContextValue,
297
- );
298
-
299
- await fillRequiredFields();
300
- await user.click(await screen.findByText(/Register Patient/i));
301
- expect(mockSavePatient).toHaveBeenCalledWith(
302
- expect.objectContaining({
303
- identifiers: [], //TODO when the identifer story is finished: { identifier: '', identifierType: '05a29f94-c0ed-11e2-94be-8c13b969e334', location: '' },
304
- person: {
305
- addresses: expect.arrayContaining([expect.any(Object)]),
306
- attributes: [],
307
- birthdate: '1993-8-2',
308
- birthdateEstimated: false,
309
- gender: expect.stringMatching(/^M$/),
310
- names: [{ givenName: 'Paul', middleName: '', familyName: 'Gaihre', preferred: true, uuid: undefined }],
311
- dead: false,
312
- uuid: expect.anything(),
313
- },
314
- uuid: expect.anything(),
315
- }),
316
- undefined,
317
- );
318
- });
319
-
320
- it('should not save the patient if validation fails', async () => {
321
- const user = userEvent.setup();
322
- const mockSavePatientForm = jest.fn();
323
-
324
- renderWithContext(
325
- <PatientRegistration isOffline={false} savePatientForm={mockSavePatientForm} />,
326
- ResourcesContextProvider,
327
- mockResourcesContextValue,
328
- );
329
-
330
- await screen.findByRole('heading', { name: /create new patient/i });
331
- await user.click(screen.getByRole('button', { name: /register patient/i }));
332
-
333
- expect(mockSavePatientForm).not.toHaveBeenCalled();
334
- });
335
-
336
- // FIXME: the register patient button is missing
337
- it.skip('renders and saves registration obs', async () => {
338
- const user = userEvent.setup();
339
-
340
- mockSaveEncounter.mockResolvedValue({} as unknown as FetchResponse);
341
- mockUseConfig.mockReturnValue(configWithObs);
342
-
343
- renderWithContext(
344
- <PatientRegistration isOffline={false} savePatientForm={FormManager.savePatientFormOnline} />,
345
- ResourcesContextProvider,
346
- mockResourcesContextValue,
347
- );
348
-
349
- await fillRequiredFields();
350
- const customSection = screen.getByLabelText('Custom Section');
351
- const weight = within(customSection).getByLabelText('Weight (kg) (optional)');
352
- await user.type(weight, '50');
353
- const complaint = within(customSection).getByLabelText('Chief Complaint (optional)');
354
- await user.type(complaint, 'sad');
355
- const nationality = within(customSection).getByLabelText('Nationality');
356
- await user.selectOptions(nationality, 'USA');
357
-
358
- await user.click(screen.getByText(/Register Patient/i));
359
-
360
- expect(mockSavePatient).toHaveBeenCalled();
361
-
362
- expect(mockSaveEncounter).toHaveBeenCalledWith(
363
- expect.objectContaining<Partial<Encounter>>({
364
- encounterType: 'reg-enc-uuid',
365
- patient: 'new-pt-uuid',
366
- obs: [
367
- { concept: 'weight-uuid', value: 50 },
368
- { concept: 'chief-complaint-uuid', value: 'sad' },
369
- { concept: 'nationality-uuid', value: 'usa' },
370
- ],
371
- }),
372
- );
373
- });
374
-
375
- // FIXME register patient button is missing
376
- it.skip('retries saving registration obs after a failed attempt', async () => {
377
- const user = userEvent.setup();
378
-
379
- mockUseConfig.mockReturnValue(configWithObs);
380
-
381
- renderWithContext(
382
- <PatientRegistration isOffline={false} savePatientForm={FormManager.savePatientFormOnline} />,
383
- ResourcesContextProvider,
384
- mockResourcesContextValue,
385
- );
386
-
387
- await fillRequiredFields();
388
- const customSection = screen.getByLabelText('Custom Section');
389
- const weight = within(customSection).getByLabelText('Weight (kg) (optional)');
390
- await user.type(weight, '-999');
391
-
392
- mockSaveEncounter.mockRejectedValue({ status: 400, responseBody: { error: { message: 'an error message' } } });
393
-
394
- const registerPatientButton = screen.getByText(/Register Patient/i);
395
-
396
- await user.click(registerPatientButton);
397
-
398
- expect(mockSavePatient).toHaveBeenCalledTimes(1);
399
- expect(mockSaveEncounter).toHaveBeenCalledTimes(1);
400
-
401
- expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ subtitle: 'an error message' }));
402
- mockSaveEncounter.mockResolvedValue({} as FetchResponse);
403
-
404
- await user.click(registerPatientButton);
405
- expect(mockSavePatient).toHaveBeenCalledTimes(2);
406
- expect(mockSaveEncounter).toHaveBeenCalledTimes(2);
407
-
408
- expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
409
- });
410
- });
411
-
412
- describe('Updating an existing patient record', () => {
413
- beforeEach(() => {
414
- mockUseConfig.mockReturnValue({
415
- ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
416
- ...mockOpenmrsConfig,
417
- });
418
- mockUsePatient.mockImplementation(() => {
419
- return {
420
- error: null,
421
- isLoading: false,
422
- patient: mockPatient,
423
- patientUuid: mockPatient.id,
424
- };
425
- });
426
- mockSavePatient.mockReturnValue({ data: { uuid: 'new-pt-uuid' }, ok: true });
427
- mockUseParams.mockReturnValue({ patientUuid: mockPatient.id });
428
- });
429
-
430
- it('edits patient demographics', async () => {
431
- const user = userEvent.setup();
432
- const mockSavePatientForm = jest.fn();
433
-
434
- mockUseInitialFormValues.mockReturnValue([
435
- {
436
- additionalFamilyName: '',
437
- additionalGivenName: '',
438
- additionalMiddleName: '',
439
- addNameInLocalLanguage: false,
440
- address: {},
441
- birthdate: mockPatient.birthDate,
442
- birthdateEstimated: false,
443
- deathCause: '',
444
- deathDate: undefined,
445
- deathTime: undefined,
446
- deathTimeFormat: 'AM',
447
- familyName: mockPatient.name[0].family,
448
- gender: mockPatient.gender,
449
- givenName: mockPatient.name[0].given[0],
450
- identifiers: {
451
- openMrsId: {
452
- autoGeneration: false,
453
- identifierName: 'OpenMRS ID',
454
- identifierTypeUuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
455
- identifierUuid: '1f0ad7a1-430f-4397-b571-59ea654a52db',
456
- identifierValue: '100GEJ',
457
- initialValue: '100GEJ',
458
- preferred: true,
459
- required: true,
460
- selectedSource: null,
461
- },
462
- idCard: {
463
- autoGeneration: false,
464
- identifierName: 'ID Card',
465
- identifierTypeUuid: 'b4143563-16cd-4439-b288-f83d61670fc8',
466
- identifierUuid: '346d09b1-8509-43c6-9697-3b4d1ce06ad6',
467
- identifierValue: '1234567890',
468
- initialValue: '1234567890',
469
- preferred: false,
470
- required: false,
471
- selectedSource: null,
472
- },
473
- },
474
- isDead: false,
475
- middleName: '',
476
- monthsEstimated: 0,
477
- nonCodedCauseOfDeath: '',
478
- patientUuid: mockPatient.id,
479
- relationships: [],
480
- telephoneNumber: '',
481
- yearsEstimated: 0,
482
- } as FormValues,
483
- jest.fn(),
484
- ]);
485
-
486
- renderWithContext(
487
- <PatientRegistration isOffline={false} savePatientForm={mockSavePatientForm} />,
488
- ResourcesContextProvider,
489
- mockResourcesContextValue,
490
- );
491
-
492
- await screen.findByRole('heading', { name: /edit patient details/i });
493
-
494
- expect(screen.queryByRole('button', { name: /register patient/i })).not.toBeInTheDocument();
495
- expect(screen.getByRole('button', { name: /update patient/i })).toBeInTheDocument();
496
- expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
497
-
498
- expect(screen.getByLabelText(/first name/i)).toHaveValue(mockPatient.name[0].given[0]);
499
- expect(screen.getByLabelText(/family name/i)).toHaveValue(mockPatient.name[0].family);
500
- // FIXME: Fix the mock so that this value is visible
501
- // expect(screen.getByLabelText(/date of birth/i)).toHaveValue(mockPatient.birthDate);
502
- expect(
503
- screen.getByRole('radio', {
504
- name: /^male$/i,
505
- }),
506
- ).toBeChecked();
507
- expect(
508
- screen.getByRole('radio', {
509
- name: /^female$/i,
510
- }),
511
- ).not.toBeChecked();
512
- expect(screen.getAllByRole('tab', { name: /yes/i })).toHaveLength(2);
513
-
514
- await user.click(screen.getByRole('button', { name: /update patient/i }));
515
-
516
- expect(mockSavePatientForm).toHaveBeenCalledWith(
517
- false,
518
- {
519
- addNameInLocalLanguage: false,
520
- additionalFamilyName: '',
521
- additionalGivenName: '',
522
- additionalMiddleName: '',
523
- address: {
524
- country: 'កម្ពុជា (Cambodia)',
525
- },
526
- birthdate: '1972-04-04',
527
- birthdateEstimated: false,
528
- deathCause: '',
529
- nonCodedCauseOfDeath: '',
530
- deathDate: undefined,
531
- deathTime: undefined,
532
- deathTimeFormat: 'AM',
533
- familyName: 'Wilson',
534
- gender: 'male',
535
- givenName: 'John',
536
- identifiers: {
537
- idCard: {
538
- autoGeneration: false,
539
- identifierName: 'ID Card',
540
- identifierTypeUuid: 'b4143563-16cd-4439-b288-f83d61670fc8',
541
- identifierUuid: '346d09b1-8509-43c6-9697-3b4d1ce06ad6',
542
- identifierValue: '1234567890',
543
- initialValue: '1234567890',
544
- preferred: false,
545
- required: false,
546
- selectedSource: null,
547
- },
548
- openMrsId: {
549
- autoGeneration: false,
550
- identifierName: 'OpenMRS ID',
551
- identifierTypeUuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
552
- identifierUuid: '1f0ad7a1-430f-4397-b571-59ea654a52db',
553
- identifierValue: '100GEJ',
554
- initialValue: '100GEJ',
555
- preferred: true,
556
- required: true,
557
- selectedSource: null,
558
- },
559
- },
560
- isDead: false,
561
- middleName: '',
562
- monthsEstimated: 0,
563
- patientUuid: '8673ee4f-e2ab-4077-ba55-4980f408773e',
564
- relationships: [],
565
- telephoneNumber: '',
566
- unidentifiedPatient: undefined,
567
- yearsEstimated: 0,
568
- },
569
- expect.anything(),
570
- expect.anything(),
571
- null,
572
- undefined,
573
- expect.anything(),
574
- expect.anything(),
575
- expect.anything(),
576
- { patientSaved: false },
577
- expect.anything(),
578
- );
579
- });
580
- });
1
+ /**
2
+ * @vitest-environment jsdom
3
+ *
4
+ * The form-submit flow under test does not fire its callback under happy-dom
5
+ * (likely a DOM-event-dispatch divergence). Run this file under jsdom.
6
+ */
7
+ import React from 'react';
8
+ import { vi, describe, it, expect, test, beforeEach } from 'vitest';
9
+ import userEvent from '@testing-library/user-event';
10
+ import { useParams } from 'react-router-dom';
11
+ import { fireEvent, screen, within } from '@testing-library/react';
12
+ import {
13
+ type FetchResponse,
14
+ getDefaultsFromConfigSchema,
15
+ showSnackbar,
16
+ useConfig,
17
+ usePatient,
18
+ } from '@openmrs/esm-framework';
19
+ import type { AddressTemplate, Encounter, FormValues } from './patient-registration.types';
20
+ import { mockedAddressTemplate } from '__mocks__';
21
+ import { mockPatient, renderWithContext } from 'tools';
22
+ import { saveEncounter, savePatient } from './patient-registration.resource';
23
+ import { esmPatientRegistrationSchema, type RegistrationConfig } from '../config-schema';
24
+ import { FormManager } from './form-manager';
25
+ import { PatientRegistration } from './patient-registration.component';
26
+ import { useInitialFormValues } from './patient-registration-hooks';
27
+ import { ResourcesContextProvider } from '../resources-context';
28
+
29
+ const mockSaveEncounter = vi.mocked(saveEncounter);
30
+ const mockSavePatient = vi.mocked(savePatient);
31
+ const mockShowSnackbar = vi.mocked(showSnackbar);
32
+ const mockUseConfig = vi.mocked(useConfig<RegistrationConfig>);
33
+ const mockUsePatient = vi.mocked(usePatient);
34
+ const mockUseParams = vi.mocked(useParams);
35
+ const mockUseInitialFormValues = vi.mocked(useInitialFormValues);
36
+
37
+ vi.mock('./field/field.resource', () => ({
38
+ useConcept: vi.fn().mockImplementation((uuid: string) => {
39
+ let data;
40
+ if (uuid === 'weight-uuid') {
41
+ data = {
42
+ uuid: 'weight-uuid',
43
+ display: 'Weight (kg)',
44
+ datatype: { display: 'Numeric', uuid: 'num' },
45
+ answers: [],
46
+ setMembers: [],
47
+ };
48
+ } else if (uuid === 'chief-complaint-uuid') {
49
+ data = {
50
+ uuid: 'chief-complaint-uuid',
51
+ display: 'Chief Complaint',
52
+ datatype: { display: 'Text', uuid: 'txt' },
53
+ answers: [],
54
+ setMembers: [],
55
+ };
56
+ } else if (uuid === 'nationality-uuid') {
57
+ data = {
58
+ uuid: 'nationality-uuid',
59
+ display: 'Nationality',
60
+ datatype: { display: 'Coded', uuid: 'cdd' },
61
+ answers: [
62
+ { display: 'USA', uuid: 'usa' },
63
+ { display: 'Mexico', uuid: 'mex' },
64
+ ],
65
+ setMembers: [],
66
+ };
67
+ }
68
+ return {
69
+ data: data ?? null,
70
+ isLoading: !data,
71
+ };
72
+ }),
73
+ useConceptAnswers: vi.fn().mockImplementation((uuid: string) => {
74
+ if (uuid === 'nationality-uuid') {
75
+ return {
76
+ data: [
77
+ { display: 'USA', uuid: 'usa' },
78
+ { display: 'Mexico', uuid: 'mex' },
79
+ ],
80
+ isLoading: false,
81
+ };
82
+ } else if (uuid === 'other-countries-uuid') {
83
+ return {
84
+ data: [
85
+ { display: 'Kenya', uuid: 'ke' },
86
+ { display: 'Uganda', uuid: 'ug' },
87
+ ],
88
+ isLoading: false,
89
+ };
90
+ }
91
+ }),
92
+ }));
93
+
94
+ vi.mock('react-router-dom', async () => ({
95
+ ...((await vi.importActual('react-router-dom')) as object),
96
+ useLocation: () => ({
97
+ pathname: 'openmrs/spa/patient-registration',
98
+ }),
99
+ useHistory: () => [],
100
+ useParams: vi.fn().mockReturnValue({ patientUuid: undefined }),
101
+ }));
102
+
103
+ vi.mock('./patient-registration.resource', async () => ({
104
+ ...((await vi.importActual('./patient-registration.resource')) as object),
105
+ saveEncounter: vi.fn(),
106
+ savePatient: vi.fn(),
107
+ }));
108
+
109
+ vi.mock('./patient-registration-hooks', async () => {
110
+ const actual = (await vi.importActual('./patient-registration-hooks')) as object;
111
+ const defaultInitialFormValues = {
112
+ additionalFamilyName: '',
113
+ additionalGivenName: '',
114
+ additionalMiddleName: '',
115
+ addNameInLocalLanguage: false,
116
+ address: {},
117
+ birthdate: '',
118
+ birthdateEstimated: false,
119
+ deathCause: '',
120
+ deathDate: undefined,
121
+ deathTime: undefined,
122
+ deathTimeFormat: 'AM',
123
+ familyName: '',
124
+ gender: '',
125
+ givenName: '',
126
+ identifiers: {},
127
+ isDead: false,
128
+ middleName: '',
129
+ monthsEstimated: 0,
130
+ nonCodedCauseOfDeath: '',
131
+ patientUuid: 'new-patient-uuid',
132
+ relationships: [],
133
+ telephoneNumber: '',
134
+ yearsEstimated: 0,
135
+ } as FormValues;
136
+
137
+ return {
138
+ ...actual,
139
+ useInitialFormValues: vi.fn().mockReturnValue([defaultInitialFormValues, vi.fn()]),
140
+ useInitialAddressFieldValues: vi.fn().mockReturnValue([{}, vi.fn()]),
141
+ usePatientUuidMap: vi.fn().mockReturnValue([{}, vi.fn()]),
142
+ };
143
+ });
144
+
145
+ const mockResourcesContextValue = {
146
+ addressTemplate: mockedAddressTemplate as AddressTemplate,
147
+ currentSession: {
148
+ authenticated: true,
149
+ sessionId: 'JSESSION',
150
+ currentProvider: { uuid: 'provider-uuid', identifier: 'PRO-123' },
151
+ },
152
+ relationshipTypes: { results: [] },
153
+ identifierTypes: [],
154
+ };
155
+
156
+ const mockOpenmrsConfig: RegistrationConfig = {
157
+ sections: ['demographics', 'contact'],
158
+ sectionDefinitions: [
159
+ { id: 'demographics', name: 'Demographics', fields: ['name', 'gender', 'dob'] },
160
+ { id: 'contact', name: 'Contact Info', fields: ['address'] },
161
+ { id: 'relationships', name: 'Relationships', fields: ['relationship'] },
162
+ ],
163
+ fieldDefinitions: [],
164
+ fieldConfigurations: {
165
+ phone: {
166
+ personAttributeUuid: '14d4f066-15f5-102d-96e4-000c29c2a5d7',
167
+ },
168
+ dateOfBirth: {
169
+ allowEstimatedDateOfBirth: true,
170
+ useEstimatedDateOfBirth: {
171
+ enabled: true,
172
+ dayOfMonth: new Date().getDate(),
173
+ month: new Date().getMonth(),
174
+ },
175
+ },
176
+ name: {
177
+ displayMiddleName: true,
178
+ allowUnidentifiedPatients: true,
179
+ defaultUnknownGivenName: 'UNKNOWN',
180
+ defaultUnknownFamilyName: 'UNKNOWN',
181
+ displayReverseFieldOrder: false,
182
+ displayCapturePhoto: true,
183
+ },
184
+ gender: [
185
+ {
186
+ value: 'male',
187
+ label: 'Male',
188
+ },
189
+ {
190
+ value: 'female',
191
+ label: 'Female',
192
+ },
193
+ ],
194
+ address: {
195
+ useAddressHierarchy: {
196
+ enabled: true,
197
+ useQuickSearch: true,
198
+ searchAddressByLevel: true,
199
+ },
200
+ },
201
+ causeOfDeath: {
202
+ conceptUuid: 'cause-of-death-concept-uuid',
203
+ },
204
+ },
205
+ links: {
206
+ submitButton: '#',
207
+ },
208
+ defaultPatientIdentifierTypes: [],
209
+ registrationObs: {
210
+ encounterTypeUuid: null,
211
+ encounterProviderRoleUuid: 'asdf',
212
+ registrationFormUuid: null,
213
+ },
214
+ freeTextFieldConceptUuid: '',
215
+ };
216
+ const configWithObs = JSON.parse(JSON.stringify(mockOpenmrsConfig));
217
+
218
+ configWithObs.fieldDefinitions = [
219
+ {
220
+ id: 'weight',
221
+ type: 'obs',
222
+ label: null,
223
+ uuid: 'weight-uuid',
224
+ placeholder: '',
225
+ validation: { required: false, matches: null },
226
+ answerConceptSetUuid: null,
227
+ customConceptAnswers: [],
228
+ },
229
+ {
230
+ id: 'chief complaint',
231
+ type: 'obs',
232
+ label: null,
233
+ uuid: 'chief-complaint-uuid',
234
+ placeholder: '',
235
+ validation: { required: false, matches: null },
236
+ answerConceptSetUuid: null,
237
+ customConceptAnswers: [],
238
+ },
239
+ {
240
+ id: 'nationality',
241
+ type: 'obs',
242
+ label: null,
243
+ uuid: 'nationality-uuid',
244
+ placeholder: '',
245
+ validation: { required: false, matches: null },
246
+ answerConceptSetUuid: null,
247
+ customConceptAnswers: [],
248
+ },
249
+ ];
250
+ configWithObs.sectionDefinitions?.push({
251
+ id: 'custom',
252
+ name: 'Custom',
253
+ fields: ['weight', 'chief complaint', 'nationality'],
254
+ });
255
+ configWithObs.sections.push('custom');
256
+ configWithObs.registrationObs.encounterTypeUuid = 'reg-enc-uuid';
257
+
258
+ const fillRequiredFields = async () => {
259
+ const user = userEvent.setup();
260
+
261
+ const demographicsSection = await screen.findByLabelText('Demographics Section');
262
+ const givenNameInput = within(demographicsSection).getByLabelText(/first/i) as HTMLInputElement;
263
+ const familyNameInput = within(demographicsSection).getByLabelText(/family/i) as HTMLInputElement;
264
+
265
+ await user.type(givenNameInput, 'Paul');
266
+ await user.type(familyNameInput, 'Gaihre');
267
+
268
+ // Set birthdate using the date picker's public API
269
+ let dateOfBirthInput: HTMLInputElement;
270
+ const birthDateString = '1993-08-02';
271
+
272
+ try {
273
+ // Approach 1: Find as textbox (like appointments test)
274
+ dateOfBirthInput = within(demographicsSection).getByRole('textbox', { name: /date of birth/i }) as HTMLInputElement;
275
+ } catch (e) {
276
+ // Approach 2: Find by label
277
+ dateOfBirthInput = within(demographicsSection).getByLabelText(/date of birth/i) as HTMLInputElement;
278
+ }
279
+
280
+ fireEvent.change(dateOfBirthInput, { target: { value: birthDateString } });
281
+ fireEvent.blur(dateOfBirthInput);
282
+
283
+ const genderInput = within(demographicsSection).getByLabelText(/Male/) as HTMLSelectElement;
284
+ await user.click(genderInput);
285
+ };
286
+
287
+ describe('Registering a new patient', () => {
288
+ beforeEach(() => {
289
+ mockUseConfig.mockReturnValue({
290
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
291
+ ...mockOpenmrsConfig,
292
+ });
293
+ mockSavePatient.mockResolvedValue({ data: { uuid: 'new-pt-uuid' }, ok: true } as any);
294
+ });
295
+
296
+ it('should render all the required fields and sections', async () => {
297
+ renderWithContext(
298
+ <PatientRegistration isOffline={false} savePatientForm={vi.fn()} />,
299
+ ResourcesContextProvider,
300
+ mockResourcesContextValue,
301
+ );
302
+
303
+ await screen.findByRole('heading', { name: /create new patient/i });
304
+
305
+ const demographicSection = screen.getByRole('region', { name: /demographics section/i });
306
+ const contactSection = screen.getByRole('region', { name: /contact info section/i });
307
+
308
+ expect(demographicSection).toBeInTheDocument();
309
+ expect(contactSection).toBeInTheDocument();
310
+ expect(screen.getByText(/jump to/i)).toBeInTheDocument();
311
+ expect(within(demographicSection).getByLabelText(/first name/i)).toBeInTheDocument();
312
+ expect(within(demographicSection).getByLabelText(/middle name \(optional\)/i)).toBeInTheDocument();
313
+ expect(within(demographicSection).getByLabelText(/family name/i)).toBeInTheDocument();
314
+ const dateOfBirthInput = within(demographicSection).getByLabelText(/date of birth/i);
315
+ expect(dateOfBirthInput).toBeInTheDocument();
316
+ expect(within(demographicSection).getByRole('radio', { name: /^male$/i })).toBeInTheDocument();
317
+ expect(within(demographicSection).getByRole('radio', { name: /^female$/i })).toBeInTheDocument();
318
+ expect(within(demographicSection).getByText(/date of birth known\?/i)).toBeInTheDocument();
319
+
320
+ expect(within(contactSection).getByRole('heading', { name: /address/i })).toBeInTheDocument();
321
+
322
+ expect(screen.getByRole('button', { name: /register patient/i })).toBeInTheDocument();
323
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
324
+ });
325
+
326
+ it('saves the patient without extra info', async () => {
327
+ const user = userEvent.setup();
328
+
329
+ renderWithContext(
330
+ <PatientRegistration isOffline={false} savePatientForm={FormManager.savePatientFormOnline} />,
331
+ ResourcesContextProvider,
332
+ mockResourcesContextValue,
333
+ );
334
+
335
+ await fillRequiredFields();
336
+ const registerButton = await screen.findByRole('button', { name: /register patient/i });
337
+ await user.click(registerButton);
338
+ expect(mockSavePatient).toHaveBeenCalledWith(
339
+ expect.objectContaining({
340
+ identifiers: [], // Note: Identifier story may need test data update when complete
341
+ person: {
342
+ addresses: expect.arrayContaining([expect.any(Object)]),
343
+ attributes: [],
344
+ birthdate: '1993-8-2',
345
+ birthdateEstimated: false,
346
+ gender: expect.stringMatching(/^M$/),
347
+ names: [{ givenName: 'Paul', middleName: '', familyName: 'Gaihre', preferred: true, uuid: undefined }],
348
+ dead: false,
349
+ uuid: expect.anything(),
350
+ },
351
+ uuid: expect.anything(),
352
+ }),
353
+ undefined,
354
+ );
355
+ });
356
+
357
+ it('should not save the patient if validation fails', async () => {
358
+ const user = userEvent.setup();
359
+ const mockSavePatientForm = vi.fn();
360
+
361
+ renderWithContext(
362
+ <PatientRegistration isOffline={false} savePatientForm={mockSavePatientForm} />,
363
+ ResourcesContextProvider,
364
+ mockResourcesContextValue,
365
+ );
366
+
367
+ await screen.findByRole('heading', { name: /create new patient/i });
368
+ await user.click(screen.getByRole('button', { name: /register patient/i }));
369
+
370
+ expect(mockSavePatientForm).not.toHaveBeenCalled();
371
+ });
372
+
373
+ it('renders and saves registration obs', async () => {
374
+ const user = userEvent.setup();
375
+
376
+ mockSaveEncounter.mockResolvedValue({} as unknown as FetchResponse);
377
+ mockUseConfig.mockReturnValue(configWithObs);
378
+
379
+ renderWithContext(
380
+ <PatientRegistration isOffline={false} savePatientForm={FormManager.savePatientFormOnline} />,
381
+ ResourcesContextProvider,
382
+ mockResourcesContextValue,
383
+ );
384
+
385
+ await fillRequiredFields();
386
+ const customSection = screen.getByLabelText('Custom Section');
387
+ const weight = within(customSection).getByLabelText('Weight (kg) (optional)');
388
+ await user.type(weight, '50');
389
+ const complaint = within(customSection).getByLabelText('Chief Complaint (optional)');
390
+ await user.type(complaint, 'sad');
391
+ const nationality = within(customSection).getByLabelText('Nationality');
392
+ await user.selectOptions(nationality, 'USA');
393
+
394
+ const registerButton = await screen.findByRole('button', { name: /register patient/i });
395
+ await user.click(registerButton);
396
+
397
+ expect(mockSavePatient).toHaveBeenCalled();
398
+
399
+ expect(mockSaveEncounter).toHaveBeenCalledWith(
400
+ expect.objectContaining<Partial<Encounter>>({
401
+ encounterType: 'reg-enc-uuid',
402
+ patient: 'new-pt-uuid',
403
+ obs: [
404
+ { concept: 'weight-uuid', value: 50 },
405
+ { concept: 'chief-complaint-uuid', value: 'sad' },
406
+ { concept: 'nationality-uuid', value: 'usa' },
407
+ ],
408
+ }),
409
+ );
410
+ });
411
+
412
+ it('retries saving registration obs after a failed attempt', async () => {
413
+ const user = userEvent.setup();
414
+
415
+ mockUseConfig.mockReturnValue(configWithObs);
416
+
417
+ renderWithContext(
418
+ <PatientRegistration isOffline={false} savePatientForm={FormManager.savePatientFormOnline} />,
419
+ ResourcesContextProvider,
420
+ mockResourcesContextValue,
421
+ );
422
+
423
+ await fillRequiredFields();
424
+ const customSection = screen.getByLabelText('Custom Section');
425
+ const weight = within(customSection).getByLabelText('Weight (kg) (optional)');
426
+ await user.type(weight, '-999');
427
+
428
+ mockSaveEncounter.mockRejectedValue({ status: 400, responseBody: { error: { message: 'an error message' } } });
429
+
430
+ const registerPatientButton = await screen.findByRole('button', { name: /register patient/i });
431
+
432
+ await user.click(registerPatientButton);
433
+
434
+ expect(mockSavePatient).toHaveBeenCalledTimes(1);
435
+ expect(mockSaveEncounter).toHaveBeenCalledTimes(1);
436
+
437
+ expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ subtitle: 'an error message' }));
438
+ mockSaveEncounter.mockResolvedValue({} as FetchResponse);
439
+
440
+ await user.click(registerPatientButton);
441
+ expect(mockSavePatient).toHaveBeenCalledTimes(2);
442
+ expect(mockSaveEncounter).toHaveBeenCalledTimes(2);
443
+
444
+ expect(mockShowSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
445
+ });
446
+ });
447
+
448
+ describe('Updating an existing patient record', () => {
449
+ beforeEach(() => {
450
+ mockUseConfig.mockReturnValue({
451
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
452
+ ...mockOpenmrsConfig,
453
+ });
454
+ mockUsePatient.mockImplementation(() => {
455
+ return {
456
+ error: null,
457
+ isLoading: false,
458
+ patient: mockPatient,
459
+ patientUuid: mockPatient.id,
460
+ };
461
+ });
462
+ mockSavePatient.mockResolvedValue({ data: { uuid: 'new-pt-uuid' }, ok: true } as any);
463
+ mockUseParams.mockReturnValue({ patientUuid: mockPatient.id });
464
+ });
465
+
466
+ it('edits patient demographics', async () => {
467
+ const user = userEvent.setup();
468
+ const mockSavePatientForm = vi.fn();
469
+
470
+ mockUseInitialFormValues.mockReturnValue([
471
+ {
472
+ additionalFamilyName: '',
473
+ additionalGivenName: '',
474
+ additionalMiddleName: '',
475
+ addNameInLocalLanguage: false,
476
+ address: {},
477
+ birthdate: mockPatient.birthDate,
478
+ birthdateEstimated: false,
479
+ deathCause: '',
480
+ deathDate: undefined,
481
+ deathTime: undefined,
482
+ deathTimeFormat: 'AM',
483
+ familyName: mockPatient.name[0].family,
484
+ gender: mockPatient.gender,
485
+ givenName: mockPatient.name[0].given[0],
486
+ identifiers: {
487
+ openMrsId: {
488
+ autoGeneration: false,
489
+ identifierName: 'OpenMRS ID',
490
+ identifierTypeUuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
491
+ identifierUuid: '1f0ad7a1-430f-4397-b571-59ea654a52db',
492
+ identifierValue: '100GEJ',
493
+ initialValue: '100GEJ',
494
+ preferred: true,
495
+ required: true,
496
+ selectedSource: null,
497
+ },
498
+ idCard: {
499
+ autoGeneration: false,
500
+ identifierName: 'ID Card',
501
+ identifierTypeUuid: 'b4143563-16cd-4439-b288-f83d61670fc8',
502
+ identifierUuid: '346d09b1-8509-43c6-9697-3b4d1ce06ad6',
503
+ identifierValue: '1234567890',
504
+ initialValue: '1234567890',
505
+ preferred: false,
506
+ required: false,
507
+ selectedSource: null,
508
+ },
509
+ },
510
+ isDead: false,
511
+ middleName: '',
512
+ monthsEstimated: 0,
513
+ nonCodedCauseOfDeath: '',
514
+ patientUuid: mockPatient.id,
515
+ relationships: [],
516
+ telephoneNumber: '',
517
+ yearsEstimated: 0,
518
+ } as FormValues,
519
+ vi.fn(),
520
+ ]);
521
+
522
+ renderWithContext(
523
+ <PatientRegistration isOffline={false} savePatientForm={mockSavePatientForm} />,
524
+ ResourcesContextProvider,
525
+ mockResourcesContextValue,
526
+ );
527
+
528
+ await screen.findByRole('heading', { name: /edit patient details/i });
529
+
530
+ expect(screen.queryByRole('button', { name: /register patient/i })).not.toBeInTheDocument();
531
+ expect(screen.getByRole('button', { name: /update patient/i })).toBeInTheDocument();
532
+ expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
533
+
534
+ expect(screen.getByLabelText(/first name/i)).toHaveValue(mockPatient.name[0].given[0]);
535
+ expect(screen.getByLabelText(/family name/i)).toHaveValue(mockPatient.name[0].family);
536
+ expect(
537
+ screen.getByRole('radio', {
538
+ name: /^male$/i,
539
+ }),
540
+ ).toBeChecked();
541
+ expect(
542
+ screen.getByRole('radio', {
543
+ name: /^female$/i,
544
+ }),
545
+ ).not.toBeChecked();
546
+ expect(screen.getAllByRole('tab', { name: /yes/i })).toHaveLength(2);
547
+
548
+ await user.click(screen.getByRole('button', { name: /update patient/i }));
549
+
550
+ expect(mockSavePatientForm).toHaveBeenCalledWith(
551
+ false,
552
+ {
553
+ addNameInLocalLanguage: false,
554
+ additionalFamilyName: '',
555
+ additionalGivenName: '',
556
+ additionalMiddleName: '',
557
+ address: {
558
+ country: 'កម្ពុជា (Cambodia)',
559
+ },
560
+ birthdate: '1972-04-04',
561
+ birthdateEstimated: false,
562
+ deathCause: '',
563
+ nonCodedCauseOfDeath: '',
564
+ deathDate: undefined,
565
+ deathTime: undefined,
566
+ deathTimeFormat: 'AM',
567
+ familyName: 'Wilson',
568
+ gender: 'male',
569
+ givenName: 'John',
570
+ identifiers: {
571
+ idCard: {
572
+ autoGeneration: false,
573
+ identifierName: 'ID Card',
574
+ identifierTypeUuid: 'b4143563-16cd-4439-b288-f83d61670fc8',
575
+ identifierUuid: '346d09b1-8509-43c6-9697-3b4d1ce06ad6',
576
+ identifierValue: '1234567890',
577
+ initialValue: '1234567890',
578
+ preferred: false,
579
+ required: false,
580
+ selectedSource: null,
581
+ },
582
+ openMrsId: {
583
+ autoGeneration: false,
584
+ identifierName: 'OpenMRS ID',
585
+ identifierTypeUuid: '05a29f94-c0ed-11e2-94be-8c13b969e334',
586
+ identifierUuid: '1f0ad7a1-430f-4397-b571-59ea654a52db',
587
+ identifierValue: '100GEJ',
588
+ initialValue: '100GEJ',
589
+ preferred: true,
590
+ required: true,
591
+ selectedSource: null,
592
+ },
593
+ },
594
+ isDead: false,
595
+ middleName: '',
596
+ monthsEstimated: 0,
597
+ patientUuid: '8673ee4f-e2ab-4077-ba55-4980f408773e',
598
+ relationships: [],
599
+ telephoneNumber: '',
600
+ unidentifiedPatient: undefined,
601
+ yearsEstimated: 0,
602
+ },
603
+ expect.anything(),
604
+ expect.anything(),
605
+ null,
606
+ undefined,
607
+ expect.anything(),
608
+ expect.anything(),
609
+ expect.anything(),
610
+ { patientSaved: false },
611
+ expect.anything(),
612
+ );
613
+ });
614
+ });