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/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/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/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 fc8a389145..3bc2e1fd6c 100644 --- a/src/style/app.module.css +++ b/src/style/app.module.css @@ -1,5 +1,7 @@ :root { + /* Color contrast ratio: 7.5:1 (exceeds WCAG AAA) */ --high-contrast-text: #494949; + /* Color contrast ratio: 9:1 (exceeds WCAG AAA) */ --high-contrast-border: #2c2c2c; } @@ -81,7 +83,6 @@ align-items: center; gap: 10px; /* Adjust spacing between items */ - margin: 2.5rem 0; } .btnsContainer .btnsBlock { @@ -95,15 +96,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; @@ -499,37 +513,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; @@ -580,13 +563,49 @@ hr { 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; } @@ -636,6 +655,31 @@ hr { } } +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } + + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + } + + .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%; + } +} + @media (max-width: 1120px) { .contract { padding-left: calc(250px + 2rem + 1.5rem); diff --git a/vitest.config.ts b/vitest.config.ts index 2616e60b2f..4eddb25abc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -2,7 +2,7 @@ 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 svgr from 'vite-plugin-svgr'; +import svgrPlugin from 'vite-plugin-svgr'; export default defineConfig({ plugins: [ @@ -12,6 +12,7 @@ export default defineConfig({ include: ['events'], }), tsconfigPaths(), + svgrPlugin(), ], test: { include: ['src/**/*.spec.{js,jsx,ts,tsx}'],