From bdd66d5b7f1bb6350518789f7ac00c1e41123e28 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Mon, 27 May 2024 17:33:54 -0400 Subject: [PATCH] #51 usertaskscard component --- src/pages/DashboardPage/DashboardPage.tsx | 5 +- .../UsersPage/components/UserTasksCard.tsx | 75 +++++++++++ .../__tests__/UserTasksCard.test.tsx | 125 ++++++++++++++++++ 3 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/pages/UsersPage/components/UserTasksCard.tsx create mode 100644 src/pages/UsersPage/components/__tests__/UserTasksCard.test.tsx diff --git a/src/pages/DashboardPage/DashboardPage.tsx b/src/pages/DashboardPage/DashboardPage.tsx index d39037b..38537c0 100644 --- a/src/pages/DashboardPage/DashboardPage.tsx +++ b/src/pages/DashboardPage/DashboardPage.tsx @@ -1,6 +1,7 @@ import { useGetCurrentUser } from 'api/useGetCurrentUser'; import LoaderSkeleton from 'components/Loader/LoaderSkeleton'; import Page from 'components/Page/Page'; +import UserTasksCard from 'pages/UsersPage/components/UserTasksCard'; /** * The `DashboardPage` component renders the content of the landing page @@ -13,7 +14,7 @@ const DashboardPage = (): JSX.Element => { return (
-
+
{user ? (

@@ -23,6 +24,8 @@ const DashboardPage = (): JSX.Element => { )}

+ + {user && }
diff --git a/src/pages/UsersPage/components/UserTasksCard.tsx b/src/pages/UsersPage/components/UserTasksCard.tsx new file mode 100644 index 0000000..e6eee0e --- /dev/null +++ b/src/pages/UsersPage/components/UserTasksCard.tsx @@ -0,0 +1,75 @@ +import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; +import filter from 'lodash/filter'; +import classNames from 'classnames'; + +import { useGetUserTasks } from '../api/useGetUserTasks'; +import Card, { CardProps } from 'components/Card/Card'; +import LoaderSkeleton from 'components/Loader/LoaderSkeleton'; + +/** + * Properties for the `UserTasksCard` React component. + * @param {number} userId - A User identifier. + * @see {@link CardProps} + */ +interface UserTasksCardProps extends CardProps { + userId: number; +} + +/** + * The `UserTasksCard` component renders card which displays summary information + * about a User's tasks. + * + * When clicked, navigates to the task details page for the User. + * @param {UserTasksCardProps} props - Component properties. + * @returns {JSX.Element} JSX + */ +const UserTasksCard = ({ + className, + userId, + testId = 'card-user-tasks', + ...props +}: UserTasksCardProps): JSX.Element => { + const navigate = useNavigate(); + const { data: tasks, error, isLoading } = useGetUserTasks({ userId }); + const incompleteTasks = filter(tasks, { completed: false }); + + const tasksMessage = useMemo(() => { + if (error) { + return 'A problem occurred fetching your tasks.'; + } + + if (incompleteTasks.length === 0) { + return 'You are all caught up!'; + } + + return `You have ${incompleteTasks.length} tasks to complete.`; + }, [error, incompleteTasks]); + + return ( +
navigate(`/app/users/${userId}/tasks`)} data-testid={testId}> + + {isLoading ? ( +
+ + +
+ ) : ( +
+
Tasks
+
{tasksMessage}
+
+ )} +
+
+ ); +}; + +export default UserTasksCard; diff --git a/src/pages/UsersPage/components/__tests__/UserTasksCard.test.tsx b/src/pages/UsersPage/components/__tests__/UserTasksCard.test.tsx new file mode 100644 index 0000000..d92e856 --- /dev/null +++ b/src/pages/UsersPage/components/__tests__/UserTasksCard.test.tsx @@ -0,0 +1,125 @@ +import { render, screen } from 'test/test-utils'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import * as UseGetUserTasks from '../../api/useGetUserTasks'; + +import UserTasksCard from '../UserTasksCard'; +import { todosFixture } from '__fixtures__/todos'; +import { UseQueryResult } from '@tanstack/react-query'; +import userEvent from '@testing-library/user-event'; + +// mock select functions from react-router-dom +const mockNavigate = vi.fn(); +vi.mock('react-router-dom', async () => { + const original = await vi.importActual('react-router-dom'); + return { + ...original, + useNavigate: () => mockNavigate, + }; +}); + +describe('UserTasksCard', () => { + const useGetUserTasksSpy = vi.spyOn(UseGetUserTasks, 'useGetUserTasks'); + + beforeEach(() => { + useGetUserTasksSpy.mockReturnValue({ + data: todosFixture, + error: null, + isLoading: false, + } as unknown as UseQueryResult); + }); + + it('should render successfully', async () => { + // ARRANGE + render(); + await screen.findByTestId('card-user-tasks'); + + // ASSERT + expect(screen.getByTestId('card-user-tasks')).toBeDefined(); + }); + + it('should use custom testId', async () => { + // ARRANGE + render(); + await screen.findByTestId('custom-testId'); + + // ASSERT + expect(screen.getByTestId('custom-testId')).toBeDefined(); + }); + + it('should use custom className', async () => { + // ARRANGE + render(); + await screen.findByTestId('card-user-tasks'); + + // ASSERT + expect(screen.getByTestId('card-user-tasks-card').classList).toContain('custom-className'); + }); + + it('should render loading state', async () => { + // ARRANGE + useGetUserTasksSpy.mockReturnValue({ + isLoading: true, + } as unknown as UseQueryResult); + + render(); + await screen.findByTestId('card-user-tasks-loader'); + + // ASSERT + expect(screen.getByTestId('card-user-tasks-loader')).toBeDefined(); + }); + + it('should render message when error occurs', async () => { + // ARRANGE + useGetUserTasksSpy.mockReturnValue({ + error: new Error('test'), + isLoading: false, + } as unknown as UseQueryResult); + render(); + await screen.findByTestId('card-user-tasks-message'); + + // ASSERT + expect(screen.getByTestId('card-user-tasks-message').textContent).toBe( + 'A problem occurred fetching your tasks.', + ); + }); + + it('should render message for zero incomplete tasks', async () => { + // ARRANGE + useGetUserTasksSpy.mockReturnValue({ + data: [], + error: null, + isLoading: false, + } as unknown as UseQueryResult); + render(); + await screen.findByTestId('card-user-tasks-message'); + + // ASSERT + expect(screen.getByTestId('card-user-tasks-message').textContent).toBe( + 'You are all caught up!', + ); + }); + + it('should render message for incomplete tasks', async () => { + // ARRANGE + render(); + await screen.findByTestId('card-user-tasks-message'); + + // ASSERT + expect(screen.getByTestId('card-user-tasks-message').textContent).toBe( + 'You have 3 tasks to complete.', + ); + }); + + it('should navigate when clicked', async () => { + // ARRANGE + render(); + await screen.findByTestId('card-user-tasks-message'); + + // ACT + await userEvent.click(screen.getByTestId('card-user-tasks')); + + // ASSERT + expect(mockNavigate).toHaveBeenCalledWith(`/app/users/1/tasks`); + }); +});