/** * @file Sidebar.test.tsx * @description Unit tests for the Sidebar: the brand, the navigation links, the small collapse button in the brand's corner, no footer / language / theme controls (those moved to the status bar and Settings), and the connection details modal carrying the version and the release check. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi } from "vitest"; import { act, render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { Sidebar } from "../Sidebar"; import { eventBus } from "../../lib/eventBus"; function renderSidebar( wsConnected: boolean, collapsed = false, extra: { onToggle?: () => void; connectionOpen?: boolean } = {} ) { return render( {})} connectionOpen={extra.connectionOpen ?? false} onConnectionOpenChange={() => {}} /> ); } describe("Sidebar", () => { it("should render the brand logo in place of the text subtitle", () => { renderSidebar(true); // One wordmark in both themes — no light/dark variant. expect(screen.getByAltText("SmartGift Solution")).toHaveAttribute( "src", expect.stringContaining("logo-main.png") ); }); it("should render the favicon mark when collapsed", () => { renderSidebar(true, true); const mark = screen.getByAltText("SmartGift Solution"); expect(mark).toHaveAttribute("src", "https://smartgift.vn/favicon.ico"); }); it("should render all navigation links", () => { renderSidebar(true); expect(screen.getByText("Dashboard")).toBeInTheDocument(); expect(screen.getByText("Sessions")).toBeInTheDocument(); // Activity Feed and Claude Config are no longer top-level entries: they // moved into tabs on /sessions and /settings respectively. expect(screen.getByText("Settings")).toBeInTheDocument(); expect(screen.queryByText("Activity Feed")).toBeNull(); }); // The live/version line lives in the status bar and the update check in the // connection details modal; language and theme live in Settings. it("has no footer, language or theme controls", () => { for (const collapsed of [false, true]) { const { unmount } = renderSidebar(true, collapsed); expect(screen.queryByText("Live")).toBeNull(); expect(screen.queryByRole("button", { name: "Connection details" })).toBeNull(); expect(screen.queryByRole("button", { name: "English" })).toBeNull(); expect(screen.queryByRole("button", { name: "Dark" })).toBeNull(); expect(screen.queryByText("Up to date")).toBeNull(); unmount(); } }); it("collapses and expands from a small button in the brand's corner", async () => { const onToggle = vi.fn(); const user = userEvent.setup(); const { unmount } = renderSidebar(true, false, { onToggle }); await user.click(screen.getByRole("button", { name: "Collapse sidebar" })); unmount(); renderSidebar(true, true, { onToggle }); await user.click(screen.getByRole("button", { name: "Expand sidebar" })); expect(onToggle).toHaveBeenCalledTimes(2); }); it("renders the workspace list when expanded and its icon in the rail", () => { renderSidebar(true); expect(screen.getByTestId("workspace-tree")).toBeInTheDocument(); renderSidebar(true, true); expect(screen.getAllByRole("button", { name: "Workspaces" }).length).toBeGreaterThan(0); }); it("should have correct navigation hrefs", () => { renderSidebar(true); const links = screen.getAllByRole("link"); const hrefs = links.map((link) => link.getAttribute("href")); expect(hrefs).toContain("/"); expect(hrefs).toContain("/sessions"); expect(hrefs).toContain("/settings"); expect(hrefs).not.toContain("/activity"); }); describe("update status line, in the connection details modal", () => { const plugin = { git_repo: false, install_kind: "plugin", situation: "plugin_managed", local_version: "1.8.10", }; const publish = (data: object) => act(() => eventBus.publish({ type: "update_status", data } as never)); it("does not claim up to date when a plugin install could not read the release", () => { renderSidebar(true, false, { connectionOpen: true }); publish({ ...plugin, update_available: false, release_check: "unreachable" }); expect(screen.queryByText("Up to date")).toBeNull(); expect(screen.getByTestId("update-status")).toHaveTextContent("Could not check for updates"); }); it("names a build installed beside the running one", () => { renderSidebar(true, false, { connectionOpen: true }); publish({ ...plugin, update_available: true, release_check: "unreachable", installed_not_running: { root: "/x", version: "1.8.12" }, }); expect(screen.getByTestId("update-status")).toHaveTextContent( "1.8.12 installed, not running" ); }); it("still says up to date when the release was read and matches", () => { renderSidebar(true, false, { connectionOpen: true }); publish({ ...plugin, update_available: false, release_check: "ok", latest_release: "v1.8.10", }); const dialog = screen.getByRole("dialog"); expect(within(dialog).getByTestId("update-status")).toHaveTextContent("Up to date"); expect(within(dialog).getByRole("button", { name: "Check for updates" })).toBeEnabled(); }); }); });