Compare commits

...

2 Commits

Author SHA1 Message Date
Engel Nyst
3a4f1bbeec Update frontend/src/hooks/__tests__/use-scroll-to-bottom.test.ts
Co-authored-by: Erkin Alp Güney <erkinalp9035@gmail.com>
2025-05-01 19:32:01 +02:00
openhands
e64e730240 Fix issue #8208: [Bug]: Auto-scroll not working consistently in chat tab 2025-05-01 16:47:14 +00:00
3 changed files with 92 additions and 7 deletions

View File

@@ -46,6 +46,13 @@ export function ChatInterface() {
const { messages } = useSelector((state: RootState) => state.chat);
const { curAgentState } = useSelector((state: RootState) => state.agent);
// Trigger auto-scroll when new messages arrive or agent state changes
React.useEffect(() => {
if (messages.length > 0 && scrollRef.current) {
scrollDomToBottom();
}
}, [messages.length, curAgentState, scrollDomToBottom]);
const [feedbackPolarity, setFeedbackPolarity] = React.useState<
"positive" | "negative"
>("positive");

View File

@@ -0,0 +1,71 @@
import { renderHook, act } from "@testing-library/react";
import { describe, it, expect, beforeEach, vi } from "vitest";
import { useScrollToBottom } from "../use-scroll-to-bottom";
describe("useScrollToBottom", () => {
let mockRef: { current: HTMLDivElement | null };
let mockDom: HTMLDivElement;
beforeEach(() => {
mockDom = {
scrollTop: 0,
clientHeight: 500,
scrollHeight: 1000,
scrollTo: vi.fn(),
} as unknown as HTMLDivElement;
mockRef = { current: mockDom };
});
it("should auto-scroll when shouldScrollToBottom is true", () => {
vi.useFakeTimers();
const { result } = renderHook(() => useScrollToBottom(mockRef));
// Verify initial state
expect(result.current.autoScroll).toBe(true);
expect(result.current.hitBottom).toBe(true);
// Trigger auto-scroll
act(() => {
result.current.scrollDomToBottom();
});
// Fast-forward timers
act(() => {
vi.runAllTimers();
});
// Verify scrollTo was called with correct parameters
expect(mockDom.scrollTo).toHaveBeenCalledWith({
top: mockDom.scrollHeight,
behavior: "smooth",
});
vi.useRealTimers();
});
it("should update scroll state when user scrolls", () => {
const { result } = renderHook(() => useScrollToBottom(mockRef));
// Simulate scroll near bottom
act(() => {
mockDom.scrollTop = mockDom.scrollHeight - mockDom.clientHeight - 40;
result.current.onChatBodyScroll(mockDom);
});
// Should be considered at bottom due to threshold
expect(result.current.hitBottom).toBe(true);
expect(result.current.autoScroll).toBe(true);
// Simulate scroll away from bottom
act(() => {
mockDom.scrollTop = 0;
result.current.onChatBodyScroll(mockDom);
});
// Should not be at bottom
expect(result.current.hitBottom).toBe(false);
expect(result.current.autoScroll).toBe(false);
});
});

View File

@@ -9,7 +9,7 @@ export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement | null>) {
// Check if the scroll position is at the bottom
const isAtBottom = useCallback((element: HTMLElement): boolean => {
const bottomThreshold = 10; // Pixels from bottom to consider "at bottom"
const bottomThreshold = 50; // Increased threshold to be more lenient
const bottomPosition = element.scrollTop + element.clientHeight;
return bottomPosition >= element.scrollHeight - bottomThreshold;
}, []);
@@ -51,15 +51,22 @@ export function useScrollToBottom(scrollRef: RefObject<HTMLDivElement | null>) {
if (shouldScrollToBottom) {
const dom = scrollRef.current;
if (dom) {
requestAnimationFrame(() => {
dom.scrollTo({
top: dom.scrollHeight,
behavior: "smooth",
// Use a short timeout to ensure content has been rendered
setTimeout(() => {
requestAnimationFrame(() => {
dom.scrollTo({
top: dom.scrollHeight,
behavior: "smooth",
});
// Check if we actually reached the bottom
if (isAtBottom(dom)) {
setHitBottom(true);
}
});
});
}, 50);
}
}
});
}, [scrollRef, shouldScrollToBottom, isAtBottom]);
return {
scrollRef,