@openmrs/esm-billing-app 1.0.2-pre.880 → 1.0.2-pre.889
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.
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/1537.js +1 -0
- package/dist/1537.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2690.js +1 -1
- package/dist/3099.js +1 -1
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/{2372.js → 4692.js} +1 -1
- package/dist/4692.js.map +1 -0
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6468.js +1 -1
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8349.js +1 -1
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js +1 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +169 -145
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/billable-services/{create-edit/add-billable-service.scss → billable-service-form/billable-service-form.scss} +30 -1
- package/src/billable-services/{create-edit/add-billable-service.test.tsx → billable-service-form/billable-service-form.test.tsx} +178 -82
- package/src/billable-services/{create-edit/add-billable-service.component.tsx → billable-service-form/billable-service-form.workspace.tsx} +63 -47
- package/src/billable-services/billable-services-home.component.tsx +2 -8
- package/src/billable-services/billable-services-left-panel-menu.component.tsx +1 -1
- package/src/billable-services/billable-services-menu-item/item.component.tsx +5 -4
- package/src/billable-services/billable-services.component.tsx +14 -11
- package/src/billable-services/cash-point/add-cash-point.modal.tsx +47 -45
- package/src/billable-services-admin-card-link.component.test.tsx +2 -2
- package/src/billable-services-admin-card-link.component.tsx +1 -1
- package/src/index.ts +8 -4
- package/src/routes.json +7 -4
- package/translations/am.json +7 -2
- package/translations/ar.json +7 -2
- package/translations/ar_SY.json +7 -2
- package/translations/bn.json +7 -2
- package/translations/de.json +7 -2
- package/translations/en.json +7 -2
- package/translations/en_US.json +7 -2
- package/translations/es.json +7 -2
- package/translations/es_MX.json +7 -2
- package/translations/fr.json +7 -2
- package/translations/he.json +7 -2
- package/translations/hi.json +7 -2
- package/translations/hi_IN.json +7 -2
- package/translations/id.json +7 -2
- package/translations/it.json +7 -2
- package/translations/ka.json +7 -2
- package/translations/km.json +7 -2
- package/translations/ku.json +7 -2
- package/translations/ky.json +7 -2
- package/translations/lg.json +7 -2
- package/translations/ne.json +7 -2
- package/translations/pl.json +7 -2
- package/translations/pt.json +7 -2
- package/translations/pt_BR.json +7 -2
- package/translations/qu.json +7 -2
- package/translations/ro_RO.json +7 -2
- package/translations/ru_RU.json +7 -2
- package/translations/si.json +7 -2
- package/translations/sw.json +7 -2
- package/translations/sw_KE.json +7 -2
- package/translations/tr.json +7 -2
- package/translations/tr_TR.json +7 -2
- package/translations/uk.json +7 -2
- package/translations/uz.json +7 -2
- package/translations/uz@Latn.json +7 -2
- package/translations/uz_UZ.json +7 -2
- package/translations/vi.json +7 -2
- package/translations/zh.json +7 -2
- package/translations/zh_CN.json +7 -2
- package/dist/2372.js.map +0 -1
- package/src/billable-services/create-edit/edit-billable-service.modal.tsx +0 -51
package/dist/routes.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billable-services-left-panel-link","component":"billableServicesLeftPanelLink","slot":"billable-services-left-panel-slot","order":0},{"name":"bill-waiver-left-panel-link","component":"billWaiverLeftPanelLink","slot":"billable-services-left-panel-slot","order":1},{"name":"billing-settings-left-panel-menu","component":"billingSettingsLeftPanelMenu","slot":"billable-services-left-panel-slot","order":2}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"
|
|
1
|
+
{"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":">=2.24.0","fhir2":">=1.2"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"},"featureFlag":"billing"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing history"},"featureFlag":"billing"},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm","featureFlag":"billing"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"billable-services-left-panel-link","component":"billableServicesLeftPanelLink","slot":"billable-services-left-panel-slot","order":0},{"name":"bill-waiver-left-panel-link","component":"billWaiverLeftPanelLink","slot":"billable-services-left-panel-slot","order":1},{"name":"billing-settings-left-panel-menu","component":"billingSettingsLeftPanelMenu","slot":"billable-services-left-panel-slot","order":2}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"add-payment-mode-modal","component":"addPaymentModeModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"workspaces":[{"name":"billing-form-workspace","title":"billingForm","component":"billingFormWorkspace","type":"form"},{"name":"billable-service-form","title":"billableServiceForm","component":"billableServiceFormWorkspace","type":"form","width":"wider"}],"featureFlags":[{"flagName":"billing","label":"Billing module","description":"This feature introduces navigation links on the patient chart and home page to allow accessing the billing module features"}],"version":"1.0.2-pre.889"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-billing-app",
|
|
3
|
-
"version": "1.0.2-pre.
|
|
3
|
+
"version": "1.0.2-pre.889",
|
|
4
4
|
"description": "O3 frontend module for handling billing concerns in healthcare settings",
|
|
5
5
|
"browser": "dist/openmrs-esm-billing-app.js",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "webpack --mode production",
|
|
14
14
|
"coverage": "yarn test --coverage",
|
|
15
15
|
"debug": "npm run serve",
|
|
16
|
-
"extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.modal.tsx' 'src/index.ts' --config ./tools/i18next-parser.config.js",
|
|
16
|
+
"extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.workspace.tsx' 'src/**/*.modal.tsx' 'src/index.ts' --config ./tools/i18next-parser.config.js",
|
|
17
17
|
"lint": "eslint src --ext ts,tsx --max-warnings=0",
|
|
18
18
|
"postinstall": "husky install",
|
|
19
19
|
"prettier": "prettier --config prettier.config.js --write \"src/**/*.{ts,tsx,css,scss}\" \"e2e/**/*.ts\"",
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
.form {
|
|
7
7
|
display: flex;
|
|
8
8
|
flex-direction: column;
|
|
9
|
+
justify-content: space-between;
|
|
9
10
|
height: 100%;
|
|
10
|
-
margin: layout.$spacing-05;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.paymentButtons {
|
|
@@ -74,3 +74,32 @@
|
|
|
74
74
|
.serviceNameLabel {
|
|
75
75
|
@include type.type-style('body-compact-02');
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
.button {
|
|
79
|
+
height: layout.$spacing-10;
|
|
80
|
+
display: flex;
|
|
81
|
+
align-content: flex-start;
|
|
82
|
+
align-items: baseline;
|
|
83
|
+
min-width: 50%;
|
|
84
|
+
|
|
85
|
+
:global(.cds--inline-loading) {
|
|
86
|
+
min-height: layout.$spacing-05 !important;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:global(.cds--inline-loading__text) {
|
|
90
|
+
@include type.type-style('body-01');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.tablet {
|
|
95
|
+
padding: layout.$spacing-06 layout.$spacing-05;
|
|
96
|
+
background-color: $ui-02;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.desktop {
|
|
100
|
+
padding: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.stack {
|
|
104
|
+
margin: layout.$spacing-05;
|
|
105
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
-
import { render, screen } from '@testing-library/react';
|
|
4
|
-
import {
|
|
3
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import { type FetchResponse } from '@openmrs/esm-framework';
|
|
5
5
|
import {
|
|
6
6
|
createBillableService,
|
|
7
7
|
updateBillableService,
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
usePaymentModes,
|
|
11
11
|
useServiceTypes,
|
|
12
12
|
} from '../billable-service.resource';
|
|
13
|
-
import
|
|
13
|
+
import BillableServiceFormWorkspace, {
|
|
14
|
+
transformServiceToFormData,
|
|
15
|
+
normalizePrice,
|
|
16
|
+
} from './billable-service-form.workspace';
|
|
14
17
|
import type { BillableService } from '../../types';
|
|
15
18
|
|
|
16
19
|
const mockUseBillableServices = jest.mocked(useBillableServices);
|
|
@@ -55,7 +58,7 @@ const mockServiceTypes = [
|
|
|
55
58
|
{ uuid: 'a487a743-62ce-4f93-a66b-c5154ee8987d', display: 'Adherence counselling service' },
|
|
56
59
|
];
|
|
57
60
|
|
|
58
|
-
// Test helpers
|
|
61
|
+
// Test helpers
|
|
59
62
|
const setupMocks = () => {
|
|
60
63
|
mockUseBillableServices.mockReturnValue({
|
|
61
64
|
billableServices: [],
|
|
@@ -69,13 +72,14 @@ const setupMocks = () => {
|
|
|
69
72
|
mockUseConceptsSearch.mockReturnValue({ searchResults: [], isSearching: false, error: null });
|
|
70
73
|
};
|
|
71
74
|
|
|
72
|
-
const
|
|
75
|
+
const renderBillableServicesForm = (props = {}) => {
|
|
73
76
|
const defaultProps = {
|
|
74
|
-
|
|
77
|
+
closeWorkspace: jest.fn(),
|
|
78
|
+
closeWorkspaceWithSavedChanges: jest.fn(),
|
|
75
79
|
...props,
|
|
76
80
|
};
|
|
77
81
|
setupMocks();
|
|
78
|
-
return render(<
|
|
82
|
+
return render(<BillableServiceFormWorkspace {...defaultProps} />);
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
interface FillOptions {
|
|
@@ -106,24 +110,22 @@ const fillRequiredFields = async (user, options: FillOptions = {}) => {
|
|
|
106
110
|
}
|
|
107
111
|
};
|
|
108
112
|
|
|
109
|
-
const submitForm = async (
|
|
110
|
-
const
|
|
111
|
-
|
|
113
|
+
const submitForm = async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
116
|
+
await user.click(saveButton);
|
|
112
117
|
};
|
|
113
118
|
|
|
114
|
-
describe('
|
|
119
|
+
describe('BillableServiceFormWorkspace', () => {
|
|
115
120
|
test('should render billable services form and generate correct payload', async () => {
|
|
116
121
|
const user = userEvent.setup();
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const formTitle = screen.getByRole('heading', { name: /Add billable service/i });
|
|
121
|
-
expect(formTitle).toBeInTheDocument();
|
|
122
|
+
const mockCloseWorkspace = jest.fn();
|
|
123
|
+
renderBillableServicesForm({ closeWorkspace: mockCloseWorkspace });
|
|
122
124
|
|
|
123
125
|
await fillRequiredFields(user);
|
|
124
126
|
mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
125
127
|
|
|
126
|
-
await submitForm(
|
|
128
|
+
await submitForm();
|
|
127
129
|
|
|
128
130
|
expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
|
|
129
131
|
expect(mockCreateBillableService).toHaveBeenCalledWith({
|
|
@@ -140,25 +142,125 @@ describe('AddBillableService', () => {
|
|
|
140
142
|
serviceStatus: 'ENABLED',
|
|
141
143
|
concept: undefined,
|
|
142
144
|
});
|
|
143
|
-
expect(navigate).toHaveBeenCalledTimes(1);
|
|
144
|
-
expect(navigate).toHaveBeenCalledWith({ to: '/openmrs/spa/billable-services' });
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
147
|
+
describe('Workspace Interactions', () => {
|
|
148
|
+
test('should call closeWorkspace when Cancel button is clicked', async () => {
|
|
149
|
+
const user = userEvent.setup();
|
|
150
|
+
const mockCloseWorkspace = jest.fn();
|
|
151
|
+
renderBillableServicesForm({ closeWorkspace: mockCloseWorkspace });
|
|
152
|
+
|
|
153
|
+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
154
|
+
await user.click(cancelButton);
|
|
155
|
+
|
|
156
|
+
expect(mockCloseWorkspace).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('should call closeWorkspaceWithSavedChanges after successful save', async () => {
|
|
160
|
+
const user = userEvent.setup();
|
|
161
|
+
const mockCloseWorkspaceWithSavedChanges = jest.fn();
|
|
162
|
+
renderBillableServicesForm({ closeWorkspaceWithSavedChanges: mockCloseWorkspaceWithSavedChanges });
|
|
163
|
+
|
|
164
|
+
await fillRequiredFields(user);
|
|
165
|
+
mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
166
|
+
await submitForm();
|
|
167
|
+
|
|
168
|
+
// Wait for async submission
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
170
|
+
|
|
171
|
+
expect(mockCloseWorkspaceWithSavedChanges).toHaveBeenCalledTimes(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('should disable buttons during submission', async () => {
|
|
175
|
+
const user = userEvent.setup();
|
|
176
|
+
let resolveCreate: (value: any) => void;
|
|
177
|
+
const createPromise = new Promise((resolve) => {
|
|
178
|
+
resolveCreate = resolve;
|
|
179
|
+
});
|
|
180
|
+
mockCreateBillableService.mockReturnValue(createPromise as any);
|
|
181
|
+
|
|
182
|
+
renderBillableServicesForm();
|
|
183
|
+
|
|
184
|
+
await fillRequiredFields(user);
|
|
185
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
186
|
+
const cancelButton = screen.getByRole('button', { name: /cancel/i });
|
|
187
|
+
|
|
188
|
+
// Click save to trigger submission
|
|
189
|
+
await user.click(saveButton);
|
|
190
|
+
|
|
191
|
+
// Buttons should be disabled during submission
|
|
192
|
+
expect(saveButton).toBeDisabled();
|
|
193
|
+
expect(cancelButton).toBeDisabled();
|
|
194
|
+
|
|
195
|
+
// Resolve the promise to complete submission
|
|
196
|
+
resolveCreate!({} as FetchResponse<any>);
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('should show loading indicator in save button during submission', async () => {
|
|
201
|
+
const user = userEvent.setup();
|
|
202
|
+
let resolveCreate: (value: any) => void;
|
|
203
|
+
const createPromise = new Promise((resolve) => {
|
|
204
|
+
resolveCreate = resolve;
|
|
205
|
+
});
|
|
206
|
+
mockCreateBillableService.mockReturnValue(createPromise as any);
|
|
207
|
+
|
|
208
|
+
renderBillableServicesForm();
|
|
209
|
+
|
|
210
|
+
await fillRequiredFields(user);
|
|
211
|
+
const saveButton = screen.getByRole('button', { name: /save/i });
|
|
212
|
+
|
|
213
|
+
await user.click(saveButton);
|
|
151
214
|
|
|
152
|
-
|
|
153
|
-
|
|
215
|
+
// Should show loading indicator
|
|
216
|
+
expect(await screen.findByText(/saving/i)).toBeInTheDocument();
|
|
154
217
|
|
|
155
|
-
|
|
218
|
+
// Resolve the promise
|
|
219
|
+
resolveCreate!({} as FetchResponse<any>);
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should call onWorkspaceClose callback after successful edit', async () => {
|
|
224
|
+
const mockOnWorkspaceClose = jest.fn();
|
|
225
|
+
const mockServiceToEdit: BillableService = {
|
|
226
|
+
uuid: 'test-uuid',
|
|
227
|
+
name: 'Test Service',
|
|
228
|
+
shortName: 'TS',
|
|
229
|
+
serviceStatus: 'ENABLED',
|
|
230
|
+
serviceType: {
|
|
231
|
+
uuid: 'type-uuid',
|
|
232
|
+
display: 'Lab service',
|
|
233
|
+
},
|
|
234
|
+
concept: null,
|
|
235
|
+
servicePrices: [
|
|
236
|
+
{
|
|
237
|
+
uuid: 'price-uuid',
|
|
238
|
+
name: 'Cash',
|
|
239
|
+
price: 100,
|
|
240
|
+
paymentMode: {
|
|
241
|
+
uuid: '63eff7a4-6f82-43c4-a333-dbcc58fe9f74',
|
|
242
|
+
name: 'Cash',
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
renderBillableServicesForm({ serviceToEdit: mockServiceToEdit, onWorkspaceClose: mockOnWorkspaceClose });
|
|
249
|
+
|
|
250
|
+
mockUpdateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
251
|
+
await submitForm();
|
|
252
|
+
|
|
253
|
+
// Wait for async submission
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
255
|
+
|
|
256
|
+
expect(mockOnWorkspaceClose).toHaveBeenCalledTimes(1);
|
|
257
|
+
});
|
|
156
258
|
});
|
|
157
259
|
|
|
158
260
|
describe('Form Validation', () => {
|
|
159
261
|
test('should accept form submission without short name (short name is optional)', async () => {
|
|
160
262
|
const user = userEvent.setup();
|
|
161
|
-
|
|
263
|
+
renderBillableServicesForm();
|
|
162
264
|
|
|
163
265
|
// Fill required fields but skip short name
|
|
164
266
|
await user.type(screen.getByRole('textbox', { name: /Service name/i }), 'Lab Test');
|
|
@@ -174,7 +276,7 @@ describe('AddBillableService', () => {
|
|
|
174
276
|
|
|
175
277
|
mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
176
278
|
|
|
177
|
-
await submitForm(
|
|
279
|
+
await submitForm();
|
|
178
280
|
|
|
179
281
|
expect(mockCreateBillableService).toHaveBeenCalledWith(
|
|
180
282
|
expect.objectContaining({
|
|
@@ -186,7 +288,7 @@ describe('AddBillableService', () => {
|
|
|
186
288
|
|
|
187
289
|
test('should enforce 255 character limit on service name input', async () => {
|
|
188
290
|
const user = userEvent.setup();
|
|
189
|
-
|
|
291
|
+
renderBillableServicesForm();
|
|
190
292
|
|
|
191
293
|
const longName = 'A'.repeat(300); // Try to type 300 characters
|
|
192
294
|
const input = screen.getByRole('textbox', { name: /Service name/i });
|
|
@@ -198,7 +300,7 @@ describe('AddBillableService', () => {
|
|
|
198
300
|
|
|
199
301
|
test('should enforce 255 character limit on short name input', async () => {
|
|
200
302
|
const user = userEvent.setup();
|
|
201
|
-
|
|
303
|
+
renderBillableServicesForm();
|
|
202
304
|
|
|
203
305
|
const longShortName = 'B'.repeat(300); // Try to type 300 characters
|
|
204
306
|
const input = screen.getByRole('textbox', { name: /Short name/i });
|
|
@@ -210,14 +312,14 @@ describe('AddBillableService', () => {
|
|
|
210
312
|
|
|
211
313
|
test('should show "Price must be greater than 0" error for zero price', async () => {
|
|
212
314
|
const user = userEvent.setup();
|
|
213
|
-
|
|
315
|
+
renderBillableServicesForm();
|
|
214
316
|
|
|
215
317
|
await fillRequiredFields(user, { skipPrice: true });
|
|
216
318
|
|
|
217
319
|
const priceInput = screen.getByRole('spinbutton', { name: /selling price/i });
|
|
218
320
|
await user.type(priceInput, '0');
|
|
219
321
|
|
|
220
|
-
await submitForm(
|
|
322
|
+
await submitForm();
|
|
221
323
|
|
|
222
324
|
expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
|
|
223
325
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
@@ -225,14 +327,14 @@ describe('AddBillableService', () => {
|
|
|
225
327
|
|
|
226
328
|
test('should show "Price must be greater than 0" error for negative price', async () => {
|
|
227
329
|
const user = userEvent.setup();
|
|
228
|
-
|
|
330
|
+
renderBillableServicesForm();
|
|
229
331
|
|
|
230
332
|
await fillRequiredFields(user, { skipPrice: true });
|
|
231
333
|
|
|
232
334
|
const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
|
|
233
335
|
await user.type(priceInput, '-10');
|
|
234
336
|
|
|
235
|
-
await submitForm(
|
|
337
|
+
await submitForm();
|
|
236
338
|
|
|
237
339
|
expect(screen.getByText('Price must be greater than 0')).toBeInTheDocument();
|
|
238
340
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
@@ -240,7 +342,7 @@ describe('AddBillableService', () => {
|
|
|
240
342
|
|
|
241
343
|
test('should show "Service name is required" error when service name is empty', async () => {
|
|
242
344
|
const user = userEvent.setup();
|
|
243
|
-
|
|
345
|
+
renderBillableServicesForm();
|
|
244
346
|
|
|
245
347
|
// Fill all fields except service name
|
|
246
348
|
await user.type(screen.getByRole('textbox', { name: /Short name/i }), 'Test Short Name');
|
|
@@ -254,15 +356,15 @@ describe('AddBillableService', () => {
|
|
|
254
356
|
const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
|
|
255
357
|
await user.type(priceInput, '100');
|
|
256
358
|
|
|
257
|
-
await submitForm(
|
|
359
|
+
await submitForm();
|
|
258
360
|
|
|
259
|
-
expect(screen.
|
|
361
|
+
expect(await screen.findByText('Service name is required')).toBeInTheDocument();
|
|
260
362
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
261
363
|
});
|
|
262
364
|
|
|
263
365
|
test('should accept valid decimal price values', async () => {
|
|
264
366
|
const user = userEvent.setup();
|
|
265
|
-
|
|
367
|
+
renderBillableServicesForm();
|
|
266
368
|
|
|
267
369
|
await fillRequiredFields(user, { skipPrice: true });
|
|
268
370
|
|
|
@@ -271,7 +373,7 @@ describe('AddBillableService', () => {
|
|
|
271
373
|
|
|
272
374
|
mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
273
375
|
|
|
274
|
-
await submitForm(
|
|
376
|
+
await submitForm();
|
|
275
377
|
|
|
276
378
|
expect(screen.queryByText('Price is required')).not.toBeInTheDocument();
|
|
277
379
|
expect(screen.queryByText('Price must be greater than 0')).not.toBeInTheDocument();
|
|
@@ -294,7 +396,7 @@ describe('AddBillableService', () => {
|
|
|
294
396
|
|
|
295
397
|
test('should show "Service type is required" error when not selected', async () => {
|
|
296
398
|
const user = userEvent.setup();
|
|
297
|
-
|
|
399
|
+
renderBillableServicesForm();
|
|
298
400
|
|
|
299
401
|
await user.type(screen.getByRole('textbox', { name: /Service name/i }), 'Test Service');
|
|
300
402
|
await user.type(screen.getByRole('textbox', { name: /Short name/i }), 'Test Short Name');
|
|
@@ -305,15 +407,15 @@ describe('AddBillableService', () => {
|
|
|
305
407
|
const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
|
|
306
408
|
await user.type(priceInput, '100');
|
|
307
409
|
|
|
308
|
-
await submitForm(
|
|
410
|
+
await submitForm();
|
|
309
411
|
|
|
310
|
-
expect(screen.
|
|
412
|
+
expect(await screen.findByText('Service type is required')).toBeInTheDocument();
|
|
311
413
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
312
414
|
});
|
|
313
415
|
|
|
314
416
|
test('should show "Payment mode is required" error when not selected', async () => {
|
|
315
417
|
const user = userEvent.setup();
|
|
316
|
-
|
|
418
|
+
renderBillableServicesForm();
|
|
317
419
|
|
|
318
420
|
await user.type(screen.getByRole('textbox', { name: /Service name/i }), 'Test Service');
|
|
319
421
|
await user.type(screen.getByRole('textbox', { name: /Short name/i }), 'Test Short Name');
|
|
@@ -324,21 +426,21 @@ describe('AddBillableService', () => {
|
|
|
324
426
|
const priceInput = screen.getByRole('spinbutton', { name: /Selling Price/i });
|
|
325
427
|
await user.type(priceInput, '100');
|
|
326
428
|
|
|
327
|
-
await submitForm(
|
|
429
|
+
await submitForm();
|
|
328
430
|
|
|
329
|
-
expect(screen.
|
|
431
|
+
expect(await screen.findByText('Payment mode is required')).toBeInTheDocument();
|
|
330
432
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
331
433
|
});
|
|
332
434
|
|
|
333
435
|
test('should show "Price is required" error when price field is empty', async () => {
|
|
334
436
|
const user = userEvent.setup();
|
|
335
|
-
|
|
437
|
+
renderBillableServicesForm();
|
|
336
438
|
|
|
337
439
|
await fillRequiredFields(user, { skipPrice: true });
|
|
338
440
|
|
|
339
|
-
await submitForm(
|
|
441
|
+
await submitForm();
|
|
340
442
|
|
|
341
|
-
expect(screen.
|
|
443
|
+
expect(await screen.findByText('Price is required')).toBeInTheDocument();
|
|
342
444
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
343
445
|
});
|
|
344
446
|
});
|
|
@@ -368,17 +470,16 @@ describe('AddBillableService', () => {
|
|
|
368
470
|
};
|
|
369
471
|
|
|
370
472
|
test('should populate form with existing service data', () => {
|
|
371
|
-
|
|
473
|
+
renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
|
|
372
474
|
|
|
373
|
-
expect(screen.getByText('Edit billable service')).toBeInTheDocument();
|
|
374
475
|
expect(screen.getByText('X-Ray Service')).toBeInTheDocument(); // Service name shown as label
|
|
375
476
|
expect(screen.getByDisplayValue('XRay')).toBeInTheDocument(); // Short name
|
|
376
477
|
});
|
|
377
478
|
|
|
378
479
|
test('should call updateBillableService instead of createBillableService', async () => {
|
|
379
480
|
const user = userEvent.setup();
|
|
380
|
-
const
|
|
381
|
-
|
|
481
|
+
const mockCloseWorkspace = jest.fn();
|
|
482
|
+
renderBillableServicesForm({ serviceToEdit: mockServiceToEdit, closeWorkspace: mockCloseWorkspace });
|
|
382
483
|
|
|
383
484
|
const shortNameInput = screen.getByDisplayValue('XRay');
|
|
384
485
|
await user.clear(shortNameInput);
|
|
@@ -386,7 +487,7 @@ describe('AddBillableService', () => {
|
|
|
386
487
|
|
|
387
488
|
mockUpdateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
388
489
|
|
|
389
|
-
await submitForm(
|
|
490
|
+
await submitForm();
|
|
390
491
|
|
|
391
492
|
expect(mockUpdateBillableService).toHaveBeenCalledTimes(1);
|
|
392
493
|
expect(mockUpdateBillableService).toHaveBeenCalledWith('existing-service-uuid', {
|
|
@@ -404,23 +505,21 @@ describe('AddBillableService', () => {
|
|
|
404
505
|
concept: undefined,
|
|
405
506
|
});
|
|
406
507
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
407
|
-
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
408
508
|
});
|
|
409
509
|
|
|
410
|
-
test('should call
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
renderAddBillableService({ serviceToEdit: mockServiceToEdit, onServiceUpdated: mockOnServiceUpdated });
|
|
510
|
+
test('should call onWorkspaceClose callback after successful edit', async () => {
|
|
511
|
+
const mockOnWorkspaceClose = jest.fn();
|
|
512
|
+
renderBillableServicesForm({ serviceToEdit: mockServiceToEdit, onWorkspaceClose: mockOnWorkspaceClose });
|
|
414
513
|
|
|
415
514
|
mockUpdateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
416
515
|
|
|
417
|
-
await submitForm(
|
|
516
|
+
await submitForm();
|
|
418
517
|
|
|
419
|
-
expect(
|
|
518
|
+
expect(mockOnWorkspaceClose).toHaveBeenCalledTimes(1);
|
|
420
519
|
});
|
|
421
520
|
|
|
422
521
|
test('should not allow editing service name in edit mode', () => {
|
|
423
|
-
|
|
522
|
+
renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
|
|
424
523
|
|
|
425
524
|
// Service name should be displayed as a label, not an editable input
|
|
426
525
|
expect(screen.getByText('X-Ray Service')).toBeInTheDocument();
|
|
@@ -431,12 +530,11 @@ describe('AddBillableService', () => {
|
|
|
431
530
|
// Scenario: User opens edit form, but payment modes/service types haven't loaded yet
|
|
432
531
|
// The form should wait for dependencies to load, then populate correctly
|
|
433
532
|
|
|
434
|
-
|
|
533
|
+
renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
|
|
435
534
|
|
|
436
|
-
// After dependencies load (handled by
|
|
535
|
+
// After dependencies load (handled by renderBillableServicesForm's setupMocks),
|
|
437
536
|
// form should display with populated data
|
|
438
|
-
await screen.findByText('
|
|
439
|
-
expect(screen.getByText('X-Ray Service')).toBeInTheDocument();
|
|
537
|
+
expect(await screen.findByText('X-Ray Service')).toBeInTheDocument();
|
|
440
538
|
expect(screen.getByDisplayValue('XRay')).toBeInTheDocument();
|
|
441
539
|
|
|
442
540
|
// This test verifies the useEffect that calls reset() when dependencies load
|
|
@@ -448,7 +546,7 @@ describe('AddBillableService', () => {
|
|
|
448
546
|
describe('Dynamic Payment Options', () => {
|
|
449
547
|
test('should add new payment option when clicking "Add payment option" button', async () => {
|
|
450
548
|
const user = userEvent.setup();
|
|
451
|
-
|
|
549
|
+
renderBillableServicesForm();
|
|
452
550
|
|
|
453
551
|
const addButton = screen.getByRole('button', { name: /Add payment option/i });
|
|
454
552
|
await user.click(addButton);
|
|
@@ -459,7 +557,7 @@ describe('AddBillableService', () => {
|
|
|
459
557
|
|
|
460
558
|
test('should be able to add multiple payment options', async () => {
|
|
461
559
|
const user = userEvent.setup();
|
|
462
|
-
|
|
560
|
+
renderBillableServicesForm();
|
|
463
561
|
|
|
464
562
|
// Add a second payment option
|
|
465
563
|
const addButton = screen.getByRole('button', { name: /Add payment option/i });
|
|
@@ -471,7 +569,7 @@ describe('AddBillableService', () => {
|
|
|
471
569
|
|
|
472
570
|
test('should allow adding multiple payment options with different payment modes', async () => {
|
|
473
571
|
const user = userEvent.setup();
|
|
474
|
-
|
|
572
|
+
renderBillableServicesForm();
|
|
475
573
|
|
|
476
574
|
// Add second payment option
|
|
477
575
|
const addButton = screen.getByRole('button', { name: /Add payment option/i });
|
|
@@ -497,7 +595,7 @@ describe('AddBillableService', () => {
|
|
|
497
595
|
await user.click(screen.getByRole('option', { name: /Lab service/i }));
|
|
498
596
|
|
|
499
597
|
mockCreateBillableService.mockResolvedValue({} as FetchResponse<any>);
|
|
500
|
-
await submitForm(
|
|
598
|
+
await submitForm();
|
|
501
599
|
|
|
502
600
|
expect(mockCreateBillableService).toHaveBeenCalledWith(
|
|
503
601
|
expect.objectContaining({
|
|
@@ -519,7 +617,7 @@ describe('AddBillableService', () => {
|
|
|
519
617
|
|
|
520
618
|
test('should validate each payment option independently', async () => {
|
|
521
619
|
const user = userEvent.setup();
|
|
522
|
-
|
|
620
|
+
renderBillableServicesForm();
|
|
523
621
|
|
|
524
622
|
// Add second payment option
|
|
525
623
|
const addButton = screen.getByRole('button', { name: /Add payment option/i });
|
|
@@ -543,10 +641,10 @@ describe('AddBillableService', () => {
|
|
|
543
641
|
await user.click(screen.getByRole('combobox', { name: /Service type/i }));
|
|
544
642
|
await user.click(screen.getByRole('option', { name: /Lab service/i }));
|
|
545
643
|
|
|
546
|
-
await submitForm(
|
|
644
|
+
await submitForm();
|
|
547
645
|
|
|
548
646
|
// Should show error for the second payment option's missing price
|
|
549
|
-
const priceErrors = screen.
|
|
647
|
+
const priceErrors = await screen.findAllByText('Price is required');
|
|
550
648
|
expect(priceErrors.length).toBeGreaterThan(0);
|
|
551
649
|
expect(mockCreateBillableService).not.toHaveBeenCalled();
|
|
552
650
|
});
|
|
@@ -555,20 +653,19 @@ describe('AddBillableService', () => {
|
|
|
555
653
|
describe('Error Handling', () => {
|
|
556
654
|
test('should display error snackbar when create API call fails', async () => {
|
|
557
655
|
const user = userEvent.setup();
|
|
558
|
-
|
|
656
|
+
renderBillableServicesForm();
|
|
559
657
|
|
|
560
658
|
await fillRequiredFields(user);
|
|
561
659
|
|
|
562
660
|
const errorMessage = 'Network error';
|
|
563
661
|
mockCreateBillableService.mockRejectedValue(new Error(errorMessage));
|
|
564
662
|
|
|
565
|
-
await submitForm(
|
|
663
|
+
await submitForm();
|
|
566
664
|
|
|
567
|
-
// Wait for async operations
|
|
568
|
-
await
|
|
665
|
+
// Wait for async operations to complete
|
|
666
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
569
667
|
|
|
570
668
|
expect(mockCreateBillableService).toHaveBeenCalledTimes(1);
|
|
571
|
-
expect(navigate).not.toHaveBeenCalled();
|
|
572
669
|
});
|
|
573
670
|
|
|
574
671
|
test('should display error snackbar when update API call fails', async () => {
|
|
@@ -596,18 +693,17 @@ describe('AddBillableService', () => {
|
|
|
596
693
|
],
|
|
597
694
|
};
|
|
598
695
|
|
|
599
|
-
|
|
696
|
+
renderBillableServicesForm({ serviceToEdit: mockServiceToEdit });
|
|
600
697
|
|
|
601
698
|
const errorMessage = 'Update failed';
|
|
602
699
|
mockUpdateBillableService.mockRejectedValue(new Error(errorMessage));
|
|
603
700
|
|
|
604
|
-
await submitForm(
|
|
701
|
+
await submitForm();
|
|
605
702
|
|
|
606
|
-
// Wait for async operations
|
|
607
|
-
await
|
|
703
|
+
// Wait for async operations to complete
|
|
704
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
608
705
|
|
|
609
706
|
expect(mockUpdateBillableService).toHaveBeenCalledTimes(1);
|
|
610
|
-
expect(navigate).not.toHaveBeenCalled();
|
|
611
707
|
});
|
|
612
708
|
});
|
|
613
709
|
});
|