test: prove diff reveal cancels a paging rail jump (#29585)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-16 02:54:08 -07:00
committed by GitHub
co-authored by OrcaWin
parent b16a71c7fb
commit feea786c93
@@ -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(<PagedTranscript />)
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<void> }): React.JSX.Element {
const [loadedOlder, setLoadedOlder] = useState(false)
const items = useMemo(() => (loadedOlder ? [OLDER, ...LOADED] : LOADED), [loadedOlder])
const loadEarlier = useCallback(async (): Promise<NativeChatOlderPageResult> => {
await holdPage()
setLoadedOlder(true)
return 'applied'
}, [holdPage])
return (
<NativeChatMessageList
session={{
...session(projectStructuredItemsToNativeChat(items)),
hasMore: !loadedOlder,
loadEarlier
}}
journalItems={items}
railOutline={loadedOlder ? [] : [{ id: 'older', text: 'Oldest prompt', hasImages: false }]}
isWorking={false}
expandSignal={false}
/>
)
}
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<void>((resolve) => {
release = resolve
})
)
const scrollTo = vi.fn()
vi.spyOn(HTMLElement.prototype, 'scrollTo').mockImplementation(scrollTo)
const { container } = render(<DiffTranscript holdPage={holdPage} />)
function DiffTranscript({ holdPage }: { holdPage: () => Promise<void> }): React.JSX.Element {
const [loadedOlder, setLoadedOlder] = useState(false)
const items = useMemo(() => (loadedOlder ? [OLDER, ...LOADED] : LOADED), [loadedOlder])
const loadEarlier = useCallback(async (): Promise<NativeChatOlderPageResult> => {
await holdPage()
setLoadedOlder(true)
return 'applied'
}, [holdPage])
return (
<NativeChatMessageList
session={{
...session(projectStructuredItemsToNativeChat(items)),
hasMore: !loadedOlder,
loadEarlier
}}
journalItems={items}
railOutline={
loadedOlder ? [] : [{ id: 'older', text: 'Oldest prompt', hasImages: false }]
}
isWorking={false}
expandSignal={false}
/>
)
}
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<HTMLElement>('[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<void>((resolve) => setTimeout(resolve, 0))
it('keeps the diff in view when its page lands', async () => {
const pages = holdFirstPage()
render(<DiffTranscript holdPage={pages.holdPage} />)
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()
})
})