@ampath/esm-laboratory-app 1.3.0-next.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 (251) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +68 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +12 -0
  7. package/.prettierrc +8 -0
  8. package/.tx/config +11 -0
  9. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  10. package/README.md +54 -0
  11. package/__mocks__/react-i18next.js +50 -0
  12. package/assets/screenshots/labs_enter_results.png +0 -0
  13. package/assets/screenshots/labs_general_dashboard.png +0 -0
  14. package/dist/1119.js +1 -0
  15. package/dist/1197.js +1 -0
  16. package/dist/1222.js +1 -0
  17. package/dist/1222.js.map +1 -0
  18. package/dist/1243.js +1 -0
  19. package/dist/1243.js.map +1 -0
  20. package/dist/2146.js +1 -0
  21. package/dist/2690.js +1 -0
  22. package/dist/3099.js +1 -0
  23. package/dist/3106.js +1 -0
  24. package/dist/3106.js.map +1 -0
  25. package/dist/312.js +1 -0
  26. package/dist/312.js.map +1 -0
  27. package/dist/3352.js +1 -0
  28. package/dist/3352.js.map +1 -0
  29. package/dist/3535.js +1 -0
  30. package/dist/3535.js.map +1 -0
  31. package/dist/3584.js +1 -0
  32. package/dist/4044.js +1 -0
  33. package/dist/4044.js.map +1 -0
  34. package/dist/4055.js +1 -0
  35. package/dist/4132.js +1 -0
  36. package/dist/4300.js +1 -0
  37. package/dist/4335.js +1 -0
  38. package/dist/439.js +1 -0
  39. package/dist/4535.js +1 -0
  40. package/dist/4535.js.map +1 -0
  41. package/dist/4618.js +1 -0
  42. package/dist/4652.js +1 -0
  43. package/dist/4748.js +2 -0
  44. package/dist/4748.js.LICENSE.txt +9 -0
  45. package/dist/4748.js.map +1 -0
  46. package/dist/4920.js +1 -0
  47. package/dist/4920.js.map +1 -0
  48. package/dist/4944.js +1 -0
  49. package/dist/5048.js +2 -0
  50. package/dist/5048.js.LICENSE.txt +29 -0
  51. package/dist/5048.js.map +1 -0
  52. package/dist/5088.js +1 -0
  53. package/dist/5088.js.map +1 -0
  54. package/dist/5173.js +1 -0
  55. package/dist/5241.js +1 -0
  56. package/dist/53.js +1 -0
  57. package/dist/53.js.map +1 -0
  58. package/dist/5339.js +1 -0
  59. package/dist/5339.js.map +1 -0
  60. package/dist/5348.js +1 -0
  61. package/dist/5348.js.map +1 -0
  62. package/dist/5380.js +1 -0
  63. package/dist/5380.js.map +1 -0
  64. package/dist/5442.js +1 -0
  65. package/dist/5661.js +1 -0
  66. package/dist/5780.js +2 -0
  67. package/dist/5780.js.LICENSE.txt +9 -0
  68. package/dist/5780.js.map +1 -0
  69. package/dist/6022.js +1 -0
  70. package/dist/6468.js +1 -0
  71. package/dist/6589.js +1 -0
  72. package/dist/6679.js +1 -0
  73. package/dist/6753.js +1 -0
  74. package/dist/6753.js.map +1 -0
  75. package/dist/6777.js +2 -0
  76. package/dist/6777.js.LICENSE.txt +19 -0
  77. package/dist/6777.js.map +1 -0
  78. package/dist/679.js +2 -0
  79. package/dist/679.js.LICENSE.txt +9 -0
  80. package/dist/679.js.map +1 -0
  81. package/dist/6840.js +1 -0
  82. package/dist/6859.js +1 -0
  83. package/dist/7097.js +1 -0
  84. package/dist/7129.js +1 -0
  85. package/dist/7129.js.map +1 -0
  86. package/dist/7159.js +1 -0
  87. package/dist/723.js +1 -0
  88. package/dist/7617.js +1 -0
  89. package/dist/791.js +1 -0
  90. package/dist/791.js.map +1 -0
  91. package/dist/795.js +1 -0
  92. package/dist/8163.js +1 -0
  93. package/dist/8349.js +1 -0
  94. package/dist/8371.js +1 -0
  95. package/dist/841.js +1 -0
  96. package/dist/841.js.map +1 -0
  97. package/dist/8618.js +1 -0
  98. package/dist/8627.js +2 -0
  99. package/dist/8627.js.LICENSE.txt +25 -0
  100. package/dist/8627.js.map +1 -0
  101. package/dist/8898.js +2 -0
  102. package/dist/8898.js.LICENSE.txt +32 -0
  103. package/dist/8898.js.map +1 -0
  104. package/dist/890.js +1 -0
  105. package/dist/9214.js +1 -0
  106. package/dist/9321.js +1 -0
  107. package/dist/9321.js.map +1 -0
  108. package/dist/9452.js +1 -0
  109. package/dist/9452.js.map +1 -0
  110. package/dist/9538.js +1 -0
  111. package/dist/9569.js +1 -0
  112. package/dist/9695.js +1 -0
  113. package/dist/9695.js.map +1 -0
  114. package/dist/986.js +1 -0
  115. package/dist/9879.js +1 -0
  116. package/dist/9895.js +1 -0
  117. package/dist/9900.js +1 -0
  118. package/dist/9910.js +1 -0
  119. package/dist/9910.js.map +1 -0
  120. package/dist/9913.js +1 -0
  121. package/dist/main.js +2 -0
  122. package/dist/main.js.LICENSE.txt +45 -0
  123. package/dist/main.js.map +1 -0
  124. package/dist/openmrs-esm-laboratory-app.js +1 -0
  125. package/dist/openmrs-esm-laboratory-app.js.buildmanifest.json +1744 -0
  126. package/dist/openmrs-esm-laboratory-app.js.map +1 -0
  127. package/dist/routes.json +1 -0
  128. package/e2e/README.md +117 -0
  129. package/e2e/commands/encounter-operations.ts +63 -0
  130. package/e2e/commands/index.ts +5 -0
  131. package/e2e/commands/patient-operations.ts +109 -0
  132. package/e2e/commands/provider-operations.ts +9 -0
  133. package/e2e/commands/test-order-operations.ts +46 -0
  134. package/e2e/commands/types/index.ts +157 -0
  135. package/e2e/commands/visit-operations.ts +38 -0
  136. package/e2e/core/global-setup.ts +32 -0
  137. package/e2e/core/index.ts +1 -0
  138. package/e2e/core/test.ts +31 -0
  139. package/e2e/fixtures/api.ts +27 -0
  140. package/e2e/fixtures/fhirApi.ts +28 -0
  141. package/e2e/fixtures/index.ts +2 -0
  142. package/e2e/pages/index.ts +1 -0
  143. package/e2e/pages/laboratory-page.ts +28 -0
  144. package/e2e/specs/add-lab-results.spec.ts +111 -0
  145. package/e2e/specs/reject-lab-request.spec.ts +88 -0
  146. package/e2e/specs/test-orders.spec.ts +69 -0
  147. package/e2e/support/github/Dockerfile +34 -0
  148. package/e2e/support/github/docker-compose.yml +24 -0
  149. package/e2e/support/github/run-e2e-docker-env.sh +58 -0
  150. package/example.env +7 -0
  151. package/jest.config.js +35 -0
  152. package/package.json +105 -0
  153. package/playwright.config.ts +42 -0
  154. package/src/components/create-dashboard-link.component.tsx +37 -0
  155. package/src/components/loader/loader.component.tsx +11 -0
  156. package/src/components/loader/loader.scss +9 -0
  157. package/src/components/orders-table/list-order-details.component.tsx +143 -0
  158. package/src/components/orders-table/list-order-details.scss +136 -0
  159. package/src/components/orders-table/order-detail.scss +18 -0
  160. package/src/components/orders-table/orders-data-table.component.tsx +349 -0
  161. package/src/components/orders-table/orders-data-table.scss +129 -0
  162. package/src/components/orders-table/orders-data-table.test.tsx +214 -0
  163. package/src/components/orders-table/orders-date-range-picker.component.tsx +32 -0
  164. package/src/components/orders-table/orders-date-range-picker.scss +7 -0
  165. package/src/components/summary-tile/lab-summary-tile.component.tsx +31 -0
  166. package/src/components/summary-tile/lab-summary-tile.scss +64 -0
  167. package/src/config-schema.ts +39 -0
  168. package/src/constants.ts +2 -0
  169. package/src/declarations.d.ts +2 -0
  170. package/src/index.ts +123 -0
  171. package/src/lab-tabs/actions/actions.scss +26 -0
  172. package/src/lab-tabs/actions/add-lab-request-results-action.component.tsx +46 -0
  173. package/src/lab-tabs/actions/amend-lab-results-action.component.tsx +40 -0
  174. package/src/lab-tabs/actions/approve-lab-results-action.component.tsx +36 -0
  175. package/src/lab-tabs/actions/pickup-lab-request-action.component.tsx +36 -0
  176. package/src/lab-tabs/actions/reject-lab-request-action.component.tsx +36 -0
  177. package/src/lab-tabs/data-table-extensions/completed-lab-requests-table.extension.tsx +8 -0
  178. package/src/lab-tabs/data-table-extensions/declined-lab-requests-table-extension.tsx +8 -0
  179. package/src/lab-tabs/data-table-extensions/in-progress-lab-requests-table.extension.tsx +8 -0
  180. package/src/lab-tabs/data-table-extensions/pending-review-lab-request-table.extension.tsx +8 -0
  181. package/src/lab-tabs/data-table-extensions/tests-ordered-table.extension.tsx +8 -0
  182. package/src/lab-tabs/laboratory-tabs.component.tsx +81 -0
  183. package/src/lab-tabs/laboratory-tabs.scss +38 -0
  184. package/src/lab-tabs/modals/approval-lab-results-modal.component.tsx +70 -0
  185. package/src/lab-tabs/modals/pickup-lab-request-modal.component.tsx +67 -0
  186. package/src/lab-tabs/modals/pickup-lab-request-modal.test.tsx +127 -0
  187. package/src/lab-tabs/modals/reject-lab-request-modal.component.tsx +86 -0
  188. package/src/lab-tabs/modals/reject-lab-request-modal.scss +13 -0
  189. package/src/lab-tabs/modals/reject-lab-request-modal.test.tsx +152 -0
  190. package/src/lab-tiles/all-lab-requests-tile.component.tsx +19 -0
  191. package/src/lab-tiles/completed-lab-requests-tile.component.tsx +19 -0
  192. package/src/lab-tiles/in-progress-lab-requests-tile.component.tsx +19 -0
  193. package/src/lab-tiles/laboratory-summary-tiles.component.tsx +52 -0
  194. package/src/lab-tiles/laboratory-summary-tiles.scss +16 -0
  195. package/src/lab-tiles/pending-review-lab-results-tile.component.tsx +22 -0
  196. package/src/laboratory-dashboard.component.tsx +30 -0
  197. package/src/laboratory-dashboard.scss +5 -0
  198. package/src/laboratory.resource.ts +108 -0
  199. package/src/root.component.tsx +15 -0
  200. package/src/routes.json +204 -0
  201. package/src/types.ts +39 -0
  202. package/tools/i18next-parser.config.js +93 -0
  203. package/tools/index.ts +1 -0
  204. package/tools/setup-tests.ts +8 -0
  205. package/tools/test-utils.ts +44 -0
  206. package/tools/update-openmrs-deps.mjs +42 -0
  207. package/translations/am.json +79 -0
  208. package/translations/ar.json +79 -0
  209. package/translations/ar_SY.json +79 -0
  210. package/translations/bn.json +79 -0
  211. package/translations/cs.json +79 -0
  212. package/translations/de.json +79 -0
  213. package/translations/en.json +79 -0
  214. package/translations/en_US.json +79 -0
  215. package/translations/es.json +79 -0
  216. package/translations/es_MX.json +79 -0
  217. package/translations/fr.json +79 -0
  218. package/translations/he.json +79 -0
  219. package/translations/hi.json +79 -0
  220. package/translations/hi_IN.json +79 -0
  221. package/translations/id.json +79 -0
  222. package/translations/it.json +79 -0
  223. package/translations/ka.json +79 -0
  224. package/translations/km.json +79 -0
  225. package/translations/ku.json +79 -0
  226. package/translations/ky.json +79 -0
  227. package/translations/lg.json +79 -0
  228. package/translations/ne.json +79 -0
  229. package/translations/pl.json +79 -0
  230. package/translations/pt.json +79 -0
  231. package/translations/pt_BR.json +79 -0
  232. package/translations/qu.json +79 -0
  233. package/translations/ro_RO.json +79 -0
  234. package/translations/ru_RU.json +79 -0
  235. package/translations/si.json +79 -0
  236. package/translations/sq.json +79 -0
  237. package/translations/sw.json +79 -0
  238. package/translations/sw_KE.json +79 -0
  239. package/translations/tr.json +79 -0
  240. package/translations/tr_TR.json +79 -0
  241. package/translations/uk.json +79 -0
  242. package/translations/uz.json +79 -0
  243. package/translations/uz@Latn.json +79 -0
  244. package/translations/uz_UZ.json +79 -0
  245. package/translations/vi.json +79 -0
  246. package/translations/zh.json +79 -0
  247. package/translations/zh_CN.json +79 -0
  248. package/translations/zh_TW.json +79 -0
  249. package/tsconfig.json +28 -0
  250. package/turbo.json +15 -0
  251. package/webpack.config.js +25 -0
