diff --git a/course-matrix/backend/__tests__/auth.test.ts b/course-matrix/backend/__tests__/auth.test.ts new file mode 100644 index 00000000..6332e27b --- /dev/null +++ b/course-matrix/backend/__tests__/auth.test.ts @@ -0,0 +1,81 @@ +import request from "supertest"; +import { describe, expect, it, test } from "@jest/globals"; +import app from "../src/index"; + +describe("Authentication API", () => { + // The unit tests below are currently commented out because they require a database connection. + // They will be uncommented out once all the necessary mocks are in place. + + // describe('POST /auth/login', () => { + // it('should return 200 and a token for valid credentials', async () => { + // const response = await request(app) + // .post('/auth/login') + // .send({ username: 'validUser', password: 'validPassword' }); + // expect(response.status).toBe(200); + // expect(response.body).toHaveProperty('token'); + // }); + // it('should return 401 for invalid credentials', async () => { + // const response = await request(app) + // .post('/auth/login') + // .send({ username: 'invalidUser', password: 'wrongPassword' }); + // expect(response.status).toBe(401); + // expect(response.body).toHaveProperty('error', 'Invalid credentials'); + // }); + // it('should return 400 if username or password is missing', async () => { + // const response = await request(app) + // .post('/auth/login') + // .send({ username: 'validUser' }); + // expect(response.status).toBe(400); + // expect(response.body).toHaveProperty('error', 'Username and password are required'); + // }); + // }); + // describe('POST /auth/register', () => { + // it('should return 201 and create a new user for valid input', async () => { + // const response = await request(app) + // .post('/auth/register') + // .send({ username: 'newUser', password: 'newPassword' }); + // expect(response.status).toBe(201); + // expect(response.body).toHaveProperty('message', 'User registered successfully'); + // }); + // it('should return 400 if username is already taken', async () => { + // await request(app) + // .post('/auth/register') + // .send({ username: 'existingUser', password: 'password123' }); + // const response = await request(app) + // .post('/auth/register') + // .send({ username: 'existingUser', password: 'password123' }); + // expect(response.status).toBe(400); + // expect(response.body).toHaveProperty('error', 'Username is already taken'); + // }); + // it('should return 400 if username or password is missing', async () => { + // const response = await request(app) + // .post('/auth/register') + // .send({ username: '' }); + // expect(response.status).toBe(400); + // expect(response.body).toHaveProperty('error', 'Username and password are required'); + // }); + // }); + // describe('GET /auth/profile', () => { + // it('should return 200 and user profile for valid token', async () => { + // const loginResponse = await request(app) + // .post('/auth/login') + // .send({ username: 'validUser', password: 'validPassword' }); + // const token = loginResponse.body.token; + // const response = await request(app) + // .get('/auth/profile') + // .set('Authorization', `Bearer ${token}`); + // expect(response.status).toBe(200); + // expect(response.body).toHaveProperty('username', 'validUser'); + // }); + // it('should return 401 if token is missing or invalid', async () => { + // const response = await request(app) + // .get('/auth/profile') + // .set('Authorization', 'Bearer invalidToken'); + // expect(response.status).toBe(401); + // expect(response.body).toHaveProperty('error', 'Unauthorized'); + // }); + // }); + it("template test", () => { + expect(2 + 3).toEqual(5); + }); +}); diff --git a/course-matrix/backend/__tests__/index.test.ts b/course-matrix/backend/__tests__/index.test.ts index 874e1000..28f3930f 100644 --- a/course-matrix/backend/__tests__/index.test.ts +++ b/course-matrix/backend/__tests__/index.test.ts @@ -7,12 +7,3 @@ describe("Sum function", () => { expect(2 + 3).toEqual(5); }); }); - -// Will finish the rest of the tests below in Sprint 3 - -// describe("GET /auth/session", () => { -// test("It should respond with 200 status", async () => { -// const response = await request(app).get("/auth/session"); -// expect(response.statusCode).toBe(200); -// }); -// }); diff --git a/course-matrix/frontend/__tests__/UserMenu.test.tsx b/course-matrix/frontend/__tests__/UserMenu.test.tsx deleted file mode 100644 index fb07ec06..00000000 --- a/course-matrix/frontend/__tests__/UserMenu.test.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import configureStore from "redux-mock-store"; -import { UserMenu } from "../src/components/UserMenu"; -import { afterEach, beforeEach, describe, expect, test } from "@jest/globals"; - -// const mockStore = configureStore([]); - -describe("UserMenu Component", () => { - beforeEach(() => { - localStorage.setItem( - "userInfo", - JSON.stringify({ - user: { - id: "123", - user_metadata: { - username: "John Doe", - email: "john.doe@example.com", - }, - }, - }), - ); - }); - - afterEach(() => { - localStorage.clear(); - }); - - test("check local storage", () => { - expect(localStorage.getItem("userInfo")).not.toBeNull(); - }); - - // Will finish the rest of the tests below in Sprint 3 - - // test("opens edit account dialog", () => { - // render( - // - // - // - // - // , - // ); - - // fireEvent.click(screen.getByText("John Doe")); - // fireEvent.click(screen.getByText("Edit Account")); - - // expect(screen.getByText("Edit Account")).toBeInTheDocument(); - // expect(screen.getByLabelText("New User Name")).toBeInTheDocument(); - // }); - - // test("opens delete account dialog", () => { - // render( - // - // - // - // - // , - // ); - - // fireEvent.click(screen.getByText("John Doe")); - // fireEvent.click(screen.getByText("Delete Account")); - - // expect(screen.getByText("Delete Account")).toBeInTheDocument(); - // expect( - // screen.getByText( - // "Are you sure you want to delete your account? This action cannot be undone.", - // ), - // ).toBeInTheDocument(); - // }); - - // test("logs out user", () => { - // render( - // - // - // - // - // , - // ); - - // fireEvent.click(screen.getByText("John Doe")); - // fireEvent.click(screen.getByText("Logout")); - - // // Add assertions to check if the user is logged out - // }); -}); diff --git a/course-matrix/frontend/__tests__/integration-tests/integration-tests.test.tsx b/course-matrix/frontend/__tests__/integration-tests/integration-tests.test.tsx new file mode 100644 index 00000000..a7cb7171 --- /dev/null +++ b/course-matrix/frontend/__tests__/integration-tests/integration-tests.test.tsx @@ -0,0 +1,128 @@ +import "@testing-library/jest-dom/jest-globals"; +import "@testing-library/jest-dom"; + +import { render, screen, fireEvent } from "@testing-library/react"; +import React from "react"; +import { MemoryRouter } from "react-router-dom"; +import { expect, test } from "@jest/globals"; +import App from "../../src/App"; +import SignupPage from "../../src/pages/Signup/SignUpPage"; +import LoginPage from "../../src/pages/Login/LoginPage"; +import Dashboard from "../../src/pages/Dashboard/Dashboard"; +import TimetableBuilder from "../../src/pages/TimetableBuilder/TimetableBuilder"; +import Home from "../../src/pages/Home/Home"; + +test("typical flow for creating an account and logging in", () => { + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + + // Going to login page + expect(screen.getByText("Login")).toBeInTheDocument(); + fireEvent.click(screen.getByText("Login")); + + // Logging in with invalid credentials + fireEvent.change(screen.getByLabelText("Email"), { target: { value: "" } }); + fireEvent.change(screen.getByLabelText("Password"), { + target: { value: "password" }, + }); + fireEvent.click(screen.getByText("Login")); + expect( + screen.getByText("Login invalid. Please check your email or password."), + ).toBeInTheDocument(); + + // Going to signup page + fireEvent.click(screen.getByText("Sign up!")); + expect(screen.getByText("Sign up")).toBeInTheDocument(); + fireEvent.change(screen.getByLabelText("Email"), { target: { value: "" } }); + fireEvent.change(screen.getByLabelText("Password"), { + target: { value: "" }, + }); + fireEvent.click(screen.getByText("Sign up")); + expect(screen.getByText("Email is required")).toBeInTheDocument(); + expect(screen.getByText("Password is required")).toBeInTheDocument(); + + // Creating an account + fireEvent.change(screen.getByLabelText("Email"), { + target: { value: "test@example.com" }, + }); + fireEvent.change(screen.getByLabelText("Password"), { + target: { value: "password123" }, + }); + fireEvent.click(screen.getByText("Sign up")); + expect(screen.getByText("Account created successfully!")).toBeInTheDocument(); + + // Logging in with valid credentials + fireEvent.change(screen.getByLabelText("Email"), { + target: { value: "test@example.com" }, + }); + fireEvent.change(screen.getByLabelText("Password"), { + target: { value: "password123" }, + }); + fireEvent.click(screen.getByText("Login")); + + // Check if user is redirected to home page + expect(screen.getByText("Home Page")).toBeInTheDocument(); +}); + +test("typical flow for creating a new timetable, adding courses, adding restrictions, saving the timetable, and editing the timetable", () => { + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + render(, { wrapper: MemoryRouter }); + + expect(screen.getByText("My Timetables")).toBeInTheDocument(); + fireEvent.click(screen.getByText("Create New")); + + expect(screen.getByText("New Timetable")).toBeInTheDocument(); + + // Adding courses + fireEvent.click(screen.getByText("Choose meeting sections manually")); + fireEvent.click(screen.getByText("Search")); + fireEvent.click(screen.getByText("ACMB10H3")); + fireEvent.click(screen.getByText("ACMC01H3")); + fireEvent.click(screen.getByText("ACMD01H3")); + screen.getAllByText("No LEC Selected").forEach((element) => { + fireEvent.click(element); + fireEvent.click(screen.getByText("LEC 01")); + }); + + // Adding a restriction + fireEvent.click(screen.getByText("Add New")); + fireEvent.click(screen.getByText("Select a type")); + fireEvent.click(screen.getByText("Restrict Entire Day")); + fireEvent.click(screen.getByText("Tuesday")); + fireEvent.click(screen.getByText("Create")); + + // Saving the timetable + fireEvent.click(screen.getByText("Create Timetable")); + fireEvent.change(screen.getByLabelText("Timetable Name"), { + target: { value: "Test Timetable Name" }, + }); + fireEvent.click(screen.getByText("Save")); + + // Check if user is redirected to home page and timetable is saved + expect(screen.getByText("My Timetables")).toBeInTheDocument(); + expect(screen.getByText("Test Timetable Name")).toBeInTheDocument(); + + // Editing the timetable by resetting all courses and restrictions + fireEvent.click(screen.getByText("Test Timetable Name")); + expect(screen.getByText("Edit Timetable")).toBeInTheDocument(); + fireEvent.click(screen.getByText("Reset")); + expect(screen.queryByText("ACMB10H3")).toBeNull(); + expect(screen.queryByText("ACMC01H3")).toBeNull(); + expect(screen.queryByText("ACMD01H3")).toBeNull(); + expect(screen.queryByText("Tuesday")).toBeNull(); + fireEvent.click(screen.getByText("Update Timetable")); + + // Check if user is redirected to home page and timetable is updated + expect(screen.getByText("My Timetables")).toBeInTheDocument(); + fireEvent.click(screen.getByText("Test Timetable Name")); + expect(screen.queryByText("ACMB10H3")).toBeNull(); + expect(screen.queryByText("ACMC01H3")).toBeNull(); + expect(screen.queryByText("ACMD01H3")).toBeNull(); + expect(screen.queryByText("Tuesday")).toBeNull(); +}); diff --git a/course-matrix/frontend/__tests__/unit-tests/TimetableBuilder.test.tsx b/course-matrix/frontend/__tests__/unit-tests/TimetableBuilder.test.tsx new file mode 100644 index 00000000..9a97f01a --- /dev/null +++ b/course-matrix/frontend/__tests__/unit-tests/TimetableBuilder.test.tsx @@ -0,0 +1,196 @@ +import "@testing-library/jest-dom/jest-globals"; +import "@testing-library/jest-dom"; + +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import { BrowserRouter } from "react-router-dom"; +import { + beforeEach, + describe, + vi, + Mock, + afterEach, + it, + expect, + MockedFunction, +} from "vitest"; +import TimetableBuilder from "../../src/pages/TimetableBuilder/TimetableBuilder"; +import { useForm, UseFormWatch } from "react-hook-form"; +import { z } from "zod"; +import React from "react"; + +vi.mock("react-hook-form", () => ({ + useForm: vi.fn(), +})); + +vi.mock("@/api/coursesApiSlice", () => ({ + useGetCoursesQuery: vi.fn(() => ({ data: [], isLoading: false })), +})); + +vi.mock("@/api/timetableApiSlice", () => ({ + useGetTimetablesQuery: vi.fn(() => ({ data: [] })), +})); + +vi.mock("@/api/eventsApiSlice", () => ({ + useGetEventsQuery: vi.fn(() => ({ data: null })), +})); + +vi.mock("@/api/offeringsApiSlice", () => ({ + useGetOfferingsQuery: vi.fn(() => ({ data: [] })), +})); + +vi.mock("@/api/restrictionsApiSlice", () => ({ + useGetRestrictionsQuery: vi.fn(() => ({ data: [] })), +})); + +vi.mock("@/utils/useDebounce", () => ({ + useDebounceValue: (value: string) => value, +})); + +describe("TimetableBuilder", () => { + const mockSetValue = vi.fn(); + const mockHandleSubmit = vi.fn((fn) => fn); + + beforeEach(() => { + (useForm as Mock).mockReturnValue({ + control: {}, + handleSubmit: mockHandleSubmit, + setValue: mockSetValue, + watch: vi.fn(() => { + const result: { unsubscribe: () => void } & any[] = []; + result.unsubscribe = () => {}; + return result; + }) as unknown as UseFormWatch, + reset: vi.fn(), + getValues: vi.fn(() => []), + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("renders the TimetableBuilder component", () => { + render( + + + , + ); + + expect(screen.getByText(/New Timetable/i)).toBeInTheDocument(); + expect( + screen.getByText(/Pick a few courses you'd like to take/i), + ).toBeInTheDocument(); + }); + + it("calls the reset function when the Reset button is clicked", () => { + const mockReset = vi.fn(); + (useForm as MockedFunction).mockReturnValue({ + reset: mockReset, + handleSubmit: mockHandleSubmit, + setValue: mockSetValue, + watch: vi.fn(() => { + const result: unknown = []; + result.unsubscribe = () => {}; + return result; + }), + getValues: vi.fn(() => []), + }); + + render( + + + , + ); + + const resetButton = screen.getByText(/Reset/i); + fireEvent.click(resetButton); + + expect(mockReset).toHaveBeenCalled(); + }); + + it("opens the custom settings modal when the Add new button is clicked", () => { + render( + + + , + ); + + const addNewButton = screen.getByText(/\+ Add new/i); + fireEvent.click(addNewButton); + + expect(screen.getByText(/Custom Settings/i)).toBeInTheDocument(); + }); + + it("displays selected courses when courses are added", async () => { + const mockWatch = vi.fn(() => [ + { id: 1, code: "CS101", name: "Introduction to Computer Science" }, + ]); + (useForm as vi.Mock).mockReturnValue({ + watch: mockWatch, + handleSubmit: mockHandleSubmit, + setValue: mockSetValue, + reset: vi.fn(), + getValues: vi.fn(() => []), + }); + + render( + + + , + ); + + expect(screen.getByText(/Selected courses: 1/i)).toBeInTheDocument(); + expect(screen.getByText(/CS101/i)).toBeInTheDocument(); + }); + + it("removes a course when the remove button is clicked", async () => { + const mockWatch = vi.fn(() => [ + { id: 1, code: "CS101", name: "Introduction to Computer Science" }, + ]); + const mockSetValue = vi.fn(); + (useForm as vi.Mock).mockReturnValue({ + watch: mockWatch, + handleSubmit: mockHandleSubmit, + setValue: mockSetValue, + reset: vi.fn(), + getValues: vi.fn(() => [ + { id: 1, code: "CS101", name: "Introduction to Computer Science" }, + ]), + }); + + render( + + + , + ); + + const removeButton = screen.getByRole("button", { name: /Remove/i }); + fireEvent.click(removeButton); + + await waitFor(() => { + expect(mockSetValue).toHaveBeenCalledWith("courses", []); + }); + }); + + it("submits the form when the Generate button is clicked", () => { + const mockSubmit = vi.fn(); + (useForm as vi.Mock).mockReturnValue({ + handleSubmit: vi.fn((fn) => fn), + setValue: mockSetValue, + watch: vi.fn(() => []), + reset: vi.fn(), + getValues: vi.fn(() => []), + }); + + render( + + + , + ); + + const generateButton = screen.getByText(/Generate/i); + fireEvent.click(generateButton); + + expect(mockHandleSubmit).toHaveBeenCalled(); + }); +}); diff --git a/course-matrix/frontend/__tests__/unit-tests/UserMenu.test.tsx b/course-matrix/frontend/__tests__/unit-tests/UserMenu.test.tsx new file mode 100644 index 00000000..0e094802 --- /dev/null +++ b/course-matrix/frontend/__tests__/unit-tests/UserMenu.test.tsx @@ -0,0 +1,88 @@ +import "@testing-library/jest-dom/jest-globals"; +import "@testing-library/jest-dom"; + +import configureStore from "redux-mock-store"; +import { UserMenu } from "../../src/components/UserMenu"; +import { afterEach, beforeEach, describe, expect, test } from "@jest/globals"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { Router } from "lucide-react"; +import React from "react"; +import { Provider } from "react-redux"; + +const mockStore = configureStore([]); + +describe("UserMenu Component", () => { + beforeEach(() => { + localStorage.setItem( + "userInfo", + JSON.stringify({ + user: { + id: "123", + user_metadata: { + username: "John Doe", + email: "john.doe@example.com", + }, + }, + }), + ); + }); + + afterEach(() => { + localStorage.clear(); + }); + + test("check local storage", () => { + expect(localStorage.getItem("userInfo")).not.toBeNull(); + }); + + test("opens edit account dialog", () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByText("John Doe")); + fireEvent.click(screen.getByText("Edit Account")); + + expect(screen.getByText("Edit Account")).toBeInTheDocument(); + expect(screen.getByLabelText("New User Name")).toBeInTheDocument(); + }); + + test("opens delete account dialog", () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByText("John Doe")); + fireEvent.click(screen.getByText("Delete Account")); + + expect(screen.getByText("Delete Account")).toBeInTheDocument(); + expect( + screen.getByText( + "Are you sure you want to delete your account? This action cannot be undone.", + ), + ).toBeInTheDocument(); + }); + + test("logs out user", () => { + render( + + + + + , + ); + + fireEvent.click(screen.getByText("John Doe")); + fireEvent.click(screen.getByText("Logout")); + + expect(localStorage.getItem("userInfo")).toBeNull(); + }); +}); diff --git a/course-matrix/frontend/jest.config.ts b/course-matrix/frontend/jest.config.ts index 93fb9522..00bf9415 100644 --- a/course-matrix/frontend/jest.config.ts +++ b/course-matrix/frontend/jest.config.ts @@ -2,7 +2,7 @@ import type { Config } from "jest"; const config: Config = { preset: "ts-jest", - moduleNameMapper: { "\\.(css|scss)$": "identity-obj-proxy" }, + moduleNameMapper: { "@/(.*)$": "/src/$1" }, // to obtain access to the matchers. moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], modulePaths: [""], @@ -16,6 +16,12 @@ const config: Config = { ], "^.+\\.(js|jsx)$": "babel-jest", }, + modulePathIgnorePatterns: [ + "/dist/", + "/node_modules/", + "/__tests__/integration-tests/", + "/__tests__/unit-tests/", + ], }; export default config; diff --git a/course-matrix/frontend/package-lock.json b/course-matrix/frontend/package-lock.json index a58e5947..7701f627 100644 --- a/course-matrix/frontend/package-lock.json +++ b/course-matrix/frontend/package-lock.json @@ -74,7 +74,8 @@ "ts-node": "^10.9.2", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", - "vite": "^6.0.5" + "vite": "^6.0.5", + "vitest": "^3.0.9" } }, "node_modules/@adobe/css-tools": { @@ -3993,6 +3994,112 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, + "node_modules/@vitest/expect": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.9.tgz", + "integrity": "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.0.9", + "@vitest/utils": "3.0.9", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.9.tgz", + "integrity": "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.0.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.9.tgz", + "integrity": "sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.9.tgz", + "integrity": "sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.0.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.9.tgz", + "integrity": "sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.0.9", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.9.tgz", + "integrity": "sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.9.tgz", + "integrity": "sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.0.9", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -4187,6 +4294,15 @@ "dequal": "^2.0.3" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/assistant-stream": { "version": "0.0.21", "resolved": "https://registry.npmjs.org/assistant-stream/-/assistant-stream-0.0.21.tgz", @@ -4475,6 +4591,15 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4543,6 +4668,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4604,6 +4745,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4984,6 +5134,15 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5205,6 +5364,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5494,6 +5659,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -5565,6 +5739,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/expect-type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.0.tgz", + "integrity": "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7634,6 +7817,12 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7661,6 +7850,15 @@ "lz-string": "bin/bin.js" } }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -8909,6 +9107,21 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9825,6 +10038,12 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -9914,6 +10133,18 @@ "node": ">=8" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "dev": true + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -10287,6 +10518,45 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -10911,6 +11181,97 @@ } } }, + "node_modules/vite-node": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.9.tgz", + "integrity": "sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", + "integrity": "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "3.0.9", + "@vitest/mocker": "3.0.9", + "@vitest/pretty-format": "^3.0.9", + "@vitest/runner": "3.0.9", + "@vitest/snapshot": "3.0.9", + "@vitest/spy": "3.0.9", + "@vitest/utils": "3.0.9", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.9", + "@vitest/ui": "3.0.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -10989,6 +11350,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/course-matrix/frontend/package.json b/course-matrix/frontend/package.json index 23872a66..9030101e 100644 --- a/course-matrix/frontend/package.json +++ b/course-matrix/frontend/package.json @@ -78,6 +78,7 @@ "ts-node": "^10.9.2", "typescript": "~5.6.2", "typescript-eslint": "^8.18.2", - "vite": "^6.0.5" + "vite": "^6.0.5", + "vitest": "^3.0.9" } } diff --git a/course-matrix/frontend/tsconfig.json b/course-matrix/frontend/tsconfig.json index fec8c8e5..586ee7ac 100644 --- a/course-matrix/frontend/tsconfig.json +++ b/course-matrix/frontend/tsconfig.json @@ -8,6 +8,8 @@ "baseUrl": ".", "paths": { "@/*": ["./src/*"] - } + }, + "jsx": "react-jsx", + "types": ["@testing-library/jest-dom"] } } diff --git a/course-matrix/frontend/tsconfig.test.json b/course-matrix/frontend/tsconfig.test.json index 05992782..dde8fcf3 100644 --- a/course-matrix/frontend/tsconfig.test.json +++ b/course-matrix/frontend/tsconfig.test.json @@ -6,6 +6,10 @@ "esModuleInterop": true, "skipLibCheck": true, "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, "jsx": "react-jsx", "types": ["jest", "@testing-library/jest-dom"] },