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();
+ });
+ });
+});