From 259bcc0025ed09fa090cdeabe1aab5582dee45a3 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 1 Jul 2024 19:55:17 +0530 Subject: [PATCH 1/5] test: add home and landing page tests --- frontend/jest.config.cjs | 1 + .../components/__tests__/HomePage.test.tsx | 98 +++++++++++++++++++ .../components/__tests__/LandingPage.test.tsx | 43 ++++++++ 3 files changed, 142 insertions(+) create mode 100644 frontend/src/components/__tests__/HomePage.test.tsx create mode 100644 frontend/src/components/__tests__/LandingPage.test.tsx diff --git a/frontend/jest.config.cjs b/frontend/jest.config.cjs index a4ba050c..d38e92f4 100644 --- a/frontend/jest.config.cjs +++ b/frontend/jest.config.cjs @@ -3,6 +3,7 @@ module.exports = { testEnvironment: 'jsdom', moduleNameMapper: { '^@/(.*)$': ['/src/$1', '/lib/$1'], + "\\.(scss|sass|css)$": "identity-obj-proxy" }, transformIgnorePatterns: [ "/node_modules/(?!react-toastify)" diff --git a/frontend/src/components/__tests__/HomePage.test.tsx b/frontend/src/components/__tests__/HomePage.test.tsx new file mode 100644 index 00000000..eabd5837 --- /dev/null +++ b/frontend/src/components/__tests__/HomePage.test.tsx @@ -0,0 +1,98 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import { HomePage } from "../HomePage"; + +// Mock dependencies +jest.mock("../HomeComponents/Navbar/Navbar", () => ({ + Navbar: () =>
Mocked Navbar
, +})); +jest.mock("../HomeComponents/Hero/Hero", () => ({ + Hero: () =>
Mocked Hero
, +})); +jest.mock("../HomeComponents/Footer/Footer", () => ({ + Footer: () =>
Mocked Footer
, +})); +jest.mock("../HomeComponents/SetupGuide/SetupGuide", () => ({ + SetupGuide: () =>
Mocked SetupGuide
, +})); +jest.mock("../HomeComponents/FAQ/FAQ", () => ({ + FAQ: () =>
Mocked FAQ
, +})); +jest.mock("../HomeComponents/Tasks/Tasks", () => ({ + Tasks: () =>
Mocked Tasks
, +})); + +const mockedNavigate = jest.fn(); +jest.mock("react-router", () => ({ + useNavigate: () => mockedNavigate, +})); +jest.mock("@/components/utils/URLs", () => ({ + url: { + backendURL: "http://mocked-backend-url/", + containerOrigin: "http://mocked-origin/", + frontendURL: "http://mocked-frontend-url/", + }, +})); + +// Mock fetch +global.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + picture: "mocked-picture-url", + email: "mocked-email", + encryption_secret: "mocked-encryption-secret", + uuid: "mocked-uuid", + name: "mocked-name", + }), + }) +) as jest.Mock; + +describe("HomePage", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it("renders correctly when user info is fetched successfully", async () => { + render( + + ); + + await waitFor(() => { + expect(screen.getByText("Mocked Navbar")).toBeInTheDocument(); + expect(screen.getByText("Mocked Hero")).toBeInTheDocument(); + expect(screen.getByText("Mocked Tasks")).toBeInTheDocument(); + expect(screen.getByText("Mocked SetupGuide")).toBeInTheDocument(); + expect(screen.getByText("Mocked FAQ")).toBeInTheDocument(); + expect(screen.getByText("Mocked Footer")).toBeInTheDocument(); + }); + }); + + it("renders session expired message when user info fetch fails", async () => { + (fetch as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ + ok: false, + }) + ); + + render( + + ); + + await waitFor(() => { + expect(screen.getByText("Session has been expired.")).toBeInTheDocument(); + }); + }); + + it("navigates to home page on fetch error", async () => { + (fetch as jest.Mock).mockImplementationOnce(() => Promise.reject("Fetch error")); + + render( + + ); + + await waitFor(() => { + expect(mockedNavigate).toHaveBeenCalledWith("/"); + }); + }); +}); diff --git a/frontend/src/components/__tests__/LandingPage.test.tsx b/frontend/src/components/__tests__/LandingPage.test.tsx new file mode 100644 index 00000000..93464876 --- /dev/null +++ b/frontend/src/components/__tests__/LandingPage.test.tsx @@ -0,0 +1,43 @@ +import { render, screen } from "@testing-library/react"; +import { LandingPage } from "../LandingPage"; + +// Mock dependencies +jest.mock("../LandingComponents/Navbar/Navbar", () => ({ + Navbar: () =>
Mocked Navbar
, +})); +jest.mock("../LandingComponents/Hero/Hero", () => ({ + Hero: () =>
Mocked Hero
, +})); +jest.mock("../LandingComponents/About/About", () => ({ + About: () =>
Mocked About
, +})); +jest.mock("../LandingComponents/HowItWorks/HowItWorks", () => ({ + HowItWorks: () =>
Mocked HowItWorks
, +})); +jest.mock("../LandingComponents/Contact/Contact", () => ({ + Contact: () =>
Mocked Contact
, +})); +jest.mock("../LandingComponents/FAQ/FAQ", () => ({ + FAQ: () =>
Mocked FAQ
, +})); +jest.mock("../LandingComponents/Footer/Footer", () => ({ + Footer: () =>
Mocked Footer
, +})); +jest.mock("../../components/utils/ScrollToTop", () => ({ + ScrollToTop: () =>
Mocked ScrollToTop
, +})); + +describe("LandingPage", () => { + it("renders all components correctly", () => { + render(); + + expect(screen.getByText("Mocked Navbar")).toBeInTheDocument(); + expect(screen.getByText("Mocked Hero")).toBeInTheDocument(); + expect(screen.getByText("Mocked About")).toBeInTheDocument(); + expect(screen.getByText("Mocked HowItWorks")).toBeInTheDocument(); + expect(screen.getByText("Mocked Contact")).toBeInTheDocument(); + expect(screen.getByText("Mocked FAQ")).toBeInTheDocument(); + expect(screen.getByText("Mocked Footer")).toBeInTheDocument(); + expect(screen.getByText("Mocked ScrollToTop")).toBeInTheDocument(); + }); +}); From acf2d8f9045342ed02836a66af7aab36512b82f6 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 1 Jul 2024 19:55:33 +0530 Subject: [PATCH 2/5] test: add task tests --- .../Tasks/__tests__/Tasks.test.tsx | 14 ++- .../Tasks/__tests__/tasks-utils.test.ts | 100 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx index f281be78..34a40c6d 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx @@ -8,7 +8,19 @@ const mockProps = { UUID: 'mockUUID', }; -// Mock fetch function to simulate API calls +// Mock functions and modules +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('../tasks-utils', () => ({ + markTaskAsCompleted: jest.fn(), + markTaskAsDeleted: jest.fn(), +})); + global.fetch = jest.fn().mockResolvedValue({ ok: true }); describe('Tasks Component', () => { diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts index f4202a17..432e5a4f 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/tasks-utils.test.ts @@ -6,6 +6,8 @@ import { handleDate, sortTasks, sortTasksById, + markTaskAsCompleted, + markTaskAsDeleted, } from "../tasks-utils"; import { Task } from "@/components/utils/types"; @@ -433,3 +435,101 @@ describe("handleDate", () => { ); }); }); + +import { url } from "@/components/utils/URLs"; +// Mock fetch and toast +global.fetch = jest.fn(); +jest.mock("react-toastify", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +describe("markTaskAsCompleted", () => { + const email = "test@example.com"; + const encryptionSecret = "secret"; + const UUID = "user-uuid"; + const taskuuid = "task-uuid"; + const backendURL = `${url.backendURL}complete-task`; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("marks task as completed successfully", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ ok: true }); + + await markTaskAsCompleted(email, encryptionSecret, UUID, taskuuid); + + expect(fetch).toHaveBeenCalledWith(backendURL, { + method: "POST", + body: JSON.stringify({ + email: email, + encryptionSecret: encryptionSecret, + UUID: UUID, + taskuuid: taskuuid, + }), + }); + expect(toast.success).toHaveBeenCalledWith( + "Task marked as completed successfully!", + { + position: "bottom-left", + autoClose: 3000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + } + ); + }); +}); + +global.fetch = jest.fn(); +jest.mock("react-toastify", () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + }, +})); + +describe("markTaskAsDeleted", () => { + const email = "test@example.com"; + const encryptionSecret = "secret"; + const UUID = "user-uuid"; + const taskuuid = "task-uuid"; + const backendURL = `${url.backendURL}delete-task`; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("marks task as deleted successfully", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ ok: true }); + + await markTaskAsDeleted(email, encryptionSecret, UUID, taskuuid); + + expect(fetch).toHaveBeenCalledWith(backendURL, { + method: "POST", + body: JSON.stringify({ + email: email, + encryptionSecret: encryptionSecret, + UUID: UUID, + taskuuid: taskuuid, + }), + }); + expect(toast.success).toHaveBeenCalledWith( + "Task marked as deleted successfully!", + { + position: "bottom-left", + autoClose: 3000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + } + ); + }); +}); From 429cf3726d64c3a2518778b21538e922501aad42 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 1 Jul 2024 20:49:46 +0530 Subject: [PATCH 3/5] test: add script for generating badge data --- frontend/badge.py | 26 ++++++++++++++++++++++++++ frontend/coverage-report.json | 3 +++ 2 files changed, 29 insertions(+) create mode 100644 frontend/badge.py create mode 100644 frontend/coverage-report.json diff --git a/frontend/badge.py b/frontend/badge.py new file mode 100644 index 00000000..fd87e9b5 --- /dev/null +++ b/frontend/badge.py @@ -0,0 +1,26 @@ +# script for extracting code coverage value for readme + +from bs4 import BeautifulSoup +import json + +def extract_coverage_data(file_path): + with open(file_path, "r", encoding="utf-8") as file: + html_content = file.read() + soup = BeautifulSoup(html_content, "lxml") + div = soup.find("div", class_="fl pad1y space-right2") + percentage = div.find("span", class_="strong").get_text(strip=True) + return {"frontend": percentage} + +def save_to_json(data, output_path): + with open(output_path, "w", encoding="utf-8") as json_file: + json.dump(data, json_file, indent=4) + + +html_file_path = "./coverage/lcov-report/index.html" +json_file_path = "./coverage-report.json" + +coverage_data = extract_coverage_data(html_file_path) + +save_to_json(coverage_data, json_file_path) + +print(f"Coverage data has been saved to {json_file_path}") diff --git a/frontend/coverage-report.json b/frontend/coverage-report.json new file mode 100644 index 00000000..234eb343 --- /dev/null +++ b/frontend/coverage-report.json @@ -0,0 +1,3 @@ +{ + "frontend": "80.33%" +} \ No newline at end of file From 4773b8e06cab36410f649c08d9e1b06c174d5086 Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 1 Jul 2024 22:52:35 +0530 Subject: [PATCH 4/5] docs: add test coverage badge --- README.md | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4c82d46b..ed940367 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,9 @@ Closed pull requests + Dynamic JSON Badge +

