From 1a5235249bfc35452ec73878461d7bb88ae0b407 Mon Sep 17 00:00:00 2001 From: Trevor Antony Date: Mon, 23 Sep 2024 15:07:29 +0300 Subject: [PATCH 1/3] Add Tests for stock item bulk import --- .../stock-items-bulk-import.component.tsx | 14 +- .../stock-items-bulk-import.test.tsx | 128 ++++++++++++++++++ 2 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx diff --git a/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx index 2d35ab0a..1fec757a 100644 --- a/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx +++ b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx @@ -59,10 +59,18 @@ const ImportDialogPopup: React.FC = ({ const onFileChanged = (event: React.ChangeEvent) => { const file = event?.target?.files?.[0]; - if (file) { + if (file && file.size <= 2 * 1024 * 1024) { + // 2MB in bytes setSelectedFile(file); } else { - event.preventDefault(); + setSelectedFile(null); + event.target.value = ""; // Clear the file input + showSnackbar({ + title: t("fileSizeError", "File size error"), + kind: "error", + isLowContrast: true, + subtitle: t("fileSizeErrorMessage", "File size must be 2MB or less"), + }); } }; @@ -82,7 +90,7 @@ const ImportDialogPopup: React.FC = ({ labelDescription="Only .csv files at 2mb or less" filenameStatus="edit" labelTitle="" - size="small" + size="sm" onChange={onFileChanged} /> diff --git a/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx new file mode 100644 index 00000000..524e92d8 --- /dev/null +++ b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ImportDialogPopup from './stock-items-bulk-import.component'; +import { showSnackbar } from '@openmrs/esm-framework'; +import { UploadStockItems } from './stock-items-bulk-import.resource'; + +jest.mock('@openmrs/esm-framework', () => ({ + showSnackbar: jest.fn(), +})); + +jest.mock('./stock-items-bulk-import.resource', () => ({ + UploadStockItems: jest.fn(), +})); + +describe('ImportDialogPopup', () => { + const mockCloseModal = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders with initial state and UI elements', () => { + render(); + + expect(screen.getByText('Import Stock Items')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Select file' })).toBeInTheDocument(); + expect(screen.getByText('Only .csv files at 2mb or less')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Upload StockItems' })).toBeInTheDocument(); + }); + + it('allows only CSV files', async () => { + render(); + + const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; + expect(fileInput.accept).toBe('.csv'); + }); + + it('does not allow files larger than 2MB', async () => { + render(); + + const largeFile = new File(['x'.repeat(2 * 1024 * 1024 + 1)], 'large.csv', { type: 'text/csv' }); + const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; + + await userEvent.upload(fileInput, largeFile); + + expect(fileInput.files).toHaveLength(0); + expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'error', + title: 'File size error', + })); + }); + + it('allows files 2MB or smaller', async () => { + render(); + + const validFile = new File(['x'.repeat(2 * 1024 * 1024)], 'valid.csv', { type: 'text/csv' }); + const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; + + await userEvent.upload(fileInput, validFile); + + expect(fileInput.files).toHaveLength(1); + expect(fileInput.files[0]).toBe(validFile); + }); + + it('closes modal when cancel button is clicked', async () => { + render(); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + await userEvent.click(cancelButton); + + expect(mockCloseModal).toHaveBeenCalledTimes(1); + }); + + it('does nothing when upload is clicked without a file', async () => { + render(); + + const uploadButton = screen.getByRole('button', { name: 'Upload StockItems' }); + await userEvent.click(uploadButton); + + expect(UploadStockItems).not.toHaveBeenCalled(); + expect(showSnackbar).not.toHaveBeenCalled(); + expect(mockCloseModal).not.toHaveBeenCalled(); + }); + + it('uploads file successfully and shows success snackbar', async () => { + (UploadStockItems as jest.Mock).mockResolvedValue({}); + render(); + + const validFile = new File(['test content'], 'valid.csv', { type: 'text/csv' }); + const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; + await userEvent.upload(fileInput, validFile); + + const uploadButton = screen.getByRole('button', { name: 'Upload StockItems' }); + await userEvent.click(uploadButton); + + await waitFor(() => { + expect(UploadStockItems).toHaveBeenCalled(); + expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'success', + title: 'Uploaded Order', + })); + expect(mockCloseModal).toHaveBeenCalled(); + }); + }); + + it('shows error snackbar on upload failure', async () => { + (UploadStockItems as jest.Mock).mockRejectedValue(new Error('Upload failed')); + render(); + + const validFile = new File(['test content'], 'valid.csv', { type: 'text/csv' }); + const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; + await userEvent.upload(fileInput, validFile); + + const uploadButton = screen.getByRole('button', { name: 'Upload StockItems' }); + await userEvent.click(uploadButton); + + await waitFor(() => { + expect(UploadStockItems).toHaveBeenCalled(); + expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ + kind: 'error', + title: 'An error occurred uploading stock items', + })); + expect(mockCloseModal).not.toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file From 8e4fafc3816b8a587262e420575305cfca168283 Mon Sep 17 00:00:00 2001 From: Trevor Antony <61781670+TrevorAntony@users.noreply.github.com> Date: Mon, 23 Sep 2024 15:11:35 +0300 Subject: [PATCH 2/3] Remove comments --- .../add-bulk-stock-item/stock-items-bulk-import.component.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx index 1fec757a..16e84b66 100644 --- a/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx +++ b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.component.tsx @@ -60,11 +60,10 @@ const ImportDialogPopup: React.FC = ({ const onFileChanged = (event: React.ChangeEvent) => { const file = event?.target?.files?.[0]; if (file && file.size <= 2 * 1024 * 1024) { - // 2MB in bytes setSelectedFile(file); } else { setSelectedFile(null); - event.target.value = ""; // Clear the file input + event.target.value = ""; showSnackbar({ title: t("fileSizeError", "File size error"), kind: "error", From da7c28d8e47d0759cf4efac9b922cf88df5df4dd Mon Sep 17 00:00:00 2001 From: Trevor Antony Date: Mon, 30 Sep 2024 15:19:30 +0300 Subject: [PATCH 3/3] no wait-for multiple assertions --- .../stock-items-bulk-import.test.tsx | 64 ++++++++++--------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx index 524e92d8..567660b6 100644 --- a/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx +++ b/src/stock-items/add-bulk-stock-item/stock-items-bulk-import.test.tsx @@ -22,7 +22,7 @@ describe('ImportDialogPopup', () => { it('renders with initial state and UI elements', () => { render(); - + expect(screen.getByText('Import Stock Items')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Select file' })).toBeInTheDocument(); expect(screen.getByText('Only .csv files at 2mb or less')).toBeInTheDocument(); @@ -32,29 +32,31 @@ describe('ImportDialogPopup', () => { it('allows only CSV files', async () => { render(); - + const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; expect(fileInput.accept).toBe('.csv'); }); it('does not allow files larger than 2MB', async () => { render(); - + const largeFile = new File(['x'.repeat(2 * 1024 * 1024 + 1)], 'large.csv', { type: 'text/csv' }); const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; await userEvent.upload(fileInput, largeFile); expect(fileInput.files).toHaveLength(0); - expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ - kind: 'error', - title: 'File size error', - })); + expect(showSnackbar).toHaveBeenCalledWith( + expect.objectContaining({ + kind: 'error', + title: 'File size error', + }), + ); }); it('allows files 2MB or smaller', async () => { render(); - + const validFile = new File(['x'.repeat(2 * 1024 * 1024)], 'valid.csv', { type: 'text/csv' }); const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; @@ -66,7 +68,7 @@ describe('ImportDialogPopup', () => { it('closes modal when cancel button is clicked', async () => { render(); - + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); await userEvent.click(cancelButton); @@ -75,7 +77,7 @@ describe('ImportDialogPopup', () => { it('does nothing when upload is clicked without a file', async () => { render(); - + const uploadButton = screen.getByRole('button', { name: 'Upload StockItems' }); await userEvent.click(uploadButton); @@ -87,7 +89,7 @@ describe('ImportDialogPopup', () => { it('uploads file successfully and shows success snackbar', async () => { (UploadStockItems as jest.Mock).mockResolvedValue({}); render(); - + const validFile = new File(['test content'], 'valid.csv', { type: 'text/csv' }); const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; await userEvent.upload(fileInput, validFile); @@ -95,20 +97,22 @@ describe('ImportDialogPopup', () => { const uploadButton = screen.getByRole('button', { name: 'Upload StockItems' }); await userEvent.click(uploadButton); - await waitFor(() => { - expect(UploadStockItems).toHaveBeenCalled(); - expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ - kind: 'success', - title: 'Uploaded Order', - })); - expect(mockCloseModal).toHaveBeenCalled(); - }); + await waitFor(() => expect(UploadStockItems).toHaveBeenCalled()); + await waitFor(() => + expect(showSnackbar).toHaveBeenCalledWith( + expect.objectContaining({ + kind: 'success', + title: 'Uploaded Order', + }), + ), + ); + await waitFor(() => expect(mockCloseModal).toHaveBeenCalledTimes(1)); }); it('shows error snackbar on upload failure', async () => { (UploadStockItems as jest.Mock).mockRejectedValue(new Error('Upload failed')); render(); - + const validFile = new File(['test content'], 'valid.csv', { type: 'text/csv' }); const fileInput = screen.getByLabelText('Select file') as HTMLInputElement; await userEvent.upload(fileInput, validFile); @@ -116,13 +120,15 @@ describe('ImportDialogPopup', () => { const uploadButton = screen.getByRole('button', { name: 'Upload StockItems' }); await userEvent.click(uploadButton); - await waitFor(() => { - expect(UploadStockItems).toHaveBeenCalled(); - expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ - kind: 'error', - title: 'An error occurred uploading stock items', - })); - expect(mockCloseModal).not.toHaveBeenCalled(); - }); + await waitFor(() => expect(UploadStockItems).toHaveBeenCalled()); + await waitFor(() => + expect(showSnackbar).toHaveBeenCalledWith( + expect.objectContaining({ + kind: 'error', + title: 'An error occurred uploading stock items', + }), + ), + ); + await waitFor(() => expect(mockCloseModal).not.toHaveBeenCalled()); }); -}); \ No newline at end of file +});