diff --git a/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx b/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx index 4ccedf4cd..d063418ad 100644 --- a/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx +++ b/frontend/src/views/AllocationAgreements/AddAllocationAgreements.jsx @@ -66,23 +66,29 @@ export const AddEditAllocationAgreements = () => { severity: location.state.severity || 'info' }) } - }, [location.state]) + }, [location.state?.message, location.state?.severity]) - const validateField = (params, field, validationFn, errorMessage, alertRef) => { - const newValue = params.newValue; + const validateField = ( + params, + field, + validationFn, + errorMessage, + alertRef + ) => { + const newValue = params.newValue if (params.colDef.field === field) { if (!validationFn(newValue)) { alertRef.current?.triggerAlert({ message: errorMessage, - severity: 'error', - }); - return false; + severity: 'error' + }) + return false } } - return true; // Proceed with the update - }; + return true // Proceed with the update + } const onGridReady = useCallback( async (params) => { @@ -154,9 +160,9 @@ export const AddEditAllocationAgreements = () => { (value) => value !== null && !isNaN(value) && value > 0, 'Quantity must be greater than 0.', alertRef - ); + ) - if (!isValid) return; + if (!isValid) return if (params.oldValue === params.newValue) return diff --git a/frontend/src/views/AllocationAgreements/__tests__/AllocationAgreements.test.jsx b/frontend/src/views/AllocationAgreements/__tests__/AllocationAgreements.test.jsx new file mode 100644 index 000000000..eb53f33cb --- /dev/null +++ b/frontend/src/views/AllocationAgreements/__tests__/AllocationAgreements.test.jsx @@ -0,0 +1,189 @@ +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { vi } from 'vitest' +import { AddEditAllocationAgreements } from '../AddAllocationAgreements' +import * as useGetAllocationAgreements from '@/hooks/useAllocationAgreement' +import * as useAllocationAgreementOptions from '@/hooks/useAllocationAgreement' +import * as useSaveAllocationAgreement from '@/hooks/useAllocationAgreement' +import { wrapper } from '@/tests/utils/wrapper' + +vi.mock('@react-keycloak/web', () => ({ + ReactKeycloakProvider: ({ children }) => children, + useKeycloak: () => ({ + keycloak: { + authenticated: true, + login: vi.fn(), + logout: vi.fn(), + register: vi.fn() + }, + initialized: true + }) +})) + +// Mock useApiService +vi.mock('@/services/useApiService', () => ({ + default: vi.fn(() => ({ + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn() + })), + useApiService: vi.fn(() => ({ + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), + delete: vi.fn() + })) +})) + +// Mock react-router-dom +const mockUseParams = vi.fn() +const mockUseLocation = vi.fn(() => ({ + state: { message: 'Test message', severity: 'info' } +})) +const mockUseNavigate = vi.fn() +const mockHasRoles = vi.fn() + +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + useParams: () => ({ + complianceReportId: '123', + compliancePeriod: '2023' + }), + useLocation: () => mockUseLocation, + useNavigate: () => mockUseNavigate +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key + }) +})) + + +describe('AddEditAllocationAgreement', () => { + const setupMocks = (overrides = {}) => { + const defaultMocks = { + useParams: { compliancePeriod: '2023', complianceReportId: '123' }, + useLocation: { state: {} } + } + + const mocks = { ...defaultMocks, ...overrides } + mockUseParams.mockReturnValue(mocks.useParams) + mockUseLocation.mockReturnValue(mocks.useLocation) + } + + beforeEach(() => { + vi.resetAllMocks() + setupMocks() + + // Reapply mocks to ensure they are correctly initialized + vi.mock('@/hooks/useAllocationAgreement', () => ({ + useAllocationAgreementOptions: vi.fn(() => ({ + data: { + allocationTransactionTypes: [ + { + allocationTransactionTypeId: 1, + type: "Purchased" + }, + { + allocationTransactionTypeId: 2, + type: "Sold" + } + ], + fuelTypes: [ + { + fuelTypeId: 1, + fuelType: "Biodiesel", + defaultCarbonIntensity: 100.21, + units: "L", + unrecognized: false, + fuelCategories: [ + { + fuelCategoryId: 2, + category: "Diesel", + defaultAndPrescribedCi: 100.21 + } + ], + fuelCodes: [ + { + fuelCodeId: 2, + fuelCode: "BCLCF124.4", + carbonIntensity: 3.62 + } + ], + provisionOfTheAct: [ + { + provisionOfTheActId: 2, + name: "Fuel code - section 19 (b) (i)" + }, + { + provisionOfTheActId: 3, + name: "Default carbon intensity - section 19 (b) (ii)" + } + ] + } + ], + provisionsOfTheAct: [ + { + provisionOfTheActId: 3, + name: "Default carbon intensity - section 19 (b) (ii)" + } + ], + fuelCodes: [ + { + fuelCodeId: 1, + fuelCode: "BCLCF102.5", + carbonIntensity: 37.21 + } + ], + unitsOfMeasure: [ + "L" + ] + }, + isLoading: false, + isFetched: true + })), + useGetAllocationAgreements: vi.fn(() => ({ + data: { allocationAgreements: [], pagination: {} }, + isLoading: false + })), + useSaveAllocationAgreement: vi.fn(() => ({ + mutateAsync: vi.fn() + })) + })) + }) + + it('renders the component', async () => { + render(, { wrapper }) + await waitFor(() => { + expect( + screen.getByText(/Enter allocation agreement details below/i) + ).toBeInTheDocument() + }) + }) + + it('should show error for 0 quantity', () => { + render(); + const quantityInput = screen.getByLabelText('Quantity'); + fireEvent.change(quantityInput, { target: { value: '0' } }); + fireEvent.blur(quantityInput); + expect(screen.getByText('Quantity must be greater than 0.')).toBeInTheDocument(); + }); + + it('should show error for empty quantity', () => { + render(); + const quantityInput = screen.getByLabelText('Quantity'); + fireEvent.change(quantityInput, { target: { value: '' } }); + fireEvent.blur(quantityInput); + expect(screen.getByText('Quantity must be greater than 0.')).toBeInTheDocument(); + }); + + it('should not show error for valid quantity', () => { + render(); + const quantityInput = screen.getByLabelText('Quantity'); + fireEvent.change(quantityInput, { target: { value: '10' } }); + fireEvent.blur(quantityInput); + expect(screen.queryByText('Quantity must be greater than 0.')).not.toBeInTheDocument(); + }); +}) diff --git a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx index a04118055..119c9f1e2 100644 --- a/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx +++ b/frontend/src/views/FuelSupplies/AddEditFuelSupplies.jsx @@ -54,13 +54,13 @@ export const AddEditFuelSupplies = () => { ) useEffect(() => { - if (location.state?.message) { + if (location?.state?.message) { alertRef.current?.triggerAlert({ message: location.state.message, severity: location.state.severity || 'info' }) } - }, [location.state]) + }, [location?.state?.message, location?.state?.severity]); const validateField = (params, field, validationFn, errorMessage, alertRef) => { const newValue = params.newValue; diff --git a/frontend/src/views/FuelSupplies/__tests__/FuelSupplies.test.jsx b/frontend/src/views/FuelSupplies/__tests__/FuelSupplies.test.jsx new file mode 100644 index 000000000..cc051c66f --- /dev/null +++ b/frontend/src/views/FuelSupplies/__tests__/FuelSupplies.test.jsx @@ -0,0 +1,232 @@ +import React from 'react' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { vi } from 'vitest' +import { AddEditFuelSupplies } from '../AddEditFuelSupplies' +import * as useFuelSupplyHooks from '@/hooks/useFuelSupply' +import { wrapper } from '@/tests/utils/wrapper' + +vi.mock('@react-keycloak/web', () => ({ + ReactKeycloakProvider: ({ children }) => children, + useKeycloak: () => ({ + keycloak: { + authenticated: true, + login: vi.fn(), + logout: vi.fn(), + register: vi.fn() + }, + initialized: true + }) +})) + +// Mock react-router-dom +const mockUseParams = vi.fn() +const mockUseLocation = vi.fn(() => ({ + state: { message: 'Test message', severity: 'info' } +})) +const mockUseNavigate = vi.fn() + +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + useParams: () => ({ + complianceReportId: '123', + compliancePeriod: '2023' + }), + useLocation: () => mockUseLocation(), + useNavigate: () => mockUseNavigate +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key + }) +})) + +// Mock hooks +vi.mock('@/hooks/useFuelSupply', () => ({ + useFuelSupplyOptions: vi.fn(() => ({ + data: { fuelTypes: [ + { + fuelTypeId: 2, + fuelType: 'CNG', + fossilDerived: false, + defaultCarbonIntensity: 63.91, + units: 'm³', + unrecognized: false, + }, + { + fuelTypeId: 3, + fuelType: 'Electric', + defaultCarbonIntensity: 12.14, + units: 'kWh', + unrecognized: false, + }, + ] }, + isLoading: false, + isFetched: true, + })), + useGetFuelSupplies: { + data: { fuelSupplies: [ + { + fuelSupplyId: 1, + complianceReportId: 2, + groupUuid: "fc44368c-ca60-4654-8f3d-32b55aa16245", + version: 0, + userType: "SUPPLIER", + actionType: "CREATE", + fuelTypeId: 3, + fuelType: { + fuelTypeId: 3, + fuelType: "Electricity", + fossilDerived: false, + defaultCarbonIntensity: 12.14, + units: "kWh" + }, + fuelCategoryId: 1, + fuelCategory: { + fuelCategoryId: 1, + category: "Gasoline" + }, + endUseId: 1, + endUseType: { + endUseTypeId: 1, + type: "Light duty motor vehicles" + }, + provisionOfTheActId: 3, + provisionOfTheAct: { + provisionOfTheActId: 3, + name: "Default carbon intensity - section 19 (b) (ii)" + }, + quantity: 1000000, + units: "kWh" + }, + { + fuelSupplyId: 2, + complianceReportId: 2, + groupUuid: "0f571126-43ae-43e7-b04b-705a22a2cbaf", + version: 0, + userType: "SUPPLIER", + actionType: "CREATE", + fuelTypeId: 3, + fuelType: { + fuelTypeId: 3, + fuelType: "Electricity", + fossilDerived: false, + defaultCarbonIntensity: 12.14, + units: "kWh" + }, + fuelCategoryId: 1, + fuelCategory: { + fuelCategoryId: 1, + category: "Gasoline" + }, + endUseId: 2, + endUseType: { + endUseTypeId: 2, + type: "Other or unknown" + }, + provisionOfTheActId: 3, + provisionOfTheAct: { + provisionOfTheActId: 3, + name: "Default carbon intensity - section 19 (b) (ii)" + }, + quantity: 100000, + units: "kWh" + } + ] + , + pagination: { + page: 1, + size: 10, + total: 2, + totalPages: 1, + }, }, + isLoading: false + }, + useSaveFuelSupply: vi.fn(() => ({ + mutateAsync: vi.fn(), + })), +})); + +describe('AddEditFuelSupplies', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + + it('renders the component with no initial data', async () => { + render(, { wrapper }) + + await waitFor(() => { + expect( + screen.getByText(/Add new supply of fuel/i) + ).toBeInTheDocument() + }) + }) + + it('should show error for 0 quantity', async () => { + render(, { wrapper }) + + const quantityInput = screen.getByLabelText(/quantity/i) + fireEvent.change(quantityInput, { target: { value: '0' } }) + fireEvent.blur(quantityInput) + + await waitFor(() => { + expect( + screen.getByText(/quantity supplied must be greater than 0./i) + ).toBeInTheDocument() + }) + }) + + it('should show error for empty quantity', async () => { + render(, { wrapper }) + + const quantityInput = screen.getByLabelText(/quantity/i) + fireEvent.change(quantityInput, { target: { value: '' } }) + fireEvent.blur(quantityInput) + + await waitFor(() => { + expect( + screen.getByText(/quantity supplied must be greater than 0./i) + ).toBeInTheDocument() + }) + }) + + it('should not show error for valid quantity', async () => { + render(, { wrapper }) + + const quantityInput = screen.getByLabelText(/quantity/i) + fireEvent.change(quantityInput, { target: { value: '10' } }) + fireEvent.blur(quantityInput) + + await waitFor(() => { + expect( + screen.queryByText(/quantity supplied must be greater than 0./i) + ).not.toBeInTheDocument() + }) + }) + + it('displays an error message when row update fails', async () => { + const mockMutateAsync = vi.fn().mockRejectedValueOnce({ + response: { + data: { + errors: [{ fields: ['quantity'], message: 'Invalid quantity' }] + } + } + }) + + vi.mocked(useFuelSupplyHooks.useSaveFuelSupply).mockReturnValueOnce({ + mutateAsync: mockMutateAsync + }) + + render(, { wrapper }) + + const quantityInput = screen.getByLabelText(/quantity/i) + fireEvent.change(quantityInput, { target: { value: '-5' } }) + fireEvent.blur(quantityInput) + + await waitFor(() => { + expect( + screen.getByText(/error updating row: invalid quantity/i) + ).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx b/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx index 29e2d13e5..f89e0096d 100644 --- a/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx +++ b/frontend/src/views/NotionalTransfers/AddEditNotionalTransfers.jsx @@ -39,13 +39,13 @@ export const AddEditNotionalTransfers = () => { const navigate = useNavigate() useEffect(() => { - if (location.state?.message) { + if (location?.state?.message) { alertRef.triggerAlert({ message: location.state.message, severity: location.state.severity || 'info' }) } - }, [location.state]) + }, [location?.state?.message, location?.state?.severity]); const validateField = (params, field, validationFn, errorMessage, alertRef) => { const newValue = params.newValue; diff --git a/frontend/src/views/NotionalTransfers/__tests__/AddEditNotionalTransfer.test.jsx b/frontend/src/views/NotionalTransfers/__tests__/AddEditNotionalTransfer.test.jsx new file mode 100644 index 000000000..18145bca7 --- /dev/null +++ b/frontend/src/views/NotionalTransfers/__tests__/AddEditNotionalTransfer.test.jsx @@ -0,0 +1,136 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { vi } from 'vitest'; +import { AddEditNotionalTransfers } from '../AddEditNotionalTransfers'; +import * as useNotionalTransfer from '@/hooks/useNotionalTransfer'; +import { wrapper } from '@/tests/utils/wrapper'; + +vi.mock('@react-keycloak/web', () => ({ + ReactKeycloakProvider: ({ children }) => children, + useKeycloak: () => ({ + keycloak: { + authenticated: true, + login: vi.fn(), + logout: vi.fn(), + register: vi.fn(), + }, + initialized: true, + }), +})); + +// Mock react-router-dom +const mockUseParams = vi.fn(); +const mockUseLocation = vi.fn(() => ({ + state: { message: 'Test message', severity: 'info' }, +})); +const mockUseNavigate = vi.fn(); + +vi.mock('react-router-dom', () => ({ + ...vi.importActual('react-router-dom'), + useParams: () => ({ + complianceReportId: '123', + compliancePeriod: '2023', + }), + useLocation: () => mockUseLocation(), + useNavigate: () => mockUseNavigate, +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key) => key, + }), +})); + +vi.mock('@/hooks/useNotionalTransfer', () => ({ + useNotionalTransferOptions: vi.fn(() => ({ + data: { + fuelCategories: [ + { + fuelCategoryId: 1, + category: "Gasoline", + description: "Gasoline" + }, + { + fuelCategoryId: 2, + category: "Diesel", + description: "Diesel" + }, + { + fuelCategoryId: 3, + category: "Jet fuel", + description: "Jet fuel" + } + ], + receivedOrTransferred: [ + "Received", + "Transferred" + ] + }, + isLoading: false, + isFetched: true, + })), + useGetAllNotionalTransfers: vi.fn(() => ({ + data: { + notionalTransfers: [] + }, + isLoading: false, + })), + useSaveNotionalTransfer: vi.fn(() => ({ + mutateAsync: vi.fn(), // Properly mock mutateAsync + })), + })); + +describe('AddEditNotionalTransfers', () => { + beforeEach(() => { + vi.resetAllMocks(); + + vi.spyOn(useNotionalTransfer, 'useSaveNotionalTransfer').mockReturnValue({ + mutateAsync: vi.fn(), // Ensure mutateAsync is mocked + }); +}); + it('renders the component successfully', async () => { + render(, { wrapper }); + + await waitFor(() => { + expect( + screen.getByText(/Add new notional transfer(s)/i) + ).toBeInTheDocument(); + }); + }); + + it('shows an error for 0 quantity', async () => { + render(, { wrapper }); + + const quantityInput = screen.getByLabelText(/quantity/i); + fireEvent.change(quantityInput, { target: { value: '0' } }); + fireEvent.blur(quantityInput); + + await waitFor(() => { + expect(screen.getByText(/quantity must be greater than 0./i)).toBeInTheDocument(); + }); + }); + + it('shows an error for empty quantity', async () => { + render(, { wrapper }); + + const quantityInput = screen.getByLabelText(/quantity/i); + fireEvent.change(quantityInput, { target: { value: '' } }); + fireEvent.blur(quantityInput); + + await waitFor(() => { + expect(screen.getByText(/quantity must be greater than 0./i)).toBeInTheDocument(); + }); + }); + + it('does not show an error for a valid quantity', async () => { + render(, { wrapper }); + + const quantityInput = screen.getByLabelText(/quantity/i); + fireEvent.change(quantityInput, { target: { value: '10' } }); + fireEvent.blur(quantityInput); + + await waitFor(() => { + expect(screen.queryByText(/quantity must be greater than 0./i)).not.toBeInTheDocument(); + }); + }); +});