Merge branch 'develop' of https://gitlab.com/witel-front-end/crm-app into feature/CFE-12_change_password_test
This commit is contained in:
@@ -7,5 +7,7 @@ NEXT_PUBLIC_PRIMARY_MAIN = "#1b4332"
|
|||||||
NEXT_PUBLIC_SECONDARY_MAIN = "#800e13"
|
NEXT_PUBLIC_SECONDARY_MAIN = "#800e13"
|
||||||
|
|
||||||
NEXT_PUBLIC_BASE_URL = "https://crm.witel.ir"
|
NEXT_PUBLIC_BASE_URL = "https://crm.witel.ir"
|
||||||
|
NEXT_PUBLIC_SERVER_SOCKET_URL = "wss://crmws.witel.ir"
|
||||||
|
NEXT_PUBLIC_POWERED_BY_URL = "https://witel.ir"
|
||||||
|
|
||||||
NODE_ENV = "development"
|
NODE_ENV = "development"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
import {server} from "./mocks/server";
|
import {server} from "./mocks/server";
|
||||||
|
import mockRouter from 'next-router-mock'
|
||||||
|
|
||||||
require('dotenv').config({path: './.env.local'});
|
require('dotenv').config({path: './.env.local'});
|
||||||
|
|
||||||
@@ -10,6 +11,8 @@ beforeAll(() => {
|
|||||||
})
|
})
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
mockRouter.query = {}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"sass": "^1.62.0",
|
"sass": "^1.62.0",
|
||||||
|
"socket.io-client": "^4.7.2",
|
||||||
"stylis": "^4.1.3",
|
"stylis": "^4.1.3",
|
||||||
"stylis-plugin-rtl": "^2.1.1",
|
"stylis-plugin-rtl": "^2.1.1",
|
||||||
"swr": "^2.1.5",
|
"swr": "^2.1.5",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"app_name": "سامانه CRM",
|
"app_name": "سامانه CRM",
|
||||||
"app_short_name": "سامانه CRM",
|
"app_short_name": "سامانه CRM",
|
||||||
"dashboard": "داشبورد",
|
"dashboard": "داشبورد",
|
||||||
|
"powered_by_witel": "توسعه یافته توسط وایتل",
|
||||||
"first_page": "خوش آمدید",
|
"first_page": "خوش آمدید",
|
||||||
"login": "ورود",
|
"login": "ورود",
|
||||||
"login_expert": "ورود کارشناس",
|
"login_expert": "ورود کارشناس",
|
||||||
|
|||||||
@@ -1,42 +1,61 @@
|
|||||||
|
import {render, screen, waitFor} from "@testing-library/react";
|
||||||
import FirstComponent from "@/components/first";
|
import FirstComponent from "@/components/first";
|
||||||
import {render, screen} from "@testing-library/react";
|
|
||||||
import MockAppWithProviders from "../../../../mocks/AppWithProvider";
|
import MockAppWithProviders from "../../../../mocks/AppWithProvider";
|
||||||
import {server} from "../../../../mocks/server";
|
import {server} from "../../../../mocks/server";
|
||||||
import {rest} from "msw";
|
import {rest} from "msw";
|
||||||
import {GET_USER_ROUTE} from "@/core/data/apiRoutes";
|
import {GET_USER_ROUTE} from "@/core/data/apiRoutes";
|
||||||
|
|
||||||
describe('First page component', () => {
|
describe("First Component From First Page", () => {
|
||||||
describe('Rendering', () => {
|
describe("Rendering", () => {
|
||||||
it('Should see app name text', () => {
|
it("App Name Text Rendered", () => {
|
||||||
render(<FirstComponent/>, {wrapper: MockAppWithProviders})
|
render(<MockAppWithProviders><FirstComponent/></MockAppWithProviders>);
|
||||||
const appNameElement = screen.getByTestId('app-name')
|
const appNameElement = screen.queryByText(/سامانه CRM/i);
|
||||||
expect(appNameElement).toHaveTextContent('سامانه CRM')
|
expect(appNameElement).toBeInTheDocument()
|
||||||
|
});
|
||||||
|
it("App version Text Rendered", () => {
|
||||||
|
render(<MockAppWithProviders><FirstComponent/></MockAppWithProviders>);
|
||||||
|
const versionControler = screen.queryByText(process.env.NEXT_PUBLIC_API_VERSION, {exact: false});
|
||||||
|
expect(versionControler).toBeInTheDocument()
|
||||||
|
});
|
||||||
|
it("Powered By Rendered With Currect URL", () => {
|
||||||
|
render(<MockAppWithProviders><FirstComponent/></MockAppWithProviders>);
|
||||||
|
const linkElement = screen.queryByText('توسعه یافته توسط وایتل');
|
||||||
|
expect(linkElement).toBeInTheDocument()
|
||||||
|
expect(linkElement).toHaveAttribute('href', process.env.NEXT_PUBLIC_POWERED_BY_URL);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('behavior', () => {
|
describe("Behavioral", () => {
|
||||||
it('Should see login button when is not auth', async () => {
|
it("Show Login Button And Do Not Show Dashboard Button When User Is Not Authenticated", async () => {
|
||||||
render(<FirstComponent/>, {wrapper: MockAppWithProviders})
|
render(<MockAppWithProviders><FirstComponent/></MockAppWithProviders>);
|
||||||
const buttonLoginOrDashboard = await screen.findByTestId('button-login-or-dashboard')
|
await waitFor(() => {
|
||||||
expect(buttonLoginOrDashboard).toHaveTextContent('ورود کارشناس')
|
const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
|
||||||
expect(buttonLoginOrDashboard).not.toHaveTextContent('داشبورد')
|
const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
|
||||||
|
expect(authenticationButtonLogin).toBeInTheDocument()
|
||||||
|
expect(authenticationButtonDashboard).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
});
|
});
|
||||||
it('Should see dashboard button when is auth', async () => {
|
it("Show Dashboard Button And Do Not Show Login Button When User Is Authenticated", async () => {
|
||||||
localStorage.setItem("_token", 'token');
|
localStorage.setItem("_token", 'token');
|
||||||
render(<FirstComponent/>, {wrapper: MockAppWithProviders})
|
render(<MockAppWithProviders><FirstComponent/></MockAppWithProviders>);
|
||||||
const buttonLoginOrDashboard = await screen.findByTestId('button-login-or-dashboard')
|
await waitFor(() => {
|
||||||
expect(buttonLoginOrDashboard).not.toHaveTextContent('ورود کارشناس')
|
const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
|
||||||
expect(buttonLoginOrDashboard).toHaveTextContent('داشبورد')
|
const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
|
||||||
|
expect(authenticationButtonLogin).not.toBeInTheDocument()
|
||||||
|
expect(authenticationButtonDashboard).toBeInTheDocument()
|
||||||
|
})
|
||||||
});
|
});
|
||||||
it('Should see login button when token expire', async () => {
|
it("Show Login Button And Do Not Show Dashboard Button When User Authentication Is Expired", async () => {
|
||||||
localStorage.setItem("_token", 'token');
|
localStorage.setItem("_token", 'token');
|
||||||
server.use([rest.get(GET_USER_ROUTE, (req, res, ctx) => {
|
server.use([rest.get(GET_USER_ROUTE, (req, res, ctx) => {
|
||||||
return res(ctx.status(403))
|
return res(ctx.status(403))
|
||||||
})])
|
})])
|
||||||
render(<FirstComponent/>, {wrapper: MockAppWithProviders})
|
render(<MockAppWithProviders><FirstComponent/></MockAppWithProviders>);
|
||||||
const buttonLoginOrDashboard = await screen.findByTestId('button-login-or-dashboard')
|
await waitFor(() => {
|
||||||
expect(buttonLoginOrDashboard).toHaveTextContent('ورود کارشناس')
|
const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
|
||||||
expect(buttonLoginOrDashboard).not.toHaveTextContent('داشبورد')
|
const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
|
||||||
|
expect(authenticationButtonLogin).toBeInTheDocument()
|
||||||
|
expect(authenticationButtonDashboard).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {NextLinkComposed} from "@/core/components/LinkRouting";
|
import LinkRouting, {NextLinkComposed} from "@/core/components/LinkRouting";
|
||||||
import CenterLayout from "@/layouts/CenterLayout";
|
import CenterLayout from "@/layouts/CenterLayout";
|
||||||
import FullPageLayout from "@/layouts/FullPageLayout";
|
import FullPageLayout from "@/layouts/FullPageLayout";
|
||||||
import useUser from "@/lib/app/hooks/useUser";
|
import useUser from "@/lib/app/hooks/useUser";
|
||||||
@@ -14,7 +14,7 @@ const FirstComponent = () => {
|
|||||||
<FullPageLayout sx={{p: 1}}>
|
<FullPageLayout sx={{p: 1}}>
|
||||||
<CenterLayout spacing={3}>
|
<CenterLayout spacing={3}>
|
||||||
<SvgDashboard width={300} height={200}/>
|
<SvgDashboard width={300} height={200}/>
|
||||||
<Typography variant="h5" data-testid="app-name" sx={{textAlign: "center"}}>
|
<Typography variant="h5" sx={{textAlign: "center"}}>
|
||||||
{t("app_name")}
|
{t("app_name")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
@@ -34,7 +34,19 @@ const FirstComponent = () => {
|
|||||||
color: 'primary.main',
|
color: 'primary.main',
|
||||||
fontFamily: 'Arial',
|
fontFamily: 'Arial',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>v{process.env.NEXT_PUBLIC_API_VERSION}</Typography>
|
}}
|
||||||
|
>
|
||||||
|
v{process.env.NEXT_PUBLIC_API_VERSION}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="center">
|
||||||
|
<LinkRouting
|
||||||
|
sx={{margin: 0.5, fontSize: "14px"}}
|
||||||
|
href={process.env.NEXT_PUBLIC_POWERED_BY_URL}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{t("powered_by_witel")}
|
||||||
|
</LinkRouting>
|
||||||
</Stack>
|
</Stack>
|
||||||
</FullPageLayout>
|
</FullPageLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
123
src/components/login-expert/LoginExpertComponent.jsx
Normal file
123
src/components/login-expert/LoginExpertComponent.jsx
Normal 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.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
|
||||||
28
src/components/login-expert/LoginLinkRouting.jsx
Normal file
28
src/components/login-expert/LoginLinkRouting.jsx
Normal 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
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -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("بازگشت به صفحه اصلی")
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 CenterLayout from "@/layouts/CenterLayout";
|
||||||
import FullPageLayout from "@/layouts/FullPageLayout";
|
import FullPageLayout from "@/layouts/FullPageLayout";
|
||||||
import useUser from "@/lib/app/hooks/useUser";
|
import LoginLinkRouting from "@/components/login-expert/LoginLinkRouting";
|
||||||
import LoginIcon from "@mui/icons-material/Login";
|
import LoginExpertComponent from "@/components/login-expert/LoginExpertComponent";
|
||||||
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";
|
|
||||||
|
|
||||||
const LoginComponent = () => {
|
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 (
|
return (
|
||||||
<FullPageLayout sx={{p: 1}}>
|
<FullPageLayout sx={{p: 1}}>
|
||||||
<CenterLayout>
|
<CenterLayout>
|
||||||
<Container maxWidth="sm">
|
<LoginExpertComponent />
|
||||||
<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>
|
|
||||||
</CenterLayout>
|
</CenterLayout>
|
||||||
<Stack direction="row" alignItems="center" justifyContent="center">
|
<LoginLinkRouting />
|
||||||
<LinkRouting
|
|
||||||
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>
|
|
||||||
</FullPageLayout>
|
</FullPageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
31
src/core/components/PasswordField/__test__/index.test.js
Normal file
31
src/core/components/PasswordField/__test__/index.test.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import {fireEvent, getByRole, render, screen} from "@testing-library/react";
|
||||||
|
import PasswordField from "@/core/components/PasswordField";
|
||||||
|
import {Formik} from "formik";
|
||||||
|
|
||||||
|
describe('Password Field component in core folder', () => {
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('Show password when visibility icon clicked',() => {
|
||||||
|
render (
|
||||||
|
<Formik initialValues={{password:''}} onSubmit={()=>{}}>
|
||||||
|
{(props)=>(
|
||||||
|
<PasswordField label="رمز عبور" />
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
)
|
||||||
|
|
||||||
|
const iconElement = screen.getByLabelText("رمز عبور")
|
||||||
|
const iconButton = screen.getByRole("button")
|
||||||
|
|
||||||
|
|
||||||
|
expect(iconElement).toHaveAttribute('type', 'password');
|
||||||
|
|
||||||
|
fireEvent.click(iconButton);
|
||||||
|
|
||||||
|
expect(iconElement).toHaveAttribute("type", "text");
|
||||||
|
|
||||||
|
fireEvent.click(iconButton);
|
||||||
|
|
||||||
|
expect(iconElement).toHaveAttribute('type', 'password');
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -36,4 +36,4 @@ const ErrorNotification = (t, status, message) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ErrorNotification;
|
export default ErrorNotification;
|
||||||
@@ -9,4 +9,4 @@ const PendingNotification = (t) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PendingNotification;
|
export default PendingNotification;
|
||||||
@@ -39,4 +39,4 @@ const SuccessNotification = (t, status) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SuccessNotification;
|
export default SuccessNotification;
|
||||||
@@ -35,4 +35,4 @@ const UploadFileNotification = (t) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UploadFileNotification;
|
export default UploadFileNotification;
|
||||||
@@ -37,4 +37,4 @@ const WarningNotification = (t, status) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WarningNotification;
|
export default WarningNotification;
|
||||||
@@ -48,7 +48,7 @@ const isServerError = status => status >= 500 && status <= 599;
|
|||||||
const isClientError = status => status >= 400 && status <= 499;
|
const isClientError = status => status >= 400 && status <= 499;
|
||||||
|
|
||||||
const errorLogic = (response, t, notification) => {
|
const errorLogic = (response, t, notification) => {
|
||||||
if (notification) ErrorNotification(t, response.status)
|
if (notification) ErrorNotification(t, response.status, response.data.message)
|
||||||
}
|
}
|
||||||
const errorValidation = (response, t, notification) => {
|
const errorValidation = (response, t, notification) => {
|
||||||
if (notification) {
|
if (notification) {
|
||||||
|
|||||||
15
src/lib/app/contexts/socket.jsx
Normal file
15
src/lib/app/contexts/socket.jsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import {createContext, useMemo} from "react";
|
||||||
|
import {io} from "socket.io-client";
|
||||||
|
|
||||||
|
export const SocketContext = createContext()
|
||||||
|
|
||||||
|
export const SocketProvider = ({children}) => {
|
||||||
|
const socket = useMemo(() => io(process.env.NEXT_PUBLIC_SERVER_SOCKET_URL, {
|
||||||
|
autoConnect: false,
|
||||||
|
auth: {
|
||||||
|
token: ""
|
||||||
|
}
|
||||||
|
}), [])
|
||||||
|
|
||||||
|
return <SocketContext.Provider value={{socket}}>{children}</SocketContext.Provider>
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import {GET_USER_ROUTE} from "@/core/data/apiRoutes";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {createContext, useCallback, useEffect, useReducer} from "react";
|
import {createContext, useCallback, useEffect, useReducer} from "react";
|
||||||
import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler";
|
import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler";
|
||||||
|
import useSocket from "@/lib/app/hooks/useSocket";
|
||||||
|
|
||||||
const initialUser = {
|
const initialUser = {
|
||||||
isAuth: false,
|
isAuth: false,
|
||||||
@@ -39,6 +40,7 @@ const reducer = (state, action) => {
|
|||||||
export const UserContext = createContext();
|
export const UserContext = createContext();
|
||||||
|
|
||||||
export const UserProvider = ({children}) => {
|
export const UserProvider = ({children}) => {
|
||||||
|
const socket = useSocket()
|
||||||
const [state, dispatch] = useReducer(reducer, initialUser);
|
const [state, dispatch] = useReducer(reducer, initialUser);
|
||||||
|
|
||||||
const clearUser = useCallback(() => {
|
const clearUser = useCallback(() => {
|
||||||
@@ -75,7 +77,7 @@ export const UserProvider = ({children}) => {
|
|||||||
headers: {authorization: `Bearer ${state.token}`},
|
headers: {authorization: `Bearer ${state.token}`},
|
||||||
})
|
})
|
||||||
.then(({data}) => {
|
.then(({data}) => {
|
||||||
if (typeof callback === "function") callback(data);
|
if (typeof callback === "function") callback(data.data);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
@@ -100,12 +102,16 @@ export const UserProvider = ({children}) => {
|
|||||||
clearUser();
|
clearUser();
|
||||||
changeAuthState(false);
|
changeAuthState(false);
|
||||||
changeLanguageState(false);
|
changeLanguageState(false);
|
||||||
|
|
||||||
|
socket.auth.token = ""
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
getUser((data) => {
|
getUser((data) => {
|
||||||
changeUser(data);
|
changeUser(data);
|
||||||
changeAuthState(true);
|
changeAuthState(true);
|
||||||
changeLanguageState(true);
|
changeLanguageState(true);
|
||||||
|
|
||||||
|
socket.auth.token = data.telephone_id
|
||||||
});
|
});
|
||||||
}, [state.token]);
|
}, [state.token]);
|
||||||
|
|
||||||
|
|||||||
10
src/lib/app/hooks/useSocket.jsx
Normal file
10
src/lib/app/hooks/useSocket.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import {useContext} from "react";
|
||||||
|
import {SocketContext} from "@/lib/app/contexts/socket";
|
||||||
|
|
||||||
|
const useSocket = () => {
|
||||||
|
const {socket} = useContext(SocketContext)
|
||||||
|
|
||||||
|
return socket
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useSocket
|
||||||
@@ -7,24 +7,27 @@ import {UserProvider} from "@/lib/app/contexts/user";
|
|||||||
import "moment/locale/fa";
|
import "moment/locale/fa";
|
||||||
import {NextIntlProvider} from "next-intl";
|
import {NextIntlProvider} from "next-intl";
|
||||||
import TitlePage from "@/core/components/TitlePage";
|
import TitlePage from "@/core/components/TitlePage";
|
||||||
|
import {SocketProvider} from "@/lib/app/contexts/socket";
|
||||||
|
|
||||||
const App = ({Component, pageProps}) => {
|
const App = ({Component, pageProps}) => {
|
||||||
|
|
||||||
return (<>
|
return (<>
|
||||||
<UserProvider>
|
<SocketProvider>
|
||||||
<LanguageProvider>
|
<UserProvider>
|
||||||
<NextIntlProvider locale={pageProps.locale} messages={pageProps.messages || {}}>
|
<LanguageProvider>
|
||||||
<MuiLayout isBot={pageProps.isBot}>
|
<NextIntlProvider locale={pageProps.locale} messages={pageProps.messages || {}}>
|
||||||
{pageProps.messages ? <TitlePage text={pageProps.title}/> : ''}
|
<MuiLayout isBot={pageProps.isBot}>
|
||||||
<LoadingProvider>
|
{pageProps.messages ? <TitlePage text={pageProps.title}/> : ''}
|
||||||
<AppLayout isBot={pageProps.isBot}>
|
<LoadingProvider>
|
||||||
<Component {...pageProps} />
|
<AppLayout isBot={pageProps.isBot}>
|
||||||
</AppLayout>
|
<Component {...pageProps} />
|
||||||
</LoadingProvider>
|
</AppLayout>
|
||||||
</MuiLayout>
|
</LoadingProvider>
|
||||||
</NextIntlProvider>
|
</MuiLayout>
|
||||||
</LanguageProvider>
|
</NextIntlProvider>
|
||||||
</UserProvider>
|
</LanguageProvider>
|
||||||
|
</UserProvider>
|
||||||
|
</SocketProvider>
|
||||||
</>)
|
</>)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user