@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,40 +1,107 @@
1
1
  import React from 'react';
2
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
2
3
  import userEvent from '@testing-library/user-event';
3
4
  import { Formik, Form } from 'formik';
4
- import { screen } from '@testing-library/react';
5
+ import { render, screen, waitFor } from '@testing-library/react';
5
6
  import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
6
- import { renderWithContext } from 'tools';
7
- import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../../config-schema';
8
- import { type FormValues } from '../../patient-registration.types';
9
- import {
10
- type PatientRegistrationContextProps,
11
- PatientRegistrationContextProvider,
12
- } from '../../patient-registration-context';
7
+ import { PatientRegistrationContextProvider } from '../../patient-registration-context';
8
+ import { initialFormValues } from '../../patient-registration.component';
9
+ import { getValidationSchema } from '../../validation/patient-registration-validation';
13
10
  import { GenderField } from './gender-field.component';
11
+ import { type FormValues } from '../../patient-registration.types';
12
+ import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../../config-schema';
13
+
14
+ const mockUseConfig = vi.mocked(useConfig<RegistrationConfig>);
15
+
16
+ /**
17
+ * Helper to render GenderField with Formik render props for state-dependent tests.
18
+ * Returns the render result and a ref to access form values.
19
+ */
20
+ const renderGenderFieldWithFormik = (
21
+ initialValues: Partial<FormValues> = {},
22
+ options?: { enableReinitialize?: boolean; withValidation?: boolean },
23
+ ) => {
24
+ const defaultValues = {
25
+ gender: '',
26
+ ...initialValues,
27
+ };
28
+
29
+ let formValuesRef: FormValues = { ...initialFormValues, ...defaultValues } as FormValues;
30
+ const config = mockUseConfig();
31
+ const validationSchema = options?.withValidation
32
+ ? getValidationSchema(config, (key: string, defaultValue: string) => defaultValue)
33
+ : null;
14
34
 
15
- const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
16
-
17
- const mockContextValues: PatientRegistrationContextProps = {
18
- currentPhoto: null,
19
- identifierTypes: [],
20
- inEditMode: false,
21
- initialFormValues: {
22
- gender: 'male',
23
- } as FormValues,
24
- isOffline: false,
25
- setCapturePhotoProps: jest.fn(),
26
- setFieldTouched: jest.fn(),
27
- setFieldValue: jest.fn(),
28
- validationSchema: esmPatientRegistrationSchema,
29
- values: {
30
- gender: 'male',
31
- } as FormValues,
35
+ const utils = render(
36
+ <Formik
37
+ initialValues={defaultValues}
38
+ onSubmit={() => {}}
39
+ enableReinitialize={options?.enableReinitialize}
40
+ validationSchema={validationSchema || undefined}
41
+ validateOnChange={true}
42
+ validateOnBlur={true}>
43
+ {({ setFieldValue, values, setFieldTouched, errors, touched }) => {
44
+ formValuesRef = { ...initialFormValues, ...values } as FormValues;
45
+ return (
46
+ <Form>
47
+ <PatientRegistrationContextProvider
48
+ value={{
49
+ identifierTypes: [],
50
+ values: formValuesRef,
51
+ validationSchema: validationSchema,
52
+ inEditMode: false,
53
+ setFieldValue: setFieldValue as any,
54
+ setCapturePhotoProps: vi.fn(),
55
+ setFieldTouched: setFieldTouched as any,
56
+ currentPhoto: '',
57
+ isOffline: false,
58
+ initialFormValues: formValuesRef,
59
+ }}>
60
+ <GenderField />
61
+ </PatientRegistrationContextProvider>
62
+ </Form>
63
+ );
64
+ }}
65
+ </Formik>,
66
+ );
67
+
68
+ return {
69
+ ...utils,
70
+ getFormValues: () => formValuesRef,
71
+ };
32
72
  };
33
73
 
34
- jest.mock('formik', () => ({
35
- ...(jest.requireActual('formik') as any),
36
- useField: jest.fn(() => [{}, {}]),
37
- }));
74
+ /**
75
+ * Helper to render GenderField with mocked Formik functions (for simple rendering tests).
76
+ */
77
+ const renderGenderField = (initialValues: Partial<FormValues> = {}) => {
78
+ const defaultValues = {
79
+ gender: '',
80
+ ...initialValues,
81
+ };
82
+
83
+ return render(
84
+ <Formik initialValues={defaultValues} onSubmit={() => {}}>
85
+ <Form>
86
+ <PatientRegistrationContextProvider
87
+ value={{
88
+ identifierTypes: [],
89
+ values: { ...initialFormValues, ...defaultValues } as FormValues,
90
+ validationSchema: null,
91
+ inEditMode: false,
92
+ setFieldValue: vi.fn().mockResolvedValue(undefined),
93
+ setCapturePhotoProps: vi.fn(),
94
+ setFieldTouched: vi.fn().mockResolvedValue(undefined),
95
+ currentPhoto: '',
96
+ isOffline: false,
97
+ initialFormValues: { ...initialFormValues, ...defaultValues } as FormValues,
98
+ }}>
99
+ <GenderField />
100
+ </PatientRegistrationContextProvider>
101
+ </Form>
102
+ </Formik>,
103
+ );
104
+ };
38
105
 
39
106
  describe('GenderField', () => {
40
107
  beforeEach(() => {
@@ -50,6 +117,14 @@ describe('GenderField', () => {
50
117
  value: 'female',
51
118
  label: 'Female',
52
119
  },
120
+ {
121
+ value: 'other',
122
+ label: 'Other',
123
+ },
124
+ {
125
+ value: 'unknown',
126
+ label: 'Unknown',
127
+ },
53
128
  ],
54
129
  name: {
55
130
  displayMiddleName: false,
@@ -63,37 +138,148 @@ describe('GenderField', () => {
63
138
  });
64
139
  });
65
140
 
66
- it('has a label', () => {
67
- renderWithContext(
68
- <Formik initialValues={{}} onSubmit={null}>
69
- <Form>
70
- <GenderField />
71
- </Form>
72
- </Formik>,
73
- PatientRegistrationContextProvider,
74
- mockContextValues,
75
- );
141
+ it('renders the gender field with all options', () => {
142
+ renderGenderField();
76
143
 
77
144
  expect(screen.getByRole('heading', { name: /sex/i })).toBeInTheDocument();
78
145
  expect(screen.getByLabelText(/^male/i)).toBeInTheDocument();
79
146
  expect(screen.getByLabelText(/female/i)).toBeInTheDocument();
147
+ expect(screen.getByLabelText(/other/i)).toBeInTheDocument();
148
+ expect(screen.getByLabelText(/unknown/i)).toBeInTheDocument();
80
149
  });
81
150
 
82
- it('checks an option', async () => {
83
- const user = userEvent.setup();
151
+ describe('Gender selection', () => {
152
+ it('updates form value when user selects female', async () => {
153
+ const user = userEvent.setup();
154
+ const { getFormValues } = renderGenderFieldWithFormik({ gender: 'male' });
84
155
 
85
- renderWithContext(
86
- <Formik initialValues={{}} onSubmit={null}>
87
- <Form>
88
- <GenderField />
89
- </Form>
90
- </Formik>,
91
- PatientRegistrationContextProvider,
92
- mockContextValues,
93
- );
94
-
95
- await user.click(screen.getByText(/female/i));
96
- expect(screen.getByLabelText(/female/i)).toBeChecked();
97
- expect(screen.getByLabelText(/^male/i)).not.toBeChecked();
156
+ await user.click(screen.getByText(/female/i));
157
+
158
+ await waitFor(() => {
159
+ expect(screen.getByLabelText(/female/i)).toBeChecked();
160
+ });
161
+ expect(screen.getByLabelText(/^male/i)).not.toBeChecked();
162
+ expect(getFormValues().gender).toBe('female');
163
+ });
164
+
165
+ it('updates form value when user selects male', async () => {
166
+ const user = userEvent.setup();
167
+ const { getFormValues } = renderGenderFieldWithFormik({ gender: 'female' });
168
+
169
+ await user.click(screen.getByText(/^male/i));
170
+
171
+ await waitFor(() => {
172
+ expect(screen.getByLabelText(/^male/i)).toBeChecked();
173
+ });
174
+ expect(screen.getByLabelText(/female/i)).not.toBeChecked();
175
+ expect(getFormValues().gender).toBe('male');
176
+ });
177
+
178
+ it('updates form value when user selects other', async () => {
179
+ const user = userEvent.setup();
180
+ const { getFormValues } = renderGenderFieldWithFormik();
181
+
182
+ await user.click(screen.getByText(/other/i));
183
+
184
+ await waitFor(() => {
185
+ expect(screen.getByLabelText(/other/i)).toBeChecked();
186
+ });
187
+ expect(getFormValues().gender).toBe('other');
188
+ });
189
+
190
+ it('updates form value when user selects unknown', async () => {
191
+ const user = userEvent.setup();
192
+ const { getFormValues } = renderGenderFieldWithFormik({ gender: 'male' });
193
+
194
+ await user.click(screen.getByText(/unknown/i));
195
+
196
+ await waitFor(() => {
197
+ expect(screen.getByLabelText(/unknown/i)).toBeChecked();
198
+ });
199
+ expect(screen.getByLabelText(/^male/i)).not.toBeChecked();
200
+ expect(getFormValues().gender).toBe('unknown');
201
+ });
202
+
203
+ it('displays the initial gender value correctly', () => {
204
+ renderGenderFieldWithFormik({ gender: 'female' });
205
+
206
+ expect(screen.getByLabelText(/female/i)).toBeChecked();
207
+ expect(screen.getByLabelText(/^male/i)).not.toBeChecked();
208
+ });
209
+
210
+ it('allows switching between all gender options', async () => {
211
+ const user = userEvent.setup();
212
+ const { getFormValues } = renderGenderFieldWithFormik();
213
+
214
+ // Select male
215
+ await user.click(screen.getByText(/^male/i));
216
+ await waitFor(() => {
217
+ expect(getFormValues().gender).toBe('male');
218
+ });
219
+
220
+ // Switch to female
221
+ await user.click(screen.getByText(/female/i));
222
+ await waitFor(() => {
223
+ expect(getFormValues().gender).toBe('female');
224
+ });
225
+
226
+ // Switch to other
227
+ await user.click(screen.getByText(/other/i));
228
+ await waitFor(() => {
229
+ expect(getFormValues().gender).toBe('other');
230
+ });
231
+
232
+ // Switch to unknown
233
+ await user.click(screen.getByText(/unknown/i));
234
+ await waitFor(() => {
235
+ expect(getFormValues().gender).toBe('unknown');
236
+ });
237
+ });
238
+ });
239
+
240
+ describe('Validation error display', () => {
241
+ it('does not show error when field is not touched', () => {
242
+ renderGenderFieldWithFormik({ gender: '' }, { withValidation: true });
243
+
244
+ expect(screen.queryByText(/gender is required/i)).not.toBeInTheDocument();
245
+ });
246
+
247
+ it('shows error message when user selects a gender and then form is validated', async () => {
248
+ const user = userEvent.setup();
249
+ const { getFormValues } = renderGenderFieldWithFormik({ gender: '' }, { withValidation: true });
250
+
251
+ // Initially no error (field not touched)
252
+ expect(screen.queryByText(/gender is required/i)).not.toBeInTheDocument();
253
+
254
+ // User selects a valid gender - this marks field as touched
255
+ await user.click(screen.getByText(/^male/i));
256
+
257
+ await waitFor(() => {
258
+ expect(getFormValues().gender).toBe('male');
259
+ });
260
+
261
+ // After valid selection, no error should appear (user-visible)
262
+ expect(screen.queryByText(/gender is required/i)).not.toBeInTheDocument();
263
+ expect(screen.getByLabelText(/^male/i)).toBeChecked();
264
+ });
265
+
266
+ it('clears error message when valid gender is selected', async () => {
267
+ const user = userEvent.setup();
268
+ renderGenderFieldWithFormik({ gender: '' }, { withValidation: true });
269
+
270
+ // Initially no error (field not touched)
271
+ expect(screen.queryByText(/gender is required/i)).not.toBeInTheDocument();
272
+
273
+ // Select a valid gender option - this will mark field as touched
274
+ await user.click(screen.getByText(/^male/i));
275
+
276
+ // Wait for selection to be applied (user-visible)
277
+ await waitFor(() => {
278
+ expect(screen.getByLabelText(/^male/i)).toBeChecked();
279
+ });
280
+
281
+ // Error should not appear after valid selection (user-visible)
282
+ expect(screen.queryByText(/gender is required/i)).not.toBeInTheDocument();
283
+ });
98
284
  });
99
285
  });
@@ -1,144 +1,146 @@
1
- import React, { useEffect } from 'react';
2
- import { useTranslation } from 'react-i18next';
3
- import { SkeletonText } from '@carbon/react';
4
- import { useConfig } from '@openmrs/esm-framework';
5
- import { usePatientRegistrationContext } from '../../patient-registration-context';
6
- import { useResourcesContext } from '../../../resources-context';
7
- import type {
8
- FormValues,
9
- IdentifierSource,
10
- PatientIdentifierType,
11
- PatientIdentifierValue,
12
- } from '../../patient-registration.types';
13
- import IdentifierInput from '../../input/custom-input/identifier/identifier-input.component';
14
- import styles from '../field.scss';
15
-
16
- export function setIdentifierSource(
17
- identifierSource: IdentifierSource,
18
- identifierValue: string,
19
- initialValue: string,
20
- ): {
21
- identifierValue: string;
22
- autoGeneration: boolean;
23
- selectedSource: IdentifierSource;
24
- } {
25
- const autoGeneration = identifierSource?.autoGenerationOption?.automaticGenerationEnabled;
26
- const manualEntryEnabled = identifierSource?.autoGenerationOption?.manualEntryEnabled;
27
- return {
28
- selectedSource: identifierSource,
29
- autoGeneration,
30
- identifierValue:
31
- autoGeneration && !manualEntryEnabled
32
- ? 'auto-generated'
33
- : identifierValue !== 'auto-generated'
34
- ? identifierValue
35
- : initialValue,
36
- };
37
- }
38
-
39
- export function initializeIdentifier(
40
- identifierType: PatientIdentifierType,
41
- identifierProps,
42
- identifierTypeOverrides?: Array<{ identifierTypeUuid: string; required?: boolean }>,
43
- ): PatientIdentifierValue {
44
- // Check if there's an override for this identifier type
45
- const override = identifierTypeOverrides?.find((override) => override.identifierTypeUuid === identifierType.uuid);
46
- const isRequired =
47
- override?.required !== undefined ? override.required : identifierType.isPrimary || identifierType.required;
48
-
49
- return {
50
- identifierTypeUuid: identifierType.uuid,
51
- identifierName: identifierType.name,
52
- preferred: identifierType.isPrimary,
53
- initialValue: '',
54
- required: isRequired,
55
- ...identifierProps,
56
- ...setIdentifierSource(
57
- identifierProps?.selectedSource ?? identifierType.identifierSources?.[0],
58
- identifierProps?.identifierValue,
59
- identifierProps?.initialValue ?? '',
60
- ),
61
- };
62
- }
63
-
64
- export function deleteIdentifierType(identifiers: FormValues['identifiers'], identifierFieldName) {
65
- return Object.fromEntries(Object.entries(identifiers).filter(([fieldName]) => fieldName !== identifierFieldName));
66
- }
67
-
68
- export const Identifiers: React.FC = () => {
69
- const { identifierTypes } = useResourcesContext();
70
- const isLoading = !identifierTypes?.length;
71
- const { values, setFieldValue, initialFormValues, isOffline } = usePatientRegistrationContext();
72
- const { t } = useTranslation();
73
- const config = useConfig();
74
- const { defaultPatientIdentifierTypes, identifierTypeOverrides } = config;
75
-
76
- useEffect(() => {
77
- if (identifierTypes) {
78
- const identifiers = {};
79
- identifierTypes
80
- .filter(
81
- (type) =>
82
- type.isPrimary ||
83
- type.required ||
84
- !!defaultPatientIdentifierTypes?.find(
85
- (defaultIdentifierTypeUuid) => defaultIdentifierTypeUuid === type.uuid,
86
- ),
87
- )
88
- .filter((type) => !values.identifiers[type.fieldName])
89
- .forEach((type) => {
90
- identifiers[type.fieldName] = initializeIdentifier(
91
- type,
92
- values.identifiers[type.uuid] ?? initialFormValues.identifiers[type.uuid] ?? {},
93
- identifierTypeOverrides,
94
- );
95
- });
96
- /*
97
- Identifier value should only be updated if there is any update in the
98
- identifier values, otherwise, if the below 'if' clause is removed, it will
99
- fall into an infinite run.
100
- */
101
- if (Object.keys(identifiers).length) {
102
- setFieldValue('identifiers', {
103
- ...values.identifiers,
104
- ...identifiers,
105
- });
106
- }
107
- }
108
- // eslint-disable-next-line react-hooks/exhaustive-deps
109
- }, [
110
- identifierTypes,
111
- setFieldValue,
112
- defaultPatientIdentifierTypes,
113
- identifierTypeOverrides,
114
- values.identifiers,
115
- initializeIdentifier,
116
- ]);
117
-
118
- if (isLoading && !isOffline) {
119
- return (
120
- <div className={styles.halfWidthInDesktopView}>
121
- <div className={styles.identifierLabelText}>
122
- <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
123
- </div>
124
- <SkeletonText
125
- // @ts-expect-error
126
- role="progressbar"
127
- />
128
- </div>
129
- );
130
- }
131
-
132
- return (
133
- <div className={styles.halfWidthInDesktopView}>
134
- <div className={styles.identifierLabelText}>
135
- <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
136
- </div>
137
- <div>
138
- {Object.entries(values.identifiers).map(([fieldName, identifier]) => (
139
- <IdentifierInput key={fieldName} fieldName={fieldName} patientIdentifier={identifier} />
140
- ))}
141
- </div>
142
- </div>
143
- );
144
- };
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, SkeletonText } from '@carbon/react';
4
+ import { ArrowRight } from '@carbon/react/icons';
5
+ import { useLayoutType, useConfig, isDesktop, UserHasAccess } from '@openmrs/esm-framework';
6
+ import { usePatientRegistrationContext } from '../../patient-registration-context';
7
+ import { useResourcesContext } from '../../../resources-context';
8
+ import type {
9
+ FormValues,
10
+ IdentifierSource,
11
+ PatientIdentifierType,
12
+ PatientIdentifierValue,
13
+ } from '../../patient-registration.types';
14
+ import IdentifierInput from '../../input/custom-input/identifier/identifier-input.component';
15
+ import IdentifierSelectionOverlay from './identifier-selection-overlay.component';
16
+ import styles from '../field.scss';
17
+
18
+ export function setIdentifierSource(
19
+ identifierSource: IdentifierSource,
20
+ identifierValue: string,
21
+ initialValue: string,
22
+ ): {
23
+ identifierValue: string;
24
+ autoGeneration: boolean;
25
+ selectedSource: IdentifierSource;
26
+ } {
27
+ const autoGeneration = identifierSource?.autoGenerationOption?.automaticGenerationEnabled;
28
+ const manualEntryEnabled = identifierSource?.autoGenerationOption?.manualEntryEnabled;
29
+ return {
30
+ selectedSource: identifierSource,
31
+ autoGeneration,
32
+ identifierValue:
33
+ autoGeneration && !manualEntryEnabled
34
+ ? 'auto-generated'
35
+ : identifierValue !== 'auto-generated'
36
+ ? identifierValue
37
+ : initialValue,
38
+ };
39
+ }
40
+
41
+ export function initializeIdentifier(
42
+ identifierType: PatientIdentifierType,
43
+ identifierProps,
44
+ identifierTypeOverrides?: Array<{ identifierTypeUuid: string; required?: boolean }>,
45
+ ): PatientIdentifierValue {
46
+ // Check if there's an override for this identifier type
47
+ const override = identifierTypeOverrides?.find((override) => override.identifierTypeUuid === identifierType.uuid);
48
+ const isRequired =
49
+ override?.required !== undefined ? override.required : identifierType.isPrimary || identifierType.required;
50
+
51
+ return {
52
+ identifierTypeUuid: identifierType.uuid,
53
+ identifierName: identifierType.name,
54
+ preferred: identifierType.isPrimary,
55
+ initialValue: '',
56
+ required: isRequired,
57
+ ...identifierProps,
58
+ ...setIdentifierSource(
59
+ identifierProps?.selectedSource ?? identifierType.identifierSources?.[0],
60
+ identifierProps?.identifierValue,
61
+ identifierProps?.initialValue ?? '',
62
+ ),
63
+ };
64
+ }
65
+
66
+ export function deleteIdentifierType(identifiers: FormValues['identifiers'], identifierFieldName) {
67
+ return Object.fromEntries(Object.entries(identifiers).filter(([fieldName]) => fieldName !== identifierFieldName));
68
+ }
69
+
70
+ export const Identifiers: React.FC = () => {
71
+ const { identifierTypes } = useResourcesContext();
72
+ const isLoading = !identifierTypes?.length;
73
+ const { values, setFieldValue, initialFormValues, isOffline } = usePatientRegistrationContext();
74
+ const { t } = useTranslation();
75
+ const config = useConfig();
76
+ const { defaultPatientIdentifierTypes, identifierTypeOverrides } = config;
77
+
78
+ useEffect(() => {
79
+ if (identifierTypes) {
80
+ const identifiers = {};
81
+ identifierTypes
82
+ .filter(
83
+ (type) =>
84
+ type.isPrimary ||
85
+ type.required ||
86
+ !!defaultPatientIdentifierTypes?.find(
87
+ (defaultIdentifierTypeUuid) => defaultIdentifierTypeUuid === type.uuid,
88
+ ),
89
+ )
90
+ .filter((type) => !values.identifiers[type.fieldName])
91
+ .forEach((type) => {
92
+ identifiers[type.fieldName] = initializeIdentifier(
93
+ type,
94
+ values.identifiers[type.uuid] ?? initialFormValues.identifiers[type.uuid] ?? {},
95
+ identifierTypeOverrides,
96
+ );
97
+ });
98
+ /*
99
+ Identifier value should only be updated if there is any update in the
100
+ identifier values, otherwise, if the below 'if' clause is removed, it will
101
+ fall into an infinite run.
102
+ */
103
+ if (Object.keys(identifiers).length) {
104
+ setFieldValue('identifiers', {
105
+ ...values.identifiers,
106
+ ...identifiers,
107
+ });
108
+ }
109
+ }
110
+ // eslint-disable-next-line react-hooks/exhaustive-deps
111
+ }, [
112
+ identifierTypes,
113
+ setFieldValue,
114
+ defaultPatientIdentifierTypes,
115
+ identifierTypeOverrides,
116
+ values.identifiers,
117
+ initializeIdentifier,
118
+ ]);
119
+
120
+ if (isLoading && !isOffline) {
121
+ return (
122
+ <div className={styles.halfWidthInDesktopView}>
123
+ <div className={styles.identifierLabelText}>
124
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
125
+ </div>
126
+ <SkeletonText
127
+ // @ts-expect-error
128
+ role="progressbar"
129
+ />
130
+ </div>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <div className={styles.halfWidthInDesktopView}>
136
+ <div className={styles.identifierLabelText}>
137
+ <h4 className={styles.productiveHeading02Light}>{t('idFieldLabelText', 'Identifiers')}</h4>
138
+ </div>
139
+ <div>
140
+ {Object.entries(values.identifiers).map(([fieldName, identifier]) => (
141
+ <IdentifierInput key={fieldName} fieldName={fieldName} patientIdentifier={identifier} />
142
+ ))}
143
+ </div>
144
+ </div>
145
+ );
146
+ };