diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..a4cba6c
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,81 @@
+image: node:latest
+
+cache:
+ paths:
+ - node_modules
+
+stages:
+ - lint
+ - test
+ - test:build
+ - deploy
+
+merge:test:lint:
+ stage: lint
+ script:
+ - npm install
+ - cp example.env.local .env.local
+ - cp example.env.local .env.test
+ - npm run lint
+ only:
+ - merge_requests
+
+merge:test:jest:
+ stage: test
+ script:
+ - npm install
+ - cp example.env.local .env.local
+ - cp example.env.local .env.test
+ - npm run test
+ only:
+ - merge_requests
+
+#merge:test:build:
+# stage: test:build
+# script:
+# - npm install
+# - cp example.env.local .env.local
+# - cp example.env.local .env.test
+# - npm run build
+# only:
+# - merge_requests
+
+test:lint:
+ stage: lint
+ script:
+ - npm install
+ - cp example.env.local .env.local
+ - cp example.env.local .env.test
+ - npm run lint
+ only:
+ - develop
+ - main
+
+test:jest:
+ stage: test
+ script:
+ - npm install
+ - cp example.env.local .env.local
+ - cp example.env.local .env.test
+ - npm run test
+ only:
+ - develop
+ - main
+
+#test:build:
+# stage: test:build
+# script:
+# - npm install
+# - cp example.env.local .env.local
+# - cp example.env.local .env.test
+# - npm run build
+# only:
+# - develop
+# - main
+
+deploy:
+ stage: deploy
+ script:
+ - echo "Run deploy project"
+ only:
+ - main
\ No newline at end of file
diff --git a/cypress.config.js b/cypress.config.js
new file mode 100644
index 0000000..3b67130
--- /dev/null
+++ b/cypress.config.js
@@ -0,0 +1,16 @@
+const { defineConfig } = require("cypress");
+
+module.exports = defineConfig({
+ e2e: {
+ setupNodeEvents(on, config) {
+ // implement node event listeners here
+ },
+ },
+
+ component: {
+ devServer: {
+ framework: "next",
+ bundler: "webpack",
+ },
+ },
+});
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
new file mode 100644
index 0000000..66ea16e
--- /dev/null
+++ b/cypress/support/commands.js
@@ -0,0 +1,25 @@
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add('login', (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
\ No newline at end of file
diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html
new file mode 100644
index 0000000..3e16e9b
--- /dev/null
+++ b/cypress/support/component-index.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Components App
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cypress/support/component.js b/cypress/support/component.js
new file mode 100644
index 0000000..8f9154b
--- /dev/null
+++ b/cypress/support/component.js
@@ -0,0 +1,27 @@
+// ***********************************************************
+// This example support/component.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
+
+import { mount } from 'cypress/react18'
+
+Cypress.Commands.add('mount', mount)
+
+// Example use:
+// cy.mount()
\ No newline at end of file
diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js
new file mode 100644
index 0000000..0e7290a
--- /dev/null
+++ b/cypress/support/e2e.js
@@ -0,0 +1,20 @@
+// ***********************************************************
+// This example support/e2e.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
\ No newline at end of file
diff --git a/example.env.local b/example.env.local
index b9a7e4c..7eb8d64 100644
--- a/example.env.local
+++ b/example.env.local
@@ -7,6 +7,7 @@ NEXT_PUBLIC_PRIMARY_MAIN = "#084070"
NEXT_PUBLIC_SECONDARY_MAIN = "#FF4E00"
NEXT_PUBLIC_BASE_URL = "https://loan.witel.ir"
+NEXT_PUBLIC_POWERED_BY_URL = "https://witel.ir"
#["NEXT_PUBLIC_HAS_WIDGET" , "NEXT_PUBLIC_HAS_NOTIFICATION"]
NEXT_PUBLIC_HAS_VALUE=["NEXT_PUBLIC_HAS_NOTIFICATION"]
diff --git a/jest.config.mjs b/jest.config.mjs
new file mode 100644
index 0000000..5bda02f
--- /dev/null
+++ b/jest.config.mjs
@@ -0,0 +1,17 @@
+import nextJest from 'next/jest.js'
+
+const createJestConfig = nextJest({
+ // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
+ dir: './',
+})
+
+// Add any custom config to be passed to Jest
+/** @type {import('jest').Config} */
+const config = {
+ // Add more setup options before each test is run
+ setupFilesAfterEnv: ['/jest.setup.js'],
+ testEnvironment: 'jest-environment-jsdom',
+}
+
+// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
+export default createJestConfig(config)
\ No newline at end of file
diff --git a/jest.setup.js b/jest.setup.js
new file mode 100644
index 0000000..06299aa
--- /dev/null
+++ b/jest.setup.js
@@ -0,0 +1,22 @@
+import '@testing-library/jest-dom'
+import {server} from "./mocks/server";
+import mockRouter from 'next-router-mock'
+
+jest.mock('next/router', () => jest.requireActual('next-router-mock'))
+
+beforeAll(() => {
+ server.listen()
+})
+beforeEach(() => {
+ localStorage.clear();
+ mockRouter.query = {}
+
+})
+
+afterEach(() => {
+ server.resetHandlers()
+})
+
+afterAll(() => {
+ server.close()
+})
\ No newline at end of file
diff --git a/mocks/AppWithProvider.jsx b/mocks/AppWithProvider.jsx
new file mode 100644
index 0000000..404c839
--- /dev/null
+++ b/mocks/AppWithProvider.jsx
@@ -0,0 +1,13 @@
+import App from "@/pages/_app";
+import fa from "&/locales/fa/app.json"
+
+const translations = {fa};
+const MockAppWithProviders = ({children, locale, title, isBot}) => {
+ const pageProps = {
+ title: title || '', isBot: isBot || true, locale: locale || 'fa', messages: translations[locale || 'fa']
+ }
+
+ return ( (<>{children}>)} pageProps={pageProps}/>)
+}
+
+export default MockAppWithProviders
\ No newline at end of file
diff --git a/mocks/handler.js b/mocks/handler.js
new file mode 100644
index 0000000..a4e5e8f
--- /dev/null
+++ b/mocks/handler.js
@@ -0,0 +1,5 @@
+import {userHandler} from "./handlers/user";
+
+export const handler = [
+ ...userHandler
+];
diff --git a/mocks/handlers/user.js b/mocks/handlers/user.js
new file mode 100644
index 0000000..10677de
--- /dev/null
+++ b/mocks/handlers/user.js
@@ -0,0 +1,19 @@
+import {rest} from "msw";
+import {GET_USER_ROUTE} from "@/core/data/apiRoutes";
+
+export const userHandler = [
+ rest.get(GET_USER_ROUTE, (req, res, ctx) => {
+ return res(ctx.json({
+ data: {
+ id: 10,
+ full_name: "Witel Company",
+ position: "Software Engineer",
+ permissions: [
+ "manage_users",
+ "manage_roles",
+ "manage_boss"
+ ],
+ }
+ }))
+ }),
+]
diff --git a/mocks/server.js b/mocks/server.js
new file mode 100644
index 0000000..0bf9e08
--- /dev/null
+++ b/mocks/server.js
@@ -0,0 +1,4 @@
+import {setupServer} from "msw/node";
+import {handler} from "./handler";
+
+export const server = setupServer(...handler)
\ No newline at end of file
diff --git a/package.json b/package.json
index a560c0c..6459d69 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,11 @@
"lint": "next lint",
"update": "run-script-os",
"update:win32": "echo 'ok'",
- "update:linux": "rm -rf .next/ && npm i && npm run build && sudo systemctl restart loan_facilities_develop_expert.service"
+ "update:linux": "rm -rf .next/ && npm i && npm run build && sudo systemctl restart loan_facilities_develop_expert.service",
+ "test": "jest",
+ "test:watch": "jest --watchAll",
+ "cypress:open": "cypress open",
+ "cypress:run": "cypress run"
},
"dependencies": {
"@emotion/react": "^11.10.6",
@@ -50,7 +54,17 @@
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
- "eslint-config-next": "^13.3.0",
+ "@testing-library/jest-dom": "^6.1.4",
+ "@testing-library/react": "^14.0.0",
+ "@testing-library/user-event": "^14.5.1",
+ "cypress": "^13.4.0",
+ "eslint-config-next": "^13.5.6",
+ "eslint-plugin-jest-dom": "^5.1.0",
+ "eslint-plugin-testing-library": "^6.1.0",
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0",
+ "msw": "^1.3.1",
+ "next-router-mock": "^0.9.10",
"run-script-os": "^1.1.6"
}
}
diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json
index 2a47ea5..9d3297f 100644
--- a/public/locales/fa/app.json
+++ b/public/locales/fa/app.json
@@ -2,6 +2,7 @@
"app_name": "سامانه جامع تسهیلات",
"app_short_name": "سامانه تسهیلات",
"dashboard": "داشبورد",
+ "powered_by_witel": "توسعه یافته توسط وایتل",
"first_page": "خوش آمدید",
"login": "ورود",
"login_expert": "ورود کارشناس",
diff --git a/src/components/first/__tests__/index.test.js b/src/components/first/__tests__/index.test.js
new file mode 100644
index 0000000..e6aff67
--- /dev/null
+++ b/src/components/first/__tests__/index.test.js
@@ -0,0 +1,61 @@
+import {render, screen, waitFor} from "@testing-library/react";
+import FirstComponent from "@/components/first";
+import MockAppWithProviders from "../../../../mocks/AppWithProvider";
+import {server} from "../../../../mocks/server";
+import {rest} from "msw";
+import {GET_USER_ROUTE} from "@/core/data/apiRoutes";
+
+describe("First Component From First Page", () => {
+ describe("Rendering", () => {
+ it("App Name Text Rendered", () => {
+ render();
+ const appNameElement = screen.queryByText(/سامانه جامع تسهیلات/i);
+ expect(appNameElement).toBeInTheDocument()
+ });
+ it("App version Text Rendered", () => {
+ render();
+ const versionControler = screen.queryByText(process.env.NEXT_PUBLIC_API_VERSION, {exact: false});
+ expect(versionControler).toBeInTheDocument()
+ });
+ it("Powered By Rendered With Currect URL", () => {
+ render();
+ const linkElement = screen.queryByText('توسعه یافته توسط وایتل');
+ expect(linkElement).toBeInTheDocument()
+ expect(linkElement).toHaveAttribute('href', process.env.NEXT_PUBLIC_POWERED_BY_URL);
+ });
+ });
+ describe("Behavioral", () => {
+ it("Show Login Button And Do Not Show Dashboard Button When User Is Not Authenticated", async () => {
+ render();
+ await waitFor(() => {
+ const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
+ const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
+ expect(authenticationButtonLogin).toBeInTheDocument()
+ expect(authenticationButtonDashboard).not.toBeInTheDocument()
+ })
+ });
+ it("Show Dashboard Button And Do Not Show Login Button When User Is Authenticated", async () => {
+ localStorage.setItem("_token", 'token');
+ render();
+ await waitFor(() => {
+ const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
+ const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
+ expect(authenticationButtonLogin).not.toBeInTheDocument()
+ expect(authenticationButtonDashboard).toBeInTheDocument()
+ })
+ });
+ it("Show Login Button And Do Not Show Dashboard Button When User Authentication Is Expired", async () => {
+ localStorage.setItem("_token", 'token');
+ server.use(rest.get(GET_USER_ROUTE, (req, res, ctx) => {
+ return res(ctx.status(401))
+ }))
+ render();
+ await waitFor(() => {
+ const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i)
+ const authenticationButtonDashboard = screen.queryByText(/داشبورد/i)
+ expect(authenticationButtonLogin).toBeInTheDocument()
+ expect(authenticationButtonDashboard).not.toBeInTheDocument()
+ })
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/first/index.jsx b/src/components/first/index.jsx
index cd5b5a5..2d144f1 100644
--- a/src/components/first/index.jsx
+++ b/src/components/first/index.jsx
@@ -1,11 +1,10 @@
-import {NextLinkComposed} from "@/core/components/LinkRouting";
+import LinkRouting, {NextLinkComposed} from "@/core/components/LinkRouting";
import CenterLayout from "@/layouts/CenterLayout";
import FullPageLayout from "@/layouts/FullPageLayout";
import useUser from "@/lib/app/hooks/useUser";
import {Button, Stack, Typography} from "@mui/material";
import {useTranslations} from "next-intl";
import SvgDashboard from "@/core/components/svgs/SvgDashboard";
-import process from "next/dist/build/webpack/loaders/resolve-url-loader/lib/postcss";
const FirstComponent = () => {
const t = useTranslations();
@@ -18,28 +17,16 @@ const FirstComponent = () => {
{t("app_name")}
- {isAuth ? (
-
- ) : (
-
- )}
+
{
color: 'primary.main',
fontFamily: 'Arial',
fontWeight: 'bold'
- }}>v{process.env.NEXT_PUBLIC_API_VERSION}
+ }}
+ >
+ v{process.env.NEXT_PUBLIC_API_VERSION}
+
+
+
+
+ {t("powered_by_witel")}
+
);
};
-export default FirstComponent;
+export default FirstComponent;
\ No newline at end of file
diff --git a/src/components/layouts/Dashboard/index.jsx b/src/components/layouts/Dashboard/index.jsx
index f54a894..953dd3c 100644
--- a/src/components/layouts/Dashboard/index.jsx
+++ b/src/components/layouts/Dashboard/index.jsx
@@ -1,10 +1,10 @@
import {useState} from "react";
-import Header from "./header";
-import Sidebar from "./sidebar";
import {Toolbar} from "@mui/material";
-import BreadCrumbs from "./breadcrumbs";
import FullPageLayout from "@/layouts/FullPageLayout";
import RolePermissionMiddleware from "@/middlewares/RolePermission";
+import Header from "@/components/layouts/Dashboard/Header";
+import Sidebar from "@/components/layouts/Dashboard/Sidebar";
+import BreadCrumbs from "@/components/layouts/Dashboard/Breadcrumbs";
const drawerWidth = 240;
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index 342f2a4..d7c2169 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -15,7 +15,7 @@ const App = ({Component, pageProps}) => {
<>
-
+
{pageProps.messages ? : ''}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 2e146c6..996ba03 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -16,6 +16,7 @@ export async function getServerSideProps({req, locale}) {
messages: (await import(`&/locales/${locale}/app.json`)).default,
title: "first_page",
isBot,
+ locale
},
};
}