Browse Source

Added tests to detect scroll jumps.

pull/10892/head
Steven Kirk 4 years ago
parent
commit
bc32c061e8
  1. 10
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  2. 146
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

10
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -130,6 +130,16 @@ namespace Avalonia.Controls
set { SetValue(AreVerticalSnapPointsRegularProperty, value); }
}
/// <summary>
/// Gets the index of the first realized element, or -1 if no elements are realized.
/// </summary>
public int FirstRealizedIndex => _realizedElements?.FirstIndex ?? -1;
/// <summary>
/// Gets the index of the last realized element, or -1 if no elements are realized.
/// </summary>
public int LastRealizedIndex => _realizedElements?.LastIndex ?? -1;
protected override Size MeasureOverride(Size availableSize)
{
if (!IsEffectivelyVisible)

146
tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs

@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10});
var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false);
var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null);
Assert.Equal(1000, scroll.Extent.Height);
@ -252,7 +252,7 @@ namespace Avalonia.Controls.UnitTests
{
using var app = App();
var items = new ObservableCollection<Button>(Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 }));
var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false);
var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null);
Assert.Equal(1000, scroll.Extent.Height);
@ -472,6 +472,122 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(10, raised);
}
[Fact]
public void Scrolling_Down_With_Larger_Element_Does_Not_Cause_Jump_And_Arrives_At_End()
{
using var app = App();
var items = Enumerable.Range(0, 1000).Select(x => new ItemWithHeight(x)).ToList();
items[20].Height = 200;
var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
new Canvas
{
Width = 100,
[!Canvas.HeightProperty] = new Binding("Height"),
});
var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: itemTemplate);
var index = target.FirstRealizedIndex;
// Scroll down to the larger element.
while (target.LastRealizedIndex < items.Count - 1)
{
scroll.LineDown();
Layout(target);
Assert.True(
target.FirstRealizedIndex >= index,
$"{target.FirstRealizedIndex} is not greater or equal to {index}");
if (scroll.Offset.Y + scroll.Viewport.Height == scroll.Extent.Height)
Assert.Equal(items.Count - 1, target.LastRealizedIndex);
index = target.FirstRealizedIndex;
}
}
[Fact]
public void Scrolling_Up_To_Larger_Element_Does_Not_Cause_Jump()
{
using var app = App();
var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
items[20].Height = 200;
var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
new Canvas
{
Width = 100,
[!Canvas.HeightProperty] = new Binding("Height"),
});
var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: itemTemplate);
// Scroll past the larger element.
scroll.Offset = new Vector(0, 600);
Layout(target);
// Precondition checks
Assert.True(target.FirstRealizedIndex > 20);
var index = target.FirstRealizedIndex;
// Scroll up to the top.
while (scroll.Offset.Y > 0)
{
scroll.LineUp();
Layout(target);
Assert.True(target.FirstRealizedIndex <= index, $"{target.FirstRealizedIndex} is not less than {index}");
index = target.FirstRealizedIndex;
}
}
[Fact]
public void Scrolling_Up_To_Smaller_Element_Does_Not_Cause_Jump()
{
using var app = App();
var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x, 30)).ToList();
items[20].Height = 25;
var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
new Canvas
{
Width = 100,
[!Canvas.HeightProperty] = new Binding("Height"),
});
var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: itemTemplate);
// Scroll past the larger element.
scroll.Offset = new Vector(0, 25 * items[0].Height);
Layout(target);
// Precondition checks
Assert.True(target.FirstRealizedIndex > 20);
var index = target.FirstRealizedIndex;
// Scroll up to the top.
while (scroll.Offset.Y > 0)
{
scroll.Offset = scroll.Offset - new Vector(0, 5);
Layout(target);
Assert.True(
target.FirstRealizedIndex <= index,
$"{target.FirstRealizedIndex} is not less than {index}");
Assert.True(
index - target.FirstRealizedIndex <= 1,
$"FirstIndex changed from {index} to {target.FirstRealizedIndex}");
index = target.FirstRealizedIndex;
}
}
private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
{
return target.GetRealizedElements()
@ -516,7 +632,7 @@ namespace Avalonia.Controls.UnitTests
private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
IEnumerable<object>? items = null,
bool useItemTemplate = true,
Optional<IDataTemplate?> itemTemplate = default,
IEnumerable<Style>? styles = null)
{
var target = new VirtualizingStackPanel();
@ -530,6 +646,7 @@ namespace Avalonia.Controls.UnitTests
var scroll = new ScrollViewer
{
Name = "PART_ScrollViewer",
Content = presenter,
Template = ScrollViewerTemplate(),
};
@ -537,13 +654,11 @@ namespace Avalonia.Controls.UnitTests
var itemsControl = new ItemsControl
{
ItemsSource = items,
Template = new FuncControlTemplate<ItemsControl>((_, _) => scroll),
Template = new FuncControlTemplate<ItemsControl>((_, ns) => scroll.RegisterInNameScope(ns)),
ItemsPanel = new FuncTemplate<Panel?>(() => target),
ItemTemplate = itemTemplate.GetValueOrDefault(DefaultItemTemplate()),
};
if (useItemTemplate)
itemsControl.ItemTemplate = new FuncDataTemplate<object>((x, _) => new Canvas { Width = 100, Height = 10 });
var root = new TestRoot(true, itemsControl);
root.ClientSize = new(100, 100);
@ -555,6 +670,11 @@ namespace Avalonia.Controls.UnitTests
return (target, scroll, itemsControl);
}
private static IDataTemplate DefaultItemTemplate()
{
return new FuncDataTemplate<object>((x, _) => new Canvas { Width = 100, Height = 10 });
}
private static void Layout(Control target)
{
var root = (ILayoutRoot?)target.GetVisualRoot();
@ -578,6 +698,18 @@ namespace Avalonia.Controls.UnitTests
private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
private class ItemWithHeight
{
public ItemWithHeight(int index, double height = 10)
{
Caption = $"Item {index}";
Height = height;
}
public string Caption { get; set; }
public double Height { get; set; }
}
private class ResettingCollection : List<string>, INotifyCollectionChanged
{
public ResettingCollection(IEnumerable<string> items)

Loading…
Cancel
Save