@@ -736,13 +736,20 @@ test('changing gap invalidates cached measurements (multi-lane)', () => {
736736
737737function setupResizeBatch ( onChange : ( instance : any , sync : boolean ) => void ) {
738738 let roCallback : ResizeObserverCallback | null = null
739+ const observe = vi . fn ( )
740+ const unobserve = vi . fn ( )
741+ const frames = new Map < number , FrameRequestCallback > ( )
742+ let frameId = 0
739743 const mockWindow = {
740- requestAnimationFrame : vi . fn ( ) ,
741- cancelAnimationFrame : vi . fn ( ) ,
744+ requestAnimationFrame : vi . fn ( ( callback : FrameRequestCallback ) => {
745+ frames . set ( ++ frameId , callback )
746+ return frameId
747+ } ) ,
748+ cancelAnimationFrame : vi . fn ( ( id : number ) => frames . delete ( id ) ) ,
742749 performance : { now : ( ) => Date . now ( ) } ,
743750 ResizeObserver : vi . fn ( function ( cb : ResizeObserverCallback ) {
744751 roCallback = cb
745- return { observe : vi . fn ( ) , unobserve : vi . fn ( ) , disconnect : vi . fn ( ) }
752+ return { observe, unobserve, disconnect : vi . fn ( ) }
746753 } ) ,
747754 }
748755 const el = {
@@ -811,7 +818,21 @@ function setupResizeBatch(onChange: (instance: any, sync: boolean) => void) {
811818 ) ,
812819 { } as ResizeObserver ,
813820 )
814- return { virtualizer, sizes, node, deliver }
821+ const flush = ( ) => {
822+ const queued = [ ...frames . values ( ) ]
823+ frames . clear ( )
824+ queued . forEach ( ( callback ) => callback ( 0 ) )
825+ }
826+ return {
827+ virtualizer,
828+ sizes,
829+ node,
830+ deliver,
831+ observe,
832+ unobserve,
833+ frames,
834+ flush,
835+ }
815836}
816837
817838test ( 'ResizeObserver batch: later entries see starts updated by earlier ones (#1218)' , ( ) => {
@@ -850,6 +871,70 @@ test('ResizeObserver batch: a sync re-render does not drop queued entries (#1036
850871 expect ( notifies ) . toEqual ( [ true ] )
851872} )
852873
874+ test ( 'unobserves rows detached by a synchronous resize render before delivery ends' , ( ) => {
875+ const { virtualizer, sizes, node, deliver, unobserve } = setupResizeBatch (
876+ ( instance , sync ) => {
877+ if ( ! sync ) return
878+ // React calls the ref with null before removing the host element.
879+ instance . measureElement ( null )
880+ node ( 5 ) . isConnected = false
881+ } ,
882+ )
883+ sizes . set ( 3 , 200 )
884+ deliver ( [ 3 ] )
885+ expect ( unobserve ) . toHaveBeenCalledWith ( node ( 5 ) )
886+ expect ( virtualizer . elementsCache . has ( 5 ) ) . toBe ( false )
887+ } )
888+
889+ test ( 'measures new rows synchronously but observes them after resize delivery' , ( ) => {
890+ const { virtualizer, sizes, node, deliver, observe, frames, flush } =
891+ setupResizeBatch ( ( instance , sync ) => {
892+ if ( sync ) {
893+ instance . measureElement ( node ( 10 ) )
894+ instance . measureElement ( node ( 11 ) )
895+ }
896+ } )
897+ sizes . set ( 10 , 120 )
898+ sizes . set ( 11 , 130 )
899+ sizes . set ( 3 , 200 )
900+ deliver ( [ 3 ] )
901+ expect ( virtualizer . getMeasurements ( ) [ 10 ] ! . size ) . toBe ( 120 )
902+ expect ( virtualizer . getMeasurements ( ) [ 11 ] ! . size ) . toBe ( 130 )
903+ expect ( observe . mock . calls . map ( ( [ target ] ) => target ) ) . not . toContain ( node ( 10 ) )
904+ expect ( observe . mock . calls . map ( ( [ target ] ) => target ) ) . not . toContain ( node ( 11 ) )
905+ expect ( frames . size ) . toBe ( 1 )
906+ flush ( )
907+ expect ( observe ) . toHaveBeenCalledWith ( node ( 10 ) , { box : 'border-box' } )
908+ expect ( observe ) . toHaveBeenCalledWith ( node ( 11 ) , { box : 'border-box' } )
909+ } )
910+
911+ test . each ( [ 'removed' , 'replaced' , 'disconnected' ] as const ) (
912+ 'does not register a deferred row after it is %s' ,
913+ ( action ) => {
914+ const { virtualizer, sizes, node, deliver, observe, flush } =
915+ setupResizeBatch ( ( instance , sync ) => {
916+ if ( sync ) instance . measureElement ( node ( 10 ) )
917+ } )
918+ sizes . set ( 10 , 120 )
919+ sizes . set ( 3 , 200 )
920+ deliver ( [ 3 ] )
921+ expect ( observe . mock . calls . map ( ( [ target ] ) => target ) ) . not . toContain ( node ( 10 ) )
922+
923+ if ( action === 'removed' ) {
924+ node ( 10 ) . isConnected = false
925+ virtualizer . measureElement ( null )
926+ } else if ( action === 'replaced' ) {
927+ const replacement = { ...node ( 10 ) }
928+ virtualizer . measureElement ( replacement )
929+ expect ( observe ) . toHaveBeenCalledWith ( replacement , { box : 'border-box' } )
930+ } else {
931+ virtualizer . _didMount ( ) ( )
932+ }
933+ flush ( )
934+ expect ( observe . mock . calls . map ( ( [ target ] ) => target ) ) . not . toContain ( node ( 10 ) )
935+ } ,
936+ )
937+
853938// ─── useAnimationFrameWithResizeObserver: frames of the scroll element's window
854939
855940// A window other than the global one, as for a list rendered into a
0 commit comments