a6bd33bcff
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).
144 lines
5.6 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|