write some behavioral test and rendered test for create form component

This commit is contained in:
2023-10-02 17:28:54 +03:30
49 changed files with 557 additions and 187 deletions

View File

@@ -1,5 +1,5 @@
import CenterLayout from "@/layouts/CenterLayout";
import DashboardLayouts from "@/layouts/dashboardLayouts";
import DashboardLayouts from "@/layouts/DashboardLayout";
import {Button, Container, Paper, Stack, Typography,} from "@mui/material";
import {Formik} from "formik";
import * as Yup from "yup";

View File

@@ -1,6 +1,6 @@
import StyledForm from "@/core/components/StyledForm";
import CenterLayout from "@/layouts/CenterLayout";
import DashboardLayouts from "@/layouts/dashboardLayouts";
import DashboardLayouts from "@/layouts/DashboardLayout";
import useUser from "@/lib/app/hooks/useUser";
import {Box, Container, Grid, Paper, Stack, TextField, Typography,} from "@mui/material";
import * as Yup from "yup";

View File

@@ -0,0 +1,9 @@
const CreateContent = () => {
return (
<>
</>
)
}
export default CreateContent

View File

@@ -1,18 +1,31 @@
import {render, screen} from "@testing-library/react";
import MockAppWithProviders from "../../../../../../mocks/AppWithProvider";
import {fireEvent, render, screen} from "@testing-library/react";
import React, {useState} from 'react';
import MockAppWithProviders from "../../../../../../../mocks/AppWithProvider";
import CreateForm from "../../CreateForm";
jest.spyOn(React, 'useState');
describe("CreateForm Component From Expert Management", () => {
describe("Rendering", () => {
it("Create Button Rendered", () => {
render(<MockAppWithProviders><CreateForm/></MockAppWithProviders>);
const versionControler = screen.queryByTitle("ثبت کارشناس");
expect(versionControler).toBeInTheDocument();
const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"});
expect(CreateButton).toBeInTheDocument();
});
it("Dialog Header Rendered", () => {
render(<MockAppWithProviders><CreateForm/></MockAppWithProviders>);
const versionControler = screen.queryByText("ثبت کارشناس");
expect(versionControler).toBeInTheDocument();
const CreateDialogHeader = screen.queryByText("افزودن کارشناس");
expect(CreateDialogHeader).toBeInTheDocument();
});
});
describe("Behavioral", () => {
it("by Clicking Create Button Dialog State Should Change (true/false)", () => {
const setOpenCreateDialogMock = jest.fn();
useState.mockReturnValue([false, setOpenCreateDialogMock]);
render(<MockAppWithProviders><CreateForm/></MockAppWithProviders>);
const CreateButton = screen.getByRole('button', {name: "افزودن کارشناس"});
fireEvent.click(CreateButton);
expect(setOpenCreateDialogMock).toHaveBeenCalledWith(true);
});
});
});

View File

