Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`);
});
});