mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-23 06:46:40 +08:00
Fix: Disable Auto-scroll when user looks back through historical chat-Bug 9062 (#9107)
### What problem does this PR solve? This code allows user chat to auto-scroll down when entered, but if user scrolls up away from the generative feedback, autoscroll is disabled. Close #9062 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --------- Co-authored-by: Charles Copley <ccopley@ancera.com>
This commit is contained in:
127
web/src/hooks/__tests__/logic-hooks.useScrollToBottom.test.tsx
Normal file
127
web/src/hooks/__tests__/logic-hooks.useScrollToBottom.test.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
jest.mock('eventsource-parser/stream', () => ({}));
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useScrollToBottom } from '../logic-hooks';
|
||||
|
||||
function createMockContainer({ atBottom = true } = {}) {
|
||||
const scrollTop = atBottom ? 100 : 0;
|
||||
const clientHeight = 100;
|
||||
const scrollHeight = 200;
|
||||
const listeners = {};
|
||||
return {
|
||||
current: {
|
||||
scrollTop,
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
addEventListener: jest.fn((event, cb) => {
|
||||
listeners[event] = cb;
|
||||
}),
|
||||
removeEventListener: jest.fn(),
|
||||
},
|
||||
listeners,
|
||||
} as any;
|
||||
}
|
||||
|
||||
// Helper to flush all timers and microtasks
|
||||
async function flushAll() {
|
||||
jest.runAllTimers();
|
||||
// Flush microtasks
|
||||
await Promise.resolve();
|
||||
// Sometimes, effects queue more timers, so run again
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe('useScrollToBottom', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should set isAtBottom true when user is at bottom', () => {
|
||||
const containerRef = createMockContainer({ atBottom: true });
|
||||
const { result } = renderHook(() => useScrollToBottom([], containerRef));
|
||||
expect(result.current.isAtBottom).toBe(true);
|
||||
});
|
||||
|
||||
it('should set isAtBottom false when user is not at bottom', () => {
|
||||
const containerRef = createMockContainer({ atBottom: false });
|
||||
const { result } = renderHook(() => useScrollToBottom([], containerRef));
|
||||
expect(result.current.isAtBottom).toBe(false);
|
||||
});
|
||||
|
||||
it('should scroll to bottom when isAtBottom is true and messages change', async () => {
|
||||
const containerRef = createMockContainer({ atBottom: true });
|
||||
const mockScroll = jest.fn();
|
||||
|
||||
function useTestScrollToBottom(messages: any, containerRef: any) {
|
||||
const hook = useScrollToBottom(messages, containerRef);
|
||||
hook.scrollRef.current = { scrollIntoView: mockScroll } as any;
|
||||
return hook;
|
||||
}
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ messages }) => useTestScrollToBottom(messages, containerRef),
|
||||
{ initialProps: { messages: [] } },
|
||||
);
|
||||
|
||||
rerender({ messages: ['msg1'] });
|
||||
await flushAll();
|
||||
|
||||
expect(mockScroll).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT scroll to bottom when isAtBottom is false and messages change', async () => {
|
||||
const containerRef = createMockContainer({ atBottom: false });
|
||||
const mockScroll = jest.fn();
|
||||
|
||||
function useTestScrollToBottom(messages: any, containerRef: any) {
|
||||
const hook = useScrollToBottom(messages, containerRef);
|
||||
hook.scrollRef.current = { scrollIntoView: mockScroll } as any;
|
||||
console.log('HOOK: isAtBottom:', hook.isAtBottom);
|
||||
return hook;
|
||||
}
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
({ messages }) => useTestScrollToBottom(messages, containerRef),
|
||||
{ initialProps: { messages: [] } },
|
||||
);
|
||||
|
||||
// Simulate user scrolls up before messages change
|
||||
await act(async () => {
|
||||
containerRef.current.scrollTop = 0;
|
||||
containerRef.current.addEventListener.mock.calls[0][1]();
|
||||
await flushAll();
|
||||
// Advance fake timers by 10ms instead of real setTimeout
|
||||
jest.advanceTimersByTime(10);
|
||||
console.log('AFTER SCROLL: isAtBottom:', result.current.isAtBottom);
|
||||
});
|
||||
|
||||
rerender({ messages: ['msg1'] });
|
||||
await flushAll();
|
||||
|
||||
console.log('AFTER RERENDER: isAtBottom:', result.current.isAtBottom);
|
||||
|
||||
expect(mockScroll).not.toHaveBeenCalled();
|
||||
|
||||
// Optionally, flush again after the assertion to see if it gets called late
|
||||
await flushAll();
|
||||
});
|
||||
|
||||
it('should indicate button should appear when user is not at bottom', () => {
|
||||
const containerRef = createMockContainer({ atBottom: false });
|
||||
const { result } = renderHook(() => useScrollToBottom([], containerRef));
|
||||
// The button should appear in the UI when isAtBottom is false
|
||||
expect(result.current.isAtBottom).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
const originalRAF = global.requestAnimationFrame;
|
||||
beforeAll(() => {
|
||||
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
||||
});
|
||||
afterAll(() => {
|
||||
global.requestAnimationFrame = originalRAF;
|
||||
});
|
||||
Reference in New Issue
Block a user