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.
 
 
 

820 lines
27 KiB

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Animation;
using Avalonia.Collections;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests;
public class TabbedPageTests
{
public class PropertyDefaults : ScopedTestBase
{
[Fact]
public void TabPlacement_DefaultIsAuto()
{
// Auto resolves to Bottom on iOS/Android and Top everywhere else.
var tp = new TabbedPage();
Assert.Equal(TabPlacement.Auto, tp.TabPlacement);
}
[Fact]
public void SelectedIndex_InitiallyMinusOne()
{
// -1 is the "no selection" sentinel used throughout the selection API.
var tp = new TabbedPage();
Assert.Equal(-1, tp.SelectedIndex);
}
[Fact]
public void SelectedPage_InitiallyNull()
{
var tp = new TabbedPage();
Assert.Null(tp.SelectedPage);
}
}
public class PropertyRoundTrips : ScopedTestBase
{
[Theory]
[InlineData(TabPlacement.Auto)]
[InlineData(TabPlacement.Top)]
[InlineData(TabPlacement.Bottom)]
[InlineData(TabPlacement.Left)]
[InlineData(TabPlacement.Right)]
public void TabPlacement_RoundTrips(TabPlacement placement)
{
var tp = new TabbedPage { TabPlacement = placement };
Assert.Equal(placement, tp.TabPlacement);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void IsKeyboardNavigationEnabled_RoundTrips(bool enabled)
{
var tp = new TabbedPage { IsKeyboardNavigationEnabled = enabled };
Assert.Equal(enabled, tp.IsKeyboardNavigationEnabled);
}
[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(3)]
public void SelectedIndex_StoredBeforeTemplateApplied(int index)
{
var tp = new TabbedPage();
tp.SelectedIndex = index;
Assert.Equal(index, tp.SelectedIndex);
}
[Fact]
public void PageTemplate_CanBeSetToNull()
{
var tp = new TabbedPage { PageTemplate = null };
Assert.Null(tp.PageTemplate);
}
[Fact]
public void PageTransition_RoundTrips()
{
var transition = new CrossFade(TimeSpan.FromMilliseconds(200));
var tp = new TabbedPage { PageTransition = transition };
Assert.Same(transition, tp.PageTransition);
}
[Fact]
public void IndicatorTemplate_RoundTrips()
{
var template = new FuncDataTemplate<object>((_, _) => new Border());
var tp = new TabbedPage { IndicatorTemplate = template };
Assert.Same(template, tp.IndicatorTemplate);
}
[Fact]
public void IndicatorTemplate_CanBeSetToNull()
{
var template = new FuncDataTemplate<object>((_, _) => new Border());
var tp = new TabbedPage { IndicatorTemplate = template };
tp.IndicatorTemplate = null;
Assert.Null(tp.IndicatorTemplate);
}
}
public class PagesCollectionTests : ScopedTestBase
{
[Fact]
public void Pages_InitiallyNonNull_EmptyList()
{
var tp = new TabbedPage();
Assert.NotNull(tp.Pages);
}
[Fact]
public void Pages_SetNewList_UpdatesProperty()
{
var tp = new TabbedPage();
var pages = new AvaloniaList<Page> { new ContentPage { Header = "A" } };
tp.Pages = pages;
Assert.Same(pages, tp.Pages);
}
[Fact]
public void Pages_Added_BecomeLogicalChildren()
{
var tp = new TabbedPage();
var pages = new AvaloniaList<Page>();
tp.Pages = pages;
var page1 = new ContentPage { Header = "Tab 1" };
var page2 = new ContentPage { Header = "Tab 2" };
pages.Add(page1);
pages.Add(page2);
var children = ((ILogical)tp).LogicalChildren;
Assert.Contains(page1, children);
Assert.Contains(page2, children);
}
[Fact]
public void Pages_Removed_RemovedFromLogicalChildren()
{
var tp = new TabbedPage();
var page1 = new ContentPage { Header = "Tab 1" };
var page2 = new ContentPage { Header = "Tab 2" };
var pages = new AvaloniaList<Page> { page1, page2 };
tp.Pages = pages;
pages.Remove(page1);
Assert.DoesNotContain(page1, ((ILogical)tp).LogicalChildren);
Assert.Contains(page2, ((ILogical)tp).LogicalChildren);
}
[Fact]
public void Pages_Replaced_OldLogicalChildrenClearedNewAdded()
{
var tp = new TabbedPage();
var old = new ContentPage { Header = "Old" };
tp.Pages = new AvaloniaList<Page> { old };
var fresh = new ContentPage { Header = "Fresh" };
tp.Pages = new AvaloniaList<Page> { fresh };
Assert.DoesNotContain(old, ((ILogical)tp).LogicalChildren);
Assert.Contains(fresh, ((ILogical)tp).LogicalChildren);
}
[Fact]
public void Pages_SetNull_ClearsLogicalChildren()
{
var tp = new TabbedPage();
var page = new ContentPage();
tp.Pages = new AvaloniaList<Page> { page };
tp.Pages = null;
Assert.DoesNotContain(page, ((ILogical)tp).LogicalChildren);
}
[Fact]
public void Pages_AddMultiple_AllBecomeLogicalChildren()
{
var tp = new TabbedPage();
var pages = new AvaloniaList<Page>();
tp.Pages = pages;
var list = new List<ContentPage>();
for (int i = 0; i < 5; i++)
{
var p = new ContentPage { Header = $"Tab {i}" };
list.Add(p);
pages.Add(p);
}
var children = ((ILogical)tp).LogicalChildren;
foreach (var p in list)
Assert.Contains(p, children);
}
[Fact]
public void Pages_Clear_RemovesAllLogicalChildren()
{
var tp = new TabbedPage();
var a = new ContentPage { Header = "A" };
var b = new ContentPage { Header = "B" };
var pages = new AvaloniaList<Page> { a, b };
tp.Pages = pages;
pages.Clear();
Assert.DoesNotContain(a, ((ILogical)tp).LogicalChildren);
Assert.DoesNotContain(b, ((ILogical)tp).LogicalChildren);
}
}
public class PagesChangedEventTests : ScopedTestBase
{
[Fact]
public void PagesChanged_FiresOnAdd()
{
var tp = new TabbedPage();
var pages = new AvaloniaList<Page>();
tp.Pages = pages;
NotifyCollectionChangedEventArgs? received = null;
tp.PagesChanged += (_, e) => received = e;
pages.Add(new ContentPage());
Assert.NotNull(received);
Assert.Equal(NotifyCollectionChangedAction.Add, received!.Action);
}
[Fact]
public void PagesChanged_FiresOnRemove()
{
var tp = new TabbedPage();
var page = new ContentPage();
var pages = new AvaloniaList<Page> { page };
tp.Pages = pages;
NotifyCollectionChangedEventArgs? received = null;
tp.PagesChanged += (_, e) => received = e;
pages.Remove(page);
Assert.NotNull(received);
Assert.Equal(NotifyCollectionChangedAction.Remove, received!.Action);
}
[Fact]
public void PagesChanged_NotFiredAfterPagesReplaced()
{
var tp = new TabbedPage();
var oldPages = new AvaloniaList<Page>();
tp.Pages = oldPages;
bool fired = false;
tp.PagesChanged += (_, _) => fired = true;
tp.Pages = new AvaloniaList<Page>();
oldPages.Add(new ContentPage());
Assert.False(fired);
}
[Fact]
public void PagesChanged_Add_ArgsContainAddedPage()
{
var tp = new TabbedPage();
var pages = new AvaloniaList<Page>();
tp.Pages = pages;
NotifyCollectionChangedEventArgs? received = null;
tp.PagesChanged += (_, e) => received = e;
var page = new ContentPage { Header = "New" };
pages.Add(page);
Assert.NotNull(received);
Assert.NotNull(received!.NewItems);
Assert.True(received.NewItems!.Contains(page));
}
[Fact]
public void PagesChanged_Remove_ArgsContainRemovedPage()
{
var tp = new TabbedPage();
var page = new ContentPage { Header = "ToRemove" };
var pages = new AvaloniaList<Page> { page };
tp.Pages = pages;
NotifyCollectionChangedEventArgs? received = null;
tp.PagesChanged += (_, e) => received = e;
pages.Remove(page);
Assert.NotNull(received);
Assert.NotNull(received!.OldItems);
Assert.True(received.OldItems!.Contains(page));
}
[Fact]
public void PagesChanged_FiresOnClear_WithResetAction()
{
var tp = new TabbedPage();
var pages = new AvaloniaList<Page>
{
new ContentPage { Header = "A" },
new ContentPage { Header = "B" },
};
tp.Pages = pages;
NotifyCollectionChangedEventArgs? received = null;
tp.PagesChanged += (_, e) => received = e;
pages.Clear();
Assert.NotNull(received);
Assert.Equal(NotifyCollectionChangedAction.Reset, received!.Action);
}
}
public class SelectionTests : ScopedTestBase
{
[Fact]
public void SelectionChanged_FiresWhenSelectionChanges()
{
var tp = new TestableTabbedPage();
PageSelectionChangedEventArgs? received = null;
tp.SelectionChanged += (_, e) => received = e;
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
tp.CallCommitSelection(0, page1);
tp.CallCommitSelection(1, page2);
Assert.NotNull(received);
Assert.Same(page1, received!.PreviousPage);
Assert.Same(page2, received!.CurrentPage);
}
[Fact]
public void SelectionChanged_NotFiredWhenSamePageSelected()
{
var tp = new TestableTabbedPage();
int count = 0;
tp.SelectionChanged += (_, _) => count++;
var page = new ContentPage { Header = "A" };
tp.CallCommitSelection(0, page);
int countAfterFirst = count;
tp.CallCommitSelection(0, page);
Assert.Equal(1, countAfterFirst); // first commit must fire exactly once
Assert.Equal(1, count); // second commit (same page) must not fire again
}
[Fact]
public void CommitSelection_UpdatesCurrentPage()
{
var tp = new TestableTabbedPage();
var page = new ContentPage { Header = "X" };
tp.CallCommitSelection(0, page);
Assert.Same(page, tp.CurrentPage);
Assert.Same(page, tp.SelectedPage);
Assert.Equal(0, tp.SelectedIndex);
}
[Fact]
public void CommitSelection_SequentialSelections_TracksCorrectPages()
{
var tp = new TestableTabbedPage();
var pages = new[]
{
new ContentPage { Header = "Feed" },
new ContentPage { Header = "Explore" },
new ContentPage { Header = "Profile" },
};
var events = new List<(Page? prev, Page? curr)>();
tp.SelectionChanged += (_, e) => events.Add((e.PreviousPage, e.CurrentPage));
tp.CallCommitSelection(0, pages[0]);
tp.CallCommitSelection(1, pages[1]);
tp.CallCommitSelection(2, pages[2]);
tp.CallCommitSelection(0, pages[0]);
Assert.Equal(4, events.Count);
Assert.Null(events[0].prev);
Assert.Same(pages[0], events[0].curr);
Assert.Same(pages[0], events[1].prev);
Assert.Same(pages[1], events[1].curr);
Assert.Same(pages[1], events[2].prev);
Assert.Same(pages[2], events[2].curr);
Assert.Same(pages[2], events[3].prev);
Assert.Same(pages[0], events[3].curr);
}
[Fact]
public void CommitSelection_NullPage_SetsCurrentPageToNull()
{
var tp = new TestableTabbedPage();
tp.CallCommitSelection(0, new ContentPage());
tp.CallCommitSelection(-1, null);
Assert.Null(tp.CurrentPage);
Assert.Null(tp.SelectedPage);
Assert.Equal(-1, tp.SelectedIndex);
}
[Fact]
public void CommitSelection_RapidChanges_TracksFinalState()
{
var tp = new TestableTabbedPage();
var pages = new[]
{
new ContentPage { Header = "A" },
new ContentPage { Header = "B" },
new ContentPage { Header = "C" },
};
for (int i = 0; i < pages.Length; i++)
tp.CallCommitSelection(i, pages[i]);
Assert.Same(pages[2], tp.CurrentPage);
Assert.Same(pages[2], tp.SelectedPage);
Assert.Equal(2, tp.SelectedIndex);
}
[Fact]
public void CurrentPageChanged_FiresOnCommitSelection()
{
var tp = new TestableTabbedPage();
int count = 0;
tp.CurrentPageChanged += (_, _) => count++;
tp.CallCommitSelection(0, new ContentPage());
Assert.Equal(1, count);
}
[Fact]
public void CurrentPageChanged_NotFiredWhenSamePageCommitted()
{
var tp = new TestableTabbedPage();
var page = new ContentPage();
tp.CallCommitSelection(0, page);
int count = 0;
tp.CurrentPageChanged += (_, _) => count++;
tp.CallCommitSelection(0, page);
Assert.Equal(0, count);
}
}
public class LifecycleTests : ScopedTestBase
{
[Fact]
public void CommitSelection_FiresNavigatedFrom_OnPreviousPage()
{
var tp = new TestableTabbedPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
tp.CallCommitSelection(0, page1);
NavigatedFromEventArgs? args = null;
page1.NavigatedFrom += (_, e) => args = e;
tp.CallCommitSelection(1, page2);
Assert.NotNull(args);
Assert.Same(page2, args!.DestinationPage);
Assert.Equal(NavigationType.Replace, args.NavigationType);
}
[Fact]
public void CommitSelection_FiresNavigatedTo_OnNewPage()
{
var tp = new TestableTabbedPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
tp.CallCommitSelection(0, page1);
NavigatedToEventArgs? args = null;
page2.NavigatedTo += (_, e) => args = e;
tp.CallCommitSelection(1, page2);
Assert.NotNull(args);
Assert.Same(page1, args!.PreviousPage);
Assert.Equal(NavigationType.Replace, args.NavigationType);
}
[Fact]
public void CommitSelection_LifecycleOrder_NavigatedFromNavigatedTo()
{
var tp = new TestableTabbedPage();
var page1 = new ContentPage { Header = "A" };
var page2 = new ContentPage { Header = "B" };
tp.CallCommitSelection(0, page1);
var order = new List<string>();
page1.NavigatedFrom += (_, _) => order.Add("NavigatedFrom");
page2.NavigatedTo += (_, _) => order.Add("NavigatedTo");
tp.CallCommitSelection(1, page2);
Assert.Equal(new[] { "NavigatedFrom", "NavigatedTo" }, order);
}
[Fact]
public void CommitSelection_SamePage_NoLifecycleEvents()
{
var tp = new TestableTabbedPage();
var page = new ContentPage { Header = "A" };
tp.CallCommitSelection(0, page);
var events = new List<string>();
page.NavigatedTo += (_, _) => events.Add("NavigatedTo");
page.NavigatedFrom += (_, _) => events.Add("NavigatedFrom");
tp.CallCommitSelection(0, page);
Assert.Empty(events);
}
[Fact]
public void CommitSelection_FirstPage_NavigatedToHasNullPrevious()
{
var tp = new TestableTabbedPage();
var page = new ContentPage { Header = "A" };
NavigatedToEventArgs? navigatedToArgs = null;
var events = new List<string>();
page.NavigatedTo += (_, e) => { navigatedToArgs = e; events.Add("NavigatedTo"); };
page.NavigatedFrom += (_, _) => events.Add("NavigatedFrom");
tp.CallCommitSelection(0, page);
Assert.Equal(new[] { "NavigatedTo" }, events);
Assert.NotNull(navigatedToArgs);
Assert.Null(navigatedToArgs!.PreviousPage);
Assert.Equal(NavigationType.Replace, navigatedToArgs.NavigationType);
}
[Fact]
public void CommitSelection_ToNull_FiresNavigatedFrom_WithNullDestination()
{
var tp = new TestableTabbedPage();
var page = new ContentPage { Header = "A" };
tp.CallCommitSelection(0, page);
NavigatedFromEventArgs? navigatedFromArgs = null;
var events = new List<string>();
page.NavigatedFrom += (_, e) => { navigatedFromArgs = e; events.Add("NavigatedFrom"); };
tp.CallCommitSelection(-1, null);
Assert.Equal(new[] { "NavigatedFrom" }, events);
Assert.NotNull(navigatedFromArgs);
Assert.Null(navigatedFromArgs!.DestinationPage);
Assert.Equal(NavigationType.Replace, navigatedFromArgs.NavigationType);
}
}
public class IsTabEnabledTests : ScopedTestBase
{
[Fact]
public void IsTabEnabled_DefaultIsTrue()
{
var page = new ContentPage();
Assert.True(TabbedPage.GetIsTabEnabled(page));
}
[Fact]
public void IsTabEnabled_SetFalse_GetFalse()
{
var page = new ContentPage();
TabbedPage.SetIsTabEnabled(page, false);
Assert.False(TabbedPage.GetIsTabEnabled(page));
}
[Fact]
public void IsTabEnabled_SetTrue_GetTrue()
{
var page = new ContentPage();
TabbedPage.SetIsTabEnabled(page, false);
TabbedPage.SetIsTabEnabled(page, true);
Assert.True(TabbedPage.GetIsTabEnabled(page));
}
}
public class FindNextEnabledTabTests : ScopedTestBase
{
[Fact]
public void Forward_SkipsDisabled()
{
var tp = new TestableTabbedPage();
var page0 = new ContentPage();
var page1 = new ContentPage();
var page2 = new ContentPage();
tp.Pages = new AvaloniaList<Page> { page0, page1, page2 };
TabbedPage.SetIsTabEnabled(page1, false);
int result = tp.CallFindNextEnabledTab(1, 1);
Assert.Equal(2, result);
}
[Fact]
public void Backward_SkipsDisabled()
{
var tp = new TestableTabbedPage();
var page0 = new ContentPage();
var page1 = new ContentPage();
var page2 = new ContentPage();
tp.Pages = new AvaloniaList<Page> { page0, page1, page2 };
TabbedPage.SetIsTabEnabled(page1, false);
int result = tp.CallFindNextEnabledTab(1, -1);
Assert.Equal(0, result);
}
[Fact]
public void NoEnabledTabAhead_ReturnsMinusOne()
{
var tp = new TestableTabbedPage();
var page0 = new ContentPage();
var page1 = new ContentPage();
tp.Pages = new AvaloniaList<Page> { page0, page1 };
TabbedPage.SetIsTabEnabled(page1, false);
int result = tp.CallFindNextEnabledTab(1, 1);
Assert.Equal(-1, result);
}
[Fact]
public void AllEnabled_ReturnsStartIndex()
{
var tp = new TestableTabbedPage();
var page0 = new ContentPage();
var page1 = new ContentPage();
var page2 = new ContentPage();
tp.Pages = new AvaloniaList<Page> { page0, page1, page2 };
int result = tp.CallFindNextEnabledTab(1, 1);
Assert.Equal(1, result);
}
[Fact]
public void MultipleConsecutiveDisabled_SkipsAll()
{
var tp = new TestableTabbedPage();
var page0 = new ContentPage();
var page1 = new ContentPage();
var page2 = new ContentPage();
var page3 = new ContentPage();
tp.Pages = new AvaloniaList<Page> { page0, page1, page2, page3 };
TabbedPage.SetIsTabEnabled(page1, false);
TabbedPage.SetIsTabEnabled(page2, false);
int result = tp.CallFindNextEnabledTab(1, 1);
Assert.Equal(3, result);
}
[Fact]
public void AllDisabled_ReturnsMinusOne()
{
var tp = new TestableTabbedPage();
var page0 = new ContentPage();
var page1 = new ContentPage();
var page2 = new ContentPage();
tp.Pages = new AvaloniaList<Page> { page0, page1, page2 };
TabbedPage.SetIsTabEnabled(page0, false);
TabbedPage.SetIsTabEnabled(page1, false);
TabbedPage.SetIsTabEnabled(page2, false);
Assert.Equal(-1, tp.CallFindNextEnabledTab(0, 1));
Assert.Equal(-1, tp.CallFindNextEnabledTab(2, -1));
}
}
public class SelectingMultiPageTests : ScopedTestBase
{
[Fact]
public void SelectedIndex_DirectProperty_RaisesChangedEvent()
{
var tp = new TestableTabbedPage();
bool raised = false;
tp.GetObservable(SelectingMultiPage.SelectedIndexProperty)
.Subscribe(_ => raised = true);
tp.CallCommitSelection(0, new ContentPage());
Assert.True(raised);
}
[Fact]
public void SelectedPage_DirectProperty_RaisesChangedEvent()
{
var tp = new TestableTabbedPage();
bool raised = false;
tp.GetObservable(SelectingMultiPage.SelectedPageProperty)
.Subscribe(_ => raised = true);
tp.CallCommitSelection(0, new ContentPage());
Assert.True(raised);
}
}
public class CreateIconControlTests : ScopedTestBase
{
[Fact]
public void Geometry_ReturnsPath()
{
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
var result = TabbedPage.CreateIconControl(geometry);
Assert.IsType<Path>(result);
Assert.Same(geometry, ((Path)result!).Data);
}
[Fact]
public void PathIcon_ReturnsPath()
{
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
var pathIcon = new PathIcon { Data = geometry };
var result = TabbedPage.CreateIconControl(pathIcon);
Assert.IsType<Path>(result);
Assert.Same(geometry, ((Path)result!).Data);
}
[Fact]
public void EmptyString_ReturnsNull()
{
var result = TabbedPage.CreateIconControl("");
Assert.Null(result);
}
[Fact]
public void NullString_ReturnsNull()
{
var result = TabbedPage.CreateIconControl((string?)null);
Assert.Null(result);
}
[Fact]
public void Null_ReturnsNull()
{
var result = TabbedPage.CreateIconControl(null);
Assert.Null(result);
}
[Fact]
public void DrawingImage_WithGeometryDrawing_ReturnsPath()
{
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
var drawing = new GeometryDrawing { Geometry = geometry };
var drawingImage = new DrawingImage(drawing);
var result = TabbedPage.CreateIconControl(drawingImage);
Assert.IsType<Path>(result);
Assert.Same(geometry, ((Path)result!).Data);
}
[Fact]
public void Path_HasStretchUniform()
{
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
var result = TabbedPage.CreateIconControl(geometry);
Assert.Equal(Stretch.Uniform, ((Path)result!).Stretch);
}
[Fact]
public void UnsupportedType_ReturnsNull()
{
var result = TabbedPage.CreateIconControl(42);
Assert.Null(result);
}
}
public class DataTemplateTests : ScopedTestBase
{
private record DataItem(string Name);
[Fact]
public void CustomPageTemplate_Build_CreatesContentPage_WithCorrectHeader()
{
var template = new FuncDataTemplate<DataItem>(
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: true);
var tp = new TabbedPage { PageTemplate = template };
var built = tp.PageTemplate!.Build(new DataItem("Electronics")) as ContentPage;
Assert.NotNull(built);
Assert.Equal("Electronics", built!.Header);
}
[Fact]
public void DefaultPageDataTemplate_IsNonNull()
{
Assert.NotNull(new TabbedPage().PageTemplate);
}
[Fact]
public void DefaultPageDataTemplate_WrapsNonPageItem_InContentPage()
{
// Data items must be wrapped in ContentPage, not shown as raw ToString() headers.
var tp = new TabbedPage();
var built = tp.PageTemplate!.Build(new DataItem("Test"));
Assert.IsType<ContentPage>(built);
}
}
private sealed class TestableTabbedPage : TabbedPage
{
public void CallCommitSelection(int index, Page? page) => CommitSelection(index, page);
public int CallFindNextEnabledTab(int start, int dir) => FindNextEnabledTab(start, dir);
protected override void ApplySelectedIndex(int index) => base.ApplySelectedIndex(index);
}
}