Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
DataTable,
DataTableSkeleton,
Dropdown,
OverflowMenu,
OverflowMenuItem,
InlineLoading,
Pagination,
Table,
Expand All @@ -17,11 +19,13 @@ import {
Tag,
Tile,
} from '@carbon/react';
import { Add, Edit } from '@carbon/react/icons';
import { Add } from '@carbon/react/icons';
import {
ErrorState,
isDesktop as desktopLayout,
getCoreTranslation,
launchWorkspace2,
showModal,
useLayoutType,
usePagination,
} from '@openmrs/esm-framework';
Expand Down Expand Up @@ -81,6 +85,17 @@ const BedAdministrationTable: React.FC = () => {
[mutateBedsGroupedByLocation],
);

const openDeleteBedModal = useCallback(
(uuid: string) => {
const dispose = showModal('delete-bed-confirmation-modal', {
uuid,
closeModal: () => dispose(),
mutateBeds: mutateBedsGroupedByLocation,
});
},
[mutateBedsGroupedByLocation],
);

const handleBedStatusChange = ({ selectedItem }: { selectedItem: string }) =>
setFilterOption(selectedItem.trim().toUpperCase());

Expand Down Expand Up @@ -123,18 +138,24 @@ const BedAdministrationTable: React.FC = () => {
occupancyStatus: <CustomTag condition={bed?.status === 'OCCUPIED'} />,
allocationStatus: <CustomTag condition={Boolean(bed.location?.uuid)} />,
actions: (
<Button
renderIcon={Edit}
onClick={() => handleLaunchBedWorkspace('edit', bed)}
kind={'ghost'}
iconDescription={t('editBed', 'Edit bed')}
hasIconOnly
size={responsiveSize}
tooltipPosition="right"
/>
<OverflowMenu flipped size={responsiveSize} aria-label={t('actions', 'Actions')}>
<OverflowMenuItem
className={styles.menuitem}
data-testid={`edit-button-${bed.uuid}`}
itemText={getCoreTranslation('edit')}
onClick={() => handleLaunchBedWorkspace('edit', bed)}
/>
<OverflowMenuItem
className={styles.menuitem}
isDelete
data-testid={`delete-button-${bed.uuid}`}
itemText={getCoreTranslation('delete')}
onClick={() => openDeleteBedModal(bed.uuid)}
/>
</OverflowMenu>
),
}));
}, [handleLaunchBedWorkspace, responsiveSize, paginatedData, t]);
}, [handleLaunchBedWorkspace, openDeleteBedModal, responsiveSize, paginatedData, t]);

if (isLoadingBedsGroupedByLocation && !bedsGroupedByLocation.length) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@
border-top: none !important;
}
}

.menuitem {
max-width: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { showSnackbar } from '@openmrs/esm-framework';
import { deleteBed } from '../../summary/summary.resource';
import DeleteBed from './delete-bed-confirmation.modal';

jest.mock('../../summary/summary.resource', () => ({
deleteBed: jest.fn(),
}));

const mockShowSnackbar = jest.mocked(showSnackbar);
const mockDeleteBed = jest.mocked(deleteBed);

const mockCloseModal = jest.fn();
const mockMutateBeds = jest.fn();

const defaultProps = {
closeModal: mockCloseModal,
uuid: 'test-bed-uuid-123',
mutateBeds: mockMutateBeds,
};

describe('DeleteBed', () => {
it('renders the delete bed confirmation modal correctly', () => {
render(<DeleteBed {...defaultProps} />);

expect(screen.getByText(/Are you sure you want to delete this bed\?/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Reason for deleting the bed/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/Enter a reason for deleting this bed/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
});

it('closes the modal when cancel button is clicked', async () => {
const user = userEvent.setup();

render(<DeleteBed {...defaultProps} />);

await user.click(screen.getByRole('button', { name: /cancel/i }));

expect(mockCloseModal).toHaveBeenCalled();
expect(mockDeleteBed).not.toHaveBeenCalled();
});

it('deletes the bed successfully when delete button is clicked and shows success snackbar ', async () => {
const user = userEvent.setup();
mockDeleteBed.mockResolvedValue({ ok: true } as any);

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockDeleteBed).toHaveBeenCalledWith({ bedId: defaultProps.uuid, reason: 'Test delete reason' });
});

expect(mockMutateBeds).toHaveBeenCalled();
expect(mockShowSnackbar).toHaveBeenCalledWith({
title: 'Bed deleted',
subtitle: 'Bed deleted successfully',
kind: 'success',
});
expect(mockCloseModal).toHaveBeenCalled();
});

it('shows error snackbar when deletion fails', async () => {
const user = userEvent.setup();
const errorMessage = 'Cannot delete bed';
mockDeleteBed.mockRejectedValue({
responseBody: {
error: {
translatedMessage: errorMessage,
},
},
});

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockShowSnackbar).toHaveBeenCalledWith({
title: 'Failed to delete bed',
subtitle: errorMessage,
kind: 'error',
});
});

expect(mockCloseModal).not.toHaveBeenCalled();
});

