diff --git a/cypress/e2e/manufacturer/manufacturer.cy.tsx b/cypress/e2e/manufacturer/manufacturer.cy.tsx index 2bf6bf157..317284ebf 100644 --- a/cypress/e2e/manufacturer/manufacturer.cy.tsx +++ b/cypress/e2e/manufacturer/manufacturer.cy.tsx @@ -2,6 +2,9 @@ describe('Manufacturer', () => { beforeEach(() => { cy.visit('/inventory-management-system/manufacturer'); }); + afterEach(() => { + cy.clearMocks(); + }); it('should render in table headers', () => { cy.visit('/inventory-management-system/manufacturer'); @@ -45,7 +48,7 @@ describe('Manufacturer', () => { cy.url().should('include', 'http://example.com'); }); - it('adds a manufacturer with all fields', async () => { + it('adds a manufacturer with all fields', () => { cy.findByRole('button', { name: 'Add Manufacturer' }).click(); cy.findByLabelText('Name *').type('Manufacturer D'); cy.findByLabelText('URL').type('http://test.co.uk'); @@ -67,12 +70,12 @@ describe('Manufacturer', () => { expect(patchRequests.length).equal(1); const request = patchRequests[0]; expect(JSON.stringify(request.body)).equal( - '{"name":"Manufacturer D","url":"http://test.co.uk", "address": {building_number: "1", "street_name": "Example Street", "town": "Oxford", "county": "Oxfordshire", "postcode": "OX1 2AB",}, "telephone": "07349612203"}' + '{"name":"Manufacturer D","url":"http://test.co.uk","address":{"building_number":"1","street_name":"Example Street","town":"Oxford","county":"Oxfordshire","postcode":"OX1 2AB"},"telephone":"07349612203"}' ); }); }); - it('adds a manufacturer with only mandatory fields', async () => { + it('adds a manufacturer with only mandatory fields', () => { cy.findByRole('button', { name: 'Add Manufacturer' }).click(); cy.findByLabelText('Name *').type('Manufacturer D'); cy.findByLabelText('Building number *').type('1'); @@ -94,7 +97,7 @@ describe('Manufacturer', () => { ); }); - it('render error messages if fields are not filled', async () => { + it('render error messages if fields are not filled', () => { cy.findByTestId('Add Manufacturer').click(); cy.findByRole('button', { name: 'Save' }).click(); cy.findByRole('dialog') @@ -118,7 +121,7 @@ describe('Manufacturer', () => { cy.contains('Please enter a post code or zip code.'); }); }); - it('displays error message when duplicate name entered', async () => { + it('displays error message when duplicate name entered', () => { cy.findByTestId('Add Manufacturer').click(); cy.findByLabelText('Name *').type('Manufacturer A'); cy.findByLabelText('Building number *').type('1'); @@ -132,7 +135,7 @@ describe('Manufacturer', () => { cy.contains('A manufacturer with the same name already exists.'); }); }); - it('invalid url displays correct error message', async () => { + it('invalid url displays correct error message', () => { cy.findByTestId('Add Manufacturer').click(); cy.findByLabelText('URL').type('test.co.uk'); @@ -145,4 +148,35 @@ describe('Manufacturer', () => { }); }); }); + + it('delete a manufacturer', () => { + cy.findAllByTestId('DeleteIcon').first().click(); + + cy.startSnoopingBrowserMockedRequest(); + + cy.findByRole('button', { name: 'Continue' }).click(); + + cy.findBrowserMockedRequests({ + method: 'DELETE', + url: '/v1/manufacturers/:id', + }).should((patchRequests) => { + expect(patchRequests.length).equal(1); + const request = patchRequests[0]; + expect(request.url.toString()).to.contain('1'); + }); + }); + + it('shows error when trying to delete manufacturer that is part of Catalogue Item', () => { + cy.findAllByTestId('DeleteIcon').eq(1).click(); + + cy.findByRole('button', { name: 'Continue' }).click(); + + cy.findByRole('dialog') + .should('be.visible') + .within(() => { + cy.contains( + 'The specified manufacturer is a part of a Catalogue Item. Please delete the Catalogue Item first.' + ); + }); + }); }); diff --git a/src/api/manufacturer.test.tsx b/src/api/manufacturer.test.tsx new file mode 100644 index 000000000..644760adb --- /dev/null +++ b/src/api/manufacturer.test.tsx @@ -0,0 +1,156 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { AddManufacturer, Manufacturer } from '../app.types'; +import { hooksWrapperWithProviders } from '../setupTests'; +import { + useAddManufacturer, + useDeleteManufacturer, + useManufacturers, +} from './manufacturer'; + +describe('manufacturer api functions', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('useAddManufacturer', () => { + let mockDataAdd: AddManufacturer; + beforeEach(() => { + mockDataAdd = { + name: 'Manufacturer D', + url: 'http://test.co.uk', + address: { + building_number: '1', + street_name: 'Example', + town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 2AB', + }, + telephone: '07349612203', + }; + }); + + it('posts a request to add manufacturer and returns successful response', async () => { + const { result } = renderHook(() => useAddManufacturer(), { + wrapper: hooksWrapperWithProviders(), + }); + expect(result.current.isIdle).toBe(true); + result.current.mutate(mockDataAdd); + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + expect(result.current.data).toEqual({ + name: 'Manufacturer D', + code: 'manufacturer-d', + url: 'http://test.co.uk', + address: { + building_number: '1', + street_name: 'Example Street', + town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 2AB', + }, + telephone: '07349612203', + id: '4', + }); + }); + + it.todo( + 'sends axios request to fetch records and throws an appropiate error on failure' + ); + }); + + describe('useDeleteManufacturer', () => { + let mockDataView: Manufacturer; + beforeEach(() => { + mockDataView = { + name: 'Manufacturer A', + url: 'http://example.com', + address: { + building_number: '1', + street_name: 'Example', + town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 2AB', + }, + telephone: '07334893348', + id: '1', + }; + }); + it('posts a request to delete a manufacturer and return a successful response', async () => { + const { result } = renderHook(() => useDeleteManufacturer(), { + wrapper: hooksWrapperWithProviders(), + }); + expect(result.current.isIdle).toBe(true); + result.current.mutate(mockDataView); + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + expect(result.current.data).toEqual(''); + }); + + it.todo( + 'sends axios request to fetch records and throws an appropriate error on fetch' + ); + }); + + describe('useManufacturer', () => { + it('sends request to fetch manufacturer data and returns successful response', async () => { + const { result } = renderHook(() => useManufacturers(), { + wrapper: hooksWrapperWithProviders(), + }); + + await waitFor(() => { + expect(result.current.isSuccess).toBeTruthy(); + }); + + expect(result.current.data).toEqual([ + { + id: '1', + name: 'Manufacturer A', + code: 'manufacturer-a', + url: 'http://example.com', + address: { + building_number: '1', + street_name: 'Example Street', + town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 2AB', + }, + telephone: '07334893348', + }, + { + id: '2', + name: 'Manufacturer B', + code: 'manufacturer-b', + url: 'http://test.com', + address: { + building_number: '2', + street_name: 'Example Street', + town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 2AB', + }, + telephone: '07294958549', + }, + { + id: '3', + name: 'Manufacturer C', + code: 'manufacturer-c', + url: 'http://test.co.uk', + address: { + building_number: '3', + street_name: 'Example Street', + town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 2AB', + }, + telephone: '07934303412', + }, + ]); + }); + + it.todo( + 'sends axios request to fetch records and throws an appropriate error on failure' + ); + }); +}); diff --git a/src/api/manufacturer.tsx b/src/api/manufacturer.tsx index 33fadcfe5..79846bf9d 100644 --- a/src/api/manufacturer.tsx +++ b/src/api/manufacturer.tsx @@ -76,3 +76,31 @@ export const useAddManufacturer = (): UseMutationResult< } ); }; + +const deleteManufacturer = async (session: Manufacturer): Promise => { + let apiUrl: string; + apiUrl = ''; + const settingsResult = await settings; + if (settingsResult) { + apiUrl = settingsResult['apiUrl']; + } + return axios + .delete(`${apiUrl}/v1/manufacturers/${session.id}`, {}) + .then((response) => response.data); +}; + +export const useDeleteManufacturer = (): UseMutationResult< + void, + AxiosError, + Manufacturer +> => { + const queryClient = useQueryClient(); + return useMutation((session: Manufacturer) => deleteManufacturer(session), { + onError: (error) => { + console.log('Got error ' + error.message); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['Manufacturers'] }); + }, + }); +}; diff --git a/src/manufacturer/AddmanufacturerDialog.component.test.tsx b/src/manufacturer/AddmanufacturerDialog.component.test.tsx index 394291904..d29dffdec 100644 --- a/src/manufacturer/AddmanufacturerDialog.component.test.tsx +++ b/src/manufacturer/AddmanufacturerDialog.component.test.tsx @@ -3,7 +3,7 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import AddManufacturerDialog, { AddManufacturerDialogProps, -} from './manufacturerDialog.component'; +} from './addManufacturerDialog.component'; import { renderComponentWithBrowserRouter } from '../setupTests'; import axios from 'axios'; diff --git a/src/manufacturer/DeleteManufacturerDialog.test.tsx b/src/manufacturer/DeleteManufacturerDialog.test.tsx new file mode 100644 index 000000000..adb5ab8e2 --- /dev/null +++ b/src/manufacturer/DeleteManufacturerDialog.test.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { RenderResult, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderComponentWithBrowserRouter } from '../setupTests'; +import { DeleteManufacturerProps } from './deleteManufacturerDialog.component'; +import DeleteManufacturerDialog from './deleteManufacturerDialog.component'; +import { Manufacturer } from '../app.types'; + +describe('Delete Manufacturer Dialog', () => { + const onClose = jest.fn(); + let props: DeleteManufacturerProps; + let manufacturer: Manufacturer; + let user; + const createView = (): RenderResult => { + return renderComponentWithBrowserRouter( + + ); + }; + beforeEach(() => { + manufacturer = { + name: 'test', + url: 'http://example.com', + address: { + building_number: '1', + street_name: 'Example Street', + town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 2AB', + }, + telephone: '056896598', + id: '1', + }; + props = { + open: true, + onClose: onClose, + manufacturer: manufacturer, + }; + user = userEvent; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders dialog correctly', async () => { + createView(); + expect(screen.getByText('Delete Manufacturer')).toBeInTheDocument(); + expect(screen.getByTestId('delete-manufacturer-name')).toHaveTextContent( + 'test' + ); + }); + + it('calls onClose when Close clicked', async () => { + createView(); + const closeButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(closeButton); + + await waitFor(() => { + expect(onClose).toHaveBeenCalled(); + }); + }); + + it('displays warning message when data not loaded', async () => { + props = { + ...props, + manufacturer: 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('calls handleDelete when Continue clicked', async () => { + createView(); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + user.click(continueButton); + + await waitFor(() => { + expect(onClose).toHaveBeenCalled(); + }); + }); + + it('displays error message when user tries to delete a manufacturer that is a part of a catalogue item', async () => { + manufacturer.id = '2'; + createView(); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + user.click(continueButton); + + await waitFor(() => { + expect( + screen.getByText( + 'The specified manufacturer is a part of a Catalogue Item. Please delete the Catalogue Item first.' + ) + ).toBeInTheDocument(); + }); + }); + + it('displays error message if an unknown error occurs', async () => { + manufacturer.id = '100'; + createView(); + const continueButton = screen.getByRole('button', { name: 'Continue' }); + user.click(continueButton); + + await waitFor(() => { + expect( + screen.getByText('Please refresh and try again') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/src/manufacturer/manufacturerDialog.component.tsx b/src/manufacturer/addManufacturerDialog.component.tsx similarity index 100% rename from src/manufacturer/manufacturerDialog.component.tsx rename to src/manufacturer/addManufacturerDialog.component.tsx diff --git a/src/manufacturer/deleteManufacturerDialog.component.tsx b/src/manufacturer/deleteManufacturerDialog.component.tsx new file mode 100644 index 000000000..bd899c18e --- /dev/null +++ b/src/manufacturer/deleteManufacturerDialog.component.tsx @@ -0,0 +1,94 @@ +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormHelperText, +} from '@mui/material'; +import React from 'react'; +import { ErrorParsing, Manufacturer } from '../app.types'; +import { AxiosError } from 'axios'; +import { useDeleteManufacturer } from '../api/manufacturer'; + +export interface DeleteManufacturerProps { + open: boolean; + onClose: () => void; + manufacturer: Manufacturer | undefined; +} + +const DeleteManufacturerDialog = (props: DeleteManufacturerProps) => { + const { open, onClose, manufacturer } = props; + + const [error, setError] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState( + undefined + ); + + const { mutateAsync: deleteManufacturer } = useDeleteManufacturer(); + + const handleClose = React.useCallback(() => { + onClose(); + setError(false); + setErrorMessage(undefined); + }, [onClose]); + + const handleDeleteManufacturer = React.useCallback(() => { + if (manufacturer) { + deleteManufacturer(manufacturer) + .then((response) => { + onClose(); + }) + .catch((error: AxiosError) => { + const response = error.response?.data as ErrorParsing; + if (response && error.response?.status === 409) { + setError(true); + setErrorMessage( + `${response.detail}. Please delete the Catalogue Item first.` + ); + return; + } + setError(true); + setErrorMessage('Please refresh and try again'); + }); + } else { + setError(true); + setErrorMessage('No data provided, Please refresh and try again'); + } + }, [manufacturer, deleteManufacturer, onClose]); + + return ( + + Delete Manufacturer + + Are you sure you want to delete{' '} + + {manufacturer?.name} + + ? + + + + + + {error && ( + + + {errorMessage} + + + )} + + ); +}; + +export default DeleteManufacturerDialog; diff --git a/src/manufacturer/manufacturer.component.test.tsx b/src/manufacturer/manufacturer.component.test.tsx index b0d2c0184..ca216857c 100644 --- a/src/manufacturer/manufacturer.component.test.tsx +++ b/src/manufacturer/manufacturer.component.test.tsx @@ -82,4 +82,24 @@ describe('Manufacturer', () => { ).toHaveStyle('background-color: inherit'); }); }); + + it('opens delete dialog and closes it correctly', async () => { + createView(); + await waitFor(() => { + expect(screen.getByText('Manufacturer A')).toBeInTheDocument(); + }); + + await user.click( + screen.getByRole('button', { name: 'Delete Manufacturer A manufacturer' }) + ); + + expect(screen.getByText('Delete Manufacturer')).toBeInTheDocument(); + + const closeButton = screen.getByRole('button', { name: 'Cancel' }); + await user.click(closeButton); + + await waitFor(() => { + expect(screen.queryByText('Delete Manufacturer')).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/manufacturer/manufacturer.component.tsx b/src/manufacturer/manufacturer.component.tsx index 0dd30b2f3..f11413e13 100644 --- a/src/manufacturer/manufacturer.component.tsx +++ b/src/manufacturer/manufacturer.component.tsx @@ -16,10 +16,12 @@ import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import React from 'react'; import { useManufacturers } from '../api/manufacturer'; -import AddManufacturerDialog from './manufacturerDialog.component'; +import { Manufacturer } from '../app.types'; +import DeleteManufacturerDialog from './deleteManufacturerDialog.component'; import { ManufacturerDetail } from '../app.types'; +import AddManufacturerDialog from './addManufacturerDialog.component'; -function Manufacturer() { +function ManufacturerComponent() { const [addManufacturer, setAddManufacturer] = React.useState({ name: '', @@ -39,6 +41,13 @@ function Manufacturer() { const { data: ManufacturerData } = useManufacturers(); + const [deleteManufacturerDialog, setDeleteManufacturerDialog] = + React.useState(false); + + const [selectedManufacturer, setSelectedManufacturer] = React.useState< + Manufacturer | undefined + >(undefined); + const [hoveredRow, setHoveredRow] = React.useState(null); const tableHeight = `calc(100vh)-(64px + 36px +50px)`; const theme = useTheme(); @@ -148,6 +157,10 @@ function Manufacturer() { { + setDeleteManufacturerDialog(true); + setSelectedManufacturer(item); + }} > @@ -205,8 +218,13 @@ function Manufacturer() { + setDeleteManufacturerDialog(false)} + manufacturer={selectedManufacturer} + /> ); } -export default Manufacturer; +export default ManufacturerComponent; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 97ccc67a0..0437ab323 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -151,6 +151,25 @@ export const handlers = [ ); }), + rest.delete('/v1/manufacturers/:id', (req, res, ctx) => { + const { id } = req.params; + const validManufacturer = ManufacturerJSON.find((value) => value.id === id); + if (validManufacturer) { + if (id === '2') { + return res( + ctx.status(409), + ctx.json({ + detail: 'The specified manufacturer is a part of a Catalogue Item', + }) + ); + } else { + return res(ctx.status(200), ctx.json('')); + } + } else { + return res(ctx.status(400), ctx.json('')); + } + }), + rest.delete('/v1/catalogue-categories/:id', (req, res, ctx) => { const { id } = req.params; const validCatalogueCategory = CatalogueCategoryJSON.find(