Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(fix admin dashboard transactions) #86

Merged
merged 1 commit into from
Jul 26, 2024
Merged
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
11 changes: 5 additions & 6 deletions src/Routes/Router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable*/
import React, { useEffect } from 'react';
import { Navigate, Outlet, Route, Routes, To } from 'react-router-dom';
import PageTitle from '../components/PageTitle';
Expand Down Expand Up @@ -42,6 +41,7 @@ import AdminOrders from '../pages/Orders/AdminOrders';
import SingleAdminOrder from '../pages/Orders/SingleAdminOrder';
import { JSX } from 'react/jsx-runtime';
import GoogleLogin from '../pages/Authentication/GoogleLogin';
import Transctions from '../pages/Transactions/Transctions';

const Router = () => {
const { userToken } = useSelector((state: RootState) => state.auth);
Expand All @@ -61,11 +61,7 @@ const Router = () => {
}
}, [location.pathname, dispatch, userToken]);

const conditionalNavigate = (
adminPath: To,
vendorPath: To,
buyerPath: JSX.Element | To | any
) => (
const conditionalNavigate = (adminPath: To, vendorPath: To, buyerPath: JSX.Element | To | any) => (
<>
{userToken && isAdmin && <Navigate to={adminPath} />}
{userToken && isVendor && <Navigate to={vendorPath} />}
Expand Down Expand Up @@ -325,6 +321,9 @@ const Router = () => {
<Route path="account" element={<DashboarInnerLayout />}>
<Route index element={<DashboardAccount />} />
</Route>
<Route path="transaction" element={<DashboarInnerLayout />}>
<Route index element={<Transctions />} />
</Route>
</Route>

<Route path="*" element={<NotFound />} />
Expand Down
79 changes: 79 additions & 0 deletions src/__test__/components/Cart/cartAction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { configureStore } from '@reduxjs/toolkit';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

import rootReducer from '../../../redux/reducers/rootReducer'; // Adjust the path to your rootReducer
import type { AppDispatch } from '../../../redux/store'; // Import types
import Cookies from 'js-cookie';
import { AddToCartData } from '../../../types/cartTypes';
import { addToCart, clearCart, fetchCart, removeFromCart } from '../../../redux/actions/cartAction';

describe('cartActions', () => {
let mockAxios: MockAdapter;
let store: ReturnType<typeof configureStore>;

beforeEach(() => {
mockAxios = new MockAdapter(axios);
store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
immutableCheck: false
})
});
localStorage.setItem('userToken', JSON.stringify({ token: 'test-token' }));
});

afterEach(() => {
mockAxios.reset();
localStorage.clear();
});

it('fetchCart should make the correct API call and handle the response', async () => {
const cartData = { data: { cart: [{ id: 'cart123', totalAmount: 100 }] } };
mockAxios.onGet(`${import.meta.env.VITE_APP_API_URL}/cart`).reply(200, cartData);

const result = await (store.dispatch as AppDispatch)(fetchCart());

expect(result.type).toBe('cart/fetchCart/fulfilled');
expect(result.payload).toEqual(cartData);
expect(mockAxios.history.get[0].url).toBe(`${import.meta.env.VITE_APP_API_URL}/cart`);
});

it('addToCart should make the correct API call and handle the response', async () => {
const addData: AddToCartData = { productId: 'prod123', quantity: 1 };
const responseData = { data: { cart: { id: 'cart123' } } };
mockAxios.onPost(`${import.meta.env.VITE_APP_API_URL}/cart`).reply(200, responseData);

const result = await (store.dispatch as AppDispatch)(addToCart(addData));

expect(result.type).toBe('cart/addToCart/fulfilled');
expect(result.payload).toEqual(responseData);
expect(mockAxios.history.post[0].url).toBe(`${import.meta.env.VITE_APP_API_URL}/cart`);
expect(Cookies.get('cartId')).toBe('cart123');
});

it('clearCart should make the correct API call and handle the response', async () => {
const responseData = { data: { cart: [] } }; // Mock response should match expected structure
mockAxios.onDelete(`${import.meta.env.VITE_APP_API_URL}/cart`).reply(200, responseData);

const result = await (store.dispatch as AppDispatch)(clearCart());

expect(result.type).toBe('cart/clearCart/fulfilled');
expect(result.payload).toEqual(responseData);
expect(mockAxios.history.delete[0].url).toBe(`${import.meta.env.VITE_APP_API_URL}/cart`);
});

it('removeFromCart should make the correct API call and handle the response', async () => {
const responseData = { data: { cart: [] } };
const itemId = 'item123';
mockAxios.onDelete(`${import.meta.env.VITE_APP_API_URL}/cart/${itemId}`).reply(200, responseData);

const result = await (store.dispatch as AppDispatch)(removeFromCart(itemId));

expect(result.type).toBe('cart/removeFromCart/fulfilled');
expect(result.payload).toEqual(responseData);
expect(mockAxios.history.delete[0].url).toBe(`${import.meta.env.VITE_APP_API_URL}/cart/${itemId}`);
});
});
63 changes: 63 additions & 0 deletions src/__test__/jwt.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import jwtDecode from 'jwt-decode';
import { DecodedToken } from '../types/CouponTypes';
import { decodedToken } from '../services';

