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.
 
 
 

1389 lines
48 KiB

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Animation;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Threading;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Controls.UnitTests;
public class CarouselPageTests
{
public class PropertyDefaults : ScopedTestBase
{
[Fact]
public void SelectedIndex_DefaultIsMinusOne()
{
var cp = new CarouselPage();
Assert.Equal(-1, cp.SelectedIndex);
}
[Fact]
public void SelectedPage_DefaultIsNull()
{
var cp = new CarouselPage();
Assert.Null(cp.SelectedPage);
}
[Fact]
public void CurrentPage_DefaultIsNull()
{
var cp = new CarouselPage();
Assert.Null(cp.CurrentPage);
}
[Fact]
public void PageTransition_DefaultIsNull()
{
var cp = new CarouselPage();
Assert.Null(cp.PageTransition);
}
[Fact]
public void IsGestureEnabled_DefaultIsTrue()
{
var cp = new CarouselPage();
Assert.True(cp.IsGestureEnabled);
}
[Fact]
public void IsKeyboardNavigationEnabled_DefaultIsTrue()
{
var cp = new CarouselPage();
Assert.True(cp.IsKeyboardNavigationEnabled);
}
}
public class PropertyRoundTrips : ScopedTestBase
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void IsGestureEnabled_RoundTrips(bool value)
{
var cp = new CarouselPage { IsGestureEnabled = value };
Assert.Equal(value, cp.IsGestureEnabled);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void IsKeyboardNavigationEnabled_RoundTrips(bool value)
{
var cp = new CarouselPage { IsKeyboardNavigationEnabled = value };
Assert.Equal(value, cp.IsKeyboardNavigationEnabled);
}
[Fact]
public void PageTransition_RoundTrip()
{
var transition = new TestPageTransition();
var cp = new CarouselPage { PageTransition = transition };
Assert.Same(transition, cp.PageTransition);
}
[Fact]
public void PageTransition_CanBeSetToNull()
{
var cp = new CarouselPage { PageTransition = new TestPageTransition() };
cp.PageTransition = null;
Assert.Null(cp.PageTransition);
}
[Fact]
public void ItemsPanel_RoundTrip()
{
var template = new FuncTemplate<Panel?>(() => new StackPanel());
var cp = new CarouselPage { ItemsPanel = template };
Assert.Same(template, cp.ItemsPanel);
}
[Fact]
public void PageTemplate_RoundTrip()
{
var template = new FuncDataTemplate<Page>((_, _) => new ContentControl());
var cp = new CarouselPage { PageTemplate = template };
Assert.Same(template, cp.PageTemplate);
}
[Fact]
public void PageTemplate_CanBeSetToNull()
{
var cp = new CarouselPage { PageTemplate = null };
Assert.Null(cp.PageTemplate);
}
}
public class SelectionBehavior : ScopedTestBase
{
[Fact]
public void SelectedIndex_SetUpdatesSelectedPageAndCurrentPage()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 0;
cp.SelectedIndex = 1;
Assert.Same(page2, cp.SelectedPage);
Assert.Same(page2, cp.CurrentPage);
}
[Fact]
public void SelectedIndex_AutoSelectsFirst_WhenPagesSet()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
Assert.Equal(0, cp.SelectedIndex);
Assert.Same(page1, cp.SelectedPage);
}
[Fact]
public void UpdateActivePage_SelectsFirst_WhenFirstPageAddedToEmptyCollection()
{
var cp = new CarouselPage();
Assert.Equal(-1, cp.SelectedIndex);
var page1 = new ContentPage { Header = "A" };
((AvaloniaList<Page>)cp.Pages!).Add(page1);
Assert.Equal(0, cp.SelectedIndex);
Assert.Same(page1, cp.SelectedPage);
}
[Fact]
public void UpdateActivePage_DoesNotOverrideExistingSelection()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 1;
((AvaloniaList<Page>)cp.Pages!).Add(new ContentPage { Header = "C" });
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void SelectedPage_SameReferenceAsCurrentPage()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 1;
Assert.Same(cp.SelectedPage, cp.CurrentPage);
}
[Fact]
public void SelectedIndex_InvalidWithPages_CoercesToFirstPage()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 999;
Assert.Equal(0, cp.SelectedIndex);
Assert.Same(page1, cp.SelectedPage);
Assert.Same(page1, cp.CurrentPage);
}
[Fact]
public void SelectedIndex_SetBeforePages_IsStored()
{
var cp = new CarouselPage();
cp.SelectedIndex = 2;
Assert.Equal(2, cp.SelectedIndex);
Assert.Null(cp.SelectedPage);
Assert.Null(cp.CurrentPage);
}
[Fact]
public void SelectedIndex_SetBeforePages_IsAppliedWhenPagesSet()
{
var cp = new CarouselPage
{
SelectedIndex = 2
};
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
var page3 = new ContentPage { Header = "C" };
cp.Pages = new AvaloniaList<Page> { page1, page2, page3 };
Assert.Equal(2, cp.SelectedIndex);
Assert.Same(page3, cp.SelectedPage);
Assert.Same(page3, cp.CurrentPage);
}
[Fact]
public void Pages_SetNull_SelectedIndex_RetainsLastValue()
{
var cp = new CarouselPage();
((AvaloniaList<Page>)cp.Pages!).Add(new ContentPage { Header = "A" });
Assert.Equal(0, cp.SelectedIndex);
cp.Pages = null;
Assert.Equal(0, cp.SelectedIndex);
}
}
public class SelectionChangedEvent : ScopedTestBase
{
[Fact]
public void SelectionChanged_FiresWhenSelectionChanges()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 0;
PageSelectionChangedEventArgs? received = null;
cp.SelectionChanged += (_, e) => received = e;
cp.SelectedIndex = 1;
Assert.NotNull(received);
Assert.Same(page1, received!.PreviousPage);
Assert.Same(page2, received.CurrentPage);
}
[Fact]
public void SelectionChanged_NotFiredWhenSamePageSelected()
{
var cp = new CarouselPage();
var page = new ContentPage { Header = "A" };
((AvaloniaList<Page>)cp.Pages!).Add(page);
cp.SelectedIndex = 0;
int count = 0;
cp.SelectionChanged += (_, _) => count++;
cp.SelectedIndex = 0;
Assert.Equal(0, count);
}
[Fact]
public void SelectionChanged_TracksSequentialSelections()
{
var cp = new CarouselPage();
var pages = new[]
{
new ContentPage { Header = "A" },
new ContentPage { Header = "B" },
new ContentPage { Header = "C" },
};
((AvaloniaList<Page>)cp.Pages!).AddRange(pages);
cp.SelectedIndex = 0;
var events = new List<(Page? prev, Page? curr)>();
cp.SelectionChanged += (_, e) => events.Add((e.PreviousPage, e.CurrentPage));
cp.SelectedIndex = 1;
cp.SelectedIndex = 2;
cp.SelectedIndex = 0;
Assert.Equal(3, events.Count);
Assert.Same(pages[0], events[0].prev);
Assert.Same(pages[1], events[0].curr);
Assert.Same(pages[1], events[1].prev);
Assert.Same(pages[2], events[1].curr);
Assert.Same(pages[2], events[2].prev);
Assert.Same(pages[0], events[2].curr);
}
[Fact]
public void SelectionChanged_PreviousPage_IsNull_OnFirstAutoSelection()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
PageSelectionChangedEventArgs? received = null;
cp.SelectionChanged += (_, e) => received = e;
((AvaloniaList<Page>)cp.Pages!).Add(page1);
Assert.NotNull(received);
Assert.Null(received!.PreviousPage);
Assert.Same(page1, received.CurrentPage);
}
}
public class CurrentPageChangedEvent : ScopedTestBase
{
[Fact]
public void CurrentPageChanged_FiresOnSelectionChange()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 0;
int count = 0;
cp.CurrentPageChanged += (_, _) => count++;
cp.SelectedIndex = 1;
Assert.Equal(1, count);
}
[Fact]
public void CurrentPageChanged_NotFiredWhenSamePageSelected()
{
var cp = new CarouselPage();
var page = new ContentPage { Header = "A" };
((AvaloniaList<Page>)cp.Pages!).Add(page);
cp.SelectedIndex = 0;
int count = 0;
cp.CurrentPageChanged += (_, _) => count++;
cp.SelectedIndex = 0;
Assert.Equal(0, count);
}
}
public class PagesChangedEvent : ScopedTestBase
{
[Fact]
public void PagesChanged_FiresOnAdd()
{
var cp = new CarouselPage();
NotifyCollectionChangedEventArgs? received = null;
cp.PagesChanged += (_, e) => received = e;
((AvaloniaList<Page>)cp.Pages!).Add(new ContentPage());
Assert.NotNull(received);
Assert.Equal(NotifyCollectionChangedAction.Add, received!.Action);
}
[Fact]
public void PagesChanged_FiresOnRemove()
{
var cp = new CarouselPage();
var page = new ContentPage();
((AvaloniaList<Page>)cp.Pages!).Add(page);
NotifyCollectionChangedEventArgs? received = null;
cp.PagesChanged += (_, e) => received = e;
((AvaloniaList<Page>)cp.Pages!).Remove(page);
Assert.NotNull(received);
Assert.Equal(NotifyCollectionChangedAction.Remove, received!.Action);
}
[Fact]
public void PagesChanged_NotFiredAfterPagesCollectionReplaced()
{
var cp = new CarouselPage();
var oldPages = (AvaloniaList<Page>)cp.Pages!;
cp.Pages = new AvaloniaList<Page>();
cp.PagesChanged += (_, _) => throw new InvalidOperationException("Should not fire for old collection");
oldPages.Add(new ContentPage());
}
}
public class PageLifecycleEvents : ScopedTestBase
{
[Fact]
public void SelectionChange_FiresNavigatedFrom_OnPreviousPage()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 0;
NavigatedFromEventArgs? args = null;
page1.NavigatedFrom += (_, e) => args = e;
cp.SelectedIndex = 1;
Assert.NotNull(args);
Assert.Same(page2, args!.DestinationPage);
}
[Fact]
public void SelectionChange_FiresNavigatedTo_OnNewPage()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 0;
NavigatedToEventArgs? args = null;
page2.NavigatedTo += (_, e) => args = e;
cp.SelectedIndex = 1;
Assert.NotNull(args);
Assert.Same(page1, args!.PreviousPage);
}
[Fact]
public void SelectionChange_LifecycleOrder_NavigatedFromThenNavigatedTo()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
cp.SelectedIndex = 0;
var order = new List<string>();
page1.NavigatedFrom += (_, _) => order.Add("NavigatedFrom");
page2.NavigatedTo += (_, _) => order.Add("NavigatedTo");
cp.SelectedIndex = 1;
Assert.Equal(new[] { "NavigatedFrom", "NavigatedTo" }, order);
}
[Fact]
public void SelectionChange_SamePage_NoLifecycleEvents()
{
var cp = new CarouselPage();
var page = new ContentPage { Header = "A" };
((AvaloniaList<Page>)cp.Pages!).Add(page);
cp.SelectedIndex = 0;
var events = new List<string>();
page.NavigatedTo += (_, _) => events.Add("NavigatedTo");
page.NavigatedFrom += (_, _) => events.Add("NavigatedFrom");
cp.SelectedIndex = 0;
Assert.Empty(events);
}
[Fact]
public void NavigatedFrom_NavigationType_IsReplace()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
NavigatedFromEventArgs? args = null;
page1.NavigatedFrom += (_, e) => args = e;
cp.SelectedIndex = 1;
Assert.NotNull(args);
Assert.Equal(NavigationType.Replace, args!.NavigationType);
}
[Fact]
public void NavigatedTo_NavigationType_IsReplace()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
NavigatedToEventArgs? args = null;
page2.NavigatedTo += (_, e) => args = e;
cp.SelectedIndex = 1;
Assert.NotNull(args);
Assert.Equal(NavigationType.Replace, args!.NavigationType);
}
[Fact]
public void NavigatedTo_FirstAutoSelection_NavigationType_IsReplace()
{
var cp = new CarouselPage();
var page = new ContentPage { Header = "A" };
NavigatedToEventArgs? args = null;
page.NavigatedTo += (_, e) => args = e;
((AvaloniaList<Page>)cp.Pages!).Add(page);
Assert.NotNull(args);
Assert.Equal(NavigationType.Replace, args!.NavigationType);
}
}
public class LogicalChildrenTests : ScopedTestBase
{
[Fact]
public void Pages_Added_BecomeLogicalChildren()
{
var cp = new CarouselPage();
var page = new ContentPage { Header = "A" };
((AvaloniaList<Page>)cp.Pages!).Add(page);
Assert.Contains(page, cp.GetLogicalChildren());
}
[Fact]
public void Pages_Removed_RemovedFromLogicalChildren()
{
var cp = new CarouselPage();
var page = new ContentPage { Header = "A" };
((AvaloniaList<Page>)cp.Pages!).Add(page);
((AvaloniaList<Page>)cp.Pages!).Remove(page);
Assert.DoesNotContain(page, cp.GetLogicalChildren());
}
[Fact]
public void Pages_Clear_RemovesAllLogicalChildren()
{
var cp = new CarouselPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { page1, page2 });
((AvaloniaList<Page>)cp.Pages!).Clear();
Assert.DoesNotContain(page1, cp.GetLogicalChildren());
Assert.DoesNotContain(page2, cp.GetLogicalChildren());
}
[Fact]
public void Pages_Replaced_OldPagesRemovedNewPagesAdded()
{
var cp = new CarouselPage();
var old1 = new ContentPage { Header = "Old1" };
var old2 = new ContentPage { Header = "Old2" };
((AvaloniaList<Page>)cp.Pages!).AddRange(new[] { old1, old2 });
var newPages = new AvaloniaList<Page> { new ContentPage { Header = "New1" } };
cp.Pages = newPages;
Assert.DoesNotContain(old1, cp.GetLogicalChildren());
Assert.DoesNotContain(old2, cp.GetLogicalChildren());
Assert.Contains(newPages[0], cp.GetLogicalChildren());
}
[Fact]
public void Pages_SetNull_ClearsLogicalChildren()
{
var cp = new CarouselPage();
var page = new ContentPage { Header = "A" };
((AvaloniaList<Page>)cp.Pages!).Add(page);
cp.Pages = null;
Assert.DoesNotContain(page, cp.GetLogicalChildren());
}
}
public class KeyboardNavigationTests : ScopedTestBase
{
[Fact]
public void RightKey_NavigatesToNextPage()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.SimulateKeyDown(Key.Right);
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void DownKey_NavigatesToNextPage()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.SimulateKeyDown(Key.Down);
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void LeftKey_NavigatesToPreviousPage()
{
var cp = MakeCarousel(3, selectedIndex: 2);
cp.SimulateKeyDown(Key.Left);
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void UpKey_NavigatesToPreviousPage()
{
var cp = MakeCarousel(3, selectedIndex: 2);
cp.SimulateKeyDown(Key.Up);
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void HomeKey_NavigatesToFirstPage()
{
var cp = MakeCarousel(3, selectedIndex: 2);
cp.SimulateKeyDown(Key.Home);
Assert.Equal(0, cp.SelectedIndex);
}
[Fact]
public void EndKey_NavigatesToLastPage()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.SimulateKeyDown(Key.End);
Assert.Equal(2, cp.SelectedIndex);
}
[Fact]
public void LeftKey_AtFirstPage_DoesNotNavigate()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.SimulateKeyDown(Key.Left);
Assert.Equal(0, cp.SelectedIndex);
}
[Fact]
public void RightKey_AtLastPage_DoesNotNavigate()
{
var cp = MakeCarousel(3, selectedIndex: 2);
cp.SimulateKeyDown(Key.Right);
Assert.Equal(2, cp.SelectedIndex);
}
[Fact]
public void KeyNavigation_Disabled_IgnoresArrowKey()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.IsKeyboardNavigationEnabled = false;
cp.SimulateKeyDown(Key.Right);
Assert.Equal(0, cp.SelectedIndex);
}
[Fact]
public void RightKey_MarksEventHandled()
{
var cp = MakeCarousel(3, selectedIndex: 0);
var handled = cp.SimulateKeyDownReturnsHandled(Key.Right);
Assert.True(handled);
}
[Fact]
public void RightKey_AtLastPage_DoesNotMarkEventHandled()
{
var cp = MakeCarousel(3, selectedIndex: 2);
var handled = cp.SimulateKeyDownReturnsHandled(Key.Right);
Assert.False(handled);
}
[Fact]
public void RtlFlowDirection_LeftKey_NavigatesToNextPage()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.FlowDirection = FlowDirection.RightToLeft;
cp.SimulateKeyDown(Key.Left);
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void RtlFlowDirection_RightKey_NavigatesToPreviousPage()
{
var cp = MakeCarousel(3, selectedIndex: 2);
cp.FlowDirection = FlowDirection.RightToLeft;
cp.SimulateKeyDown(Key.Right);
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void RtlFlowDirection_LeftKey_AtLastPage_DoesNotNavigate()
{
var cp = MakeCarousel(3, selectedIndex: 2);
cp.FlowDirection = FlowDirection.RightToLeft;
cp.SimulateKeyDown(Key.Left);
Assert.Equal(2, cp.SelectedIndex);
}
[Fact]
public void RtlFlowDirection_RightKey_AtFirstPage_DoesNotNavigate()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.FlowDirection = FlowDirection.RightToLeft;
cp.SimulateKeyDown(Key.Right);
Assert.Equal(0, cp.SelectedIndex);
}
private static TestableCarouselPage MakeCarousel(int count, int selectedIndex)
{
var cp = new TestableCarouselPage();
for (var i = 0; i < count; i++)
((AvaloniaList<Page>)cp.Pages!).Add(new ContentPage { Header = $"P{i}" });
cp.SelectedIndex = selectedIndex;
return cp;
}
}
public class WheelBehavior : ScopedTestBase
{
[Fact]
public void WheelDown_NavigatesForward()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.SimulateWheel(new Vector(0, -1));
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void WheelUp_NavigatesBackward()
{
var cp = MakeCarousel(3, selectedIndex: 2);
cp.SimulateWheel(new Vector(0, 1));
Assert.Equal(1, cp.SelectedIndex);
}
[Fact]
public void WheelDown_AtLastPage_DoesNotHandleEvent()
{
var cp = MakeCarousel(3, selectedIndex: 2);
var handled = cp.SimulateWheelReturnsHandled(new Vector(0, -1));
Assert.Equal(2, cp.SelectedIndex);
Assert.False(handled);
}
[Fact]
public void WheelUp_AtFirstPage_DoesNotHandleEvent()
{
var cp = MakeCarousel(3, selectedIndex: 0);
var handled = cp.SimulateWheelReturnsHandled(new Vector(0, 1));
Assert.Equal(0, cp.SelectedIndex);
Assert.False(handled);
}
[Fact]
public void Wheel_WhenGestureDisabled_DoesNotHandleEvent()
{
var cp = MakeCarousel(3, selectedIndex: 0);
cp.IsGestureEnabled = false;
var handled = cp.SimulateWheelReturnsHandled(new Vector(0, -1));
Assert.Equal(0, cp.SelectedIndex);
Assert.False(handled);
}
[Fact]
public void Wheel_HandledByChild_DoesNotNavigate()
{
var cp = new CarouselPage
{
Width = 400,
Height = 300,
IsGestureEnabled = true,
Template = CreateCarouselPageTemplate(),
};
var child = new Border();
child.AddHandler(InputElement.PointerWheelChangedEvent, (_, e) => e.Handled = true);
var page0 = new ContentPage { Header = "P0", Content = child };
var page1 = new ContentPage { Header = "P1" };
var page2 = new ContentPage { Header = "P2" };
((AvaloniaList<Page>)cp.Pages!).Add(page0);
((AvaloniaList<Page>)cp.Pages!).Add(page1);
((AvaloniaList<Page>)cp.Pages!).Add(page2);
var root = new TestRoot(cp) { ClientSize = new Size(400, 300) };
root.LayoutManager.ExecuteInitialLayoutPass();
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
var wheelArgs = new PointerWheelEventArgs(
child,
pointer,
root,
default,
0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
KeyModifiers.None,
new Vector(0, -1))
{
RoutedEvent = InputElement.PointerWheelChangedEvent
};
child.RaiseEvent(wheelArgs);
Assert.True(wheelArgs.Handled);
Assert.Equal(0, cp.SelectedIndex);
}
private static FuncControlTemplate<CarouselPage> CreateCarouselPageTemplate()
{
return new FuncControlTemplate<CarouselPage>((_, scope) =>
new Carousel
{
Name = "PART_Carousel",
Template = new FuncControlTemplate((c, ns) =>
new ScrollViewer
{
Name = "PART_ScrollViewer",
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(ns)
}.RegisterInNameScope(ns)),
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
}.RegisterInNameScope(scope));
}
private static TestableCarouselPage MakeCarousel(int count, int selectedIndex)
{
var cp = new TestableCarouselPage();
for (var i = 0; i < count; i++)
((AvaloniaList<Page>)cp.Pages!).Add(new ContentPage { Header = $"P{i}" });
cp.SelectedIndex = selectedIndex;
_ = new TestRoot { Child = cp };
return cp;
}
}
public class DataTemplateTests : ScopedTestBase
{
private record DataItem(string Name);
[Fact]
public void ItemsSource_SelectedPage_IsResolvedAfterLayout()
{
var cp = CreateTemplatedCarouselPage<CarouselPage>(
new[] { new DataItem("First"), new DataItem("Second") });
var root = new TestRoot { ClientSize = new Size(400, 300), Child = cp };
root.ExecuteInitialLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.NotNull(cp.SelectedPage);
Assert.Equal("First", cp.SelectedPage!.Header?.ToString());
Assert.Equal("Page 1 of 2: First", Avalonia.Automation.AutomationProperties.GetName(cp));
}
[Fact]
public void KeyboardNavigation_UsesItemsSourceCount()
{
var cp = CreateTemplatedCarouselPage<TestableCarouselPage>(
new[] { new DataItem("First"), new DataItem("Second") });
var root = new TestRoot { ClientSize = new Size(400, 300), Child = cp };
root.ExecuteInitialLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
cp.SimulateKeyDown(Key.Right);
root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal(1, cp.SelectedIndex);
Assert.NotNull(cp.SelectedPage);
Assert.Equal("Second", cp.SelectedPage!.Header?.ToString());
Assert.Equal("Page 2 of 2: Second", Avalonia.Automation.AutomationProperties.GetName(cp));
}
[Fact]
public void WheelNavigation_UsesItemsSourceCount()
{
var cp = CreateTemplatedCarouselPage<TestableCarouselPage>(
new[] { new DataItem("First"), new DataItem("Second") });
var root = new TestRoot { ClientSize = new Size(400, 300), Child = cp };
root.ExecuteInitialLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
var handled = cp.SimulateWheelReturnsHandled(new Vector(0, -1));
root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.True(handled);
Assert.Equal(1, cp.SelectedIndex);
Assert.NotNull(cp.SelectedPage);
Assert.Equal("Second", cp.SelectedPage!.Header?.ToString());
}
[Fact]
public void ItemsSource_SelectionChanges_FireLifecycleOnGeneratedPages()
{
var cp = CreateTemplatedCarouselPage<TestableCarouselPage>(
new[] { new DataItem("First"), new DataItem("Second") },
item => new TrackingPage
{
Header = item?.Name,
Content = new Border { Width = 400, Height = 300 }
});
var root = new TestRoot { ClientSize = new Size(400, 300), Child = cp };
root.ExecuteInitialLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
var firstPage = Assert.IsType<TrackingPage>(cp.SelectedPage);
Assert.Equal(1, firstPage.NavigatedToCount);
Assert.Equal(0, firstPage.NavigatedFromCount);
cp.SimulateKeyDown(Key.Right);
root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
var secondPage = Assert.IsType<TrackingPage>(cp.SelectedPage);
Assert.Equal(1, firstPage.NavigatedFromCount);
Assert.Equal(1, secondPage.NavigatedToCount);
Assert.Same(secondPage, cp.CurrentPage);
}
[Fact]
public void PageTemplate_ChangedAfterContainersRealized_UpdatesSelectedPage()
{
var cp = CreateTemplatedCarouselPage<CarouselPage>(
new[] { new DataItem("First"), new DataItem("Second") },
item => new ContentPage
{
Header = $"Detail {item?.Name}",
Content = new Border { Width = 400, Height = 300 }
});
var root = new TestRoot { ClientSize = new Size(400, 300), Child = cp };
root.ExecuteInitialLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
var originalPage = Assert.IsType<ContentPage>(cp.SelectedPage);
Assert.Equal("Detail First", originalPage.Header);
cp.PageTemplate = new FuncDataTemplate<DataItem>(
(item, _) => new ContentPage
{
Header = $"Showcase {item?.Name}",
Content = new Border { Width = 400, Height = 300 }
},
supportsRecycling: false);
root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
var updatedPage = Assert.IsType<ContentPage>(cp.SelectedPage);
Assert.NotSame(originalPage, updatedPage);
Assert.Equal("Showcase First", updatedPage.Header);
}
private static T CreateTemplatedCarouselPage<T>(IEnumerable<DataItem> items)
where T : CarouselPage, new()
{
return CreateTemplatedCarouselPage<T>(
items,
item => new ContentPage
{
Header = item?.Name,
Content = new Border { Width = 400, Height = 300 }
});
}
private static T CreateTemplatedCarouselPage<T>(IEnumerable<DataItem> items, Func<DataItem?, Page> pageFactory)
where T : CarouselPage, new()
{
return new T
{
Width = 400,
Height = 300,
ItemsSource = items,
PageTemplate = new FuncDataTemplate<DataItem>(
(item, _) => pageFactory(item),
supportsRecycling: false),
Template = CreateCarouselPageTemplate(),
};
}
private static FuncControlTemplate<CarouselPage> CreateCarouselPageTemplate()
{
return new FuncControlTemplate<CarouselPage>((_, scope) =>
new Carousel
{
Name = "PART_Carousel",
Template = CarouselTemplate(),
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
}.RegisterInNameScope(scope));
}
private static IControlTemplate CarouselTemplate()
{
return new FuncControlTemplate((c, ns) =>
new ScrollViewer
{
Name = "PART_ScrollViewer",
Template = ScrollViewerTemplate(),
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(ns)
}.RegisterInNameScope(ns));
}
private static FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate((_, ns) =>
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
}.RegisterInNameScope(ns));
}
private sealed class TrackingPage : ContentPage
{
public int NavigatedToCount { get; private set; }
public int NavigatedFromCount { get; private set; }
protected override void OnNavigatedTo(NavigatedToEventArgs args)
{
NavigatedToCount++;
base.OnNavigatedTo(args);
}
protected override void OnNavigatedFrom(NavigatedFromEventArgs args)
{
NavigatedFromCount++;
base.OnNavigatedFrom(args);
}
}
}
private sealed class TestableCarouselPage : CarouselPage
{
public void SimulateKeyDown(Key key)
{
var e = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = key };
OnKeyDown(e);
}
public bool SimulateKeyDownReturnsHandled(Key key)
{
var e = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = key };
OnKeyDown(e);
return e.Handled;
}
public void SimulateWheel(Vector delta)
{
SimulateWheelReturnsHandled(delta);
}
public bool SimulateWheelReturnsHandled(Vector delta)
{
var pointer = new FakePointer();
var e = new PointerWheelEventArgs(this, pointer, this, default, 0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.Other),
KeyModifiers.None, delta)
{
RoutedEvent = PointerWheelChangedEvent
};
RaiseEvent(e);
return e.Handled;
}
}
public class SwipeGestureTests : ScopedTestBase
{
[Fact]
public void MouseSwipe_Advances_Page()
{
var clock = new MockGlobalClock();
using var app = UnitTestApplication.Start(
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
using var sync = UnitTestSynchronizationContext.Begin();
var (cp, carousel, panel) = CreateSwipeReadyCarouselPage();
var mouse = new MouseTestHelper();
mouse.Down(panel, position: new Point(200, 100));
mouse.Move(panel, new Point(40, 100));
mouse.Up(panel, position: new Point(40, 100));
clock.Pulse(TimeSpan.Zero);
clock.Pulse(TimeSpan.FromSeconds(1));
sync.ExecutePostedCallbacks();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal(1, carousel.SelectedIndex);
Assert.Equal(1, cp.SelectedIndex);
Assert.Same(((AvaloniaList<Page>)cp.Pages!)[1], cp.CurrentPage);
}
[Fact]
public void TouchSwipe_Advances_Page()
{
var clock = new MockGlobalClock();
using var app = UnitTestApplication.Start(
TestServices.MockPlatformRenderInterface.With(globalClock: clock));
using var sync = UnitTestSynchronizationContext.Begin();
var (cp, carousel, panel) = CreateSwipeReadyCarouselPage();
var touch = new TouchTestHelper();
touch.Down(panel, new Point(200, 100));
touch.Move(panel, new Point(40, 100));
touch.Up(panel, new Point(40, 100));
clock.Pulse(TimeSpan.Zero);
clock.Pulse(TimeSpan.FromSeconds(1));
sync.ExecutePostedCallbacks();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
Assert.Equal(1, carousel.SelectedIndex);
Assert.Equal(1, cp.SelectedIndex);
Assert.Same(((AvaloniaList<Page>)cp.Pages!)[1], cp.CurrentPage);
}
private static (CarouselPage Page, Carousel Carousel, VirtualizingCarouselPanel Panel) CreateSwipeReadyCarouselPage()
{
var cp = new CarouselPage
{
Width = 400,
Height = 300,
IsGestureEnabled = true,
PageTransition = new PageSlide(TimeSpan.FromMilliseconds(1)),
Pages = new AvaloniaList<Page>
{
new ContentPage { Header = "A", Content = new Border { Width = 400, Height = 300 } },
new ContentPage { Header = "B", Content = new Border { Width = 400, Height = 300 } },
new ContentPage { Header = "C", Content = new Border { Width = 400, Height = 300 } }
},
Template = new FuncControlTemplate<CarouselPage>((parent, scope) =>
new Carousel
{
Name = "PART_Carousel",
Template = CarouselTemplate(),
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
[~ItemsControl.ItemsSourceProperty] = parent[~CarouselPage.PagesProperty],
[~ItemsControl.ItemTemplateProperty] = parent[~CarouselPage.PageTemplateProperty],
[~ItemsControl.ItemsPanelProperty] = parent[~CarouselPage.ItemsPanelProperty],
[~Carousel.PageTransitionProperty] = parent[~CarouselPage.PageTransitionProperty],
}.RegisterInNameScope(scope))
};
var root = new TestRoot
{
ClientSize = new Size(400, 300),
Child = cp
};
root.ExecuteInitialLayoutPass();
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
var carousel = cp.GetVisualDescendants().OfType<Carousel>().Single();
var panel = Assert.IsType<VirtualizingCarouselPanel>(carousel.Presenter!.Panel!);
Assert.True(carousel.IsSwipeEnabled);
var recognizer = Assert.Single(panel.GestureRecognizers.OfType<SwipeGestureRecognizer>());
Assert.True(recognizer.IsEnabled);
Assert.True(recognizer.CanHorizontallySwipe);
recognizer.IsMouseEnabled = true;
return (cp, carousel, panel);
}
private static IControlTemplate CarouselTemplate()
{
return new FuncControlTemplate((c, ns) =>
new ScrollViewer
{
Name = "PART_ScrollViewer",
Template = ScrollViewerTemplate(),
HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
Content = new ItemsPresenter
{
Name = "PART_ItemsPresenter",
[~ItemsPresenter.ItemsPanelProperty] = c[~ItemsControl.ItemsPanelProperty],
}.RegisterInNameScope(ns)
}.RegisterInNameScope(ns));
}
private static FuncControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((parent, scope) =>
new Panel
{
Children =
{
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
}.RegisterInNameScope(scope),
}
});
}
}
public class InteractiveTransitionTests : ScopedTestBase
{
[Fact]
public void PageSlide_Update_AppliesTranslateTransformToFrom()
{
var parent = new Canvas { Width = 400, Height = 300 };
var from = new Border();
var to = new Border();
parent.Children.Add(from);
parent.Children.Add(to);
parent.Measure(new Size(400, 300));
parent.Arrange(new Rect(0, 0, 400, 300));
var slide = new PageSlide(TimeSpan.FromMilliseconds(300));
slide.Update(0.5, from, to, true, 400, Array.Empty<PageTransitionItem>());
Assert.IsType<TranslateTransform>(from.RenderTransform);
var ft = (TranslateTransform)from.RenderTransform!;
Assert.Equal(-200, ft.X);
}
[Fact]
public void PageSlide_Update_AppliesTranslateTransformToTo()
{
var parent = new Canvas { Width = 400, Height = 300 };
var from = new Border();
var to = new Border();
parent.Children.Add(from);
parent.Children.Add(to);
parent.Measure(new Size(400, 300));
parent.Arrange(new Rect(0, 0, 400, 300));
var slide = new PageSlide(TimeSpan.FromMilliseconds(300));
slide.Update(0.5, from, to, true, 400, Array.Empty<PageTransitionItem>());
Assert.IsType<TranslateTransform>(to.RenderTransform);
var tt = (TranslateTransform)to.RenderTransform!;
Assert.Equal(200, tt.X);
}
[Fact]
public void CrossFade_Update_SetsOpacity()
{
var from = new Border();
var to = new Border();
var crossFade = new CrossFade(TimeSpan.FromMilliseconds(300));
crossFade.Update(0.5, from, to, true, 0, Array.Empty<PageTransitionItem>());
Assert.Equal(0.5, from.Opacity, 2);
Assert.Equal(0.5, to.Opacity, 2);
Assert.True(to.IsVisible);
}
}
public class CarouselIsSwipeEnabledTests : ScopedTestBase
{
[Fact]
public void IsSwipeEnabled_DefaultIsFalse()
{
var carousel = new Carousel();
Assert.False(carousel.IsSwipeEnabled);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void IsSwipeEnabled_RoundTrips(bool value)
{
var carousel = new Carousel { IsSwipeEnabled = value };
Assert.Equal(value, carousel.IsSwipeEnabled);
}
[Fact]
public void GetTransitionAxis_ReturnsNull_WhenNoTransition()
{
var carousel = new Carousel();
Assert.Null(carousel.GetTransitionAxis());
}
[Fact]
public void GetTransitionAxis_ReturnsNull_WhenNonPageSlideTransition()
{
var carousel = new Carousel { PageTransition = new CrossFade(TimeSpan.FromMilliseconds(200)) };
Assert.Null(carousel.GetTransitionAxis());
}
[Fact]
public void GetTransitionAxis_ReturnsVertical_WhenPageSlideVertical()
{
var carousel = new Carousel
{
PageTransition = new PageSlide(TimeSpan.FromMilliseconds(200), PageSlide.SlideAxis.Vertical)
};
Assert.Equal(PageSlide.SlideAxis.Vertical, carousel.GetTransitionAxis());
}
[Fact]
public void GetTransitionAxis_ReturnsVertical_WhenRotate3DVertical()
{
var carousel = new Carousel
{
PageTransition = new Rotate3DTransition(TimeSpan.FromMilliseconds(200), PageSlide.SlideAxis.Vertical)
};
Assert.Equal(PageSlide.SlideAxis.Vertical, carousel.GetTransitionAxis());
}
}
private sealed class TestPageTransition : IPageTransition
{
public Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
=> Task.CompletedTask;
}
private sealed class FakePointer : IPointer
{
public int Id { get; } = Pointer.GetNextFreeId();
public void Capture(IInputElement? control) => Captured = control;
public IInputElement? Captured { get; set; }
public PointerType Type => PointerType.Mouse;
public bool IsPrimary => true;
}
public class VisualTreeLifecycleTests : ScopedTestBase
{
[Fact]
public void Detach_And_Reattach_CollectionChangedStillUpdatesSelection()
{
var pages = new AvaloniaList<Page>();
var cp = new CarouselPage { Pages = pages };
var root = new TestRoot { Child = cp };
var page1 = new ContentPage { Header = "A" };
pages.Add(page1);
Assert.Same(page1, cp.SelectedPage);
root.Child = null;
root.Child = cp;
var page2 = new ContentPage { Header = "B" };
pages.Add(page2);
pages.Remove(page1);
Assert.Same(page2, cp.SelectedPage);
}
}
}