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()
})
})