csharpc-sharpdotnetxamlavaloniauicross-platformcross-platform-xamlavaloniaguimulti-platformuser-interfacedotnetcore
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.
1476 lines
52 KiB
1476 lines
52 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Linq;
|
|
using Avalonia.Animation;
|
|
using Avalonia.Collections;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Shapes;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.GestureRecognizers;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.LogicalTree;
|
|
using Avalonia.Media;
|
|
using Avalonia.Threading;
|
|
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_SetNull_ClearsCurrentPage()
|
|
{
|
|
var tp = new TestableTabbedPage();
|
|
var page = new ContentPage();
|
|
tp.Pages = new AvaloniaList<Page> { page };
|
|
tp.CallCommitSelection(0, page);
|
|
Assert.NotNull(tp.CurrentPage);
|
|
tp.Pages = null;
|
|
Assert.Null(tp.CurrentPage);
|
|
}
|
|
|
|
[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 KeyboardNavigationTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void IsKeyboardNavigationEnabled_Default_IsTrue()
|
|
{
|
|
var tp = new TabbedPage();
|
|
Assert.True(tp.IsKeyboardNavigationEnabled);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsKeyboardNavigationEnabled_False_RightKey_IsNotHandled()
|
|
{
|
|
var tp = new TestableTabbedPage { IsKeyboardNavigationEnabled = false };
|
|
tp.Pages = new AvaloniaList<Page> { new ContentPage(), new ContentPage() };
|
|
tp.SelectedIndex = 0;
|
|
|
|
bool handled = tp.SimulateKeyDownReturnsHandled(Key.Right);
|
|
|
|
Assert.False(handled);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsKeyboardNavigationEnabled_False_CtrlTab_IsNotHandled()
|
|
{
|
|
var tp = new TestableTabbedPage { IsKeyboardNavigationEnabled = false };
|
|
tp.Pages = new AvaloniaList<Page> { new ContentPage(), new ContentPage() };
|
|
tp.SelectedIndex = 0;
|
|
|
|
bool handled = tp.SimulateKeyDownWithModifiersReturnsHandled(Key.Tab, KeyModifiers.Control);
|
|
|
|
Assert.False(handled);
|
|
}
|
|
|
|
[Fact]
|
|
public void IsKeyboardNavigationEnabled_True_NoTemplate_KeyIsNotHandled()
|
|
{
|
|
var tp = new TestableTabbedPage { IsKeyboardNavigationEnabled = true };
|
|
tp.Pages = new AvaloniaList<Page> { new ContentPage(), new ContentPage() };
|
|
|
|
bool handled = tp.SimulateKeyDownReturnsHandled(Key.Right);
|
|
|
|
Assert.False(handled);
|
|
}
|
|
}
|
|
|
|
public class KeyboardNavigationWithTemplateTests : ScopedTestBase
|
|
{
|
|
// Builds a TabbedPage with a real PART_TabControl wired up so OnKeyDown can navigate.
|
|
private static TestableTabbedPage MakeTabbed(int pageCount, int selectedIndex = 0,
|
|
TabPlacement placement = TabPlacement.Top)
|
|
{
|
|
var tp = new TestableTabbedPage { TabPlacement = placement };
|
|
for (int i = 0; i < pageCount; i++)
|
|
((AvaloniaList<Page>)tp.Pages!).Add(new ContentPage { Header = $"Tab {i}" });
|
|
|
|
tp.Template = new FuncControlTemplate<TabbedPage>((parent, scope) =>
|
|
new TabControl
|
|
{
|
|
Name = "PART_TabControl",
|
|
ItemsSource = parent.Pages,
|
|
}.RegisterInNameScope(scope));
|
|
|
|
_ = new TestRoot { Child = tp };
|
|
tp.ApplyTemplate();
|
|
tp.SelectedIndex = selectedIndex;
|
|
return tp;
|
|
}
|
|
|
|
[Fact]
|
|
public void RightKey_NavigatesToNextPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0);
|
|
tp.SimulateKeyDown(Key.Right);
|
|
Assert.Equal(1, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void LeftKey_NavigatesToPreviousPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 1);
|
|
tp.SimulateKeyDown(Key.Left);
|
|
Assert.Equal(0, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void DownKey_WithVerticalPlacement_NavigatesToNextPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0, placement: TabPlacement.Left);
|
|
tp.SimulateKeyDown(Key.Down);
|
|
Assert.Equal(1, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void UpKey_WithVerticalPlacement_NavigatesToPreviousPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 1, placement: TabPlacement.Left);
|
|
tp.SimulateKeyDown(Key.Up);
|
|
Assert.Equal(0, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RightKey_AtLastPage_DoesNotNavigate()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 2);
|
|
tp.SimulateKeyDown(Key.Right);
|
|
Assert.Equal(2, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void LeftKey_AtFirstPage_DoesNotNavigate()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0);
|
|
tp.SimulateKeyDown(Key.Left);
|
|
Assert.Equal(0, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RightKey_MarksEventHandled()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0);
|
|
bool handled = tp.SimulateKeyDownReturnsHandled(Key.Right);
|
|
Assert.True(handled);
|
|
}
|
|
|
|
[Fact]
|
|
public void RightKey_AtLastPage_DoesNotMarkEventHandled()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 2);
|
|
bool handled = tp.SimulateKeyDownReturnsHandled(Key.Right);
|
|
Assert.False(handled);
|
|
}
|
|
|
|
[Fact]
|
|
public void CtrlTab_NavigatesToNextPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0);
|
|
bool handled = tp.SimulateKeyDownWithModifiersReturnsHandled(Key.Tab, KeyModifiers.Control);
|
|
Assert.Equal(1, tp.SelectedIndex);
|
|
Assert.True(handled);
|
|
}
|
|
|
|
[Fact]
|
|
public void CtrlShiftTab_NavigatesToPreviousPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 1);
|
|
bool handled = tp.SimulateKeyDownWithModifiersReturnsHandled(Key.Tab, KeyModifiers.Control | KeyModifiers.Shift);
|
|
Assert.Equal(0, tp.SelectedIndex);
|
|
Assert.True(handled);
|
|
}
|
|
|
|
[Fact]
|
|
public void RtlFlowDirection_LeftKey_NavigatesToNextPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0);
|
|
tp.FlowDirection = FlowDirection.RightToLeft;
|
|
tp.SimulateKeyDown(Key.Left);
|
|
Assert.Equal(1, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RtlFlowDirection_RightKey_NavigatesToPreviousPage()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 1);
|
|
tp.FlowDirection = FlowDirection.RightToLeft;
|
|
tp.SimulateKeyDown(Key.Right);
|
|
Assert.Equal(0, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RightKey_SkipsDisabledTab()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0);
|
|
TabbedPage.SetIsTabEnabled((Page)((System.Collections.IList)tp.Pages!)[1]!, false);
|
|
tp.SimulateKeyDown(Key.Right);
|
|
Assert.Equal(2, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void RightKey_AllTabsAhead_Disabled_DoesNotNavigate()
|
|
{
|
|
var tp = MakeTabbed(3, selectedIndex: 0);
|
|
TabbedPage.SetIsTabEnabled((Page)((System.Collections.IList)tp.Pages!)[1]!, false);
|
|
TabbedPage.SetIsTabEnabled((Page)((System.Collections.IList)tp.Pages!)[2]!, false);
|
|
tp.SimulateKeyDown(Key.Right);
|
|
Assert.Equal(0, tp.SelectedIndex);
|
|
}
|
|
}
|
|
|
|
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 PageIconTemplateTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Page_Icon_AcceptsControlValue()
|
|
{
|
|
var icon = new PathIcon { Data = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) } };
|
|
var page = new ContentPage { Icon = icon };
|
|
Assert.Same(icon, page.Icon);
|
|
}
|
|
|
|
[Fact]
|
|
public void Page_Icon_AcceptsNonControlValue()
|
|
{
|
|
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
|
|
var page = new ContentPage { Icon = geometry };
|
|
Assert.Same(geometry, page.Icon);
|
|
}
|
|
|
|
[Fact]
|
|
public void Page_IconTemplate_RoundTrips()
|
|
{
|
|
var template = new FuncDataTemplate<object>((_, _) => new Border());
|
|
var page = new ContentPage { IconTemplate = template };
|
|
Assert.Same(template, page.IconTemplate);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawerPage_DrawerIconTemplate_RoundTrips()
|
|
{
|
|
var template = new FuncDataTemplate<object>((_, _) => new Border());
|
|
var dp = new DrawerPage { DrawerIconTemplate = template };
|
|
Assert.Same(template, dp.DrawerIconTemplate);
|
|
}
|
|
|
|
[Fact]
|
|
public void DrawerPage_DrawerIcon_With_Geometry_Does_Not_Throw()
|
|
{
|
|
var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) };
|
|
var dp = new DrawerPage
|
|
{
|
|
DrawerIcon = geometry,
|
|
DrawerIconTemplate = new FuncDataTemplate<object>((_, _) => new PathIcon()),
|
|
};
|
|
var root = new TestRoot { Child = dp };
|
|
|
|
dp.DrawerIcon = new EllipseGeometry { Rect = new Rect(0, 0, 20, 20) };
|
|
Assert.NotNull(dp.DrawerIcon);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewModelItems_WithPageTemplate_BuildContainersAsContentPages()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("Electronics"),
|
|
new("Books"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
var logicals = ((ILogical)tp).LogicalChildren;
|
|
Assert.Contains(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "Electronics");
|
|
Assert.Contains(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "Books");
|
|
}
|
|
|
|
[Fact]
|
|
public void ItemsSource_SelectedPage_IsNotNullAfterContainersRealized()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("First"),
|
|
new("Second"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.NotNull(tp.SelectedPage);
|
|
Assert.Equal(0, tp.SelectedIndex);
|
|
Assert.IsType<ContentPage>(tp.SelectedPage);
|
|
Assert.Equal("First", tp.SelectedPage!.Header?.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void NonListItemsSource_SelectedPage_And_AutomationName_AreResolved()
|
|
{
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = EnumerateItems(new("First"), new("Second")),
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.NotNull(tp.SelectedPage);
|
|
Assert.Equal("First", tp.SelectedPage!.Header?.ToString());
|
|
Assert.Equal("Tab 1 of 2: First", new Avalonia.Automation.Peers.TabbedPageAutomationPeer(tp).GetName());
|
|
|
|
tp.SelectedIndex = 1;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.NotNull(tp.SelectedPage);
|
|
Assert.Equal("Second", tp.SelectedPage!.Header?.ToString());
|
|
Assert.Equal("Tab 2 of 2: Second", new Avalonia.Automation.Peers.TabbedPageAutomationPeer(tp).GetName());
|
|
}
|
|
|
|
[Fact]
|
|
public void ItemsSource_SelectionChanged_ReportsCorrectPage()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("Alpha"),
|
|
new("Beta"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Page? reportedPage = null;
|
|
tp.SelectionChanged += (_, e) => reportedPage = e.CurrentPage;
|
|
|
|
tp.SelectedIndex = 1;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.NotNull(tp.SelectedPage);
|
|
Assert.Equal(1, tp.SelectedIndex);
|
|
Assert.Equal("Beta", tp.SelectedPage!.Header?.ToString());
|
|
Assert.NotNull(reportedPage);
|
|
Assert.Equal("Beta", reportedPage!.Header?.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewModelItems_RemovedFromCollection_TemplateCreatedPageRemovedFromLogicalChildren()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("A"),
|
|
new("B"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
items.RemoveAt(1);
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
var logicals = ((ILogical)tp).LogicalChildren;
|
|
Assert.DoesNotContain(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "B");
|
|
}
|
|
|
|
[Fact]
|
|
public void ItemsSource_Replaced_NoPhantomLogicalChildren()
|
|
{
|
|
var first = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("One"),
|
|
new("Two"),
|
|
};
|
|
var second = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("Three"),
|
|
new("Four"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = first,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
tp.ItemsSource = second;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
var logicals = ((ILogical)tp).LogicalChildren;
|
|
Assert.DoesNotContain(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "One");
|
|
Assert.DoesNotContain(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "Two");
|
|
Assert.Contains(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "Three");
|
|
Assert.Contains(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "Four");
|
|
}
|
|
|
|
[Fact]
|
|
public void PageTemplate_ChangedAfterContainersRealized_RebuildsExistingContainers()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("X"),
|
|
new("Y"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = "old-" + item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
tp.PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = "new-" + item!.Name }, supportsRecycling: false);
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
var logicals = ((ILogical)tp).LogicalChildren;
|
|
Assert.DoesNotContain(logicals, l => l is ContentPage cp && cp.Header?.ToString()?.StartsWith("old-") == true);
|
|
Assert.Contains(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "new-X");
|
|
Assert.Contains(logicals, l => l is ContentPage cp && cp.Header?.ToString() == "new-Y");
|
|
}
|
|
|
|
[Fact]
|
|
public void PageTemplate_ChangedAfterContainersRealized_UpdatesSelectedPage()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("A"),
|
|
new("B"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = "old-" + item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
tp.PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = "new-" + item!.Name }, supportsRecycling: false);
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.NotNull(tp.SelectedPage);
|
|
Assert.Equal("new-A", tp.SelectedPage!.Header?.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public void ReapplyingTemplate_DoesNotLeavePhantomLogicalChildren()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("A"),
|
|
new("B"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
var firstSelectedPage = tp.SelectedPage;
|
|
|
|
tp.Template = CreateTabbedPageTemplate();
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(2, ((ILogical)tp).LogicalChildren.Count);
|
|
Assert.DoesNotContain(firstSelectedPage!, ((ILogical)tp).LogicalChildren);
|
|
}
|
|
|
|
[Fact]
|
|
public void PageTemplate_SetToNullAfterContainersRealized_ClearsGeneratedPages()
|
|
{
|
|
var items = new System.Collections.ObjectModel.ObservableCollection<DataItem>
|
|
{
|
|
new("A"),
|
|
new("B"),
|
|
};
|
|
|
|
var tp = new TabbedPage
|
|
{
|
|
Width = 400, Height = 300,
|
|
ItemsSource = items,
|
|
PageTemplate = new FuncDataTemplate<DataItem>(
|
|
(item, _) => new ContentPage { Header = item!.Name }, supportsRecycling: false),
|
|
Template = CreateTabbedPageTemplate(),
|
|
};
|
|
|
|
var root = new TestRoot { ClientSize = new Size(400, 300), Child = tp };
|
|
root.ExecuteInitialLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
var originalSelectedPage = tp.SelectedPage;
|
|
|
|
tp.PageTemplate = null;
|
|
root.LayoutManager.ExecuteLayoutPass();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Null(tp.SelectedPage);
|
|
Assert.Null(tp.CurrentPage);
|
|
Assert.Empty(((ILogical)tp).LogicalChildren);
|
|
Assert.DoesNotContain(originalSelectedPage!, ((ILogical)tp).LogicalChildren);
|
|
}
|
|
|
|
private static FuncControlTemplate<TabbedPage> CreateTabbedPageTemplate()
|
|
{
|
|
return new FuncControlTemplate<TabbedPage>((parent, scope) =>
|
|
{
|
|
var tc = new TabControl
|
|
{
|
|
Name = "PART_TabControl",
|
|
Template = new FuncControlTemplate<TabControl>((_, tcScope) =>
|
|
new ItemsPresenter
|
|
{
|
|
Name = "PART_ItemsPresenter",
|
|
}.RegisterInNameScope(tcScope)),
|
|
};
|
|
tc.RegisterInNameScope(scope);
|
|
return tc;
|
|
});
|
|
}
|
|
|
|
private static IEnumerable<DataItem> EnumerateItems(params DataItem[] items)
|
|
{
|
|
foreach (var item in items)
|
|
yield return item;
|
|
}
|
|
}
|
|
|
|
public class SwipeGestureTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void SameGestureId_OnlyAdvancesOneTab()
|
|
{
|
|
var tp = CreateSwipeReadyTabbedPage();
|
|
|
|
var firstSwipe = new SwipeGestureEventArgs(7, new Vector(20, 0), default);
|
|
var repeatedSwipe = new SwipeGestureEventArgs(7, new Vector(20, 0), default);
|
|
|
|
tp.RaiseEvent(firstSwipe);
|
|
tp.RaiseEvent(repeatedSwipe);
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.True(firstSwipe.Handled);
|
|
Assert.False(repeatedSwipe.Handled);
|
|
Assert.Equal(1, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void NewGestureId_CanAdvanceAgain()
|
|
{
|
|
var tp = CreateSwipeReadyTabbedPage();
|
|
|
|
tp.RaiseEvent(new SwipeGestureEventArgs(7, new Vector(20, 0), default));
|
|
tp.RaiseEvent(new SwipeGestureEventArgs(8, new Vector(20, 0), default));
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(2, tp.SelectedIndex);
|
|
}
|
|
|
|
[Fact]
|
|
public void MouseSwipe_Advances_Tab()
|
|
{
|
|
var tp = CreateSwipeReadyTabbedPage();
|
|
var mouse = new MouseTestHelper();
|
|
|
|
mouse.Down(tp, position: new Point(200, 100));
|
|
mouse.Move(tp, new Point(160, 100));
|
|
mouse.Up(tp, position: new Point(160, 100));
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(1, tp.SelectedIndex);
|
|
}
|
|
|
|
private static TabbedPage CreateSwipeReadyTabbedPage()
|
|
{
|
|
var tp = new TabbedPage
|
|
{
|
|
IsGestureEnabled = true,
|
|
Width = 400,
|
|
Height = 300,
|
|
TabPlacement = TabPlacement.Top,
|
|
SelectedIndex = 0,
|
|
Pages = new AvaloniaList<Page>
|
|
{
|
|
new ContentPage { Header = "A" },
|
|
new ContentPage { Header = "B" },
|
|
new ContentPage { Header = "C" }
|
|
},
|
|
Template = new FuncControlTemplate<TabbedPage>((parent, scope) =>
|
|
{
|
|
var tabControl = new TabControl
|
|
{
|
|
Name = "PART_TabControl",
|
|
ItemsSource = parent.Pages
|
|
};
|
|
scope.Register("PART_TabControl", tabControl);
|
|
return tabControl;
|
|
})
|
|
};
|
|
tp.GestureRecognizers.OfType<SwipeGestureRecognizer>().First().IsMouseEnabled = true;
|
|
|
|
var root = new TestRoot
|
|
{
|
|
ClientSize = new Size(400, 300),
|
|
Child = tp
|
|
};
|
|
tp.ApplyTemplate();
|
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken);
|
|
|
|
return tp;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
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 bool SimulateKeyDownWithModifiersReturnsHandled(Key key, KeyModifiers modifiers)
|
|
{
|
|
var e = new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = key, KeyModifiers = modifiers };
|
|
OnKeyDown(e);
|
|
return e.Handled;
|
|
}
|
|
}
|
|
|
|
public class VisualTreeLifecycleTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Detach_And_Reattach_CollectionChangedStillFiresPagesChanged()
|
|
{
|
|
var pages = new AvaloniaList<Page>();
|
|
var tp = new TabbedPage { Pages = pages };
|
|
var root = new TestRoot { Child = tp };
|
|
|
|
root.Child = null;
|
|
root.Child = tp;
|
|
|
|
int fireCount = 0;
|
|
tp.PagesChanged += (_, _) => fireCount++;
|
|
|
|
pages.Add(new ContentPage { Header = "A" });
|
|
pages.Add(new ContentPage { Header = "B" });
|
|
|
|
Assert.Equal(2, fireCount);
|
|
}
|
|
}
|
|
}
|
|
|