@@ -182,6 +179,6 @@ const organizationAgendaCategory: FC = ({
/>
);
-};
+}
export default organizationAgendaCategory;
diff --git a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryErrorMocks.ts b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryErrorMocks.ts
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryErrorMocks.ts
rename to src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryErrorMocks.ts
diff --git a/src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryMocks.ts b/src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryMocks.ts
similarity index 100%
rename from src/components/OrgSettings/AgendaItemCategories/OrganizationAgendaCategoryMocks.ts
rename to src/screens/OrganizationAgendaCategory/OrganizationAgendaCategoryMocks.ts
diff --git a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx b/src/screens/UserPortal/Campaigns/Campaigns.test.tsx
index 17b7eec4d5..443d643cff 100644
--- a/src/screens/UserPortal/Campaigns/Campaigns.test.tsx
+++ b/src/screens/UserPortal/Campaigns/Campaigns.test.tsx
@@ -155,17 +155,14 @@ describe('Testing User Campaigns Screen', () => {
it('Check if All details are rendered correctly', async () => {
renderCampaigns(link1);
-
- const detailContainer = await screen.findByTestId('detailContainer1');
- const detailContainer2 = await screen.findByTestId('detailContainer2');
await waitFor(() => {
- expect(detailContainer).toBeInTheDocument();
- expect(detailContainer2).toBeInTheDocument();
+ const detailContainer = screen.getByTestId('detailContainer1');
expect(detailContainer).toHaveTextContent('School Campaign');
expect(detailContainer).toHaveTextContent('$22000');
expect(detailContainer).toHaveTextContent('2024-07-28');
expect(detailContainer).toHaveTextContent('2025-08-31');
expect(detailContainer).toHaveTextContent('Active');
+ const detailContainer2 = screen.getByTestId('detailContainer2');
expect(detailContainer2).toHaveTextContent('Hospital Campaign');
expect(detailContainer2).toHaveTextContent('$9000');
expect(detailContainer2).toHaveTextContent('2024-07-28');
@@ -294,6 +291,18 @@ describe('Testing User Campaigns Screen', () => {
});
});
+ it('Redirect to My Pledges screen', async () => {
+ renderCampaigns(link1);
+
+ const myPledgesBtn = await screen.findByText(cTranslations.myPledges);
+ expect(myPledgesBtn).toBeInTheDocument();
+ userEvent.click(myPledgesBtn);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('pledgeScreen')).toBeInTheDocument();
+ });
+ });
+
it('open and closes add pledge modal', async () => {
renderCampaigns(link1);
@@ -309,16 +318,4 @@ describe('Testing User Campaigns Screen', () => {
expect(screen.queryByTestId('pledgeModalCloseBtn')).toBeNull(),
);
});
-
- it('Redirect to My Pledges screen', async () => {
- renderCampaigns(link1);
-
- const myPledgesBtn = await screen.findByText(cTranslations.myPledges);
- expect(myPledgesBtn).toBeInTheDocument();
- userEvent.click(myPledgesBtn);
-
- await waitFor(() => {
- expect(screen.getByTestId('pledgeScreen')).toBeInTheDocument();
- });
- });
});
diff --git a/src/screens/UserPortal/Campaigns/CampaignsMocks.ts b/src/screens/UserPortal/Campaigns/CampaignsMocks.ts
index f64401bca5..7b91fac025 100644
--- a/src/screens/UserPortal/Campaigns/CampaignsMocks.ts
+++ b/src/screens/UserPortal/Campaigns/CampaignsMocks.ts
@@ -1,63 +1,6 @@
import { USER_DETAILS } from 'GraphQl/Queries/Queries';
import { USER_FUND_CAMPAIGNS } from 'GraphQl/Queries/fundQueries';
-const userDetailsQuery = {
- request: {
- query: USER_DETAILS,
- variables: {
- id: 'userId',
- },
- },
- result: {
- data: {
- user: {
- user: {
- _id: 'userId',
- joinedOrganizations: [
- {
- _id: '6537904485008f171cf29924',
- __typename: 'Organization',
- },
- ],
- firstName: 'Harve',
- lastName: 'Lance',
- email: 'testuser1@example.com',
- image: null,
- createdAt: '2023-04-13T04:53:17.742Z',
- birthDate: null,
- educationGrade: null,
- employmentStatus: null,
- gender: null,
- maritalStatus: null,
- phone: null,
- address: {
- line1: 'Line1',
- countryCode: 'CountryCode',
- city: 'CityName',
- state: 'State',
- __typename: 'Address',
- },
- registeredEvents: [],
- membershipRequests: [],
- __typename: 'User',
- },
- appUserProfile: {
- _id: '67078abd85008f171cf2991d',
- adminFor: [],
- isSuperAdmin: false,
- appLanguageCode: 'en',
- pluginCreationAllowed: true,
- createdOrganizations: [],
- createdEvents: [],
- eventAdmin: [],
- __typename: 'AppUserProfile',
- },
- __typename: 'UserData',
- },
- },
- },
-};
-
export const MOCKS = [
{
request: {
@@ -230,7 +173,62 @@ export const MOCKS = [
},
},
},
- userDetailsQuery,
+ {
+ request: {
+ query: USER_DETAILS,
+ variables: {
+ id: 'userId',
+ },
+ },
+ result: {
+ data: {
+ user: {
+ user: {
+ _id: 'userId',
+ joinedOrganizations: [
+ {
+ _id: '6537904485008f171cf29924',
+ __typename: 'Organization',
+ },
+ ],
+ firstName: 'Harve',
+ lastName: 'Lance',
+ email: 'testuser1@example.com',
+ image: null,
+ createdAt: '2023-04-13T04:53:17.742Z',
+ birthDate: null,
+ educationGrade: null,
+ employmentStatus: null,
+ gender: null,
+ maritalStatus: null,
+ phone: null,
+ address: {
+ line1: 'Line1',
+ countryCode: 'CountryCode',
+ city: 'CityName',
+ state: 'State',
+ __typename: 'Address',
+ },
+ registeredEvents: [],
+ membershipRequests: [],
+ __typename: 'User',
+ },
+ appUserProfile: {
+ _id: '67078abd85008f171cf2991d',
+ adminFor: [],
+ isSuperAdmin: false,
+ appLanguageCode: 'en',
+ pluginCreationAllowed: true,
+ createdOrganizations: [],
+ createdEvents: [],
+ eventAdmin: [],
+ __typename: 'AppUserProfile',
+ },
+ __typename: 'UserData',
+ },
+ },
+ },
+ },
];
export const EMPTY_MOCKS = [
@@ -251,7 +249,62 @@ export const EMPTY_MOCKS = [
},
},
},
- userDetailsQuery,
+ {
+ request: {
+ query: USER_DETAILS,
+ variables: {
+ id: 'userId',
+ },
+ },
+ result: {
+ data: {
+ user: {
+ user: {
+ _id: 'userId',
+ joinedOrganizations: [
+ {
+ _id: '6537904485008f171cf29924',
+ __typename: 'Organization',
+ },
+ ],
+ firstName: 'Harve',
+ lastName: 'Lance',
+ email: 'testuser1@example.com',
+ image: null,
+ createdAt: '2023-04-13T04:53:17.742Z',
+ birthDate: null,
+ educationGrade: null,
+ employmentStatus: null,
+ gender: null,
+ maritalStatus: null,
+ phone: null,
+ address: {
+ line1: 'Line1',
+ countryCode: 'CountryCode',
+ city: 'CityName',
+ state: 'State',
+ __typename: 'Address',
+ },
+ registeredEvents: [],
+ membershipRequests: [],
+ __typename: 'User',
+ },
+ appUserProfile: {
+ _id: '67078abd85008f171cf2991d',
+ adminFor: [],
+ isSuperAdmin: false,
+ appLanguageCode: 'en',
+ pluginCreationAllowed: true,
+ createdOrganizations: [],
+ createdEvents: [],
+ eventAdmin: [],
+ __typename: 'AppUserProfile',
+ },
+ __typename: 'UserData',
+ },
+ },
+ },
+ },
];
export const USER_FUND_CAMPAIGNS_ERROR = [
@@ -268,5 +321,4 @@ export const USER_FUND_CAMPAIGNS_ERROR = [
},
error: new Error('Error fetching campaigns'),
},
- userDetailsQuery,
];
diff --git a/src/screens/UserPortal/Pledges/Pledge.test.tsx b/src/screens/UserPortal/Pledges/Pledge.test.tsx
index 3d5eef94c2..ecdd25a1d3 100644
--- a/src/screens/UserPortal/Pledges/Pledge.test.tsx
+++ b/src/screens/UserPortal/Pledges/Pledge.test.tsx
@@ -126,6 +126,20 @@ describe('Testing User Pledge Screen', () => {
});
});
+ it('should render the Campaign Pledge screen with error', async () => {
+ renderMyPledges(link2);
+ await waitFor(() => {
+ expect(screen.getByTestId('errorMsg')).toBeInTheDocument();
+ });
+ });
+
+ it('renders the empty pledge component', async () => {
+ renderMyPledges(link3);
+ await waitFor(() =>
+ expect(screen.getByText(translations.noPledges)).toBeInTheDocument(),
+ );
+ });
+
it('check if user image renders', async () => {
renderMyPledges(link1);
await waitFor(() => {
@@ -338,18 +352,4 @@ describe('Testing User Pledge Screen', () => {
expect(screen.queryByTestId('pledgeModalCloseBtn')).toBeNull(),
);
});
-
- it('should render the Campaign Pledge screen with error', async () => {
- renderMyPledges(link2);
- await waitFor(() => {
- expect(screen.getByTestId('errorMsg')).toBeInTheDocument();
- });
- });
-
- it('renders the empty pledge component', async () => {
- renderMyPledges(link3);
- await waitFor(() =>
- expect(screen.getByText(translations.noPledges)).toBeInTheDocument(),
- );
- });
});
diff --git a/src/screens/UserPortal/Pledges/PledgesMocks.ts b/src/screens/UserPortal/Pledges/PledgesMocks.ts
index c7666987ff..9aa3780fbd 100644
--- a/src/screens/UserPortal/Pledges/PledgesMocks.ts
+++ b/src/screens/UserPortal/Pledges/PledgesMocks.ts
@@ -1,64 +1,11 @@
-import { DELETE_PLEDGE } from 'GraphQl/Mutations/PledgeMutation';
+import {
+ CREATE_PlEDGE,
+ DELETE_PLEDGE,
+ UPDATE_PLEDGE,
+} from 'GraphQl/Mutations/PledgeMutation';
import { USER_DETAILS } from 'GraphQl/Queries/Queries';
import { USER_PLEDGES } from 'GraphQl/Queries/fundQueries';
-const userDetailsQuery = {
- request: {
- query: USER_DETAILS,
- variables: {
- id: 'userId',
- },
- },
- result: {
- data: {
- user: {
- user: {
- _id: 'userId',
- joinedOrganizations: [
- {
- _id: '6537904485008f171cf29924',
- __typename: 'Organization',
- },
- ],
- firstName: 'Harve',
- lastName: 'Lance',
- email: 'testuser1@example.com',
- image: null,
- createdAt: '2023-04-13T04:53:17.742Z',
- birthDate: null,
- educationGrade: null,
- employmentStatus: null,
- gender: null,
- maritalStatus: null,
- phone: null,
- address: {
- line1: 'Line1',
- countryCode: 'CountryCode',
- city: 'CityName',
- state: 'State',
- __typename: 'Address',
- },
- registeredEvents: [],
- membershipRequests: [],
- __typename: 'User',
- },
- appUserProfile: {
- _id: '67078abd85008f171cf2991d',
- adminFor: [],
- isSuperAdmin: false,
- appLanguageCode: 'en',
- pluginCreationAllowed: true,
- createdOrganizations: [],
- createdEvents: [],
- eventAdmin: [],
- __typename: 'AppUserProfile',
- },
- __typename: 'UserData',
- },
- },
- },
-};
-
export const MOCKS = [
{
request: {
@@ -554,7 +501,62 @@ export const MOCKS = [
},
},
},
- userDetailsQuery,
+ {
+ request: {
+ query: USER_DETAILS,
+ variables: {
+ id: 'userId',
+ },
+ },
+ result: {
+ data: {
+ user: {
+ user: {
+ _id: 'userId',
+ joinedOrganizations: [
+ {
+ _id: '6537904485008f171cf29924',
+ __typename: 'Organization',
+ },
+ ],
+ firstName: 'Harve',
+ lastName: 'Lance',
+ email: 'testuser1@example.com',
+ image: null,
+ createdAt: '2023-04-13T04:53:17.742Z',
+ birthDate: null,
+ educationGrade: null,
+ employmentStatus: null,
+ gender: null,
+ maritalStatus: null,
+ phone: null,
+ address: {
+ line1: 'Line1',
+ countryCode: 'CountryCode',
+ city: 'CityName',
+ state: 'State',
+ __typename: 'Address',
+ },
+ registeredEvents: [],
+ membershipRequests: [],
+ __typename: 'User',
+ },
+ appUserProfile: {
+ _id: '67078abd85008f171cf2991d',
+ adminFor: [],
+ isSuperAdmin: false,
+ appLanguageCode: 'en',
+ pluginCreationAllowed: true,
+ createdOrganizations: [],
+ createdEvents: [],
+ eventAdmin: [],
+ __typename: 'AppUserProfile',
+ },
+ __typename: 'UserData',
+ },
+ },
+ },
+ },
];
export const EMPTY_MOCKS = [
@@ -575,7 +577,6 @@ export const EMPTY_MOCKS = [
},
},
},
- userDetailsQuery,
];
export const USER_PLEDGES_ERROR = [
@@ -592,5 +593,4 @@ export const USER_PLEDGES_ERROR = [
},
error: new Error('Error fetching pledges'),
},
- userDetailsQuery,
];
diff --git a/src/state/reducers/routesReducer.test.ts b/src/state/reducers/routesReducer.test.ts
index 8bdc1c069b..8d8de8a5dd 100644
--- a/src/state/reducers/routesReducer.test.ts
+++ b/src/state/reducers/routesReducer.test.ts
@@ -17,6 +17,7 @@ describe('Testing Routes reducer', () => {
{ name: 'Events', url: '/orgevents/undefined' },
{ name: 'Venues', url: '/orgvenues/undefined' },
{ name: 'Action Items', url: '/orgactionitems/undefined' },
+ { name: 'Agenda Items Category', url: '/orgagendacategory/undefined' },
{ name: 'Posts', url: '/orgpost/undefined' },
{
name: 'Block/Unblock',
@@ -69,6 +70,11 @@ describe('Testing Routes reducer', () => {
comp_id: 'orgactionitems',
component: 'OrganizationActionItems',
},
+ {
+ name: 'Agenda Items Category',
+ comp_id: 'orgagendacategory',
+ component: 'OrganizationAgendaCategory',
+ },
{ name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' },
{ name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' },
{
@@ -120,6 +126,7 @@ describe('Testing Routes reducer', () => {
{ name: 'Events', url: '/orgevents/orgId' },
{ name: 'Venues', url: '/orgvenues/orgId' },
{ name: 'Action Items', url: '/orgactionitems/orgId' },
+ { name: 'Agenda Items Category', url: '/orgagendacategory/orgId' },
{ name: 'Posts', url: '/orgpost/orgId' },
{ name: 'Block/Unblock', url: '/blockuser/orgId' },
{ name: 'Advertisement', url: '/orgads/orgId' },
@@ -169,6 +176,11 @@ describe('Testing Routes reducer', () => {
comp_id: 'orgactionitems',
component: 'OrganizationActionItems',
},
+ {
+ name: 'Agenda Items Category',
+ comp_id: 'orgagendacategory',
+ component: 'OrganizationAgendaCategory',
+ },
{ name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' },
{ name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' },
{
@@ -216,6 +228,7 @@ describe('Testing Routes reducer', () => {
{ name: 'Events', url: '/orgevents/undefined' },
{ name: 'Venues', url: '/orgvenues/undefined' },
{ name: 'Action Items', url: '/orgactionitems/undefined' },
+ { name: 'Agenda Items Category', url: '/orgagendacategory/undefined' },
{ name: 'Posts', url: '/orgpost/undefined' },
{
name: 'Block/Unblock',
@@ -271,6 +284,11 @@ describe('Testing Routes reducer', () => {
comp_id: 'orgactionitems',
component: 'OrganizationActionItems',
},
+ {
+ name: 'Agenda Items Category',
+ comp_id: 'orgagendacategory',
+ component: 'OrganizationAgendaCategory',
+ },
{ name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' },
{ name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' },
{
diff --git a/src/state/reducers/routesReducer.ts b/src/state/reducers/routesReducer.ts
index 5d50f5402d..878fe73099 100644
--- a/src/state/reducers/routesReducer.ts
+++ b/src/state/reducers/routesReducer.ts
@@ -77,6 +77,11 @@ const components: ComponentType[] = [
comp_id: 'orgactionitems',
component: 'OrganizationActionItems',
},
+ {
+ name: 'Agenda Items Category',
+ comp_id: 'orgagendacategory',
+ component: 'OrganizationAgendaCategory',
+ },
{ name: 'Posts', comp_id: 'orgpost', component: 'OrgPost' },
{ name: 'Block/Unblock', comp_id: 'blockuser', component: 'BlockUser' },
{ name: 'Advertisement', comp_id: 'orgads', component: 'Advertisements' },
diff --git a/src/utils/interfaces.ts b/src/utils/interfaces.ts
index 495234a5a1..3bdf2a7d98 100644
--- a/src/utils/interfaces.ts
+++ b/src/utils/interfaces.ts
@@ -11,8 +11,6 @@ export interface InterfaceActionItemCategoryInfo {
_id: string;
name: string;
isDisabled: boolean;
- createdAt: string;
- creator: { _id: string; firstName: string; lastName: string };
}
export interface InterfaceActionItemCategoryList {
@@ -25,20 +23,18 @@ export interface InterfaceActionItemInfo {
_id: string;
firstName: string;
lastName: string;
- image: string | null;
};
assigner: {
_id: string;
firstName: string;
lastName: string;
- image: string | null;
};
actionItemCategory: {
_id: string;
name: string;
};
preCompletionNotes: string;
- postCompletionNotes: string | null;
+ postCompletionNotes: string;
assignmentDate: Date;
dueDate: Date;
completionDate: Date;
@@ -46,13 +42,12 @@ export interface InterfaceActionItemInfo {
event: {
_id: string;
title: string;
- } | null;
+ };
creator: {
_id: string;
firstName: string;
lastName: string;
};
- allotedHours: number | null;
}
export interface InterfaceActionItemList {
@@ -566,8 +561,3 @@ export interface InterfaceAgendaItemList {
export interface InterfaceMapType {
[key: string]: string;
}
-
-export interface InterfaceCustomFieldData {
- type: string;
- name: string;
-}
diff --git a/src/utils/useSession.test.tsx b/src/utils/useSession.test.tsx
new file mode 100644
index 0000000000..32287ccbb0
--- /dev/null
+++ b/src/utils/useSession.test.tsx
@@ -0,0 +1,544 @@
+import type { ReactNode } from 'react';
+import React from 'react';
+import { renderHook, act, waitFor } from '@testing-library/react';
+import { MockedProvider } from '@apollo/client/testing';
+import { toast } from 'react-toastify';
+import useSession from './useSession';
+import { GET_COMMUNITY_SESSION_TIMEOUT_DATA } from 'GraphQl/Queries/Queries';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
+import { errorHandler } from 'utils/errorHandler';
+import { BrowserRouter } from 'react-router-dom';
+
+jest.mock('react-toastify', () => ({
+ toast: {
+ info: jest.fn(),
+ warning: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+jest.mock('utils/errorHandler', () => ({
+ errorHandler: jest.fn(),
+}));
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+const MOCKS = [
+ {
+ request: {
+ query: GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ },
+ result: {
+ data: {
+ getCommunityData: {
+ timeout: 30,
+ },
+ },
+ },
+ delay: 100,
+ },
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ result: {
+ data: {
+ revokeRefreshTokenForUser: true,
+ },
+ },
+ },
+];
+
+const wait = (ms: number): Promise
=>
+ new Promise((resolve) => setTimeout(resolve, ms));
+
+describe('useSession Hook', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ jest.spyOn(window, 'addEventListener').mockImplementation(jest.fn());
+ jest.spyOn(window, 'removeEventListener').mockImplementation(jest.fn());
+ Object.defineProperty(global, 'localStorage', {
+ value: {
+ clear: jest.fn(),
+ },
+ writable: true,
+ });
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ jest.useRealTimers();
+ jest.restoreAllMocks();
+ });
+
+ test('should handle visibility change to visible', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ // Simulate visibility change
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(15 * 60 * 1000);
+ });
+
+ await waitFor(() => {
+ expect(window.addEventListener).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(window.addEventListener).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(toast.warning).toHaveBeenCalledWith('sessionWarning');
+ });
+
+ jest.useRealTimers();
+ });
+
+ test('should handle visibility change to hidden and ensure no warning appears in 15 minutes', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ writable: true,
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(15 * 60 * 1000);
+ });
+
+ await waitFor(() => {
+ expect(window.removeEventListener).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(window.removeEventListener).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(toast.warning).not.toHaveBeenCalled();
+ });
+
+ jest.useRealTimers();
+ });
+
+ test('should register event listeners on startSession', async () => {
+ const addEventListenerMock = jest.fn();
+ const originalWindowAddEventListener = window.addEventListener;
+ const originalDocumentAddEventListener = document.addEventListener;
+
+ window.addEventListener = addEventListenerMock;
+ document.addEventListener = addEventListenerMock;
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ expect(addEventListenerMock).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(addEventListenerMock).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(addEventListenerMock).toHaveBeenCalledWith(
+ 'visibilitychange',
+ expect.any(Function),
+ );
+
+ window.addEventListener = originalWindowAddEventListener;
+ document.addEventListener = originalDocumentAddEventListener;
+ });
+
+ test('should call handleLogout after session timeout', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(31 * 60 * 1000);
+ });
+
+ await waitFor(() => {
+ expect(global.localStorage.clear).toHaveBeenCalled();
+ expect(toast.warning).toHaveBeenCalledTimes(2);
+ expect(toast.warning).toHaveBeenNthCalledWith(1, 'sessionWarning');
+ expect(toast.warning).toHaveBeenNthCalledWith(2, 'sessionLogout', {
+ autoClose: false,
+ });
+ });
+ });
+
+ test('should show a warning toast before session expiration', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ jest.advanceTimersByTime(15 * 60 * 1000);
+ });
+
+ await waitFor(() =>
+ expect(toast.warning).toHaveBeenCalledWith('sessionWarning'),
+ );
+
+ jest.useRealTimers();
+ });
+
+ test('should handle error when revoking token fails', async () => {
+ const consoleErrorMock = jest.spyOn(console, 'error').mockImplementation();
+
+ const errorMocks = [
+ {
+ request: {
+ query: GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ },
+ result: {
+ data: {
+ getCommunityData: {
+ timeout: 30,
+ },
+ },
+ },
+ delay: 1000,
+ },
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ error: new Error('Failed to revoke refresh token'),
+ },
+ ];
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ result.current.handleLogout();
+ });
+
+ await waitFor(() =>
+ expect(consoleErrorMock).toHaveBeenCalledWith(
+ 'Error revoking refresh token:',
+ expect.any(Error),
+ ),
+ );
+
+ consoleErrorMock.mockRestore();
+ });
+
+ test('should set session timeout based on fetched data', async () => {
+ jest.spyOn(global, 'setTimeout');
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ expect(global.setTimeout).toHaveBeenCalled();
+ });
+
+ test('should call errorHandler on query error', async () => {
+ const errorMocks = [
+ {
+ request: {
+ query: GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ },
+ error: new Error('An error occurred'),
+ },
+ ];
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ await waitFor(() => expect(errorHandler).toHaveBeenCalled());
+ });
+ //dfghjkjhgfds
+
+ test('should remove event listeners on endSession', async () => {
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ // Mock the removeEventListener functions for both window and document
+ const removeEventListenerMock = jest.fn();
+
+ // Temporarily replace the real methods with the mock
+ const originalWindowRemoveEventListener = window.removeEventListener;
+ const originalDocumentRemoveEventListener = document.removeEventListener;
+
+ window.removeEventListener = removeEventListenerMock;
+ document.removeEventListener = removeEventListenerMock;
+
+ // await waitForNextUpdate();
+
+ act(() => {
+ result.current.startSession();
+ });
+
+ act(() => {
+ result.current.endSession();
+ });
+
+ // Test that event listeners were removed
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'mousemove',
+ expect.any(Function),
+ );
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'keydown',
+ expect.any(Function),
+ );
+ expect(removeEventListenerMock).toHaveBeenCalledWith(
+ 'visibilitychange',
+ expect.any(Function),
+ );
+
+ // Restore the original removeEventListener functions
+ window.removeEventListener = originalWindowRemoveEventListener;
+ document.removeEventListener = originalDocumentRemoveEventListener;
+ });
+
+ test('should call initialize timers when session is still active when the user returns to the tab', async () => {
+ jest.useFakeTimers();
+ jest.spyOn(global, 'setTimeout').mockImplementation(jest.fn());
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ jest.advanceTimersByTime(1000);
+
+ // Set initial visibility state to visible
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ // Start the session
+ act(() => {
+ result.current.startSession();
+ jest.advanceTimersByTime(10 * 60 * 1000); // Fast-forward
+ });
+
+ // Simulate the user leaving the tab (set visibility to hidden)
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ // Fast-forward time by more than the session timeout
+ act(() => {
+ jest.advanceTimersByTime(5 * 60 * 1000); // Fast-forward
+ });
+
+ // Simulate the user returning to the tab
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ jest.advanceTimersByTime(1000);
+
+ expect(global.setTimeout).toHaveBeenCalled();
+
+ // Restore real timers
+ jest.useRealTimers();
+ });
+
+ test('should call handleLogout when session expires due to inactivity away from tab', async () => {
+ jest.useFakeTimers(); // Use fake timers to control time
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ jest.advanceTimersByTime(1000);
+
+ // Set initial visibility state to visible
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ // Start the session
+ act(() => {
+ result.current.startSession();
+ jest.advanceTimersByTime(10 * 60 * 1000); // Fast-forward
+ });
+
+ // Simulate the user leaving the tab (set visibility to hidden)
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'hidden',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ // Fast-forward time by more than the session timeout
+ act(() => {
+ jest.advanceTimersByTime(32 * 60 * 1000); // Fast-forward by 32 minutes
+ });
+
+ // Simulate the user returning to the tab
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ });
+
+ act(() => {
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ jest.advanceTimersByTime(250);
+
+ await waitFor(() => {
+ expect(global.localStorage.clear).toHaveBeenCalled();
+ expect(toast.warning).toHaveBeenCalledWith('sessionLogout', {
+ autoClose: false,
+ });
+ });
+
+ // Restore real timers
+ jest.useRealTimers();
+ });
+
+ test('should handle logout and revoke token', async () => {
+ jest.useFakeTimers();
+
+ const { result } = renderHook(() => useSession(), {
+ wrapper: ({ children }: { children?: ReactNode }) => (
+
+ {children}
+
+ ),
+ });
+
+ act(() => {
+ result.current.startSession();
+ result.current.handleLogout();
+ });
+
+ await waitFor(() => {
+ expect(global.localStorage.clear).toHaveBeenCalled();
+ expect(toast.warning).toHaveBeenCalledWith('sessionLogout', {
+ autoClose: false,
+ });
+ });
+
+ jest.useRealTimers();
+ });
+});
diff --git a/src/utils/useSession.tsx b/src/utils/useSession.tsx
new file mode 100644
index 0000000000..4279e7c850
--- /dev/null
+++ b/src/utils/useSession.tsx
@@ -0,0 +1,164 @@
+import { useMutation, useQuery } from '@apollo/client';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
+import { GET_COMMUNITY_SESSION_TIMEOUT_DATA } from 'GraphQl/Queries/Queries';
+import { t } from 'i18next';
+import { useEffect, useState, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router-dom';
+import { toast } from 'react-toastify';
+import { errorHandler } from 'utils/errorHandler';
+
+type UseSessionReturnType = {
+ startSession: () => void;
+ endSession: () => void;
+ handleLogout: () => void;
+ extendSession: () => void; //for when logged in already, simply extend session
+};
+
+/**
+ * Custom hook for managing user session timeouts in a React application.
+ *
+ * This hook handles:
+ * - Starting and ending the user session.
+ * - Displaying a warning toast at half of the session timeout duration.
+ * - Logging the user out and displaying a session expiration toast when the session times out.
+ * - Automatically resetting the timers when user activity is detected.
+ * - Pausing session timers when the tab is inactive and resuming them when it becomes active again.
+ *
+ * @returns UseSessionReturnType - An object with methods to start and end the session, and to handle logout.
+ */
+const useSession = (): UseSessionReturnType => {
+ const { t: tCommon } = useTranslation('common');
+
+ let startTime: number;
+ let timeoutDuration: number;
+ const [sessionTimeout, setSessionTimeout] = useState(30);
+ // const sessionTimeout = 30;
+ const sessionTimerRef = useRef(null);
+ const warningTimerRef = useRef(null);
+ const navigate = useNavigate();
+
+ const [revokeRefreshToken] = useMutation(REVOKE_REFRESH_TOKEN);
+ const { data, error: queryError } = useQuery(
+ GET_COMMUNITY_SESSION_TIMEOUT_DATA,
+ );
+
+ useEffect(() => {
+ if (queryError) {
+ errorHandler(t, queryError as Error);
+ } else {
+ const sessionTimeoutData = data?.getCommunityData;
+ if (sessionTimeoutData) {
+ setSessionTimeout(sessionTimeoutData.timeout);
+ }
+ }
+ }, [data, queryError]);
+
+ const resetTimers = (): void => {
+ if (sessionTimerRef.current) clearTimeout(sessionTimerRef.current);
+ if (warningTimerRef.current) clearTimeout(warningTimerRef.current);
+ };
+
+ const endSession = (): void => {
+ resetTimers();
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession);
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+
+ const handleLogout = async (): Promise => {
+ try {
+ await revokeRefreshToken();
+ } catch (error) {
+ console.error('Error revoking refresh token:', error);
+ // toast.error('Failed to revoke session. Please try again.');
+ }
+ localStorage.clear();
+ endSession();
+ navigate('/');
+ toast.warning(tCommon('sessionLogout'), { autoClose: false });
+ };
+
+ const initializeTimers = (
+ timeLeft?: number,
+ warningTimeLeft?: number,
+ ): void => {
+ const warningTime = warningTimeLeft ?? sessionTimeout / 2;
+ const sessionTimeoutInMilliseconds =
+ (timeLeft || sessionTimeout) * 60 * 1000;
+ const warningTimeInMilliseconds = warningTime * 60 * 1000;
+
+ timeoutDuration = sessionTimeoutInMilliseconds;
+ startTime = Date.now();
+
+ warningTimerRef.current = setTimeout(() => {
+ toast.warning(tCommon('sessionWarning'));
+ }, warningTimeInMilliseconds);
+
+ sessionTimerRef.current = setTimeout(async () => {
+ await handleLogout();
+ }, sessionTimeoutInMilliseconds);
+ };
+
+ const extendSession = (): void => {
+ resetTimers();
+ initializeTimers();
+ };
+
+ const startSession = (): void => {
+ resetTimers();
+ initializeTimers();
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession);
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ window.addEventListener('mousemove', extendSession);
+ window.addEventListener('keydown', extendSession);
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+ };
+
+ const handleVisibilityChange = async (): Promise => {
+ if (document.visibilityState === 'hidden') {
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession);
+ resetTimers(); // Optionally reset timers to prevent them from running in the background
+ } else if (document.visibilityState === 'visible') {
+ window.removeEventListener('mousemove', extendSession);
+ window.removeEventListener('keydown', extendSession); // Ensure no duplicates
+ window.addEventListener('mousemove', extendSession);
+ window.addEventListener('keydown', extendSession);
+
+ // Calculate remaining time now that the tab is active again
+ const elapsedTime = Date.now() - startTime;
+ const remainingTime = timeoutDuration - elapsedTime;
+
+ const remainingSessionTime = Math.max(remainingTime, 0); // Ensures the remaining time is non-negative and measured in ms;
+
+ if (remainingSessionTime > 0) {
+ // Calculate remaining warning time only if session time is positive
+ const remainingWarningTime = Math.max(remainingSessionTime / 2, 0);
+ initializeTimers(
+ remainingSessionTime / 60 / 1000,
+ remainingWarningTime / 60 / 1000,
+ );
+ } else {
+ // Handle session expiration immediately if time has run out
+ await handleLogout();
+ }
+ }
+ };
+
+ useEffect(() => {
+ return () => {
+ endSession();
+ };
+ }, []);
+
+ return {
+ startSession,
+ endSession,
+ handleLogout,
+ extendSession,
+ };
+};
+
+export default useSession;