diff --git a/frontend/__tests__/components/browser.test.tsx b/frontend/__tests__/components/browser.test.tsx index 304b49c3fc..dffd0b9eb2 100644 --- a/frontend/__tests__/components/browser.test.tsx +++ b/frontend/__tests__/components/browser.test.tsx @@ -1,6 +1,8 @@ import { describe, it, expect, afterEach, vi } from "vitest"; +import { screen, render } from "@testing-library/react"; +import React from "react"; -// Mock useParams before importing components +// Mock modules before importing the component vi.mock("react-router", async () => { const actual = await vi.importActual("react-router"); return { @@ -9,7 +11,11 @@ vi.mock("react-router", async () => { }; }); -// Mock i18next +vi.mock("#/context/conversation-context", () => ({ + useConversation: () => ({ conversationId: "test-conversation-id" }), + ConversationProvider: ({ children }: { children: React.ReactNode }) => children, +})); + vi.mock("react-i18next", async () => { const actual = await vi.importActual("react-i18next"); return { @@ -23,38 +29,56 @@ vi.mock("react-i18next", async () => { }; }); -import { screen } from "@testing-library/react"; -import { renderWithProviders } from "../../test-utils"; +// Mock redux +const mockDispatch = vi.fn(); +let mockBrowserState = { + url: "https://example.com", + screenshotSrc: "", +}; + +vi.mock("react-redux", async () => { + const actual = await vi.importActual("react-redux"); + return { + ...actual, + useDispatch: () => mockDispatch, + useSelector: () => mockBrowserState, + }; +}); + +// Import the component after all mocks are set up import { BrowserPanel } from "#/components/features/browser/browser"; describe("Browser", () => { afterEach(() => { vi.clearAllMocks(); + // Reset the mock state + mockBrowserState = { + url: "https://example.com", + screenshotSrc: "", + }; }); + it("renders a message if no screenshotSrc is provided", () => { - renderWithProviders(, { - preloadedState: { - browser: { - url: "https://example.com", - screenshotSrc: "", - }, - }, - }); + // Set the mock state for this test + mockBrowserState = { + url: "https://example.com", + screenshotSrc: "", + }; + + render(); // i18n empty message key expect(screen.getByText("BROWSER$NO_PAGE_LOADED")).toBeInTheDocument(); }); it("renders the url and a screenshot", () => { - renderWithProviders(, { - preloadedState: { - browser: { - url: "https://example.com", - screenshotSrc: - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==", - }, - }, - }); + // Set the mock state for this test + mockBrowserState = { + url: "https://example.com", + screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==", + }; + + render(); expect(screen.getByText("https://example.com")).toBeInTheDocument(); expect(screen.getByAltText("BROWSER$SCREENSHOT_ALT")).toBeInTheDocument(); diff --git a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx index 67e373f40f..ce8ad1c8a7 100644 --- a/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx +++ b/frontend/__tests__/components/features/conversation-panel/conversation-panel.test.tsx @@ -34,10 +34,6 @@ describe("ConversationPanel", () => { } }); - const { endSessionMock } = vi.hoisted(() => ({ - endSessionMock: vi.fn(), - })); - beforeAll(() => { vi.mock("react-router", async (importOriginal) => ({ ...(await importOriginal()), @@ -46,11 +42,6 @@ describe("ConversationPanel", () => { useLocation: vi.fn(() => ({ pathname: "/conversation" })), useParams: vi.fn(() => ({ conversationId: "2" })), })); - - vi.mock("#/hooks/use-end-session", async (importOriginal) => ({ - ...(await importOriginal()), - useEndSession: vi.fn(() => endSessionMock), - })); }); const mockConversations = [ @@ -145,47 +136,6 @@ describe("ConversationPanel", () => { expect(cards).toHaveLength(3); }); - it("should call endSession after deleting a conversation that is the current session", async () => { - const user = userEvent.setup(); - const mockData = [...mockConversations]; - const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations"); - getUserConversationsSpy.mockImplementation(async () => mockData); - - const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation"); - deleteUserConversationSpy.mockImplementation(async (id: string) => { - const index = mockData.findIndex(conv => conv.conversation_id === id); - if (index !== -1) { - mockData.splice(index, 1); - } - // Wait for React Query to update its cache - await new Promise(resolve => setTimeout(resolve, 0)); - }); - - renderConversationPanel(); - - let cards = await screen.findAllByTestId("conversation-card"); - const ellipsisButton = within(cards[1]).getByTestId("ellipsis-button"); - await user.click(ellipsisButton); - const deleteButton = screen.getByTestId("delete-button"); - - // Click the second delete button - await user.click(deleteButton); - - // Confirm the deletion - const confirmButton = screen.getByRole("button", { name: /confirm/i }); - await user.click(confirmButton); - - expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument(); - - // Wait for the cards to update with a longer timeout - await waitFor(() => { - const updatedCards = screen.getAllByTestId("conversation-card"); - expect(updatedCards).toHaveLength(2); - }, { timeout: 2000 }); - - expect(endSessionMock).toHaveBeenCalledOnce(); - }); - it("should delete a conversation", async () => { const user = userEvent.setup(); const mockData = [ diff --git a/frontend/__tests__/routes/_oh.app.test.tsx b/frontend/__tests__/routes/_oh.app.test.tsx index 489cb37210..1b35e0430f 100644 --- a/frontend/__tests__/routes/_oh.app.test.tsx +++ b/frontend/__tests__/routes/_oh.app.test.tsx @@ -13,15 +13,7 @@ describe("App", () => { { Component: App, path: "/conversation/:conversationId" }, ]); - const { endSessionMock } = vi.hoisted(() => ({ - endSessionMock: vi.fn(), - })); - beforeAll(() => { - vi.mock("#/hooks/use-end-session", () => ({ - useEndSession: vi.fn(() => endSessionMock), - })); - vi.mock("#/hooks/use-terminal", () => ({ useTerminal: vi.fn(), })); @@ -35,44 +27,4 @@ describe("App", () => { renderWithProviders(); await screen.findByTestId("app-route"); }); - - it("should call endSession if the user does not have permission to view conversation", async () => { - const getConversationSpy = vi.spyOn(OpenHands, "getConversation"); - - getConversationSpy.mockResolvedValue(null); - renderWithProviders(); - - await waitFor(() => { - expect(endSessionMock).toHaveBeenCalledOnce(); - expect(errorToastSpy).toHaveBeenCalledOnce(); - }); - }); - - it("should not call endSession if the user has permission", async () => { - const getConversationSpy = vi.spyOn(OpenHands, "getConversation"); - - getConversationSpy.mockResolvedValue({ - conversation_id: "9999", - last_updated_at: "", - created_at: "", - title: "", - selected_repository: "", - status: "STOPPED", - }); - const { rerender } = renderWithProviders( - , - ); - - await waitFor(() => { - expect(endSessionMock).not.toHaveBeenCalled(); - expect(errorToastSpy).not.toHaveBeenCalled(); - }); - - rerender(); - - await waitFor(() => { - expect(endSessionMock).not.toHaveBeenCalled(); - expect(errorToastSpy).not.toHaveBeenCalled(); - }); - }); }); diff --git a/frontend/src/components/features/browser/browser.tsx b/frontend/src/components/features/browser/browser.tsx index 00bf8e1965..444c699037 100644 --- a/frontend/src/components/features/browser/browser.tsx +++ b/frontend/src/components/features/browser/browser.tsx @@ -1,12 +1,26 @@ -import { useSelector } from "react-redux"; +import { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; import { RootState } from "#/store"; import { BrowserSnapshot } from "./browser-snapshot"; import { EmptyBrowserMessage } from "./empty-browser-message"; +import { useConversation } from "#/context/conversation-context"; +import { + initialState as browserInitialState, + setUrl, + setScreenshotSrc, +} from "#/state/browser-slice"; export function BrowserPanel() { const { url, screenshotSrc } = useSelector( (state: RootState) => state.browser, ); + const { conversationId } = useConversation(); + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(setUrl(browserInitialState.url)); + dispatch(setScreenshotSrc(browserInitialState.screenshotSrc)); + }, [conversationId]); const imgSrc = screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,") diff --git a/frontend/src/components/features/conversation-panel/conversation-panel.tsx b/frontend/src/components/features/conversation-panel/conversation-panel.tsx index f35ed756ad..2558438dc3 100644 --- a/frontend/src/components/features/conversation-panel/conversation-panel.tsx +++ b/frontend/src/components/features/conversation-panel/conversation-panel.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { NavLink, useParams } from "react-router"; +import { NavLink, useParams, useNavigate } from "react-router"; import { useTranslation } from "react-i18next"; import { I18nKey } from "#/i18n/declaration"; import { ConversationCard } from "./conversation-card"; @@ -8,7 +8,6 @@ import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation" import { ConfirmDeleteModal } from "./confirm-delete-modal"; import { LoadingSpinner } from "#/components/shared/loading-spinner"; import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation"; -import { useEndSession } from "#/hooks/use-end-session"; import { ExitConversationModal } from "./exit-conversation-modal"; import { useClickOutsideElement } from "#/hooks/use-click-outside-element"; @@ -18,9 +17,9 @@ interface ConversationPanelProps { export function ConversationPanel({ onClose }: ConversationPanelProps) { const { t } = useTranslation(); - const { conversationId: cid } = useParams(); - const endSession = useEndSession(); + const { conversationId: currentConversationId } = useParams(); const ref = useClickOutsideElement(onClose); + const navigate = useNavigate(); const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] = React.useState(false); @@ -48,8 +47,8 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) { { conversationId: selectedConversationId }, { onSuccess: () => { - if (cid === selectedConversationId) { - endSession(); + if (selectedConversationId === currentConversationId) { + navigate("/"); } }, }, @@ -129,7 +128,6 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) { {confirmExitConversationModalVisible && ( { - endSession(); onClose(); }} onClose={() => setConfirmExitConversationModalVisible(false)} diff --git a/frontend/src/components/features/sidebar/sidebar.tsx b/frontend/src/components/features/sidebar/sidebar.tsx index 093044791b..59220eceda 100644 --- a/frontend/src/components/features/sidebar/sidebar.tsx +++ b/frontend/src/components/features/sidebar/sidebar.tsx @@ -1,34 +1,23 @@ import React from "react"; -import { FaListUl } from "react-icons/fa"; -import { useDispatch } from "react-redux"; import posthog from "posthog-js"; -import { NavLink, useLocation } from "react-router"; -import { useTranslation } from "react-i18next"; +import { useLocation } from "react-router"; import { useGitUser } from "#/hooks/query/use-git-user"; import { UserActions } from "./user-actions"; import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button"; import { DocsButton } from "#/components/shared/buttons/docs-button"; -import { ExitProjectButton } from "#/components/shared/buttons/exit-project-button"; +import { NewProjectButton } from "#/components/shared/buttons/new-project-button"; import { SettingsButton } from "#/components/shared/buttons/settings-button"; +import { ConversationPanelButton } from "#/components/shared/buttons/conversation-panel-button"; import { SettingsModal } from "#/components/shared/modals/settings/settings-modal"; import { useSettings } from "#/hooks/query/use-settings"; import { ConversationPanel } from "../conversation-panel/conversation-panel"; -import { useEndSession } from "#/hooks/use-end-session"; -import { setCurrentAgentState } from "#/state/agent-slice"; -import { AgentState } from "#/types/agent-state"; -import { TooltipButton } from "#/components/shared/buttons/tooltip-button"; import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper"; import { useLogout } from "#/hooks/mutation/use-logout"; import { useConfig } from "#/hooks/query/use-config"; -import { cn } from "#/utils/utils"; import { displayErrorToast } from "#/utils/custom-toast-handlers"; -import { I18nKey } from "#/i18n/declaration"; export function Sidebar() { - const { t } = useTranslation(); const location = useLocation(); - const dispatch = useDispatch(); - const endSession = useEndSession(); const user = useGitUser(); const { data: config } = useConfig(); const { @@ -73,11 +62,6 @@ export function Sidebar() { location.pathname, ]); - const handleEndSession = () => { - dispatch(setCurrentAgentState(AgentState.LOADING)); - endSession(); - }; - const handleLogout = async () => { await logout(); posthog.reset(); @@ -89,34 +73,18 @@ export function Sidebar() {