119 lines
3.0 KiB
TypeScript
119 lines
3.0 KiB
TypeScript
import { render } from '@testing-library/react';
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import { AgentThread } from '../../src/components/agent-workspace/agent-thread';
|
|
|
|
vi.mock('sonner', () => ({
|
|
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
|
|
}));
|
|
|
|
type Message = {
|
|
_id: string;
|
|
_creationTime: number;
|
|
role: 'user' | 'assistant';
|
|
content: string;
|
|
status?: string;
|
|
};
|
|
|
|
const message = (id: string, content: string): Message => ({
|
|
_id: id,
|
|
_creationTime: Date.now(),
|
|
role: 'assistant',
|
|
content,
|
|
status: 'complete',
|
|
});
|
|
|
|
const baseProps = {
|
|
jobId: 'job-1',
|
|
events: [],
|
|
interactions: [],
|
|
workspaceChanges: [],
|
|
disabled: false,
|
|
agentTurnActive: true,
|
|
onOpenFile: vi.fn(),
|
|
onOpenDiff: vi.fn(),
|
|
};
|
|
|
|
const renderThread = (messages: Message[]) =>
|
|
render(
|
|
// Types are exercised at runtime; cast the fixture shapes to satisfy the
|
|
// Doc<> generics without pulling the full backend types into the test.
|
|
<AgentThread {...baseProps} messages={messages as never} />,
|
|
);
|
|
|
|
/** Stubs the scroll geometry of the messages list so we control near-bottom. */
|
|
const stubScrollGeometry = (
|
|
container: HTMLElement,
|
|
geometry: { scrollHeight: number; scrollTop: number; clientHeight: number },
|
|
) => {
|
|
const node = container.querySelector<HTMLElement>('.overflow-y-auto');
|
|
if (!node) throw new Error('scroll container not found');
|
|
const scrollTo = vi.fn();
|
|
Object.defineProperty(node, 'scrollTo', {
|
|
configurable: true,
|
|
value: scrollTo,
|
|
});
|
|
Object.defineProperty(node, 'scrollHeight', {
|
|
configurable: true,
|
|
get: () => geometry.scrollHeight,
|
|
});
|
|
Object.defineProperty(node, 'clientHeight', {
|
|
configurable: true,
|
|
get: () => geometry.clientHeight,
|
|
});
|
|
let scrollTop = geometry.scrollTop;
|
|
Object.defineProperty(node, 'scrollTop', {
|
|
configurable: true,
|
|
get: () => scrollTop,
|
|
set: (value: number) => {
|
|
scrollTop = value;
|
|
},
|
|
});
|
|
return { node, scrollTo };
|
|
};
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('AgentThread auto-scroll', () => {
|
|
it('does not yank to the bottom while the user is scrolled up', () => {
|
|
const { container, rerender } = renderThread([message('m1', 'first')]);
|
|
const { scrollTo } = stubScrollGeometry(container, {
|
|
scrollHeight: 1000,
|
|
scrollTop: 100,
|
|
clientHeight: 200,
|
|
});
|
|
|
|
rerender(
|
|
<AgentThread
|
|
{...baseProps}
|
|
messages={[message('m1', 'first'), message('m2', 'second')] as never}
|
|
/>,
|
|
);
|
|
|
|
expect(scrollTo).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('auto-scrolls to newest when already near the bottom', () => {
|
|
const { container, rerender } = renderThread([message('m1', 'first')]);
|
|
const { scrollTo } = stubScrollGeometry(container, {
|
|
scrollHeight: 1000,
|
|
scrollTop: 780,
|
|
clientHeight: 200,
|
|
});
|
|
|
|
rerender(
|
|
<AgentThread
|
|
{...baseProps}
|
|
messages={[message('m1', 'first'), message('m2', 'second')] as never}
|
|
/>,
|
|
);
|
|
|
|
expect(scrollTo).toHaveBeenCalledWith({
|
|
top: 1000,
|
|
behavior: 'smooth',
|
|
});
|
|
});
|
|
});
|