Skip to content

Commit

Permalink
Merge branch 'main' of github.com:All-Hands-AI/OpenHands into enyst/d…
Browse files Browse the repository at this point in the history
…elegation
  • Loading branch information
enyst committed Jan 9, 2025
2 parents 8d7382c + 8907fed commit 24f15cd
Show file tree
Hide file tree
Showing 37 changed files with 730 additions and 409 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe("ConversationCard", () => {
const onClick = vi.fn();
const onDelete = vi.fn();
const onChangeTitle = vi.fn();
const onDownloadWorkspace = vi.fn();

afterEach(() => {
vi.clearAllMocks();
Expand Down Expand Up @@ -233,6 +234,120 @@ describe("ConversationCard", () => {
expect(onClick).not.toHaveBeenCalled();
});

it("should call onDownloadWorkspace when the download button is clicked", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);

const menu = screen.getByTestId("context-menu");
const downloadButton = within(menu).getByTestId("download-button");

await user.click(downloadButton);

expect(onDownloadWorkspace).toHaveBeenCalled();
});

it("should not display the edit or delete options if the handler is not provided", async () => {
const user = userEvent.setup();
const { rerender } = render(
<ConversationCard
onClick={onClick}
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);

expect(screen.queryByTestId("edit-button")).toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).not.toBeInTheDocument();

// toggle to hide the context menu
await user.click(ellipsisButton);

rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

await user.click(ellipsisButton);

expect(screen.queryByTestId("edit-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).toBeInTheDocument();
});

it("should not render the ellipsis button if there are no actions", () => {
const { rerender } = render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();

rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();

rerender(
<ConversationCard
onClick={onClick}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.queryByTestId("ellipsis-button")).toBeInTheDocument();

rerender(
<ConversationCard
onClick={onClick}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);

expect(screen.queryByTestId("ellipsis-button")).not.toBeInTheDocument();
});

describe("state indicator", () => {
it("should render the 'STOPPED' indicator by default", () => {
render(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, afterEach } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import userEvent from "@testing-library/user-event";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
import OpenHands from "#/api/open-hands";

describe("SettingsForm", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");

const onCloseMock = vi.fn();

afterEach(() => {
vi.clearAllMocks();
});

getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
Expand All @@ -19,10 +28,10 @@ describe("SettingsForm", () => {
Component: () => (
<SettingsForm
settings={DEFAULT_SETTINGS}
models={[]}
agents={[]}
securityAnalyzers={[]}
onClose={() => {}}
models={["anthropic/claude-3-5-sonnet-20241022", "model2"]}
agents={["CodeActAgent", "agent2"]}
securityAnalyzers={["analyzer1", "analyzer2"]}
onClose={onCloseMock}
/>
),
path: "/",
Expand All @@ -35,11 +44,33 @@ describe("SettingsForm", () => {
});

it("should show runtime size selector when advanced options are enabled", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);
const advancedSwitch = screen.getByRole("switch", {
name: "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL",
});
fireEvent.click(advancedSwitch);
await screen.findByText("SETTINGS_FORM$RUNTIME_SIZE_LABEL");

const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);

await screen.findByTestId("runtime-size");
});

it("should not submit the form if required fields are empty", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);

expect(screen.queryByTestId("custom-model-input")).not.toBeInTheDocument();

const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);

const customModelInput = screen.getByTestId("custom-model-input");
expect(customModelInput).toBeInTheDocument();

await user.clear(customModelInput);

const saveButton = screen.getByTestId("save-settings-button");
await user.click(saveButton);

expect(saveSettingsSpy).not.toHaveBeenCalled();
expect(onCloseMock).not.toHaveBeenCalled();
});
});
13 changes: 13 additions & 0 deletions frontend/__tests__/context/ws-client-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,17 @@ describe("Propagate error message", () => {
type: 'error'
});
});

it("should display error including translation id when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message, data: {msg_id: '..id..'}})

expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
id: '..id..',
status_update: true,
type: 'error'
});
});
});
55 changes: 28 additions & 27 deletions frontend/src/components/features/controls/controls.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
import { useParams } from "react-router";
import React from "react";
import { useSelector } from "react-redux";
import posthog from "posthog-js";
import { AgentControlBar } from "./agent-control-bar";
import { AgentStatusBar } from "./agent-status-bar";
import { ProjectMenuCard } from "../project-menu/ProjectMenuCard";
import { useAuth } from "#/context/auth-context";
import { RootState } from "#/store";
import { SecurityLock } from "./security-lock";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { ConversationCard } from "../conversation-panel/conversation-card";
import { DownloadModal } from "#/components/shared/download-modal";

interface ControlsProps {
setSecurityOpen: (isOpen: boolean) => void;
showSecurityLock: boolean;
lastCommitData: GitHubCommit | null;
}

export function Controls({
setSecurityOpen,
showSecurityLock,
lastCommitData,
}: ControlsProps) {
const { gitHubToken } = useAuth();
const { selectedRepository } = useSelector(
(state: RootState) => state.initialQuery,
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
const params = useParams();
const { data: conversation } = useUserConversation(
params.conversationId ?? null,
);

const projectMenuCardData = React.useMemo(
() =>
selectedRepository && lastCommitData
? {
repoName: selectedRepository,
lastCommit: lastCommitData,
avatar: null, // TODO: fetch repo avatar
}
: null,
[selectedRepository, lastCommitData],
);
const [downloading, setDownloading] = React.useState(false);

const handleDownloadWorkspace = () => {
posthog.capture("download_workspace_button_clicked");
setDownloading(true);
};

return (
<div className="flex items-center justify-between">
Expand All @@ -46,9 +37,19 @@ export function Controls({
)}
</div>

<ProjectMenuCard
isConnectedToGitHub={!!gitHubToken}
githubData={projectMenuCardData}
<ConversationCard
variant="compact"
onDownloadWorkspace={handleDownloadWorkspace}
title={conversation?.title ?? ""}
lastUpdatedAt={conversation?.created_at ?? ""}
selectedRepository={conversation?.selected_repository ?? null}
status={conversation?.status}
/>

<DownloadModal
initialPath=""
onClose={() => setDownloading(false)}
isOpen={downloading}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { cn } from "#/utils/utils";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";

interface ConversationCardContextMenuProps {
onClose: () => void;
onDelete: (event: React.MouseEvent<HTMLButtonElement>) => void;
onEdit: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDownload?: (event: React.MouseEvent<HTMLButtonElement>) => void;
position?: "top" | "bottom";
}

export function ConversationCardContextMenu({
onClose,
onDelete,
onEdit,
onDownload,
position = "bottom",
}: ConversationCardContextMenuProps) {
const ref = useClickOutsideElement<HTMLUListElement>(onClose);

return (
<ContextMenu
ref={ref}
testId="context-menu"
className="left-full float-right"
className={cn(
"right-0 absolute",
position === "top" && "bottom-full",
position === "bottom" && "top-full",
)}
>
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
Delete
</ContextMenuListItem>
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
Edit Title
</ContextMenuListItem>
{onDelete && (
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
Delete
</ContextMenuListItem>
)}
{onEdit && (
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
Edit Title
</ContextMenuListItem>
)}
{onDownload && (
<ContextMenuListItem testId="download-button" onClick={onDownload}>
Download Workspace
</ContextMenuListItem>
)}
</ContextMenu>
);
}
Loading

0 comments on commit 24f15cd

Please sign in to comment.