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 }, }; }