diff --git a/react-ystemandchess/src/components/footer/Footer.test.tsx b/react-ystemandchess/src/components/footer/Footer.test.tsx
new file mode 100644
index 00000000..85de065e
--- /dev/null
+++ b/react-ystemandchess/src/components/footer/Footer.test.tsx
@@ -0,0 +1,44 @@
+import { render, screen } from "@testing-library/react";
+import Footer from "./Footer";
+
+test("renders social media links with correct hrefs", () => {
+ render();
+
+ const twitter = screen.getByAltText("twitter-icon");
+ const instagram = screen.getByAltText("instagram-icon");
+ const facebook = screen.getByAltText("facebook-icon");
+ const google = screen.getByAltText("google-icon");
+
+ expect(twitter).toBeInTheDocument();
+ expect(instagram).toBeInTheDocument();
+ expect(facebook).toBeInTheDocument();
+ expect(google).toBeInTheDocument();
+
+ expect(twitter.closest("a")?.getAttribute("href")).toBe("/");
+ expect(instagram.closest("a")?.getAttribute("href")).toBe(
+ "https://www.instagram.com/stemwithstemy/",
+ );
+ expect(facebook.closest("a")?.getAttribute("href")).toBe(
+ "https://www.facebook.com/YSTEMandChess/",
+ );
+ expect(google.closest("a")?.getAttribute("href")).toBe("/");
+});
+
+test("renders sponsor logos", () => {
+ render();
+
+ expect(screen.getByAltText("ventive-logo")).toBeInTheDocument();
+ expect(screen.getByAltText("kount-logo")).toBeInTheDocument();
+ expect(screen.getByAltText("idahoCentral-logo")).toBeInTheDocument();
+ expect(screen.getByAltText("PH-logo")).toBeInTheDocument();
+});
+
+test("renders partner logos", () => {
+ render();
+
+ expect(screen.getByAltText("boiseRescue-logo")).toBeInTheDocument();
+ expect(screen.getByAltText("boysAndGirls-logo")).toBeInTheDocument();
+ expect(screen.getByAltText("possible-logo")).toBeInTheDocument();
+ expect(screen.getByAltText("boiseDistrict-logo")).toBeInTheDocument();
+ expect(screen.getByAltText("rotary-logo")).toBeInTheDocument();
+});
diff --git a/react-ystemandchess/src/components/footer/Footer.tsx b/react-ystemandchess/src/components/footer/Footer.tsx
index 7dee8d3d..88520ac8 100644
--- a/react-ystemandchess/src/components/footer/Footer.tsx
+++ b/react-ystemandchess/src/components/footer/Footer.tsx
@@ -1,9 +1,9 @@
/**
* Footer Component
- *
+ *
* This component displays the website footer with contact information,
* social media links, sponsors, and partners.
- *
+ *
* Features:
* - Contact information (email and phone)
* - Social media icon links
@@ -42,14 +42,14 @@ const Footer = () => {
-
+
-
+
diff --git a/react-ystemandchess/src/components/navbar/NavBar.test.tsx b/react-ystemandchess/src/components/navbar/NavBar.test.tsx
new file mode 100644
index 00000000..f28ffaf8
--- /dev/null
+++ b/react-ystemandchess/src/components/navbar/NavBar.test.tsx
@@ -0,0 +1,185 @@
+import React from "react";
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import { MemoryRouter } from "react-router";
+import NavBar from "./NavBar";
+
+jest.mock("../../globals", () => ({
+ SetPermissionLevel: jest.fn(),
+}));
+
+jest.mock("react-cookie", () => ({
+ useCookies: jest.fn(),
+}));
+
+jest.mock("framer-motion", () => {
+ const React = require("react");
+ return {
+ motion: {
+ div: ({ children, ...rest }: any) => {children}
,
+ },
+ };
+});
+
+jest.mock("@fortawesome/react-fontawesome", () => ({
+ FontAwesomeIcon: (props: any) => ,
+}));
+
+jest.mock(
+ "react-router-dom",
+ () => {
+ const React = require("react");
+ return {
+ Link: ({ to, children, ...rest }: any) => (
+
+ {children}
+
+ ),
+ };
+ },
+ { virtual: true },
+);
+
+import { SetPermissionLevel } from "../../globals";
+import { useCookies } from "react-cookie";
+
+describe("NavBar", () => {
+ const mockedSetPermissionLevel = SetPermissionLevel as jest.Mock;
+ const mockedUseCookies = useCookies as jest.Mock;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const renderNavBar = () =>
+ render(
+
+
+ ,
+ );
+
+ test("renders Login link when user is logged out", async () => {
+ mockedUseCookies.mockReturnValue([{}, jest.fn(), jest.fn()]);
+ mockedSetPermissionLevel.mockResolvedValue({ error: "unauthenticated" });
+
+ renderNavBar();
+
+ expect(await screen.findByText(/Login/i)).toBeInTheDocument();
+ expect(screen.queryByText(/Alice/i)).toBeNull();
+ });
+
+ test("renders username and hides Login when user is logged in", async () => {
+ mockedUseCookies.mockReturnValue([
+ { login: "mockToken" },
+ jest.fn(),
+ jest.fn(),
+ ]);
+ mockedSetPermissionLevel.mockResolvedValue({
+ username: "Alice",
+ role: "student",
+ });
+
+ renderNavBar();
+
+ const usernameButton = await screen.findByRole("button", {
+ name: /Alice/i,
+ });
+ expect(screen.queryByText(/Login/i)).toBeNull();
+
+ fireEvent.click(usernameButton);
+ const profileLink = await screen.findByText(/Profile/i);
+ expect(profileLink.closest("a")).toHaveAttribute(
+ "href",
+ "/student-profile",
+ );
+ });
+
+ test("shows Add Student in profile dropdown for parent role", async () => {
+ mockedUseCookies.mockReturnValue([
+ { login: "mockToken" },
+ jest.fn(),
+ jest.fn(),
+ ]);
+ mockedSetPermissionLevel.mockResolvedValue({
+ username: "Bob",
+ role: "parent",
+ });
+
+ renderNavBar();
+
+ const usernameButton = await screen.findByRole("button", {
+ name: /Bob/i,
+ });
+ fireEvent.click(usernameButton);
+
+ const addStudentLink = await screen.findByText(/Add Student/i);
+ expect(addStudentLink.closest("a")).toHaveAttribute(
+ "href",
+ "/parent-add-student",
+ );
+ });
+
+ test("toggles About Us dropdown open and closes on outside click", async () => {
+ mockedUseCookies.mockReturnValue([{}, jest.fn(), jest.fn()]);
+ mockedSetPermissionLevel.mockResolvedValue({ error: "unauthenticated" });
+
+ renderNavBar();
+
+ const aboutUsTrigger = screen.getByText(/About Us/i);
+ fireEvent.click(aboutUsTrigger);
+
+ expect(
+ await screen.findByText(/Benefit of Computer Science/i),
+ ).toBeInTheDocument();
+
+ fireEvent.mouseDown(document.body);
+
+ await waitFor(() =>
+ expect(screen.queryByText(/Benefit of Computer Science/i)).toBeNull(),
+ );
+ });
+
+ test("mobile menu toggles navigation visibility", async () => {
+ mockedUseCookies.mockReturnValue([{}, jest.fn(), jest.fn()]);
+ mockedSetPermissionLevel.mockResolvedValue({ error: "unauthenticated" });
+
+ renderNavBar();
+
+ const navsBefore = screen.getAllByRole("navigation").length;
+ const toggleBtn = screen.getByRole("button", { name: /toggle menu/i });
+
+ fireEvent.click(toggleBtn);
+ const navsOpen = screen.getAllByRole("navigation").length;
+ expect(navsOpen).toBeGreaterThan(navsBefore);
+
+ fireEvent.click(toggleBtn);
+ const navsClosed = screen.getAllByRole("navigation").length;
+ expect(navsClosed).toBe(navsBefore);
+ });
+
+ test("logout removes cookies and redirects to /login", async () => {
+ const removeCookieMock = jest.fn();
+ mockedUseCookies.mockReturnValue([
+ { login: "mockToken" },
+ jest.fn(),
+ removeCookieMock,
+ ]);
+ mockedSetPermissionLevel.mockResolvedValue({
+ username: "Alice",
+ role: "student",
+ });
+
+ renderNavBar();
+
+ const usernameButton = await screen.findByRole("button", {
+ name: /Alice/i,
+ });
+ fireEvent.click(usernameButton);
+
+ const logoutBtn = await screen.findByText(/Log Out/i);
+ fireEvent.click(logoutBtn);
+
+ expect(removeCookieMock).toHaveBeenCalledWith("login");
+ expect(removeCookieMock).toHaveBeenCalledWith("eventId");
+ expect(removeCookieMock).toHaveBeenCalledWith("timerStatus");
+ });
+});
diff --git a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useChessSocket.test.tsx b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useChessSocket.test.tsx
index 9eb4780e..c1060a39 100644
--- a/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useChessSocket.test.tsx
+++ b/react-ystemandchess/src/features/lessons/piece-lessons/lesson-overlay/hooks/useChessSocket.test.tsx
@@ -1,28 +1,49 @@
-import { render } from "@testing-library/react";
+import { render, screen, act } from "@testing-library/react";
import { useChessSocket } from "./useChessSocket";
-// Mock socket.io-client for Jest
-jest.mock("socket.io-client", () => ({
- io: () => ({
- on: jest.fn(),
- emit: jest.fn(),
- disconnect: jest.fn(),
- id: "test-socket-id",
- }),
-}));
-
-// Dummy component to execute the hook
-const HookExecutor = () => {
- useChessSocket({
+jest.mock("socket.io-client");
+
+const SocketStatus = () => {
+ const socket = useChessSocket({
student: "test-student",
+ serverUrl: "http://localhost",
onMove: () => {},
- serverUrl: "http://localhost", // <- required field
});
- return null;
+ return
{String(socket.connected)}
;
};
-describe("useChessSocket Hook (CI Stub)", () => {
- it("initializes without crashing", () => {
- render();
+describe("useChessSocket connection/disconnection", () => {
+ it("updates connected state on connect/disconnect and disconnects on unmount", () => {
+ const { io } = require("socket.io-client");
+ const mockSocket = {
+ on: jest.fn(),
+ emit: jest.fn(),
+ disconnect: jest.fn(),
+ };
+ io.mockReturnValue(mockSocket);
+
+ const { unmount } = render();
+
+ const connectHandler = mockSocket.on.mock.calls.find(
+ (c: any[]) => c[0] === "connect",
+ )?.[1];
+ const disconnectHandler = mockSocket.on.mock.calls.find(
+ (c: any[]) => c[0] === "disconnect",
+ )?.[1];
+
+ expect(screen.getByTestId("socket-connected")).toHaveTextContent("false");
+
+ act(() => {
+ connectHandler && connectHandler();
+ });
+ expect(screen.getByTestId("socket-connected")).toHaveTextContent("true");
+
+ act(() => {
+ disconnectHandler && disconnectHandler("io client disconnect");
+ });
+ expect(screen.getByTestId("socket-connected")).toHaveTextContent("false");
+
+ unmount();
+ expect(mockSocket.disconnect).toHaveBeenCalled();
});
});