Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

375 lines
12 KiB
TypeScript

import { act, fireEvent, render, screen, within } from "@testing-library/react";
import { createRef } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
AgentToolSidebar,
type AgentToolSidebarHandle
} from "./AgentToolSidebar.tsx";
import type { AgentToolSidebarCopy } from "./Toolbar.tsx";
import type { AgentToolPanelDefinition } from "./model.ts";
const panels = [
{ id: "files", label: "Files" },
{ id: "terminal", label: "Terminal" },
{ id: "browser", label: "Browser" }
] as const;
const copy: AgentToolSidebarCopy = {
close: "Close",
closeRightPanel: "Close right panel",
expand: "Expand",
newTab: "New tab",
openRightPanel: "Open right panel",
resizeSidebar: "Resize sidebar",
shrink: "Shrink",
tool: "Tools"
};
describe("AgentToolSidebar", () => {
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
});
it("renders only the host-provided panel choices", async () => {
const { container } = renderSidebar();
fireEvent.click(screen.getByLabelText("Open right panel"));
expect(await screen.findByText("Files")).toBeInTheDocument();
expect(screen.getByText("Terminal")).toBeInTheDocument();
expect(screen.getByText("Browser")).toBeInTheDocument();
expect(screen.queryByText("Tasks")).not.toBeInTheDocument();
expect(screen.queryByText("Apps")).not.toBeInTheDocument();
expect(screen.queryByText("Messages")).not.toBeInTheDocument();
const sidebarHeader = container.querySelector(
'[data-agent-tool-sidebar-header="true"]'
);
expect(sidebarHeader).not.toHaveClass("border-b");
fireEvent.click(screen.getByText("Files"));
expect(sidebarHeader).toHaveClass("border-b");
});
it("keeps contextual panels command-openable but out of creation menus", async () => {
const ref = createRef<AgentToolSidebarHandle>();
renderSidebar(ref, undefined, [
...panels,
{ canAdd: false, id: "side", label: "Side" }
]);
fireEvent.click(screen.getByLabelText("Open right panel"));
expect(
screen.queryByRole("button", { name: "Side" })
).not.toBeInTheDocument();
await act(async () => {
ref.current?.openPanel("side", "source-session");
});
expect(screen.getByText("side content")).toBeVisible();
fireEvent.pointerDown(screen.getByLabelText("New tab"), {
button: 0,
ctrlKey: false
});
expect(
await screen.findByRole("menuitem", { name: "Files" })
).toBeInTheDocument();
expect(
screen.queryByRole("menuitem", { name: "Side" })
).not.toBeInTheDocument();
});
it("hides the sidebar toggle when the host disables that entry", () => {
render(
<AgentToolSidebar
containerWidth={900}
copy={copy}
header={{
layout: "overlay",
owner: "host",
render: (layout) => <header>{layout.actions}</header>
}}
panels={panels}
renderPanel={({ tab }) => <div>{tab.panel} content</div>}
resizeContainerContentWidth={async (width) => ({ width })}
showToggleButton={false}
>
<main>Agent content</main>
</AgentToolSidebar>
);
expect(screen.queryByLabelText("Open right panel")).not.toBeInTheDocument();
});
it("opens a panel through the shared handle and keeps its content mounted", async () => {
vi.useFakeTimers();
const ref = createRef<AgentToolSidebarHandle>();
renderSidebar(ref);
await act(async () => {
ref.current?.openPanel("terminal");
});
await act(async () => {
vi.runAllTimers();
});
expect(screen.getByText("terminal content")).toBeVisible();
await act(async () => {
ref.current?.openPanel("files");
});
await act(async () => {
vi.runAllTimers();
});
expect(screen.getByText("files content")).toBeVisible();
expect(
screen.getByText("terminal content").closest("[aria-hidden]")
).toHaveAttribute("aria-hidden", "true");
});
it("restores the host width when the sidebar closes", async () => {
const resizeContainerContentWidth = vi.fn(async (width: number) => ({
width
}));
const ref = createRef<AgentToolSidebarHandle>();
renderSidebar(ref, resizeContainerContentWidth);
await act(async () => {
ref.current?.openPanel("files");
});
await act(async () => {
ref.current?.close();
});
expect(resizeContainerContentWidth).toHaveBeenLastCalledWith(
900,
undefined
);
});
it("collapses for a container constraint without resizing the host window", async () => {
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
callback(0);
return 1;
});
vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => {});
const ref = createRef<AgentToolSidebarHandle>();
const resizeContainerContentWidth = vi.fn(async (width: number) => ({
width
}));
render(
<AgentToolSidebar
ref={ref}
containerWidth={1500}
copy={copy}
header={{
layout: "overlay",
owner: "window",
render: (layout) => <header>{layout.actions}</header>
}}
mainContentMinWidthPx={750}
panels={panels}
renderPanel={({ tab }) => <div>{tab.panel} content</div>}
resizeContainerContentWidth={resizeContainerContentWidth}
>
<main>Agent content</main>
</AgentToolSidebar>
);
await act(async () => {
ref.current?.openPanel("files");
});
const resizeCallCount = resizeContainerContentWidth.mock.calls.length;
await act(async () => {
ref.current?.collapseForContainerConstraint();
});
expect(screen.getByLabelText("Open right panel")).toBeInTheDocument();
expect(resizeContainerContentWidth).toHaveBeenCalledTimes(resizeCallCount);
});
it("registers one host-owned header without adding window chrome or a panel header", () => {
const { container } = render(
<AgentToolSidebar
containerWidth={900}
copy={copy}
header={{
layout: "overlay",
owner: "host",
render: (layout) => (
<div data-testid="host-header-actions">{layout.actions}</div>
)
}}
panels={panels}
renderPanel={({ tab }) => <div>{tab.panel} content</div>}
resizeContainerContentWidth={async (width) => ({ width })}
>
<main>Agent content</main>
</AgentToolSidebar>
);
const hostHeader = screen.getByTestId("host-header-actions");
fireEvent.click(within(hostHeader).getByLabelText("Open right panel"));
expect(
within(hostHeader).getByLabelText("Close right panel")
).toBeInTheDocument();
expect(
container.querySelector('[data-agent-tool-sidebar-header-spacer="true"]')
).toBeInTheDocument();
expect(
container.querySelector(".workbench-window__header")
).not.toBeInTheDocument();
expect(
container.querySelector(".workbench-window__body")
).not.toBeInTheDocument();
expect(
container.querySelector(
'[data-agent-tool-sidebar="true"] [data-agent-tool-sidebar-header="true"]'
)
).not.toBeInTheDocument();
});
it("does not reserve header space when the host stacks the body below it", () => {
const { container } = render(
<AgentToolSidebar
containerWidth={900}
copy={copy}
header={{
layout: "stacked",
owner: "host",
render: (layout) => <div>{layout.actions}</div>
}}
panels={panels}
renderPanel={({ tab }) => <div>{tab.panel} content</div>}
resizeContainerContentWidth={async (width) => ({ width })}
>
<main>Agent content</main>
</AgentToolSidebar>
);
fireEvent.click(screen.getByLabelText("Open right panel"));
expect(
container.querySelector('[data-agent-tool-sidebar-header-spacer="true"]')
).not.toBeInTheDocument();
});
it("reserves collapsed tool actions in the inline window header", () => {
const { container } = renderSidebar();
const header = container.querySelector<HTMLElement>(
".workbench-window__header"
);
expect(
header?.style.getPropertyValue("--agent-gui-tool-sidebar-layout-width")
).toBe("132px");
});
it("lets host-owned blank header gestures bubble while controls remain interactive", () => {
const handleParentDoubleClick = vi.fn();
const handleParentPointerDown = vi.fn();
const { container } = render(
<div
onDoubleClick={handleParentDoubleClick}
onPointerDown={handleParentPointerDown}
>
<AgentToolSidebar
containerWidth={900}
copy={copy}
header={{
layout: "overlay",
owner: "host",
render: (layout) => <div>{layout.actions}</div>
}}
panels={panels}
renderPanel={({ tab }) => <div>{tab.panel} content</div>}
resizeContainerContentWidth={async (width) => ({ width })}
>
<main>Agent content</main>
</AgentToolSidebar>
</div>
);
fireEvent.click(screen.getByLabelText("Open right panel"));
fireEvent.click(screen.getByText("Files"));
const header = container.querySelector(
'[data-agent-tool-sidebar-header="true"]'
);
const tabList = container.querySelector(
'[data-agent-tool-tab-list="true"]'
);
const toolbar = container.querySelector(
'[data-agent-tool-sidebar-toolbar="true"]'
);
expect(header).toHaveAttribute(
"data-agent-tool-sidebar-drag-region",
"true"
);
expect(header).not.toHaveClass("nodrag");
expect(header?.className).toContain("[-webkit-app-region:no-drag]");
expect(header?.className).not.toContain("[-webkit-app-region:drag]");
expect(tabList).not.toHaveClass("nodrag");
expect(tabList?.className).not.toContain("[-webkit-app-region:drag]");
expect(screen.getByRole("tab", { name: "Files" })).toHaveClass("nodrag");
expect(toolbar).toHaveClass("nodrag");
fireEvent.pointerDown(tabList as HTMLElement);
fireEvent.doubleClick(tabList as HTMLElement);
expect(handleParentPointerDown).toHaveBeenCalledOnce();
expect(handleParentDoubleClick).toHaveBeenCalledOnce();
fireEvent.pointerDown(screen.getByRole("tab", { name: "Files" }));
fireEvent.doubleClick(screen.getByRole("tab", { name: "Files" }));
fireEvent.pointerDown(toolbar as HTMLElement);
fireEvent.doubleClick(toolbar as HTMLElement);
expect(handleParentPointerDown).toHaveBeenCalledOnce();
expect(handleParentDoubleClick).toHaveBeenCalledOnce();
});
it("keeps native-window header dragging for a window-owned header", () => {
const { container } = renderSidebar();
const header = container.querySelector(
'[data-agent-tool-sidebar-header="true"]'
);
expect(header?.className).toContain("[-webkit-app-region:drag]");
expect(header?.className).not.toContain("[-webkit-app-region:no-drag]");
});
});
function renderSidebar(
ref = createRef<AgentToolSidebarHandle>(),
resizeContainerContentWidth = vi.fn(async (width: number) => ({ width })),
panelDefinitions: readonly AgentToolPanelDefinition[] = panels
) {
vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => {
callback(0);
return 1;
});
vi.spyOn(window, "cancelAnimationFrame").mockImplementation(() => {});
return render(
<AgentToolSidebar
ref={ref}
containerWidth={900}
copy={copy}
header={{
layout: "overlay",
owner: "window",
render: (layout) => <header>{layout.actions}</header>
}}
panels={panelDefinitions}
renderPanel={({ tab }) => <div>{tab.panel} content</div>}
resizeContainerContentWidth={resizeContainerContentWidth}
>
<main>Agent content</main>
</AgentToolSidebar>
);
}