diff --git a/cypress/e2e/filtering.cy.ts b/cypress/e2e/filtering.cy.ts index 4df83fa8..0001883d 100644 --- a/cypress/e2e/filtering.cy.ts +++ b/cypress/e2e/filtering.cy.ts @@ -684,5 +684,39 @@ describe('Filtering Component', () => { ); cy.findByRole('button', { name: 'Save' }).should('not.be.disabled'); }); + + it('delete favourite filter', () => { + cy.findByRole('button', { + name: 'Delete test 1 favourite filter', + }).click(); + + cy.startSnoopingBrowserMockedRequest(); + cy.findByRole('button', { name: 'Continue' }).click(); + + cy.findBrowserMockedRequests({ + method: 'DELETE', + url: '/users/filters/:id', + }).should((deleteRequests) => { + expect(deleteRequests.length).equal(1); + }); + }); + + it('display api error message for delete', () => { + cy.findByRole('button', { + name: 'Delete test 3 favourite filter', + }).click(); + + cy.startSnoopingBrowserMockedRequest(); + cy.findByRole('button', { name: 'Continue' }).click(); + + cy.findBrowserMockedRequests({ + method: 'DELETE', + url: '/users/filters/:id', + }).should((deleteRequests) => { + expect(deleteRequests.length).equal(1); + }); + + cy.findByText('error').should('exist'); + }); }); }); diff --git a/e2e/real/filtering.spec.ts b/e2e/real/filtering.spec.ts index b3b8ae65..b4176c96 100644 --- a/e2e/real/filtering.spec.ts +++ b/e2e/real/filtering.spec.ts @@ -97,7 +97,7 @@ test('should be able to add a multiple filters', async ({ page }) => { await expect(page.getByText('1–7 of 7')).toBeVisible(); }); -test('CRU favourite filter', async ({ page }) => { +test('CRUD favourite filter', async ({ page }) => { await page.getByRole('button', { name: 'Filters' }).click(); await page.getByText('Favourite filters').click(); @@ -151,4 +151,14 @@ test('CRU favourite filter', async ({ page }) => { // Assert the changes were successful (checking 'test 1' and updated filter value) const updatedTextField = page.locator('input[value="test 1"]'); await expect(updatedTextField).toHaveValue('test 1'); + + await page + .getByRole('button', { name: 'Delete test 1 favourite filter' }) + .click(); + + await page.getByRole('button', { name: 'Continue' }).click(); + + expect( + await page.getByRole('button', { name: 'Delete test 1 favourite filter' }) + ).not.toBeInViewport(); }); diff --git a/src/api/favouriteFilters.test.tsx b/src/api/favouriteFilters.test.tsx index 8f6084cc..2854ab95 100644 --- a/src/api/favouriteFilters.test.tsx +++ b/src/api/favouriteFilters.test.tsx @@ -3,6 +3,7 @@ import { FavouriteFilterPatch, FavouriteFilterPost } from '../app.types'; import { hooksWrapperWithProviders } from '../testUtils'; import { useAddFavouriteFilter, + useDeleteFavouriteFilter, useEditFavouriteFilter, } from './favouriteFilters'; @@ -33,10 +34,6 @@ describe('favourite filters api functions', () => { expect(result.current.data).toEqual('1'); }); - - it.todo( - 'sends axios request to add favourite filters for a user and throws an appropriate error on failure' - ); }); describe('useEditFavouriteFilter', () => { @@ -60,9 +57,22 @@ describe('favourite filters api functions', () => { expect(result.current.data).toEqual('Updated 1'); }); + }); + + describe('useDeleteFavouriteFilter', () => { + it('delete request to delete user session and returns successful response', async () => { + const { result } = renderHook(() => useDeleteFavouriteFilter(), { + wrapper: hooksWrapperWithProviders(), + }); + expect(result.current.isIdle).toBe(true); - it.todo( - 'sends axios request to edit favourite filters for a user and throws an appropriate error on failure' - ); + result.current.mutate('1'); + + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + + expect(result.current.data).toEqual(''); + }); }); }); diff --git a/src/api/favouriteFilters.tsx b/src/api/favouriteFilters.tsx index 0ec6db78..75d61248 100644 --- a/src/api/favouriteFilters.tsx +++ b/src/api/favouriteFilters.tsx @@ -127,3 +127,31 @@ export const useFavouriteFilters = (): UseQueryResult< }, }); }; + +const deleteFavouriteFilter = (apiUrl: string, id: string): Promise => { + return axios + .delete(`${apiUrl}/users/filters/${id}`, { + headers: { + Authorization: `Bearer ${readSciGatewayToken()}`, + }, + }) + .then((response) => response.data); +}; + +export const useDeleteFavouriteFilter = (): UseMutationResult< + void, + AxiosError, + string +> => { + const { apiUrl } = useAppSelector(selectUrls); + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => deleteFavouriteFilter(apiUrl, id), + onError: (error) => { + console.log('Got error ' + error.message); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['favouriteFilters'] }); + }, + }); +}; diff --git a/src/filtering/deleteFavouriteFilterDialogue.component.test.tsx b/src/filtering/deleteFavouriteFilterDialogue.component.test.tsx new file mode 100644 index 00000000..fffdf6b9 --- /dev/null +++ b/src/filtering/deleteFavouriteFilterDialogue.component.test.tsx @@ -0,0 +1,86 @@ +import { RenderResult, screen, waitFor } from '@testing-library/react'; +import userEvent, { UserEvent } from '@testing-library/user-event'; +import favouriteFiltersJson from '../mocks/favouriteFilters.json'; +import { renderComponentWithProviders } from '../testUtils'; +import DeleteFavouriteFilterDialogue, { + DeleteFavouriteFilterDialogueProps, +} from './deleteFavouriteFilterDialogue.component'; + +describe('delete favourite filter dialogue', () => { + let props: DeleteFavouriteFilterDialogueProps; + let user: UserEvent; + const onClose = vi.fn(); + + const createView = (): RenderResult => { + return renderComponentWithProviders( + + ); + }; + + beforeEach(() => { + props = { + open: true, + onClose: onClose, + favouriteFilter: favouriteFiltersJson[0], + }; + user = userEvent.setup(); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + it('renders correctly', async () => { + createView(); + expect(screen.getByText('Delete Favourite filter')).toBeInTheDocument(); + expect( + screen.getByTestId('delete-favourite-filter-name') + ).toHaveTextContent('test'); + }); + + it('calls onClose when Close button is clicked', async () => { + createView(); + const closeButton = screen.getByRole('button', { name: 'Close' }); + await user.click(closeButton); + + await waitFor(() => { + expect(onClose).toHaveBeenCalled(); + }); + }); + + it('displays warning message when favourite filter data is not loaded', async () => { + props = { + ...props, + favouriteFilter: undefined, + }; + createView(); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + await user.click(continueButton); + const helperTexts = screen.getByText( + 'No data provided, Please refresh and try again' + ); + expect(helperTexts).toBeInTheDocument(); + expect(onClose).not.toHaveBeenCalled(); + }); + + it('displays warning message when api errors', async () => { + props = { + ...props, + favouriteFilter: favouriteFiltersJson[2], + }; + createView(); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + await user.click(continueButton); + const helperTexts = screen.getByText('error'); + expect(helperTexts).toBeInTheDocument(); + expect(onClose).not.toHaveBeenCalled(); + }); + + it('calls handleDeleteFavouriteFilter when continue button is clicked with a valid favourite filter name', async () => { + createView(); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + await user.click(continueButton); + + await waitFor(() => { + expect(onClose).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/filtering/deleteFavouriteFilterDialogue.component.tsx b/src/filtering/deleteFavouriteFilterDialogue.component.tsx new file mode 100644 index 00000000..39a878ec --- /dev/null +++ b/src/filtering/deleteFavouriteFilterDialogue.component.tsx @@ -0,0 +1,91 @@ +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormHelperText, +} from '@mui/material'; +import React, { useState } from 'react'; + +import { AxiosError } from 'axios'; +import { useDeleteFavouriteFilter } from '../api/favouriteFilters'; +import { FavouriteFilter } from '../app.types'; + +export interface DeleteFavouriteFilterDialogueProps { + open: boolean; + onClose: () => void; + favouriteFilter: FavouriteFilter | undefined; +} + +const DeleteFavouriteFilterDialogue = ( + props: DeleteFavouriteFilterDialogueProps +) => { + const { open, onClose, favouriteFilter } = props; + + const [error, setError] = useState(false); + const [errorMessage, setErrorMessage] = React.useState( + undefined + ); + + const handleClose = React.useCallback(() => { + onClose(); + setError(false); + setErrorMessage(undefined); + }, [onClose]); + + const { mutateAsync: deleteFavouriteFilter } = useDeleteFavouriteFilter(); + + const handleDeleteFavouriteFilter = React.useCallback(() => { + if (favouriteFilter) { + deleteFavouriteFilter(favouriteFilter._id) + .then(() => { + handleClose(); + }) + .catch((error: AxiosError) => { + setError(true); + setErrorMessage((error.response?.data as { detail: string }).detail); + }); + } else { + setError(true); + setErrorMessage('No data provided, Please refresh and try again'); + } + }, [deleteFavouriteFilter, handleClose, favouriteFilter]); + + return ( + + Delete Favourite filter + + Are you sure you want to delete{' '} + + {favouriteFilter?.name} + + ? + + + + + + {error && ( + + + {errorMessage} + + + )} + + ); +}; + +export default DeleteFavouriteFilterDialogue; diff --git a/src/filtering/filterDialogue.component.test.tsx b/src/filtering/filterDialogue.component.test.tsx index 126a55bc..96be3e21 100644 --- a/src/filtering/filterDialogue.component.test.tsx +++ b/src/filtering/filterDialogue.component.test.tsx @@ -57,7 +57,7 @@ describe('Filter dialogue component', () => { expect(baseElement).toMatchSnapshot(); }); - it('opens and closes Add new favourite filter dialogue', async () => { + it('opens and closes edit favourite filter dialogue', async () => { createView(); await user.click(screen.getByText('Favourite filters')); @@ -75,6 +75,24 @@ describe('Filter dialogue component', () => { }); }); + it('opens and closes delete favourite filter dialogue', async () => { + createView(); + + await user.click(screen.getByText('Favourite filters')); + await user.click( + await screen.findByRole('button', { + name: 'Delete test 1 favourite filter', + }) + ); + + expect(screen.getAllByText('Close').length).toEqual(2); + + await user.click(screen.getAllByText('Close')[1]); + await waitFor(() => { + expect(screen.getAllByText('Close').length).toEqual(1); + }); + }); + it('opens and closes edit new favourite filter dialogue', async () => { createView(); diff --git a/src/filtering/filterDialogue.component.tsx b/src/filtering/filterDialogue.component.tsx index ae80f302..800a4b0c 100644 --- a/src/filtering/filterDialogue.component.tsx +++ b/src/filtering/filterDialogue.component.tsx @@ -30,6 +30,7 @@ import { } from '../state/slices/filterSlice'; import { selectSearchParams } from '../state/slices/searchSlice'; import { a11yProps, StyledTab, TabPanel } from '../views/viewTabs.component'; +import DeleteFavouriteFilterDialogue from './deleteFavouriteFilterDialogue.component'; import FavouriteFiltersDialogue from './favouriteFiltersDialogue.component'; import FilterInput from './filterInput.component'; import { parseFilter, Token } from './filterParser'; @@ -172,8 +173,11 @@ const FilterDialogue = (props: FilterDialogueProps) => { false | 'post' | 'patch' >(false); - const [selectedFavouriteFilters, setSelectedFavouriteFilters] = - React.useState(undefined); + const [openDeleteDialogue, setOpenDeleteDialogue] = + React.useState(false); + const [selectedFavouriteFilter, setSelectedFavouriteFilter] = React.useState< + FavouriteFilter | undefined + >(undefined); const [tabValue, setTabValue] = React.useState('Filters'); const handleTabChange = ( @@ -324,7 +328,6 @@ const FilterDialogue = (props: FilterDialogueProps) => { }, [incomingCount, incomingFilters]); const { data: favouriteFilterData } = useFavouriteFilters(); - return ( {