diff --git a/.github/workflows/check-tsdoc.js b/.github/workflows/check-tsdoc.js index d5c3b33b90..0400f5a108 100644 --- a/.github/workflows/check-tsdoc.js +++ b/.github/workflows/check-tsdoc.js @@ -23,6 +23,7 @@ async function findTsxFiles(dir) { } else if ( filePath.endsWith('.tsx') && !filePath.endsWith('.test.tsx') && + !filePath.endsWith('.spec.tsx') && !filesToSkip.includes(path.relative(dir, filePath)) ) { results.push(filePath); diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 7cb9c10f69..5096758ea5 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -49,7 +49,7 @@ jobs: - name: Run formatting if check fails if: failure() - run: npm run format + run: npm run format:fix - name: Check for type errors if: steps.changed-files.outputs.only_changed != 'true' @@ -101,6 +101,7 @@ jobs: .node-version .husky/** scripts/** + src/style/** schema.graphql package.json tsconfig.json diff --git a/Dockerfile b/Dockerfile index b0ffe03f79..6b13a712b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,15 @@ -# Step 1: Build Stage -FROM node:20.10.0-alpine AS builder -WORKDIR /talawa-admin +FROM node:20.10.0 AS build + +WORKDIR /usr/src/app COPY package*.json ./ + RUN npm install COPY . . -ENV NODE_ENV=production - RUN npm run build -#Step 2: Production -FROM nginx:1.27.3-alpine AS production - -ENV NODE_ENV=production +EXPOSE 4321 -COPY config/docker/setup/nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=builder /talawa-admin/build /usr/share/nginx/html -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["npm", "run", "serve"] \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index d15ec53598..204d6f8851 100644 --- a/jest.config.js +++ b/jest.config.js @@ -36,6 +36,7 @@ export default { '/src', ], moduleNameMapper: { + '\\.(css|scss|sass|less)$': 'identity-obj-proxy', '^react-native$': 'react-native-web', '^@dicebear/core$': '/scripts/__mocks__/@dicebear/core.ts', '^@dicebear/collection$': @@ -67,6 +68,7 @@ export default { 'src/components/AddOn/support/services/Render.helper.ts', 'src/components/SecuredRoute/SecuredRoute.tsx', 'src/reportWebVitals.ts', + 'src/screens/UserPortal/Volunteer/Actions/Actions.spec.tsx', ], coverageThreshold: { global: { @@ -78,6 +80,7 @@ export default { '/node_modules/', '/build/', '/public/', + '/src/screens/UserPortal/Volunteer/Actions/Actions.spec.tsx', ], coverageDirectory: './coverage/jest', coverageReporters: ['text', 'html', 'text-summary', 'lcov'], diff --git a/package-lock.json b/package-lock.json index 7acc4343b4..3eb85993c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -101,6 +101,7 @@ "@typescript-eslint/parser": "^8.5.0", "@vitest/coverage-istanbul": "^2.1.5", "babel-jest": "^29.7.0", + "cross-env": "^7.0.3", "eslint": "^8.49.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", diff --git a/package.json b/package.json index dd4047e81d..da331b88d2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "babel-plugin-transform-import-meta": "^2.2.1", "bootstrap": "^5.3.3", "chart.js": "^4.4.6", + "customize-cra": "^1.0.0", "dayjs": "^1.11.13", "dotenv": "^16.4.5", "flag-icons": "^7.2.3", @@ -136,6 +137,7 @@ "@typescript-eslint/parser": "^8.5.0", "@vitest/coverage-istanbul": "^2.1.5", "babel-jest": "^29.7.0", + "cross-env": "^7.0.3", "eslint": "^8.49.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.31.0", @@ -155,8 +157,7 @@ "tsx": "^4.19.1", "vitest": "^2.1.5", "whatwg-fetch": "^3.6.20", - "vite-plugin-svgr": "^4.2.0", - "cross-env": "^7.0.3" + "vite-plugin-svgr": "^4.2.0" }, "resolutions": { "@apollo/client": "^3.4.0-beta.19", diff --git a/public/locales/en/errors.json b/public/locales/en/errors.json index 752c0db750..ffb0e6f146 100644 --- a/public/locales/en/errors.json +++ b/public/locales/en/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "Email not registered", "notFoundMsg": "Oops! The Page you requested was not found!", "errorOccurredCouldntCreate": "An error occurred. Couldn't create {{entity}}", - "errorLoading": "Error occured while loading {{entity}} data" + "errorLoading": "Error occured while loading {{entity}} data", + "invalidPhoneNumber": "Please enter a valid phone number", + "invalidEducationGrade": "Please select a valid education grade", + "invalidEmploymentStatus": "Please select a valid employment status", + "invalidMaritalStatus": "Please select a valid marital status", + "error400": "The submitted information is invalid. Please check your inputs and try again" } diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 5da5ef49ee..57b4c5de98 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -923,7 +923,7 @@ "register": "register" }, "addOnStore": { - "title": "Add On Store", + "title": "Plugin Store", "searchName": "Ex: Donations", "search": "Search", "enable": "Enabled", @@ -1227,7 +1227,7 @@ "RstartDate": "Select Start Date", "RendDate": "Select End Date", "RClose": "Close the window", - "addNew": "Create new advertisement", + "addNew": "Create", "EXname": "Ex. Cookie Shop", "EXlink": "Ex. http://yourwebsite.com/photo", "createAdvertisement": "Create Advertisement", diff --git a/public/locales/fr/errors.json b/public/locales/fr/errors.json index e9a7cf4fd9..ae53237404 100644 --- a/public/locales/fr/errors.json +++ b/public/locales/fr/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "Email non enregistré", "notFoundMsg": "Oops! ", "errorOccurredCouldntCreate": "Une erreur s'est produite. Impossible de créer {{entity}}", - "errorLoading": "Une erreur s'est produite lors du chargement des données {{entity}}" + "errorLoading": "Une erreur s'est produite lors du chargement des données {{entity}}", + "invalidPhoneNumber": "Veuillez entrer un numéro de téléphone valide", + "invalidEducationGrade": "Veuillez sélectionner un niveau d'études valide", + "invalidEmploymentStatus": "Veuillez sélectionner un statut d'emploi valide", + "invalidMaritalStatus": "Veuillez sélectionner un état matrimonial valide", + "error400": "Réponse non réussie. Code d'état 400 reçu du serveur" } diff --git a/public/locales/hi/errors.json b/public/locales/hi/errors.json index 63b6c3f5d3..64f9523180 100644 --- a/public/locales/hi/errors.json +++ b/public/locales/hi/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "ईमेल पंजीकृत नहीं है", "notFoundMsg": "उफ़! ", "errorOccurredCouldntCreate": "एक त्रुटि हुई। {{entity}} नहीं बना सके", - "errorLoading": "{{entity}} डेटा लोड करते समय त्रुटि हुई" + "errorLoading": "{{entity}} डेटा लोड करते समय त्रुटि हुई", + "invalidPhoneNumber": "कृपया एक मान्य फोन-नंबर दर्ज करे", + "invalidEducationGrade": "कृपया एक शिक्षा ग्रेड चुनें", + "invalidEmploymentStatus": "कृपया वैध रोजगार स्थिति चुनें", + "invalidMaritalStatus": "कृपया वैध वैवाहिक स्थिति चुनें", + "error400": "आपकी जानकारी सहेजी नहीं जा सकी। कृपया अपनी प्रविष्टियों की जांच करें और पुनः प्रयास करें।" } diff --git a/public/locales/sp/errors.json b/public/locales/sp/errors.json index 39b579abac..7489356b5e 100644 --- a/public/locales/sp/errors.json +++ b/public/locales/sp/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "Email not registered", "notFoundMsg": "Oops! The Page you requested was not found!", "errorOccurredCouldntCreate": "Ocurrió un error. No se pudo crear {{entity}}", - "errorLoading": "Ocurrió un error al cargar los datos de {{entity}}" + "errorLoading": "Ocurrió un error al cargar los datos de {{entity}}", + "invalidPhoneNumber": "Por favor, introduzca un número de teléfono válido", + "invalidEducationGrade": "Por favor seleccione un grado de educación válido", + "invalidEmploymentStatus": "Por favor seleccione un estado de empleo válido", + "invalidMaritalStatus": "Por favor seleccione un estado civil válido", + "error400": "Respuesta no exitosa. Se recibió el código de estado 400 del servidor" } diff --git a/public/locales/zh/errors.json b/public/locales/zh/errors.json index c872f367a5..c289d67aa1 100644 --- a/public/locales/zh/errors.json +++ b/public/locales/zh/errors.json @@ -7,5 +7,10 @@ "emailNotRegistered": "邮箱未注册", "notFoundMsg": "哎呀!", "errorOccurredCouldntCreate": "发生错误。 无法创建{{entity}}", - "errorLoading": "加载{{entity}}数据时出错" + "errorLoading": "加载{{entity}}数据时出错", + "invalidPhoneNumber": "请选择一个有效的电话号码", + "invalidEducationGrade": "请选择教育年级", + "invalidEmploymentStatus": "请选择有效的就业状况", + "invalidMaritalStatus": "请选择有效的婚姻状况", + "error400": "响应不成功. 从服务器收到状态代码 400" } diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css b/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css index 1f1ea89996..c5dd86c8d4 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.module.css @@ -7,8 +7,12 @@ margin-left: auto; display: flex !important; align-items: center; + background-color: transparent; + color: #31bb6b; +} +.card { + border: 4px solid green; } - .entryaction i { margin-right: 8px; } diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.spec.tsx b/src/components/AddOn/core/AddOnEntry/AddOnEntry.spec.tsx new file mode 100644 index 0000000000..01ec40a917 --- /dev/null +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.spec.tsx @@ -0,0 +1,241 @@ +/** + * Unit tests for the AddOnEntry component. + * + * This file contains tests for the AddOnEntry component to ensure it behaves as expected + * under various scenarios. + */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { BrowserRouter } from 'react-router-dom'; +import AddOnEntry from './AddOnEntry'; +import { + ApolloClient, + ApolloProvider, + InMemoryCache, + ApolloLink, + HttpLink, +} from '@apollo/client'; +import { describe, test, vi, expect } from 'vitest'; +import type { NormalizedCacheObject } from '@apollo/client'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import { BACKEND_URL } from 'Constant/constant'; +import i18nForTest from 'utils/i18nForTest'; +import { I18nextProvider } from 'react-i18next'; +import userEvent from '@testing-library/user-event'; +import { MockedProvider, wait } from '@apollo/react-testing'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import { ADD_ON_ENTRY_MOCK } from './AddOnEntryMocks'; +import { ToastContainer } from 'react-toastify'; +import useLocalStorage from 'utils/useLocalstorage'; + +const { getItem } = useLocalStorage(); + +const link = new StaticMockLink(ADD_ON_ENTRY_MOCK, true); + +const httpLink = new HttpLink({ + uri: BACKEND_URL, + headers: { + authorization: 'Bearer ' + getItem('token') || '', + }, +}); +console.error = vi.fn(); +const client: ApolloClient = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.from([httpLink]), +}); +let mockID: string | undefined = '1'; + +vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), + useParams: () => ({ orgId: mockID }), +})); + +describe('Testing AddOnEntry', () => { + const props = { + id: 'string', + enabled: true, + title: 'string', + description: 'string', + createdBy: 'string', + component: 'string', + installed: true, + configurable: true, + modified: true, + isInstalled: true, + getInstalledPlugins: (): { sample: string } => { + return { sample: 'sample' }; + }, + }; + + test('should render modal and take info to add plugin for registered organization', () => { + const { getByTestId } = render( + + + + + + + + + , + ); + expect(getByTestId('AddOnEntry')).toBeInTheDocument(); + }); + + test('uses default values for title and description when not provided', () => { + const mockGetInstalledPlugins = vi.fn(); + render( + + + + + + + + + , + ); + + const titleElement = screen.getByText('No title provided'); + const descriptionElement = screen.getByText('Description not available'); + expect(titleElement).toBeInTheDocument(); + expect(descriptionElement).toBeInTheDocument(); + }); + + test('renders correctly', () => { + const props = { + id: '1', + title: 'Test Addon', + description: 'Test addon description', + createdBy: 'Test User', + component: 'string', + installed: true, + configurable: true, + modified: true, + isInstalled: true, + uninstalledOrgs: [], + enabled: true, + getInstalledPlugins: (): { sample: string } => { + return { sample: 'sample' }; + }, + }; + + const { getByText } = render( + + + + + + + + + , + ); + + expect(getByText('Test Addon')).toBeInTheDocument(); + expect(getByText('Test addon description')).toBeInTheDocument(); + expect(getByText('Test User')).toBeInTheDocument(); + }); + + test('Uninstall Button works correctly', async () => { + const props = { + id: '1', + title: 'Test Addon', + description: 'Test addon description', + createdBy: 'Test User', + component: 'string', + installed: true, + configurable: true, + modified: true, + isInstalled: true, + uninstalledOrgs: [], + enabled: true, + getInstalledPlugins: (): { sample: string } => { + return { sample: 'sample' }; + }, + }; + mockID = 'undefined'; + const { findByText, getByTestId } = render( + + + + + + + + + + , + ); + + const btn = await getByTestId('AddOnEntry_btn_install'); + await userEvent.click(btn); + expect(btn.innerHTML).toMatch(/Install/i); + expect( + await findByText('This feature is now removed from your organization'), + ).toBeInTheDocument(); + + await userEvent.click(btn); + expect(btn.innerHTML).toMatch(/Uninstall/i); + expect( + await findByText('This feature is now enabled in your organization'), + ).toBeInTheDocument(); + }); + + it('Check if uninstalled orgs includes current org', async () => { + const props = { + id: '1', + title: 'Test Addon', + description: 'Test addon description', + createdBy: 'Test User', + component: 'string', + installed: true, + configurable: true, + modified: true, + isInstalled: true, + uninstalledOrgs: ['undefined'], + enabled: true, + getInstalledPlugins: (): { sample: string } => { + return { sample: 'sample' }; + }, + }; + + const { getByTestId } = render( + + + + + {} + + + + , + ); + await wait(100); + const btn = getByTestId('AddOnEntry_btn_install'); + expect(btn.innerHTML).toMatch(/install/i); + }); + + test('should redirect to /orglist if orgId is undefined', async () => { + mockID = undefined; + render( + + + + + + + + + , + ); + expect(window.location.pathname).toEqual('/orglist'); + }); +}); diff --git a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx index 257917e2c2..12805568f6 100644 --- a/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx +++ b/src/components/AddOn/core/AddOnEntry/AddOnEntry.tsx @@ -17,9 +17,9 @@ interface InterfaceAddOnEntryProps { description?: string; // Optional props createdBy: string; component?: string; // Optional props - modified?: any; // Optional props + modified?: boolean; // Optional props uninstalledOrgs: string[]; - getInstalledPlugins: () => any; + getInstalledPlugins: () => void; } /** @@ -59,6 +59,7 @@ function addOnEntry({ // Getting orgId from URL parameters const { orgId: currentOrg } = useParams(); + // console.log(currentOrg); if (!currentOrg) { // If orgId is not present in the URL, navigate to the org list page return ; @@ -101,7 +102,10 @@ function addOnEntry({ return ( <> - + {/* {uninstalledOrgs.includes(currentOrg) && ( )} */} - {title} + {title} {createdBy} @@ -134,7 +138,7 @@ function addOnEntry({ ) : ( )} {/* {installed ? 'Remove' : configurable ? 'Installed' : 'Install'} */} diff --git a/src/components/AddOn/core/AddOnStore/AddOnStore.module.css b/src/components/AddOn/core/AddOnStore/AddOnStore.module.css index 8a34c03be5..9f5bb6c868 100644 --- a/src/components/AddOn/core/AddOnStore/AddOnStore.module.css +++ b/src/components/AddOn/core/AddOnStore/AddOnStore.module.css @@ -11,8 +11,12 @@ border-bottom: 3px solid #31bb6b; width: 15%; } - -.actioninput { +.input { + display: flex; + position: relative; + width: 560px; +} +/* .actioninput { text-decoration: none; margin-bottom: 50px; border-color: #e8e5e5; @@ -23,9 +27,46 @@ padding-right: 10px; padding-left: 10px; box-shadow: none; +} */ +.actioninput { + margin-top: 10px; + margin-bottom: 10px; + background-color: white; + box-shadow: 0 1px 1px #31bb6b; +} +.inputField > button { + padding-top: 10px; + padding-bottom: 10px; } .actionradio input { width: fit-content; margin: inherit; } +.cardGridItem { + width: 38vw; +} +.justifysp { + display: grid; + width: 100%; + justify-content: space-between; + align-items: baseline; + grid-template-rows: auto; + grid-template-columns: repeat(2, 1fr); + grid-gap: 0.8rem 0.4rem; +} + +@media screen and (max-width: 600px) { + .cardGridItem { + width: 100%; + } + .justifysp { + display: grid; + width: 100%; + justify-content: center; + align-items: start; + grid-template-rows: auto; + grid-template-columns: 1fr; + grid-gap: 0.8rem 0.4rem; + } +} diff --git a/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx b/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx index e76e2a7b73..abb4a80ce8 100644 --- a/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx +++ b/src/components/AddOn/core/AddOnStore/AddOnStore.test.tsx @@ -22,7 +22,11 @@ import useLocalStorage from 'utils/useLocalstorage'; import { MockedProvider } from '@apollo/react-testing'; const { getItem } = useLocalStorage(); - +interface InterfacePlugin { + enabled: boolean; + pluginName: string; + component: string; +} jest.mock('components/AddOn/support/services/Plugin.helper', () => ({ __esModule: true, default: jest.fn().mockImplementation(() => ({ @@ -60,16 +64,18 @@ jest.mock('components/AddOn/support/services/Plugin.helper', () => ({ }, // Add more mock data as needed ]), - generateLinks: jest.fn().mockImplementation((plugins) => { - return plugins - .filter((plugin: { enabled: any }) => plugin.enabled) - .map((installedPlugin: { pluginName: any; component: string }) => { - return { - name: installedPlugin.pluginName, - url: `/plugin/${installedPlugin.component.toLowerCase()}`, - }; - }); - }), + generateLinks: jest + .fn() + .mockImplementation((plugins: InterfacePlugin[]) => { + return plugins + .filter((plugin) => plugin.enabled) + .map((installedPlugin) => { + return { + name: installedPlugin.pluginName, + url: `/plugin/${installedPlugin.component.toLowerCase()}`, + }; + }); + }), })), })); @@ -301,77 +307,6 @@ describe('Testing AddOnStore Component', () => { expect(message.length).toBeGreaterThanOrEqual(1); }); - test('check filters enabled and disabled under Installed tab', async () => { - const mocks = [ORGANIZATIONS_LIST_MOCK, PLUGIN_GET_MOCK]; - render( - - - - - - - - - - - , - ); - - await wait(); - userEvent.click(screen.getByText('Installed')); - - expect(screen.getByText('Filters')).toBeInTheDocument(); - expect(screen.getByLabelText('Enabled')).toBeInTheDocument(); - expect(screen.getByLabelText('Disabled')).toBeInTheDocument(); - - fireEvent.click(screen.getByLabelText('Enabled')); - expect(screen.getByLabelText('Enabled')).toBeChecked(); - fireEvent.click(screen.getByLabelText('Disabled')); - expect(screen.getByLabelText('Disabled')).toBeChecked(); - }); - - test('check the working search bar when on Installed tab', async () => { - const mocks = [ORGANIZATIONS_LIST_MOCK, PLUGIN_GET_MOCK]; - - const { container } = render( - - - - - - - - - - - , - ); - await wait(); - userEvent.click(screen.getByText('Installed')); - - await wait(); - let searchText = ''; - fireEvent.change(screen.getByPlaceholderText('Ex: Donations'), { - target: { value: searchText }, - }); - expect(container).toHaveTextContent('Plugin 1'); - expect(container).toHaveTextContent('Plugin 3'); - - searchText = 'Plugin 1'; - fireEvent.change(screen.getByPlaceholderText('Ex: Donations'), { - target: { value: searchText }, - }); - const plugin1Elements = screen.queryAllByText('Plugin 1'); - expect(plugin1Elements.length).toBeGreaterThan(1); - - searchText = 'Test Plugin'; - fireEvent.change(screen.getByPlaceholderText('Ex: Donations'), { - target: { value: searchText }, - }); - const message = screen.getAllByText('Plugin does not exists'); - expect(message.length).toBeGreaterThanOrEqual(1); - }); - test('AddOnStore loading test', async () => { expect(true).toBe(true); const mocks = [ORGANIZATIONS_LIST_MOCK, PLUGIN_LOADING_MOCK]; diff --git a/src/components/AddOn/core/AddOnStore/AddOnStore.tsx b/src/components/AddOn/core/AddOnStore/AddOnStore.tsx index 878ad64e31..90a32d9bb3 100644 --- a/src/components/AddOn/core/AddOnStore/AddOnStore.tsx +++ b/src/components/AddOn/core/AddOnStore/AddOnStore.tsx @@ -1,16 +1,27 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useState } from 'react'; -// import PropTypes from 'react'; import styles from './AddOnStore.module.css'; import AddOnEntry from '../AddOnEntry/AddOnEntry'; -import Action from '../../support/components/Action/Action'; import { useQuery } from '@apollo/client'; -import { PLUGIN_GET } from 'GraphQl/Queries/Queries'; // GraphQL query for fetching plugins -import { Col, Form, Row, Tab, Tabs } from 'react-bootstrap'; +import { PLUGIN_GET } from 'GraphQl/Queries/Queries'; // PLUGIN_LIST +import { Col, Dropdown, Form, Row, Tab, Tabs, Button } from 'react-bootstrap'; import PluginHelper from 'components/AddOn/support/services/Plugin.helper'; import { store } from './../../../../state/store'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import { Search } from '@mui/icons-material'; + +interface InterfacePluginHelper { + _id: string; + pluginName?: string; + pluginDesc?: string; + pluginCreatedBy: string; + pluginInstallStatus?: boolean; + uninstalledOrgs: string[]; + installed: boolean; + enabled: boolean; + name: string; + component: string; +} /** * Component for managing and displaying plugins in the store. @@ -30,12 +41,13 @@ function addOnStore(): JSX.Element { const [isStore, setIsStore] = useState(true); const [showEnabled, setShowEnabled] = useState(true); const [searchText, setSearchText] = useState(''); - const [, setDataList] = useState([]); + const [, setDataList] = useState([]); - // type plugData = { pluginName: String, plug }; - const { data, loading } = useQuery(PLUGIN_GET); + const { data, loading } = useQuery<{ getPlugins: InterfacePluginHelper[] }>( + PLUGIN_GET, + ); - const { orgId } = useParams(); + const { orgId } = useParams<{ orgId: string }>(); /** * Fetches store plugins and updates the Redux store with the plugin data. @@ -44,10 +56,10 @@ function addOnStore(): JSX.Element { const getStorePlugins = async (): Promise => { let plugins = await new PluginHelper().fetchStore(); const installIds = (await new PluginHelper().fetchInstalled()).map( - (plugin: any) => plugin.id, + (plugin: InterfacePluginHelper) => plugin._id, ); - plugins = plugins.map((plugin: any) => { - plugin.installed = installIds.includes(plugin.id); + plugins = plugins.map((plugin: InterfacePluginHelper) => { + plugin.installed = installIds.includes(plugin._id); return plugin; }); store.dispatch({ type: 'UPDATE_STORE', payload: plugins }); @@ -57,8 +69,8 @@ function addOnStore(): JSX.Element { * Sets the list of installed plugins in the component's state. */ /* istanbul ignore next */ - const getInstalledPlugins: () => any = () => { - setDataList(data); + const getInstalledPlugins: () => void = () => { + setDataList(data?.getPlugins ?? []); }; /** @@ -66,10 +78,14 @@ function addOnStore(): JSX.Element { * * @param tab - The key of the selected tab (either 'available' or 'installed'). */ - const updateSelectedTab = (tab: any): void => { + const updateSelectedTab = (tab: string): void => { setIsStore(tab === 'available'); /* istanbul ignore next */ - isStore ? getStorePlugins() : getInstalledPlugins(); + if (tab === 'available') { + getStorePlugins(); + } else { + getInstalledPlugins(); + } }; /** @@ -77,10 +93,23 @@ function addOnStore(): JSX.Element { * * @param ev - The event object from the filter change. */ - const filterChange = (ev: any): void => { + const filterChange = (ev: React.ChangeEvent): void => { setShowEnabled(ev.target.value === 'enabled'); }; + const filterPlugins = ( + plugins: InterfacePluginHelper[], + searchTerm: string, + ): InterfacePluginHelper[] => { + if (!searchTerm) { + return plugins; + } + + return plugins.filter((plugin) => + plugin.pluginName?.toLowerCase().includes(searchTerm.toLowerCase()), + ); + }; + // Show a loader while the data is being fetched /* istanbul ignore next */ if (loading) { @@ -93,9 +122,23 @@ function addOnStore(): JSX.Element { return ( <> - - - + + +
setSearchText(e.target.value)} /> - + +
{!isStore && ( - -
-
- - -
-
-
+ + filterChange( + e as unknown as React.ChangeEvent, + ) + } + > + + {showEnabled ? t('enable') : t('disable')} + + + + {t('enable')} + + + {t('disable')} + + + )} - -
-

