diff --git a/frontend/src/tests/Sidebar.test.tsx b/frontend/src/tests/Sidebar.test.tsx new file mode 100644 index 00000000..0e7602b8 --- /dev/null +++ b/frontend/src/tests/Sidebar.test.tsx @@ -0,0 +1,145 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { MemoryRouter } from "react-router-dom"; +import { describe, it, expect, vi } from "vitest"; +import Sidebar from "../components/sidebar/Sidebar"; +import { ROUTES } from "../AppRoutes"; + +// mock i18next to return the key as the translation +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +// replace the child components with simple stand-ins. They have their own +// tests (or are not part of what we test here), and this keeps the Sidebar +// test focused on the Sidebar itself +vi.mock("../components/languageSwitcher/LanguageSwitcher", () => ({ + default: () =>
, +})); + +vi.mock("../components/ThemeToggle", () => ({ + default: () =>
, +})); + +// render the Sidebar inside a router, as NavLink needs one. +// "route" is the page the router starts on, which decides the active link +function renderSidebar({ + isOpen = false, + route = ROUTES.plantMatch, +}: { isOpen?: boolean; route?: string } = {}) { + const user = userEvent.setup(); + const onCloseSidebar = vi.fn(); // a spy that records how many times it is called + + render( + + + , + ); + + return { user, onCloseSidebar }; +} + +describe("Sidebar", () => { + // sidebar displays the title, links to all pages, language switcher, and theme toggle + it("displays the title", () => { + renderSidebar(); + + expect(screen.getByText("sidebar.title")).toBeInTheDocument(); + }); + + it("displays navigation links to all pages", () => { + renderSidebar(); + + expect( + screen.getByRole("link", { name: "sidebar.plantMatch" }), + ).toHaveAttribute("href", ROUTES.plantMatch); + expect( + screen.getByRole("link", { name: "sidebar.explorePlants" }), + ).toHaveAttribute("href", ROUTES.explore); + expect( + screen.getByRole("link", { name: "sidebar.myMatches" }), + ).toHaveAttribute("href", ROUTES.myMatches); + expect( + screen.getByRole("link", { name: "sidebar.profile" }), + ).toHaveAttribute("href", ROUTES.profile); + }); + + it("displays the language switcher and theme toggle", () => { + renderSidebar(); + + expect(screen.getByTestId("language-switcher")).toBeInTheDocument(); + expect(screen.getByTestId("theme-toggle")).toBeInTheDocument(); + }); + + // on mobile the sidebar is closed after the user picks a page or clicks the close button. The onCloseSidebar prop is called in both cases. + it("closes the sidebar when a link is clicked", async () => { + const { user, onCloseSidebar } = renderSidebar(); + + await user.click(screen.getByRole("link", { name: "sidebar.profile" })); + + expect(onCloseSidebar).toHaveBeenCalledTimes(1); + }); + + it("closes the sidebar when the close button is clicked", async () => { + const { user, onCloseSidebar } = renderSidebar(); + + await user.click(screen.getByRole("button", { name: "Close menu" })); + + expect(onCloseSidebar).toHaveBeenCalledTimes(1); + }); + + // data-open controls whether the sidebar is shown or hidden on mobile, + // so it should be "true" when isOpen is true and "false" when it is false. + // The