From feea786c93ee068df4b2f2c2227b1195cc91b603 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 16 Oct 2026 02:54:08 -0700 Subject: [PATCH] test: prove diff reveal cancels a paging rail jump (#29585) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- ...ChatMessageList.rail-history-jump.test.tsx | 207 +++++++++--------- 1 file changed, 101 insertions(+), 106 deletions(-) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx index d2699f93c43..85f1e5ae936 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.rail-history-jump.test.tsx @@ -44,7 +44,6 @@ import { layout, marker, overrideLayoutProperty, - scrollTranscript, session, stubLayout, stubResizeObserver @@ -270,8 +269,10 @@ describe('jumping from the rail while following the end', () => { fireEvent.click(screen.getByRole('button', { name: prompt })) if (loaded) { expect(Math.abs(rowOffsetFromViewportTop(prompt))).toBeLessThanOrEqual(2) + await frame() + expect(Math.abs(rowOffsetFromViewportTop(prompt))).toBeLessThanOrEqual(2) } - await settle(60) + await settle(loaded ? 59 : 60) expect(HTMLElement.prototype.scrollTo).toHaveBeenCalled() expect(HTMLElement.prototype.scrollTo).not.toHaveBeenCalledWith( @@ -424,115 +425,109 @@ describe('jumping from the rail while following the end', () => { expect(distanceFromBottom()).toBe(0) }) - it('lands a jump to a loaded message within one frame', async () => { - render() - await settle(10) - - fireEvent.click(screen.getByRole('button', { name: 'prompt-45' })) - await frame() - - // A smooth scroll would still be easing in here, a few pixels from where it started. - expect(Math.abs(rowOffsetFromViewportTop('prompt-45'))).toBeLessThanOrEqual(2) - }) -}) - -describe('revealing a diff while a rail jump pages', () => { - let restoreLayout = (): void => {} - beforeEach(() => { - restoreLayout = stubLayout() - }) - afterEach(() => { - restoreLayout() - vi.restoreAllMocks() - }) - - function journalItem(itemId: string, body: AgentJournalItemBody, sequence: number) { - return { itemId, body, sequence, observedAt: sequence * 1000, revision: 1 } - } - function prompt(itemId: string, text: string, sequence: number) { - return journalItem( - itemId, - { kind: 'message', role: 'user', blocks: [{ type: 'text', text }] }, - sequence - ) - } - - const patch = '@@ -1 +1 @@\n-before\n+after' - const OLDER = prompt('older', 'Oldest prompt', 1) - const LOADED: AgentJournalRenderItem[] = [ - prompt('user', 'Edit it', 2), - journalItem( - 'diff', - { - kind: 'diff', - path: 'src/a.ts', - patch: { head: patch, truncated: false, digest: 'fixture', byteLength: patch.length } - }, - 3 - ), - prompt('user-2', 'Second prompt', 4), - prompt('user-3', 'Third prompt', 5), - ...Array.from({ length: 200 }, (_, index) => - journalItem( - `tail-${index}`, - { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text: `marker-${index}` }] }, - index + 6 + describe('revealing a diff while a rail jump pages', () => { + function journalItem(itemId: string, body: AgentJournalItemBody, sequence: number) { + return { itemId, body, sequence, observedAt: sequence * 1000, revision: 1 } + } + function prompt(itemId: string, text: string, sequence: number) { + return journalItem( + itemId, + { kind: 'message', role: 'user', blocks: [{ type: 'text', text }] }, + sequence ) - ) - ] + } - function DiffTranscript({ holdPage }: { holdPage: () => Promise }): React.JSX.Element { - const [loadedOlder, setLoadedOlder] = useState(false) - const items = useMemo(() => (loadedOlder ? [OLDER, ...LOADED] : LOADED), [loadedOlder]) - const loadEarlier = useCallback(async (): Promise => { - await holdPage() - setLoadedOlder(true) - return 'applied' - }, [holdPage]) - return ( - - ) - } + const patch = '@@ -1 +1 @@\n-before\n+after' + const OLDER = prompt('older', 'Oldest prompt', 1) + const LOADED: AgentJournalRenderItem[] = [ + prompt('user', 'Edit it', 2), + journalItem( + 'diff', + { + kind: 'diff', + path: 'src/a.ts', + patch: { head: patch, truncated: false, digest: 'fixture', byteLength: patch.length } + }, + 3 + ), + ...Array.from({ length: 200 }, (_, index) => + journalItem( + `tail-${index}`, + { + kind: 'message', + role: 'assistant', + blocks: [{ type: 'text', text: `marker-${index}` }] + }, + index + 4 + ) + ) + ] - it('keeps the diff in view when its page lands', async () => { - let release = (): void => {} - const holdPage = vi.fn( - () => - new Promise((resolve) => { - release = resolve - }) - ) - const scrollTo = vi.fn() - vi.spyOn(HTMLElement.prototype, 'scrollTo').mockImplementation(scrollTo) - const { container } = render() + function DiffTranscript({ holdPage }: { holdPage: () => Promise }): React.JSX.Element { + const [loadedOlder, setLoadedOlder] = useState(false) + const items = useMemo(() => (loadedOlder ? [OLDER, ...LOADED] : LOADED), [loadedOlder]) + const loadEarlier = useCallback(async (): Promise => { + await holdPage() + setLoadedOlder(true) + return 'applied' + }, [holdPage]) + return ( + + ) + } - fireEvent.click(screen.getByRole('button', { name: 'Oldest prompt' })) - // Anti-vacuous: the older page is in flight. - expect(holdPage).toHaveBeenCalledTimes(1) - fireEvent.click(screen.getByRole('button', { name: /1 changed file/ })) - fireEvent.click(screen.getByRole('button', { name: /src\/a.ts/ })) - scrollTranscript(container, 6000) - expect(screen.getByText('Edited')).toBeInTheDocument() - scrollTo.mockClear() + function expectDiffInView(): void { + expect( + screen.getByRole('button', { name: /^Edited .*a\.ts(?:\s|$)/, expanded: true }) + ).toBeInTheDocument() + const row = screen.getByText('Edited').closest('[data-index]') + if (!row) { + throw new Error('revealed diff has no mounted row') + } + const top = layout.aboveTranscriptPx + row.offsetTop - scroller().scrollTop + expect(row.offsetHeight).toBeGreaterThan(0) + expect(top).toBeGreaterThanOrEqual(-2) + expect(top).toBeLessThan(scroller().clientHeight - 2) + } - await act(async () => { - release() - await new Promise((resolve) => setTimeout(resolve, 0)) + it('keeps the diff in view when its page lands', async () => { + const pages = holdFirstPage() + render() + await settle(10) + expect(distanceFromBottom()).toBe(0) + expect(screen.queryByText('Edited')).toBeNull() + + await pickUnloadedWhilePaging('Oldest prompt') + expect(pages.asked()).toBe(1) + expect(screen.getByRole('button', { name: 'Oldest prompt' })).toHaveAttribute( + 'aria-busy', + 'true' + ) + + fireEvent.click(screen.getByRole('button', { name: /1 changed file/ })) + fireEvent.click(screen.getByRole('button', { name: /src\/a.ts/ })) + expect(screen.getByRole('button', { name: 'Oldest prompt' })).not.toHaveAttribute('aria-busy') + await settle(40) + expectDiffInView() + + pages.release() + await settle(60) + + expect(pages.asked()).toBe(1) + expect(screen.getByRole('button', { name: 'Oldest prompt' })).not.toHaveAttribute('aria-busy') + expectDiffInView() }) - - // The abandoned jump would have taken the pin and smooth-scrolled up to the oldest - // prompt; the prepend's own anchoring is an instant write. - expect(scrollTo.mock.calls.filter(([options]) => options?.behavior === 'smooth')).toEqual([]) - expect(screen.getByText('Edited')).toBeInTheDocument() }) })