diff --git a/README.md b/README.md index e0c1d71..2656db3 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,21 @@ A serverless, progressive, responsive starter user interface (UI) with React at ## Helpful Hints +### Viewing the Starter Kit + +If you do not wish to check out the repository and run the starter kit locally, you may view the latest release of the React Starter Kit at [https://react-starter.leanstacks.net/][app]. Please see the _Authentication_ section below for instructions to sign in. + +> **NOTE:** This app does not collect any data and does not use cookies. + ### Data This project's API integration uses the simulated REST endpoints made available by [JSON Placeholder](https://jsonplaceholder.typicode.com/). -There are some limitations to the JSONP Placeholder APIs. The primary limitation is that the API is stateless. You may create (`post`), update (`put`), and delete items within the JSON Placeholder collections; however, the collections are not actually mutated and persisted by JSON Placeholder. Within the starter kit, we update the TanStack Query caches upon successful mutation to simulate a stateful back end. +There are some limitations to the JSON Placeholder APIs. The primary limitation is that the API is stateless. You may create (`post`), update (`put`), and delete items within the JSON Placeholder collections; however, the collections are not actually mutated and persisted by JSON Placeholder. Within the starter kit, we update the TanStack Query caches upon successful mutation to simulate a stateful back end. ### Authentication -When running the application, you may sign in with any of the JSON Placeholder [Users](https://jsonplaceholder.typicode.com/users). Simply enter the _Username_ value from any user in the API and use any value for the _Password_. For example, try username `Bret` and password `abc123`. +When using the application, you may sign in with any of the JSON Placeholder [Users](https://jsonplaceholder.typicode.com/users). Simply enter the _Username_ value from any user in the API and use any value for the _Password_. For example, try username `Kamren` or `Samantha` and password `abc123`. ## About @@ -273,6 +279,7 @@ This project uses GitHub Actions to perform DevOps automation activities such as - [Storybook][storybook] - [GitHub Actions][ghactions] +[app]: https://react-starter.leanstacks.net/ 'React Starter Kit | LeanStacks' [repo]: https://github.com/leanstacks/react-starter-kit 'GitHub Repository' [nvm]: https://github.com/nvm-sh/nvm 'Node Version Manager' [react]: https://react.dev 'React' diff --git a/src/common/components/Breadcrumbs/Breadcrumbs.tsx b/src/common/components/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 0000000..812369d --- /dev/null +++ b/src/common/components/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,153 @@ +import { PropsWithChildren } from 'react'; + +import { BaseComponentProps } from 'common/utils/types'; +import { cn } from 'common/utils/css'; +import { default as CommonLink, LinkProps as CommonLinkProps } from 'common/components/Link/Link'; +import FAIcon, { FAIconProps } from '../Icon/FAIcon'; + +/** + * Properties for the `Breadcrumbs` component. + */ +export interface BreadcrumbsProps extends BaseComponentProps, PropsWithChildren {} + +/** + * The `Breadcrumbs` component renders a heirarchy of links as a path to the + * current route. + */ +const Breadcrumbs = ({ + children, + className, + testId = 'breadcrumbs', +}: BreadcrumbsProps): JSX.Element => { + return ( + + ); +}; + +/** + * The `List` contains the list of items in the breadcrumbs. It is located immediately + * within the `Breadcrumbs` component and contains one to many `Item` components. + */ +const List = ({ + children, + className, + testId = 'breadcrumbs-list', +}: BaseComponentProps & PropsWithChildren): JSX.Element => { + return ( +
    + {children} +
