-
+
diff --git a/frontend/src/components/LoadingButton.tsx b/frontend/src/components/LoadingButton.tsx
index 7e7e423..b1a93aa 100644
--- a/frontend/src/components/LoadingButton.tsx
+++ b/frontend/src/components/LoadingButton.tsx
@@ -12,6 +12,17 @@ interface LoadingButtonProps {
type?: 'button' | 'submit' | 'reset';
}
+/**
+ * Renders a button component that can display text or a loading spinner based
+ * on the loading state.
+ * @param loading - Indicates if the button is in a loading state.
+ * @param onClick - The function to be executed when the button is clicked.
+ * @param text - The text to be displayed on the button.
+ * @param disabled - Indicates if the button is disabled.
+ * @param sx - Custom styling for the button.
+ * @param type - The type of the button.
+ * @returns The loading button component.
+ */
export default function LoadingButton({ loading, onClick, text, disabled=false, sx, type = 'button' }: LoadingButtonProps) {
return (
diff --git a/frontend/src/components/LoadingModel.tsx b/frontend/src/components/LoadingModel.tsx
index 7c60aac..09e07de 100644
--- a/frontend/src/components/LoadingModel.tsx
+++ b/frontend/src/components/LoadingModel.tsx
@@ -15,6 +15,12 @@ interface LoadingModalProp {
open: boolean;
}
+/**
+ * Renders a modal component for displaying a loading indicator.
+ *
+ * @param {boolean} open - Determines whether the modal is open or closed.
+ * @returns {JSX.Element} A modal component with a circular loading indicator.
+ */
export default function LoadingModal({open}: LoadingModalProp) {
return (
diff --git a/frontend/src/components/ModSiderbar.tsx b/frontend/src/components/ModSiderbar.tsx
index 15579e7..f132ac9 100644
--- a/frontend/src/components/ModSiderbar.tsx
+++ b/frontend/src/components/ModSiderbar.tsx
@@ -17,11 +17,18 @@ interface SidebarProps {
}
const drawerWidth = 240;
+/**
+ * Functional component for the ModSidebar that displays a list of campus
+ * solutions with links to their respective Gantt charts.
+ *
+ * @param marginTop The top margin for the sidebar.
+ * @param width The width of the sidebar.
+ */
export default function ModSidebar({ marginTop, width }: SidebarProps) {
const [selectedCampus, setSelectedCampus] = useState
("");
const handleItemClick = (campusName: string) => {
- setSelectedCampus(campusName); // Update the selected campus
+ setSelectedCampus(campusName);
};
let campusSolutions: TimetableSolution[];
diff --git a/frontend/src/components/SkipButton.tsx b/frontend/src/components/SkipButton.tsx
index d25160d..f92857b 100644
--- a/frontend/src/components/SkipButton.tsx
+++ b/frontend/src/components/SkipButton.tsx
@@ -1,10 +1,35 @@
import Button from "@mui/material/Button";
import { useNavigate } from "react-router-dom";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
+import { useEffect, useState } from "react";
+/**
+ * Functional component for rendering a Skip Button that allows users to
+ * modify their timetable directly from the front page.
+ * Uses sessionStorage to check for existing campus solutions and conditionally
+ * renders different button styles based on the presence of solutions.
+ */
export default function SkipButton() {
const navigate = useNavigate();
- if (sessionStorage.getItem("campusSolutions") === undefined) {
+ const [hasSolution, setHasSolution] = useState(false);
+
+ useEffect(() => {
+ const checkHasSolution = () => {
+ const campusSolutions = sessionStorage.getItem("campusSolutions");
+ setHasSolution(campusSolutions !== null);
+ };
+
+ checkHasSolution();
+ window.addEventListener("fetchSolutionFinished", checkHasSolution);
+
+ return () => {
+ window.removeEventListener("fetchSolutionFinished", checkHasSolution);
+ };
+
+ }, []);
+
+
+ if (!hasSolution) {
return (
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx
index 195e094..225395f 100644
--- a/frontend/src/pages/LoginPage.tsx
+++ b/frontend/src/pages/LoginPage.tsx
@@ -6,6 +6,13 @@ import VIT_Logo from '../assets/logo.png';
import { REMOTE_API_URL } from '../scripts/api';
import LoadingButton from '../components/LoadingButton';
+/**
+ * Component for rendering the login page with username and password input
+ * fields.
+ * Handles form submission to validate credentials with the backend.
+ * If credentials are valid, sets the authorization header and navigates to
+ * the enrolment page.
+ */
export default function LoginPage() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
diff --git a/frontend/src/pages/SendData.tsx b/frontend/src/pages/SendData.tsx
index e96a357..6f35776 100644
--- a/frontend/src/pages/SendData.tsx
+++ b/frontend/src/pages/SendData.tsx
@@ -16,7 +16,6 @@ import LoadingButton from "../components/LoadingButton";
* (will remove and replace with display page in next sprint).
*
* @returns button for sending timetable problem and temporary display for timetable solution.
- * TODO: change button and UI elements to fit with VIT themes.
*/
export default function SendData() {
diff --git a/frontend/src/pages/TimetableMod.tsx b/frontend/src/pages/TimetableMod.tsx
index 45d8544..d17b07a 100644
--- a/frontend/src/pages/TimetableMod.tsx
+++ b/frontend/src/pages/TimetableMod.tsx
@@ -20,7 +20,7 @@ export default function TimetableMod() {
const { authHeader } = useAuthContext();
useEffect(() => {
- fetch(REMOTE_API_URL + "/timetabling/view", { headers: { 'Authorization': authHeader } })
+ fetch(REMOTE_API_URL + "/timetabling", { headers: { 'Authorization': authHeader } })
.then((response) => {
if (!response.ok) {
throw new Error("Network response was not ok");
diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts
index 66bf109..1283b3a 100644
--- a/frontend/src/scripts/solutionParsing.ts
+++ b/frontend/src/scripts/solutionParsing.ts
@@ -1,14 +1,11 @@
import { TimetableSolution, Weekday } from "./api";
-import {
- TimelineGroup,
- TimelineItem,
-} from "vis-timeline/standalone";
+import { TimelineGroup, TimelineItem } from "vis-timeline/standalone";
export type GanttItem = TimelineItem & {
UnitId: number;
campus: string;
course: string;
-}
+};
export type GanttGroup = TimelineGroup & {
treeLevel: number;
@@ -23,11 +20,19 @@ export type GanttItems = {
};
export type rawDate = {
- dayOfWeek: Weekday,
- time: string,
+ dayOfWeek: Weekday;
+ time: string;
};
const startDate = "2024-10-14";
+/**
+ * Format the given backend solution into suitable format for displaying in the
+ * Ganttchart.
+ *
+ * @param campusSolution The timetable solution for a specific campus.
+ * @returns GanttItems containing activities, rooms, and buildings for
+ * visualization.
+ */
export function getGanttItems(campusSolution: TimetableSolution): GanttItems {
let ganttActivities: GanttItem[] = [];
let ganttRooms: GanttGroup[] = [];
@@ -38,31 +43,27 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems {
const groupEnum = new Map
();
let counter = 1;
-
campusSolution.units.forEach((activity) => {
- // console.log("start");
if (!activityEnum.has(activity.unitId)) {
activityEnum.set(activity.unitId, counter);
counter++;
}
- let newRoom:boolean;
- let newBuilding:boolean;
+ let newRoom: boolean;
+ let newBuilding: boolean;
newRoom = false;
newBuilding = false;
if (!groupEnum.has(activity.room.roomCode)) {
groupEnum.set(activity.room.roomCode, counter);
counter++;
newRoom = true;
-
}
if (!groupEnum.has(activity.room.buildingId)) {
groupEnum.set(activity.room.buildingId, counter);
counter++;
newBuilding = true;
}
- // console.log(buildingLookup.get(activity.room.buildingId)?.nestedGroups);
//=============================Handle Rooms=================================
- if (newRoom) {
+ if (newRoom) {
const ganttRoom: GanttGroup = {
originalId: activity.room.roomCode,
id: groupEnum.get(activity.room.roomCode) || 0,
@@ -87,7 +88,7 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems {
//=============================Handle Buildings=============================
if (newBuilding) {
const ganttBuilding: GanttGroup = {
- originalId:activity.room.buildingId,
+ originalId: activity.room.buildingId,
id: groupEnum.get(activity.room.buildingId) || 0,
content: activity.room.buildingId,
treeLevel: 1,
@@ -114,7 +115,6 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems {
const buildingCheck = buildingLookup.get(activity.room.buildingId);
const roomGroup = groupEnum.get(activity.room.roomCode);
- // console.log(roomGroup);
if (buildingCheck && roomGroup) {
if (
buildingCheck.nestedGroups !== undefined &&
@@ -125,7 +125,6 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems {
} else {
throw new Error("LOGIC ERROR IN getGanttItems");
}
- // console.log("end");
});
_return = {
@@ -136,8 +135,29 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems {
return _return;
}
-function parseDate(startDate: string, dayOfWeek: string, startTime: string):Date {
- const daysOfWeek = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'];
+/**
+ * Parses the output backend date format into suitable frontend format for
+ * displaying
+ *
+ * @param startDate The starting date in string format (e.g., '2024-10-14')
+ * @param dayOfWeek The day of the week in string format (e.g., 'MONDAY').
+ * @param startTime The starting time in string format (e.g., 'HH:MM:SS').
+ * @returns The calculated final date based on the input parameters.
+ */
+function parseDate(
+ startDate: string,
+ dayOfWeek: string,
+ startTime: string
+): Date {
+ const daysOfWeek = [
+ "SUNDAY",
+ "MONDAY",
+ "TUESDAY",
+ "WEDNESDAY",
+ "THURSDAY",
+ "FRIDAY",
+ "SATURDAY",
+ ];
const baseDate = new Date(startDate);
@@ -146,14 +166,21 @@ function parseDate(startDate: string, dayOfWeek: string, startTime: string):Date
const dayDifference = (targetDayIndex + 7 - currentDayIndex) % 7;
- const [hours, minutes, seconds] = startTime.split(':').map(Number);
+ const [hours, minutes, seconds] = startTime.split(":").map(Number);
const finalDate = new Date(baseDate);
finalDate.setDate(baseDate.getDate() + dayDifference);
- finalDate.setHours(hours, minutes, seconds, 0);
+ finalDate.setHours(hours, minutes, seconds, 0);
return finalDate;
}
+/**
+ * Converts a JavaScript Date object to the correct format for the backend
+ * framework.
+ * @param date The Date object to convert.
+ * @returns The rawDate object with the day of the week (as Weekday enum) and
+ * the time in "HH:MM:SS" format.
+ */
export function toRawDate(date: Date): rawDate {
const daysOfWeek = [
"SUNDAY",
@@ -178,16 +205,13 @@ export function toRawDate(date: Date): rawDate {
return { dayOfWeek: dayOfWeek as Weekday, time: startTime };
}
-//TODO: Parse data to send to backend
-export function formatSolution2Save(items: GanttItems) {
- items
-}
-
-//TODO: Parse data for downloading
-export function format2CSV(items: GanttItems) {
- items
-}
-
+/**
+ * Finds a specific campus solution from a list of timetable solutions.
+ * @param campus - The name of the campus to search for.
+ * @param solutions - An array of timetable solutions to search within.
+ * @returns The timetable solution corresponding to the specified campus,
+ * or null if not found.
+ */
export function findCampusSolution(
campus: string,
solutions: TimetableSolution[]
diff --git a/frontend/src/tests/api.test.ts b/frontend/src/tests/api.test.ts
index 0f26572..981864e 100644
--- a/frontend/src/tests/api.test.ts
+++ b/frontend/src/tests/api.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { fetchTimetableSolution, LOCAL_API_URL, TimetableProblem } from '../scripts/api';
+import { fetchTimetableSolution, REMOTE_API_URL, TimetableProblem } from '../scripts/api';
import moment from 'moment';
import { AuthHeader } from '../security/AuthContext';
@@ -23,7 +23,7 @@ describe('fetchTimetableSolution', { timeout: 200000 }, () => {
const authHeader: AuthHeader = `Basic ${btoa(`${import.meta.env.VITE_FRONTEND_USERNAME}:${import.meta.env.VITE_FRONTEND_PASSWORD}`)}`;
- const solution = await fetchTimetableSolution(problem, authHeader, LOCAL_API_URL);
+ const solution = await fetchTimetableSolution(problem, authHeader, REMOTE_API_URL);
expect(solution).not.toBeNull();
expect(solution?.units[0].dayOfWeek).toEqual(problem.daysOfWeek[0]);
expect(solution?.units[0].startTime).toEqual(problem.startTimes[0]);
@@ -55,7 +55,7 @@ describe('fetchTimetableSolution', { timeout: 200000 }, () => {
};
const authHeader: AuthHeader = `Basic ${btoa(`${import.meta.env.VITE_FRONTEND_USERNAME}:${import.meta.env.VITE_FRONTEND_PASSWORD}`)}`;
- const solutions = await Promise.all([fetchTimetableSolution(problem0, authHeader, LOCAL_API_URL), fetchTimetableSolution(problem1, authHeader, LOCAL_API_URL)]);
+ const solutions = await Promise.all([fetchTimetableSolution(problem0, authHeader, REMOTE_API_URL), fetchTimetableSolution(problem1, authHeader, REMOTE_API_URL)]);
for (let i = 0; i < solutions.length; i++) {
expect(solutions[i]).not.toBeNull();
diff --git a/frontend/src/tests/components/BackButton.test.tsx b/frontend/src/tests/components/BackButton.test.tsx
new file mode 100644
index 0000000..a8ce94c
--- /dev/null
+++ b/frontend/src/tests/components/BackButton.test.tsx
@@ -0,0 +1,38 @@
+import { afterEach, describe, it, expect } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import BackButton from '../../components/BackButton';
+
+// Automatically clean up the DOM after each test
+afterEach(() => {
+ cleanup();
+});
+
+describe('BackButton', () => {
+ it('renders as an active button by default', () => {
+ // Render the default BackButton
+ render();
+
+ const backButton = screen.getByRole('button');
+
+ // Check if the button is in the document
+ expect(backButton).toBeInTheDocument();
+
+ // Check if the button is enabled (active by default)
+ expect(backButton).toBeEnabled();
+ });
+
+ it('renders as a disabled button with props', () => {
+ // Render the BackButton with the disabled prop set to true
+ render();
+
+ const backButton = screen.getByRole('button');
+
+ // Check if the button is in the document
+ expect(backButton).toBeInTheDocument();
+
+ // Check if the button is disabled
+ expect(backButton).toBeDisabled();
+ });
+
+});
diff --git a/frontend/src/tests/components/DisplayFile.test.tsx b/frontend/src/tests/components/DisplayFile.test.tsx
new file mode 100644
index 0000000..c896477
--- /dev/null
+++ b/frontend/src/tests/components/DisplayFile.test.tsx
@@ -0,0 +1,30 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest';
+import DisplayFile from '../../components/DisplayFile';
+
+describe('DisplayFile', () => {
+
+ it('renders an empty Box when fileChosen is null', () => {
+ const { container } = render();
+
+ // The Box is rendered, but nothing else
+ const emptyBox = container.querySelector('.MuiBox-root');
+ expect(emptyBox).toBeInTheDocument();
+ expect(emptyBox).toBeEmptyDOMElement(); // Box should be empty when fileChosen is null
+ });
+
+ it('renders the file name and FilePresentIcon when fileChosen is not null', () => {
+ const mockFile = new File([''], 'example.txt', { type: 'text/plain' });
+
+ render();
+
+ // Check that the file name is rendered
+ const fileName = screen.getByText('example.txt');
+ expect(fileName).toBeInTheDocument();
+
+ // Check that the FilePresentIcon is rendered
+ const fileIcon = screen.getByTestId('FilePresentIcon');
+ expect(fileIcon).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/tests/components/LoadingButton.test.tsx b/frontend/src/tests/components/LoadingButton.test.tsx
new file mode 100644
index 0000000..2f0e3fc
--- /dev/null
+++ b/frontend/src/tests/components/LoadingButton.test.tsx
@@ -0,0 +1,76 @@
+import { afterEach, describe, it, expect, vi } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import LoadingButton from '../../components/LoadingButton';
+import '@testing-library/jest-dom/vitest';
+import userEvent from '@testing-library/user-event';
+
+// Automatically clean up the DOM after each test
+afterEach(() => {
+ cleanup();
+});
+
+describe('LoadingButton', () => {
+
+ it('shows a disabled button with CircularProgress and no text when loading is true', () => {
+ render(
+ {}}
+ text="Submit"
+ />
+ );
+
+ // Check that the button is disabled
+ const button = screen.getByRole('button');
+ expect(button).toBeDisabled();
+
+ // Check that CircularProgress is present
+ const circularProgress = screen.getByRole('progressbar');
+ expect(circularProgress).toBeInTheDocument();
+
+ // Check that the button does not display the text "Submit"
+ expect(button).not.toHaveTextContent('Submit');
+ });
+
+ it('shows an enabled button with text and no CircularProgress when loading is false', () => {
+ render(
+ {}}
+ text="Submit"
+ />
+ );
+
+ // Check that the button is enabled
+ const button = screen.getByRole('button');
+ expect(button).toBeEnabled();
+
+ // Check that CircularProgress is not present
+ const circularProgress = screen.queryByRole('progressbar');
+ expect(circularProgress).not.toBeInTheDocument();
+
+ // Check that the button displays the text "Submit"
+ expect(button).toHaveTextContent('Submit');
+ });
+
+ it('triggers the onClick function when clicked and not loading', async () => {
+ const handleClick = vi.fn();
+
+ render(
+
+ );
+
+ // Simulate user clicking the button
+ const button = screen.getByRole('button');
+ const user = userEvent.setup();
+ await user.click(button);
+
+ // Ensure the onClick function is called
+ expect(handleClick).toHaveBeenCalledTimes(1);
+ });
+
+});
diff --git a/frontend/src/tests/components/LoadingModal.test.tsx b/frontend/src/tests/components/LoadingModal.test.tsx
new file mode 100644
index 0000000..87e238e
--- /dev/null
+++ b/frontend/src/tests/components/LoadingModal.test.tsx
@@ -0,0 +1,34 @@
+import { afterEach, describe, it, expect } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import '@testing-library/jest-dom/vitest'; // For jest-dom matchers
+import LoadingModal from '../../components/LoadingModel';
+
+// Automatically clean up the DOM after each test
+afterEach(() => {
+ cleanup();
+});
+
+describe('LoadingModal', () => {
+ it('renders the modal with CircularProgress when open is true', () => {
+ render();
+
+ // Modal should be present
+ const modal = screen.getByRole('presentation');
+ expect(modal).toBeInTheDocument();
+
+ // CircularProgress should be present inside the modal
+ const progress = screen.getByRole('progressbar');
+ expect(progress).toBeInTheDocument();
+ });
+
+ it('does not render the modal or CircularProgress when open is false', () => {
+ render();
+
+ // Modal and CircularProgress should not be present
+ const modal = screen.queryByRole('presentation');
+ expect(modal).not.toBeInTheDocument();
+
+ const progress = screen.queryByRole('progressbar');
+ expect(progress).not.toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/tests/components/ProceedButton.test.tsx b/frontend/src/tests/components/ProceedButton.test.tsx
new file mode 100644
index 0000000..f1903d1
--- /dev/null
+++ b/frontend/src/tests/components/ProceedButton.test.tsx
@@ -0,0 +1,60 @@
+import { afterEach, describe, it, expect, vi } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import ProceedButton from '../../components/ProceedButton';
+import { MemoryRouter, useNavigate } from 'react-router-dom';
+import '@testing-library/jest-dom/vitest';
+import userEvent from '@testing-library/user-event';
+
+// Automatically clean up the DOM after each test
+afterEach(() => {
+ cleanup();
+});
+
+// Mock useNavigate to test navigation behavior
+vi.mock(import('react-router-dom'), async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useNavigate: vi.fn(), // Mock useNavigate only
+ };
+});
+
+afterEach(() => {
+ vi.clearAllMocks(); // Clear mocks after each test
+});
+
+describe('ProceedButton', () => {
+ it('renders a disabled button when fileChosen is null', () => {
+ render(
+
+
+
+ );
+
+ // Check that the button is disabled
+ const button = screen.getByRole('button', { name: /proceed/i });
+ expect(button).toBeDisabled();
+ });
+
+ it('renders an enabled button and triggers navigation when fileChosen is provided', async () => {
+ const navigateMock = vi.fn();
+ vi.mocked(useNavigate).mockReturnValue(navigateMock);
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ // Check that the button is enabled
+ const button = screen.getByRole('button', { name: /proceed/i });
+ expect(button).toBeEnabled();
+
+ // Simulate clicking the button
+ await user.click(button);
+
+ // Ensure that navigate is called with the correct route
+ expect(navigateMock).toHaveBeenCalledWith('/seminfo/room');
+ });
+});
diff --git a/frontend/src/tests/components/SkipButton.test.tsx b/frontend/src/tests/components/SkipButton.test.tsx
new file mode 100644
index 0000000..cd1b7b0
--- /dev/null
+++ b/frontend/src/tests/components/SkipButton.test.tsx
@@ -0,0 +1,63 @@
+import { afterEach, describe, it, expect, vi } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import SkipButton from '../../components/SkipButton';
+import { MemoryRouter, useNavigate } from 'react-router-dom';
+import '@testing-library/jest-dom/vitest';
+import userEvent from '@testing-library/user-event';
+
+// Automatically clean up the DOM after each test
+afterEach(() => {
+ cleanup();
+});
+
+// Mock useNavigate to test navigation behavior
+vi.mock(import('react-router-dom'), async (importOriginal) => {
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ useNavigate: vi.fn(),
+ };
+});
+
+describe('SkipButton', () => {
+ afterEach(() => {
+ vi.clearAllMocks(); // Clear mocks after each test
+ sessionStorage.clear(); // Clear sessionStorage after each test
+ });
+
+ it('renders a disabled button when there are no solution in sessionStorage', () => {
+ render(
+
+
+
+ );
+
+ // Check that the button is disabled
+ const button = screen.getByRole('button', { name: /modify timetable/i });
+ expect(button).toBeDisabled();
+ });
+
+ it('renders an enabled button and triggers navigation when solution exists in sessionStorage', async () => {
+ // Set sessionStorage value
+ sessionStorage.setItem('campusSolutions', 'true');
+ const navigateMock = vi.fn();
+ vi.mocked(useNavigate).mockReturnValue(navigateMock);
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ // Check that the button is enabled
+ const button = screen.getByRole('button', { name: /modify timetable/i });
+ expect(button).toBeEnabled();
+
+ // Simulate clicking the button
+ await user.click(button);
+
+ // Ensure that navigate is called with the correct route
+ expect(navigateMock).toHaveBeenCalledWith('/timetablemod');
+ });
+});
diff --git a/frontend/src/tests/components/UploadPopUp.test.tsx b/frontend/src/tests/components/UploadPopUp.test.tsx
new file mode 100644
index 0000000..e36061a
--- /dev/null
+++ b/frontend/src/tests/components/UploadPopUp.test.tsx
@@ -0,0 +1,119 @@
+import { afterEach, describe, it, expect, vi, afterAll, beforeAll } from 'vitest';
+import { render, screen, cleanup } from '@testing-library/react';
+import UploadPopUp from '../../components/UploadPopUp';
+import { MemoryRouter } from 'react-router-dom';
+import '@testing-library/jest-dom/vitest';
+import userEvent from '@testing-library/user-event';
+
+// Automatically clean up the DOM after each test
+afterEach(() => {
+ cleanup();
+});
+
+describe('UploadPopUp', () => {
+
+ beforeAll(() => {
+ globalThis.alert = vi.fn(); // Mock alert
+ });
+
+ afterAll(() => {
+ vi.restoreAllMocks(); // Restore original alert after tests
+ });
+
+
+ it('renders the "Generate Timetable" button', () => {
+ render(
+
+
+
+ );
+
+ // Check if the button to open the modal is rendered
+ const generateButton = screen.getByRole('button', { name: /generate timetable/i });
+ expect(generateButton).toBeInTheDocument();
+ });
+
+ it('opens the modal when "Generate Timetable" button is clicked', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ // Click the "Generate Timetable" button
+ const generateButton = screen.getByRole('button', { name: /generate timetable/i });
+ await user.click(generateButton);
+
+ // Check if the modal elements are visible
+ expect(screen.getByText(/upload file/i)).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /proceed/i })).toBeInTheDocument();
+ });
+
+ it('closes the modal when clicking outside', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ // Open the modal
+ const generateButton = screen.getByRole('button', { name: /generate timetable/i });
+ await user.click(generateButton);
+
+ // Click outside the modal (backdrop)
+ const backdrop = screen.getByText(/upload file/i).closest('.base-Modal-root')?.querySelector('.base-Backdrop-open');
+ if (backdrop)
+ {
+ await user.click(backdrop);
+ }
+
+ // Ensure modal elements are no longer in the DOM
+ expect(screen.queryByText(/upload file/i)).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /proceed/i })).not.toBeInTheDocument();
+ });
+
+ it('renders disabled ProceedButton when no file is selected', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ // Open the modal
+ const generateButton = screen.getByRole('button', { name: /generate timetable/i });
+ await user.click(generateButton);
+
+ // Ensure Proceed button is disabled initially
+ const proceedButton = screen.getByRole('button', { name: /proceed/i });
+ expect(proceedButton).toBeDisabled();
+ });
+
+ it('does not enable ProceedButton when an invalid file is selected', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+
+ );
+
+ // Open the modal
+ const generateButton = screen.getByRole('button', { name: /generate timetable/i });
+ await user.click(generateButton);
+
+ // Simulate file selection
+ const file = new File(['file content'], 'example.txt', { type: 'text/plain' });
+ const uploadButton = screen.getByText(/upload file/i); // Assuming the text is in UploadButton
+ await user.upload(uploadButton, file);
+
+ // Check that Proceed button is now enabled
+ const proceedButton = screen.getByRole('button', { name: /proceed/i });
+ expect(proceedButton).not.toBeEnabled();
+ });
+});
diff --git a/frontend/src/tests/security.test.ts b/frontend/src/tests/security.test.ts
new file mode 100644
index 0000000..584b710
--- /dev/null
+++ b/frontend/src/tests/security.test.ts
@@ -0,0 +1,42 @@
+import { describe, it, expect } from 'vitest';
+import { REMOTE_API_URL } from '../scripts/api';
+import { AuthHeader } from '../security/AuthContext';
+
+
+/**
+ * Test Basic Authentication with backend.
+ */
+describe('authentication', { timeout: 200000 }, () => {
+
+ it('allow access given correct username and password', async () => {
+
+ const authHeader: AuthHeader = `Basic ${btoa(`${import.meta.env.VITE_FRONTEND_USERNAME}:${import.meta.env.VITE_FRONTEND_PASSWORD}`)}`;
+ const response = await fetch(REMOTE_API_URL + "/login", {
+ method: 'GET',
+ headers: {
+ 'Authorization': authHeader,
+ },
+ });
+
+ expect(response.status).not.equal(401);
+
+ const text = await response.text();
+ expect(text).equal("jetedge");
+
+ });
+
+ it('does not allow access given wrong username or password', async () => {
+
+ const authHeader: AuthHeader = '';
+ const response = await fetch(REMOTE_API_URL + "/login", {
+ method: 'GET',
+ headers: {
+ 'Authorization': authHeader,
+ },
+ });
+
+ expect(response.status).equal(401);
+
+ });
+
+});
\ No newline at end of file
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
new file mode 100644
index 0000000..16950e3
--- /dev/null
+++ b/frontend/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ test: {
+ environment: 'jsdom',
+ }
+});
\ No newline at end of file
diff --git a/tests/Integration Testing.pdf b/tests/Integration Testing.pdf
new file mode 100644
index 0000000..00b04a0
Binary files /dev/null and b/tests/Integration Testing.pdf differ
diff --git a/tests/Security Testing.pdf b/tests/Security Testing.pdf
new file mode 100644
index 0000000..4657a12
Binary files /dev/null and b/tests/Security Testing.pdf differ
diff --git a/tests/System Testing.pdf b/tests/System Testing.pdf
new file mode 100644
index 0000000..87a43e9
Binary files /dev/null and b/tests/System Testing.pdf differ
diff --git a/tests/Unit Testing (Backend).pdf b/tests/Unit Testing (Backend).pdf
new file mode 100644
index 0000000..68d6e16
Binary files /dev/null and b/tests/Unit Testing (Backend).pdf differ
diff --git a/tests/Unit Testing (Frontend).pdf b/tests/Unit Testing (Frontend).pdf
new file mode 100644
index 0000000..c6b2a11
Binary files /dev/null and b/tests/Unit Testing (Frontend).pdf differ
diff --git a/tests/User Acceptance Testing.pdf b/tests/User Acceptance Testing.pdf
new file mode 100644
index 0000000..4435f8e
Binary files /dev/null and b/tests/User Acceptance Testing.pdf differ