22de5009ff
- Artifacts (/artifacts): claude.ai pages sessions publish (captured from the Artifact tool's hook), pinned workspace files, skills and plugins, each kept as a snapshot; shared ones appear in a Hub and a skill or plugin installs into the viewer's own account (server/lib/artifacts.js, /api/artifacts, docs/plans/artifacts/). - Workspace extra folders: clone one or more related repos beside the workspace and add them in one step (POST /api/lanes/:id/sources/clone). - A pane that ended by itself while tmux kept running is no longer offered for resume (tmux.serverStartedAt in reconcileOrphans). - Changes panel: a folder that is not a git repo opens on Files, and its files open from there. - Plus the parallel sessions' work in this tree: File Explorer button in the panel header, user delete, RSA SSH keys, chat/terminal and pet fixes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
5.7 KiB
TypeScript
146 lines
5.7 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");
|
|
// The file manager serves a shared server; a local dashboard has none.
|
|
expect(hrefs).not.toContain("/files");
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|