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.
1741 lines
57 KiB
1741 lines
57 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Animation;
|
|
using Avalonia.Controls.Presenters;
|
|
using Avalonia.Controls.Primitives;
|
|
using Avalonia.Controls.Templates;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.LogicalTree;
|
|
using Avalonia.UnitTests;
|
|
using Xunit;
|
|
|
|
namespace Avalonia.Controls.UnitTests;
|
|
|
|
public class NavigationPageTests
|
|
{
|
|
public class PushTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task Push_SinglePage_StackDepthBecomesOne()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Equal(1, nav.StackDepth);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(3)]
|
|
[InlineData(10)]
|
|
public async Task Push_MultipleTimes_StackDepthMatchesCount(int n)
|
|
{
|
|
var nav = new NavigationPage();
|
|
for (int i = 0; i < n; i++)
|
|
await nav.PushAsync(new ContentPage { Header = $"Page {i}" });
|
|
Assert.Equal(n, nav.StackDepth);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_SetsCurrentPageToTopPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
Assert.Same(top, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_SetsIsInNavigationPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var page = new ContentPage();
|
|
Assert.False(page.IsInNavigationPage);
|
|
await nav.PushAsync(page);
|
|
Assert.True(page.IsInNavigationPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_SetsNavigationProperty()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var page = new ContentPage();
|
|
await nav.PushAsync(page);
|
|
Assert.Same(nav, page.Navigation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_DuplicatePage_Throws()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var page = new ContentPage();
|
|
await nav.PushAsync(page);
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => nav.PushAsync(page));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_FiresPushedEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
NavigationEventArgs? received = null;
|
|
nav.Pushed += (_, e) => received = e;
|
|
|
|
var page = new ContentPage();
|
|
await nav.PushAsync(page);
|
|
|
|
Assert.NotNull(received);
|
|
Assert.Same(page, received.Page);
|
|
Assert.Equal(NavigationType.Push, received.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_InvokesNavigatedTo_OnPushedPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
NavigatedToEventArgs? args = null;
|
|
var second = new ContentPage();
|
|
second.NavigatedTo += (_, e) => args = e;
|
|
|
|
await nav.PushAsync(second);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(root, args.PreviousPage);
|
|
Assert.Equal(NavigationType.Push, args.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_InvokesNavigatedFrom_OnPreviousPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
NavigatedFromEventArgs? args = null;
|
|
root.NavigatedFrom += (_, e) => args = e;
|
|
await nav.PushAsync(root);
|
|
|
|
var second = new ContentPage();
|
|
await nav.PushAsync(second);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(second, args.DestinationPage);
|
|
Assert.Equal(NavigationType.Push, args.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushAsync_WhenNavigatingFromCancels_DoesNotPush()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
root.Navigating += args =>
|
|
{
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(root, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_ReentrantFromNavigatedTo_IsIgnoredNotThrown()
|
|
{
|
|
// Verifies that a re-entrant Push called from inside a NavigatedTo
|
|
// lifecycle callback is silently ignored rather than throwing.
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
var second = new ContentPage();
|
|
second.NavigatedTo += async (_, _) => await nav.PushAsync(new ContentPage());
|
|
|
|
await nav.PushAsync(second); // must not throw
|
|
|
|
Assert.Equal(2, nav.StackDepth);
|
|
Assert.Same(second, nav.CurrentPage);
|
|
}
|
|
}
|
|
|
|
public class PopTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task Pop_OnEmptyStack_ReturnsNull()
|
|
{
|
|
var nav = new NavigationPage();
|
|
Assert.Null(await nav.PopAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Pop_OnRootOnly_ReturnsNull_AndKeepsRoot()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var result = await nav.PopAsync();
|
|
Assert.Null(result);
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(root, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Pop_ReturnsPoppedPage_AndDecrementsStack()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
var result = await nav.PopAsync();
|
|
|
|
Assert.Same(top, result);
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(root, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Pop_ClearsIsInNavigationPage_OnPoppedPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
await nav.PopAsync();
|
|
|
|
Assert.False(top.IsInNavigationPage);
|
|
Assert.Null(top.Navigation);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Pop_FiresPoppedEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
NavigationEventArgs? received = null;
|
|
nav.Popped += (_, e) => received = e;
|
|
|
|
var root = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
await nav.PopAsync();
|
|
|
|
Assert.NotNull(received);
|
|
Assert.Same(top, received.Page);
|
|
Assert.Equal(NavigationType.Pop, received.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Pop_InvokesNavigatedTo_OnRevealedPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
NavigatedToEventArgs? args = null;
|
|
root.NavigatedTo += (_, e) => args = e;
|
|
await nav.PushAsync(root);
|
|
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
await nav.PopAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(top, args.PreviousPage);
|
|
Assert.Equal(NavigationType.Pop, args.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAsync_WhenNavigatingFromCancels_DoesNotPop()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
top.Navigating += args =>
|
|
{
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
await nav.PopAsync();
|
|
|
|
Assert.Equal(2, nav.StackDepth);
|
|
Assert.Same(top, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAsync_InvokesNavigatedTo_OnRevealedPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
bool navigatedTo = false;
|
|
root.NavigatedTo += (_, _) => navigatedTo = true;
|
|
|
|
await nav.PopAsync();
|
|
|
|
Assert.True(navigatedTo);
|
|
}
|
|
}
|
|
|
|
public class NavigationStackTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task NavigationStack_RootAtIndexZero_TopAtLastIndex()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var middle = new ContentPage { Header = "Middle" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(middle);
|
|
await nav.PushAsync(top);
|
|
|
|
var stack = nav.NavigationStack;
|
|
Assert.Equal(3, stack.Count);
|
|
Assert.Same(root, stack[0]);
|
|
Assert.Same(middle, stack[1]);
|
|
Assert.Same(top, stack[2]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanGoBack_FalseWithOneEntry()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.False(nav.CanGoBack);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanGoBack_TrueWithTwoEntries()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.True(nav.CanGoBack);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CanGoBack_FalseAfterPop()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PopAsync();
|
|
Assert.False(nav.CanGoBack);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StackDepth_AlwaysEqualsNavigationStack_Count()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
Assert.Equal(nav.NavigationStack.Count, nav.StackDepth);
|
|
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Equal(nav.NavigationStack.Count, nav.StackDepth);
|
|
|
|
await nav.PopAsync();
|
|
Assert.Equal(nav.NavigationStack.Count, nav.StackDepth);
|
|
}
|
|
}
|
|
|
|
public class BackButtonVisibilityTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task BackButtonVisible_FalseWhenStackDepthIsOne()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Equal(false, nav.IsBackButtonEffectivelyVisible);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BackButtonVisible_TrueWhenStackDepthIsTwo()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Equal(true, nav.IsBackButtonEffectivelyVisible);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BackButtonVisible_FalseWhenIsBackButtonVisibleIsFalse()
|
|
{
|
|
var nav = new NavigationPage { IsBackButtonVisible = false };
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Equal(false, nav.IsBackButtonEffectivelyVisible);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BackButtonVisible_FalseWhenPerPageIsBackButtonVisibleIsFalse()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
var top = new ContentPage();
|
|
NavigationPage.SetHasBackButton(top, false);
|
|
await nav.PushAsync(top);
|
|
Assert.Equal(false, nav.IsBackButtonEffectivelyVisible);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BackButtonVisible_TrueAfterRestoringGlobalVisibility()
|
|
{
|
|
var nav = new NavigationPage { IsBackButtonVisible = false };
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
nav.IsBackButtonVisible = true;
|
|
Assert.Equal(true, nav.IsBackButtonEffectivelyVisible);
|
|
}
|
|
}
|
|
|
|
public class PopToRootTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PopToRoot_LeavesOnlyFirstPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(root, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRoot_FiresPoppedToRootEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
bool fired = false;
|
|
nav.PoppedToRoot += (_, _) => fired = true;
|
|
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.True(fired);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRoot_InvokesNavigatedFrom_OnCurrentPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
NavigatedFromEventArgs? args = null;
|
|
top.NavigatedFrom += (_, e) => args = e;
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Equal(NavigationType.PopToRoot, args!.NavigationType);
|
|
Assert.Same(root, args!.DestinationPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRoot_InvokesNavigatedTo_OnRootPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
NavigatedToEventArgs? args = null;
|
|
root.NavigatedTo += (_, e) => args = e;
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Equal(NavigationType.PopToRoot, args!.NavigationType);
|
|
Assert.Same(top, args!.PreviousPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRoot_NavigatedFrom_FiresAfterStateChange()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
int stackDepthAtEvent = -1;
|
|
top.NavigatedFrom += (_, _) => stackDepthAtEvent = nav.StackDepth;
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.Equal(1, stackDepthAtEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRoot_WhenAlreadyAtRoot_DoesNothing()
|
|
{
|
|
var nav = new NavigationPage();
|
|
bool fired = false;
|
|
nav.PoppedToRoot += (_, _) => fired = true;
|
|
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.False(fired);
|
|
}
|
|
}
|
|
|
|
public class InsertRemoveTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task InsertPage_AddsPageBeforeTarget()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
var middle = new ContentPage { Header = "Middle" };
|
|
nav.InsertPage(middle, top);
|
|
|
|
var stack = nav.NavigationStack;
|
|
Assert.Equal(3, stack.Count);
|
|
Assert.Same(root, stack[0]);
|
|
Assert.Same(middle, stack[1]);
|
|
Assert.Same(top, stack[2]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemovePage_RemovesFromMiddleOfStack()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var middle = new ContentPage { Header = "Middle" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(middle);
|
|
await nav.PushAsync(top);
|
|
|
|
nav.RemovePage(middle);
|
|
|
|
var stack = nav.NavigationStack;
|
|
Assert.Equal(2, stack.Count);
|
|
Assert.Same(root, stack[0]);
|
|
Assert.Same(top, stack[1]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertPage_FiresPageInsertedEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
PageInsertedEventArgs? args = null;
|
|
nav.PageInserted += (_, e) => args = e;
|
|
|
|
var inserted = new ContentPage();
|
|
nav.InsertPage(inserted, top);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(inserted, args.Page);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemovePage_FiresPageRemovedEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var mid = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(mid);
|
|
await nav.PushAsync(top);
|
|
|
|
PageRemovedEventArgs? args = null;
|
|
nav.PageRemoved += (_, e) => args = e;
|
|
|
|
nav.RemovePage(mid);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(mid, args.Page);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertPage_NullPage_ThrowsArgumentNullException()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var before = new ContentPage();
|
|
await nav.PushAsync(before);
|
|
Assert.Throws<ArgumentNullException>(() => nav.InsertPage(null!, before));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertPage_NullBefore_ThrowsArgumentNullException()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Throws<ArgumentNullException>(() => nav.InsertPage(new ContentPage(), null!));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemovePage_NullPage_ThrowsArgumentNullException()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Throws<ArgumentNullException>(() => nav.RemovePage(null!));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertPage_DoesNotFireNavigatedTo_OnInsertedPage()
|
|
{
|
|
// InsertPage should not invoke NavigatedTo on a non-current (background) page.
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
bool navigatedToFired = false;
|
|
var inserted = new ContentPage();
|
|
inserted.NavigatedTo += (_, _) => navigatedToFired = true;
|
|
|
|
nav.InsertPage(inserted, top);
|
|
|
|
Assert.False(navigatedToFired);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task InsertPage_DuplicatePage_ThrowsInvalidOperationException()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
Assert.Throws<InvalidOperationException>(() => nav.InsertPage(root, top));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RemovePage_PageNotInStack_IsNoOp()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
var stranger = new ContentPage();
|
|
// Should not throw and should not change stack depth
|
|
nav.RemovePage(stranger);
|
|
Assert.Equal(1, nav.StackDepth);
|
|
}
|
|
}
|
|
|
|
public class ModalTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PushModal_AddsToModalStack()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
var modal = new ContentPage { Header = "Modal" };
|
|
await nav.PushModalAsync(modal);
|
|
|
|
Assert.Equal(1, nav.ModalStack.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopModal_RemovesFromModalStack()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
var modal = new ContentPage();
|
|
await nav.PushModalAsync(modal);
|
|
await nav.PopModalAsync();
|
|
|
|
Assert.Equal(0, nav.ModalStack.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ModalStack_IsOrderedBottomToTop()
|
|
{
|
|
// Index 0 = oldest (bottom-most); last index = topmost, consistent with NavigationStack.
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
var m1 = new ContentPage { Header = "M1" };
|
|
var m2 = new ContentPage { Header = "M2" };
|
|
var m3 = new ContentPage { Header = "M3" };
|
|
await nav.PushModalAsync(m1);
|
|
await nav.PushModalAsync(m2);
|
|
await nav.PushModalAsync(m3);
|
|
|
|
Assert.Equal(new[] { m1, m2, m3 }, nav.ModalStack);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushModal_FiresModalPushedEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
ModalPushedEventArgs? args = null;
|
|
nav.ModalPushed += (_, e) => args = e;
|
|
|
|
var modal = new ContentPage();
|
|
await nav.PushModalAsync(modal);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(modal, args.Modal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopModal_FiresModalPoppedEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
var modal = new ContentPage();
|
|
await nav.PushModalAsync(modal);
|
|
|
|
ModalPoppedEventArgs? args = null;
|
|
nav.ModalPopped += (_, e) => args = e;
|
|
|
|
await nav.PopModalAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(modal, args.Modal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopModal_OnEmptyStack_ReturnsNull()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
var result = await nav.PopModalAsync();
|
|
Assert.Null(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushModal_InvokesNavigatedFrom_OnCoveredPage_WithPushModalType()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
|
|
NavigatedFromEventArgs? args = null;
|
|
root.NavigatedFrom += (_, e) => args = e;
|
|
|
|
await nav.PushModalAsync(new ContentPage { Header = "Modal" });
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Equal(NavigationType.PushModal, args!.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushModal_InvokesNavigatedTo_OnModalPage_WithPushModalType()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
var modal = new ContentPage { Header = "Modal" };
|
|
NavigatedToEventArgs? args = null;
|
|
modal.NavigatedTo += (_, e) => args = e;
|
|
|
|
await nav.PushModalAsync(modal);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Equal(NavigationType.PushModal, args!.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopModal_InvokesNavigatedFrom_OnPoppedModal_WithPopModalType()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
var modal = new ContentPage { Header = "Modal" };
|
|
await nav.PushModalAsync(modal);
|
|
|
|
NavigatedFromEventArgs? args = null;
|
|
modal.NavigatedFrom += (_, e) => args = e;
|
|
|
|
await nav.PopModalAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Equal(NavigationType.PopModal, args!.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopModal_InvokesNavigatedTo_OnRevealedPage_WithPopModalType()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushModalAsync(new ContentPage { Header = "Modal" });
|
|
|
|
NavigatedToEventArgs? args = null;
|
|
root.NavigatedTo += (_, e) => args = e;
|
|
|
|
await nav.PopModalAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Equal(NavigationType.PopModal, args!.NavigationType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushModal_NavigatedFrom_DestinationPage_IsTheModal()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
|
|
var modal = new ContentPage { Header = "Modal" };
|
|
NavigatedFromEventArgs? args = null;
|
|
root.NavigatedFrom += (_, e) => args = e;
|
|
|
|
await nav.PushModalAsync(modal);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(modal, args!.DestinationPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushModal_NavigatedTo_PreviousPage_IsCoveredPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
|
|
var modal = new ContentPage { Header = "Modal" };
|
|
NavigatedToEventArgs? args = null;
|
|
modal.NavigatedTo += (_, e) => args = e;
|
|
|
|
await nav.PushModalAsync(modal);
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(root, args!.PreviousPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopModal_NavigatedFrom_DestinationPage_IsRevealedPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var modal = new ContentPage { Header = "Modal" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushModalAsync(modal);
|
|
|
|
NavigatedFromEventArgs? args = null;
|
|
modal.NavigatedFrom += (_, e) => args = e;
|
|
|
|
await nav.PopModalAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(root, args!.DestinationPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopModal_NavigatedTo_PreviousPage_IsPoppedModal()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var modal = new ContentPage { Header = "Modal" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushModalAsync(modal);
|
|
|
|
NavigatedToEventArgs? args = null;
|
|
root.NavigatedTo += (_, e) => args = e;
|
|
|
|
await nav.PopModalAsync();
|
|
|
|
Assert.NotNull(args);
|
|
Assert.Same(modal, args!.PreviousPage);
|
|
}
|
|
}
|
|
|
|
public class AttachedPropertyTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task EffectiveBarHeight_UsesPageOverride()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var page = new ContentPage();
|
|
NavigationPage.SetBarHeightOverride(page, 60.0);
|
|
await nav.PushAsync(page);
|
|
Assert.Equal(60.0, nav.EffectiveBarHeight);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EffectiveBarHeight_FallsBackToGlobalBarHeight()
|
|
{
|
|
var nav = new NavigationPage { BarHeight = 56.0 };
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.Equal(56.0, nav.EffectiveBarHeight);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BarLayoutBehavior_DefaultAppliesNavBarInsetPseudoClass()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
Assert.True(nav.Classes.Contains(":nav-bar-inset"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BarLayoutBehavior_OverlayRemovesNavBarInsetPseudoClass()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var page = new ContentPage();
|
|
NavigationPage.SetBarLayoutBehavior(page, BarLayoutBehavior.Overlay);
|
|
await nav.PushAsync(page);
|
|
Assert.False(nav.Classes.Contains(":nav-bar-inset"));
|
|
}
|
|
}
|
|
|
|
public class InitialContentTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public void Content_SetBeforePush_IsUsedAsInitialPage()
|
|
{
|
|
var page = new ContentPage { Header = "Initial" };
|
|
var nav = new NavigationPage { Content = page };
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(page, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Content_SetAfterPush_IsIgnored()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var first = new ContentPage { Header = "First" };
|
|
await nav.PushAsync(first);
|
|
|
|
// Setting Content when stack is already populated should not push again
|
|
nav.Content = new ContentPage { Header = "Second" };
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(first, nav.CurrentPage);
|
|
}
|
|
}
|
|
|
|
public class TransitionCancellationTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PushAsync_WithTransition_WhenCancelled_StackUnchangedAfterCancel()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
// Cancel the first navigation
|
|
bool shouldCancel = true;
|
|
root.Navigating += args =>
|
|
{
|
|
if (shouldCancel)
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
var customTransition = new CrossFade(TimeSpan.FromMilliseconds(100));
|
|
await nav.PushAsync(new ContentPage(), customTransition);
|
|
|
|
Assert.Equal(1, nav.StackDepth);
|
|
|
|
// Stop cancelling and push again: override should not leak
|
|
shouldCancel = false;
|
|
var second = new ContentPage();
|
|
await nav.PushAsync(second);
|
|
Assert.Equal(2, nav.StackDepth);
|
|
Assert.Same(second, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAsync_WithTransition_WhenCancelled_StackUnchangedAfterCancel()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
top.Navigating += args =>
|
|
{
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
await nav.PopAsync(new CrossFade(TimeSpan.FromMilliseconds(100)));
|
|
|
|
Assert.Equal(2, nav.StackDepth);
|
|
Assert.Same(top, nav.CurrentPage);
|
|
}
|
|
}
|
|
|
|
public class NavigatingEventTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PopToRootAsync_AwaitsAsyncNavigatingHandler()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
bool handlerInvoked = false;
|
|
top.Navigating += args =>
|
|
{
|
|
handlerInvoked = true;
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.True(handlerInvoked);
|
|
Assert.Equal(2, nav.StackDepth);
|
|
Assert.Same(top, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToPageAsync_AwaitsAsyncNavigatingHandler()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var middle = new ContentPage();
|
|
await nav.PushAsync(middle);
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
bool handlerInvoked = false;
|
|
top.Navigating += args =>
|
|
{
|
|
handlerInvoked = true;
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
await nav.PopToPageAsync(root);
|
|
|
|
Assert.True(handlerInvoked);
|
|
Assert.Equal(3, nav.StackDepth);
|
|
Assert.Same(top, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Push_Sync_InvokesNavigatingEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
bool handlerInvoked = false;
|
|
root.Navigating += args =>
|
|
{
|
|
handlerInvoked = true;
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
Assert.True(handlerInvoked);
|
|
Assert.Equal(1, nav.StackDepth);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Pop_Sync_InvokesNavigatingEvent()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
bool handlerInvoked = false;
|
|
top.Navigating += args =>
|
|
{
|
|
handlerInvoked = true;
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
await nav.PopAsync();
|
|
|
|
Assert.True(handlerInvoked);
|
|
Assert.Equal(2, nav.StackDepth);
|
|
}
|
|
}
|
|
|
|
public class LogicalChildrenTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task Pop_RemovesPageFromLogicalChildren()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
await nav.PopAsync();
|
|
|
|
Assert.DoesNotContain(top, nav.GetLogicalChildren());
|
|
Assert.Contains(root, nav.GetLogicalChildren());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRootAsync_RemovesIntermediatePagesFromLogicalChildren()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
var middle = new ContentPage();
|
|
await nav.PushAsync(middle);
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(top);
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.Contains(root, nav.GetLogicalChildren());
|
|
Assert.DoesNotContain(middle, nav.GetLogicalChildren());
|
|
Assert.DoesNotContain(top, nav.GetLogicalChildren());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReplaceAsync_SwapsLogicalChildren()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var original = new ContentPage();
|
|
await nav.PushAsync(original);
|
|
|
|
var replacement = new ContentPage();
|
|
await nav.ReplaceAsync(replacement);
|
|
|
|
Assert.DoesNotContain(original, nav.GetLogicalChildren());
|
|
Assert.Contains(replacement, nav.GetLogicalChildren());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PushAsync_AddsPageToLogicalChildren()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
var second = new ContentPage();
|
|
await nav.PushAsync(second);
|
|
|
|
var children = new List<ILogical>(nav.GetLogicalChildren());
|
|
Assert.Contains(root, children);
|
|
Assert.Contains(second, children);
|
|
}
|
|
}
|
|
|
|
public class PopAllModalsTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PopAllModals_EmptyStack_DoesNothing()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
await nav.PopAllModalsAsync(); // must not throw
|
|
|
|
Assert.Equal(0, nav.ModalStack.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_SingleModal_ClearsModalStack()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushModalAsync(new ContentPage());
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
Assert.Equal(0, nav.ModalStack.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_MultipleModals_ClearsEntireStack()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
await nav.PushModalAsync(new ContentPage { Header = "M1" });
|
|
await nav.PushModalAsync(new ContentPage { Header = "M2" });
|
|
await nav.PushModalAsync(new ContentPage { Header = "M3" });
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
Assert.Equal(0, nav.ModalStack.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_FiresModalPopped_ForEachModal()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
var m1 = new ContentPage { Header = "M1" };
|
|
var m2 = new ContentPage { Header = "M2" };
|
|
var m3 = new ContentPage { Header = "M3" };
|
|
await nav.PushModalAsync(m1);
|
|
await nav.PushModalAsync(m2);
|
|
await nav.PushModalAsync(m3);
|
|
|
|
var popped = new List<Page>();
|
|
nav.ModalPopped += (_, e) => popped.Add(e.Modal);
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
Assert.Equal(3, popped.Count);
|
|
// LIFO order: m3 was pushed last so must be popped first.
|
|
Assert.Equal(new[] { m3, m2, m1 }, popped);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_FiresNavigatedFrom_OnAllModals()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
var m1 = new ContentPage { Header = "M1" };
|
|
var m2 = new ContentPage { Header = "M2" };
|
|
await nav.PushModalAsync(m1);
|
|
await nav.PushModalAsync(m2);
|
|
|
|
var navigatedFrom = new List<string>();
|
|
m1.NavigatedFrom += (_, _) => navigatedFrom.Add("M1");
|
|
m2.NavigatedFrom += (_, _) => navigatedFrom.Add("M2");
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
// LIFO order: M2 was pushed last, so it navigates from first.
|
|
Assert.Equal(new[] { "M2", "M1" }, navigatedFrom);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_FiresNavigatedTo_OnUnderlyingPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushModalAsync(new ContentPage { Header = "M1" });
|
|
await nav.PushModalAsync(new ContentPage { Header = "M2" });
|
|
|
|
bool navigatedTo = false;
|
|
root.NavigatedTo += (_, _) => navigatedTo = true;
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
Assert.True(navigatedTo);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_NavigatedFrom_NavigationTypeIsPopModal()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
var m1 = new ContentPage();
|
|
var m2 = new ContentPage();
|
|
await nav.PushModalAsync(m1);
|
|
await nav.PushModalAsync(m2);
|
|
|
|
NavigationType? m1Type = null;
|
|
NavigationType? m2Type = null;
|
|
m1.NavigatedFrom += (_, e) => m1Type = e.NavigationType;
|
|
m2.NavigatedFrom += (_, e) => m2Type = e.NavigationType;
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
Assert.Equal(NavigationType.PopModal, m1Type);
|
|
Assert.Equal(NavigationType.PopModal, m2Type);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_NavigatedTo_NavigationTypeIsPopModal()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushModalAsync(new ContentPage());
|
|
|
|
NavigationType? type = null;
|
|
root.NavigatedTo += (_, e) => type = e.NavigationType;
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
Assert.Equal(NavigationType.PopModal, type);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModals_NavigatedTo_PreviousPageIsTopModal()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var m1 = new ContentPage();
|
|
var m2 = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushModalAsync(m1);
|
|
await nav.PushModalAsync(m2);
|
|
|
|
Page? previousPage = null;
|
|
root.NavigatedTo += (_, e) => previousPage = e.PreviousPage;
|
|
|
|
await nav.PopAllModalsAsync();
|
|
|
|
Assert.Same(m2, previousPage);
|
|
}
|
|
}
|
|
|
|
public class ReplaceTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task ReplaceAsync_NullPage_Throws()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
await Assert.ThrowsAsync<ArgumentNullException>(() => nav.ReplaceAsync(null!));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReplaceAsync_OnEmptyStack_PushesPage()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var page = new ContentPage { Header = "Replaced" };
|
|
|
|
await nav.ReplaceAsync(page);
|
|
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(page, nav.CurrentPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReplaceAsync_ReplacesTopPage_StackDepthUnchanged()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
|
|
var replacement = new ContentPage { Header = "Replacement" };
|
|
await nav.ReplaceAsync(replacement);
|
|
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(replacement, nav.CurrentPage);
|
|
Assert.DoesNotContain(root, nav.NavigationStack);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReplaceAsync_FiresLifecycleEventsInOrder()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var original = new ContentPage { Header = "Original" };
|
|
await nav.PushAsync(original);
|
|
|
|
var replacement = new ContentPage { Header = "Replacement" };
|
|
var order = new List<string>();
|
|
original.NavigatedFrom += (_, _) => order.Add("Original: NavigatedFrom");
|
|
replacement.NavigatedTo += (_, _) => order.Add("Replacement: NavigatedTo");
|
|
|
|
await nav.ReplaceAsync(replacement);
|
|
|
|
Assert.Equal(new[]
|
|
{
|
|
"Original: NavigatedFrom",
|
|
"Replacement: NavigatedTo",
|
|
}, order);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReplaceAsync_NavigationType_IsReplace()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var original = new ContentPage { Header = "Original" };
|
|
await nav.PushAsync(original);
|
|
|
|
var replacement = new ContentPage { Header = "Replacement" };
|
|
NavigationType? arrivedType = null;
|
|
NavigationType? departedType = null;
|
|
replacement.NavigatedTo += (_, e) => arrivedType = e.NavigationType;
|
|
original.NavigatedFrom += (_, e) => departedType = e.NavigationType;
|
|
|
|
await nav.ReplaceAsync(replacement);
|
|
|
|
Assert.Equal(NavigationType.Replace, arrivedType);
|
|
Assert.Equal(NavigationType.Replace, departedType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ReplaceAsync_WhenCancelled_DoesNotReplace()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var original = new ContentPage { Header = "Original" };
|
|
await nav.PushAsync(original);
|
|
|
|
original.Navigating += args =>
|
|
{
|
|
args.Cancel = true;
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
var replacement = new ContentPage { Header = "Replacement" };
|
|
await nav.ReplaceAsync(replacement);
|
|
|
|
Assert.Equal(1, nav.StackDepth);
|
|
Assert.Same(original, nav.CurrentPage);
|
|
}
|
|
}
|
|
|
|
public class PopToRootLifecycleTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PopToRootAsync_PreviousPage_IsNotNull()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var second = new ContentPage { Header = "Second" };
|
|
var third = new ContentPage { Header = "Third" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(second);
|
|
await nav.PushAsync(third);
|
|
|
|
Page? receivedPreviousPage = null;
|
|
root.NavigatedTo += (_, args) => receivedPreviousPage = args.PreviousPage;
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.Same(third, receivedPreviousPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRootAsync_NavigationType_IsPopToRoot()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
NavigationType? receivedType = null;
|
|
root.NavigatedTo += (_, args) => receivedType = args.NavigationType;
|
|
|
|
await nav.PopToRootAsync();
|
|
|
|
Assert.Equal(NavigationType.PopToRoot, receivedType);
|
|
}
|
|
}
|
|
|
|
public class PopToPageLifecycleTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PopToPageAsync_PreviousPage_IsNotNull()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var target = new ContentPage { Header = "Target" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(target);
|
|
await nav.PushAsync(top);
|
|
|
|
Page? receivedPreviousPage = null;
|
|
target.NavigatedTo += (_, args) => receivedPreviousPage = args.PreviousPage;
|
|
|
|
await nav.PopToPageAsync(target);
|
|
|
|
Assert.Same(top, receivedPreviousPage);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToPageAsync_NavigationType_IsPop()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage { Header = "Root" };
|
|
var target = new ContentPage { Header = "Target" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(target);
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
NavigationType? receivedType = null;
|
|
target.NavigatedTo += (_, args) => receivedType = args.NavigationType;
|
|
|
|
await nav.PopToPageAsync(target);
|
|
|
|
Assert.Equal(NavigationType.Pop, receivedType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToPageAsync_IntermediatePages_NavigationTypeIsPop()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
var target = new ContentPage();
|
|
var middle = new ContentPage();
|
|
var top = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(target);
|
|
await nav.PushAsync(middle);
|
|
await nav.PushAsync(top);
|
|
|
|
NavigationType? middleType = null;
|
|
NavigationType? topType = null;
|
|
middle.NavigatedFrom += (_, e) => middleType = e.NavigationType;
|
|
top.NavigatedFrom += (_, e) => topType = e.NavigationType;
|
|
|
|
await nav.PopToPageAsync(target);
|
|
|
|
Assert.Equal(NavigationType.Pop, topType);
|
|
Assert.Equal(NavigationType.Pop, middleType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToPageAsync_PageNotInStack_ThrowsArgumentException()
|
|
{
|
|
var nav = new NavigationPage();
|
|
await nav.PushAsync(new ContentPage());
|
|
|
|
var stranger = new ContentPage();
|
|
await Assert.ThrowsAsync<ArgumentException>(() => nav.PopToPageAsync(stranger));
|
|
}
|
|
}
|
|
|
|
public class ModalTransitionCancellationTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PushModalAsync_CancelledTransition_StillFiresLifecycleEvents()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
var modal = new ContentPage();
|
|
bool navigatedFromFired = false;
|
|
bool navigatedToFired = false;
|
|
bool modalPushedFired = false;
|
|
root.NavigatedFrom += (_, _) => navigatedFromFired = true;
|
|
modal.NavigatedTo += (_, _) => navigatedToFired = true;
|
|
nav.ModalPushed += (_, _) => modalPushedFired = true;
|
|
|
|
await nav.PushModalAsync(modal, null);
|
|
|
|
Assert.True(navigatedFromFired);
|
|
Assert.True(navigatedToFired);
|
|
Assert.True(modalPushedFired);
|
|
Assert.Equal(1, nav.ModalStack.Count);
|
|
Assert.Same(modal, nav.ModalStack[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAllModalsAsync_CancelledTransition_StillClearsStackAndFiresEvents()
|
|
{
|
|
var nav = new NavigationPage();
|
|
var root = new ContentPage();
|
|
await nav.PushAsync(root);
|
|
|
|
var m1 = new ContentPage();
|
|
var m2 = new ContentPage();
|
|
await nav.PushModalAsync(m1);
|
|
await nav.PushModalAsync(m2);
|
|
|
|
bool m1PoppedFired = false;
|
|
bool m2PoppedFired = false;
|
|
bool rootNavigatedToFired = false;
|
|
nav.ModalPopped += (_, e) =>
|
|
{
|
|
if (ReferenceEquals(e.Modal, m1)) m1PoppedFired = true;
|
|
if (ReferenceEquals(e.Modal, m2)) m2PoppedFired = true;
|
|
};
|
|
root.NavigatedTo += (_, _) => rootNavigatedToFired = true;
|
|
|
|
await nav.PopAllModalsAsync(null);
|
|
|
|
Assert.Equal(0, nav.ModalStack.Count);
|
|
Assert.True(m1PoppedFired);
|
|
Assert.True(m2PoppedFired);
|
|
Assert.True(rootNavigatedToFired);
|
|
}
|
|
}
|
|
|
|
public class LifecycleAfterTransitionTests : ScopedTestBase
|
|
{
|
|
[Fact]
|
|
public async Task PushAsync_LifecycleEvents_FireAfterTransition()
|
|
{
|
|
var tcs = new TaskCompletionSource();
|
|
var transition = new ControllableTransition(tcs.Task);
|
|
var nav = CreateNavigationPage(transition);
|
|
|
|
var root = new ContentPage { Header = "Root" };
|
|
await nav.PushAsync(root);
|
|
|
|
bool navigatedFromDuringTransition = false;
|
|
bool navigatedToDuringTransition = false;
|
|
bool pushedDuringTransition = false;
|
|
|
|
var second = new ContentPage { Header = "Second" };
|
|
root.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted;
|
|
second.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted;
|
|
nav.Pushed += (_, _) => pushedDuringTransition = !tcs.Task.IsCompleted;
|
|
|
|
var pushTask = nav.PushAsync(second);
|
|
|
|
tcs.SetResult();
|
|
await pushTask;
|
|
|
|
Assert.False(navigatedFromDuringTransition);
|
|
Assert.False(navigatedToDuringTransition);
|
|
Assert.False(pushedDuringTransition);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopAsync_LifecycleEvents_FireAfterTransition()
|
|
{
|
|
var tcs = new TaskCompletionSource();
|
|
var nav = CreateNavigationPage(null);
|
|
|
|
var root = new ContentPage { Header = "Root" };
|
|
var top = new ContentPage { Header = "Top" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(top);
|
|
|
|
nav.PageTransition = new ControllableTransition(tcs.Task);
|
|
|
|
bool navigatedFromDuringTransition = false;
|
|
bool navigatedToDuringTransition = false;
|
|
bool poppedDuringTransition = false;
|
|
|
|
top.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted;
|
|
root.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted;
|
|
nav.Popped += (_, _) => poppedDuringTransition = !tcs.Task.IsCompleted;
|
|
|
|
var popTask = nav.PopAsync();
|
|
|
|
tcs.SetResult();
|
|
await popTask;
|
|
|
|
Assert.False(navigatedFromDuringTransition);
|
|
Assert.False(navigatedToDuringTransition);
|
|
Assert.False(poppedDuringTransition);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PopToRootAsync_LifecycleEvents_FireAfterTransition()
|
|
{
|
|
var tcs = new TaskCompletionSource();
|
|
var nav = CreateNavigationPage(null);
|
|
|
|
var root = new ContentPage { Header = "Root" };
|
|
var second = new ContentPage { Header = "Second" };
|
|
var third = new ContentPage { Header = "Third" };
|
|
await nav.PushAsync(root);
|
|
await nav.PushAsync(second);
|
|
await nav.PushAsync(third);
|
|
|
|
nav.PageTransition = new ControllableTransition(tcs.Task);
|
|
|
|
bool navigatedFromDuringTransition = false;
|
|
bool navigatedToDuringTransition = false;
|
|
bool poppedToRootDuringTransition = false;
|
|
|
|
second.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted;
|
|
third.NavigatedFrom += (_, _) => navigatedFromDuringTransition = !tcs.Task.IsCompleted;
|
|
root.NavigatedTo += (_, _) => navigatedToDuringTransition = !tcs.Task.IsCompleted;
|
|
nav.PoppedToRoot += (_, _) => poppedToRootDuringTransition = !tcs.Task.IsCompleted;
|
|
|
|
var popTask = nav.PopToRootAsync();
|
|
|
|
tcs.SetResult();
|
|
await popTask;
|
|
|
|
Assert.False(navigatedFromDuringTransition);
|
|
Assert.False(navigatedToDuringTransition);
|
|
Assert.False(poppedToRootDuringTransition);
|
|
}
|
|
|
|
private static NavigationPage CreateNavigationPage(IPageTransition? transition)
|
|
{
|
|
var nav = new NavigationPage
|
|
{
|
|
PageTransition = transition,
|
|
Template = NavigationPageTemplate()
|
|
};
|
|
var root = new TestRoot { Child = nav };
|
|
root.LayoutManager.ExecuteInitialLayoutPass();
|
|
return nav;
|
|
}
|
|
|
|
private static IControlTemplate NavigationPageTemplate()
|
|
{
|
|
return new FuncControlTemplate<NavigationPage>((parent, ns) =>
|
|
{
|
|
var contentHost = new Panel
|
|
{
|
|
Name = "PART_ContentHost",
|
|
Children =
|
|
{
|
|
new ContentPresenter { Name = "PART_PageBackPresenter" }.RegisterInNameScope(ns),
|
|
new ContentPresenter { Name = "PART_PagePresenter" }.RegisterInNameScope(ns),
|
|
}
|
|
}.RegisterInNameScope(ns);
|
|
|
|
return new Panel
|
|
{
|
|
Children =
|
|
{
|
|
new Border
|
|
{
|
|
Name = "PART_NavigationBar",
|
|
Child = new Button { Name = "PART_BackButton" }.RegisterInNameScope(ns)
|
|
}.RegisterInNameScope(ns),
|
|
contentHost,
|
|
new ContentPresenter { Name = "PART_TopCommandBar" }.RegisterInNameScope(ns),
|
|
new ContentPresenter { Name = "PART_ModalBackPresenter" }.RegisterInNameScope(ns),
|
|
new ContentPresenter { Name = "PART_ModalPresenter" }.RegisterInNameScope(ns),
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
private class ControllableTransition : IPageTransition
|
|
{
|
|
private readonly Task _gate;
|
|
|
|
public ControllableTransition(Task gate)
|
|
{
|
|
_gate = gate;
|
|
}
|
|
|
|
public async Task Start(Visual? from, Visual? to, bool forward, CancellationToken cancellationToken)
|
|
{
|
|
if (to != null)
|
|
to.IsVisible = true;
|
|
await _gate;
|
|
if (from != null)
|
|
from.IsVisible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|