@@ -1154,6 +1154,70 @@ describe('useRecycleScroller', () => {
11541154 expect ( recoveryRange [ 1 ] ) . toBeGreaterThanOrEqual ( baselineRange [ 1 ] )
11551155 } )
11561156
1157+ it ( 'still assigns a view for an in-range index whose cached size is 0' , async ( ) => {
1158+ // Regression for issue #906: the step-2 loop in updateVisibleItems used
1159+ // to early-return when `sizesValue[i].size` was 0 (or `sizesValue[i]`
1160+ // was undefined), skipping view assignment for that index. Combined
1161+ // with step 1 having already recycled every prior view (on the
1162+ // itemsChanged / non-continuous paths), the DOM slot was left blank
1163+ // until the next reconciliation tick. The fix falls back to
1164+ // `_computedMinItemSize` so every index in the resolved range claims
1165+ // a pooled view regardless of cache transients.
1166+ const { vm } = mountHarness ( {
1167+ items : Array . from ( { length : 5 } , ( _ , id ) => ( { id, size : 20 } ) ) ,
1168+ itemSize : null ,
1169+ minItemSize : 20 ,
1170+ clientHeight : 100 ,
1171+ } )
1172+
1173+ await nextTick ( )
1174+ await nextTick ( )
1175+
1176+ // Sanity: all five items should be in the visible pool to start with.
1177+ const initialIndices = vm . visiblePool . map ( ( view : View ) => view . nr . index )
1178+ expect ( initialIndices ) . toEqual ( [ 0 , 1 , 2 , 3 , 4 ] )
1179+
1180+ // Punch size=0 into the middle of the cache. The accumulators stay
1181+ // populated so the binary search still includes index 2 in the range —
1182+ // we want to exercise the per-index assignment loop, not the gate.
1183+ const sizesRef = vm . sizes as Array < { accumulator : number , size : number | undefined } | undefined >
1184+ const savedSize = sizesRef [ 2 ] ! . size
1185+ sizesRef [ 2 ] ! . size = 0
1186+
1187+ vm . updateVisibleItems ( true )
1188+
1189+ sizesRef [ 2 ] ! . size = savedSize
1190+
1191+ const recoveryIndices = vm . visiblePool . map ( ( view : View ) => view . nr . index )
1192+ expect ( recoveryIndices ) . toContain ( 2 )
1193+ } )
1194+
1195+ it ( 'still assigns a view when an in-range sizesValue entry is undefined' , async ( ) => {
1196+ // Same regression as the size=0 case (issue #906), but exercising the
1197+ // `sizesValue[i] && ...` branch. The fallback must catch undefined
1198+ // entries too — a sparse cache slot would otherwise also skip the slot.
1199+ const { vm } = mountHarness ( {
1200+ items : Array . from ( { length : 5 } , ( _ , id ) => ( { id, size : 20 } ) ) ,
1201+ itemSize : null ,
1202+ minItemSize : 20 ,
1203+ clientHeight : 100 ,
1204+ } )
1205+
1206+ await nextTick ( )
1207+ await nextTick ( )
1208+
1209+ const sizesRef = vm . sizes as Array < { accumulator : number , size : number | undefined } | undefined >
1210+ const saved = sizesRef [ 2 ]
1211+ sizesRef [ 2 ] = undefined
1212+
1213+ vm . updateVisibleItems ( true )
1214+
1215+ sizesRef [ 2 ] = saved
1216+
1217+ const recoveryIndices = vm . visiblePool . map ( ( view : View ) => view . nr . index )
1218+ expect ( recoveryIndices ) . toContain ( 2 )
1219+ } )
1220+
11571221 it ( 'does not crash on 0 → 1 items transition in variable-size mode' , async ( ) => {
11581222 // Regression: in variable-size mode the size cache is computed lazily from
11591223 // `items`. When an empty list gains its first row, an upstream wrapper can
0 commit comments