{t('pHeading')}

- {searchText ? ( -

- Search results for {searchText} -

- ) : null} +
+ { + if (eventKey) { + updateSelectedTab(eventKey); + } + }} + > + +
+ {(() => { + const filteredPlugins = filterPlugins( + data?.getPlugins || [], + searchText, + ); - {t('pMessage')}; + } + + return ( +
+ {filteredPlugins.map((plug, i) => ( +
+ +
+ ))} +
+ ); + })()} +
+
+ - - {data.getPlugins.filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ).length === 0 ? ( -

{t('pMessage')}

- ) : ( - data.getPlugins - .filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ) - .map( - ( - plug: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - uninstalledOrgs: string[]; - getInstalledPlugins: () => any; - }, - i: React.Key | null | undefined, - ): JSX.Element => ( - - ), - ) - )} -
- - {data.getPlugins - .filter( - (plugin: any) => !plugin.uninstalledOrgs.includes(orgId), - ) - .filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ).length === 0 ? ( -

{t('pMessage')}

- ) : ( - data.getPlugins - .filter( - (plugin: any) => !plugin.uninstalledOrgs.includes(orgId), - ) - .filter( - (val: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }) => { - if (searchText == '') { - return val; - } else if ( - val.pluginName - ?.toLowerCase() - .includes(searchText.toLowerCase()) - ) { - return val; - } - }, - ) - .map( - ( - plug: { - _id: string; - pluginName: string | undefined; - pluginDesc: string | undefined; - pluginCreatedBy: string; - uninstalledOrgs: string[]; - pluginInstallStatus: boolean | undefined; - getInstalledPlugins: () => any; - }, - i: React.Key | null | undefined, - ): JSX.Element => ( - - ), - ) - )} -
-
-
- +
+ {(() => { + const installedPlugins = (data?.getPlugins || []).filter( + (plugin) => !plugin.uninstalledOrgs.includes(orgId ?? ''), + ); + const filteredPlugins = filterPlugins( + installedPlugins, + searchText, + ); + + if (filteredPlugins.length === 0) { + return

{t('pMessage')}

; + } + + return filteredPlugins.map((plug, i) => ( +
+ +
+ )); + })()} +
+ + +
); diff --git a/src/components/AddOn/support/components/Action/Action.spec.tsx b/src/components/AddOn/support/components/Action/Action.spec.tsx new file mode 100644 index 0000000000..e0682a5645 --- /dev/null +++ b/src/components/AddOn/support/components/Action/Action.spec.tsx @@ -0,0 +1,31 @@ +/** + * Unit tests for the Action component. + * + * This file contains tests for the Action component to ensure it behaves as expected + * under various scenarios. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { Provider } from 'react-redux'; +import { describe, test, expect } from 'vitest'; + +import { store } from 'state/store'; +import Action from './Action'; + +describe('Testing Action Component', () => { + const props = { + children: 'dummy children', + label: 'dummy label', + }; + + test('should render props and text elements for the page component', () => { + const { getByText } = render( + + + , + ); + + expect(getByText(props.label)).toBeInTheDocument(); + expect(getByText(props.children)).toBeInTheDocument(); + }); +}); diff --git a/src/components/AddOn/support/services/Plugin.helper.test.ts b/src/components/AddOn/support/services/Plugin.helper.test.ts index e024734247..39f0a5d12c 100644 --- a/src/components/AddOn/support/services/Plugin.helper.test.ts +++ b/src/components/AddOn/support/services/Plugin.helper.test.ts @@ -9,7 +9,18 @@ describe('Testing src/components/AddOn/support/services/Plugin.helper.ts', () => expect(pluginHelper).toHaveProperty('generateLinks'); }); test('generateLinks should return proper objects', () => { - const obj = { enabled: true, name: 'demo', component: 'samplecomponent' }; + const obj = { + enabled: true, + name: 'demo', + component: 'samplecomponent', + _id: 'someId', + pluginName: 'pluginName', + pluginDesc: 'pluginDesc', + pluginCreatedBy: 'creator', + pluginInstallStatus: true, + uninstalledOrgs: ['org1', 'org2'], + installed: true, + }; const objToMatch = { name: 'demo', url: '/plugin/samplecomponent' }; const pluginHelper = new PluginHelper(); const val = pluginHelper.generateLinks([obj]); diff --git a/src/components/Advertisements/Advertisements.module.css b/src/components/Advertisements/Advertisements.module.css index 8a34c03be5..6d9eb7f612 100644 --- a/src/components/Advertisements/Advertisements.module.css +++ b/src/components/Advertisements/Advertisements.module.css @@ -1,6 +1,13 @@ .container { display: flex; } +.listBox { + display: grid; + width: 100%; + grid-template-rows: auto; + grid-template-columns: repeat(6, 1fr); + grid-gap: 0.8rem 0.4rem; +} .logintitle { color: #707070; @@ -11,15 +18,24 @@ border-bottom: 3px solid #31bb6b; width: 15%; } - +.input { + display: flex; + position: relative; + width: 560px; +} +.justifysp { + display: grid; + width: 100%; + margin-top: 30px; +} .actioninput { text-decoration: none; - margin-bottom: 50px; + /* margin-bottom: 50px; */ border-color: #e8e5e5; - width: 80%; + background-color: white; border-radius: 7px; - padding-top: 5px; - padding-bottom: 5px; + padding-top: 10px; + padding-bottom: 10px; padding-right: 10px; padding-left: 10px; box-shadow: none; diff --git a/src/components/Advertisements/Advertisements.test.tsx b/src/components/Advertisements/Advertisements.test.tsx index c0992a1012..88bbb1255c 100644 --- a/src/components/Advertisements/Advertisements.test.tsx +++ b/src/components/Advertisements/Advertisements.test.tsx @@ -461,7 +461,7 @@ describe('Testing Advertisement Component', () => { await wait(); const date = await screen.findAllByTestId('Ad_end_date'); - const dateString = date[1].innerHTML; + const dateString = date[0].innerHTML; const dateMatch = dateString.match( /\b(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2})\s+(\d{4})\b/, ); diff --git a/src/components/Advertisements/Advertisements.tsx b/src/components/Advertisements/Advertisements.tsx index f20c2a7d8e..5f0e2b2033 100644 --- a/src/components/Advertisements/Advertisements.tsx +++ b/src/components/Advertisements/Advertisements.tsx @@ -2,30 +2,16 @@ import React, { useEffect, useState } from 'react'; import styles from './Advertisements.module.css'; import { useQuery } from '@apollo/client'; import { ORGANIZATION_ADVERTISEMENT_LIST } from 'GraphQl/Queries/Queries'; -import { Col, Row, Tab, Tabs } from 'react-bootstrap'; +import { Button, Col, Form, Row, Tab, Tabs } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import AdvertisementEntry from './core/AdvertisementEntry/AdvertisementEntry'; import AdvertisementRegister from './core/AdvertisementRegister/AdvertisementRegister'; import { useParams } from 'react-router-dom'; import type { InterfaceQueryOrganizationAdvertisementListItem } from 'utils/interfaces'; import InfiniteScroll from 'react-infinite-scroll-component'; +import { Search } from '@mui/icons-material'; -/** - * The `Advertisements` component displays a list of advertisements for a specific organization. - * It uses a tab-based interface to toggle between active and archived advertisements. - * - * The component utilizes the `useQuery` hook from Apollo Client to fetch advertisements data - * and implements infinite scrolling to load more advertisements as the user scrolls. - * - * @example - * return ( - * - * ) - * - */ - -export default function Advertisements(): JSX.Element { - // Retrieve the organization ID from URL parameters +export default function advertisements(): JSX.Element { const { orgId: currentOrgId } = useParams(); // Translation hook for internationalization const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); @@ -43,20 +29,14 @@ export default function Advertisements(): JSX.Element { name: string; type: 'BANNER' | 'MENU' | 'POPUP'; mediaUrl: string; - endDate: string; // Assuming it's a string in the format 'yyyy-MM-dd' - startDate: string; // Assuming it's a string in the format 'yyyy-MM-dd' + endDate: string; + startDate: string; }; // GraphQL query to fetch the list of advertisements - const { - data: orgAdvertisementListData, - refetch, - }: { - data?: { - organizations: InterfaceQueryOrganizationAdvertisementListItem[]; - }; - refetch: () => void; - } = useQuery(ORGANIZATION_ADVERTISEMENT_LIST, { + const { data: orgAdvertisementListData, refetch } = useQuery<{ + organizations: InterfaceQueryOrganizationAdvertisementListItem[]; + }>(ORGANIZATION_ADVERTISEMENT_LIST, { variables: { id: currentOrgId, after: after, @@ -99,19 +79,45 @@ export default function Advertisements(): JSX.Element { return ( <> - +
- {/* Component for registering a new advertisement */} - + +
+ setSearchText("search")} + /> + +
+ + - {/* Tabs for active and archived advertisements */} - {/* Tab for active advertisements */} - + - - {/* Tab for archived advertisements */} new Date(ad.endDate) < new Date(), ).length !== 0 && (
-
{tCommon('endOfResults')}
+
{t('endOfResults')}
) } diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css index 879d96a0a0..e4f244807f 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css @@ -20,7 +20,7 @@ .admedia { object-fit: cover; - height: 20rem; + height: 16rem; } .buttons { @@ -28,6 +28,10 @@ justify-content: flex-end; } +.card { + width: 28rem; +} + .dropdownButton { background-color: transparent; color: #000; diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx index 7368ded68e..7656f1f0cf 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx @@ -38,6 +38,7 @@ function AdvertisementEntry({ startDate = new Date(), setAfter, }: InterfaceAddOnEntryProps): JSX.Element { + console.log(id, type); const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); const { t: tCommon } = useTranslation('common'); @@ -98,7 +99,7 @@ function AdvertisementEntry({ {Array.from({ length: 1 }).map((_, idx) => ( - +
)}
diff --git a/src/components/EventCalendar/YearlyEventCalender.tsx b/src/components/EventCalendar/YearlyEventCalender.tsx index 63870ded3c..2852ca8cfb 100644 --- a/src/components/EventCalendar/YearlyEventCalender.tsx +++ b/src/components/EventCalendar/YearlyEventCalender.tsx @@ -46,11 +46,11 @@ interface InterfaceCalendarProps { viewType?: ViewType; } -enum Status { - ACTIVE = 'ACTIVE', - BLOCKED = 'BLOCKED', - DELETED = 'DELETED', -} +// enum Status { +// ACTIVE = 'ACTIVE', +// BLOCKED = 'BLOCKED', +// DELETED = 'DELETED', +// } /** * Enum for different user roles. @@ -64,12 +64,12 @@ enum Role { /** * Interface for event attendees. */ -interface InterfaceIEventAttendees { - userId: string; - user?: string; - status?: Status; - createdAt?: Date; -} +// interface InterfaceIEventAttendees { +// userId: string; +// user?: string; +// status?: Status; +// createdAt?: Date; +// } /** * Interface for organization list. @@ -177,7 +177,6 @@ const Calendar: React.FC = ({ * Navigates to the previous year. */ const handlePrevYear = (): void => { - /*istanbul ignore next*/ setCurrentYear(currentYear - 1); }; @@ -185,7 +184,6 @@ const Calendar: React.FC = ({ * Navigates to the next year. */ const handleNextYear = (): void => { - /*istanbul ignore next*/ setCurrentYear(currentYear + 1); }; @@ -239,7 +237,6 @@ const Calendar: React.FC = ({ return dayjs(event.startDate).isSame(date, 'day'); }); - /*istanbul ignore next*/ const renderedEvents = eventsForCurrentDate?.map((datas: InterfaceEventListCardProps) => { const attendees: { _id: string }[] = []; @@ -276,7 +273,6 @@ const Calendar: React.FC = ({ ); }) || []; - /*istanbul ignore next*/ const toggleExpand = (index: string): void => { if (expandedY === index) { setExpandedY(null); @@ -285,7 +281,6 @@ const Calendar: React.FC = ({ } }; - /*istanbul ignore next*/ return (
{ const searchInput = await screen.findByTestId('searchByName'); expect(searchInput).toBeInTheDocument(); - userEvent.type(searchInput, 'Category 1'); - userEvent.type(searchInput, '{enter}'); + // Simulate typing and pressing ENTER + userEvent.type(searchInput, 'Category 1{enter}'); + + // Wait for the filtering to complete await waitFor(() => { - expect(screen.getByText('Category 1')).toBeInTheDocument(); - expect(screen.queryByText('Category 2')).toBeNull(); + // Assert only "Category 1" is visible + const categories = screen.getAllByTestId('categoryName'); + expect(categories).toHaveLength(1); + expect(categories[0]).toHaveTextContent('Category 1'); }); }); diff --git a/src/components/OrgSettings/General/GeneralSettings.tsx b/src/components/OrgSettings/General/GeneralSettings.tsx index 4dbca1b6eb..739456150a 100644 --- a/src/components/OrgSettings/General/GeneralSettings.tsx +++ b/src/components/OrgSettings/General/GeneralSettings.tsx @@ -1,6 +1,6 @@ import React, { type FC } from 'react'; import { Card, Col, Form, Row } from 'react-bootstrap'; -import styles from 'screens/OrgSettings/OrgSettings.module.css'; +import styles from '../../../../src/style/app.module.css'; import OrgProfileFieldSettings from './OrgProfileFieldSettings/OrgProfileFieldSettings'; import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; import DeleteOrg from './DeleteOrg/DeleteOrg'; diff --git a/src/components/OrganizationScreen/OrganizationScreen.test.tsx b/src/components/OrganizationScreen/OrganizationScreen.test.tsx index cd039cc3ca..bc9aef388d 100644 --- a/src/components/OrganizationScreen/OrganizationScreen.test.tsx +++ b/src/components/OrganizationScreen/OrganizationScreen.test.tsx @@ -81,15 +81,13 @@ describe('Testing OrganizationScreen', () => { fireEvent.click(closeButton); // Check for contract class after closing - expect(screen.getByTestId('mainpageright')).toHaveClass('_expand_ccl5z_8'); + expect(screen.getByTestId('mainpageright')).toHaveClass(styles.expand); const openButton = screen.getByTestId('openMenu'); fireEvent.click(openButton); // Check for expand class after opening - expect(screen.getByTestId('mainpageright')).toHaveClass( - '_contract_ccl5z_61', - ); + expect(screen.getByTestId('mainpageright')).toHaveClass(styles.contract); }); test('handles window resize', () => { diff --git a/src/screens/EventVolunteers/Requests/Requests.test.tsx b/src/screens/EventVolunteers/Requests/Requests.spec.tsx similarity index 92% rename from src/screens/EventVolunteers/Requests/Requests.test.tsx rename to src/screens/EventVolunteers/Requests/Requests.spec.tsx index 3b55ea872c..e51e28ab3f 100644 --- a/src/screens/EventVolunteers/Requests/Requests.test.tsx +++ b/src/screens/EventVolunteers/Requests/Requests.spec.tsx @@ -1,3 +1,10 @@ +/** + * Testing component for managing and displaying Volunteer Membership requests for an event. + * + * This component allows users to view, filter, sort, and create action items. It also allows users to accept or reject volunteer membership requests. + * + * + */ import React, { act } from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { LocalizationProvider } from '@mui/x-date-pickers'; @@ -20,11 +27,12 @@ import { UPDATE_ERROR_MOCKS, } from './Requests.mocks'; import { toast } from 'react-toastify'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -74,14 +82,14 @@ const renderRequests = (link: ApolloLink): RenderResult => { describe('Testing Requests Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), + vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), useParams: () => ({ orgId: 'orgId', eventId: 'eventId' }), })); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { @@ -102,10 +110,7 @@ describe('Testing Requests Screen', () => { , ); - - await waitFor(() => { - expect(screen.getByTestId('paramsError')).toBeInTheDocument(); - }); + expect(window.location.pathname).toBe('/'); }); it('should render Requests screen', async () => { diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index c68ecaceb0..180009926c 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -153,7 +153,6 @@ const loginPage = (): JSX.Element => { try { await fetch(BACKEND_URL as string); } catch (error) { - /* istanbul ignore next */ errorHandler(t, error); } } @@ -165,7 +164,6 @@ const loginPage = (): JSX.Element => { recaptchaToken: string | null, ): Promise => { try { - /* istanbul ignore next */ if (REACT_APP_USE_RECAPTCHA !== 'yes') { return true; } @@ -177,7 +175,6 @@ const loginPage = (): JSX.Element => { return data.recaptcha; } catch { - /* istanbul ignore next */ toast.error(t('captchaError') as string); } }; @@ -199,7 +196,7 @@ const loginPage = (): JSX.Element => { } = signformState; const isVerified = await verifyRecaptcha(recaptchaToken); - /* istanbul ignore next */ + if (!isVerified) { toast.error(t('Please_check_the_captcha') as string); return; @@ -242,7 +239,6 @@ const loginPage = (): JSX.Element => { }, }); - /* istanbul ignore next */ if (signUpData) { toast.success( t( @@ -260,7 +256,6 @@ const loginPage = (): JSX.Element => { }); } } catch (error) { - /* istanbul ignore next */ errorHandler(t, error); } } else { @@ -285,7 +280,7 @@ const loginPage = (): JSX.Element => { const loginLink = async (e: ChangeEvent): Promise => { e.preventDefault(); const isVerified = await verifyRecaptcha(recaptchaToken); - /* istanbul ignore next */ + if (!isVerified) { toast.error(t('Please_check_the_captcha') as string); return; @@ -299,7 +294,6 @@ const loginPage = (): JSX.Element => { }, }); - /* istanbul ignore next */ if (loginData) { i18n.changeLanguage(loginData.login.appUserProfile.appLanguageCode); const { login } = loginData; @@ -336,7 +330,6 @@ const loginPage = (): JSX.Element => { toast.warn(tErrors('notFound') as string); } } catch (error) { - /* istanbul ignore next */ errorHandler(t, error); } }; @@ -494,14 +487,12 @@ const loginPage = (): JSX.Element => {
) : ( - /* istanbul ignore next */ <> )}
) : !isLoading && orgsData?.organizationsConnection.length == 0 && - /* istanbul ignore next */ searchByName.length > 0 ? ( - /* istanbul ignore next */

{tCommon('noResultsFoundFor')} "{searchByName}" diff --git a/src/screens/OrgSettings/OrgSettings.module.css b/src/screens/OrgSettings/OrgSettings.module.css deleted file mode 100644 index 9952a9a459..0000000000 --- a/src/screens/OrgSettings/OrgSettings.module.css +++ /dev/null @@ -1,55 +0,0 @@ -.headerBtn { - box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 2px; -} -.settingsContainer { - min-height: 100vh; -} - -.settingsBody { - min-height: 100vh; - margin: 2.5rem 1rem; -} - -.cardHeader { - padding: 1.25rem 1rem 1rem 1rem; - border-bottom: 1px solid var(--bs-gray-200); - display: flex; - justify-content: space-between; - align-items: center; -} - -.cardHeader .cardTitle { - font-size: 1.2rem; - font-weight: 600; -} - -.cardBody { - min-height: 180px; -} - -.cardBody .textBox { - margin: 0 0 3rem 0; - color: var(--bs-secondary); -} - -hr { - border: none; - height: 1px; - background-color: var(--bs-gray-500); -} - -.settingsTabs { - display: none; -} - -@media (min-width: 577px) { - .settingsDropdown { - display: none; - } -} - -@media (min-width: 577px) { - .settingsTabs { - display: block; - } -} diff --git a/src/screens/OrgSettings/OrgSettings.test.tsx b/src/screens/OrgSettings/OrgSettings.spec.tsx similarity index 54% rename from src/screens/OrgSettings/OrgSettings.test.tsx rename to src/screens/OrgSettings/OrgSettings.spec.tsx index a9aec5f33d..5b5179d644 100644 --- a/src/screens/OrgSettings/OrgSettings.test.tsx +++ b/src/screens/OrgSettings/OrgSettings.spec.tsx @@ -1,28 +1,38 @@ +import type { ReactElement } from 'react'; import React from 'react'; -import { MockedProvider } from '@apollo/react-testing'; -import type { RenderResult } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; -import 'jest-location-mock'; -import { I18nextProvider } from 'react-i18next'; -import { Provider } from 'react-redux'; +import userEvent from '@testing-library/user-event'; import { MemoryRouter, Route, Routes } from 'react-router-dom'; - +import { Provider } from 'react-redux'; +import { I18nextProvider } from 'react-i18next'; +import { LocalizationProvider } from '@mui/x-date-pickers'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import { MockedProvider } from '@apollo/react-testing'; import { store } from 'state/store'; -import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; import OrgSettings from './OrgSettings'; -import userEvent from '@testing-library/user-event'; -import type { ApolloLink } from '@apollo/client'; -import { LocalizationProvider } from '@mui/x-date-pickers'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { MOCKS } from './OrgSettings.mocks'; const link1 = new StaticMockLink(MOCKS); - -const renderOrganisationSettings = (link: ApolloLink): RenderResult => { +const mockRouterParams = (orgId: string | undefined): void => { + vi.doMock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId }), + }; + }); +}; +const renderOrganisationSettings = ( + link = link1, + orgId = 'orgId', +): ReturnType => { + mockRouterParams(orgId); return render( - + @@ -30,7 +40,9 @@ const renderOrganisationSettings = (link: ApolloLink): RenderResult => { } />

} + element={ +
Redirected to Home
+ } /> @@ -42,28 +54,37 @@ const renderOrganisationSettings = (link: ApolloLink): RenderResult => { }; describe('Organisation Settings Page', () => { - beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + afterEach(() => { + vi.unmock('react-router-dom'); }); - afterAll(() => { - jest.clearAllMocks(); - }); + const SetupRedirectTest = async (): Promise => { + const useParamsMock = vi.fn(() => ({ orgId: undefined })); + vi.doMock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: useParamsMock, + }; + }); + const orgSettingsModule = await import('./OrgSettings'); + return ; + }; it('should redirect to fallback URL if URL params are undefined', async () => { + const OrgSettings = await SetupRedirectTest(); render( - + - } /> + } + element={ +
Redirected to Home
+ } />
@@ -71,13 +92,16 @@ describe('Organisation Settings Page', () => {
, ); + await waitFor(() => { - expect(screen.getByTestId('paramsError')).toBeInTheDocument(); + const paramsErrorElement = screen.getByTestId('paramsError'); + expect(paramsErrorElement).toBeInTheDocument(); + expect(paramsErrorElement.textContent).toBe('Redirected to Home'); }); }); - test('should render the organisation settings page', async () => { - renderOrganisationSettings(link1); + it('should render the organisation settings page', async () => { + renderOrganisationSettings(); await waitFor(() => { expect(screen.getByTestId('generalSettings')).toBeInTheDocument(); @@ -88,10 +112,11 @@ describe('Organisation Settings Page', () => { screen.getByTestId('agendaItemCategoriesSettings'), ).toBeInTheDocument(); }); - userEvent.click(screen.getByTestId('generalSettings')); + userEvent.click(screen.getByTestId('generalSettings')); await waitFor(() => { expect(screen.getByTestId('generalTab')).toBeInTheDocument(); + expect(screen.getByTestId('generalTab')).toBeVisible(); }); userEvent.click(screen.getByTestId('actionItemCategoriesSettings')); @@ -104,4 +129,19 @@ describe('Organisation Settings Page', () => { expect(screen.getByTestId('agendaItemCategoriesTab')).toBeInTheDocument(); }); }); + + it('should render dropdown for settings tabs', async () => { + renderOrganisationSettings(); + + await waitFor(() => { + expect(screen.getByTestId('settingsDropdownToggle')).toBeInTheDocument(); + }); + + userEvent.click(screen.getByTestId('settingsDropdownToggle')); + + const dropdownItems = screen.getAllByRole('button', { + name: /general|actionItemCategories|agendaItemCategories/i, + }); + expect(dropdownItems).toHaveLength(3); + }); }); diff --git a/src/screens/OrgSettings/OrgSettings.tsx b/src/screens/OrgSettings/OrgSettings.tsx index e4ae5424a6..c7b01138ae 100644 --- a/src/screens/OrgSettings/OrgSettings.tsx +++ b/src/screens/OrgSettings/OrgSettings.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Button, Dropdown, Row, Col } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; -import styles from './OrgSettings.module.css'; +import styles from 'style/app.module.css'; import OrgActionItemCategories from 'components/OrgSettings/ActionItemCategories/OrgActionItemCategories'; import OrganizationAgendaCategory from 'components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategory'; import { Navigate, useParams } from 'react-router-dom'; @@ -26,7 +26,7 @@ const settingtabs: SettingType[] = [ * * @returns The rendered component displaying the organization settings. */ -function orgSettings(): JSX.Element { +function OrgSettings(): JSX.Element { // Translation hook for internationalization const { t } = useTranslation('translation', { keyPrefix: 'orgSettings', @@ -126,4 +126,4 @@ function orgSettings(): JSX.Element { ); } -export default orgSettings; +export default OrgSettings; diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.module.css b/src/screens/OrganizationDashboard/OrganizationDashboard.module.css deleted file mode 100644 index 3ffe274196..0000000000 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.module.css +++ /dev/null @@ -1,35 +0,0 @@ -.cardHeader { - padding: 1.25rem 1rem 1rem 1rem; - border-bottom: 1px solid var(--bs-gray-200); - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; -} - -.cardHeader .cardTitle { - font-size: 1.2rem; - font-weight: 600; -} - -.cardBody { - min-height: 180px; - padding-top: 0; - max-height: 570px; - overflow-y: scroll; - width: 100%; - max-width: 400px; -} - -.cardBody .emptyContainer { - display: flex; - height: 180px; - justify-content: center; - align-items: center; -} - -.rankings { - aspect-ratio: 1; - border-radius: 50%; - width: 35px; -} diff --git a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx index ebea874d2e..abc712289c 100644 --- a/src/screens/OrganizationDashboard/OrganizationDashboard.tsx +++ b/src/screens/OrganizationDashboard/OrganizationDashboard.tsx @@ -31,7 +31,7 @@ import type { InterfaceQueryOrganizationsListObject, InterfaceVolunteerRank, } from 'utils/interfaces'; -import styles from './OrganizationDashboard.module.css'; +import styles from 'style/app.module.css'; import { VOLUNTEER_RANKING } from 'GraphQl/Queries/EventVolunteerQueries'; /** @@ -41,7 +41,7 @@ import { VOLUNTEER_RANKING } from 'GraphQl/Queries/EventVolunteerQueries'; * * @returns The rendered component. */ -function organizationDashboard(): JSX.Element { +function OrganizationDashboard(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'dashboard' }); const { t: tCommon } = useTranslation('common'); const { t: tErrors } = useTranslation('errors'); @@ -299,7 +299,7 @@ function organizationDashboard(): JSX.Element { {t('viewAll')} - + {loadingEvent ? ( [...Array(4)].map((_, index) => { return ; @@ -341,7 +341,7 @@ function organizationDashboard(): JSX.Element { {t('viewAll')} - + {loadingPost ? ( [...Array(4)].map((_, index) => { return ; @@ -392,7 +392,7 @@ function organizationDashboard(): JSX.Element { {loadingOrgData ? ( @@ -435,7 +435,10 @@ function organizationDashboard(): JSX.Element { {t('viewAll')} - + {rankingsLoading ? ( [...Array(3)].map((_, index) => { return ; @@ -483,4 +486,4 @@ function organizationDashboard(): JSX.Element { ); } -export default organizationDashboard; +export default OrganizationDashboard; diff --git a/src/screens/Requests/Requests.test.tsx b/src/screens/Requests/Requests.spec.tsx similarity index 91% rename from src/screens/Requests/Requests.test.tsx rename to src/screens/Requests/Requests.spec.tsx index 4606fdae08..820b24c40d 100644 --- a/src/screens/Requests/Requests.test.tsx +++ b/src/screens/Requests/Requests.spec.tsx @@ -1,8 +1,6 @@ import React, { act } from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { render, screen } from '@testing-library/react'; -import 'jest-localstorage-mock'; -import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; @@ -22,6 +20,34 @@ import { MOCKS4, } from './RequestsMocks'; import useLocalStorage from 'utils/useLocalstorage'; +import { vi } from 'vitest'; + +/** + * Set up `localStorage` stubs for testing. + */ + +vi.stubGlobal('localStorage', { + getItem: vi.fn(), + setItem: vi.fn(), + clear: vi.fn(), + removeItem: vi.fn(), +}); + +/** + * Mock `window.location` for testing redirection behavior. + */ + +Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost/', + assign: vi.fn(), + reload: vi.fn(), + pathname: '/', + search: '', + hash: '', + origin: 'http://localhost', + }, +}); const { setItem, removeItem } = useLocalStorage(); @@ -33,6 +59,14 @@ const link5 = new StaticMockLink(MOCKS_WITH_ERROR, true); const link6 = new StaticMockLink(MOCKS3, true); const link7 = new StaticMockLink(MOCKS4, true); +/** + * Utility function to wait for a specified amount of time. + * Wraps `setTimeout` in an `act` block for testing purposes. + * + * @param ms - The duration to wait in milliseconds. Default is 100ms. + * @returns A promise that resolves after the specified time. + */ + async function wait(ms = 100): Promise { await act(() => { return new Promise((resolve) => { @@ -53,7 +87,6 @@ afterEach(() => { describe('Testing Requests screen', () => { test('Component should be rendered properly', async () => { - const loadMoreRequests = jest.fn(); render( diff --git a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx b/src/screens/UserPortal/Campaigns/Campaigns.spec.tsx similarity index 88% rename from src/screens/UserPortal/Campaigns/Campaigns.test.tsx rename to src/screens/UserPortal/Campaigns/Campaigns.spec.tsx index 17b7eec4d5..09cde6b25d 100644 --- a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx +++ b/src/screens/UserPortal/Campaigns/Campaigns.spec.tsx @@ -20,39 +20,57 @@ import i18nForTest from 'utils/i18nForTest'; import type { ApolloLink } from '@apollo/client'; import useLocalStorage from 'utils/useLocalstorage'; import Campaigns from './Campaigns'; +import { vi, it, expect, describe } from 'vitest'; import { EMPTY_MOCKS, MOCKS, USER_FUND_CAMPAIGNS_ERROR, } from './CampaignsMocks'; -jest.mock('react-toastify', () => ({ +/* Mocking 'react-toastify` */ +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); -jest.mock('@mui/x-date-pickers/DateTimePicker', () => { + +/* Mocking `@mui/x-date-pickers/DateTimePicker` */ +vi.mock('@mui/x-date-pickers/DateTimePicker', async () => { + const actual = await vi.importActual( + '@mui/x-date-pickers/DesktopDateTimePicker', + ); return { - DateTimePicker: jest.requireActual( - '@mui/x-date-pickers/DesktopDateTimePicker', - ).DesktopDateTimePicker, + DateTimePicker: actual.DesktopDateTimePicker, }; }); + const { setItem } = useLocalStorage(); +/** + * Creates a mocked Apollo link for testing. + */ const link1 = new StaticMockLink(MOCKS); const link2 = new StaticMockLink(USER_FUND_CAMPAIGNS_ERROR); const link3 = new StaticMockLink(EMPTY_MOCKS); + const cTranslations = JSON.parse( JSON.stringify( i18nForTest.getDataByLanguage('en')?.translation.userCampaigns, ), ); + const pTranslations = JSON.parse( JSON.stringify(i18nForTest.getDataByLanguage('en')?.translation.pledges), ); +/* + * Renders the `Campaigns` component for testing. + * + * @param link - The mocked Apollo link used for testing. + * @returns The rendered result of the `Campaigns` component. + */ + const renderCampaigns = (link: ApolloLink): RenderResult => { return render( @@ -79,26 +97,38 @@ const renderCampaigns = (link: ApolloLink): RenderResult => { ); }; +/** + * Test suite for the User Campaigns screen. + */ describe('Testing User Campaigns Screen', () => { beforeEach(() => { setItem('userId', 'userId'); }); beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + /** + * Mocks the `useParams` function from `react-router-dom` to simulate URL parameters. + */ + vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: vi.fn(() => ({ orgId: 'orgId' })), // Mock `useParams` + }; + }); }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); afterEach(() => { cleanup(); }); + /** + * Verifies that the User Campaigns screen renders correctly with mock data. + */ it('should render the User Campaigns screen', async () => { renderCampaigns(link1); await waitFor(() => { @@ -108,6 +138,9 @@ describe('Testing User Campaigns Screen', () => { }); }); + /** + * Ensures the app redirects to the fallback URL if `userId` is null in LocalStorage. + */ it('should redirect to fallback URL if userId is null in LocalStorage', async () => { setItem('userId', null); renderCampaigns(link1); @@ -116,7 +149,11 @@ describe('Testing User Campaigns Screen', () => { }); }); + /** + * Ensures the app redirects to the fallback URL if URL parameters are undefined. + */ it('should redirect to fallback URL if URL params are undefined', async () => { + vi.unmock('react-router-dom'); // unmocking to get real behavior from useParams render( diff --git a/src/screens/UserPortal/Donate/Donate.test.tsx b/src/screens/UserPortal/Donate/Donate.spec.tsx similarity index 93% rename from src/screens/UserPortal/Donate/Donate.test.tsx rename to src/screens/UserPortal/Donate/Donate.spec.tsx index c4d435415e..b13056f5f9 100644 --- a/src/screens/UserPortal/Donate/Donate.test.tsx +++ b/src/screens/UserPortal/Donate/Donate.spec.tsx @@ -1,8 +1,14 @@ +/** + * Unit tests for the Donate component. + * + * This file contains tests for the Donate component to ensure it behaves as expected + * under various scenarios. + */ import React, { act } from 'react'; import { render, screen } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; import { I18nextProvider } from 'react-i18next'; - +import { vi } from 'vitest'; import { ORGANIZATION_DONATION_CONNECTION_LIST, USER_ORGANIZATION_CONNECTION, @@ -132,35 +138,35 @@ async function wait(ms = 100): Promise { }); } -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: '' }), +vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), + useParams: vi.fn(() => ({ orgId: '' })), })); -jest.mock('react-toastify', () => ({ +vi.mock('react-toastify', () => ({ toast: { - error: jest.fn(), - success: jest.fn(), + error: vi.fn(), + success: vi.fn(), }, })); describe('Testing Donate Screen [User Portal]', () => { Object.defineProperty(window, 'matchMedia', { writable: true, - value: jest.fn().mockImplementation((query) => ({ + value: vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, - addListener: jest.fn(), // Deprecated - removeListener: jest.fn(), // Deprecated - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), + addListener: vi.fn(), // Deprecated + removeListener: vi.fn(), // Deprecated + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), })), }); beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('Screen should be rendered properly', async () => { diff --git a/src/screens/UserPortal/UserScreen/UserScreen.test.tsx b/src/screens/UserPortal/UserScreen/UserScreen.spec.tsx similarity index 73% rename from src/screens/UserPortal/UserScreen/UserScreen.test.tsx rename to src/screens/UserPortal/UserScreen/UserScreen.spec.tsx index 642b231a66..65c5e6a650 100644 --- a/src/screens/UserPortal/UserScreen/UserScreen.test.tsx +++ b/src/screens/UserPortal/UserScreen/UserScreen.spec.tsx @@ -1,8 +1,19 @@ +/** + * This file contains unit tests for the UserScreen component. + * + * The tests cover: + * - Rendering of the correct title based on the location. + * - Functionality of the LeftDrawer component. + * - Behavior when the orgId is undefined. + * + * These tests use Vitest for test execution and MockedProvider for mocking GraphQL queries. + */ + import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, it, vi, beforeEach, expect } from 'vitest'; import { MockedProvider } from '@apollo/react-testing'; -import { fireEvent, render, screen } from '@testing-library/react'; import { I18nextProvider } from 'react-i18next'; -import 'jest-location-mock'; import { Provider } from 'react-redux'; import { BrowserRouter, useNavigate } from 'react-router-dom'; import { store } from 'state/store'; @@ -10,15 +21,20 @@ import i18nForTest from 'utils/i18nForTest'; import UserScreen from './UserScreen'; import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries'; import { StaticMockLink } from 'utils/StaticMockLink'; +import '@testing-library/jest-dom'; let mockID: string | undefined = '123'; let mockLocation: string | undefined = '/user/organization/123'; -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: mockID }), - useLocation: () => ({ pathname: mockLocation }), -})); +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: mockID }), + useLocation: () => ({ pathname: mockLocation }), + useNavigate: vi.fn(), // Mock only the necessary parts + }; +}); const MOCKS = [ { @@ -72,8 +88,13 @@ const clickToggleMenuBtn = (toggleButton: HTMLElement): void => { fireEvent.click(toggleButton); }; -describe('Testing LeftDrawer in OrganizationScreen', () => { - test('renders the correct title based on the titleKey for posts', () => { +describe('UserScreen tests with LeftDrawer functionality', () => { + beforeEach(() => { + mockID = '123'; + mockLocation = '/user/organization/123'; + }); + + it('renders the correct title for posts', () => { render( @@ -90,7 +111,7 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { expect(titleElement).toHaveTextContent('Posts'); }); - test('renders the correct title based on the titleKey', () => { + it('renders the correct title for people', () => { mockLocation = '/user/people/123'; render( @@ -109,7 +130,7 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { expect(titleElement).toHaveTextContent('People'); }); - test('LeftDrawer should toggle correctly based on window size and user interaction', async () => { + it('toggles LeftDrawer correctly based on window size and user interaction', () => { render( @@ -121,27 +142,30 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { , ); + const toggleButton = screen.getByTestId('closeMenu') as HTMLElement; const icon = toggleButton.querySelector('i'); - // Resize window to a smaller width + // Resize to small screen and check toggle state resizeWindow(800); clickToggleMenuBtn(toggleButton); expect(icon).toHaveClass('fa fa-angle-double-left'); - // Resize window back to a larger width + // Resize to large screen and check toggle state resizeWindow(1000); clickToggleMenuBtn(toggleButton); expect(icon).toHaveClass('fa fa-angle-double-right'); + // Check state on re-click clickToggleMenuBtn(toggleButton); expect(icon).toHaveClass('fa fa-angle-double-left'); }); - test('should be redirected to root when orgId is undefined', async () => { + it('redirects to root when orgId is undefined', () => { mockID = undefined; - const navigate = jest.fn(); - jest.spyOn({ useNavigate }, 'useNavigate').mockReturnValue(navigate); + const navigate = vi.fn(); + vi.spyOn({ useNavigate }, 'useNavigate').mockReturnValue(navigate); + render( @@ -153,6 +177,7 @@ describe('Testing LeftDrawer in OrganizationScreen', () => { , ); + expect(window.location.pathname).toEqual('/'); }); }); diff --git a/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.test.tsx b/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.spec.tsx similarity index 90% rename from src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.test.tsx rename to src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.spec.tsx index 43e0b15cdb..f636d8d8d4 100644 --- a/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.test.tsx +++ b/src/screens/UserPortal/Volunteer/UpcomingEvents/UpcomingEvents.spec.tsx @@ -21,11 +21,20 @@ import { } from './UpcomingEvents.mocks'; import { toast } from 'react-toastify'; import useLocalStorage from 'utils/useLocalstorage'; +import { vi } from 'vitest'; -jest.mock('react-toastify', () => ({ +/** + * Unit tests for the UpcomingEvents component. + * + * This file contains tests to verify the functionality and behavior of the UpcomingEvents component + * under various scenarios, including successful data fetching, error handling, and user interactions. + * Mocked dependencies are used to ensure isolated testing of the component. + */ + +vi.mock('react-toastify', () => ({ toast: { - success: jest.fn(), - error: jest.fn(), + success: vi.fn(), + error: vi.fn(), }, })); @@ -81,10 +90,13 @@ const renderUpcomingEvents = (link: ApolloLink): RenderResult => { describe('Testing Upcoming Events Screen', () => { beforeAll(() => { - jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useParams: () => ({ orgId: 'orgId' }), - })); + vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useParams: () => ({ orgId: 'orgId' }), + }; + }); }); beforeEach(() => { @@ -92,7 +104,7 @@ describe('Testing Upcoming Events Screen', () => { }); afterAll(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); it('should redirect to fallback URL if URL params are undefined', async () => { diff --git a/src/screens/Users/Users.module.css b/src/screens/Users/Users.module.css deleted file mode 100644 index 0750dba108..0000000000 --- a/src/screens/Users/Users.module.css +++ /dev/null @@ -1,95 +0,0 @@ -.btnsContainer { - display: flex; - margin: 2.5rem 0 2.5rem 0; -} - -.btnsContainer .btnsBlock { - display: flex; -} - -.btnsContainer .btnsBlock button { - margin-left: 1rem; - display: flex; - justify-content: center; - align-items: center; -} - -.btnsContainer .inputContainer { - flex: 1; - position: relative; -} -.btnsContainer .input { - width: 70%; - position: relative; -} - -.btnsContainer input { - outline: 1px solid var(--bs-gray-400); -} - -.btnsContainer .inputContainer button { - width: 52px; -} - -.listBox { - width: 100%; - flex: 1; -} - -.notFound { - flex: 1; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; -} - -@media (max-width: 1020px) { - .btnsContainer { - flex-direction: column; - margin: 1.5rem 0; - } - .btnsContainer .input { - width: 100%; - } - .btnsContainer .btnsBlock { - margin: 1.5rem 0 0 0; - justify-content: space-between; - } - - .btnsContainer .btnsBlock button { - margin: 0; - } - - .btnsContainer .btnsBlock div button { - margin-right: 1.5rem; - } -} - -/* For mobile devices */ - -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } - - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } - - .btnsContainer .btnsBlock div { - flex: 1; - } - - .btnsContainer .btnsBlock div[title='Sort organizations'] { - margin-right: 0.5rem; - } - - .btnsContainer .btnsBlock button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } -} diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index 72acba5b5c..3ea165d7b0 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -16,7 +16,7 @@ import TableLoader from 'components/TableLoader/TableLoader'; import UsersTableItem from 'components/UsersTableItem/UsersTableItem'; import InfiniteScroll from 'react-infinite-scroll-component'; import type { InterfaceQueryUserListItem } from 'utils/interfaces'; -import styles from './Users.module.css'; +import styles from '../../style/app.module.css'; import useLocalStorage from 'utils/useLocalstorage'; import type { ApolloError } from '@apollo/client'; /** @@ -91,8 +91,16 @@ const Users = (): JSX.Element => { }: { data?: { users: InterfaceQueryUserListItem[] }; loading: boolean; - fetchMore: any; - refetch: any; + fetchMore: (options: { + variables: Record; + updateQuery: ( + previousQueryResult: { users: InterfaceQueryUserListItem[] }, + options: { + fetchMoreResult?: { users: InterfaceQueryUserListItem[] }; + }, + ) => { users: InterfaceQueryUserListItem[] }; + }) => void; + refetch: (variables?: Record) => void; error?: ApolloError; } = useQuery(USER_LIST, { variables: { @@ -171,9 +179,11 @@ const Users = (): JSX.Element => { setHasMore(true); }; - const handleSearchByEnter = (e: any): void => { + const handleSearchByEnter = ( + e: React.KeyboardEvent, + ): void => { if (e.key === 'Enter') { - const { value } = e.target; + const { value } = e.target as HTMLInputElement; handleSearch(value); } }; @@ -211,16 +221,16 @@ const Users = (): JSX.Element => { { fetchMoreResult, }: { - fetchMoreResult: { users: InterfaceQueryUserListItem[] } | undefined; + fetchMoreResult?: { users: InterfaceQueryUserListItem[] }; }, - ): { users: InterfaceQueryUserListItem[] } | undefined => { + ) => { setIsLoadingMore(false); - if (!fetchMoreResult) return prev; + if (!fetchMoreResult) return prev || { users: [] }; if (fetchMoreResult.users.length < perPageResult) { setHasMore(false); } return { - users: [...(prev?.users || []), ...(fetchMoreResult.users || [])], + users: [...(prev?.users || []), ...fetchMoreResult.users], }; }, }); @@ -401,15 +411,23 @@ const Users = (): JSX.Element => { usersData && displayedUsers.length === 0 && searchByName.length > 0 ? ( -
+

{tCommon('noResultsFoundFor')} "{searchByName}"

-
+ ) : isLoading == false && usersData === undefined && displayedUsers.length === 0 ? ( -
+

{t('noUserFound')}

) : ( diff --git a/src/style/app.module.css b/src/style/app.module.css index eb6f370861..e0382fe70c 100644 --- a/src/style/app.module.css +++ b/src/style/app.module.css @@ -1,3 +1,34 @@ +:root { + --dropdown-border-color: #555555; + --dropdown-text-color: #555555; + --high-contrast-text: #494949; + /* Color contrast ratio: 9:1 (exceeds WCAG AAA) */ + --high-contrast-border: #2c2c2c; + --dropdown-hover-color: #eff1f7; + --grey-bg-color: #eaebef; + --subtle-blue-grey: #7c9beb; + --subtle-blue-grey-hover: #5f7e91; + --modal-width: 670px; + --modal-max-width: 680px; + --input-shadow-color: #dddddd; + --delete-button-bg: #f8d6dc; + --delete-button-color: #ff4d4f; + --search-button-bg: #a8c7fa; + --search-button-border: #555555; + --table-image-size: 50px; + --table-head-bg: var( + --bs-primary, + blue + ); /* Assuming var(--bs-primary) is defined elsewhere */ + --table-head-color: white; + --table-header-color: var(--bs-greyish-black, black); + --table-head-radius: 20px; + --table-bg-color: #eaebef; + --tablerow-bg-color: #eff1f7; + --row-background: var(--bs-white, white); + --font-size-header: 16px; +} + .noOutline input { outline: none; } @@ -76,7 +107,6 @@ align-items: center; gap: 10px; /* Adjust spacing between items */ - margin: 2.5rem 0; } .btnsContainer .btnsBlock { @@ -90,15 +120,28 @@ align-items: center; } -.btnsContainer .input { +.btnsContainer .inputContainer { flex: 1; position: relative; } -.btnsContainer .input button { +.btnsContainer .input { + width: 70%; +} + +.btnsContainer input { + outline: 1px solid var(--bs-gray-400); +} + +.btnsContainer .inputContainer button { width: 52px; } +.listBox { + width: 100%; + flex: 1; +} + .inputField { margin-top: 10px; margin-bottom: 10px; @@ -234,7 +277,7 @@ .editButton { background-color: var(--search-button-bg); border-color: var(--search-button-border); - color: #555555; + color: var(--high-contrast-text); margin-left: 2; } @@ -242,7 +285,7 @@ margin-bottom: 10px; background-color: var(--search-button-bg); border-color: var(--grey-bg-color); - color: #555555; + color: var(--high-contrast-text); } .addButton:hover { @@ -443,7 +486,7 @@ hr { flex-direction: row; font-weight: 900; font-size: 16px; - color: rgb(80, 80, 80); + color: var(--high-contrast-text); } .rankings { @@ -494,37 +537,6 @@ hr { outline-offset: -2px; } -@media (max-width: 520px) { - .btnsContainer { - margin-bottom: 0; - } - - .btnsContainer .btnsBlock { - display: block; - margin-top: 1rem; - margin-right: 0; - } - - .btnsContainer .btnsBlock div { - flex: 1; - } - - .btnsContainer .btnsBlock div[title='Sort organizations'] { - margin-right: 0.5rem; - } - - .btnsContainer .btnsBlock button { - margin-bottom: 1rem; - margin-right: 0; - width: 100%; - } -} - -.listBox { - width: 100%; - flex: 1; -} - .listTable { width: 100%; box-sizing: border-box; @@ -556,6 +568,93 @@ hr { flex-direction: column; } +.headerBtn { + box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 2px; +} + +.settingsContainer { + min-height: 100vh; +} + +.settingsBody { + min-height: 100vh; + margin: 2.5rem 1rem; +} + +.cardHeader { + padding: 1.25rem 1rem 1rem 1rem; + border-bottom: 1px solid var(--bs-gray-200); + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} +.cardHeader .cardTitle { + font-size: 1.2rem; + font-weight: 600; +} + +.containerBody { + min-height: 180px; + padding-top: 0; + max-height: 570px; + overflow-y: auto; + width: 100%; + max-width: min(400px, 90vw); + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.3) transparent; + + &::-webkit-scrollbar { + width: thin; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.3); + } +} + +.containerBody .emptyContainer { + display: flex; + min-height: 180px; + justify-content: center; + align-items: center; +} + +.containerBody .rankings { + aspect-ratio: 1; + border-radius: 50%; + width: 35px; +} + +.cardBody { + min-height: 180px; +} + +.cardBody .textBox { + margin: 0 0 3rem 0; + color: var(--high-contrast-text); +} + +.settingsTabs { + display: none; +} + +@media (min-width: 576px) { + .settingsDropdown { + display: none; + } +} + +@media (min-width: 576px) { + .settingsTabs { + display: block; + } +} + @media (max-width: 1020px) { .btnsContainer { flex-direction: column; @@ -580,16 +679,67 @@ hr { } } -@media (max-width: 1120px) { - .contract { - padding-left: calc(250px + 2rem + 1.5rem); +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } + + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + } + + .btnsContainer .btnsBlock div { + flex: 1; } - .listBox .itemCard { + .btnsContainer .btnsBlock div[title='Sort organizations'] { + margin-right: 0.5rem; + } + + .btnsContainer .btnsBlock button { + margin-bottom: 1rem; + margin-right: 0; width: 100%; } } +.listBox { + width: 100%; + flex: 1; +} + +.listTable { + width: 100%; + box-sizing: border-box; + background: #ffffff; + border: 1px solid #0000001f; + border-radius: 24px; +} + +.listBox .customTable { + margin-bottom: 0%; +} + +.requestsTable thead th { + font-size: 20px; + font-weight: 400; + line-height: 24px; + letter-spacing: 0em; + text-align: left; + color: #000000; + border-bottom: 1px solid #dddddd; + padding: 1.5rem; +} + +.notFound { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + @-webkit-keyframes load8 { 0% { -webkit-transform: rotate(0deg); diff --git a/src/utils/errorHandler.test.tsx b/src/utils/errorHandler.test.tsx index 45f46e6389..f229e8d5fa 100644 --- a/src/utils/errorHandler.test.tsx +++ b/src/utils/errorHandler.test.tsx @@ -11,23 +11,87 @@ jest.mock('react-toastify', () => ({ describe('Test if errorHandler is working properly', () => { const t: TFunction = (key: string) => key; - const tErrors: TFunction = (key: string, options?: Record) => - key; + const tErrors: TFunction = ( + key: string, + options?: Record, + ) => { + if (options) { + console.log(`options are passed, but the function returns only ${key}`); + } + return key; + }; - it('should call toast.error with the correct message if error message is "Failed to fetch"', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call toast.error with the correct message if error message is "Failed to fetch"', async () => { const error = new Error('Failed to fetch'); errorHandler(t, error); expect(toast.error).toHaveBeenCalledWith(tErrors('talawaApiUnavailable')); }); - it('should call toast.error with the error message if it is not "Failed to fetch"', () => { - const error = new Error('Some other error message'); + it('should call toast.error with the correct message if error message contains this substring "Value is not a valid phone number"', () => { + const error = new Error('This value is not a valid phone number'); errorHandler(t, error); + expect(toast.error).toHaveBeenCalledWith(tErrors('invalidPhoneNumber')); + }); + + test.each([ + ['EducationGrade', 'invalidEducationGrade'], + ['EmploymentStatus', 'invalidEmploymentStatus'], + ['MaritalStatus', 'invalidMaritalStatus'], + ])('should handle invalid %s error', (field, expectedKey) => { + const error = new Error(`This value does not exist in "${field}"`); + errorHandler(t, error); + expect(toast.error).toHaveBeenCalledWith(tErrors(expectedKey)); + }); + + it('should call toast.error with the correct message if error message contains this substring "status code 400"', () => { + const error = new Error('Server responded with status code 400'); + errorHandler(t, error); + + expect(toast.error).toHaveBeenCalledWith(tErrors('error400')); + }); + + it('should handle error messages with different cases', () => { + errorHandler(t, new Error('VALUE IS NOT A VALID PHONE NUMBER')); + expect(toast.error).toHaveBeenCalledWith(tErrors('invalidPhoneNumber')); + + errorHandler(t, new Error('This Value Does Not Exist in "EducationGrade"')); + expect(toast.error).toHaveBeenCalledWith(tErrors('invalidEducationGrade')); + }); + it('should call toast.error with the error message if it is an instance of error but have not matched any error message patterns', () => { + const error = new Error('Bandhan sent an error message'); + errorHandler(t, error); expect(toast.error).toHaveBeenCalledWith(error.message); }); + it('should handle different types for the first parameter while still showing error messages', () => { + errorHandler(undefined, new Error('Some error')); + expect(toast.error).toHaveBeenCalled(); + + errorHandler(null, new Error('Some error')); + expect(toast.error).toHaveBeenCalled(); + + errorHandler({}, new Error('Some error')); + expect(toast.error).toHaveBeenCalled(); + }); + + it('should handle non-null but non-Error objects for the error parameter', () => { + errorHandler(t, { message: 'Error message in object' }); + expect(toast.error).toHaveBeenCalledWith( + tErrors('unknownError', { msg: { message: 'Error message in object' } }), + ); + + errorHandler(t, 'Direct error message'); + expect(toast.error).toHaveBeenCalledWith( + tErrors('unknownError', { msg: 'Direct error message' }), + ); + }); + it('should call toast.error with the error message if error object is falsy', () => { const error = null; errorHandler(t, error); diff --git a/src/utils/errorHandler.tsx b/src/utils/errorHandler.tsx index b7a22210a8..e4e543e940 100644 --- a/src/utils/errorHandler.tsx +++ b/src/utils/errorHandler.tsx @@ -5,18 +5,26 @@ import i18n from './i18n'; /** This function is used to handle api errors in the application. It takes in the error object and displays the error message to the user. - If the error is due to the Talawa API being unavailable, it displays a custom message. + If the error is due to the Talawa API being unavailable, it displays a custom message. And for other error cases, it is using regular expression (case-insensitive) to match and show valid messages */ export const errorHandler = (a: unknown, error: unknown): void => { const tErrors: TFunction = i18n.getFixedT(null, 'errors'); if (error instanceof Error) { - switch (error.message) { - case 'Failed to fetch': - toast.error(tErrors('talawaApiUnavailable') as string); - break; - // Add more cases as needed - default: - toast.error(error.message); + const errorMessage = error.message; + if (errorMessage === 'Failed to fetch') { + toast.error(tErrors('talawaApiUnavailable')); + } else if (errorMessage.match(/value is not a valid phone number/i)) { + toast.error(tErrors('invalidPhoneNumber')); + } else if (errorMessage.match(/does not exist in "EducationGrade"/i)) { + toast.error(tErrors('invalidEducationGrade')); + } else if (errorMessage.match(/does not exist in "EmploymentStatus"/i)) { + toast.error(tErrors('invalidEmploymentStatus')); + } else if (errorMessage.match(/does not exist in "MaritalStatus"/i)) { + toast.error(tErrors('invalidMaritalStatus')); + } else if (errorMessage.match(/status code 400/i)) { + toast.error(tErrors('error400')); + } else { + toast.error(errorMessage); } } else { toast.error(tErrors('unknownError', { msg: error }) as string); diff --git a/vitest.config.ts b/vitest.config.ts index cd08488b3c..c158cf9c2a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,8 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import { nodePolyfills } from 'vite-plugin-node-polyfills'; import tsconfigPaths from 'vite-tsconfig-paths'; +import svgrPlugin from 'vite-plugin-svgr'; export default defineConfig({ plugins: [ @@ -10,11 +11,13 @@ export default defineConfig({ include: ['events'], }), tsconfigPaths(), + svgrPlugin(), ], test: { include: ['src/**/*.spec.{js,jsx,ts,tsx}'], globals: true, environment: 'jsdom', + setupFiles: 'vitest.setup.ts', coverage: { enabled: true, provider: 'istanbul', diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom';