Download __tests__/components/features/chat/plan-preview.test.tsx from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 13.5 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/components/features/chat/plan-preview.test.tsx
- Command line
-
hf download hf://SaylorTwift/openhands/__tests__/components/features/chat/plan-preview.test.tsx
-
curl -L -o plan-preview.test.tsx https://huggingface.co/SaylorTwift/openhands/resolve/main/__tests__/components/features/chat/plan-preview.test.tsx
13.5 kB
| import React from "react"; | |
| import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; | |
| import { screen } from "@testing-library/react"; | |
| import userEvent from "@testing-library/user-event"; | |
| import { renderWithProviders } from "test-utils"; | |
| import { PlanPreview } from "#/components/features/chat/plan-preview"; | |
| import { ScrollProvider } from "#/context/scroll-context"; | |
| import { useConversationStore } from "#/stores/conversation-store"; | |
| import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store"; | |
| import { createChatMessage } from "#/services/chat-service"; | |
| // PlanPreview uses useScrollContext; wrap with ScrollProvider and a mock value. | |
| const mockScrollDomToBottom = vi.fn(); | |
| const scrollContextValue = { | |
| scrollRef: { current: null }, | |
| autoScroll: true, | |
| setAutoScroll: vi.fn(), | |
| scrollDomToBottom: mockScrollDomToBottom, | |
| hitBottom: true, | |
| setHitBottom: vi.fn(), | |
| onChatBodyScroll: vi.fn(), | |
| }; | |
| function renderPlanPreview(ui: React.ReactElement) { | |
| return renderWithProviders( | |
| <ScrollProvider value={scrollContextValue}>{ui}</ScrollProvider>, | |
| ); | |
| } | |
| // Mock i18n - need to preserve initReactI18next and I18nextProvider for test-utils | |
| vi.mock("react-i18next", async (importOriginal) => { | |
| const actual = await importOriginal<typeof import("react-i18next")>(); | |
| return { | |
| ...actual, | |
| useTranslation: () => ({ | |
| t: (key: string) => key, | |
| }), | |
| }; | |
| }); | |
| // Mock services (underlying dependencies of the hook) | |
| const mockSend = vi.fn().mockResolvedValue({ queued: false }); | |
| vi.mock("#/hooks/use-send-message", () => ({ | |
| useSendMessage: vi.fn(() => ({ | |
| send: mockSend, | |
| })), | |
| })); | |
| vi.mock("#/services/chat-service", () => ({ | |
| createChatMessage: vi.fn((content, imageUrls, fileUrls, timestamp) => ({ | |
| action: "message", | |
| args: { content, image_urls: imageUrls, file_urls: fileUrls, timestamp }, | |
| })), | |
| })); | |
| vi.mock("#/hooks/use-conversation-id", () => ({ | |
| useConversationId: () => ({ conversationId: "test-conversation-id" }), | |
| useOptionalConversationId: () => ({ conversationId: "test-conversation-id" }), | |
| })); | |
| describe("PlanPreview", () => { | |
| beforeEach(() => { | |
| vi.clearAllMocks(); | |
| mockSend.mockResolvedValue({ queued: false }); | |
| // Reset store states | |
| localStorage.clear(); | |
| useOptimisticUserMessageStore.setState({ | |
| pendingMessages: [], | |
| }); | |
| useConversationStore.setState({ | |
| conversationMode: "plan", | |
| selectedTab: null, | |
| isRightPanelShown: false, | |
| hasRightPanelToggled: false, | |
| }); | |
| }); | |
| afterEach(() => { | |
| vi.clearAllMocks(); | |
| // Clean up store states | |
| useConversationStore.setState({ | |
| conversationMode: "code", | |
| }); | |
| useOptimisticUserMessageStore.setState({ | |
| pendingMessages: [], | |
| }); | |
| localStorage.clear(); | |
| }); | |
| it("should render nothing when planContent is null", () => { | |
| // Arrange & Act | |
| const { container } = renderPlanPreview(<PlanPreview planContent={null} />); | |
| // Assert | |
| expect(container.firstChild).toBeNull(); | |
| }); | |
| it("should render nothing when planContent is undefined", () => { | |
| // Arrange & Act | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={undefined} />, | |
| ); | |
| // Assert | |
| expect(container.firstChild).toBeNull(); | |
| }); | |
| it("should render markdown content when planContent is provided", () => { | |
| const planContent = "# Plan Title\n\nThis is the plan content."; | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={planContent} />, | |
| ); | |
| // Check that component rendered and contains the content (markdown may break up text) | |
| expect(container.firstChild).not.toBeNull(); | |
| expect(container.textContent).toContain("Plan Title"); | |
| expect(container.textContent).toContain("This is the plan content."); | |
| }); | |
| it("should render full content when length is less than or equal to 300 characters", () => { | |
| const planContent = "A".repeat(300); | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={planContent} />, | |
| ); | |
| // Content should be present (may be broken up by markdown) | |
| expect(container.textContent).toContain(planContent); | |
| expect(screen.queryByText(/COMMON\$READ_MORE/i)).not.toBeInTheDocument(); | |
| }); | |
| it("should truncate content when length exceeds 300 characters", () => { | |
| const longContent = "A".repeat(350); | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={longContent} />, | |
| ); | |
| // Truncated content should be present (may be broken up by markdown) | |
| expect(container.textContent).toContain("A".repeat(300)); | |
| expect(container.textContent).toContain("..."); | |
| expect(container.textContent).toContain("COMMON$READ_MORE"); | |
| }); | |
| it("should render Build button", () => { | |
| renderPlanPreview(<PlanPreview planContent="Plan content" />); | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| expect(buildButton).toBeInTheDocument(); | |
| }); | |
| it("should switch to code mode when Build button is clicked", async () => { | |
| // Arrange | |
| useConversationStore.setState({ conversationMode: "plan" }); | |
| const user = userEvent.setup(); | |
| renderPlanPreview(<PlanPreview planContent="Plan content" />); | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| // Act | |
| await user.click(buildButton); | |
| // Assert | |
| expect(useConversationStore.getState().conversationMode).toBe("code"); | |
| }); | |
| it("should send build prompt message when Build button is clicked", async () => { | |
| // Arrange | |
| const user = userEvent.setup(); | |
| const expectedPrompt = | |
| "Execute the plan based on the .agents_tmp/PLAN.md file."; | |
| renderPlanPreview(<PlanPreview planContent="Plan content" />); | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| // Act | |
| await user.click(buildButton); | |
| // Assert | |
| expect(createChatMessage).toHaveBeenCalledTimes(1); | |
| expect(createChatMessage).toHaveBeenCalledWith( | |
| expectedPrompt, | |
| [], | |
| [], | |
| expect.any(String), | |
| ); | |
| expect(mockSend).toHaveBeenCalledTimes(1); | |
| expect(mockSend).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| action: "message", | |
| args: expect.objectContaining({ | |
| content: expectedPrompt, | |
| }), | |
| }), | |
| ); | |
| }); | |
| it("should enqueue a pending user message when Build button is clicked", async () => { | |
| // Arrange | |
| useOptimisticUserMessageStore.setState({ pendingMessages: [] }); | |
| const user = userEvent.setup(); | |
| const expectedPrompt = | |
| "Execute the plan based on the .agents_tmp/PLAN.md file."; | |
| renderPlanPreview(<PlanPreview planContent="Plan content" />); | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| // Act | |
| await user.click(buildButton); | |
| // Assert | |
| const pending = useOptimisticUserMessageStore.getState().pendingMessages; | |
| expect(pending).toHaveLength(1); | |
| expect(pending[0].text).toBe(expectedPrompt); | |
| expect(pending[0].status).toBe("sending"); | |
| }); | |
| it("should disable Build button when isBuildDisabled is true", () => { | |
| // Arrange | |
| renderPlanPreview( | |
| <PlanPreview planContent="Plan content" isBuildDisabled={true} />, | |
| ); | |
| // Act | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| // Assert | |
| expect(buildButton).toBeDisabled(); | |
| }); | |
| it("should not disable Build button when isBuildDisabled is false", () => { | |
| // Arrange | |
| renderPlanPreview( | |
| <PlanPreview planContent="Plan content" isBuildDisabled={false} />, | |
| ); | |
| // Act | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| // Assert | |
| expect(buildButton).not.toBeDisabled(); | |
| }); | |
| it("should not disable Build button when isBuildDisabled is undefined", () => { | |
| // Arrange | |
| renderPlanPreview(<PlanPreview planContent="Plan content" />); | |
| // Act | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| // Assert | |
| expect(buildButton).not.toBeDisabled(); | |
| }); | |
| it("should not call onBuildClick when Build button is disabled and clicked", async () => { | |
| // Arrange | |
| const user = userEvent.setup(); | |
| const onBuildClick = vi.fn(); | |
| renderPlanPreview( | |
| <PlanPreview planContent="Plan content" isBuildDisabled={true} />, | |
| ); | |
| const buildButton = screen.getByTestId("plan-preview-build-button"); | |
| // Act | |
| await user.click(buildButton); | |
| // Assert | |
| expect(onBuildClick).not.toHaveBeenCalled(); | |
| }); | |
| it("should render header with PLAN_MD text", () => { | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent="Plan content" />, | |
| ); | |
| // Check that the translation key is rendered (i18n mock returns the key) | |
| expect(container.textContent).toContain("COMMON$PLAN_MD"); | |
| }); | |
| it("should render plan content", () => { | |
| const planContent = `# Heading 1 | |
| ## Heading 2 | |
| - List item 1 | |
| - List item 2 | |
| **Bold text** and *italic text*`; | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={planContent} />, | |
| ); | |
| expect(container.textContent).toContain("Heading 1"); | |
| expect(container.textContent).toContain("Heading 2"); | |
| }); | |
| it("should use planHeadings components for h1 headings", () => { | |
| // Arrange | |
| const planContent = "# Main Title"; | |
| // Act | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={planContent} />, | |
| ); | |
| // Assert | |
| const h1 = container.querySelector("h1"); | |
| expect(h1).toBeInTheDocument(); | |
| expect(h1).toHaveTextContent("Main Title"); | |
| }); | |
| it("should use planHeadings components for h2 headings", () => { | |
| // Arrange | |
| const planContent = "## Section Title"; | |
| // Act | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={planContent} />, | |
| ); | |
| // Assert | |
| const h2 = container.querySelector("h2"); | |
| expect(h2).toBeInTheDocument(); | |
| expect(h2).toHaveTextContent("Section Title"); | |
| }); | |
| it("should use planHeadings components for h3 headings", () => { | |
| // Arrange | |
| const planContent = "### Subsection Title"; | |
| // Act | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={planContent} />, | |
| ); | |
| // Assert | |
| const h3 = container.querySelector("h3"); | |
| expect(h3).toBeInTheDocument(); | |
| expect(h3).toHaveTextContent("Subsection Title"); | |
| }); | |
| it("should use planHeadings components for all heading levels", () => { | |
| // Arrange | |
| const planContent = `# H1 Title | |
| ## H2 Title | |
| ### H3 Title | |
| #### H4 Title | |
| ##### H5 Title | |
| ###### H6 Title`; | |
| // Act | |
| const { container } = renderPlanPreview( | |
| <PlanPreview planContent={planContent} />, | |
| ); | |
| // Assert | |
| expect(container.querySelector("h1")).toBeInTheDocument(); | |
| expect(container.querySelector("h2")).toBeInTheDocument(); | |
| expect(container.querySelector("h3")).toBeInTheDocument(); | |
| expect(container.querySelector("h4")).toBeInTheDocument(); | |
| expect(container.querySelector("h5")).toBeInTheDocument(); | |
| expect(container.querySelector("h6")).toBeInTheDocument(); | |
| }); | |
| it("should call selectTab with 'planner' when View button is clicked", async () => { | |
| const user = userEvent.setup(); | |
| const planContent = "Plan content"; | |
| const conversationId = "test-conversation-id"; | |
| // Arrange: Set up initial state | |
| useConversationStore.setState({ | |
| selectedTab: null, | |
| isRightPanelShown: false, | |
| hasRightPanelToggled: false, | |
| }); | |
| renderPlanPreview(<PlanPreview planContent={planContent} />); | |
| // Act: Click the View button | |
| const viewButton = screen.getByTestId("plan-preview-view-button"); | |
| await user.click(viewButton); | |
| // Assert: selectTab was called with 'planner' and the drawer opened. | |
| // Opening the drawer also mirrors `rightPanelShown` into the | |
| // conversation's localStorage blob, alongside the selected tab. | |
| expect(useConversationStore.getState().selectedTab).toBe("planner"); | |
| expect(useConversationStore.getState().hasRightPanelToggled).toBe(true); | |
| const storedState = JSON.parse( | |
| localStorage.getItem(`conversation-state-${conversationId}`)!, | |
| ); | |
| expect(storedState.selectedTab).toBe("planner"); | |
| expect(storedState.rightPanelShown).toBe(true); | |
| }); | |
| it("should call selectTab with 'planner' when Read more button is clicked", async () => { | |
| const user = userEvent.setup(); | |
| const longContent = "A".repeat(350); | |
| const conversationId = "test-conversation-id"; | |
| // Arrange: Set up initial state | |
| useConversationStore.setState({ | |
| selectedTab: null, | |
| isRightPanelShown: false, | |
| hasRightPanelToggled: false, | |
| }); | |
| renderPlanPreview(<PlanPreview planContent={longContent} />); | |
| // Act: Click the Read more button | |
| const readMoreButton = screen.getByTestId("plan-preview-read-more-button"); | |
| await user.click(readMoreButton); | |
| // Assert: selectTab was called with 'planner' and the drawer opened. | |
| // Opening the drawer also mirrors `rightPanelShown` into the | |
| // conversation's localStorage blob, alongside the selected tab. | |
| expect(useConversationStore.getState().selectedTab).toBe("planner"); | |
| expect(useConversationStore.getState().hasRightPanelToggled).toBe(true); | |
| const storedState = JSON.parse( | |
| localStorage.getItem(`conversation-state-${conversationId}`)!, | |
| ); | |
| expect(storedState.selectedTab).toBe("planner"); | |
| expect(storedState.rightPanelShown).toBe(true); | |
| }); | |
| }); | |