diff --git a/src/utils/useSession.spec.tsx b/src/utils/useSession.spec.tsx new file mode 100644 index 0000000000..9b50039ba2 --- /dev/null +++ b/src/utils/useSession.spec.tsx @@ -0,0 +1,688 @@ +import type { ReactNode } from 'react'; +import React from 'react'; +import { renderHook } from '@testing-library/react'; +import { MockedProvider } from '@apollo/client/testing'; +import { toast } from 'react-toastify'; +import { describe, beforeEach, afterEach, test, expect, vi } from 'vitest'; +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'; + +vi.mock('react-toastify', () => ({ + toast: { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock('utils/errorHandler', () => ({ + errorHandler: vi.fn(), +})); + +vi.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, + }, + }, + }, +]; +describe('useSession Hook', () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(window, 'addEventListener').mockImplementation(vi.fn()); + vi.spyOn(window, 'removeEventListener').mockImplementation(vi.fn()); + Object.defineProperty(global, 'localStorage', { + value: { + clear: vi.fn(), + }, + writable: true, + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + test('should handle visibility change to visible', async () => { + vi.useFakeTimers(); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true, + }); + + result.current.startSession(); + + document.dispatchEvent(new Event('visibilitychange')); + + vi.advanceTimersByTime(15 * 60 * 1000); + + await vi.waitFor(() => { + expect(window.addEventListener).toHaveBeenCalledWith( + 'mousemove', + expect.any(Function), + ); + expect(window.addEventListener).toHaveBeenCalledWith( + 'keydown', + expect.any(Function), + ); + expect(toast.warning).toHaveBeenCalledWith('sessionWarning'); + }); + + vi.useRealTimers(); + }); + + test('should handle visibility change to hidden and ensure no warning appears in 15 minutes', async () => { + vi.useFakeTimers(); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + Object.defineProperty(document, 'visibilityState', { + value: 'hidden', + writable: true, + }); + + result.current.startSession(); + + document.dispatchEvent(new Event('visibilitychange')); + + vi.advanceTimersByTime(15 * 60 * 1000); + + await vi.waitFor(() => { + expect(window.removeEventListener).toHaveBeenCalledWith( + 'mousemove', + expect.any(Function), + ); + expect(window.removeEventListener).toHaveBeenCalledWith( + 'keydown', + expect.any(Function), + ); + expect(toast.warning).not.toHaveBeenCalled(); + }); + + vi.useRealTimers(); + }); + + test('should register event listeners on startSession', async () => { + const addEventListenerSpy = vi.fn(); + const windowAddEventListenerSpy = vi + .spyOn(window, 'addEventListener') + .mockImplementation(addEventListenerSpy); + const documentAddEventListenerSpy = vi + .spyOn(document, 'addEventListener') + .mockImplementation(addEventListenerSpy); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + + await vi.waitFor(() => { + const calls = addEventListenerSpy.mock.calls; + expect(calls.length).toBe(4); + + const eventTypes = calls.map((call) => call[0]); + expect(eventTypes).toContain('mousemove'); + expect(eventTypes).toContain('keydown'); + expect(eventTypes).toContain('visibilitychange'); + + calls.forEach((call) => { + expect(call[1]).toBeTypeOf('function'); + }); + }); + + windowAddEventListenerSpy.mockRestore(); + documentAddEventListenerSpy.mockRestore(); + }); + + test('should call handleLogout after session timeout', async () => { + vi.useFakeTimers(); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + + vi.advanceTimersByTime(31 * 60 * 1000); + + await vi.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 () => { + vi.useFakeTimers(); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + + vi.advanceTimersByTime(15 * 60 * 1000); + + await vi.waitFor(() => + expect(toast.warning).toHaveBeenCalledWith('sessionWarning'), + ); + + vi.useRealTimers(); + }); + + test('should handle error when revoking token fails', async () => { + const consoleErrorMock = vi + .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} + + ), + }); + + result.current.startSession(); + result.current.handleLogout(); + + await vi.waitFor(() => + expect(consoleErrorMock).toHaveBeenCalledWith( + 'Error revoking refresh token:', + expect.any(Error), + ), + ); + + consoleErrorMock.mockRestore(); + }); + + test('should set session timeout based on fetched data', async () => { + vi.spyOn(global, 'setTimeout'); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + 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} + + ), + }); + + result.current.startSession(); + + await vi.waitFor(() => expect(errorHandler).toHaveBeenCalled()); + }); + + test('should remove event listeners on endSession', async () => { + const removeEventListenerSpy = vi.fn(); + const windowRemoveEventListenerSpy = vi + .spyOn(window, 'removeEventListener') + .mockImplementation(removeEventListenerSpy); + const documentRemoveEventListenerSpy = vi + .spyOn(document, 'removeEventListener') + .mockImplementation(removeEventListenerSpy); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + result.current.endSession(); + + await vi.waitFor(() => { + const calls = removeEventListenerSpy.mock.calls; + expect(calls.length).toBe(6); + + const eventTypes = calls.map((call) => call[0]); + expect(eventTypes).toContain('mousemove'); + expect(eventTypes).toContain('keydown'); + expect(eventTypes).toContain('visibilitychange'); + + calls.forEach((call) => { + expect(call[1]).toBeTypeOf('function'); + }); + }); + + windowRemoveEventListenerSpy.mockRestore(); + documentRemoveEventListenerSpy.mockRestore(); + }); + + test('should call initialize timers when session is still active when the user returns to the tab', async () => { + vi.useFakeTimers(); + vi.spyOn(global, 'setTimeout').mockImplementation(vi.fn()); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + vi.advanceTimersByTime(1000); + + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true, + }); + + result.current.startSession(); + vi.advanceTimersByTime(10 * 60 * 1000); + + Object.defineProperty(document, 'visibilityState', { + value: 'hidden', + writable: true, + }); + + document.dispatchEvent(new Event('visibilitychange')); + + vi.advanceTimersByTime(5 * 60 * 1000); + + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true, + }); + + document.dispatchEvent(new Event('visibilitychange')); + + vi.advanceTimersByTime(1000); + + expect(global.setTimeout).toHaveBeenCalled(); + + vi.useRealTimers(); + }); + + test('should call handleLogout when session expires due to inactivity away from tab', async () => { + vi.useFakeTimers(); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + vi.advanceTimersByTime(1000); + + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true, + }); + + result.current.startSession(); + vi.advanceTimersByTime(10 * 60 * 1000); + + Object.defineProperty(document, 'visibilityState', { + value: 'hidden', + writable: true, + }); + + document.dispatchEvent(new Event('visibilitychange')); + + vi.advanceTimersByTime(32 * 60 * 1000); + + Object.defineProperty(document, 'visibilityState', { + value: 'visible', + writable: true, + }); + + document.dispatchEvent(new Event('visibilitychange')); + + vi.advanceTimersByTime(250); + + await vi.waitFor(() => { + expect(global.localStorage.clear).toHaveBeenCalled(); + expect(toast.warning).toHaveBeenCalledWith('sessionLogout', { + autoClose: false, + }); + }); + + vi.useRealTimers(); + }); + + test('should handle logout and revoke token', async () => { + vi.useFakeTimers(); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + result.current.handleLogout(); + + await vi.waitFor(() => { + expect(global.localStorage.clear).toHaveBeenCalled(); + expect(toast.warning).toHaveBeenCalledWith('sessionLogout', { + autoClose: false, + }); + }); + + vi.useRealTimers(); + }); +}); +test('should extend session when called directly', async () => { + vi.useFakeTimers(); + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + + // Advance time to just before warning + vi.advanceTimersByTime(14 * 60 * 1000); + + // Extend session + result.current.extendSession(); + + // Advance time to where warning would have been + vi.advanceTimersByTime(1 * 60 * 1000); + + // Warning shouldn't have been called yet since we extended + expect(toast.warning).not.toHaveBeenCalled(); + + // Advance to new warning time + vi.advanceTimersByTime(14 * 60 * 1000); + + await vi.waitFor(() => + expect(toast.warning).toHaveBeenCalledWith('sessionWarning'), + ); + + vi.useRealTimers(); +}); + +test('should properly clean up on unmount', () => { + // Mock document.removeEventListener + const documentRemoveEventListener = vi.spyOn(document, 'removeEventListener'); + + const { result, unmount } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + unmount(); + + expect(window.removeEventListener).toHaveBeenCalledWith( + 'mousemove', + expect.any(Function), + ); + expect(window.removeEventListener).toHaveBeenCalledWith( + 'keydown', + expect.any(Function), + ); + expect(documentRemoveEventListener).toHaveBeenCalledWith( + 'visibilitychange', + expect.any(Function), + ); + + documentRemoveEventListener.mockRestore(); +}); +test('should handle missing community data', async () => { + vi.useFakeTimers(); + const setTimeoutSpy = vi.spyOn(global, 'setTimeout'); + + const nullDataMocks = [ + { + request: { + query: GET_COMMUNITY_SESSION_TIMEOUT_DATA, + }, + result: { + data: { + getCommunityData: null, + }, + }, + }, + ]; + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + + // Wait for timers to be set + await vi.waitFor(() => { + expect(setTimeoutSpy).toHaveBeenCalled(); + }); + + // Get all setTimeout calls + const timeoutCalls = setTimeoutSpy.mock.calls; + + // Check for warning timeout (15 minutes = 900000ms) + const hasWarningTimeout = timeoutCalls.some( + (call: Parameters) => { + const [, ms] = call; + return typeof ms === 'number' && ms === (30 * 60 * 1000) / 2; + }, + ); + + // Check for session timeout (30 minutes = 1800000ms) + const hasSessionTimeout = timeoutCalls.some( + (call: Parameters) => { + const [, ms] = call; + return typeof ms === 'number' && ms === 30 * 60 * 1000; + }, + ); + + expect(hasWarningTimeout).toBe(true); + expect(hasSessionTimeout).toBe(true); + + setTimeoutSpy.mockRestore(); + vi.useRealTimers(); +}); + +test('should handle event listener errors gracefully', async () => { + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + const mockError = new Error('Event listener error'); + + // Mock addEventListener to throw an error + const addEventListenerSpy = vi + .spyOn(window, 'addEventListener') + .mockImplementationOnce(() => { + throw mockError; + }); + + try { + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + } catch { + // Error should be caught and logged + expect(consoleErrorSpy).toHaveBeenCalled(); + } + + consoleErrorSpy.mockRestore(); + addEventListenerSpy.mockRestore(); +}); + +test('should handle session timeout data updates', async () => { + vi.useFakeTimers(); + const setTimeoutSpy = vi.spyOn(global, 'setTimeout'); + + const customMocks = [ + { + request: { + query: GET_COMMUNITY_SESSION_TIMEOUT_DATA, + }, + result: { + data: { + getCommunityData: { + timeout: 45, + }, + }, + }, + }, + ]; + + const { result } = renderHook(() => useSession(), { + wrapper: ({ children }: { children?: ReactNode }) => ( + + {children} + + ), + }); + + result.current.startSession(); + + // Wait for the query and timers + await vi.waitFor(() => { + expect(setTimeoutSpy).toHaveBeenCalled(); + }); + + const timeoutCalls = setTimeoutSpy.mock.calls; + const expectedWarningTime = (45 * 60 * 1000) / 2; + const expectedSessionTime = 45 * 60 * 1000; + + const hasWarningTimeout = timeoutCalls.some((call) => { + const duration = call[1] as number; + return ( + Math.abs(duration - expectedWarningTime) <= expectedWarningTime * 0.05 + ); // ±5% + }); + + const hasSessionTimeout = timeoutCalls.some((call) => { + const duration = call[1] as number; + return ( + Math.abs(duration - expectedSessionTime) <= expectedSessionTime * 0.05 + ); // ±5% + }); + + expect(hasWarningTimeout).toBe(false); + expect(hasSessionTimeout).toBe(false); + + setTimeoutSpy.mockRestore(); + vi.useRealTimers(); +}); diff --git a/src/utils/useSession.test.tsx b/src/utils/useSession.test.tsx deleted file mode 100644 index 32287ccbb0..0000000000 --- a/src/utils/useSession.test.tsx +++ /dev/null @@ -1,544 +0,0 @@ -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(); - }); -});