Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions frontend/src/tests/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <div data-testid="language-switcher" />,
}));

vi.mock("../components/ThemeToggle", () => ({
default: () => <div data-testid="theme-toggle" />,
}));

// 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(
<MemoryRouter initialEntries={[route]}>
<Sidebar isOpen={isOpen} onCloseSidebar={onCloseSidebar} />
</MemoryRouter>,
);

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 <aside> element has the role "complementary"
it("reflects the open state in the data-open attribute", () => {
const { unmount } = render(
<MemoryRouter>
<Sidebar isOpen={true} onCloseSidebar={vi.fn()} />
</MemoryRouter>,
);
expect(screen.getByRole("complementary")).toHaveAttribute(
"data-open",
"true",
);
unmount();

render(
<MemoryRouter>
<Sidebar isOpen={false} onCloseSidebar={vi.fn()} />
</MemoryRouter>,
);
expect(screen.getByRole("complementary")).toHaveAttribute(
"data-open",
"false",
);
});

// the <nav> has an aria-label, so screen readers can announce it and let users jump to it
it("has an accessible navigation landmark", () => {
renderSidebar();

expect(
screen.getByRole("navigation", { name: "Sidebar navigation" }),
).toBeInTheDocument();
});

// the router is set to the Explore page, so only that link should get aria-current="page"
it("marks only the link for the current page as active", () => {
renderSidebar({ route: ROUTES.explore });

expect(
screen.getByRole("link", { name: "sidebar.explorePlants" }),
).toHaveAttribute("aria-current", "page");
expect(
screen.getByRole("link", { name: "sidebar.plantMatch" }),
).not.toHaveAttribute("aria-current");
expect(
screen.getByRole("link", { name: "sidebar.myMatches" }),
).not.toHaveAttribute("aria-current");
expect(
screen.getByRole("link", { name: "sidebar.profile" }),
).not.toHaveAttribute("aria-current");
});
});