it('disables delete button when delete reason is empty', async () => {
render(<DeleteBed {...defaultProps} />);

const deleteButton = screen.getByRole('button', { name: /delete/i });
expect(deleteButton).toBeDisabled();
});

it('enables delete button when delete reason is filled', async () => {
const user = userEvent.setup();
render(<DeleteBed {...defaultProps} />);

const deleteButton = screen.getByRole('button', { name: /delete/i });
expect(deleteButton).toBeDisabled();

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

expect(deleteButton).toBeEnabled();
});

it('shows loading state while deleting', async () => {
const user = userEvent.setup();
mockDeleteBed.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

expect(deleteButton).toBeDisabled();
expect(screen.getByText(/deleting\.\.\./i)).toBeInTheDocument();

await waitFor(() => {
expect(mockDeleteBed).toHaveBeenCalled();
});
});

it('disables delete button while deletion is in progress', async () => {
const user = userEvent.setup();
mockDeleteBed.mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
expect(deleteButton).toBeEnabled();

await user.click(deleteButton);

expect(deleteButton).toBeDisabled();
expect(screen.getByText(/deleting\.\.\./i)).toBeInTheDocument();

await waitFor(() => {
expect(mockDeleteBed).toHaveBeenCalled();
});
});

it('shows generic error message when deletion fails without specific error message', async () => {
const user = userEvent.setup();
mockDeleteBed.mockRejectedValue({ responseBody: {} });

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockShowSnackbar).toHaveBeenCalledWith({
title: 'Failed to delete bed',
subtitle: 'Unable to delete bed. Please try again.',
kind: 'error',
});
});
});

it('calls mutateBeds after successful deletion', async () => {
const user = userEvent.setup();
mockDeleteBed.mockResolvedValue({ ok: true } as any);

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockMutateBeds).toHaveBeenCalled();
});
});

it('does not call mutateBeds when deletion fails', async () => {
const localMutateBeds = jest.fn(); // Fresh mock to ensure zero calls

const user = userEvent.setup();
mockDeleteBed.mockRejectedValue(new Error('Network error'));

render(<DeleteBed {...defaultProps} mutateBeds={localMutateBeds} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockShowSnackbar).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'error',
}),
);
});

expect(localMutateBeds).not.toHaveBeenCalled();
});

it('re-enables delete button after failed deletion', async () => {
const user = userEvent.setup();
mockDeleteBed.mockRejectedValue(new Error('Failed'));

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockShowSnackbar).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'error',
}),
);
});

expect(deleteButton).toBeEnabled();
});

it('closes the modal on successful deletion', async () => {
const user = userEvent.setup();
mockDeleteBed.mockResolvedValue({ ok: true } as any);

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockCloseModal).toHaveBeenCalled();
});
});

it('does not closes the modal on failed deletion', async () => {
const user = userEvent.setup();
mockDeleteBed.mockRejectedValue(new Error('Failed'));

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, 'Test delete reason');

const deleteButton = screen.getByRole('button', { name: /delete/i });
await user.click(deleteButton);

await waitFor(() => {
expect(mockCloseModal).not.toHaveBeenCalled();
});
});
});

it('trims void reason before sending to API', async () => {
const user = userEvent.setup();
mockDeleteBed.mockResolvedValue({ ok: true } as any);

render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, ' Test delete reason with spaces ');

await user.click(screen.getByRole('button', { name: /delete/i }));

await waitFor(() => {
expect(mockDeleteBed).toHaveBeenCalledWith({ bedId: defaultProps.uuid, reason: 'Test delete reason with spaces' });
});
});

it('disables delete button when void reason contains only whitespace', async () => {
const user = userEvent.setup();
render(<DeleteBed {...defaultProps} />);

const deleteReasonInput = screen.getByLabelText(/Reason for deleting the bed/i);
await user.type(deleteReasonInput, ' ');

const deleteButton = screen.getByRole('button', { name: /delete/i });
expect(deleteButton).toBeDisabled();
});
Loading