@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
@@ -0,0 +1,346 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect, test, beforeEach, type Mock } from 'vitest';
3
+ import { Form, Formik } from 'formik';
4
+ import { screen, waitFor } from '@testing-library/react';
5
+ import userEvent from '@testing-library/user-event';
6
+ import { getDefaultsFromConfigSchema, useConfig, useLayoutType, isDesktop } from '@openmrs/esm-framework';
7
+ import { mockPatient, mockSession } from '__mocks__';
8
+ import { renderWithContext } from 'tools';
9
+ import { esmPatientRegistrationSchema, type RegistrationConfig } from '../../../config-schema';
10
+ import {
11
+ PatientRegistrationContextProvider,
12
+ type PatientRegistrationContextProps,
13
+ } from '../../patient-registration-context';
14
+ import { ResourcesContextProvider } from '../../../resources-context';
15
+ import PatientIdentifierOverlay from './identifier-selection-overlay.component';
16
+ import type { AddressTemplate, FormValues, PatientIdentifierType } from '../../patient-registration.types';
17
+ import type { Resources } from '../../../offline.resources';
18
+
19
+ const mockUseConfig = vi.mocked(useConfig<RegistrationConfig>);
20
+ const mockUseLayoutType = vi.mocked(useLayoutType);
21
+ const mockIsDesktop = vi.mocked(isDesktop);
22
+
23
+ // Mock identifier types
24
+ const mockIdentifierTypes: PatientIdentifierType[] = [
25
+ {
26
+ uuid: 'openmrs-id-uuid',
27
+ name: 'OpenMRS ID',
28
+ fieldName: 'openMrsId',
29
+ format: '^[0-9]{5,6}$',
30
+ formatDescription: 'Format: 5-6 digits',
31
+ required: true,
32
+ isPrimary: true,
33
+ uniquenessBehavior: 'UNIQUE',
34
+ identifierSources: [
35
+ {
36
+ uuid: 'source-1-uuid',
37
+ name: 'Auto-generated',
38
+ autoGenerationOption: {
39
+ automaticGenerationEnabled: true,
40
+ manualEntryEnabled: false,
41
+ },
42
+ },
43
+ ],
44
+ },
45
+ {
46
+ uuid: 'passport-id-uuid',
47
+ name: 'Passport Number',
48
+ fieldName: 'passportId',
49
+ format: '',
50
+ required: false,
51
+ isPrimary: false,
52
+ uniquenessBehavior: 'NON_UNIQUE',
53
+ identifierSources: [
54
+ {
55
+ uuid: 'source-2-uuid',
56
+ name: 'Manual Entry',
57
+ autoGenerationOption: {
58
+ automaticGenerationEnabled: false,
59
+ manualEntryEnabled: true,
60
+ },
61
+ },
62
+ ],
63
+ },
64
+ {
65
+ uuid: 'national-id-uuid',
66
+ name: 'National ID',
67
+ fieldName: 'nationalId',
68
+ format: '',
69
+ required: false,
70
+ isPrimary: false,
71
+ uniquenessBehavior: 'NON_UNIQUE',
72
+ identifierSources: [],
73
+ },
74
+ ];
75
+
76
+ const mockResourcesContextValue: Resources = {
77
+ addressTemplate: {} as AddressTemplate,
78
+ currentSession: mockSession.data,
79
+ identifierTypes: mockIdentifierTypes,
80
+ relationshipTypes: { results: [] },
81
+ } as Resources;
82
+
83
+ const mockInitialFormValues: FormValues = {
84
+ additionalFamilyName: '',
85
+ additionalGivenName: '',
86
+ additionalMiddleName: '',
87
+ addNameInLocalLanguage: false,
88
+ address: {},
89
+ birthdate: null,
90
+ birthdateEstimated: false,
91
+ deathCause: '',
92
+ deathDate: '',
93
+ deathTime: '',
94
+ deathTimeFormat: 'AM' as 'AM' | 'PM',
95
+ nonCodedCauseOfDeath: '',
96
+ familyName: 'Doe',
97
+ gender: 'male',
98
+ givenName: 'John',
99
+ identifiers: {},
100
+ isDead: false,
101
+ middleName: 'Test',
102
+ monthsEstimated: 0,
103
+ patientUuid: mockPatient.uuid,
104
+ relationships: [],
105
+ telephoneNumber: '',
106
+ yearsEstimated: 0,
107
+ };
108
+
109
+ const mockContextValues: PatientRegistrationContextProps = {
110
+ currentPhoto: null,
111
+ inEditMode: false,
112
+ identifierTypes: [],
113
+ initialFormValues: mockInitialFormValues,
114
+ isOffline: false,
115
+ setCapturePhotoProps: vi.fn(),
116
+ setFieldValue: vi.fn(),
117
+ setInitialFormValues: vi.fn(),
118
+ validationSchema: null,
119
+ values: mockInitialFormValues,
120
+ } as unknown as PatientRegistrationContextProps;
121
+
122
+ /**
123
+ * Helper to render PatientIdentifierOverlay with Formik.
124
+ */
125
+ function renderIdentifierSelectionOverlay(
126
+ contextValues: PatientRegistrationContextProps = mockContextValues,
127
+ resourcesContextValue: Resources = mockResourcesContextValue,
128
+ ) {
129
+ const mockSetFieldValue = vi.fn();
130
+ const mockCloseOverlay = vi.fn();
131
+
132
+ const utils = renderWithContext(
133
+ <Formik initialValues={{}} onSubmit={() => {}}>
134
+ <Form>
135
+ <PatientRegistrationContextProvider value={contextValues}>
136
+ <PatientIdentifierOverlay setFieldValue={mockSetFieldValue} closeOverlay={mockCloseOverlay} />
137
+ </PatientRegistrationContextProvider>
138
+ </Form>
139
+ </Formik>,
140
+ ResourcesContextProvider,
141
+ resourcesContextValue,
142
+ );
143
+
144
+ return {
145
+ ...utils,
146
+ mockSetFieldValue,
147
+ mockCloseOverlay,
148
+ };
149
+ }
150
+
151
+ describe('PatientIdentifierOverlay component', () => {
152
+ beforeEach(() => {
153
+ mockIsDesktop.mockReturnValue(true);
154
+ mockUseLayoutType.mockReturnValue('desktop' as any);
155
+ mockUseConfig.mockReturnValue({
156
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
157
+ defaultPatientIdentifierTypes: [],
158
+ });
159
+ });
160
+
161
+ describe('Rendering', () => {
162
+ it('renders overlay with header', () => {
163
+ renderIdentifierSelectionOverlay();
164
+
165
+ // Header text appears in the overlay header (may appear in both header and button)
166
+ const headers = screen.queryAllByText(/configure identifiers/i);
167
+ expect(headers.length).toBeGreaterThan(0);
168
+ });
169
+
170
+ it('renders instruction text', () => {
171
+ renderIdentifierSelectionOverlay();
172
+
173
+ expect(screen.getByText(/select the identifiers you'd like to add for this patient/i)).toBeInTheDocument();
174
+ });
175
+
176
+ it('renders all identifier types as checkboxes', () => {
177
+ renderIdentifierSelectionOverlay();
178
+
179
+ expect(screen.getByLabelText('OpenMRS ID')).toBeInTheDocument();
180
+ expect(screen.getByLabelText('Passport Number')).toBeInTheDocument();
181
+ expect(screen.getByLabelText('National ID')).toBeInTheDocument();
182
+ });
183
+
184
+ it('renders search input when there are more than 7 identifier types', () => {
185
+ const manyIdentifierTypes = Array.from({ length: 8 }, (_, i) => ({
186
+ ...mockIdentifierTypes[0],
187
+ uuid: `uuid-${i}`,
188
+ name: `Identifier ${i}`,
189
+ fieldName: `identifier${i}`,
190
+ required: false,
191
+ isPrimary: false,
192
+ }));
193
+
194
+ renderIdentifierSelectionOverlay(mockContextValues, {
195
+ ...mockResourcesContextValue,
196
+ identifierTypes: manyIdentifierTypes,
197
+ });
198
+
199
+ // Search input has both labelText and placeholder with same text, so use getAllByLabelText
200
+ expect(screen.getAllByLabelText(/search identifier/i).length).toBeGreaterThan(0);
201
+ });
202
+
203
+ it('does not render search input when there are 7 or fewer identifier types', () => {
204
+ renderIdentifierSelectionOverlay();
205
+
206
+ expect(screen.queryByRole('searchbox', { name: /search identifier/i })).not.toBeInTheDocument();
207
+ });
208
+ });
209
+
210
+ describe('User interaction', () => {
211
+ it('allows selecting an identifier type', async () => {
212
+ const user = userEvent.setup();
213
+ const { mockSetFieldValue } = renderIdentifierSelectionOverlay();
214
+
215
+ const passportCheckbox = screen.getByLabelText('Passport Number');
216
+ await user.click(passportCheckbox);
217
+
218
+ expect(passportCheckbox).toBeChecked();
219
+ });
220
+
221
+ it('allows deselecting an identifier type', async () => {
222
+ const user = userEvent.setup();
223
+ renderIdentifierSelectionOverlay();
224
+
225
+ const passportCheckbox = screen.getByLabelText('Passport Number');
226
+ await user.click(passportCheckbox);
227
+ expect(passportCheckbox).toBeChecked();
228
+
229
+ await user.click(passportCheckbox);
230
+ expect(passportCheckbox).not.toBeChecked();
231
+ });
232
+
233
+ it('displays identifier source options when identifier is selected and has sources', async () => {
234
+ const user = userEvent.setup();
235
+ renderIdentifierSelectionOverlay();
236
+
237
+ const passportCheckbox = screen.getByLabelText('Passport Number');
238
+ await user.click(passportCheckbox);
239
+
240
+ await waitFor(() => {
241
+ expect(screen.getByText(/source/i)).toBeInTheDocument();
242
+ });
243
+ expect(screen.getByLabelText('Manual Entry')).toBeInTheDocument();
244
+ });
245
+
246
+ it('allows selecting identifier source', async () => {
247
+ const user = userEvent.setup();
248
+ renderIdentifierSelectionOverlay();
249
+
250
+ const passportCheckbox = screen.getByLabelText('Passport Number');
251
+ await user.click(passportCheckbox);
252
+
253
+ await waitFor(() => {
254
+ const sourceRadio = screen.getByLabelText('Manual Entry');
255
+ expect(sourceRadio).toBeInTheDocument();
256
+ });
257
+
258
+ const sourceRadio = screen.getByLabelText('Manual Entry');
259
+ await user.click(sourceRadio);
260
+
261
+ expect(sourceRadio).toBeChecked();
262
+ });
263
+
264
+ it('filters identifier types when searching', async () => {
265
+ const user = userEvent.setup();
266
+ const manyIdentifierTypes = Array.from({ length: 8 }, (_, i) => {
267
+ if (i < 3) {
268
+ return mockIdentifierTypes[i];
269
+ }
270
+ return {
271
+ ...mockIdentifierTypes[0],
272
+ uuid: `test-uuid-${i}`,
273
+ name: `Test Identifier ${i}`,
274
+ fieldName: `testId${i}`,
275
+ required: false,
276
+ isPrimary: false,
277
+ };
278
+ });
279
+
280
+ renderIdentifierSelectionOverlay(mockContextValues, {
281
+ ...mockResourcesContextValue,
282
+ identifierTypes: manyIdentifierTypes,
283
+ });
284
+
285
+ // Search input has both labelText and placeholder with same text, get the first one
286
+ const searchInputs = screen.getAllByLabelText(/search identifier/i);
287
+ const searchInput = searchInputs[0];
288
+ await user.type(searchInput, 'Passport');
289
+
290
+ expect(screen.getByLabelText('Passport Number')).toBeInTheDocument();
291
+ expect(screen.queryByLabelText('Test Identifier 3')).not.toBeInTheDocument();
292
+ });
293
+
294
+ it('calls setFieldValue and closeOverlay when Configure button is clicked', async () => {
295
+ const user = userEvent.setup();
296
+ const { mockSetFieldValue, mockCloseOverlay } = renderIdentifierSelectionOverlay();
297
+
298
+ const passportCheckbox = screen.getByLabelText('Passport Number');
299
+ await user.click(passportCheckbox);
300
+
301
+ const configureButton = screen.getByRole('button', { name: /configure identifiers/i });
302
+ await user.click(configureButton);
303
+
304
+ expect(mockSetFieldValue).toHaveBeenCalledWith('identifiers', expect.any(Object));
305
+ expect(mockCloseOverlay).toHaveBeenCalledTimes(1);
306
+ });
307
+
308
+ it('calls closeOverlay when Cancel button is clicked', async () => {
309
+ const user = userEvent.setup();
310
+ const { mockCloseOverlay } = renderIdentifierSelectionOverlay();
311
+
312
+ const cancelButton = screen.getByRole('button', { name: /cancel/i });
313
+ await user.click(cancelButton);
314
+
315
+ expect(mockCloseOverlay).toHaveBeenCalledTimes(1);
316
+ });
317
+ });
318
+
319
+ describe('Disabled states', () => {
320
+ it('disables primary identifier type checkbox', () => {
321
+ renderIdentifierSelectionOverlay();
322
+
323
+ const openmrsIdCheckbox = screen.getByLabelText('OpenMRS ID');
324
+ expect(openmrsIdCheckbox).toBeDisabled();
325
+ });
326
+
327
+ it('disables required identifier type checkbox', () => {
328
+ renderIdentifierSelectionOverlay();
329
+
330
+ const openmrsIdCheckbox = screen.getByLabelText('OpenMRS ID');
331
+ expect(openmrsIdCheckbox).toBeDisabled();
332
+ });
333
+
334
+ it('disables identifier type that is in defaultPatientIdentifierTypes', () => {
335
+ mockUseConfig.mockReturnValue({
336
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
337
+ defaultPatientIdentifierTypes: ['passport-id-uuid'],
338
+ });
339
+
340
+ renderIdentifierSelectionOverlay();
341
+
342
+ const passportCheckbox = screen.getByLabelText('Passport Number');
343
+ expect(passportCheckbox).toBeDisabled();
344
+ });
345
+ });
346
+ });
@@ -122,8 +122,8 @@ export const NameField = () => {
122
122
  size="md"
123
123
  selectedIndex={isPatientUnknown ? 1 : 0}
124
124
  onChange={toggleNameKnown}>
125
- <Switch name="known" text={t('yes', 'Yes')} />
126
- <Switch name="unknown" text={t('no', 'No')} />
125
+ <Switch name="known">{t('yes', 'Yes')}</Switch>
126
+ <Switch name="unknown">{t('no', 'No')}</Switch>
127
127
  </ContentSwitcher>
128
128
  </>
129
129
  )}
@@ -0,0 +1,282 @@
1
+ import React from 'react';
2
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
3
+ import userEvent from '@testing-library/user-event';
4
+ import { Formik, Form } from 'formik';
5
+ import { render, screen, waitFor } from '@testing-library/react';
6
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
7
+ import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../../config-schema';
8
+ import { PatientRegistrationContextProvider } from '../../patient-registration-context';
9
+ import { initialFormValues } from '../../patient-registration.component';
10
+ import { NameField, unidentifiedPatientAttributeTypeUuid } from './name-field.component';
11
+ import { type FormValues } from '../../patient-registration.types';
12
+
13
+ const mockUseConfig = vi.mocked(useConfig<RegistrationConfig>);
14
+
15
+ /**
16
+ * Helper to render NameField with Formik render props for state-dependent tests.
17
+ */
18
+ const renderNameFieldWithFormik = (
19
+ initialValues: Partial<FormValues> = {},
20
+ options?: { enableReinitialize?: boolean },
21
+ ) => {
22
+ const defaultValues: Partial<FormValues> = {
23
+ givenName: '',
24
+ familyName: '',
25
+ middleName: '',
26
+ attributes: {
27
+ [unidentifiedPatientAttributeTypeUuid]: 'false',
28
+ ...(initialValues.attributes || {}),
29
+ },
30
+ ...initialValues,
31
+ };
32
+
33
+ let formValuesRef: FormValues = { ...initialFormValues, ...defaultValues } as FormValues;
34
+
35
+ const utils = render(
36
+ <Formik initialValues={defaultValues} onSubmit={() => {}} enableReinitialize={options?.enableReinitialize}>
37
+ {({ setFieldValue, values, setFieldTouched }) => {
38
+ formValuesRef = { ...initialFormValues, ...values } as FormValues;
39
+ return (
40
+ <Form>
41
+ <PatientRegistrationContextProvider
42
+ value={{
43
+ identifierTypes: [],
44
+ values: formValuesRef,
45
+ validationSchema: null,
46
+ inEditMode: false,
47
+ setFieldValue: setFieldValue as any,
48
+ setCapturePhotoProps: vi.fn(),
49
+ setFieldTouched: setFieldTouched as any,
50
+ currentPhoto: '',
51
+ isOffline: false,
52
+ initialFormValues: formValuesRef,
53
+ }}>
54
+ <NameField />
55
+ </PatientRegistrationContextProvider>
56
+ </Form>
57
+ );
58
+ }}
59
+ </Formik>,
60
+ );
61
+
62
+ return {
63
+ ...utils,
64
+ getFormValues: () => formValuesRef,
65
+ };
66
+ };
67
+
68
+ describe('NameField', () => {
69
+ beforeEach(() => {
70
+ mockUseConfig.mockReturnValue({
71
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
72
+ fieldConfigurations: {
73
+ name: {
74
+ displayCapturePhoto: false,
75
+ allowUnidentifiedPatients: true,
76
+ defaultUnknownGivenName: 'UNKNOWN',
77
+ defaultUnknownFamilyName: 'UNKNOWN',
78
+ displayMiddleName: true,
79
+ displayReverseFieldOrder: false,
80
+ },
81
+ } as RegistrationConfig['fieldConfigurations'],
82
+ });
83
+ });
84
+
85
+ it('renders the name field with all inputs', () => {
86
+ renderNameFieldWithFormik();
87
+
88
+ expect(screen.getByRole('heading', { name: /full name/i })).toBeInTheDocument();
89
+ expect(screen.getByLabelText(/first name/i)).toBeInTheDocument();
90
+ expect(screen.getByLabelText(/family name/i)).toBeInTheDocument();
91
+ expect(screen.getByLabelText(/middle name/i)).toBeInTheDocument();
92
+ });
93
+
94
+ describe('Unidentified patient toggle', () => {
95
+ it('shows toggle when allowUnidentifiedPatients is enabled', () => {
96
+ renderNameFieldWithFormik();
97
+
98
+ expect(screen.getByText(/patient's name is known\?/i)).toBeInTheDocument();
99
+ expect(screen.getByRole('tab', { name: /yes/i })).toBeInTheDocument();
100
+ expect(screen.getByRole('tab', { name: /no/i })).toBeInTheDocument();
101
+ });
102
+
103
+ it('hides name inputs when patient is unknown', async () => {
104
+ const user = userEvent.setup();
105
+ renderNameFieldWithFormik({
106
+ attributes: {
107
+ [unidentifiedPatientAttributeTypeUuid]: 'false',
108
+ },
109
+ });
110
+
111
+ await user.click(screen.getByRole('tab', { name: /no/i }));
112
+
113
+ await waitFor(() => {
114
+ expect(screen.queryByLabelText(/first name/i)).not.toBeInTheDocument();
115
+ });
116
+ expect(screen.queryByLabelText(/family name/i)).not.toBeInTheDocument();
117
+ });
118
+
119
+ it('shows name inputs when patient is known', () => {
120
+ renderNameFieldWithFormik({
121
+ attributes: {
122
+ [unidentifiedPatientAttributeTypeUuid]: 'false',
123
+ },
124
+ });
125
+
126
+ expect(screen.getByLabelText(/first name/i)).toBeInTheDocument();
127
+ expect(screen.getByLabelText(/family name/i)).toBeInTheDocument();
128
+ });
129
+
130
+ it('sets unknown name values when user selects unknown', async () => {
131
+ const user = userEvent.setup();
132
+ const { getFormValues } = renderNameFieldWithFormik(
133
+ {
134
+ attributes: {
135
+ [unidentifiedPatientAttributeTypeUuid]: 'false',
136
+ },
137
+ givenName: 'John',
138
+ familyName: 'Doe',
139
+ },
140
+ { enableReinitialize: true },
141
+ );
142
+
143
+ await user.click(screen.getByRole('tab', { name: /no/i }));
144
+
145
+ // Wait for UI to update - name inputs should be hidden
146
+ await waitFor(() => {
147
+ expect(screen.queryByLabelText(/first name/i)).not.toBeInTheDocument();
148
+ });
149
+
150
+ // Verify name values were set to UNKNOWN (user-visible through form state)
151
+ await waitFor(() => {
152
+ const formValues = getFormValues();
153
+ expect(formValues.givenName).toBe('UNKNOWN');
154
+ });
155
+ const formValues = getFormValues();
156
+ expect(formValues.familyName).toBe('UNKNOWN');
157
+ });
158
+
159
+ it('shows name inputs when user switches from unknown to known', async () => {
160
+ const user = userEvent.setup();
161
+ const { getFormValues } = renderNameFieldWithFormik(
162
+ {
163
+ attributes: {
164
+ [unidentifiedPatientAttributeTypeUuid]: 'true',
165
+ },
166
+ givenName: 'UNKNOWN',
167
+ familyName: 'UNKNOWN',
168
+ },
169
+ { enableReinitialize: true },
170
+ );
171
+
172
+ // Verify initial state - patient is unknown
173
+ const initialValues = getFormValues();
174
+ expect(initialValues.attributes?.[unidentifiedPatientAttributeTypeUuid]).toBe('true');
175
+
176
+ // When patient is unknown, name inputs should be hidden (component logic: !isPatientUnknown)
177
+ // But the component may render them initially before the attribute value is processed
178
+ // Let's just verify the toggle works
179
+ await user.click(screen.getByRole('tab', { name: /yes/i }));
180
+
181
+ // Wait for UI to update - name inputs should appear (user-visible behavior)
182
+ await waitFor(() => {
183
+ expect(screen.getByLabelText(/first name/i)).toBeInTheDocument();
184
+ });
185
+ expect(screen.getByLabelText(/family name/i)).toBeInTheDocument();
186
+ });
187
+ });
188
+
189
+ describe('Name input', () => {
190
+ it('allows user to enter first name', async () => {
191
+ const user = userEvent.setup();
192
+ const { getFormValues } = renderNameFieldWithFormik();
193
+
194
+ const firstNameInput = screen.getByLabelText(/first name/i) as HTMLInputElement;
195
+ await user.type(firstNameInput, 'John');
196
+
197
+ await waitFor(() => {
198
+ expect(getFormValues().givenName).toBe('John');
199
+ });
200
+ });
201
+
202
+ it('allows user to enter family name', async () => {
203
+ const user = userEvent.setup();
204
+ const { getFormValues } = renderNameFieldWithFormik();
205
+
206
+ const familyNameInput = screen.getByLabelText(/family name/i) as HTMLInputElement;
207
+ await user.type(familyNameInput, 'Doe');
208
+
209
+ await waitFor(() => {
210
+ expect(getFormValues().familyName).toBe('Doe');
211
+ });
212
+ });
213
+
214
+ it('allows user to enter middle name when displayMiddleName is enabled', async () => {
215
+ const user = userEvent.setup();
216
+ const { getFormValues } = renderNameFieldWithFormik();
217
+
218
+ const middleNameInput = screen.getByLabelText(/middle name/i) as HTMLInputElement;
219
+ await user.type(middleNameInput, 'Michael');
220
+
221
+ await waitFor(() => {
222
+ expect(getFormValues().middleName).toBe('Michael');
223
+ });
224
+ });
225
+
226
+ it('hides middle name when displayMiddleName is disabled', () => {
227
+ mockUseConfig.mockReturnValue({
228
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
229
+ fieldConfigurations: {
230
+ name: {
231
+ displayCapturePhoto: false,
232
+ allowUnidentifiedPatients: true,
233
+ defaultUnknownGivenName: 'UNKNOWN',
234
+ defaultUnknownFamilyName: 'UNKNOWN',
235
+ displayMiddleName: false,
236
+ displayReverseFieldOrder: false,
237
+ },
238
+ } as RegistrationConfig['fieldConfigurations'],
239
+ });
240
+
241
+ renderNameFieldWithFormik();
242
+
243
+ expect(screen.queryByLabelText(/middle name/i)).not.toBeInTheDocument();
244
+ });
245
+ });
246
+
247
+ describe('Field order', () => {
248
+ it('renders fields in default order when displayReverseFieldOrder is false', () => {
249
+ renderNameFieldWithFormik();
250
+
251
+ const inputs = screen.getAllByRole('textbox');
252
+ const firstNameIndex = inputs.findIndex((input) => (input as HTMLInputElement).id === 'givenName');
253
+ const familyNameIndex = inputs.findIndex((input) => (input as HTMLInputElement).id === 'familyName');
254
+
255
+ expect(firstNameIndex).toBeLessThan(familyNameIndex);
256
+ });
257
+
258
+ it('renders fields in reverse order when displayReverseFieldOrder is true', () => {
259
+ mockUseConfig.mockReturnValue({
260
+ ...getDefaultsFromConfigSchema(esmPatientRegistrationSchema),
261
+ fieldConfigurations: {
262
+ name: {
263
+ displayCapturePhoto: false,
264
+ allowUnidentifiedPatients: true,
265
+ defaultUnknownGivenName: 'UNKNOWN',
266
+ defaultUnknownFamilyName: 'UNKNOWN',
267
+ displayMiddleName: true,
268
+ displayReverseFieldOrder: true,
269
+ },
270
+ } as RegistrationConfig['fieldConfigurations'],
271
+ });
272
+
273
+ renderNameFieldWithFormik();
274
+
275
+ const inputs = screen.getAllByRole('textbox');
276
+ const familyNameIndex = inputs.findIndex((input) => (input as HTMLInputElement).id === 'familyName');
277
+ const firstNameIndex = inputs.findIndex((input) => (input as HTMLInputElement).id === 'givenName');
278
+
279
+ expect(familyNameIndex).toBeLessThan(firstNameIndex);
280
+ });
281
+ });
282
+ });