Skip to content

Commit

Permalink
#51 usertaskscard component
Browse files Browse the repository at this point in the history
  • Loading branch information
mwarman committed May 27, 2024
1 parent 188a770 commit bdd66d5
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 1 deletion.
5 changes: 4 additions & 1 deletion src/pages/DashboardPage/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,7 +14,7 @@ const DashboardPage = (): JSX.Element => {
return (
<Page testId="page-dashboard">
<div className="container mx-auto min-h-[50vh]">
<div className="my-4 grid grid-cols-1 gap-8 md:grid-cols-2">
<div className="my-4 grid grid-cols-1 gap-y-8 md:grid-cols-2 md:gap-8">
<div className="col-span-2">
{user ? (
<h1 className="text-xl">
Expand All @@ -23,6 +24,8 @@ const DashboardPage = (): JSX.Element => {
<LoaderSkeleton className="h-7" testId="page-dashboard-loader" />
)}
</div>

{user && <UserTasksCard userId={user.id} />}
</div>
</div>
</Page>
Expand Down
75 changes: 75 additions & 0 deletions src/pages/UsersPage/components/UserTasksCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div onClick={() => navigate(`/app/users/${userId}/tasks`)} data-testid={testId}>
<Card
className={classNames(
'transition ease-in-out hover:-translate-y-1 hover:scale-105 hover:cursor-pointer hover:bg-blue-600/10',
className,
)}
testId={`${testId}-card`}
{...props}
>
{isLoading ? (
<div data-testid={`${testId}-loader`}>
<LoaderSkeleton className="mb-2 h-7 w-20" />
<LoaderSkeleton className="h-4" />
</div>
) : (
<div>
<div className="text-xl font-bold">Tasks</div>
<div data-testid={`${testId}-message`}>{tasksMessage}</div>
</div>
)}
</Card>
</div>
);
};

export default UserTasksCard;
125 changes: 125 additions & 0 deletions src/pages/UsersPage/components/__tests__/UserTasksCard.test.tsx
Original file line number Diff line number Diff line change
@@ -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<UseGetUserTasks.Task[]>);
});

it('should render successfully', async () => {
// ARRANGE
render(<UserTasksCard userId={1} />);
await screen.findByTestId('card-user-tasks');

// ASSERT
expect(screen.getByTestId('card-user-tasks')).toBeDefined();
});

it('should use custom testId', async () => {
// ARRANGE
render(<UserTasksCard userId={1} testId="custom-testId" />);
await screen.findByTestId('custom-testId');

// ASSERT
expect(screen.getByTestId('custom-testId')).toBeDefined();
});

it('should use custom className', async () => {
// ARRANGE
render(<UserTasksCard userId={1} className="custom-className" />);
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<UseGetUserTasks.Task[]>);

render(<UserTasksCard userId={1} />);
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<UseGetUserTasks.Task[]>);
render(<UserTasksCard userId={1} />);
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<UseGetUserTasks.Task[]>);
render(<UserTasksCard userId={1} />);
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(<UserTasksCard userId={1} />);
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(<UserTasksCard userId={1} />);
await screen.findByTestId('card-user-tasks-message');

// ACT
await userEvent.click(screen.getByTestId('card-user-tasks'));

// ASSERT
expect(mockNavigate).toHaveBeenCalledWith(`/app/users/1/tasks`);
});
});

0 comments on commit bdd66d5

Please sign in to comment.