@openmrs/esm-billing-app 1.0.2-pre.761 → 1.0.2-pre.767

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.
@@ -1,27 +1,20 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
- import { render, screen, act } from '@testing-library/react';
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
4
  import { getDefaultsFromConfigSchema, showModal, useConfig } from '@openmrs/esm-framework';
5
5
  import { type MappedBill } from '../types';
6
6
  import { configSchema, type BillingConfig } from '../config-schema';
7
7
  import InvoiceTable from './invoice-table.component';
8
8
 
9
9
  const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
10
+ const mockShowModal = jest.mocked(showModal);
10
11
 
11
12
  jest.mock('../helpers', () => ({
12
13
  convertToCurrency: jest.fn((price) => `USD ${price}`),
13
14
  }));
14
15
 
15
16
  describe('InvoiceTable', () => {
16
- beforeEach(() => {
17
- mockUseConfig.mockReturnValue({
18
- ...getDefaultsFromConfigSchema(configSchema),
19
- defaultCurrency: 'USD',
20
- showEditBillButton: true,
21
- });
22
- });
23
-
24
- const bill: MappedBill = {
17
+ const defaultBill: MappedBill = {
25
18
  uuid: 'bill-uuid',
26
19
  id: 123,
27
20
  patientUuid: 'patient-uuid',
@@ -76,50 +69,294 @@ describe('InvoiceTable', () => {
76
69
  tenderedAmount: 300,
77
70
  };
78
71
 
79
- it('renders the table and displays line items correctly', () => {
80
- render(<InvoiceTable bill={bill} />);
72
+ beforeEach(() => {
73
+ mockUseConfig.mockReturnValue({
74
+ ...getDefaultsFromConfigSchema(configSchema),
75
+ defaultCurrency: 'USD',
76
+ });
77
+ });
78
+
79
+ it('should render table headers correctly', () => {
80
+ render(<InvoiceTable bill={defaultBill} />);
81
+
82
+ expect(screen.getByText(/line items/i)).toBeInTheDocument();
83
+ expect(screen.getByText(/items to be billed/i)).toBeInTheDocument();
84
+ expect(screen.getByRole('columnheader', { name: /no/i })).toBeInTheDocument();
85
+ expect(screen.getByRole('columnheader', { name: /bill item/i })).toBeInTheDocument();
86
+ expect(screen.getByRole('columnheader', { name: /bill code/i })).toBeInTheDocument();
87
+ expect(screen.getByRole('columnheader', { name: /status/i })).toBeInTheDocument();
88
+ expect(screen.getByRole('columnheader', { name: /quantity/i })).toBeInTheDocument();
89
+ expect(screen.getByRole('columnheader', { name: /price/i })).toBeInTheDocument();
90
+ expect(screen.getByRole('columnheader', { name: /total/i })).toBeInTheDocument();
91
+ });
92
+
93
+ it('should render line items correctly', () => {
94
+ render(<InvoiceTable bill={defaultBill} />);
81
95
 
82
96
  expect(screen.getByText('Item 1')).toBeInTheDocument();
83
97
  expect(screen.getByText('Item 2')).toBeInTheDocument();
84
98
  expect(screen.getByTestId('receipt-number-0')).toHaveTextContent('12345');
99
+ expect(screen.getByTestId('receipt-number-1')).toHaveTextContent('12345');
100
+ });
101
+
102
+ it('should display loading skeleton when bill is loading', () => {
103
+ render(<InvoiceTable bill={defaultBill} isLoadingBill={true} />);
104
+
105
+ expect(screen.getByTestId('loader')).toBeInTheDocument();
106
+ expect(screen.queryByText(/line items/i)).not.toBeInTheDocument();
107
+ });
108
+
109
+ it('should display payment status for each line item', () => {
110
+ render(<InvoiceTable bill={defaultBill} />);
111
+
112
+ expect(screen.getByText('PAID')).toBeInTheDocument();
113
+ expect(screen.getByText('PENDING')).toBeInTheDocument();
85
114
  });
86
115
 
87
- it('renders the edit button and calls showModal when clicked', async () => {
116
+ it('should display correct quantities', () => {
117
+ render(<InvoiceTable bill={defaultBill} />);
118
+
119
+ // Item 1 has quantity 1, Item 2 has quantity 2
120
+ const rows = screen.getAllByRole('row');
121
+ expect(rows).toHaveLength(3); // Header row + 2 data rows
122
+ });
123
+
124
+ it('should calculate and display line item totals correctly', () => {
125
+ const billWithCalculation: MappedBill = {
126
+ ...defaultBill,
127
+ lineItems: [
128
+ {
129
+ uuid: '1',
130
+ item: 'Service A',
131
+ paymentStatus: 'PENDING',
132
+ quantity: 3,
133
+ price: 100,
134
+ display: '',
135
+ voided: false,
136
+ voidReason: '',
137
+ billableService: 'Service A',
138
+ priceName: '',
139
+ priceUuid: '',
140
+ lineItemOrder: 0,
141
+ resourceVersion: '',
142
+ },
143
+ ],
144
+ };
145
+
146
+ render(<InvoiceTable bill={billWithCalculation} />);
147
+
148
+ // Total should be 3 * 100 = 300
149
+ expect(screen.getByText('USD 300')).toBeInTheDocument();
150
+ });
151
+
152
+ it('should render edit buttons for all line items', () => {
153
+ render(<InvoiceTable bill={defaultBill} />);
154
+
155
+ const editButton1 = screen.getByTestId('edit-button-1');
156
+ const editButton2 = screen.getByTestId('edit-button-2');
157
+
158
+ expect(editButton1).toBeInTheDocument();
159
+ expect(editButton2).toBeInTheDocument();
160
+ });
161
+
162
+ it('should open edit modal when edit button is clicked', async () => {
88
163
  const user = userEvent.setup();
89
- render(<InvoiceTable bill={bill} />);
164
+ render(<InvoiceTable bill={defaultBill} />);
90
165
 
91
166
  const editButton = screen.getByTestId('edit-button-1');
92
167
  await user.click(editButton);
93
- expect(showModal).toHaveBeenCalledWith('edit-bill-line-item-dialog', expect.anything());
168
+
169
+ expect(mockShowModal).toHaveBeenCalledTimes(1);
170
+ expect(mockShowModal).toHaveBeenCalledWith(
171
+ 'edit-bill-line-item-dialog',
172
+ expect.objectContaining({
173
+ bill: defaultBill,
174
+ item: expect.objectContaining({ uuid: '1' }),
175
+ }),
176
+ );
94
177
  });
95
178
 
96
- it('displays a skeleton loader when the bill is loading', () => {
97
- render(<InvoiceTable bill={bill} isLoadingBill={true} />);
179
+ it('should filter line items based on search term', async () => {
180
+ const user = userEvent.setup();
181
+ render(<InvoiceTable bill={defaultBill} />);
98
182
 
99
- expect(screen.getByTestId('loader')).toBeInTheDocument();
183
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
184
+ await user.type(searchInput, 'Item 2');
185
+
186
+ await waitFor(() => {
187
+ expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
188
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
189
+ });
100
190
  });
101
191
 
102
- it('filters line items based on the search term', async () => {
192
+ it('should show all items when search is cleared', async () => {
103
193
  const user = userEvent.setup();
104
- render(<InvoiceTable bill={bill} />);
194
+ render(<InvoiceTable bill={defaultBill} />);
195
+
105
196
  const searchInput = screen.getByPlaceholderText(/search this table/i);
106
197
 
107
- await user.type(searchInput, 'Item 2');
198
+ // Search for Item 1
199
+ await user.type(searchInput, 'Item 1');
108
200
 
109
- expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
110
- expect(screen.getByText('Item 2')).toBeInTheDocument();
201
+ await waitFor(() => {
202
+ expect(screen.queryByText('Item 2')).not.toBeInTheDocument();
203
+ });
204
+
205
+ // Clear search
206
+ await user.clear(searchInput);
207
+
208
+ await waitFor(() => {
209
+ expect(screen.getByText('Item 1')).toBeInTheDocument();
210
+ expect(screen.getByText('Item 2')).toBeInTheDocument();
211
+ });
212
+ });
213
+
214
+ it('should display empty state when no line items exist', () => {
215
+ const emptyBill: MappedBill = {
216
+ ...defaultBill,
217
+ lineItems: [],
218
+ };
219
+
220
+ render(<InvoiceTable bill={emptyBill} />);
221
+
222
+ expect(screen.getByText(/no matching items to display/i)).toBeInTheDocument();
223
+ expect(screen.getByText(/check the filters above/i)).toBeInTheDocument();
111
224
  });
112
225
 
113
- it('resets isRedirecting to false after timeout', async () => {
226
+ it('should show empty state when search has no results', async () => {
114
227
  const user = userEvent.setup();
115
- render(<InvoiceTable bill={bill} />);
228
+ render(<InvoiceTable bill={defaultBill} />);
116
229
 
117
- const button = screen.getByTestId('edit-button-1');
118
- await user.click(button);
119
- act(() => {
120
- jest.advanceTimersByTime(1000);
230
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
231
+ await user.type(searchInput, 'NonexistentItem');
232
+
233
+ await waitFor(() => {
234
+ expect(screen.getByText(/no matching items to display/i)).toBeInTheDocument();
235
+ expect(screen.getByText(/check the filters above/i)).toBeInTheDocument();
121
236
  });
237
+ });
238
+
239
+ it('should handle line items with zero price', () => {
240
+ const billWithZeroPrice: MappedBill = {
241
+ ...defaultBill,
242
+ lineItems: [
243
+ {
244
+ uuid: '1',
245
+ item: 'Free Service',
246
+ paymentStatus: 'PAID',
247
+ quantity: 1,
248
+ price: 0,
249
+ display: '',
250
+ voided: false,
251
+ voidReason: '',
252
+ billableService: 'Free Service',
253
+ priceName: '',
254
+ priceUuid: '',
255
+ lineItemOrder: 0,
256
+ resourceVersion: '',
257
+ },
258
+ ],
259
+ };
122
260
 
123
- expect(button).not.toBeDisabled();
261
+ render(<InvoiceTable bill={billWithZeroPrice} />);
262
+
263
+ // USD 0 appears for both price and total
264
+ expect(screen.getAllByText('USD 0').length).toBeGreaterThan(0);
265
+ });
266
+
267
+ it('should handle line items with zero quantity', () => {
268
+ const billWithZeroQuantity: MappedBill = {
269
+ ...defaultBill,
270
+ lineItems: [
271
+ {
272
+ uuid: '1',
273
+ item: 'Service',
274
+ paymentStatus: 'PENDING',
275
+ quantity: 0,
276
+ price: 100,
277
+ display: '',
278
+ voided: false,
279
+ voidReason: '',
280
+ billableService: 'Service',
281
+ priceName: '',
282
+ priceUuid: '',
283
+ lineItemOrder: 0,
284
+ resourceVersion: '',
285
+ },
286
+ ],
287
+ };
288
+
289
+ render(<InvoiceTable bill={billWithZeroQuantity} />);
290
+
291
+ // Total should be 0 * 100 = 0
292
+ expect(screen.getByText('USD 0')).toBeInTheDocument();
293
+ });
294
+
295
+ it('should use billableService name when available, otherwise use item name', () => {
296
+ const billWithBillableService: MappedBill = {
297
+ ...defaultBill,
298
+ lineItems: [
299
+ {
300
+ uuid: '1',
301
+ item: 'Item Name',
302
+ billableService: 'Billable Service Name',
303
+ paymentStatus: 'PAID',
304
+ quantity: 1,
305
+ price: 100,
306
+ display: '',
307
+ voided: false,
308
+ voidReason: '',
309
+ priceName: '',
310
+ priceUuid: '',
311
+ lineItemOrder: 0,
312
+ resourceVersion: '',
313
+ },
314
+ {
315
+ uuid: '2',
316
+ item: 'Item Without Billable',
317
+ billableService: '',
318
+ paymentStatus: 'PENDING',
319
+ quantity: 1,
320
+ price: 200,
321
+ display: '',
322
+ voided: false,
323
+ voidReason: '',
324
+ priceName: '',
325
+ priceUuid: '',
326
+ lineItemOrder: 1,
327
+ resourceVersion: '',
328
+ },
329
+ ],
330
+ };
331
+
332
+ render(<InvoiceTable bill={billWithBillableService} />);
333
+
334
+ expect(screen.getByText('Billable Service Name')).toBeInTheDocument();
335
+ expect(screen.getByText('Item Without Billable')).toBeInTheDocument();
336
+ });
337
+
338
+ it('should display line item numbers starting from 1', () => {
339
+ render(<InvoiceTable bill={defaultBill} />);
340
+
341
+ // Check the table body for numbered rows
342
+ const rows = screen.getAllByRole('row');
343
+ // First row is header, so data rows start at index 1
344
+ expect(rows.length).toBeGreaterThan(2);
345
+ });
346
+
347
+ it('should pass correct currency to convertToCurrency helper', () => {
348
+ render(<InvoiceTable bill={defaultBill} />);
349
+
350
+ // Verify prices are formatted with USD - multiple occurrences expected
351
+ expect(screen.getAllByText('USD 100').length).toBeGreaterThan(0);
352
+ expect(screen.getAllByText('USD 200').length).toBeGreaterThan(0);
353
+ });
354
+
355
+ it('should render search input in expanded state', () => {
356
+ render(<InvoiceTable bill={defaultBill} />);
357
+
358
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
359
+ expect(searchInput).toBeInTheDocument();
360
+ expect(searchInput).toBeVisible();
124
361
  });
125
362
  });