Merge branch 'main' into docs/gitea-operational-quirks-runbook
All checks were successful
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 10s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 16s
sop-tier-check / tier-check (pull_request) Successful in 17s
CI / Detect changes (pull_request) Successful in 38s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 39s
E2E API Smoke Test / detect-changes (pull_request) Successful in 40s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 37s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 37s
CI / Platform (Go) (pull_request) Successful in 8s
CI / Canvas (Next.js) (pull_request) Successful in 8s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 7s
CI / Python Lint & Test (pull_request) Successful in 14s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 8s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 7s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 9s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 10s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
audit-force-merge / audit (pull_request) Successful in 19s

This commit is contained in:
Molecule AI · core-lead 2026-05-11 12:35:58 +00:00
commit db647de1cd

View File

@ -0,0 +1,291 @@
// @vitest-environment jsdom
/**
* Toolbar tests.
*
* Covers:
* - Renders with 0 workspaces
* - Shows online/offline/failed/provisioning status pills when nodes exist
* - WebSocket status pill: connected "Live"
* - WebSocket status pill: connecting "Reconnecting"
* - WebSocket status pill: disconnected "Offline"
* - Stop All button visible when activeTasks > 0
* - Restart Pending button visible when needsRestart nodes exist
* - Help button opens the help popover
* - Help popover closes on Escape or pointer-outside
* - KeyboardShortcutsDialog opens via ? shortcut (when not in input)
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
import React from "react";
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
// Reset store state between tests so mutations don't leak.
beforeEach(() => {
defaultStore.nodes = [];
defaultStore.wsStatus = "connected";
defaultStore.showA2AEdges = false;
defaultStore.selectedNodeId = null;
mockSetShowA2AEdges.mockClear();
mockSetPanelTab.mockClear();
mockSetSearchOpen.mockClear();
mockUpdateNodeData.mockClear();
});
// ── Mock targets ───────────────────────────────────────────────────────────────
vi.mock("@/components/Toaster", () => ({
showToast: vi.fn(),
}));
vi.mock("@/components/ConfirmDialog", () => ({
ConfirmDialog: () => null,
}));
vi.mock("@/components/settings/SettingsButton", () => ({
SettingsButton: () => null,
}));
vi.mock("@/components/settings/SettingsPanel", () => ({
settingsGearRef: { current: null },
}));
vi.mock("@/components/ThemeToggle", () => ({
ThemeToggle: () => null,
}));
vi.mock("@/components/KeyboardShortcutsDialog", () => ({
KeyboardShortcutsDialog: ({ open }: { open: boolean; onClose: () => void }) =>
open ? <div role="dialog" data-testid="shortcuts-dialog">Shortcuts</div> : null,
}));
vi.mock("@/lib/design-tokens", () => ({
statusDotClass: (status: string) => {
const map: Record<string, string> = {
online: "bg-emerald-400",
offline: "bg-zinc-500",
paused: "bg-indigo-400",
degraded: "bg-amber-400",
failed: "bg-red-400",
provisioning: "bg-sky-400",
};
return map[status] ?? "bg-zinc-500";
},
}));
vi.mock("@/lib/api", () => ({
api: {
post: vi.fn(() => Promise.resolve()),
},
}));
// ── Store mocks ────────────────────────────────────────────────────────────────
const mockSetShowA2AEdges = vi.fn();
const mockSetPanelTab = vi.fn();
const mockSetSearchOpen = vi.fn();
const mockUpdateNodeData = vi.fn();
const makeNodes = (
statuses: Array<"online" | "offline" | "failed" | "provisioning">,
activeTasks: number[] = [],
needsRestart: boolean[] = [],
parentIds: (string | null)[] = []
) => {
return statuses.map((status, i) => ({
id: `ws-${i}`,
data: {
name: `Workspace ${i}`,
role: "agent",
tier: 1,
status,
parentId: parentIds[i] ?? null,
activeTasks: activeTasks[i] ?? 0,
needsRestart: needsRestart[i] ?? false,
},
}));
};
// Nodes must be React Flow nodes (id + data), but Toolbar only reads data fields.
// makeNodes returns { id, data: { activeTasks, needsRestart, ... } }.
const toStoreNodes = (nodes: ReturnType<typeof makeNodes>) =>
nodes.map((n) => ({ id: n.id, data: n.data }));
const defaultStore = {
nodes: [] as ReturnType<typeof makeNodes>,
wsStatus: "connected" as "connected" | "connecting" | "disconnected",
showA2AEdges: false,
selectedNodeId: null as string | null,
sidePanelWidth: 480,
setShowA2AEdges: mockSetShowA2AEdges,
setPanelTab: mockSetPanelTab,
setSearchOpen: mockSetSearchOpen,
updateNodeData: mockUpdateNodeData,
selectedNodeIds: new Set<string>(),
clearSelection: vi.fn(),
batchRestart: vi.fn(() => Promise.resolve()),
batchPause: vi.fn(() => Promise.resolve()),
batchDelete: vi.fn(() => Promise.resolve()),
};
vi.mock("@/store/canvas", () => ({
useCanvasStore: vi.fn((selector: (s: typeof defaultStore) => unknown) =>
selector(defaultStore)
),
}));
// ── Component under test ───────────────────────────────────────────────────────
import { Toolbar } from "../Toolbar";
// ── Tests ─────────────────────────────────────────────────────────────────────
describe("Toolbar — workspace count display", () => {
it("shows '0 workspaces' when the canvas is empty", () => {
render(<Toolbar />);
expect(screen.getByText(/0 workspaces?/)).toBeTruthy();
});
it("shows 'N workspaces' when nodes exist", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["online", "online"]));
render(<Toolbar />);
expect(screen.getByText(/2 workspaces?/)).toBeTruthy();
});
});
describe("Toolbar — status pills", () => {
it("shows the online pill when nodes are online", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["online"]));
render(<Toolbar />);
// StatusPill uses aria-label
expect(screen.getByLabelText(/1 online/i)).toBeTruthy();
});
it("shows the offline pill only when offline nodes exist", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["offline"]));
render(<Toolbar />);
expect(screen.getByLabelText(/1 offline/i)).toBeTruthy();
});
it("shows the failed pill when failed nodes exist", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["failed"]));
render(<Toolbar />);
expect(screen.getByLabelText(/1 failed/i)).toBeTruthy();
});
it("shows the provisioning pill when provisioning nodes exist", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["provisioning"]));
render(<Toolbar />);
expect(screen.getByLabelText(/1 starting/i)).toBeTruthy();
});
it("suppresses offline pill when no offline nodes", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["online", "online"]));
render(<Toolbar />);
expect(screen.queryByLabelText(/offline/i)).toBeNull();
});
});
describe("Toolbar — WebSocket status pill", () => {
it('shows "Live" when connected', () => {
defaultStore.wsStatus = "connected";
render(<Toolbar />);
expect(screen.getByText("Live")).toBeTruthy();
});
it('shows "Reconnecting" when connecting', () => {
defaultStore.wsStatus = "connecting";
render(<Toolbar />);
expect(screen.getByText("Reconnecting")).toBeTruthy();
});
it('shows "Offline" when disconnected', () => {
defaultStore.wsStatus = "disconnected";
render(<Toolbar />);
expect(screen.getByText("Offline")).toBeTruthy();
});
});
describe("Toolbar — Stop All", () => {
it("is hidden when no active tasks", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["online"], [0]));
render(<Toolbar />);
expect(screen.queryByRole("button", { name: /Stop All/i })).toBeNull();
});
it("is visible when active tasks > 0", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["online", "online"], [2, 2]));
render(<Toolbar />);
// aria-label: "Stop all running tasks (2)"
expect(screen.getByRole("button", { name: /stop all running tasks/i })).toBeTruthy();
});
});
describe("Toolbar — Restart Pending", () => {
it("is hidden when no nodes need restart", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["online"], [], [false]));
render(<Toolbar />);
expect(screen.queryByRole("button", { name: /Restart Pending/i })).toBeNull();
});
it("is visible when nodes need restart", () => {
defaultStore.nodes = toStoreNodes(makeNodes(["online"], [], [true]));
render(<Toolbar />);
// aria-label: "Restart 1 workspaces pending config or secret changes"
expect(screen.getByRole("button", { name: /restart 1 workspace/i })).toBeTruthy();
});
});
describe("Toolbar — Help popover", () => {
it("opens when help button is clicked", () => {
render(<Toolbar />);
const helpBtn = screen.getByRole("button", { name: /open shortcuts and tips/i });
fireEvent.click(helpBtn);
expect(screen.getByRole("dialog")).toBeTruthy();
});
it("closes when close button is clicked", () => {
render(<Toolbar />);
const helpBtn = screen.getByRole("button", { name: /open shortcuts and tips/i });
fireEvent.click(helpBtn);
expect(screen.getByRole("dialog")).toBeTruthy();
const closeBtn = screen.getByRole("button", { name: /close help dialog/i });
fireEvent.click(closeBtn);
expect(screen.queryByRole("dialog")).toBeNull();
});
});
describe("Toolbar — A2A edges toggle", () => {
it("calls setShowA2AEdges on click", () => {
defaultStore.showA2AEdges = false;
render(<Toolbar />);
const toggle = screen.getByRole("button", { name: /show a2a edges/i });
fireEvent.click(toggle);
expect(mockSetShowA2AEdges).toHaveBeenCalledWith(true);
});
});
describe("Toolbar — ? shortcut opens shortcuts dialog", () => {
it("opens KeyboardShortcutsDialog when ? is pressed outside an input", () => {
render(<Toolbar />);
expect(screen.queryByTestId("shortcuts-dialog")).toBeNull();
fireEvent.keyDown(window, { key: "?" });
expect(screen.getByTestId("shortcuts-dialog")).toBeTruthy();
});
it("does not fire ? shortcut when focus is in an input", () => {
render(
<div>
<input data-testid="test-input" type="text" />
<Toolbar />
</div>
);
const input = screen.getByTestId("test-input");
fireEvent.focus(input);
// Fire on the input element so e.target.tagName === "INPUT" is true
fireEvent.keyDown(input, { key: "?" });
expect(screen.queryByTestId("shortcuts-dialog")).toBeNull();
});
});