@@ -5,7 +5,11 @@ import { describe, expect, mock, test } from "bun:test";
55import { testRender } from "@opentui/react/test-utils" ;
66import { act } from "react" ;
77import type { HunkHostClient } from "../mcp/client" ;
8- import type { HunkSessionRegistration , SessionServerMessage } from "../mcp/types" ;
8+ import type {
9+ HunkSessionRegistration ,
10+ HunkSessionSnapshot ,
11+ SessionServerMessage ,
12+ } from "../mcp/types" ;
913import type { AppBootstrap , LayoutMode } from "../core/types" ;
1014import { createTestGitAppBootstrap } from "../../test/helpers/app-bootstrap" ;
1115import { createTestDiffFile as buildTestDiffFile , lines } from "../../test/helpers/diff-helpers" ;
@@ -41,6 +45,7 @@ function createMockHostClient() {
4145 type Bridge = Parameters < HunkHostClient [ "setBridge" ] > [ 0 ] ;
4246
4347 let bridge : Bridge = null ;
48+ let latestSnapshot : HunkSessionSnapshot | null = null ;
4449 const registration : HunkSessionRegistration = {
4550 sessionId : "session-1" ,
4651 pid : process . pid ,
@@ -59,9 +64,12 @@ function createMockHostClient() {
5964 setBridge : ( nextBridge : Bridge ) => {
6065 bridge = nextBridge ;
6166 } ,
62- updateSnapshot : ( ) => { } ,
67+ updateSnapshot : ( snapshot : HunkSessionSnapshot ) => {
68+ latestSnapshot = snapshot ;
69+ } ,
6370 } as unknown as HunkHostClient ,
6471 getBridge : ( ) => bridge ,
72+ getLatestSnapshot : ( ) => latestSnapshot ,
6573 navigateToHunk : async (
6674 input : Extract < SessionServerMessage , { command : "navigate_to_hunk" } > [ "input" ] ,
6775 ) => {
@@ -226,6 +234,40 @@ function createTwoFileHunkBootstrap(): AppBootstrap {
226234 } ) ;
227235}
228236
237+ function createMouseScrollSelectionBootstrap ( ) : AppBootstrap {
238+ const firstBeforeLines = createNumberedAssignmentLines ( 1 , 12 ) ;
239+ const secondBeforeLines = Array . from (
240+ { length : 90 } ,
241+ ( _ , index ) => `export const line${ String ( index + 13 ) . padStart ( 2 , "0" ) } = ${ index + 13 } ;` ,
242+ ) ;
243+ const secondAfterLines = [ ...secondBeforeLines ] ;
244+
245+ secondAfterLines [ 0 ] = "export const line13 = 1300;" ;
246+ secondAfterLines [ 59 ] = "export const line72 = 7200;" ;
247+ secondAfterLines [ 60 ] = "export const line73 = 7300;" ;
248+ secondAfterLines [ 61 ] = "export const line74 = 7400;" ;
249+
250+ return createTestGitAppBootstrap ( {
251+ changesetId : "changeset:mouse-scroll-selection" ,
252+ files : [
253+ createTestDiffFile (
254+ "first" ,
255+ "first.ts" ,
256+ lines ( ...firstBeforeLines ) ,
257+ lines ( "export const line01 = 101;" , ...createNumberedAssignmentLines ( 2 , 11 ) ) ,
258+ true ,
259+ ) ,
260+ createTestDiffFile (
261+ "second" ,
262+ "second.ts" ,
263+ lines ( ...secondBeforeLines ) ,
264+ lines ( ...secondAfterLines ) ,
265+ true ,
266+ ) ,
267+ ] ,
268+ } ) ;
269+ }
270+
229271function createCollapsedTopBootstrap ( ) : AppBootstrap {
230272 const beforeLines = Array . from (
231273 { length : 400 } ,
@@ -293,6 +335,29 @@ async function waitForFrame(
293335 return frame ;
294336}
295337
338+ async function waitForSnapshot (
339+ setup : Awaited < ReturnType < typeof testRender > > ,
340+ getSnapshot : ( ) => HunkSessionSnapshot | null ,
341+ predicate : ( snapshot : HunkSessionSnapshot ) => boolean ,
342+ attempts = 8 ,
343+ ) {
344+ let snapshot = getSnapshot ( ) ;
345+
346+ for ( let attempt = 0 ; attempt < attempts ; attempt += 1 ) {
347+ if ( snapshot && predicate ( snapshot ) ) {
348+ return snapshot ;
349+ }
350+
351+ await act ( async ( ) => {
352+ await Bun . sleep ( 30 ) ;
353+ await setup . renderOnce ( ) ;
354+ } ) ;
355+ snapshot = getSnapshot ( ) ;
356+ }
357+
358+ return snapshot ;
359+ }
360+
296361function firstVisibleAddedLine ( frame : string ) {
297362 return frame . match ( / l i n e \d { 2 } = 1 \d { 2 } / ) ?. [ 0 ] ?? null ;
298363}
@@ -1669,6 +1734,55 @@ describe("App interactions", () => {
16691734 }
16701735 } ) ;
16711736
1737+ test ( "mouse wheel scrolling updates the active file and hunk to the viewport center" , async ( ) => {
1738+ const { getLatestSnapshot, hostClient } = createMockHostClient ( ) ;
1739+ const setup = await testRender (
1740+ < AppHost bootstrap = { createMouseScrollSelectionBootstrap ( ) } hostClient = { hostClient } /> ,
1741+ {
1742+ width : 220 ,
1743+ height : 12 ,
1744+ } ,
1745+ ) ;
1746+
1747+ try {
1748+ await flush ( setup ) ;
1749+
1750+ expect ( getLatestSnapshot ( ) ) . toMatchObject ( {
1751+ selectedFilePath : "first.ts" ,
1752+ selectedHunkIndex : 0 ,
1753+ } ) ;
1754+
1755+ let snapshot = getLatestSnapshot ( ) ;
1756+ for ( let index = 0 ; index < 24 ; index += 1 ) {
1757+ await act ( async ( ) => {
1758+ await setup . mockMouse . scroll ( 120 , 7 , "down" ) ;
1759+ } ) ;
1760+ await flush ( setup ) ;
1761+
1762+ snapshot = await waitForSnapshot (
1763+ setup ,
1764+ getLatestSnapshot ,
1765+ ( currentSnapshot ) =>
1766+ currentSnapshot . selectedFilePath === "second.ts" &&
1767+ currentSnapshot . selectedHunkIndex === 1 ,
1768+ 4 ,
1769+ ) ;
1770+ if ( snapshot ?. selectedFilePath === "second.ts" && snapshot . selectedHunkIndex === 1 ) {
1771+ break ;
1772+ }
1773+ }
1774+
1775+ expect ( snapshot ) . toMatchObject ( {
1776+ selectedFilePath : "second.ts" ,
1777+ selectedHunkIndex : 1 ,
1778+ } ) ;
1779+ } finally {
1780+ await act ( async ( ) => {
1781+ setup . renderer . destroy ( ) ;
1782+ } ) ;
1783+ }
1784+ } ) ;
1785+
16721786 test ( "clicking a sidebar file makes that file own the top of the review pane" , async ( ) => {
16731787 const setup = await testRender ( < AppHost bootstrap = { createTwoFileHunkBootstrap ( ) } /> , {
16741788 width : 220 ,
0 commit comments