@@ -940,6 +940,90 @@ test('RO callback should not delete cache entry if node was replaced by React',
940940 expect ( virtualizer . elementsCache . get ( 3 ) ) . toBe ( nodeB )
941941} )
942942
943+ test ( 'ignores connected stale ref and ResizeObserver measurements after count shrinks' , ( ) => {
944+ let roCallback : ResizeObserverCallback | null = null
945+ const MockResizeObserver = vi . fn ( function ( cb : ResizeObserverCallback ) {
946+ roCallback = cb
947+ return {
948+ observe : vi . fn ( ) ,
949+ unobserve : vi . fn ( ) ,
950+ disconnect : vi . fn ( ) ,
951+ }
952+ } )
953+ const mockWindow = {
954+ requestAnimationFrame : vi . fn ( ) ,
955+ cancelAnimationFrame : vi . fn ( ) ,
956+ performance : { now : ( ) => Date . now ( ) } ,
957+ ResizeObserver : MockResizeObserver ,
958+ }
959+ const mockScrollElement = {
960+ scrollTop : 0 ,
961+ scrollLeft : 0 ,
962+ scrollWidth : 1000 ,
963+ scrollHeight : 5000 ,
964+ offsetWidth : 400 ,
965+ offsetHeight : 600 ,
966+ ownerDocument : { defaultView : mockWindow } ,
967+ } as unknown as HTMLDivElement
968+
969+ let virtualizer : Virtualizer < HTMLDivElement , HTMLElement >
970+ const getItemKey = vi . fn ( ( index : number ) => {
971+ if ( index < 0 || index >= virtualizer . options . count ) {
972+ throw new Error ( `getItemKey received stale index ${ index } ` )
973+ }
974+ return index
975+ } )
976+ virtualizer = new Virtualizer ( {
977+ count : 22 ,
978+ estimateSize : ( ) => 50 ,
979+ getItemKey,
980+ useCachedMeasurements : true ,
981+ getScrollElement : ( ) => mockScrollElement ,
982+ scrollToFn : vi . fn ( ) ,
983+ observeElementRect : ( _instance , cb ) => {
984+ cb ( { width : 400 , height : 600 } )
985+ return ( ) => { }
986+ } ,
987+ observeElementOffset : ( _instance , cb ) => {
988+ cb ( 0 , false )
989+ return ( ) => { }
990+ } ,
991+ } )
992+ virtualizer . _willUpdate ( )
993+
994+ const staleNode = {
995+ getAttribute : ( ) => '21' ,
996+ getBoundingClientRect : ( ) => ( { height : 50 , width : 400 } ) ,
997+ isConnected : true ,
998+ setAttribute : vi . fn ( ) ,
999+ } as unknown as HTMLElement
1000+ virtualizer . measureElement ( staleNode )
1001+
1002+ virtualizer . setOptions ( { ...virtualizer . options , count : 1 } )
1003+ getItemKey . mockClear ( )
1004+
1005+ expect ( ( ) => virtualizer . measureElement ( staleNode ) ) . not . toThrow ( )
1006+ expect ( getItemKey ) . not . toHaveBeenCalled ( )
1007+
1008+ getItemKey . mockClear ( )
1009+ expect ( roCallback ) . not . toBeNull ( )
1010+ expect ( ( ) => {
1011+ roCallback ! (
1012+ [
1013+ {
1014+ target : staleNode ,
1015+ contentRect : { height : 50 , width : 400 } as DOMRectReadOnly ,
1016+ borderBoxSize : [ { blockSize : 50 , inlineSize : 400 } ] ,
1017+ contentBoxSize : [ { blockSize : 50 , inlineSize : 400 } ] ,
1018+ devicePixelContentBoxSize : [ { blockSize : 50 , inlineSize : 400 } ] ,
1019+ } as ResizeObserverEntry ,
1020+ ] ,
1021+ { } as ResizeObserver ,
1022+ )
1023+ } ) . not . toThrow ( )
1024+ expect ( getItemKey ) . not . toHaveBeenCalled ( )
1025+ } )
1026+
9431027// ─── setOptions behavioral contract ──────────────────────────────────────────
9441028// These tests pin down how setOptions merges defaults with user-supplied opts.
9451029// They guard against regressions when changing the merge mechanism
0 commit comments