diff --git a/__spec__/custon-renders.tsx b/__spec__/custon-renders.tsx new file mode 100644 index 0000000..0678601 --- /dev/null +++ b/__spec__/custon-renders.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from 'react'; +import { createMemoryRouter, RouterProvider } from 'react-router-dom'; + +import { render } from './utils'; + +export const renderWithProvider = (Component: ReactNode) => { + const routes = [ + { + path: '/', + element: Component, + }, + ]; + + const router = createMemoryRouter(routes, { + initialEntries: ['/'], + initialIndex: 0, + }); + + return render(); +}; diff --git a/jest.config.ts b/jest.config.ts index 84ef5f3..5f87303 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,11 +9,12 @@ const config: Config = { 'src', '', ], + rootDir: './', verbose: true, preset: 'ts-jest/presets/js-with-ts', moduleNameMapper: { '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': - './configs/jest/__mocks__/fileMock.ts', + '/configs/jest/__mocks__/fileMock.ts', '\\.(css|less|scss)$': 'identity-obj-proxy', }, transformIgnorePatterns: [ diff --git a/src/apps/MoneyControllApp/ui/MoneyControllApp.spec.tsx b/src/apps/MoneyControllApp/ui/MoneyControllApp.spec.tsx deleted file mode 100644 index 1be32a9..0000000 --- a/src/apps/MoneyControllApp/ui/MoneyControllApp.spec.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { screen, render } from '@testing-library/react'; - -import MoneyControllApp from './MoneyControllApp.ui'; - -describe('Test MoneyControllApp', () => { - test('Render MoneyControllApp', () => { - render(); - - const appElem = screen.getByTestId('money-contoll-app'); - expect(appElem).toBeInTheDocument(); - }); -}); diff --git a/src/pages/AnalyzePage/ui/AnalyzePage.spec.tsx b/src/pages/AnalyzePage/ui/AnalyzePage.spec.tsx new file mode 100644 index 0000000..e3ad0c1 --- /dev/null +++ b/src/pages/AnalyzePage/ui/AnalyzePage.spec.tsx @@ -0,0 +1,32 @@ +import { render, screen } from '@testing-library/react'; + +import AnalyzePage from './AnalyzePage.ui'; +import AnalyzePageToolbar from './AnalyzePageToolbar.ui'; + +describe('Test AnalyzePage', () => { + const setup = () => render( + , + ); + + test('should render correctly', () => { + setup(); + + const pageElem = screen.getByTestId('analyze-page'); + expect(pageElem).toBeVisible(); + expect(pageElem).toHaveTextContent('Analyze page'); + }); +}); + +describe('Test AnalyzePageToolbar', () => { + const setup = () => render( + , + ); + + test('should render correctly', () => { + setup(); + + const toolbarElem = screen.getByTestId('analyze-page-toolbar'); + expect(toolbarElem).toBeVisible(); + expect(toolbarElem).toHaveTextContent('Analyze Page Toolbar'); + }); +}); diff --git a/src/pages/AnalyzePage/ui/AnalyzePage.ui.tsx b/src/pages/AnalyzePage/ui/AnalyzePage.ui.tsx index 9bb9675..410145c 100644 --- a/src/pages/AnalyzePage/ui/AnalyzePage.ui.tsx +++ b/src/pages/AnalyzePage/ui/AnalyzePage.ui.tsx @@ -1,7 +1,8 @@ -import type { FC } from 'react'; +import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; -const AnalyzePage: FC = () => ( -
+type AnalyzePageProps = DetailedHTMLProps, HTMLDivElement>; +const AnalyzePage: FC = (props) => ( +
Analyze page
); diff --git a/src/pages/AnalyzePage/ui/AnalyzePageToolbar.ui.tsx b/src/pages/AnalyzePage/ui/AnalyzePageToolbar.ui.tsx index 252b4e6..8fbac9c 100644 --- a/src/pages/AnalyzePage/ui/AnalyzePageToolbar.ui.tsx +++ b/src/pages/AnalyzePage/ui/AnalyzePageToolbar.ui.tsx @@ -1,8 +1,12 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; -const AnalyzePageToolbar = () => ( - +import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar'; +import type { FC } from 'react'; + +type AnalyzePageToolbarProps = ToolbarProps; +const AnalyzePageToolbar: FC = (props) => ( + Analyze Page Toolbar diff --git a/src/pages/ErrorPage/ui/ErrorPage.spec.tsx b/src/pages/ErrorPage/ui/ErrorPage.spec.tsx new file mode 100644 index 0000000..79a32f8 --- /dev/null +++ b/src/pages/ErrorPage/ui/ErrorPage.spec.tsx @@ -0,0 +1,52 @@ +import { render, screen } from '@testing-library/react'; + +import { ErrorPageToolbar } from 'pages/ErrorPage'; +import type { ErrorPageToolbarProps } from 'pages/ErrorPage/ui/ErrorPageToolbar.ui'; + +import ErrorPage from './ErrorPage.ui'; + +import type { ErrorPageProps } from './ErrorPage.ui'; + +describe('Test ErrorPage', () => { + const setup = (props?: Partial) => render( + , + ); + + test('should render ErrorBoundary', () => { + setup(); + + const pageElem = screen.getByTestId('error-page'); + expect(pageElem).toBeVisible(); + expect(pageElem).toHaveTextContent('Что-то пошло не так'); + }); + + test('should render 404', () => { + setup({ type: '404' }); + + const pageElem = screen.getByTestId('error-page'); + expect(pageElem).toBeVisible(); + expect(pageElem).not.toHaveTextContent('Что-то пошло не так'); + }); +}); + +describe('Test ErrorPageToolbar', () => { + const setup = (props?: Partial) => render( + , + ); + + test('should render ErrorBoundary', () => { + setup(); + + const toolbarElem = screen.getByTestId('error-page-toolbar'); + expect(toolbarElem).toBeVisible(); + expect(toolbarElem).toHaveTextContent('Непредвиденная ошибка'); + }); + + test('should render 404', () => { + setup({ type: '404' }); + + const toolbarElem = screen.getByTestId('error-page-toolbar'); + expect(toolbarElem).toBeVisible(); + expect(toolbarElem).toHaveTextContent('Страницы не существует'); + }); +}); diff --git a/src/pages/ErrorPage/ui/ErrorPage.ui.tsx b/src/pages/ErrorPage/ui/ErrorPage.ui.tsx index 62eabd2..398fe9e 100644 --- a/src/pages/ErrorPage/ui/ErrorPage.ui.tsx +++ b/src/pages/ErrorPage/ui/ErrorPage.ui.tsx @@ -4,14 +4,16 @@ import Typography from '@mui/material/Typography'; import _404Image from 'public/assets/images/404.png'; import OopsImage from 'public/assets/images/oops.png'; +import type { GridProps } from '@mui/material/Grid/Grid'; import type { FC } from 'react'; -type ErrorPageProps = { +export type ErrorPageProps = GridProps & { type?: '404' | 'ErrorBoundary'; }; const ErrorPage: FC = (props) => { const { type = 'ErrorBoundary', + ...restProps } = props; if (type === 'ErrorBoundary') { @@ -22,6 +24,7 @@ const ErrorPage: FC = (props) => { direction="column" justifyContent="center" rowGap={5} + {...restProps} > @@ -37,7 +40,7 @@ const ErrorPage: FC = (props) => { } return ( - + asd diff --git a/src/pages/ErrorPage/ui/ErrorPageToolbar.ui.tsx b/src/pages/ErrorPage/ui/ErrorPageToolbar.ui.tsx index f8eef69..a92fca7 100644 --- a/src/pages/ErrorPage/ui/ErrorPageToolbar.ui.tsx +++ b/src/pages/ErrorPage/ui/ErrorPageToolbar.ui.tsx @@ -1,9 +1,10 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; +import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar'; import type { FC } from 'react'; -type ErrorPageToolbarProps = { +export type ErrorPageToolbarProps = ToolbarProps & { type?: '404' | 'ErrorBoundary'; }; @@ -15,10 +16,11 @@ const TITLE: Record, string> = { const ErrorPageToolbar: FC = (props) => { const { type = 'ErrorBoundary', + ...restProps } = props; return ( - + {TITLE[type]} diff --git a/src/pages/PaymentsPage/ui/PaymentsPage.spec.tsx b/src/pages/PaymentsPage/ui/PaymentsPage.spec.tsx new file mode 100644 index 0000000..4b66c65 --- /dev/null +++ b/src/pages/PaymentsPage/ui/PaymentsPage.spec.tsx @@ -0,0 +1,35 @@ +import { render, screen } from '@testing-library/react'; + +import PaymentsPage from './PaymentsPage.ui'; +import PaymentsPageToolbar from './PaymentsPageToolbar.ui'; + +import type { PaymentsPageProps } from './PaymentsPage.ui'; +import type { PaymentsPageToolbarProps } from './PaymentsPageToolbar.ui'; + +describe('Test PaymentsPage', () => { + const setup = (props?: Partial) => render( + , + ); + + test('should render correctly', () => { + setup(); + + const pageElem = screen.getByTestId('payments-page'); + expect(pageElem).toBeVisible(); + expect(pageElem).toHaveTextContent('Payments Page'); + }); +}); + +describe('Test PaymentsPageToolbar', () => { + const setup = (props?: Partial) => render( + , + ); + + test('should render correctly', () => { + setup(); + + const toolbarElem = screen.getByTestId('payments-page-toolbar'); + expect(toolbarElem).toBeVisible(); + expect(toolbarElem).toHaveTextContent('Payments Page Toolbar'); + }); +}); diff --git a/src/pages/PaymentsPage/ui/PaymentsPage.ui.tsx b/src/pages/PaymentsPage/ui/PaymentsPage.ui.tsx index 31e5afb..d15c64b 100644 --- a/src/pages/PaymentsPage/ui/PaymentsPage.ui.tsx +++ b/src/pages/PaymentsPage/ui/PaymentsPage.ui.tsx @@ -1,7 +1,8 @@ -import type { FC } from 'react'; +import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; -const PaymentsPage: FC = () => ( -
+export type PaymentsPageProps = DetailedHTMLProps, HTMLDivElement>; +const PaymentsPage: FC = (props) => ( +
Payments Page
); diff --git a/src/pages/PaymentsPage/ui/PaymentsPageToolbar.ui.tsx b/src/pages/PaymentsPage/ui/PaymentsPageToolbar.ui.tsx index 0a844ca..472229f 100644 --- a/src/pages/PaymentsPage/ui/PaymentsPageToolbar.ui.tsx +++ b/src/pages/PaymentsPage/ui/PaymentsPageToolbar.ui.tsx @@ -1,10 +1,12 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; +import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar'; import type { FC } from 'react'; -const PaymentsPageToolbar: FC = () => ( - +export type PaymentsPageToolbarProps = ToolbarProps; +const PaymentsPageToolbar: FC = (props) => ( + Payments Page Toolbar diff --git a/src/pages/SchedulePage/ui/SchedulePage.spec.tsx b/src/pages/SchedulePage/ui/SchedulePage.spec.tsx new file mode 100644 index 0000000..dfb306d --- /dev/null +++ b/src/pages/SchedulePage/ui/SchedulePage.spec.tsx @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/react'; + +import { SchedulePage, SchedulePageToolbar } from 'pages/SchedulePage'; + +describe('Test SchedulePage', () => { + const setup = () => render( + , + ); + + test('should render correctly', () => { + setup(); + + const pageElem = screen.getByTestId('schedule-page'); + expect(pageElem).toBeVisible(); + expect(pageElem).toHaveTextContent('Schedule Page'); + }); +}); + +describe('Test SchedulePageToolabr', () => { + const setup = () => render( + , + ); + + test('should render correctly', () => { + setup(); + + const toolbarElem = screen.getByTestId('schedule-page-toolbar'); + expect(toolbarElem).toBeVisible(); + expect(toolbarElem).toHaveTextContent('Schedule Page Toolbar'); + }); +}); diff --git a/src/pages/SchedulePage/ui/SchedulePage.ui.tsx b/src/pages/SchedulePage/ui/SchedulePage.ui.tsx index 4a6c82a..31a5d2f 100644 --- a/src/pages/SchedulePage/ui/SchedulePage.ui.tsx +++ b/src/pages/SchedulePage/ui/SchedulePage.ui.tsx @@ -1,7 +1,8 @@ -import type { FC } from 'react'; +import type { DetailedHTMLProps, FC, HTMLAttributes } from 'react'; -const SchedulePage: FC = () => ( -
+export type SchedulePageProps = DetailedHTMLProps, HTMLDivElement>; +const SchedulePage: FC = (props) => ( +
Schedule Page
); diff --git a/src/pages/SchedulePage/ui/SchedulePageToolbar.ui.tsx b/src/pages/SchedulePage/ui/SchedulePageToolbar.ui.tsx index 6144077..38f5013 100644 --- a/src/pages/SchedulePage/ui/SchedulePageToolbar.ui.tsx +++ b/src/pages/SchedulePage/ui/SchedulePageToolbar.ui.tsx @@ -1,10 +1,12 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; +import type { ToolbarProps } from '@mui/material/Toolbar/Toolbar'; import type { FC } from 'react'; -const SchedulePageToolbar: FC = () => ( - +type SchedulePageToolbarProps = ToolbarProps; +const SchedulePageToolbar: FC = (props) => ( + Schedule Page Toolbar diff --git a/src/widgets/AsideMenuLayout/ui/AsideMenuLayout.spec.tsx b/src/widgets/AsideMenuLayout/ui/AsideMenuLayout.spec.tsx new file mode 100644 index 0000000..f349181 --- /dev/null +++ b/src/widgets/AsideMenuLayout/ui/AsideMenuLayout.spec.tsx @@ -0,0 +1,116 @@ +import { renderWithProvider } from '__spec__/custon-renders'; + +import PieChartIcon from '@mui/icons-material/PieChart'; +import { fireEvent, screen } from '@testing-library/react'; + +import AsideMenuLayout from './AsideMenuLayout.ui'; + +const toggleSwitcher = (testId = 'drawer-switcher') => { + const switcherElem = screen.getByTestId(testId); + fireEvent.click(switcherElem); +}; + +describe('Test AsideMenuLayout', () => { + const setup = () => { + const Content = () => ( +
+ Test Content +
+ ); + + const Toolbar = () => ( +
+ Test Toolbar +
+ ); + + const pages = [ + { + icon: , + title: 'Test Route', + path: '/test-route', + }, + { + icon: , + title: 'Another Test Route', + path: '/another-test-route', + }, + ]; + + return renderWithProvider( + } + data-testid="aside-menu-layout" + DrawerSwitcherProps={{ + // @ts-ignore + 'data-testid': 'drawer-switcher', + }} + pages={pages} + Toolbar={} + />, + ); + }; + + test('should render correctly', () => { + setup(); + + const layoutElem = screen.getByTestId('aside-menu-layout'); + expect(layoutElem).toBeVisible(); + }); + + test('should render toolbar', () => { + setup(); + + const toolbarElem = screen.getByText('Test Toolbar'); + expect(toolbarElem).toBeVisible(); + }); + + test('should render content', () => { + setup(); + + const toolbarElem = screen.getByText('Test Content'); + expect(toolbarElem).toBeVisible(); + }); + + test('should render navigation routes', () => { + setup(); + + const linkElem = screen.getByText('Test Route'); + expect(linkElem).toBeVisible(); + + const anotherLinkElem = screen.getByText('Another Test Route'); + expect(anotherLinkElem).toBeVisible(); + }); + + describe('Test switch open/close drawer', () => { + test('should have correct close icon', () => { + setup(); + + const closeIconElem = screen.getByTestId('ChevronLeftIcon'); + expect(closeIconElem).toBeVisible(); + }); + + test('should have correct open icon', () => { + setup(); + toggleSwitcher(); + + const openIconElem = screen.getByTestId('MenuIcon'); + expect(openIconElem).toBeVisible(); + }); + + test('should show link text', () => { + setup(); + + const linkTextElem = screen.getByText('Test Route'); + expect(linkTextElem).toBeVisible(); + }); + + test('should hide link text', () => { + setup(); + toggleSwitcher(); + + const linkTextElem = screen.queryByText('Test Route'); + expect(linkTextElem).not.toBeVisible(); + }); + }); +}); diff --git a/src/widgets/AsideMenuLayout/ui/AsideMenuLayout.ui.tsx b/src/widgets/AsideMenuLayout/ui/AsideMenuLayout.ui.tsx index 51f9a01..e94bc10 100644 --- a/src/widgets/AsideMenuLayout/ui/AsideMenuLayout.ui.tsx +++ b/src/widgets/AsideMenuLayout/ui/AsideMenuLayout.ui.tsx @@ -10,23 +10,32 @@ import AppBar from './components/AppBar'; import Drawer from './components/Drawer'; import DrawerHeader from './components/DrawerMenu'; import DrawerSwitcher from './components/DrawerSwitcher'; -import MenuItem from './components/MenuItem'; +import MenuItem from './components/MenuItem/MenuItem.ui'; -import type { MenuItemProps } from './components/MenuItem'; -import type { FC, ReactNode } from 'react'; +import type { MenuItemProps } from './components/MenuItem/MenuItem.ui'; +import type { BoxProps } from '@mui/material/Box/Box'; +import type { + FC, + ReactNode, + DetailedHTMLProps, + HTMLAttributes, +} from 'react'; const drawerWidth = 250; -type AsideMenuLayoutProps = { +type AsideMenuLayoutProps = BoxProps & { pages: MenuItemProps['page'][]; Content: ReactNode; Toolbar?: ReactNode; + DrawerSwitcherProps?: DetailedHTMLProps, HTMLDivElement>; }; const AsideMenuLayout: FC = (props) => { const { pages, Content, Toolbar, + DrawerSwitcherProps, + ...restProps } = props; const [isOpen, setIsOpen] = useState(true); @@ -39,7 +48,7 @@ const AsideMenuLayout: FC = (props) => { }; return ( - + = (props) => { { isOpen ? : } diff --git a/src/widgets/AsideMenuLayout/ui/components/AppBar/index.tsx b/src/widgets/AsideMenuLayout/ui/components/AppBar/index.tsx index 98e9d21..582417b 100644 --- a/src/widgets/AsideMenuLayout/ui/components/AppBar/index.tsx +++ b/src/widgets/AsideMenuLayout/ui/components/AppBar/index.tsx @@ -8,7 +8,7 @@ type AppBarProps = MuiAppBarProps & { drawerWidth: number; }; -const AppBar = styled(MuiAppBar, { shouldForwardProp: (prop) => prop !== 'open' })( +const AppBar = styled(MuiAppBar, { shouldForwardProp: (prop) => !['open', 'drawerWidth'].includes(prop as string) })( ({ theme, open, drawerWidth }) => ({ transition: theme.transitions.create(['width', 'margin'], { easing: theme.transitions.easing.sharp, diff --git a/src/widgets/AsideMenuLayout/ui/components/Drawer/index.tsx b/src/widgets/AsideMenuLayout/ui/components/Drawer/index.tsx index 29e654d..f09dbcd 100644 --- a/src/widgets/AsideMenuLayout/ui/components/Drawer/index.tsx +++ b/src/widgets/AsideMenuLayout/ui/components/Drawer/index.tsx @@ -24,7 +24,7 @@ const closedMixin = (theme: Theme): CSSObject => ({ width: `calc(${theme.spacing(8)} + 1px)`, }); -const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( +const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => !['open', 'drawerWidth'].includes(prop as string) })( ({ theme, open, drawerWidth }) => ({ width: drawerWidth, flexShrink: 0, diff --git a/src/widgets/AsideMenuLayout/ui/components/MenuItem/MenuItem.spec.tsx b/src/widgets/AsideMenuLayout/ui/components/MenuItem/MenuItem.spec.tsx new file mode 100644 index 0000000..b30f37d --- /dev/null +++ b/src/widgets/AsideMenuLayout/ui/components/MenuItem/MenuItem.spec.tsx @@ -0,0 +1,70 @@ +import { renderWithProvider } from '__spec__/custon-renders'; + +import PieChartIcon from '@mui/icons-material/PieChart'; +import { screen } from '@testing-library/react'; + +import MenuItem from './MenuItem.ui'; + +import type { MenuItemProps } from './MenuItem.ui'; + +describe('MenuItem', () => { + const setup = (props?: Partial) => renderWithProvider( + , + title: 'Test Route', + path: '/test-route', + }} + {...props} + />, + ); + + test('should render', () => { + setup(); + + const itemElem = screen.getByTestId('menu-item'); + expect(itemElem).toBeVisible(); + }); + + test('should render correct text', () => { + setup(); + + const textElem = screen.getByText('Test Route'); + expect(textElem).toBeVisible(); + }); + + test('should render correct icon', () => { + setup(); + + const iconElem = screen.getByTestId('PieChartIcon'); + expect(iconElem).toBeVisible(); + }); + + test('should have correct href', () => { + setup(); + + const itemElem = screen.getByTestId('menu-item-link'); + expect(itemElem).toHaveAttribute('href', '/test-route'); + }); + + describe('Test closed MenuItem', () => { + test('should not render text if closed', () => { + setup({ isOpen: false }); + + const textElem = screen.getByText('Test Route'); + expect(textElem).not.toBeVisible(); + }); + + test('should render correct icon if closed', () => { + setup({ isOpen: false }); + + const iconElem = screen.getByTestId('PieChartIcon'); + expect(iconElem).toBeVisible(); + }); + }); +}); diff --git a/src/widgets/AsideMenuLayout/ui/components/MenuItem/index.tsx b/src/widgets/AsideMenuLayout/ui/components/MenuItem/MenuItem.ui.tsx similarity index 82% rename from src/widgets/AsideMenuLayout/ui/components/MenuItem/index.tsx rename to src/widgets/AsideMenuLayout/ui/components/MenuItem/MenuItem.ui.tsx index 58fe27d..b46e5db 100644 --- a/src/widgets/AsideMenuLayout/ui/components/MenuItem/index.tsx +++ b/src/widgets/AsideMenuLayout/ui/components/MenuItem/MenuItem.ui.tsx @@ -6,7 +6,9 @@ import { styled, useTheme } from '@mui/material/styles'; import Typography from '@mui/material/Typography'; import { NavLink as RRDNavLink } from 'react-router-dom'; +import type { ListItemProps } from '@mui/material/ListItem/ListItem'; import type { FC, ReactNode } from 'react'; +import type { NavLinkProps } from 'react-router-dom'; const NavLink = styled(RRDNavLink)(({ theme }) => ({ textDecoration: 'none', @@ -14,25 +16,28 @@ const NavLink = styled(RRDNavLink)(({ theme }) => ({ width: '100%', })); -export type MenuItemProps = { +export type MenuItemProps = ListItemProps & { page: { icon: ReactNode; title: string; path: string; }; - isOpen: boolean; + isOpen?: boolean; + NavLinkProps?: NavLinkProps; }; const MenuItem: FC = (props) => { const { page, - isOpen, + isOpen = true, + NavLinkProps, + ...restProps } = props; const theme = useTheme(); return ( - - + + {({ isActive }) => (