From b4f00379b822723132961c822d3b3701d33e5be5 Mon Sep 17 00:00:00 2001
From: Hiep Le <69354317+hieptl@users.noreply.github.com>
Date: Fri, 13 Mar 2026 23:47:03 +0700
Subject: [PATCH] fix(frontend): auto-scroll not working in Planner tab when
plan content updates (#13355)
---
.../__tests__/routes/planner-tab.test.tsx | 82 ++++++++++++++++++-
frontend/src/routes/planner-tab.tsx | 17 +++-
2 files changed, 94 insertions(+), 5 deletions(-)
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();