diff --git a/frontend/__tests__/routes/planner-tab.test.tsx b/frontend/__tests__/routes/planner-tab.test.tsx index 8f139ffc5f..011f8649ed 100644 --- a/frontend/__tests__/routes/planner-tab.test.tsx +++ b/frontend/__tests__/routes/planner-tab.test.tsx @@ -1,5 +1,5 @@ -import { screen } from "@testing-library/react"; -import { describe, expect, it, vi, beforeEach } from "vitest"; +import { screen, act } from "@testing-library/react"; +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import PlannerTab from "#/routes/planner-tab"; import { renderWithProviders } from "../../test-utils"; import { useConversationStore } from "#/stores/conversation-store"; @@ -12,8 +12,15 @@ vi.mock("#/hooks/use-handle-plan-click", () => ({ })); describe("PlannerTab", () => { + const originalRAF = global.requestAnimationFrame; + beforeEach(() => { vi.clearAllMocks(); + // Make requestAnimationFrame execute synchronously for testing + global.requestAnimationFrame = (cb: FrameRequestCallback) => { + cb(0); + return 0; + }; // Reset store state to defaults useConversationStore.setState({ planContent: null, @@ -21,6 +28,10 @@ describe("PlannerTab", () => { }); }); + afterEach(() => { + global.requestAnimationFrame = originalRAF; + }); + describe("Create a plan button", () => { it("should be enabled when conversation mode is 'code'", () => { // Arrange @@ -52,4 +63,71 @@ describe("PlannerTab", () => { expect(button).toBeDisabled(); }); }); + + describe("Auto-scroll behavior", () => { + it("should scroll to bottom when plan content is updated", () => { + // Arrange + const scrollTopSetter = vi.fn(); + const mockScrollHeight = 500; + + // Mock scroll properties on HTMLElement prototype + const originalScrollHeightDescriptor = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + "scrollHeight", + ); + const originalScrollTopDescriptor = Object.getOwnPropertyDescriptor( + HTMLElement.prototype, + "scrollTop", + ); + + Object.defineProperty(HTMLElement.prototype, "scrollHeight", { + get: () => mockScrollHeight, + configurable: true, + }); + Object.defineProperty(HTMLElement.prototype, "scrollTop", { + get: () => 0, + set: scrollTopSetter, + configurable: true, + }); + + try { + // Render with initial plan content + useConversationStore.setState({ + planContent: "# Initial Plan", + conversationMode: "plan", + }); + + renderWithProviders(); + + // Clear calls from initial render + scrollTopSetter.mockClear(); + + // Act - Update plan content which should trigger auto-scroll + act(() => { + useConversationStore.setState({ + planContent: "# Updated Plan\n\nMore content added here.", + }); + }); + + // Assert - scrollTop should be set to scrollHeight + expect(scrollTopSetter).toHaveBeenCalledWith(mockScrollHeight); + } finally { + // Restore original descriptors + if (originalScrollHeightDescriptor) { + Object.defineProperty( + HTMLElement.prototype, + "scrollHeight", + originalScrollHeightDescriptor, + ); + } + if (originalScrollTopDescriptor) { + Object.defineProperty( + HTMLElement.prototype, + "scrollTop", + originalScrollTopDescriptor, + ); + } + } + }); + }); }); diff --git a/frontend/src/routes/planner-tab.tsx b/frontend/src/routes/planner-tab.tsx index 11e5f8e3c0..3355971136 100644 --- a/frontend/src/routes/planner-tab.tsx +++ b/frontend/src/routes/planner-tab.tsx @@ -11,11 +11,22 @@ import { cn } from "#/utils/utils"; function PlannerTab() { const { t } = useTranslation(); - const { scrollRef: scrollContainerRef, onChatBodyScroll } = useScrollToBottom( - React.useRef(null), - ); + const scrollRef = React.useRef(null); + const { + scrollRef: scrollContainerRef, + onChatBodyScroll, + autoScroll, + scrollDomToBottom, + } = useScrollToBottom(scrollRef); const { planContent, conversationMode } = useConversationStore(); + + // Auto-scroll to bottom when plan content changes + React.useEffect(() => { + if (autoScroll) { + scrollDomToBottom(); + } + }, [planContent, autoScroll, scrollDomToBottom]); const isPlanMode = conversationMode === "plan"; const { handlePlanClick } = useHandlePlanClick();