diff --git a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.test.tsx b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.test.tsx index 85cb73a415..589bde09f0 100644 --- a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.test.tsx +++ b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.test.tsx @@ -1,6 +1,7 @@ import { waitFor, screen, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { FormProvider, useForm } from 'react-hook-form'; +import { Link } from 'react-router-dom'; import { PreloadedState } from '@reduxjs/toolkit'; import { vi, type Mock } from 'vitest'; @@ -10,6 +11,7 @@ import { mockedAppletId, mockedCurrentWorkspace, mockedFullSubjectId1, + mockedFullSubjectId2, } from 'shared/mock'; import { Roles, JEST_TEST_TIMEOUT, MAX_LIMIT, ParticipantTag } from 'shared/consts'; import { initialStateData } from 'shared/state'; @@ -27,6 +29,7 @@ const activity2Id = '2'; const routePath = page.appletParticipantActivityDetailsDataReview; const route1 = `/dashboard/${mockedAppletId}/participants/${mockedFullSubjectId1}/activities/${activity1Id}/responses?selectedDate=2023-12-27`; const route2 = `/dashboard/${mockedAppletId}/participants/${mockedFullSubjectId1}/activities/${activity2Id}/responses?selectedDate=2023-12-15&answerId=answer-id-2-2&isFeedbackVisible=true`; +const route3 = `/dashboard/${mockedAppletId}/participants/${mockedFullSubjectId2}/activities/${activity1Id}/responses?selectedDate=2023-12-27`; // route1 with different participant const routeWithoutSelectedDate = `/dashboard/${mockedAppletId}/participants/${mockedFullSubjectId1}/activities/${activity1Id}/responses`; const preloadedState: PreloadedState = { workspaces: { @@ -784,6 +787,74 @@ describe('RespondentDataReview', () => { }); }); + test('renders component after navigation to different participant', async () => { + const getMock = authApiClient.get as unknown as Mock; + getMock.mockImplementation((url: string) => { + if (url.endsWith(`/answers/applet/${mockedAppletId}/dates`)) { + return Promise.resolve(mockedGetWithDates); + } + if (url.endsWith(`/answers/applet/${mockedAppletId}/review/flows`)) { + return Promise.resolve(mockedGetWithFlows1); + } + if (url.endsWith(`/answers/applet/${mockedAppletId}/review/activities`)) { + return Promise.resolve(mockedGetWithActivities2); + } + if (url.includes('/assessment')) { + return Promise.resolve(mockAssessment); + } + + return Promise.resolve(mockedGetWithResponses); + }); + + const getDecryptedActivityDataMock = vi.fn().mockReturnValue(mockDecryptedActivityData); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + dashboardHooks.useDecryptedActivityData.mockReturnValue(getDecryptedActivityDataMock); + + window.HTMLElement.prototype.scrollTo = () => {}; + + renderWithProviders( + <> + + Navigate + + + , + { + preloadedState, + route: route1, + routePath, + }, + ); + + await waitFor(() => { + expect(authApiClient.get).toHaveBeenCalledWith( + `/answers/applet/${mockedAppletId}/review/activities`, + expect.objectContaining({ + params: expect.objectContaining({ + createdDate: '2023-12-27', + targetSubjectId: mockedFullSubjectId1, + }), + }), + ); + }); + + await userEvent.click(screen.getByTestId('navigate-to-participant-2')); + + await waitFor(() => { + expect(authApiClient.get).toHaveBeenCalledWith( + `/answers/applet/${mockedAppletId}/review/activities`, + expect.objectContaining({ + params: expect.objectContaining({ + createdDate: '2023-12-27', + targetSubjectId: mockedFullSubjectId2, + }), + }), + ); + }); + }); + test('test if default review date is equal to last activity completed date', async () => { // This test verifies that when a route includes a selectedDate matching the user's // lastSeen/lastActivityCompleted date, the date picker component renders correctly diff --git a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.tsx b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.tsx index 19a8bb7372..d371f74791 100644 --- a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.tsx +++ b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/RespondentDataReview.tsx @@ -43,7 +43,7 @@ import { ReviewMenu } from './ReviewMenu'; import { useActivityAnswersAndAssessment } from './hooks/useActivityAnswersAndAssessment/useActivityAnswersAndAssessment'; import { useReviewActivitiesAndFlows } from './hooks/useReviewActivitiesAndFlows/useReviewActivitiesAndFlows'; -export const RespondentDataReview = () => { +const RespondentDataReviewInner = () => { const { appletId, subjectId, activityId, activityFlowId } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); const answerId = searchParams.get('answerId') || null; @@ -271,11 +271,15 @@ export const RespondentDataReview = () => { } const selectedDate = parseDateToMidnightLocal(selectedDateParam); - if ( - prevSelectedDateRef.current !== selectedDateParam && - responseDates.some((date) => date.getTime() === selectedDate.getTime()) - ) { - handleSetInitialDate(selectedDate); + if (prevSelectedDateRef.current !== selectedDateParam) { + // Set last submit date if selected date is unavailable + const hasSelectedDate = responseDates.some( + (date) => date.getTime() === selectedDate.getTime(), + ); + + handleSetInitialDate( + hasSelectedDate ? selectedDate : responseDates[responseDates.length - 1], + ); } }, [responseDates, selectedDateParam, handleSetInitialDate]); @@ -379,3 +383,11 @@ export const RespondentDataReview = () => { ); }; + +export const RespondentDataReview = () => { + const { subjectId, activityId, activityFlowId } = useParams(); + + // Switching participant or activity keeps the same route pattern, so remount to + // drop the previous selection instead of leaving stale dates, answers and responses + return ; +}; diff --git a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.test.tsx b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.test.tsx index 00266db6e8..f23a0a4f7a 100644 --- a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.test.tsx +++ b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.test.tsx @@ -108,6 +108,18 @@ describe('Review Menu Item component', () => { }); }); + test('renders and functions correctly when an answer ID is present in the route and an answer is already selected', async () => { + renderComponent(routeWithAnswerId, { isSelected: true, selectedAnswer: latestAnswer }); + + await waitFor(() => { + expect(mockedOnSelectAnswer).toHaveBeenCalledWith({ + answer: preselectedAnswer, + isRouteCreated: true, + }); + }); + expect(mockedOnSelectItem).toHaveBeenCalledWith(mockedActivity); + }); + test('renders with already selected activity', async () => { renderComponent(routeWithoutAnswerId, { isSelected: true }); diff --git a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.tsx b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.tsx index a27c59ab68..62b51f14df 100644 --- a/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.tsx +++ b/src/modules/Dashboard/features/RespondentData/RespondentDataReview/ReviewMenu/ReviewMenuItem/ReviewMenuItem.tsx @@ -47,13 +47,17 @@ export const ReviewMenuItem = ({ if (!answerId && !submitId) return; const answerByRoute = answerDates.find( - (answer) => answer.answerId === answerId || answer.submitId === submitId, + (answer) => + (answerId && answer.answerId === answerId) || (submitId && answer.submitId === submitId), ); if (!answerByRoute) return; setIsOpen(true); - if (selectedAnswer) return; + const isAnswerByRouteSelected = + (answerId && selectedAnswer?.answerId === answerId) || + (submitId && selectedAnswer?.submitId === submitId); + if (isAnswerByRouteSelected) return; onSelectAnswer({ answer: answerByRoute, isRouteCreated: true }); onSelectItem(item); diff --git a/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.test.tsx b/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.test.tsx index 146c96045c..72eb0a35a1 100644 --- a/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.test.tsx +++ b/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.test.tsx @@ -6,7 +6,6 @@ import { vi } from 'vitest'; import { mockedAlert, mockedFullSubjectId1 } from 'shared/mock'; import { renderWithProviders } from 'shared/utils/renderWithProviders'; -import * as useEncryptionStorageFunc from 'shared/hooks/useEncryptionStorage'; import { Notification } from './Notification'; @@ -82,12 +81,7 @@ describe('Notification', () => { fireEvent.click(screen.getByTestId(`notification-${mockedAlert.id}`)); expect(mockedSetCurrentId).toBeCalledWith(''); }); - test('should navigate when click on response data button without existed encryption', async () => { - const mockedgGetAppletPrivateKey = vi.fn().mockReturnValue(''); - vi.spyOn(useEncryptionStorageFunc, 'useEncryptionStorage').mockReturnValue({ - getAppletPrivateKey: mockedgGetAppletPrivateKey, - }); - + test('should navigate to the answer associated with the alert when click on response data button', async () => { renderWithProviders( { expect(button).toBeInTheDocument(); await userEvent.click(button); - expect(mockedUseNavigate).toBeCalledWith( - `/dashboard/${mockedAlert.appletId}/participants/${mockedFullSubjectId1}/activities/${mockedAlert.activityId}/responses`, - ); - }); - - test('should navigate when click on response data button with existed encryption', async () => { - const mockedgGetAppletPrivateKey = vi.fn().mockReturnValue('123'); - vi.spyOn(useEncryptionStorageFunc, 'useEncryptionStorage').mockReturnValue({ - getAppletPrivateKey: mockedgGetAppletPrivateKey, + expect(mockedUseNavigate).toBeCalledWith({ + pathname: `/dashboard/${mockedAlert.appletId}/participants/${mockedFullSubjectId1}/activities/${mockedAlert.activityId}/responses`, + search: `selectedDate=2023-08-03&answerId=${mockedAlert.answerId}`, }); - - renderWithProviders( - , - ); - - const button = screen.getByRole('button', { - name: /takeMeToTheResponseData/i, - }); - expect(button).toBeInTheDocument(); - await userEvent.click(button); - - expect(mockedUseNavigate).toBeCalledWith( - `/dashboard/${mockedAlert.appletId}/participants/${mockedFullSubjectId1}/activities/${mockedAlert.activityId}/responses`, - ); }); }); diff --git a/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.tsx b/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.tsx index b65d48aa79..6143d4e763 100644 --- a/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.tsx +++ b/src/shared/layouts/BaseLayout/components/TopBar/Notifications/Notification/Notification.tsx @@ -1,6 +1,6 @@ import { Box } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { generatePath, useNavigate } from 'react-router-dom'; +import { createSearchParams, generatePath, useNavigate } from 'react-router-dom'; import { useAppDispatch } from 'redux/store'; import { page } from 'resources'; @@ -44,6 +44,7 @@ export const Notification = ({ type, activityId, answerId, + createdAt, }: NotificationProps) => { const { t } = useTranslation('app'); const dispatch = useAppDispatch(); @@ -74,14 +75,17 @@ export const Notification = ({ }; const navigateToResponseData = () => { - navigate( - generatePath(page.appletParticipantActivityDetailsDataReview, { + navigate({ + pathname: generatePath(page.appletParticipantActivityDetailsDataReview, { appletId, subjectId, activityId, - answerId, }), - ); + search: createSearchParams({ + selectedDate: createdAt.substring(0, 10), // ISO 8601 date in UTC + ...(answerId && { answerId }), + }).toString(), + }); }; return (