@@ -1,4 +1,13 @@
const Create = () => {
import {Button, Dialog, DialogTitle, Stack, Tooltip} from "@mui/material";
import {useTranslations} from "next-intl";
import DataSaverOnIcon from "@mui/icons-material/DataSaverOn";
import {useState} from "react";
import CreateContent from "./CreateContent";
const Create = ({mutate, fetchUrl}) => {
const t = useTranslations();
const [openCreateDialog, setOpenCreateDialog] = useState(false);
return (
<>
<Stack direction={"row"} spacing={2}>

View File

@@ -1,4 +1,4 @@
import DashboardLayouts from "@/layouts/dashboardLayouts";
import DashboardLayouts from "@/layouts/DashboardLayout";
const DashboardFirstComponent = () => {
return <DashboardLayouts></DashboardLayouts>;

View File

@@ -0,0 +1,19 @@
import LinkRouting from "@/core/components/LinkRouting";
import {Typography} from "@mui/material";
import {useTranslations} from "next-intl";
export default function BreadcrumbItem(props) {
const t = useTranslations();
const isLast = props.index === props.RouterArray.length - 1;
const url = `/${props.RouterArray.slice(0, props.index + 1).join("/")}`;
return isLast ? (
<Typography variant="body2" color="primary">
{t("sidebar." + props.label)}
</Typography>
) : (
<LinkRouting underline="hover" color="inherit" passHref href={url}>
<Typography variant="body2">{t("sidebar." + props.label)}</Typography>
</LinkRouting>
);
}

View File

@@ -0,0 +1,49 @@
import {useRouter} from "next/router";
import {Box, Breadcrumbs} from "@mui/material";
import {useTheme} from "@mui/material/styles";
import {NavigateBefore, NavigateNext} from "@mui/icons-material";
import BreadcrumbItem from "./BreadcrumbItem";
const BreadCrumbs = (props) => {
const {isVisible} = props;
const theme = useTheme();
const router = useRouter();
if (!isVisible) {
return null;
}
const {pathname} = router;
const RouterArray = pathname.split("/").filter((segment) => segment !== "");
if (RouterArray.length === 1) {
return null;
}
return (
<Box p={3} component="span">
<Breadcrumbs
maxItems={2}
separator={
theme.direction === "ltr" ? (
<NavigateNext fontSize="small"/>
) : (
<NavigateBefore fontSize="small"/>
)
}
aria-label="breadcrumb"
>
{RouterArray.map((segment, index) => (
<BreadcrumbItem
RouterArray={RouterArray}
label={segment}
key={segment}
index={index}
/>
))}
</Breadcrumbs>
</Box>
);
};
export default BreadCrumbs;

View File

@@ -0,0 +1,13 @@
import PermPhoneMsgIcon from '@mui/icons-material/PermPhoneMsg';
import StyledFab from "@/core/components/StyledFab";
const CallWidgetButton = ({setOpen}) => {
return (
<StyledFab color="primary" aria-label="open-dialog"
onClick={() => setOpen(true)}>
<PermPhoneMsgIcon/>
</StyledFab>
)
}
export default CallWidgetButton

View File

@@ -0,0 +1,24 @@
import {Dialog} from "@mui/material";
import {DialogTransition} from "@/core/components/DialogTransition";
const CallWidgetDialog = ({open, setOpen}) => {
const handleClose = () => {
setOpen(false);
};
return (
<>
<Dialog
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={DialogTransition}
>
</Dialog>
</>
)
}
export default CallWidgetDialog

View File

@@ -0,0 +1,15 @@
import CallWidgetButton from "@/components/layouts/Dashboard/CallWidget/CallWidgetButton";
import CallWidgetDialog from "@/components/layouts/Dashboard/CallWidget/CallWidgetDialog";
import {useState} from "react";
const CallWidget = () => {
const [open, setOpen] = useState(false)
return (
<>
<CallWidgetButton setOpen={setOpen}/>
<CallWidgetDialog open={open} setOpen={setOpen}/>
</>
)
}
export default CallWidget

View File

@@ -0,0 +1,18 @@
import {Avatar, Stack, Typography} from "@mui/material";
import useUser from "@/lib/app/hooks/useUser";
export default function ProfileData() {
const {user} = useUser();
return (
<Stack alignItems="center" spacing={2} sx={{p: 3}}>
<Avatar
sx={{width: "80px", height: "80px"}}
alt="User Image"
src={user.avatar}
/>
<Typography sx={{fontSize: 15, fontWeight: 600}} textAlign="center">
{user.username}
</Typography>
</Stack>
);
}

View File

@@ -0,0 +1,62 @@
import {Avatar, IconButton, Menu, Tooltip} from "@mui/material";
import {useState} from "react";
import ProfileData from "./ProfileData";
import ProfileOptions from "./ProfileOptions";
import {useTranslations} from "next-intl";
import useUser from "@/lib/app/hooks/useUser";
function ProfileMenu() {
const t = useTranslations();
const [anchorElUser, setAnchorElUser] = useState(null);
const {user} = useUser();
const handleOpenUserMenu = (event) => {
setAnchorElUser(event.currentTarget);
};
const handleCloseUserMenu = () => {
setAnchorElUser(null);
};
return (
<>
<Tooltip title={t("header.open_profile")} arrow>
<IconButton onClick={handleOpenUserMenu} sx={{p: 0}}>
<Avatar
sx={{
width: 24,
height: 24,
backgroundColor: "#fff",
color: "primary.main",
}}
alt="User Image"
src={user.avatar}
/>
</IconButton>
</Tooltip>
<Menu
MenuListProps={{sx: {py: 0}}}
sx={{
mt: 6,
}}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu}
>
<ProfileData/>
<ProfileOptions handleCloseUserMenu={handleCloseUserMenu}/>
</Menu>
</>
);
}
export default ProfileMenu;

View File

@@ -0,0 +1,49 @@
import {Box, Button, MenuItem, Typography} from "@mui/material";
import MeetingRoomIcon from "@mui/icons-material/MeetingRoom";
import useUser from "@/lib/app/hooks/useUser";
import {useTranslations} from "next-intl";
export default function ProfileOptionLogOut({handleCloseUserMenu}) {
const t = useTranslations();
const {clearToken} = useUser();
const handleClickLogOut = () => {
handleCloseUserMenu();
clearToken();
};
return (
<>
<MenuItem
component={Button}
to={{
pathname: "/dashboard/logout",
}}
sx={{
display: "flex",
justifyContent: "center",
borderTop: 1,
px: 3,
py: 1.5,
borderColor: "#e1e1e1",
textTransform: "unset",
}}
onClick={handleClickLogOut}
>
<Box sx={{display: "flex", alignItems: "center", flex: 1}}>
<Box
sx={{
display: "flex",
alignItems: "center",
color: "primary.main",
pr: 2,
}}
>
<MeetingRoomIcon/>
</Box>
<Typography sx={{flex: 1}} textAlign="start">
{t("header.logout")}
</Typography>
</Box>
</MenuItem>
</>
);
}

View File

@@ -0,0 +1,49 @@
import {Box, MenuItem, Typography} from "@mui/material";
import {NextLinkComposed} from "@/core/components/LinkRouting";
import {useTranslations} from "next-intl";
import headerProfileItems from "@/core/data/headerProfileItems";
import ProfileOptionLogOut from "./ProfileOptionLogOut";
export default function ProfileOptions({handleCloseUserMenu}) {
const t = useTranslations();
return (
<>
{headerProfileItems.map((profile_item) => (
<MenuItem
component={NextLinkComposed}
to={{
pathname: profile_item.route,
}}
sx={{
display: "flex",
justifyContent: "center",
borderTop: 1,
px: 3,
py: 1.5,
borderColor: "#e1e1e1",
}}
key={profile_item.key}
onClick={handleCloseUserMenu}
>
<Box sx={{display: "flex", alignItems: "center", flex: 1}}>
<Box
sx={{
display: "flex",
alignItems: "center",
color: "primary.main",
pr: 2,
}}
>
{profile_item.icon}
</Box>
<Typography sx={{flex: 1}} textAlign="start">
{t(profile_item.name)}
</Typography>
</Box>
</MenuItem>
))}
<ProfileOptionLogOut handleCloseUserMenu={handleCloseUserMenu}/>
</>
);
}

View File

@@ -0,0 +1,72 @@
import MenuIcon from "@mui/icons-material/Menu";
import {AppBar, Box, Container, CssBaseline, IconButton, Stack, Toolbar, useTheme} from "@mui/material";
import ProfileMenu from "./ProfileMenu";
function Header({drawerWidth, handleDrawerToggle}) {
const theme = useTheme();
return (
<>
<CssBaseline/>
<AppBar
position="fixed"
sx={{
width: {md: `calc(100% - ${drawerWidth}px)`},
ml: {md: `${drawerWidth}px`},
}}
>
<Container maxWidth="xxl">
<Toolbar
disableGutters
sx={{
display: "flex",
}}
>
<Stack direction="row" justifyContent="flex-start" sx={{flex: 1}}>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerToggle}
edge="start"
sx={{display: {md: "none"}, m: 0}}
>
<MenuIcon/>
</IconButton>
</Stack>
<Stack
direction="row"
justifyContent="center"
sx={{
flex: 1,
position: "relative",
...theme.mixins.toolbar,
}}
>
<Box
sx={{
position: "relative",
my: 1,
width: 56,
height: 56,
"@media (min-width:600px)": {maxWidth: 64, maxHeight: 64},
"@media (min-width:0px)": {
"@media (orientation: landscape)": {
width: 48,
height: 48,
},
},
}}
>
</Box>
</Stack>
<Stack direction="row" justifyContent="flex-end" sx={{flex: 1}}>
<ProfileMenu/>
</Stack>
</Toolbar>
</Container>
</AppBar>
</>
);
}
export default Header;

View File

@@ -0,0 +1,27 @@
import {Divider, Stack, Toolbar, Typography} from "@mui/material";
import {useTranslations} from "next-intl";
import SidebarList from "./SidebarList";
import useUser from "@/lib/app/hooks/useUser";
const SidebarDrawer = ({handleDrawerToggle}) => {
const {user} = useUser()
const t = useTranslations();
return (
<>
<Toolbar>
<Stack>
<Typography variant="h6" sx={{color: "primary.main"}}>
{t("app_short_name")}
</Typography>
<Typography variant="caption">
{user.full_name} | {user.position}
</Typography>
</Stack>
</Toolbar>
<Divider/>
<SidebarList handleDrawerToggle={handleDrawerToggle}/>
</>
);
};
export default SidebarDrawer;

View File

@@ -0,0 +1,71 @@
import {Divider, List} from "@mui/material";
import {Fragment, useEffect, useReducer} from "react";
import SidebarListItem from "./SidebarListItem";
import sidebarMenu from "@/core/data/sidebarMenu";
import {useRouter} from "next/router";
import useUser from "@/lib/app/hooks/useUser";
function reducer(state, action) {
switch (action.type) {
case "COLLAPSE_MENU":
return state.map((itemsArr) =>
itemsArr.map((item) =>
action.key == item.key
? {...item, showSubItem: !item.showSubItem}
: item
)
);
case "SELECTED":
return state.map((itemsArr) =>
itemsArr.map((item) => {
if (item.type === "page") {
if (action.route === item.route)
return {...item, selected: true}
else
return {...item, selected: false}
}
return {...item}
// item.subItem.map((subitem) => {
// if (action.route === subitem.route)
// return {...item, showSubItem: true, subItem: {...subitem, selected: true}}
// else
// return {...item, showSubItem: false, subItem: {...subitem, selected: false}}
// })
}
)
);
default:
throw new Error();
}
}
export default function SidebarList({handleDrawerToggle}) {
const [itemMenu, dispatch] = useReducer(reducer, sidebarMenu);
const {user} = useUser();
const router = useRouter();
useEffect(() => {
dispatch({type: "SELECTED", route: router.pathname});
}, [router.pathname]);
return (
<List>
{itemMenu.map((itemArr, index) => (
<Fragment key={index}>
{itemArr.map((item) =>
<Fragment key={item.key}>
{(user.permissions.includes(item.permission) || item.permission === "all") &&
<SidebarListItem
item={item}
dispatch={dispatch}
handleDrawerToggle={handleDrawerToggle}
/>}
</Fragment>
)}
<Divider/>
</Fragment>
))}
</List>
);
}

View File

@@ -0,0 +1,84 @@
import {NextLinkComposed} from "@/core/components/LinkRouting";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import {Badge, IconButton, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography,} from "@mui/material";
import {useTranslations} from "next-intl";
import {Fragment} from "react";
import SidebarListSubItem from "./SidebarListSubItem";
import useNotification from "@/lib/app/hooks/useNotification";
const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => {
const t = useTranslations();
const {notification_count} = useNotification()
return (
<>
<ListItem disablePadding secondaryAction={
<IconButton>
<Badge
badgeContent={notification_count ? notification_count[item.name] : 0}
color="error"
variant="standard"
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
/>
</IconButton>
}>
<ListItemButton
selected={item.selected}
{...(item.type == "page" && {
component: NextLinkComposed,
to: {
pathname: item.route,
},
})}
onClick={() => {
if (item.type == "menu") {
dispatch({type: "COLLAPSE_MENU", key: item.key});
}
handleDrawerToggle();
}}
sx={{
minHeight: 48,
}}
>
<ListItemIcon
sx={{
minWidth: 0,
justifyContent: "center",
color: "primary.main",
pr: 2,
}}
>
{item.icon}
</ListItemIcon>
<ListItemText
primary={t(item.key)}
secondary={
item.secondary !== undefined ? (
<Typography variant="caption" color="textSecondary">
{t(item.secondary)}
</Typography>
) : null
}
/>
{item.type == "menu" &&
(item.showSubItem ? <ExpandLess/> : <ExpandMore/>)}
</ListItemButton>
</ListItem>
{item.subItem && (
<SidebarListSubItem
item={item}
handleDrawerToggle={handleDrawerToggle}
/>
)}
</>
);
};
export default SidebarListItem;

View File

@@ -0,0 +1,71 @@
import {NextLinkComposed} from "@/core/components/LinkRouting";
import {
Badge,
Collapse,
IconButton,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
Typography,
} from "@mui/material";
import {useTranslations} from "next-intl";
import useNotification from "@/lib/app/hooks/useNotification";
const SidebarListSubItem = ({item, handleDrawerToggle}) => {
const t = useTranslations();
const {notification_count} = useNotification()
return (
<Collapse in={item.showSubItem} timeout="auto" mountOnEnter={true} unmountOnExit={true}>
<List component="div" disablePadding sx={{bgcolor: "#f6f6f6", pr: 1}}>
{item.subItem.map((subitem, index) => (
<ListItem key={subitem.key} disablePadding secondaryAction={
<IconButton>
<Badge
badgeContent={notification_count ? notification_count[subitem.name] : 0}
color="error"
variant="standard"
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
/>
</IconButton>
}>
<ListItemButton
selected={subitem.selected}
component={NextLinkComposed}
to={{
pathname: subitem.route,
}}
sx={{
minHeight: 48,
}}
>
<ListItemIcon
sx={{
minWidth: 0,
justifyContent: "center",
pr: 2,
}}
>
{subitem.icon}
</ListItemIcon>
<ListItemText primary={t(subitem.key)} secondary={
subitem.secondary !== undefined ? (
<Typography variant="caption" color="textSecondary">
{t(subitem.secondary)}
</Typography>
) : null
}/>
</ListItemButton>
</ListItem>
))}
</List>
</Collapse>
);
};
export default SidebarListSubItem;

View File

@@ -0,0 +1,46 @@
import {Box, Drawer} from "@mui/material";
import SidebarDrawer from "./SidebarDrawer";
const Sidebar = (props) => {
return (
<Box
component="nav"
sx={{width: {md: props.drawerWidth}, flexShrink: {sm: 0}}}
aria-label="mailbox folders"
>
<Drawer
container={props.container}
variant="temporary"
open={props.mobileOpen}
onClose={props.handleDrawerToggle}
ModalProps={{
keepMounted: true,
}}
sx={{
display: {xs: "block", md: "none"},
"& .MuiDrawer-paper": {
boxSizing: "border-box",
width: props.drawerWidth,
},
}}
>
<SidebarDrawer handleDrawerToggle={props.handleDrawerToggle}/>
</Drawer>
<Drawer
variant="permanent"
sx={{
display: {xs: "none", md: "block"},
"& .MuiDrawer-paper": {
boxSizing: "border-box",
width: props.drawerWidth,
},
}}
open
>
<SidebarDrawer handleDrawerToggle={props.handleDrawerToggle}/>
</Drawer>
</Box>
);
};
export default Sidebar;

View File

@@ -0,0 +1,45 @@
import Header from "src/components/layouts/Dashboard/Header";
import Sidebar from "src/components/layouts/Dashboard/Sidebar";
import FullPageLayout from "@/layouts/FullPageLayout";
import {Toolbar} from "@mui/material";
import BreadCrumbs from "src/components/layouts/Dashboard/Breadcrumbs";
import {useState} from "react";
const drawerWidth = 240;
const Dashboard = (props) => {
const {window} = props;
const [mobileOpen, setMobileOpen] = useState(false);
const container =
window !== undefined ? () => window().document.body : undefined;
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
return (
<FullPageLayout direction="row">
<Header
handleDrawerToggle={handleDrawerToggle}
drawerWidth={drawerWidth}
/>
<Sidebar
container={container}
mobileOpen={mobileOpen}
handleDrawerToggle={handleDrawerToggle}
drawerWidth={drawerWidth}
/>
<FullPageLayout
component="main"
sx={{flexGrow: 1, width: {sm: `calc(100% - ${drawerWidth}px)`}}}
>
<Toolbar/>
<FullPageLayout sx={{mt: 3}}>
<BreadCrumbs isVisible={true}/>
{props.children}
</FullPageLayout>
</FullPageLayout>
</FullPageLayout>
)
}
export default Dashboard

View File

@@ -0,0 +1,123 @@
import {Box, Button, Container, Paper, Stack, TextField, Typography} from "@mui/material";
import {Field, Formik} from "formik";
import SvgLogin from "@/core/components/svgs/SvgLogin";
import StyledForm from "@/core/components/StyledForm";
import PasswordField from "@/core/components/PasswordField";
import LoginIcon from "@mui/icons-material/Login";
import {GET_USER_TOKEN} from "@/core/data/apiRoutes";
import * as Yup from "yup";
import useRequest from "@/lib/app/hooks/useRequest";
import useUser from "@/lib/app/hooks/useUser";
import {useTranslations} from "next-intl";
const LoginExpertComponent = () => {
const t = useTranslations();
const requestServer = useRequest()
const {setToken} = useUser(); // pass token to set token
const handleSubmit = (values, props) => {
requestServer(GET_USER_TOKEN, 'post', {
data: {
username: values.username,
password: values.password,
},
success: {
notification: {show: false}
}
}).then((response) => {
setToken(response.data.data.token)
}).catch(() => {
props.setSubmitting(false)
})
};
const initialValues = {
username: "",
password: "",
};
const validationSchema = Yup.object().shape({
username: Yup.string().required(t("LoginPage.username_error_message_required")),
password: Yup.string().required(t("LoginPage.password_error_message_required")),
});
return (
<Container maxWidth="sm">
<Paper elevation={0}>
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
>
{(props) => (
<Stack spacing={2} sx={{p: 2}}>
<Stack
sx={{width: "100%"}}
alignItems='center'
>
<SvgLogin width={300} height={200}/>
</Stack>
<Typography margin={2} variant="h4" textAlign="center">
{t("login_expert")}
</Typography>
<StyledForm sx={{width: "100%"}}>
<Stack spacing={3} sx={{p: 2}}>
<Field
as={TextField}
name="username"
variant="outlined"
label={t("LoginPage.text_field_user_name")}
placeholder={t(
"LoginPage.text_field_enter_your_username"
)}
type={"text"}
error={
props.touched.username && props.errors.username
? true
: false
}
fullWidth
helperText={
props.touched.username ? props.errors.username : null
}
/>
<PasswordField
name="password"
label={t("LoginPage.text_field_password")}
error={
props.touched.password && props.errors.password
? true
: false
}
helperText={
props.touched.password ? props.errors.password : null
}
placeholder={t(
"LoginPage.text_field_enter_your_password"
)}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Button
type="submit"
variant="contained"
fullWidth
size="medium"
endIcon={<LoginIcon/>}
disabled={props.isSubmitting || !(props.values.username && props.values.password)}
>
{t("LoginPage.button_submit")}
</Button>
</Box>
</Stack>
</StyledForm>
</Stack>
)}
</Formik>
</Paper>
</Container>
)
}
export default LoginExpertComponent

View File

@@ -0,0 +1,28 @@
import LinkRouting from "@/core/components/LinkRouting";
import {Stack} from "@mui/material";
import {useRouter} from "next/router";
import {useTranslations} from "next-intl";
const LoginLinkRouting = () => {
const t = useTranslations();
const router = useRouter()
const backUrlDecodedPath = router.query?.back_url
return (
<Stack direction="row" alignItems="center" justifyContent="center">
<LinkRouting
data-testid="link_routing"
sx={{margin: 2}}
href={
backUrlDecodedPath ? decodeURIComponent(backUrlDecodedPath) : "/"
}
>
{t("LoginPage.link_routing_back_to")}{" "}
{backUrlDecodedPath
? t("LoginPage.link_routing_previuos_page")
: t("LoginPage.link_routing_main_page")}
</LinkRouting>
</Stack>
)
}
export default LoginLinkRouting

View File

@@ -0,0 +1,153 @@
import {act, findByText, fireEvent, getByText, render, screen, waitFor} from '@testing-library/react';
import MockAppWithProviders from '../../../../mocks/AppWithProvider';
import mockRouter from 'next-router-mock'
import LoginExpertComponent from "@/components/login-expert/LoginExpertComponent";
import LoginLinkRouting from "@/components/login-expert/LoginLinkRouting";
describe('Login expert component from login page', () => {
describe('Rendering', () => {
it('LoginExpertComponent should see login expert text on the page',() => {
render(
<MockAppWithProviders>
<LoginExpertComponent />
</MockAppWithProviders>
);
const loginExpertElement = screen.getByRole('heading', { level: 4 });
expect(loginExpertElement).toHaveTextContent('ورود کارشناس');
});
it('LoginExpertComponent from LoginComponent Button should render login text', () => {
render(
<MockAppWithProviders>
<LoginExpertComponent />
</MockAppWithProviders>
);
const button = screen.getByText("ورود");
// Assertions for button props
expect(button).toHaveAttribute('type', 'submit');
expect(button).toHaveTextContent('ورود');
});
it("Show error when username is empty", async ()=>{
render(
<MockAppWithProviders>
<LoginExpertComponent />
</MockAppWithProviders>
);
const usernameInput = screen.getByLabelText("نام کاربری")
expect(screen.queryByText("وارد کردن نام کاربری الزامیست")).not.toBeInTheDocument()
fireEvent.blur(usernameInput);
await waitFor(()=>{
expect(screen.queryByText("وارد کردن نام کاربری الزامیست")).toBeInTheDocument()
})
fireEvent.change(usernameInput, {target : {value : "testuser"}})
await waitFor(()=>{
expect(screen.queryByText("وارد کردن نام کاربری الزامیست")).not.toBeInTheDocument()
})
})
it("Show error when password is empty", async ()=>{
render(
<MockAppWithProviders>
<LoginExpertComponent />
</MockAppWithProviders>
);
const passwordInput = screen.getByLabelText("رمز عبور")
expect(screen.queryByText("وارد کردن رمز عبور الزامیست")).not.toBeInTheDocument()
fireEvent.blur(passwordInput);
await waitFor(()=>{
expect(screen.queryByText("وارد کردن رمز عبور الزامیست")).toBeInTheDocument()
})
fireEvent.change(passwordInput, {target : {value : "testuser"}})
await waitFor(()=>{
expect(screen.queryByText("وارد کردن رمز عبور الزامیست")).not.toBeInTheDocument()
})
})
});
describe("Behavior", ()=>{
it('Should fill the username field', async () => {
render(
<MockAppWithProviders>
<LoginExpertComponent />
</MockAppWithProviders>
);
// Get username and password input elements by their name attributes
const usernameInput = screen.getByLabelText('نام کاربری');
fireEvent.change(usernameInput, {target: {value: 'testuser'}});
await act(async ()=>{
// Simulate user input
expect(usernameInput).toHaveValue('testuser');
})
});
it('Should fill the password fields', async () => {
render(
<MockAppWithProviders>
<LoginExpertComponent />
</MockAppWithProviders>
);
// Get username and password input elements by their name attributes
const passwordInput = screen.getByLabelText('رمز عبور');
fireEvent.change(passwordInput, {target: {value: 'password123'}});
await act(async ()=>{
// Simulate user input
expect(passwordInput).toHaveValue('password123');
})
});
// this test is for when back url does not exist to run this test you should comment if in mock func
it('Should get main page when back url dos not exist', async () => {
render(
<MockAppWithProviders>
<LoginLinkRouting/>
</MockAppWithProviders>
)
const linkElement = await screen.findByTestId("link_routing")
expect(linkElement).toHaveTextContent("بازگشت به صفحه اصلی")
});
})
describe("validation", ()=>{
it("Disabled submit button until fields completed", async ()=> {
render(
<MockAppWithProviders>
<LoginExpertComponent/>
</MockAppWithProviders>
)
const usernameInput = screen.getByLabelText('نام کاربری');
const passwordInput = screen.getByLabelText('رمز عبور');
const submitButton = screen.getByText('ورود');
expect(submitButton).toBeDisabled();
fireEvent.change(usernameInput, {target: {value: 'testuser'}});
await waitFor(async () => {
// Now, the submit button should be enabled because both fields are completed
expect(submitButton).toBeDisabled();
});
fireEvent.change(passwordInput, {target: {value: 'password123'}});
await waitFor(async () => {
// Now, the submit button should be enabled because both fields are completed
expect(submitButton).toBeEnabled();
});
})
})
});

View File

@@ -0,0 +1,18 @@
import {render, screen} from "@testing-library/react";
import MockAppWithProviders from "../../../../mocks/AppWithProvider";
import LoginLinkRouting from "@/components/login-expert/LoginLinkRouting";
describe("Login expert component from login page",()=>{
describe('Rendering', () => {
it('LoginLinkRouting from LoginComponent Should get main page when back url dos not exist', async () => {
render(
<MockAppWithProviders>
<LoginLinkRouting/>
</MockAppWithProviders>
)
const linkElement = await screen.findByTestId("link_routing")
expect(linkElement).toHaveTextContent("بازگشت به صفحه اصلی")
});
})
})

View File

@@ -1,151 +1,16 @@
import LinkRouting from "@/core/components/LinkRouting";
import PasswordField from "@/core/components/PasswordField";
import StyledForm from "@/core/components/StyledForm";
import {GET_USER_TOKEN} from "@/core/data/apiRoutes";
import CenterLayout from "@/layouts/CenterLayout";
import FullPageLayout from "@/layouts/FullPageLayout";
import useUser from "@/lib/app/hooks/useUser";
import LoginIcon from "@mui/icons-material/Login";
import {Box, Button, Container, Paper, Stack, TextField, Typography,} from "@mui/material";
import {Field, Formik} from "formik";
import {useTranslations} from "next-intl";
import {useSearchParams} from "next/navigation";
import * as Yup from "yup";
import useDirection from "@/lib/app/hooks/useDirection";
import SvgLogin from "@/core/components/svgs/SvgLogin";
import useRequest from "@/lib/app/hooks/useRequest";
import LoginLinkRouting from "@/components/login-expert/LoginLinkRouting";
import LoginExpertComponent from "@/components/login-expert/LoginExpertComponent";
const LoginComponent = () => {
const t = useTranslations();
const {directionApp} = useDirection(); // should delete because we don't have direction anymore
const {setToken} = useUser(); // pass token to set token
const requestServer = useRequest()
// getting url query
const searchParams = useSearchParams();
const backUrlDecodedPath = searchParams.get("back_url");
//formik properties
const handleSubmit = (values, props) => {
requestServer(GET_USER_TOKEN, 'post', {
data: {
username: values.username,
password: values.password,
},
success: {
notification: {show: false}
}
}).then((response) => {
setToken(response.data.token)
}).catch(() => {
props.setSubmitting(false)
})
};
const initialValues = {
username: "",
password: "",
};
const validationSchema = Yup.object().shape({
username: Yup.string().required(t("LoginPage.username_error_message_required")),
password: Yup.string().required(t("LoginPage.password_error_message_required")),
});
return (
<FullPageLayout sx={{p: 1}}>
<CenterLayout>
<Container maxWidth="sm">
<Paper elevation={0}>
<Formik
initialValues={initialValues}
onSubmit={handleSubmit}
validationSchema={validationSchema}
>
{(props) => (
<Stack spacing={2} sx={{p: 2}}>
<Stack
sx={{width: "100%"}}
alignItems='center'
>
<SvgLogin width={300} height={200}/>
</Stack>
<Typography margin={2} variant="h4" textAlign="center">
{t("login_expert")}
</Typography>
<StyledForm sx={{width: "100%"}}>
<Stack spacing={3} sx={{p: 2}}>
<Field
as={TextField}
name="username"
variant="outlined"
label={t("LoginPage.text_field_user_name")}
placeholder={t(
"LoginPage.text_field_enter_your_username"
)}
type={"text"}
error={
props.touched.username && props.errors.username
? true
: false
}
fullWidth
helperText={
props.touched.username ? props.errors.username : null
}
/>
<PasswordField
name="password"
label={t("LoginPage.text_field_password")}
error={
props.touched.password && props.errors.password
? true
: false
}
helperText={
props.touched.password ? props.errors.password : null
}
placeholder={t(
"LoginPage.text_field_enter_your_password"
)}
/>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Button
type="submit"
variant="contained"
fullWidth
size="medium"
endIcon={<LoginIcon/>}
disabled={props.isSubmitting}
>
{t("LoginPage.button_submit")}
</Button>
</Box>
</Stack>
</StyledForm>
</Stack>
)}
</Formik>
</Paper>
</Container>
<LoginExpertComponent />
</CenterLayout>
<Stack direction="row" alignItems="center" justifyContent="center">
<LinkRouting
sx={{margin: 2}}
href={
backUrlDecodedPath ? decodeURIComponent(backUrlDecodedPath) : "/"
}
>
{t("LoginPage.link_routing_back_to")}{"Witel"}
{backUrlDecodedPath
? t("LoginPage.link_routing_previuos_page")
: t("LoginPage.link_routing_main_page")}
</LinkRouting>
</Stack>
<LoginLinkRouting />
</FullPageLayout>
);
};