diff --git a/Bearded.UI.Tests/Bearded.UI.Tests.csproj b/Bearded.UI.Tests/Bearded.UI.Tests.csproj index ba85a64..7659caf 100644 --- a/Bearded.UI.Tests/Bearded.UI.Tests.csproj +++ b/Bearded.UI.Tests/Bearded.UI.Tests.csproj @@ -1,4 +1,4 @@ - + Bearded.UI.Tests net5.0;net6.0;netcoreapp3.1 @@ -20,4 +20,15 @@ + + + + + + + + + + + diff --git a/Bearded.UI.Tests/IsExternalInit.cs b/Bearded.UI.Tests/IsExternalInit.cs new file mode 100644 index 0000000..5ac1285 --- /dev/null +++ b/Bearded.UI.Tests/IsExternalInit.cs @@ -0,0 +1,6 @@ +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + // HACK: This is needed in order to use C#9+ features in netstandard + public class IsExternalInit { } +} diff --git a/Bearded.UI.Tests/Navigation/NodeLifecycleTests.cs b/Bearded.UI.Tests/Navigation/NodeLifecycleTests.cs new file mode 100644 index 0000000..211cd80 --- /dev/null +++ b/Bearded.UI.Tests/Navigation/NodeLifecycleTests.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using Bearded.UI.Controls; +using Bearded.UI.Navigation; +using FluentAssertions; +using Xunit; + +namespace Bearded.UI.Tests.Navigation; + +public sealed class NodeLifecycleTests +{ + private readonly NavigationController nav; + + public NodeLifecycleTests() + { + var root = new CompositeControl(); + nav = new NavigationController( + root, + new DependencyResolver(), + new Dictionary { { typeof(TestNode), TestNode.ModelFactory } }, + new Dictionary { { typeof(TestNode), TestNode.ViewFactory } }); + } + + [Fact] + public void PushingNodesInitializesCorrectly() + { + var node = nav.Push(TestParameter.UniqueInstance()); + + node.Initialized.Should().BeTrue(); + node.Terminated.Should().BeFalse(); + } + + [Fact] + public void ReplacingNodesInitializesAndTerminatesCorrectly() + { + var existingNode = nav.Push(TestParameter.UniqueInstance()); + var replacingNode = nav.Replace(TestParameter.UniqueInstance(), existingNode); + + existingNode.Terminated.Should().BeTrue(); + replacingNode.Initialized.Should().BeTrue(); + replacingNode.Terminated.Should().BeFalse(); + } + + [Fact] + public void ReplacingAllNodesInitializesAndTerminatesCorrectly() + { + var existingNode1 = nav.Push(TestParameter.UniqueInstance()); + var existingNode2 = nav.Push(TestParameter.UniqueInstance()); + var replacingNode = nav.ReplaceAll(TestParameter.UniqueInstance()); + + existingNode1.Terminated.Should().BeTrue(); + existingNode2.Terminated.Should().BeTrue(); + replacingNode.Initialized.Should().BeTrue(); + replacingNode.Terminated.Should().BeFalse(); + } + + [Fact] + public void ClosingNodesTerminatesCorrectly() + { + var node = nav.Push(TestParameter.UniqueInstance()); + nav.Close(node); + + node.Initialized.Should().BeTrue(); + node.Terminated.Should().BeTrue(); + } + + [Fact] + public void ClosingAllNodesTerminatesCorrectly() + { + var node1 = nav.Push(TestParameter.UniqueInstance()); + var node2 = nav.Push(TestParameter.UniqueInstance()); + nav.CloseAll(); + + node1.Initialized.Should().BeTrue(); + node1.Terminated.Should().BeTrue(); + node2.Initialized.Should().BeTrue(); + node2.Terminated.Should().BeTrue(); + } + + [Fact] + public void ExitingNavigationTerminatesCorrectly() + { + var node1 = nav.Push(TestParameter.UniqueInstance()); + var node2 = nav.Push(TestParameter.UniqueInstance()); + nav.Exit(); + + node1.Initialized.Should().BeTrue(); + node1.Terminated.Should().BeTrue(); + node2.Initialized.Should().BeTrue(); + node2.Terminated.Should().BeTrue(); + } + + [Fact] + public void ParameterIsPassedIntoModel() + { + var param = TestParameter.UniqueInstance(); + var node = nav.Push(param); + + node.PassedInParameter.Should().Be(param); + } + + private sealed class TestNode : NavigationNode + { + public static Func ModelFactory => () => new TestNode(); + public static Func ViewFactory => _ => new SimpleControl(); + + public bool Initialized { get; private set; } + public bool Terminated { get; private set; } + public TestParameter? PassedInParameter { get; private set; } + + protected override void Initialize(DependencyResolver dependencies, TestParameter parameters) + { + Initialized = true; + PassedInParameter = parameters; + } + + public override void Terminate() + { + Terminated = true; + } + } + + private sealed class TestParameter + { + // Equality should by default use reference equality, so each new instance is unique. + public static TestParameter UniqueInstance() => new(); + } +} diff --git a/Bearded.UI/Navigation/NavigationController.cs b/Bearded.UI/Navigation/NavigationController.cs index d72e870..f145660 100644 --- a/Bearded.UI/Navigation/NavigationController.cs +++ b/Bearded.UI/Navigation/NavigationController.cs @@ -36,59 +36,64 @@ public void Exit() public void CloseAll() { - while (root.Children.Count > 0) - root.Remove(root.Children[0]); + foreach (var (node, view) in viewsByModel) + { + node.Terminate(); + root.Remove(view); + } viewsByModel.Clear(); } public void Close(INavigationNode toClose) { - var viewToReplace = viewsByModel[toClose]; - root.Remove(viewToReplace); + var viewToRemove = viewsByModel[toClose]; + toClose.Terminate(); + root.Remove(viewToRemove); viewsByModel.Remove(toClose); } - public void ReplaceAll() + public TModel ReplaceAll() where TModel : NavigationNode { - ReplaceAll(default(Void)); + return ReplaceAll(default); } - public void ReplaceAll(TParameters parameters) + public TModel ReplaceAll(TParameters parameters) where TModel : NavigationNode { CloseAll(); - Push(parameters); + return Push(parameters); } - public void Replace(INavigationNode toReplace) + public TModel Replace(INavigationNode toReplace) where TModel : NavigationNode { - Replace(default(Void), toReplace); + return Replace(default, toReplace); } - public void Replace(TParameters parameters, INavigationNode toReplace) + public TModel Replace(TParameters parameters, INavigationNode toReplace) where TModel : NavigationNode { - toReplace.Terminate(); var viewToReplace = viewsByModel[toReplace]; - var (_, view) = instantiateModelAndView(parameters); + toReplace.Terminate(); + var (model, view) = instantiateModelAndView(parameters); new AnchorTemplate(viewToReplace).ApplyTo(view); root.AddOnTopOf(viewToReplace, view); root.Remove(viewToReplace); viewsByModel.Remove(toReplace); + return model; } public TModel Push() where TModel : NavigationNode { - return Push(default(Void)); + return Push(default); } public TModel Push(Func build) where TModel : NavigationNode { - return Push(default(Void), build); + return Push(default, build); } public TModel Push(TParameters parameters) @@ -117,13 +122,11 @@ public TModel Push(Func build) return (model, view); } - private Func findModelFactory() - => (Func) modelFactories[typeof(T)]; + private Func findModelFactory() => (Func) modelFactories[typeof(T)]; - private Func findViewFactory() - => (Func) viewFactories[typeof(T)]; + private Func findViewFactory() => (Func) viewFactories[typeof(T)]; - private NavigationContext createNavigationContext(T parameters) - => new NavigationContext(this, dependencyResolver, parameters); + private NavigationContext createNavigationContext(T parameters) => + new(this, dependencyResolver, parameters); } }