// Mock jwt-decode module
vi.mock('jwt-decode', () => ({
default: vi.fn()
}));

describe('decodedToken', () => {
const originalError = console.error;

beforeAll(() => {
console.error = vi.fn(); // Mock console.error
});

afterAll(() => {
console.error = originalError; // Restore console.error
});

afterEach(() => {
localStorage.clear();
vi.clearAllMocks();
});

it('should return testData if provided', () => {
const testData: DecodedToken = {
id: '123',
email: '[email protected]',
userType: 'admin',
iat: 1615552560,
exp: 1615556160
};
const result = decodedToken({ testData });
expect(result).toEqual(testData);
});

it('should return null if no token in localStorage', () => {
const result = decodedToken({});
expect(result).toBeNull();
expect(console.error).toHaveBeenCalledWith('No user token found in localStorage');
});

it('should return null if token structure is invalid', () => {
localStorage.setItem('userToken', JSON.stringify({}));
const result = decodedToken({});
expect(result).toBeNull();
expect(console.error).toHaveBeenCalledWith('Invalid token structure');
});

it('should return null if there is an error in decoding token', () => {
const mockToken = 'mock.jwt.token';
(jwtDecode as any).mockImplementation(() => {
throw new Error('Invalid token');
});

localStorage.setItem('userToken', JSON.stringify({ token: mockToken }));

const result = decodedToken({});
expect(result).toBeNull();
expect(console.error).toHaveBeenCalledWith('Error decoding token', expect.any(Error));
});
});
175 changes: 175 additions & 0 deletions src/__test__/pages/transactions.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import React from 'react';
import { render, screen, waitFor, act } from '@testing-library/react';
import { describe, it, vi, afterEach } from 'vitest';
import axios from 'axios';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from '../../redux/store';
import Transactions from '../../pages/Transactions/Transctions';

vi.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

const mockData = {
statistics: {
totalAmount: 5000,
averagePaymentAmount: 1000,
totalCapturedAmount: 4000,
totalPayments: 5,
successfulPayments: 4,
pendingPayments: 1
},
payments: [
{
id: 'pay_1',
amount: 1000,
created: 1628353200,
status: 'succeeded',
payment_method_types: ['card']
},
{
id: 'pay_2',
amount: 1200,
created: 12628353200,
status: 'pending',
payment_method_types: ['card']
}
]
};

const emptyPaymentsData = {
statistics: {
totalAmount: 0,
averagePaymentAmount: 0,
totalCapturedAmount: 0,
totalPayments: 0,
successfulPayments: 0,
pendingPayments: 0
},
payments: []
};

