Files
AgentHub/client/src/components/__tests__/Sidebar.test.tsx
T
nntrivi2001 a6bd33bcff feat: workspaces own their sessions; desktop parity; sidebar and settings cleanup
Workspace sessions (docs/plans/desktop-parity, phases 1-5 and 7):
- sessions.lane_id: a workspace lists only the sessions its panes held,
  stamped by bindRunSession (first writer wins, nested `claude -p`
  excluded); GET /api/sessions?lane=&archived=
- session row state dot, double-click rename, archive (sessions.archived_at,
  hides only); the activity bar's duplicate Sessions tab removed
- Fork beside Resume (--fork-session, Claude only), Ctrl/Cmd+click a live
  session opens it in a second pane, a diff line's comment is pasted into
  the workspace's last-focused pane (lib/panePaste.ts)

dashboard_runs stability:
- a pane's previous run is archived as <pane>@<started_at> instead of being
  overwritten by INSERT OR REPLACE
- a run is flagged abandoned only once its tmux session is gone, at boot and
  on the run-heal beat (panes outlive a server restart)

UI:
- sidebar: no footer, language or theme block; small collapse button in the
  brand corner; version + update check in the connection details modal, a
  status-bar dot while a release is out; language in Settings -> General
- one Switch component for every switch, the themed Checkbox for every
  checkbox (no native checkboxes)

Also: AI Tools catalog trimmed to 10 built-ins (custom-file prompt modes
tested through fixtures), and the workspace plans under docs/plans/ (the
multi-user server plan lives in the AgentHub-Server repo).
2026-10-02 13:19:50 +07:00

144 lines
5.6 KiB
TypeScript

/**
* @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ĩ <vinnt@smartgift.vn>
*/
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(
<MemoryRouter>
<Sidebar
wsConnected={wsConnected}
collapsed={collapsed}
onToggle={extra.onToggle ?? (() => {})}
connectionOpen={extra.connectionOpen ?? false}
onConnectionOpenChange={() => {}}
/>
</MemoryRouter>
);
}
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();
});
});
});