@@ -0,0 +1,27 @@
1
+ /* eslint-disable react-hooks/rules-of-hooks */
2
+ import { type APIRequestContext, type PlaywrightWorkerArgs, type WorkerFixture } from '@playwright/test';
3
+
4
+ /**
5
+ * A fixture which initializes an [`APIRequestContext`](https://playwright.dev/docs/api/class-apirequestcontext)
6
+ * that is bound to the configured OpenMRS API server. The context is automatically authenticated
7
+ * using the configured admin account.
8
+ *
9
+ * Use the request context like this:
10
+ * ```ts
11
+ * test('your test', async ({ api }) => {
12
+ * const res = await api.get('patient/1234');
13
+ * await expect(res.ok()).toBeTruthy();
14
+ * });
15
+ * ```
16
+ */
17
+ export const api: WorkerFixture<APIRequestContext, PlaywrightWorkerArgs> = async ({ playwright }, use) => {
18
+ const ctx = await playwright.request.newContext({
19
+ baseURL: `${process.env.E2E_BASE_URL}/ws/rest/v1/`,
20
+ httpCredentials: {
21
+ username: process.env.E2E_USER_ADMIN_USERNAME,
22
+ password: process.env.E2E_USER_ADMIN_PASSWORD,
23
+ },
24
+ });
25
+
26
+ await use(ctx);
27
+ };
@@ -0,0 +1,28 @@
1
+ import { type APIRequestContext, type PlaywrightWorkerArgs, type WorkerFixture } from '@playwright/test';
2
+
3
+ /**
4
+ * A fixture which initializes an [`APIRequestContext`](https://playwright.dev/docs/api/class-apirequestcontext)
5
+ * that is bound to the configured OpenMRS API server. The context is automatically authenticated
6
+ * using the configured admin account.
7
+ *
8
+ * Use the request context like this:
9
+ * ```ts
10
+ * test('your test', async ({ api }) => {
11
+ * const res = await api.get('patient/1234');
12
+ * await expect(res.ok()).toBeTruthy();
13
+ * });
14
+ * ```
15
+ */
16
+
17
+ export const fhirApi: WorkerFixture<APIRequestContext, PlaywrightWorkerArgs> = async ({ playwright }, use) => {
18
+ const fhirctx = await playwright.request.newContext({
19
+ baseURL: `${process.env.E2E_BASE_URL}/ws/fhir2/R4/`,
20
+ httpCredentials: {
21
+ username: process.env.E2E_USER_ADMIN_USERNAME,
22
+ password: process.env.E2E_USER_ADMIN_PASSWORD,
23
+ },
24
+ });
25
+
26
+ // eslint-disable-next-line react-hooks/rules-of-hooks
27
+ await use(fhirctx);
28
+ };
@@ -0,0 +1,2 @@
1
+ export * from './api';
2
+ export * from './fhirApi';
@@ -0,0 +1 @@
1
+ export * from './laboratory-page';
@@ -0,0 +1,28 @@
1
+ import { type Page } from '@playwright/test';
2
+
3
+ export class LaboratoryPage {
4
+ constructor(readonly page: Page) {}
5
+
6
+ async goTo() {
7
+ await this.page.goto('/openmrs/spa/home/laboratory');
8
+ }
9
+
10
+ async navigateToTab(tabName: string) {
11
+ await this.page.getByRole('tab', { name: tabName }).click();
12
+ }
13
+
14
+ async expandPatientRow(patientName: string) {
15
+ await this.page
16
+ .getByRole('row', { name: new RegExp(`Expand current row ${patientName}`) })
17
+ .getByLabel('Expand current row')
18
+ .click();
19
+ }
20
+
21
+ async searchFor(text: string) {
22
+ await this.page.getByPlaceholder('Search this list').fill(text);
23
+ }
24
+
25
+ getPatientRow(patientName: string) {
26
+ return this.page.getByRole('row', { name: new RegExp(patientName) });
27
+ }
28
+ }
@@ -0,0 +1,111 @@
1
+ import { expect } from '@playwright/test';
2
+ import { type Order, type Visit } from '@openmrs/esm-framework';
3
+ import {
4
+ generateRandomTestOrder,
5
+ deleteTestOrder,
6
+ createEncounter,
7
+ deleteEncounter,
8
+ getProvider,
9
+ startVisit,
10
+ endVisit,
11
+ } from '../commands';
12
+ import { test } from '../core';
13
+ import { type Encounter, type Provider } from '../commands/types';
14
+ import { LaboratoryPage } from '../pages';
15
+
16
+ let testOrder: Order;
17
+ let encounter: Encounter;
18
+ let orderer: Provider;
19
+ let fullName: string;
20
+ let visit: Visit;
21
+
22
+ test.beforeEach(async ({ api, patient }) => {
23
+ orderer = await getProvider(api);
24
+ visit = await startVisit(api, patient.uuid);
25
+ encounter = await createEncounter(api, patient.uuid, orderer.uuid, visit);
26
+ testOrder = await generateRandomTestOrder(api, patient.uuid, encounter, orderer.uuid);
27
+ fullName = patient.person?.display;
28
+ });
29
+
30
+ test.describe('Laboratory order workflow', () => {
31
+ test('Add lab results via the lab app', async ({ page }) => {
32
+ const laboratoryPage = new LaboratoryPage(page);
33
+
34
+ await test.step('Given I navigate to the laboratory page', async () => {
35
+ await laboratoryPage.goTo();
36
+ await expect(page).toHaveURL(process.env.E2E_BASE_URL + `/spa/home/laboratory`);
37
+ });
38
+
39
+ await test.step('When I expand the patient row on the Tests ordered tab', async () => {
40
+ await expect(page.getByRole('tab', { name: 'Tests ordered' })).toBeVisible();
41
+ await laboratoryPage.expandPatientRow(fullName);
42
+ });
43
+
44
+ await test.step('Then I should see the order with status "not picked"', async () => {
45
+ await expect(page.getByText(/Status:Order not picked/i)).toBeVisible();
46
+ await expect(page.getByRole('cell', { name: 'serum glucose' })).toBeVisible();
47
+ });
48
+
49
+ await test.step('When I click Pick Lab Request and confirm', async () => {
50
+ await page.getByRole('button', { name: 'Pick Lab Request' }).first().click();
51
+ await page.getByRole('button', { name: 'Pick up lab request' }).click();
52
+ });
53
+
54
+ await test.step('Then I should see a success notification', async () => {
55
+ await expect(page.getByText(/You have successfully picked an order/i)).toBeVisible();
56
+ });
57
+
58
+ await test.step('When I navigate to the In progress tab', async () => {
59
+ await laboratoryPage.navigateToTab('In progress');
60
+ });
61
+
62
+ await test.step('And I expand the patient row', async () => {
63
+ await laboratoryPage.expandPatientRow(fullName);
64
+ });
65
+
66
+ await test.step('Then I should see the order with In progress status', async () => {
67
+ await expect(page.getByLabel('Structured list section').getByText('In progress')).toBeVisible();
68
+ await expect(page.getByRole('cell', { name: 'serum glucose' })).toBeVisible();
69
+ });
70
+
71
+ await test.step('When I click Add lab results and enter a value', async () => {
72
+ await page.getByRole('button', { name: 'Add lab results' }).click();
73
+ await page.getByRole('spinbutton', { name: 'serum glucose (>= 0' }).fill('35');
74
+ });
75
+
76
+ await test.step('And I save the results', async () => {
77
+ await page.getByRole('button', { name: 'Save and close' }).click();
78
+ });
79
+
80
+ await test.step('Then I should see a success notification', async () => {
81
+ await expect(page.getByText(/Lab results for .* have been successfully updated/i)).toBeVisible();
82
+ });
83
+
84
+ await test.step('When I navigate to the Completed tab', async () => {
85
+ await laboratoryPage.navigateToTab('Completed');
86
+ });
87
+
88
+ await test.step('And I expand the patient row', async () => {
89
+ await laboratoryPage.expandPatientRow(fullName);
90
+ });
91
+
92
+ await test.step('Then I should see the order with Completed status', async () => {
93
+ await expect(page.getByLabel('Structured list section').getByText('Completed')).toBeVisible();
94
+ await expect(
95
+ page.getByLabel('Structured list section').getByRole('cell', { name: 'serum glucose' }),
96
+ ).toBeVisible();
97
+ });
98
+ });
99
+ });
100
+
101
+ test.afterEach(async ({ api }) => {
102
+ if (visit) {
103
+ await endVisit(api, visit);
104
+ }
105
+ if (encounter?.uuid) {
106
+ await deleteEncounter(api, encounter.uuid);
107
+ }
108
+ if (testOrder?.uuid) {
109
+ await deleteTestOrder(api, testOrder.uuid);
110
+ }
111
+ });
@@ -0,0 +1,88 @@
1
+ import { expect } from '@playwright/test';
2
+ import { type Order, type Visit } from '@openmrs/esm-framework';
3
+ import {
4
+ generateRandomTestOrder,
5
+ deleteTestOrder,
6
+ createEncounter,
7
+ deleteEncounter,
8
+ getProvider,
9
+ startVisit,
10
+ endVisit,
11
+ } from '../commands';
12
+ import { test } from '../core';
13
+ import { type Encounter, type Provider } from '../commands/types';
14
+ import { LaboratoryPage } from '../pages';
15
+
16
+ let testOrder: Order;
17
+ let encounter: Encounter;
18
+ let orderer: Provider;
19
+ let fullName: string;
20
+ let visit: Visit;
21
+
22
+ test.beforeEach(async ({ api, patient }) => {
23
+ orderer = await getProvider(api);
24
+ visit = await startVisit(api, patient.uuid);
25
+ encounter = await createEncounter(api, patient.uuid, orderer.uuid, visit);
26
+ testOrder = await generateRandomTestOrder(api, patient.uuid, encounter, orderer.uuid);
27
+ fullName = patient.person?.display;
28
+ });
29
+
30
+ test('Reject a lab request', async ({ page }) => {
31
+ const laboratoryPage = new LaboratoryPage(page);
32
+
33
+ await test.step('Given I navigate to the laboratory page', async () => {
34
+ await laboratoryPage.goTo();
35
+ await expect(page.getByRole('tab', { name: 'Tests ordered' })).toBeVisible();
36
+ });
37
+
38
+ await test.step('When I expand the patient row', async () => {
39
+ await laboratoryPage.expandPatientRow(fullName);
40
+ });
41
+
42
+ await test.step('Then I should see the test order', async () => {
43
+ await expect(page.getByRole('cell', { name: 'serum glucose' })).toBeVisible();
44
+ });
45
+
46
+ await test.step('When I click the Reject Lab Request button', async () => {
47
+ await page.getByRole('button', { name: 'Reject Lab Request' }).first().click();
48
+ });
49
+
50
+ await test.step('Then I should see the rejection modal with the test type', async () => {
51
+ await expect(page.getByRole('heading', { name: /Reject lab request/ })).toBeVisible();
52
+ await expect(page.getByText(/Test type:/i)).toBeVisible();
53
+ });
54
+
55
+ await test.step('When I enter a rejection comment and confirm', async () => {
56
+ await page.getByRole('textbox', { name: 'Fulfiller comment' }).fill('Sample was contaminated');
57
+ await page.getByRole('button', { name: 'danger Reject', exact: true }).click();
58
+ });
59
+
60
+ await test.step('Then I should see a success notification', async () => {
61
+ await expect(page.getByText(/Lab request rejected/i)).toBeVisible();
62
+ });
63
+
64
+ await test.step('When I navigate to the Declined tests tab', async () => {
65
+ await laboratoryPage.navigateToTab('Declined tests');
66
+ });
67
+
68
+ await test.step('And I expand the patient row', async () => {
69
+ await laboratoryPage.expandPatientRow(fullName);
70
+ });
71
+
72
+ await test.step('Then I should see the order with Declined status', async () => {
73
+ await expect(page.getByRole('cell', { name: 'serum glucose' })).toBeVisible();
74
+ await expect(page.getByRole('cell', { name: 'Declined' })).toBeVisible();
75
+ });
76
+ });
77
+
78
+ test.afterEach(async ({ api }) => {
79
+ if (visit) {
80
+ await endVisit(api, visit);
81
+ }
82
+ if (encounter?.uuid) {
83
+ await deleteEncounter(api, encounter.uuid);
84
+ }
85
+ if (testOrder?.uuid) {
86
+ await deleteTestOrder(api, testOrder.uuid);
87
+ }
88
+ });
@@ -0,0 +1,69 @@
1
+ import { expect } from '@playwright/test';
2
+ import { type Order, type Visit } from '@openmrs/esm-framework';
3
+ import {
4
+ generateRandomTestOrder,
5
+ deleteTestOrder,
6
+ createEncounter,
7
+ deleteEncounter,
8
+ getProvider,
9
+ startVisit,
10
+ endVisit,
11
+ } from '../commands';
12
+ import { test } from '../core';
13
+ import { type Encounter, type Provider } from '../commands/types';
14
+ import { LaboratoryPage } from '../pages';
15
+
16
+ const url = process.env.E2E_BASE_URL;
17
+
18
+ let testOrder: Order;
19
+ let encounter: Encounter;
20
+ let orderer: Provider;
21
+ let fullName: string;
22
+ let visit: Visit;
23
+
24
+ test.beforeEach(async ({ api, patient }) => {
25
+ orderer = await getProvider(api);
26
+ visit = await startVisit(api, patient.uuid);
27
+ encounter = await createEncounter(api, patient.uuid, orderer.uuid, visit);
28
+ testOrder = await generateRandomTestOrder(api, patient.uuid, encounter, orderer.uuid);
29
+ fullName = patient.person?.display;
30
+ });
31
+
32
+ test('View test orders', async ({ page }) => {
33
+ const laboratoryPage = new LaboratoryPage(page);
34
+
35
+ await test.step('Given I navigate to the laboratory page', async () => {
36
+ await laboratoryPage.goTo();
37
+ await expect(page).toHaveURL(url + `/spa/home/laboratory`);
38
+ });
39
+
40
+ await test.step('Then I should see the Tests ordered tab', async () => {
41
+ await expect(page.getByRole('tab', { name: 'Tests ordered' })).toBeVisible();
42
+ });
43
+
44
+ await test.step('And I should see the patient in the orders list', async () => {
45
+ await expect(laboratoryPage.getPatientRow(fullName)).toBeVisible();
46
+ });
47
+
48
+ await test.step('When I expand the patient row', async () => {
49
+ await laboratoryPage.expandPatientRow(fullName);
50
+ });
51
+
52
+ await test.step('Then I should see the order status, test name, and urgency', async () => {
53
+ await expect(page.getByText(/Status:Order not picked/i)).toBeVisible();
54
+ await expect(page.getByRole('cell', { name: 'serum glucose' })).toBeVisible();
55
+ await expect(page.getByText(/Routine/i)).toBeVisible();
56
+ });
57
+ });
58
+
59
+ test.afterEach(async ({ api }) => {
60
+ if (visit) {
61
+ await endVisit(api, visit);
62
+ }
63
+ if (encounter?.uuid) {
64
+ await deleteEncounter(api, encounter.uuid);
65
+ }
66
+ if (testOrder?.uuid) {
67
+ await deleteTestOrder(api, testOrder.uuid);
68
+ }
69
+ });
@@ -0,0 +1,34 @@
1
+ # syntax=docker/dockerfile:1.3
2
+ FROM --platform=$BUILDPLATFORM node:18-alpine as dev
3
+
4
+ ARG APP_SHELL_VERSION=next
5
+
6
+ RUN mkdir -p /app
7
+ WORKDIR /app
8
+
9
+ COPY . .
10
+
11
+ RUN npm_config_legacy_peer_deps=true npm install -g openmrs@${APP_SHELL_VERSION:-next}
12
+ ARG CACHE_BUST
13
+ RUN npm_config_legacy_peer_deps=true openmrs assemble --manifest --mode config --config spa-assemble-config.json --target ./spa
14
+
15
+ FROM --platform=$BUILDPLATFORM openmrs/openmrs-reference-application-3-frontend:nightly as frontend
16
+ FROM nginx:1.23-alpine
17
+
18
+ RUN apk update && \
19
+ apk upgrade && \
20
+ # add more utils for sponge to support our startup script
21
+ apk add --no-cache moreutils
22
+
23
+ # clear any default files installed by nginx
24
+ RUN rm -rf /usr/share/nginx/html/*
25
+
26
+ COPY --from=frontend /etc/nginx/nginx.conf /etc/nginx/nginx.conf
27
+ # this assumes that NOTHING in the framework is in a subdirectory
28
+ COPY --from=frontend /usr/share/nginx/html/* /usr/share/nginx/html/
29
+ COPY --from=frontend /usr/local/bin/startup.sh /usr/local/bin/startup.sh
30
+ RUN chmod +x /usr/local/bin/startup.sh
31
+
32
+ COPY --from=dev /app/spa/ /usr/share/nginx/html/
33
+
34
+ CMD ["/usr/local/bin/startup.sh"]
@@ -0,0 +1,24 @@
1
+ # This docker compose file is used to create a backend environment for the e2e.yml workflow.
2
+ # The images are pre-filled with data so that the backend environment can be started within a short time.
3
+ version: "3.7"
4
+
5
+ services:
6
+ gateway:
7
+ image: openmrs/openmrs-reference-application-3-gateway:${TAG:-nightly}
8
+ ports:
9
+ - "8080:80"
10
+
11
+ frontend:
12
+ build:
13
+ context: .
14
+ environment:
15
+ SPA_PATH: /openmrs/spa
16
+ API_URL: /openmrs
17
+
18
+ backend:
19
+ image: openmrs/openmrs-reference-application-3-backend:nightly-with-data
20
+ depends_on:
21
+ - db
22
+
23
+ db:
24
+ image: openmrs/openmrs-reference-application-3-db:nightly-with-data
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash -eu
2
+
3
+ # get the dir containing the script
4
+ script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
5
+ # create a temporary working directory
6
+ working_dir=$(mktemp -d "${TMPDIR:-/tmp/}openmrs-e2e-frontends.XXXXXXXXXX")
7
+ # get a list of all the apps in this workspace
8
+ apps=$(yarn workspaces list --json | jq -r 'select(.name | test("-app")) | .name')
9
+ # this array will hold all of the packed app names
10
+ app_names=()
11
+
12
+ echo "Creating packed archives of apps..."
13
+ # for each app
14
+ for app in $apps
15
+ do
16
+ # @openmrs/esm-whatever -> _openmrs_esm_whatever
17
+ app_name=$(echo "$app" | tr '[:punct:]' '_');
18
+ # add to our array
19
+ app_names+=("$app_name.tgz");
20
+ # run yarn pack for our app and add it to the working directory
21
+ # yarn workspace "$app" pack -o "$working_dir/$app_name.tgz" >/dev/null;
22
+ yarn pack -o "$working_dir/$app_name.tgz" >/dev/null
23
+ done;
24
+ echo "Created packed app archives"
25
+
26
+ echo "Creating dynamic spa-assemble-config.json..."
27
+ # dynamically assemble our list of frontend modules, prepending the login app and
28
+ # primary navigation apps; apps will all be in the /app directory of the Docker
29
+ # container
30
+ jq -n \
31
+ --arg apps "$apps" \
32
+ --arg app_names "$(echo ${app_names[@]})" \
33
+ '{
34
+ "@openmrs/esm-primary-navigation-app": "next",
35
+ "@openmrs/esm-home-app": "next",
36
+ "@openmrs/esm-patient-chart-app": "next",
37
+ "@openmrs/esm-patient-banner-app": "next",
38
+ "@openmrs/esm-patient-orders-app": "next",
39
+ "@openmrs/esm-patient-tests-app": "next"
40
+ } + (
41
+ ($apps | split("\n")) as $apps | ($app_names | split(" ") | map("/app/" + .)) as $app_files
42
+ | [$apps, $app_files]
43
+ | transpose
44
+ | map({"key": .[0], "value": .[1]})
45
+ | from_entries
46
+ )' | jq '{"frontendModules": .}' > "$working_dir/spa-assemble-config.json"
47
+ echo "Created dynamic spa-assemble-config.json"
48
+
49
+ echo "Copying Docker configuration..."
50
+ cp "$script_dir/Dockerfile" "$working_dir/Dockerfile"
51
+ cp "$script_dir/docker-compose.yml" "$working_dir/docker-compose.yml"
52
+
53
+ cd $working_dir
54
+ echo "Starting Docker containers..."
55
+ # CACHE_BUST to ensure the assemble step is always run
56
+ docker compose build --build-arg CACHE_BUST=$(date +%s) frontend
57
+ docker compose up -d
58
+
package/example.env ADDED
@@ -0,0 +1,7 @@
1
+ # This is an example environment file for configuring dynamic values.
2
+ E2E_BASE_URL=http://localhost:8080/openmrs
3
+ E2E_USER_ADMIN_USERNAME=admin
4
+ E2E_USER_ADMIN_PASSWORD=Admin123
5
+ E2E_LOGIN_DEFAULT_LOCATION_UUID=44c3efb0-2583-4c80-a79e-1f756a03c0a1
6
+ # The above location UUID is for the "Outpatient Clinic" location in the reference application
7
+
package/jest.config.js ADDED
@@ -0,0 +1,35 @@
1
+ /** @type {import('jest').Config} */
2
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
3
+ const path = require('path');
4
+
5
+ module.exports = {
6
+ clearMocks: true,
7
+ transform: {
8
+ '^.+\\.[jt]sx?$': ['@swc/jest'],
9
+ },
10
+ transformIgnorePatterns: ['/node_modules/(?!@openmrs|.+\\.pnp\\.[^\\/]+$)'],
11
+ moduleNameMapper: {
12
+ '^dexie$': require.resolve('dexie'),
13
+ '@openmrs/esm-framework': '@openmrs/esm-framework/mock',
14
+ '\\.(s?css)$': 'identity-obj-proxy',
15
+ '^lodash-es/(.*)$': 'lodash/$1',
16
+ '^lodash-es$': 'lodash',
17
+ '^@tools$': path.resolve(__dirname, 'tools'),
18
+ '^@tools/(.*)$': path.resolve(__dirname, 'tools', '$1'),
19
+ '^@mocks/(.*)$': path.resolve(__dirname, '__mocks__', '$1'),
20
+ '^uuid$': path.resolve(__dirname, 'node_modules', 'uuid', 'dist', 'index.js'),
21
+ },
22
+ collectCoverageFrom: [
23
+ '!**/node_modules/**',
24
+ '!**/e2e/**',
25
+ ],
26
+ testPathIgnorePatterns: [
27
+ "/node_modules/",
28
+ "/e2e/" // Ignore the e2e directory containing Playwright tests
29
+ ],
30
+ setupFilesAfterEnv: ['<rootDir>/tools/setup-tests.ts'],
31
+ testEnvironment: 'jsdom',
32
+ testEnvironmentOptions: {
33
+ url: 'http://localhost/',
34
+ },
35
+ };
package/package.json ADDED
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "@ampath/esm-laboratory-app",
3
+ "version": "1.3.0-next.2",
4
+ "license": "MPL-2.0",
5
+ "description": "An O3 frontend module for managing laboratory requests and queues",
6
+ "browser": "dist/openmrs-esm-laboratory-app.js",
7
+ "main": "src/index.ts",
8
+ "source": true,
9
+ "scripts": {
10
+ "start": "openmrs develop",
11
+ "serve": "webpack serve --mode=development",
12
+ "build": "webpack --mode production",
13
+ "analyze": "webpack --mode=production --env analyze=true",
14
+ "lint": "eslint src --ext ts,tsx",
15
+ "prettier": "prettier --write \"src/**/*.{css,scss,ts,tsx}\" --list-different",
16
+ "typescript": "tsc",
17
+ "test": "jest --config jest.config.js --passWithNoTests --color",
18
+ "verify": "turbo lint typescript test --color",
19
+ "extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ./tools/i18next-parser.config.js",
20
+ "coverage": "yarn test -- --coverage",
21
+ "postinstall": "husky install",
22
+ "test-e2e": "playwright test"
23
+ },
24
+ "browserslist": [
25
+ "extends browserslist-config-openmrs"
26
+ ],
27
+ "keywords": [
28
+ "openmrs",
29
+ "microfrontends",
30
+ "laboratory"
31
+ ],
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/openmrs/openmrs-esm-laboratory.git"
35
+ },
36
+ "homepage": "https://github.com/openmrs/openmrs-esm-laboratory#readme",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/openmrs/openmrs-esm-laboratory/issues"
42
+ },
43
+ "lint-staged": {
44
+ "*.{ts,tsx}": "eslint --cache --fix --max-warnings 0",
45
+ "*.{css,scss,ts,tsx}": "prettier --write --list-different"
46
+ },
47
+ "dependencies": {
48
+ "@carbon/react": "^1.83.0",
49
+ "lodash-es": "^4.17.21",
50
+ "react-hook-form": "^7.52.1"
51
+ },
52
+ "peerDependencies": {
53
+ "@openmrs/esm-framework": "9.x",
54
+ "dayjs": "1.x",
55
+ "react": "18.x",
56
+ "react-i18next": "16.x",
57
+ "react-router-dom": "6.x"
58
+ },
59
+ "devDependencies": {
60
+ "@openmrs/esm-framework": "next",
61
+ "@playwright/test": "^1.52.0",
62
+ "@swc/cli": "^0.1.62",
63
+ "@swc/core": "^1.3.62",
64
+ "@swc/jest": "^0.2.26",
65
+ "@testing-library/dom": "^8.20.0",
66
+ "@testing-library/jest-dom": "^5.16.5",
67
+ "@testing-library/react": "^16.3.2",
68
+ "@testing-library/user-event": "^14.4.3",
69
+ "@types/dotenv": "^8.2.3",
70
+ "@types/jest": "^28.1.8",
71
+ "@types/react": "^18.3.21",
72
+ "@types/react-dom": "^18.3.0",
73
+ "@types/webpack-env": "^1.18.1",
74
+ "@typescript-eslint/eslint-plugin": "^8.45.0",
75
+ "@typescript-eslint/parser": "^8.45.0",
76
+ "css-loader": "^6.8.1",
77
+ "dayjs": "^1.11.11",
78
+ "eslint": "^8.57.0",
79
+ "eslint-plugin-import": "^2.31.0",
80
+ "eslint-plugin-react-hooks": "^5.0.0",
81
+ "file-saver": "^2.0.5",
82
+ "husky": "^8.0.0",
83
+ "i18next-parser": "^9.3.0",
84
+ "identity-obj-proxy": "^3.0.0",
85
+ "jest": "^28.1.3",
86
+ "jest-cli": "^28.1.3",
87
+ "jest-environment-jsdom": "^28.1.3",
88
+ "lint-staged": "^14.0.1",
89
+ "openmrs": "next",
90
+ "prettier": "^2.8.8",
91
+ "pretty-quick": "^3.1.3",
92
+ "raw-loader": "^4.0.2",
93
+ "react": "^18.3.1",
94
+ "react-dom": "^18.3.1",
95
+ "react-i18next": "^16.0.0",
96
+ "react-router-dom": "^6.11.2",
97
+ "swc-loader": "^0.2.3",
98
+ "turbo": "^2.5.2",
99
+ "typescript": "^5.0.0",
100
+ "webpack": "5.99.9",
101
+ "webpack-cli": "^6.0.1"
102
+ },
103
+ "packageManager": "yarn@4.10.3",
104
+ "stableVersion": "1.3.0"
105
+ }
@@ -0,0 +1,42 @@
1
+ /* eslint-disable no-console */
2
+ import { devices, type PlaywrightTestConfig } from '@playwright/test';
3
+ import { config as dotenvConfig } from 'dotenv';
4
+ import { resolve } from 'node:path';
5
+
6
+ // Suppress dotenv promotional messages
7
+ const originalLog = console.log;
8
+ console.log = () => {};
9
+ dotenvConfig({ path: resolve(process.cwd(), '.env') });
10
+ dotenvConfig();
11
+ console.log = originalLog;
12
+
13
+ // See https://playwright.dev/docs/test-configuration.
14
+ const config: PlaywrightTestConfig = {
15
+ testDir: './e2e/specs',
16
+ timeout: 3 * 60 * 1000,
17
+ expect: {
18
+ timeout: 40 * 1000,
19
+ },
20
+ fullyParallel: true,
21
+ forbidOnly: !!process.env.CI,
22
+ retries: 0,
23
+ reporter: process.env.CI ? [['junit', { outputFile: 'results.xml' }], ['html']] : [['html']],
24
+ globalSetup: require.resolve('./e2e/core/global-setup'),
25
+ use: {
26
+ baseURL: `${process.env.E2E_BASE_URL}/spa/`,
27
+ storageState: 'e2e/storageState.json',
28
+ video: 'retain-on-failure',
29
+ trace: 'retain-on-failure',
30
+ },
31
+ projects: [
32
+ {
33
+ name: 'chromium',
34
+ use: {
35
+ ...devices['Desktop Chrome'],
36
+ channel: 'chromium',
37
+ },
38
+ },
39
+ ],
40
+ };
41
+
42
+ export default config;