+ ); +}; +Breadcrumbs.List = List; + +/** + * The `Item` represents a single element of the breadcrumbs list. An item typically + * represents a single element of the path to which a user may navigate. + * + * An `Item` may contain any type of child; however, an item typically contains a `Link`, + * `Page`, or a `DropdownMenu`. + */ +const Item = ({ + children, + className, + testId = 'breadcrumbs-item', +}: BaseComponentProps & PropsWithChildren): JSX.Element => { + return ( +
  • + {children} +
  • + ); +}; +Breadcrumbs.Item = Item; + +/** + * A `Link` represents a navigation link item within breadcrumbs. A link is + * a child of an `Item`. Use the `to` property to specify the relative path for + * the link. + */ +const Link = ({ + children, + className, + testId = 'breadcrumbs-link', + ...props +}: CommonLinkProps): JSX.Element => { + return ( + + {children} + + ); +}; +Breadcrumbs.Link = Link; + +/** + * A `Page` represents an item within breadcrumbs with no associated navigation. + * Use the `Page` as an alternative to the `Link` when the breadcrumb item is + * not clickable. A `Page` is commonly used for the current page, i.e. the last + * element in the breadcrumbs. + */ +const Page = ({ + children, + className, + testId = 'breadcrumbs-page', +}: BaseComponentProps & PropsWithChildren): JSX.Element => { + return ( + + {children} + + ); +}; +Breadcrumbs.Page = Page; + +/** + * The `Separator` component renders an icon which visually separates two + * breadcrumbs elements. The default icon is a right chevron. + */ +const Separator = ({ + className, + icon = 'chevronRight', + size = 'sm', + testId = 'breadcrumbs-separator', + ...iconProps +}: BaseComponentProps & + Omit & + Partial>): JSX.Element => { + return ( +
  • + +
  • + ); +}; +Breadcrumbs.Separator = Separator; + +/** + * The `Ellipsis` component renders a horizontal ellipsis icon. This is useful when + * the number of breadcrumbs elements is too great to render all of them. Use the + * ellipsis to represent breadcrumbs items which have been omitted to shorten the + * overall breadcrumbs list. Alternatively, use the ellipsis as the trigger for + * a `DropdownMenu` within the breadcrumbs to allow navigation to those routes + * which have been omitted for brevity. + */ +const Ellipsis = ({ + className, + testId = 'breadcrumbs-ellipsis', +}: BaseComponentProps): JSX.Element => { + return ( + + + More + + ); +}; +Breadcrumbs.Ellipsis = Ellipsis; + +export default Breadcrumbs; diff --git a/src/common/components/Breadcrumbs/__stories__/Breadcrumbs.stories.tsx b/src/common/components/Breadcrumbs/__stories__/Breadcrumbs.stories.tsx new file mode 100644 index 0000000..209c822 --- /dev/null +++ b/src/common/components/Breadcrumbs/__stories__/Breadcrumbs.stories.tsx @@ -0,0 +1,97 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { MemoryRouter } from 'react-router-dom'; + +import Breadcrumbs from '../Breadcrumbs'; +import DropdownMenu from 'common/components/Dropdown/DropdownMenu'; + +const meta = { + title: 'Common/Breadcrumbs', + component: Breadcrumbs, + parameters: { + layout: 'centered', + }, + decorators: [ + (Story) => ( + + + + ), + ], + tags: ['autodocs'], + argTypes: { + children: { description: 'The content.' }, + className: { description: 'Additional CSS classes.' }, + testId: { description: 'The test identifier.' }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + render: (args) => ( + + + + Home + + + + Tasks + + + + + dolorum laboriosam eos qui iure aliquam + + + + + Edit + + + + ), + args: { + className: 'w-100', + }, +}; + +export const WithDropdownMenu: Story = { + render: (args) => ( + + + + Home + + + + + + + + + Components + Settings + Tasks + + + + + + + dolorum laboriosam eos qui iure aliquam + + + + + Edit + + + + ), + args: { + className: 'w-100 h-40', + }, +}; diff --git a/src/common/components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx b/src/common/components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx new file mode 100644 index 0000000..bcc0017 --- /dev/null +++ b/src/common/components/Breadcrumbs/__tests__/Breadcrumbs.test.tsx @@ -0,0 +1,62 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen } from 'test/test-utils'; + +import Breadcrumbs from '../Breadcrumbs'; + +describe('Breadcrumbs', () => { + it('should render successfully', async () => { + // ARRANGE + render( + + + + Home + + + + + + + + Page + + + , + ); + await screen.findByTestId('breadcrumbs'); + + // ASSERT + expect(screen.getByTestId('breadcrumbs')).toBeDefined(); + }); + + it('should render inner components successfully', async () => { + // ARRANGE + render( + + + + Home + + + + + + + + Page + + + , + ); + await screen.findByTestId('breadcrumbs'); + + // ASSERT + expect(screen.getByTestId('breadcrumbs')).toBeDefined(); + expect(screen.getByTestId('breadcrumbs-list')).toBeDefined(); + expect(screen.getAllByTestId('breadcrumbs-item')).toHaveLength(3); + expect(screen.getAllByTestId('breadcrumbs-separator')).toHaveLength(2); + expect(screen.getByTestId('breadcrumbs-ellipsis')).toBeDefined(); + expect(screen.getByTestId('breadcrumbs-page')).toBeDefined(); + }); +}); diff --git a/src/common/components/Icon/FAIcon.tsx b/src/common/components/Icon/FAIcon.tsx index 7a45228..d0fb176 100644 --- a/src/common/components/Icon/FAIcon.tsx +++ b/src/common/components/Icon/FAIcon.tsx @@ -7,12 +7,15 @@ import { faBuilding, faCheck, faChevronDown, + faChevronRight, faChevronUp, faCircleCheck, faCircleExclamation, faCircleInfo, faCircleNotch, faCircleXmark, + faEllipsis, + faEllipsisVertical, faEnvelope, faLanguage, faLink, @@ -48,6 +51,7 @@ export type FAIconName = | 'building' | 'check' | 'chevronDown' + | 'chevronRight' | 'chevronUp' | 'circleCheck' | 'circleExclamation' @@ -55,6 +59,8 @@ export type FAIconName = | 'circleNotch' | 'circleRegular' | 'circleXmark' + | 'ellipsis' + | 'ellipsisVertical' | 'envelope' | 'language' | 'link' @@ -97,6 +103,7 @@ const icons: Record = { building: faBuilding, check: faCheck, chevronDown: faChevronDown, + chevronRight: faChevronRight, chevronUp: faChevronUp, circleCheck: faCircleCheck, circleExclamation: faCircleExclamation, @@ -104,6 +111,8 @@ const icons: Record = { circleNotch: faCircleNotch, circleRegular: faCircleRegular, circleXmark: faCircleXmark, + ellipsis: faEllipsis, + ellipsisVertical: faEllipsisVertical, envelope: faEnvelope, language: faLanguage, link: faLink, diff --git a/src/common/components/Router/Router.tsx b/src/common/components/Router/Router.tsx index c9abb73..88c4d94 100644 --- a/src/common/components/Router/Router.tsx +++ b/src/common/components/Router/Router.tsx @@ -18,6 +18,7 @@ import ComponentsPage from 'pages/Components/ComponentsPage'; import AlertComponents from 'pages/Components/components/AlertComponents'; import AvatarComponents from 'pages/Components/components/AvatarComponents'; import BadgeComponents from 'pages/Components/components/BadgeComponents'; +import BreadcrumbsComponents from 'pages/Components/components/BreadcrumbsComponents'; import ButtonComponents from 'pages/Components/components/ButtonComponents'; import CardComponents from 'pages/Components/components/CardComponents'; import DialogComponents from 'pages/Components/components/DialogComponents'; @@ -100,6 +101,10 @@ export const routes: RouteObject[] = [ path: 'badge', element: , }, + { + path: 'breadcrumbs', + element: , + }, { path: 'button', element: , @@ -117,7 +122,7 @@ export const routes: RouteObject[] = [ element: , }, { - path: 'searchinput', + path: 'search-input', element: , }, { diff --git a/src/pages/Components/ComponentsPage.tsx b/src/pages/Components/ComponentsPage.tsx index dfbe49b..0c09c96 100644 --- a/src/pages/Components/ComponentsPage.tsx +++ b/src/pages/Components/ComponentsPage.tsx @@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom'; import Page from 'common/components/Page/Page'; import MenuNavLink from 'common/components/Menu/MenuNavLink'; import Heading from 'common/components/Text/Heading'; +import ComponentsPageBreadcrumbs from './components/ComponentsPageBreadcrumbs'; /** * The `ComponentsPage` component renders the layout for the components page. @@ -14,6 +15,8 @@ const ComponentsPage = (): JSX.Element => { return (
    + + Components @@ -29,6 +32,9 @@ const ComponentsPage = (): JSX.Element => { Badge + + Breadcrumbs + Button @@ -41,7 +47,7 @@ const ComponentsPage = (): JSX.Element => { Dropdown - + Search Input diff --git a/src/pages/Components/components/BreadcrumbsComponents.tsx b/src/pages/Components/components/BreadcrumbsComponents.tsx new file mode 100644 index 0000000..39a1364 --- /dev/null +++ b/src/pages/Components/components/BreadcrumbsComponents.tsx @@ -0,0 +1,150 @@ +import { createColumnHelper } from '@tanstack/react-table'; + +import { BaseComponentProps } from 'common/utils/types'; +import { ComponentProperty } from '../model/components'; +import Table from 'common/components/Table/Table'; +import CodeSnippet from 'common/components/Text/CodeSnippet'; +import Heading from 'common/components/Text/Heading'; +import Breadcrumbs from 'common/components/Breadcrumbs/Breadcrumbs'; +import DropdownMenu from 'common/components/Dropdown/DropdownMenu'; + +/** + * The `BreadcrumbsComponents` React component renders a set of examples illustrating + * the use of the `Breadcrumbs` family of components. + */ +const BreadcrumbsComponents = ({ + className, + testId = 'components-breadcrumbs', +}: BaseComponentProps): JSX.Element => { + const columnHelper = createColumnHelper(); + const breadcrumbsData: ComponentProperty[] = [ + { + name: 'children', + description: 'The content to be displayed.', + }, + { + name: 'className', + description: 'Optional. Additional CSS class names.', + }, + { + name: 'testId', + description: 'Optional. Identifier for testing.', + }, + ]; + const columns = [ + columnHelper.accessor('name', { + cell: (info) => ( + {info.getValue()} + ), + header: () => 'Name', + }), + columnHelper.accessor('description', { + cell: (info) => info.renderValue(), + header: () => 'Description', + }), + ]; + + return ( +
    +
    + + Breadcrumbs Component + + +
    + The Breadcrumbs component displays a + heirarchy of links as a path to the current route. +
    + +
    + + Properties + + data={breadcrumbsData} columns={columns} /> +
    + + + Examples + + + + Breadcrumbs components + +
    + The Breadcrumbs component is a compound component. It has component properties which allow + you to compose Breadcrumbs content. Those components include: List, Item, Link, Page, + Ellipsis, and Separator. +
    +
    +
    + + + + Home + + + + + + + + + Components + Settings + Tasks + + + + + + + dolorum laboriosam eos qui iure aliquam + + + + + Edit + + + +
    + + + + Home + + + + + + + + + Components + Settings + Tasks + + + + + + + dolorum laboriosam eos qui iure aliquam + + + + + Edit + + +`} + /> +
    +
    +
    + ); +}; + +export default BreadcrumbsComponents; diff --git a/src/pages/Components/components/ComponentsPageBreadcrumbs.tsx b/src/pages/Components/components/ComponentsPageBreadcrumbs.tsx new file mode 100644 index 0000000..818cd90 --- /dev/null +++ b/src/pages/Components/components/ComponentsPageBreadcrumbs.tsx @@ -0,0 +1,42 @@ +import { useLocation } from 'react-router-dom'; + +import { BaseComponentProps } from 'common/utils/types'; +import Breadcrumbs from 'common/components/Breadcrumbs/Breadcrumbs'; + +/** + * The `ComponentsPageBreadcrumbs` component renders the `Breadcrumbs` for the components + * family of pages. + */ +const ComponentsPageBreadcrumbs = ({ + className, + testId = 'page-components-breadcrumbs', +}: BaseComponentProps): JSX.Element => { + const location = useLocation(); + const pathElements = location.pathname.split('/'); + + return ( + + + + Home + + + + Components + + {!!pathElements[3] && ( + <> + + + + {pathElements[3].replace('-', ' ')} + + + + )} + + + ); +}; + +export default ComponentsPageBreadcrumbs; diff --git a/src/pages/Components/components/__tests__/BreadcrumbsComponents.test.tsx b/src/pages/Components/components/__tests__/BreadcrumbsComponents.test.tsx new file mode 100644 index 0000000..623fcfe --- /dev/null +++ b/src/pages/Components/components/__tests__/BreadcrumbsComponents.test.tsx @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest'; + +import { render, screen } from 'test/test-utils'; + +import BreadcrumbsComponents from '../BreadcrumbsComponents'; + +describe('BreadcrumbsComponents', () => { + it('should render successfully', async () => { + // ARRANGE + render(); + await screen.findByTestId('components-breadcrumbs'); + + // ASSERT + expect(screen.getByTestId('components-breadcrumbs')).toBeDefined(); + }); +}); diff --git a/src/pages/Components/components/__tests__/ComponentsPageBreadcrumbs.test.tsx b/src/pages/Components/components/__tests__/ComponentsPageBreadcrumbs.test.tsx new file mode 100644 index 0000000..c792e64 --- /dev/null +++ b/src/pages/Components/components/__tests__/ComponentsPageBreadcrumbs.test.tsx @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { Navigate, Route, Routes } from 'react-router-dom'; + +import { render, screen } from 'test/test-utils'; + +import ComponentsPageBreadcrumbs from '../ComponentsPageBreadcrumbs'; + +describe('ComponentsPageBreadcrumbs', () => { + it('should render successfully', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-components-breadcrumbs'); + + // ASSERT + expect(screen.getByTestId('page-components-breadcrumbs')).toBeDefined(); + }); + + it('should render third path element breadcrumb', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-components-breadcrumbs-page-alert'); + + // ASSERT + expect(screen.getByTestId('page-components-breadcrumbs-page-alert')).toBeDefined(); + expect(screen.getByTestId('page-components-breadcrumbs-page-alert')).toHaveTextContent( + /alert/i, + ); + }); +}); diff --git a/src/pages/Settings/SettingsPage.tsx b/src/pages/Settings/SettingsPage.tsx index 349de10..3479997 100644 --- a/src/pages/Settings/SettingsPage.tsx +++ b/src/pages/Settings/SettingsPage.tsx @@ -5,6 +5,7 @@ import Avatar from 'common/components/Icon/Avatar'; import LoaderSkeleton from 'common/components/Loader/LoaderSkeleton'; import MenuNavLink from 'common/components/Menu/MenuNavLink'; import Page from 'common/components/Page/Page'; +import SettingsPageBreadcrumbs from './components/SettingsPageBreadcrumbs'; /** * The `SettingsPage` component renders the layout for the settings page. It @@ -17,6 +18,8 @@ const SettingsPage = (): JSX.Element => { return (
    + + {user ? (
    diff --git a/src/pages/Settings/components/SettingsPageBreadcrumbs.tsx b/src/pages/Settings/components/SettingsPageBreadcrumbs.tsx new file mode 100644 index 0000000..8ccfb52 --- /dev/null +++ b/src/pages/Settings/components/SettingsPageBreadcrumbs.tsx @@ -0,0 +1,42 @@ +import { useLocation } from 'react-router-dom'; + +import { BaseComponentProps } from 'common/utils/types'; +import Breadcrumbs from 'common/components/Breadcrumbs/Breadcrumbs'; + +/** + * The `SettingsPageBreadcrumbs` component renders the `Breadcrumbs` for the settings + * family of pages. + */ +const SettingsPageBreadcrumbs = ({ + className, + testId = 'page-settings-breadcrumbs', +}: BaseComponentProps): JSX.Element => { + const location = useLocation(); + const pathElements = location.pathname.split('/'); + + return ( + + + + Home + + + + Settings + + {!!pathElements[3] && ( + <> + + + + {pathElements[3].replace('-', ' ')} + + + + )} + + + ); +}; + +export default SettingsPageBreadcrumbs; diff --git a/src/pages/Settings/components/__tests__/SettingsPageBreadcrumbs.test.tsx b/src/pages/Settings/components/__tests__/SettingsPageBreadcrumbs.test.tsx new file mode 100644 index 0000000..a9b9400 --- /dev/null +++ b/src/pages/Settings/components/__tests__/SettingsPageBreadcrumbs.test.tsx @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { Navigate, Route, Routes } from 'react-router-dom'; + +import { render, screen } from 'test/test-utils'; + +import SettingsPageBreadcrumbs from '../SettingsPageBreadcrumbs'; + +describe('SettingsPageBreadcrumbs', () => { + it('should render successfully', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-settings-breadcrumbs'); + + // ASSERT + expect(screen.getByTestId('page-settings-breadcrumbs')).toBeDefined(); + }); + + it('should render third path element breadcrumb', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-settings-breadcrumbs-page-appearance'); + + // ASSERT + expect(screen.getByTestId('page-settings-breadcrumbs-page-appearance')).toBeDefined(); + expect(screen.getByTestId('page-settings-breadcrumbs-page-appearance')).toHaveTextContent( + /appearance/i, + ); + }); +}); diff --git a/src/pages/Tasks/TasksPage.tsx b/src/pages/Tasks/TasksPage.tsx index afe22e2..b30e47f 100644 --- a/src/pages/Tasks/TasksPage.tsx +++ b/src/pages/Tasks/TasksPage.tsx @@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'; import { PropsWithTestId } from 'common/utils/types'; import { useGetCurrentUser } from 'common/api/useGetCurrentUser'; import Page from 'common/components/Page/Page'; +import TasksPageBreadcrumbs from './components/TasksPageBreadcrumbs'; import UserInfo from './components/UserInfo'; import Card from 'common/components/Card/Card'; import FAIcon from 'common/components/Icon/FAIcon'; @@ -24,6 +25,8 @@ const TasksPage = ({ testId = 'page-tasks' }: PropsWithTestId): JSX.Element => { return (
    + + {/* page heading */}

    {t('tasks', { ns: 'tasks' })}

    diff --git a/src/pages/Tasks/components/TasksPageBreadcrumbs.tsx b/src/pages/Tasks/components/TasksPageBreadcrumbs.tsx new file mode 100644 index 0000000..4e148f0 --- /dev/null +++ b/src/pages/Tasks/components/TasksPageBreadcrumbs.tsx @@ -0,0 +1,77 @@ +import { useLocation, useParams } from 'react-router-dom'; +import toNumber from 'lodash/toNumber'; + +import { BaseComponentProps } from 'common/utils/types'; +import { useGetTask } from '../api/useGetTask'; +import Breadcrumbs from 'common/components/Breadcrumbs/Breadcrumbs'; +import LoaderSkeleton from 'common/components/Loader/LoaderSkeleton'; + +/** + * The `TasksPageBreadcrumbs` component renders the `Breadcrumbs` for the tasks + * family of pages. + */ +const TasksPageBreadcrumbs = ({ + className, + testId = 'page-tasks-breadcrumbs', +}: BaseComponentProps): JSX.Element => { + const location = useLocation(); + const params = useParams(); + const pathElements = location.pathname.split('/'); + + const hasTask = !!params.taskId; + const hasTaskAdd = pathElements.includes('add'); + const hasTaskEdit = pathElements.includes('edit'); + + const { data: task, isLoading: isLoadingTask } = useGetTask({ taskId: toNumber(params.taskId) }); + + return ( + + + + + Home + + + + + + Tasks + + + {hasTaskAdd && ( + <> + + + Add + + + )} + {hasTask && ( + <> + + + {!!task && ( + + {task.title} + + )} + {isLoadingTask && ( + + )} + + + )} + {hasTaskEdit && ( + <> + + + Edit + + + )} + + + ); +}; + +export default TasksPageBreadcrumbs; diff --git a/src/pages/Tasks/components/__tests__/TasksPageBreadcrumbs.test.tsx b/src/pages/Tasks/components/__tests__/TasksPageBreadcrumbs.test.tsx new file mode 100644 index 0000000..24d5820 --- /dev/null +++ b/src/pages/Tasks/components/__tests__/TasksPageBreadcrumbs.test.tsx @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; +import { Navigate, Route, Routes } from 'react-router-dom'; + +import { render, screen } from 'test/test-utils'; + +import TasksPageBreadcrumbs from '../TasksPageBreadcrumbs'; + +describe('TasksPageBreadcrumbs', () => { + it('should render successfully', async () => { + // ARRANGE + render(); + await screen.findByTestId('page-tasks-breadcrumbs'); + + // ASSERT + expect(screen.getByTestId('page-tasks-breadcrumbs')).toBeDefined(); + }); + + it('should display breadcrumbs for a task', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-tasks-breadcrumbs-link-task'); + + // ASSERT + expect(screen.getByTestId('page-tasks-breadcrumbs-link-home')).toBeDefined(); + expect(screen.getByTestId('page-tasks-breadcrumbs-link-tasks')).toBeDefined(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-page-tasks-add')).toBeNull(); + expect(screen.getByTestId('page-tasks-breadcrumbs-link-task')).toBeDefined(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-page-task-edit')).toBeNull(); + }); + + it('should display breadcrumbs for the task list', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-tasks-breadcrumbs-link-tasks'); + + // ASSERT + expect(screen.getByTestId('page-tasks-breadcrumbs-link-home')).toBeDefined(); + expect(screen.getByTestId('page-tasks-breadcrumbs-link-tasks')).toBeDefined(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-page-tasks-add')).toBeNull(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-link-task')).toBeNull(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-page-task-edit')).toBeNull(); + }); + + it('should display breadcrumbs for task add', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-tasks-breadcrumbs-page-task-add'); + + // ASSERT + expect(screen.getByTestId('page-tasks-breadcrumbs-link-home')).toBeDefined(); + expect(screen.getByTestId('page-tasks-breadcrumbs-link-tasks')).toBeDefined(); + expect(screen.getByTestId('page-tasks-breadcrumbs-page-task-add')).toBeDefined(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-link-task')).toBeNull(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-page-task-edit')).toBeNull(); + }); + + it('should display breadcrumbs for task edit', async () => { + // ARRANGE + render( + + } /> + } /> + } /> + } /> + } /> + , + ); + await screen.findByTestId('page-tasks-breadcrumbs-link-task'); + + // ASSERT + expect(screen.getByTestId('page-tasks-breadcrumbs-link-home')).toBeDefined(); + expect(screen.getByTestId('page-tasks-breadcrumbs-link-tasks')).toBeDefined(); + expect(screen.queryByTestId('page-tasks-breadcrumbs-page-task-add')).toBeNull(); + expect(screen.getByTestId('page-tasks-breadcrumbs-link-task')).toBeDefined(); + expect(screen.getByTestId('page-tasks-breadcrumbs-page-task-edit')).toBeDefined(); + }); +});