A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

95 lines
4.4 KiB

using Avalonia.Controls.PullToRefresh;
using Avalonia.Input;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.PullToRefresh
{
public class RefreshInfoProviderTests : ScopedTestBase
{
// Repro for the "_entered desync" bug.
//
// Real-world flow that triggers it:
// 1. User starts pulling. ScrollablePullGestureRecognizer raises PullGestureEvent
// -> InteractingStateEntered sets IsInteractingForRefresh = true
// and _entered = true.
// 2. The pull motion produces a small ScrollChanged that pushes the scroll offset
// past the threshold. ScrollViewerIRefreshInfoProviderAdapter.ScrollViewer_ScrollChanged
// writes IsInteractingForRefresh = false directly, bypassing PullGestureEnded.
// (ScrollViewer_PointerReleased does the same.)
// 3. _entered stays true.
// 4. The user is still pulling, more PullGestureEvents arrive.
// InteractingStateEntered short-circuits because _entered is already true,
// so IsInteractingForRefresh is NOT reasserted.
// 5. RefreshVisualizer never re-enters the Interacting state -> spinner does not appear.
[Fact]
public void IsInteractingForRefresh_is_reasserted_after_being_cleared_externally()
{
var provider = new RefreshInfoProvider(
PullDirection.TopToBottom,
new Size(100, 100),
visual: null);
var pullArgs = new PullGestureEventArgs(0, new Vector(0, 50), PullDirection.TopToBottom);
// 1. First PullGestureEvent of a gesture
provider.InteractingStateEntered(this, pullArgs);
Assert.True(provider.IsInteractingForRefresh,
"IsInteractingForRefresh should be true after the first PullGestureEvent");
// 2. Adapter clears the flag directly (simulating ScrollViewer_ScrollChanged
// or ScrollViewer_PointerReleased)
provider.IsInteractingForRefresh = false;
Assert.False(provider.IsInteractingForRefresh);
// 3. Pull is still in progress, the next PullGestureEvent arrives
provider.InteractingStateEntered(this, pullArgs);
// BUG: stays false because _entered short-circuits the assignment.
// After the fix this assertion must pass.
Assert.True(provider.IsInteractingForRefresh,
"PullGestureEvent must re-assert IsInteractingForRefresh after it was cleared by something other than PullGestureEnded");
}
// Repro for the typo where horizontal pulls checked Height==0 instead of Width==0.
// With Width==0, value.X / Width produces +Infinity / NaN, which then breaks every
// downstream consumer of InteractionRatio (Math.Min(1, NaN) returns NaN).
[Fact]
public void Horizontal_pull_with_zero_width_produces_safe_InteractionRatio()
{
var provider = new RefreshInfoProvider(
PullDirection.LeftToRight,
new Size(0, 100),
visual: null);
provider.ValuesChanged(new Vector(50, 0));
Assert.False(double.IsNaN(provider.InteractionRatio));
Assert.False(double.IsInfinity(provider.InteractionRatio));
}
// Sanity check for the existing happy-path: a complete gesture lifecycle
// (Entered -> Exited -> Entered) must toggle IsInteractingForRefresh correctly.
[Fact]
public void Normal_gesture_lifecycle_toggles_IsInteractingForRefresh_correctly()
{
var provider = new RefreshInfoProvider(
PullDirection.TopToBottom,
new Size(100, 100),
visual: null);
var pullArgs = new PullGestureEventArgs(0, new Vector(0, 50), PullDirection.TopToBottom);
var endArgs = new PullGestureEndedEventArgs(0, PullDirection.TopToBottom);
provider.InteractingStateEntered(this, pullArgs);
Assert.True(provider.IsInteractingForRefresh);
provider.InteractingStateExited(this, endArgs);
Assert.False(provider.IsInteractingForRefresh);
// Next gesture should work
provider.InteractingStateEntered(this, pullArgs);
Assert.True(provider.IsInteractingForRefresh);
}
}
}