Skip to content

Commit 9ee3a38

Browse files
[release/10.0] [Blazor] Increment MaxItemCount when OverscanCount > MaxItemCount (#63767)
* Virtualize test * Fix test * Fix test --------- Co-authored-by: Javier Calvarro Nelson <[email protected]>
1 parent b862c38 commit 9ee3a38

File tree

4 files changed

+114
-2
lines changed

4 files changed

+114
-2
lines changed

src/Components/Web/src/Virtualization/Virtualize.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ private void CalcualteItemDistribution(
362362
_ => MaxItemCount
363363
};
364364

365+
// Count the OverscanCount as used capacity, so we don't end up in a situation where
366+
// the user has set a very low MaxItemCount and we end up in an infinite loading loop.
367+
maxItemCount += OverscanCount * 2;
368+
365369
itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / _itemSize) - OverscanCount);
366370
visibleItemCapacity = (int)Math.Ceiling(containerSize / _itemSize) + 2 * OverscanCount;
367371
unusedItemCapacity = Math.Max(0, visibleItemCapacity - maxItemCount);

src/Components/test/E2ETest/Tests/VirtualizationTest.cs

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,14 @@ public void CanLimitMaxItemsRendered(bool useAppContext)
291291
// we only render 10 items due to the MaxItemCount setting
292292
var scrollArea = Browser.Exists(By.Id("virtualize-scroll-area"));
293293
var getItems = () => scrollArea.FindElements(By.ClassName("my-item"));
294-
Browser.Equal(10, () => getItems().Count);
294+
Browser.Equal(16, () => getItems().Count);
295295
Browser.Equal("Id: 0; Name: Thing 0", () => getItems().First().Text);
296296

297297
// Scrolling still works and loads new data, though there's no guarantee about
298298
// exactly how many items will show up at any one time
299299
Browser.ExecuteJavaScript("document.getElementById('virtualize-scroll-area').scrollTop = 300;");
300300
Browser.NotEqual("Id: 0; Name: Thing 0", () => getItems().First().Text);
301-
Browser.True(() => getItems().Count > 3 && getItems().Count <= 10);
301+
Browser.True(() => getItems().Count > 3 && getItems().Count <= 16);
302302
}
303303

304304
[Fact]
@@ -573,6 +573,101 @@ public void EmptyContentRendered_Async()
573573
int GetPlaceholderCount() => Browser.FindElements(By.Id("async-placeholder")).Count;
574574
}
575575

576+
[Fact]
577+
public void CanElevateEffectiveMaxItemCount_WhenOverscanExceedsMax()
578+
{
579+
Browser.MountTestComponent<VirtualizationLargeOverscan>();
580+
var container = Browser.Exists(By.Id("virtualize-large-overscan"));
581+
// Ensure we have an initial contiguous batch and the elevated effective max has kicked in (>= OverscanCount)
582+
var indices = GetVisibleItemIndices();
583+
Browser.True(() => indices.Count >= 200);
584+
585+
// Give focus so PageDown works
586+
container.Click();
587+
588+
var js = (IJavaScriptExecutor)Browser;
589+
var lastMaxIndex = -1;
590+
var lastScrollTop = -1L;
591+
592+
// Check if we've reached (or effectively reached) the bottom
593+
var scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container);
594+
var clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container);
595+
var scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container);
596+
while (scrollTop + clientHeight < scrollHeight)
597+
{
598+
// Validate contiguity on the current page
599+
Browser.True(() => IsCurrentViewContiguous(indices));
600+
601+
// Track progress in indices
602+
var currentMax = indices.Max();
603+
Assert.True(currentMax >= lastMaxIndex, $"Unexpected backward movement: previous max {lastMaxIndex}, current max {currentMax}.");
604+
lastMaxIndex = currentMax;
605+
606+
// Send PageDown
607+
container.SendKeys(Keys.PageDown);
608+
609+
// Wait for scrollTop to change (progress) to avoid infinite loop
610+
var prevScrollTop = scrollTop;
611+
Browser.True(() =>
612+
{
613+
var st = (long)js.ExecuteScript("return arguments[0].scrollTop", container);
614+
if (st > prevScrollTop)
615+
{
616+
lastScrollTop = st;
617+
return true;
618+
}
619+
return false;
620+
});
621+
scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container);
622+
clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container);
623+
scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container);
624+
}
625+
626+
// Final contiguous assertion at bottom
627+
Browser.True(() => IsCurrentViewContiguous());
628+
629+
// Helper: check visible items contiguous with no holes
630+
bool IsCurrentViewContiguous(List<int> existingIndices = null)
631+
{
632+
var indices = existingIndices ?? GetVisibleItemIndices();
633+
if (indices.Count == 0)
634+
{
635+
return false;
636+
}
637+
638+
if (indices[^1] - indices[0] != indices.Count - 1)
639+
{
640+
return false;
641+
}
642+
for (var i = 1; i < indices.Count; i++)
643+
{
644+
if (indices[i] - indices[i - 1] != 1)
645+
{
646+
return false;
647+
}
648+
}
649+
return true;
650+
}
651+
652+
List<int> GetVisibleItemIndices()
653+
{
654+
var elements = container.FindElements(By.CssSelector(".large-overscan-item"));
655+
var list = new List<int>(elements.Count);
656+
foreach (var el in elements)
657+
{
658+
var text = el.Text;
659+
if (text.StartsWith("Item ", StringComparison.Ordinal))
660+
{
661+
if (int.TryParse(text.AsSpan(5), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
662+
{
663+
list.Add(value);
664+
}
665+
}
666+
}
667+
return list;
668+
}
669+
}
670+
576671
private string[] GetPeopleNames(IWebElement container)
577672
{
578673
var peopleElements = container.FindElements(By.CssSelector(".person span"));

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
<option value="BasicTestApp.VirtualizationMaxItemCount">Virtualization MaxItemCount</option>
120120
<option value="BasicTestApp.VirtualizationMaxItemCount_AppContext">Virtualization MaxItemCount (via AppContext)</option>
121121
<option value="BasicTestApp.VirtualizationTable">Virtualization HTML table</option>
122+
<option value="BasicTestApp.VirtualizationLargeOverscan">Virtualization large overscan</option>
122123
<option value="BasicTestApp.HotReload.RenderOnHotReload">Render on hot reload</option>
123124
<option value="BasicTestApp.SectionsTest.ParentComponentWithTwoChildren">Sections test</option>
124125
<option value="BasicTestApp.SectionsTest.SectionsWithCascadingParameters">Sections with Cascading parameters test</option>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@* Test component to validate behavior when OverscanCount greatly exceeds MaxItemCount. *@
2+
@using Microsoft.AspNetCore.Components.Web.Virtualization
3+
4+
<div id="virtualize-large-overscan" style="height: 600px; overflow-y: auto; outline: 1px solid #999; background:#f8f8f8;">
5+
<Virtualize Items="_items" ItemSize="30" MaxItemCount="100" OverscanCount="200">
6+
<div class="large-overscan-item" @key="context" style="height:30px; line-height:30px; border-bottom:1px solid #ddd;">Item @context</div>
7+
</Virtualize>
8+
</div>
9+
10+
@code {
11+
private IList<int> _items = Enumerable.Range(0, 5000).ToList();
12+
}

0 commit comments

Comments
 (0)