describe('Transactions Component', () => {
afterEach(() => {
vi.clearAllMocks();
localStorage.clear();
});

it('renders Transactions component without crashing', async () => {
localStorage.setItem('userToken', JSON.stringify({ token: 'mocked-token' }));
mockedAxios.get.mockResolvedValue({ data: mockData });

await act(async () => {
render(
<Provider store={store}>
<MemoryRouter>
<Transactions />
</MemoryRouter>
</Provider>
);
});

await waitFor(() => {
expect(screen.getByTestId('transactions')).toBeInTheDocument();
});

expect(screen.getByTestId('totalVendors')).toBeInTheDocument();
expect(screen.getByTestId('totalVendors')).toHaveTextContent('5,000 Rwf');
});
it('displays No Transactions Found when payments array is empty', async () => {
localStorage.setItem('userToken', JSON.stringify({ token: 'mocked-token' }));
mockedAxios.get.mockResolvedValue({ data: emptyPaymentsData });

await act(async () => {
render(
<Provider store={store}>
<MemoryRouter>
<Transactions />
</MemoryRouter>
</Provider>
);
});

await waitFor(() => {
expect(screen.getByText('No Transactions Found')).toBeInTheDocument();
});
});

it('displays "No data available" when no data is fetched', async () => {
mockedAxios.get.mockResolvedValueOnce({ data: null });

await act(async () => {
render(
<Provider store={store}>
<MemoryRouter>
<Transactions />
</MemoryRouter>
</Provider>
);
});

await waitFor(() => {
expect(screen.getByText('No data available')).toBeInTheDocument();
});
});

// it('displays "No Transactions Found" when payments array is empty', async () => {
// localStorage.setItem('userToken', JSON.stringify({ token: 'mocked-token' }));
// mockedAxios.get.mockResolvedValue({ data: mockData });

// await act(async () => {
// render(
// <Provider store={store}>
// <MemoryRouter>
// <Transactions />
// </MemoryRouter>
// </Provider>
// );
// });

// await waitFor(() => {
// expect(screen.getByTestId('transactions')).toBeInTheDocument();
// });

// expect(screen.getByTestId('totalVendors')).toBeInTheDocument();
// expect(screen.getByTestId('totalVendors')).toHaveTextContent('5,000 Rwf');
// });

// it('displays "No Transactions Found" when payments array is empty', async () => {
// localStorage.setItem('userToken', JSON.stringify({ token: 'mocked-token' }));
// // mockedAxios.get.mockResolvedValue({ data: emptyPaymentsData });
// mockedAxios.get.mockResolvedValue({ data: mockData });
// await act(async () => {
// render(
// <Provider store={store}>
// <MemoryRouter>
// <Transactions />
// </MemoryRouter>
// </Provider>
// );
// });

// await waitFor(() => {
// expect(screen.getByText('No Transactions Found')).toBeInTheDocument();
// });
// });

it('handles missing token correctly', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

await act(async () => {
render(
<Provider store={store}>
<MemoryRouter>
<Transactions />
</MemoryRouter>
</Provider>
);
});

expect(consoleSpy).toHaveBeenCalledWith('Token not found');
expect(screen.getByText('No data available')).toBeInTheDocument();

consoleSpy.mockRestore();
});
});
13 changes: 12 additions & 1 deletion src/components/Dashboard/DashboardSideBar/DashboardSideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import two from '/3.svg';
import three from '/Component 3.svg';
import f from '/user-square.svg';
import dashboardIcon from '/Dashboard.svg';
import { CircleX } from 'lucide-react';
import { BadgeDollarSign, CircleX } from 'lucide-react';
import userIcon from '../../../assets/Enquiry.svg';
import { useJwt } from 'react-jwt';
import { useSelector } from 'react-redux';
Expand Down Expand Up @@ -87,6 +87,17 @@ const DashboardSideBar: React.FC<DashboardSideBarProps> = ({ openNav, setOpenNav
<img src={userIcon} alt="Products" className="w-5 lg:w-6" /> Users
</NavLink>
)}
{decodedToken?.role.toLowerCase() === 'admin' && (
<NavLink
to="transaction"
className={({ isActive }) =>
`flex items-center gap-1 px-3 py-2 w-full rounded transition-all duration-300 ease-in-out hover:bg-primary hover:text-white ${isActive ? 'bg-primary text-white' : ''}`
}
>
<BadgeDollarSign />
Transactions
</NavLink>
)}
</div>
<div className="mt-auto md:pt-4 text-[#7c7c7c] flex flex-col gap-1 w-full pt-4 md:border-t md:border-neutral-300 text-[.75rem] xmd:text-[.82rem] lg:text-[.9rem] ">
<NavLink
Expand Down
4 changes: 3 additions & 1 deletion src/components/SingleProduct/SingleProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ function SingleProduct() {
}

if (!addToCartloading && !addToCartError) {
toast.success('Product added to cart');
setTimeout(() => {
toast.success('Product added to cart');
}, 5000);
}
};

Expand Down
Loading