Website • @@ -23,7 +26,6 @@ --- - ## Guide to setup the frontend for development purposes: - ```bash @@ -36,21 +38,21 @@ ### Set environment variables in .env as: - *For docker usage:* +_For docker usage:_ - ```bash - VITE_BACKEND_URL="http://localhost:8000/" - VITE_FRONTEND_URL="http://localhost:80" - VITE_CONTAINER_ORIGIN="http://localhost:8080/" - ``` +```bash +VITE_BACKEND_URL="http://localhost:8000/" +VITE_FRONTEND_URL="http://localhost:80" +VITE_CONTAINER_ORIGIN="http://localhost:8080/" +``` - *For normal npm usage:* +_For normal npm usage:_ - ```bash - VITE_BACKEND_URL="http://localhost:8000/" - VITE_FRONTEND_URL="http://localhost:5173" - VITE_CONTAINER_ORIGIN="http://localhost:8080/" - ``` +```bash +VITE_BACKEND_URL="http://localhost:8000/" +VITE_FRONTEND_URL="http://localhost:5173" +VITE_CONTAINER_ORIGIN="http://localhost:8080/" +``` - Note: The ports can be changed on demand, and if you want to do so, be sure to change ports of the Dockerfiles as well as the ports in docker-compose.yml @@ -72,7 +74,7 @@ - Go to [Google cloud credential page](https://console.cloud.google.com/apis/credentials) for generating client id and secret. - Add the Client ID and secret as an environment variable -- *Sample .env format:* +- _Sample .env format:_ ```bash CLIENT_ID="client_ID" @@ -97,10 +99,12 @@ only while using Docker Container use + ``` FRONTEND_ORIGIN_DEV="http://localhost:" CONTAINER_ORIGIN="http://localhost:8080/" ``` + if you want to run by `npm run dev` - Run the application: @@ -116,7 +120,6 @@ docker-compose up ``` - ## Taskchampion Sync Server: - Setup the Taskchampion Sync Server "As a Container" by following the [official documentation](https://github.com/GothenburgBitFactory/taskchampion-sync-server/tree/main) @@ -159,7 +162,7 @@ // Initialize Firebase export const app = initializeApp(firebaseConfig); ``` -- Download it, and store it at frontend/src/lib/ by the name *firestore.js* +- Download it, and store it at frontend/src/lib/ by the name _firestore.js_ ## Run the Containers: From 25598b91114fc674e074b82e69dd78b85665be1a Mon Sep 17 00:00:00 2001 From: Abhishek Date: Mon, 1 Jul 2024 22:56:01 +0530 Subject: [PATCH 5/5] docs: fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed940367..55c2991b 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ VITE_CONTAINER_ORIGIN="http://localhost:8080/" ``` FRONTEND_ORIGIN_DEV="http://localhost" - CONTAINER_ORIGIN="http://172.19.0.2:8080/" + CONTAINER_ORIGIN="http://YOUR_DOCKER_CONTAINER_IP:8080/" ``` only while using Docker Container