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).
This commit is contained in:
@@ -1,24 +1,28 @@
|
||||
/**
|
||||
* @file Sidebar.test.tsx
|
||||
* @description Unit tests for the Sidebar component, which is responsible for rendering the application's sidebar navigation. The tests cover rendering of the brand name, subtitle, navigation links, WebSocket connection status, and version number. The tests use React Testing Library and Vitest for assertions and mocking.
|
||||
* @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 } from "vitest";
|
||||
import { act, render, screen, waitFor } from "@testing-library/react";
|
||||
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) {
|
||||
function renderSidebar(
|
||||
wsConnected: boolean,
|
||||
collapsed = false,
|
||||
extra: { onToggle?: () => void; connectionOpen?: boolean } = {}
|
||||
) {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<Sidebar
|
||||
wsConnected={wsConnected}
|
||||
collapsed={collapsed}
|
||||
onToggle={() => {}}
|
||||
connectionOpen={false}
|
||||
onToggle={extra.onToggle ?? (() => {})}
|
||||
connectionOpen={extra.connectionOpen ?? false}
|
||||
onConnectionOpenChange={() => {}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@@ -51,17 +55,29 @@ describe("Sidebar", () => {
|
||||
expect(screen.queryByText("Activity Feed")).toBeNull();
|
||||
});
|
||||
|
||||
// The live/version line moved to the status bar (StatusBar.test.tsx); the
|
||||
// rail keeps an icon-only button for the connection details.
|
||||
it("keeps the connection button in the rail only", () => {
|
||||
renderSidebar(true);
|
||||
expect(screen.queryByText("Live")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Connection details" })).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("offers the connection details from the collapsed rail", () => {
|
||||
renderSidebar(false, true);
|
||||
expect(screen.getByRole("button", { name: "Connection details" })).toBeInTheDocument();
|
||||
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", () => {
|
||||
@@ -81,39 +97,7 @@ describe("Sidebar", () => {
|
||||
expect(hrefs).not.toContain("/activity");
|
||||
});
|
||||
|
||||
it("should render both language options in expanded mode", () => {
|
||||
renderSidebar(true);
|
||||
expect(screen.getByRole("button", { name: "English" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Vietnamese" })).toBeInTheDocument();
|
||||
// Chinese and Korean were dropped; offering them would render raw keys.
|
||||
expect(screen.queryByRole("button", { name: "Chinese" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Korean" })).toBeNull();
|
||||
});
|
||||
|
||||
it("should switch to Vietnamese when Vietnamese option is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar(true);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Vietnamese" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Tổng quan")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should cycle language in collapsed mode", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSidebar(true, true);
|
||||
|
||||
expect(screen.getByText("EN")).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Switch to Vietnamese" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("VI")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("update status line", () => {
|
||||
describe("update status line, in the connection details modal", () => {
|
||||
const plugin = {
|
||||
git_repo: false,
|
||||
install_kind: "plugin",
|
||||
@@ -124,32 +108,36 @@ describe("Sidebar", () => {
|
||||
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);
|
||||
renderSidebar(true, false, { connectionOpen: true });
|
||||
publish({ ...plugin, update_available: false, release_check: "unreachable" });
|
||||
expect(screen.queryByText("Up to date")).toBeNull();
|
||||
expect(screen.getByText("Could not check for updates")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("update-status")).toHaveTextContent("Could not check for updates");
|
||||
});
|
||||
|
||||
it("names a build installed beside the running one", () => {
|
||||
renderSidebar(true);
|
||||
renderSidebar(true, false, { connectionOpen: true });
|
||||
publish({
|
||||
...plugin,
|
||||
update_available: true,
|
||||
release_check: "unreachable",
|
||||
installed_not_running: { root: "/x", version: "1.8.12" },
|
||||
});
|
||||
expect(screen.getByText("1.8.12 installed, not running")).toBeInTheDocument();
|
||||
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);
|
||||
renderSidebar(true, false, { connectionOpen: true });
|
||||
publish({
|
||||
...plugin,
|
||||
update_available: false,
|
||||
release_check: "ok",
|
||||
latest_release: "v1.8.10",
|
||||
});
|
||||
expect(screen.getByText("Up to date")).toBeInTheDocument();
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user