// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Testing.Input; using osu.Framework.Utils; using osuTK; using osuTK.Graphics; using osuTK.Input; namespace osu.Framework.Tests.Visual.UserInterface { public class TestSceneScreenStack : FrameworkTestScene { private TestScreen baseScreen; private ScreenStack stack; private readonly List slowLoaders = new List(); [SetUp] public void SetupTest() => Schedule(() => { Clear(); Add(stack = new ScreenStack(baseScreen = new TestScreen()) { RelativeSizeAxes = Axes.Both }); stack.ScreenPushed += (last, current) => { if (current is TestScreenSlow slow) slowLoaders.Add(slow); }; }); [TearDownSteps] public void Teardown() { AddStep("unblock any slow loaders", () => { foreach (var slow in slowLoaders) slow.AllowLoad.Set(); slowLoaders.Clear(); }); } [Test] public void TestPushFocusLost() { TestScreen screen1 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen { EagerFocus = true }); AddUntilStep("wait for focus grab", () => GetContainingInputManager().FocusedDrawable == screen1); pushAndEnsureCurrent(() => new TestScreen(), () => screen1); AddUntilStep("focus lost", () => GetContainingInputManager().FocusedDrawable != screen1); } [Test] public void TestPushFocusTransferred() { TestScreen screen1 = null, screen2 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen { EagerFocus = true }); AddUntilStep("wait for focus grab", () => GetContainingInputManager().FocusedDrawable == screen1); pushAndEnsureCurrent(() => screen2 = new TestScreen { EagerFocus = true }, () => screen1); AddUntilStep("focus transferred", () => GetContainingInputManager().FocusedDrawable == screen2); } [Test] public void TestPushStackTwice() { TestScreen testScreen = null; AddStep("public push", () => stack.Push(testScreen = new TestScreen())); AddStep("ensure succeeds", () => Assert.IsTrue(stack.CurrentScreen == testScreen)); AddStep("ensure internal throws", () => Assert.Throws(() => stack.Push(null, new TestScreen()))); } [Test] public void TestAddScreenWithoutStackFails() { AddStep("ensure throws", () => Assert.Throws(() => Add(new TestScreen()))); } [Test] public void TestPushInstantExitScreen() { AddStep("push non-valid screen", () => baseScreen.Push(new TestScreen { ValidForPush = false })); AddAssert("stack is single", () => stack.InternalChildren.Count == 1); } [Test] public void TestPushInstantExitScreenEmpty() { AddStep("fresh stack with non-valid screen", () => { Clear(); Add(stack = new ScreenStack(baseScreen = new TestScreen { ValidForPush = false }) { RelativeSizeAxes = Axes.Both }); }); AddAssert("stack is empty", () => stack.InternalChildren.Count == 0); } [Test] public void TestPushPop() { TestScreen screen1 = null, screen2 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen()); AddAssert("baseScreen suspended to screen1", () => baseScreen.SuspendedTo == screen1); AddAssert("screen1 entered from baseScreen", () => screen1.EnteredFrom == baseScreen); // we don't support pushing a screen that has been entered AddStep("bad push", () => Assert.Throws(typeof(ScreenStack.ScreenAlreadyEnteredException), () => screen1.Push(screen1))); pushAndEnsureCurrent(() => screen2 = new TestScreen(), () => screen1); AddAssert("screen1 suspended to screen2", () => screen1.SuspendedTo == screen2); AddAssert("screen2 entered from screen1", () => screen2.EnteredFrom == screen1); AddAssert("ensure child", () => screen1.GetChildScreen() == screen2); AddAssert("ensure parent 1", () => screen1.GetParentScreen() == baseScreen); AddAssert("ensure parent 2", () => screen2.GetParentScreen() == screen1); AddStep("pop", () => screen2.Exit()); AddAssert("screen1 resumed from screen2", () => screen1.ResumedFrom == screen2); AddAssert("screen2 exited to screen1", () => screen2.ExitedTo == screen1); AddAssert("screen2 has lifetime end", () => screen2.LifetimeEnd != double.MaxValue); AddAssert("ensure child gone", () => screen1.GetChildScreen() == null); AddAssert("ensure parent gone", () => screen2.GetParentScreen() == null); AddAssert("ensure not current", () => !screen2.IsCurrentScreen()); AddStep("pop", () => screen1.Exit()); AddAssert("baseScreen resumed from screen1", () => baseScreen.ResumedFrom == screen1); AddAssert("screen1 exited to baseScreen", () => screen1.ExitedTo == baseScreen); AddAssert("screen1 has lifetime end", () => screen1.LifetimeEnd != double.MaxValue); AddUntilStep("screen1 is removed", () => screen1.Parent == null); } [Test] public void TestMultiLevelExit() { TestScreen screen1 = null, screen2 = null, screen3 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen()); pushAndEnsureCurrent(() => screen2 = new TestScreen { ValidForResume = false }, () => screen1); pushAndEnsureCurrent(() => screen3 = new TestScreen(), () => screen2); AddStep("bad exit", () => Assert.Throws(typeof(ScreenStack.ScreenHasChildException), () => screen1.Exit())); AddStep("exit", () => screen3.Exit()); AddAssert("screen3 exited to screen2", () => screen3.ExitedTo == screen2); AddAssert("screen2 not resumed from screen3", () => screen2.ResumedFrom == null); AddAssert("screen2 exited to screen1", () => screen2.ExitedTo == screen1); AddAssert("screen1 resumed from screen2", () => screen1.ResumedFrom == screen2); AddAssert("screen3 has lifetime end", () => screen3.LifetimeEnd != double.MaxValue); AddAssert("screen2 has lifetime end", () => screen2.LifetimeEnd != double.MaxValue); AddAssert("screen 2 is not alive", () => !screen2.AsDrawable().IsAlive); AddAssert("ensure child gone", () => screen1.GetChildScreen() == null); AddAssert("ensure current", () => screen1.IsCurrentScreen()); AddAssert("ensure not current", () => !screen2.IsCurrentScreen()); AddAssert("ensure not current", () => !screen3.IsCurrentScreen()); } [Test] public void TestAsyncPush() { TestScreenSlow screen1 = null; AddStep("push slow", () => baseScreen.Push(screen1 = new TestScreenSlow())); AddAssert("base screen registered suspend", () => baseScreen.SuspendedTo == screen1); AddAssert("ensure not current", () => !screen1.IsCurrentScreen()); AddStep("allow load", () => screen1.AllowLoad.Set()); AddUntilStep("ensure current", () => screen1.IsCurrentScreen()); } [Test] public void TestAsyncPreloadPush() { TestScreenSlow screen1 = null; AddStep("preload slow", () => { screen1 = new TestScreenSlow(); screen1.AllowLoad.Set(); LoadComponentAsync(screen1); }); pushAndEnsureCurrent(() => screen1); } [Test] public void TestExitBeforePush() { TestScreenSlow screen1 = null; TestScreen screen2 = null; AddStep("push slow", () => baseScreen.Push(screen1 = new TestScreenSlow())); AddStep("exit slow", () => screen1.Exit()); AddStep("allow load", () => screen1.AllowLoad.Set()); AddUntilStep("wait for screen to load", () => screen1.LoadState >= LoadState.Ready); AddAssert("ensure not current", () => !screen1.IsCurrentScreen()); AddAssert("ensure base still current", () => baseScreen.IsCurrentScreen()); AddStep("push fast", () => baseScreen.Push(screen2 = new TestScreen())); AddUntilStep("ensure new current", () => screen2.IsCurrentScreen()); } [Test] public void TestScreenPushedAfterExiting() { TestScreen screen1 = null; AddStep("push", () => stack.Push(screen1 = new TestScreen())); AddUntilStep("wait for current", () => screen1.IsCurrentScreen()); AddStep("exit screen1", () => screen1.Exit()); AddUntilStep("ensure exited", () => !screen1.IsCurrentScreen()); AddStep("push again", () => Assert.Throws(() => stack.Push(screen1))); } [Test] public void TestPushToNonLoadedScreenFails() { TestScreenSlow screen1 = null; AddStep("push slow", () => stack.Push(screen1 = new TestScreenSlow())); AddStep("push second slow", () => Assert.Throws(() => screen1.Push(new TestScreenSlow()))); } [Test] public void TestPushAlreadyLoadedScreenFails() { TestScreen screen1 = null; AddStep("push once", () => stack.Push(screen1 = new TestScreen())); AddUntilStep("wait for screen to be loaded", () => screen1.IsLoaded); AddStep("exit", () => screen1.Exit()); AddStep("push again fails", () => Assert.Throws(() => stack.Push(screen1))); AddAssert("stack in valid state", () => stack.CurrentScreen == baseScreen); } [Test] public void TestEventOrder() { List order = new List(); var screen1 = new TestScreen { Entered = () => order.Add(1), Suspended = () => order.Add(2), Resumed = () => order.Add(5), }; var screen2 = new TestScreen { Entered = () => order.Add(3), Exited = () => order.Add(4), }; AddStep("push screen1", () => stack.Push(screen1)); AddUntilStep("ensure current", () => screen1.IsCurrentScreen()); AddStep("preload screen2", () => LoadComponentAsync(screen2)); AddUntilStep("wait for load", () => screen2.LoadState == LoadState.Ready); AddStep("push screen2", () => screen1.Push(screen2)); AddUntilStep("ensure current", () => screen2.IsCurrentScreen()); AddStep("exit screen2", () => screen2.Exit()); AddUntilStep("ensure exited", () => !screen2.IsCurrentScreen()); AddStep("push screen2", () => screen1.Exit()); AddUntilStep("ensure exited", () => !screen1.IsCurrentScreen()); AddAssert("order is correct", () => order.SequenceEqual(order.OrderBy(i => i))); } [Test] public void TestComeVisibleFromHidden() { TestScreen screen1 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen { Alpha = 0 }); AddUntilStep("screen1 is visible", () => screen1.Alpha > 0); pushAndEnsureCurrent(() => new TestScreen { Alpha = 0 }, () => screen1); } [TestCase(false, false)] [TestCase(false, true)] [TestCase(true, false)] [TestCase(true, true)] public void TestAsyncEventOrder(bool earlyExit, bool suspendImmediately) { TestScreenSlow screen1 = null; TestScreenSlow screen2 = null; List order = null; if (!suspendImmediately) { AddStep("override stack", () => { // we can't use the [SetUp] screen stack as we need to change the ctor parameters. Clear(); Add(stack = new ScreenStack(baseScreen = new TestScreen(id: 0)) { RelativeSizeAxes = Axes.Both }); }); } AddStep("Perform setup", () => { order = new List(); screen1 = new TestScreenSlow(1) { Entered = () => order.Add(1), Suspended = () => order.Add(2), Resumed = () => order.Add(5), }; screen2 = new TestScreenSlow(2) { Entered = () => order.Add(3), Exited = () => order.Add(4), }; }); AddStep("push slow", () => stack.Push(screen1)); AddStep("push second slow", () => stack.Push(screen2)); AddStep("allow load 1", () => screen1.AllowLoad.Set()); AddUntilStep("ensure screen1 not current", () => !screen1.IsCurrentScreen()); AddUntilStep("ensure screen2 not current", () => !screen2.IsCurrentScreen()); // but the stack has a different idea of "current" AddAssert("ensure screen2 is current at the stack", () => stack.CurrentScreen == screen2); if (suspendImmediately) AddUntilStep("screen1's suspending fired", () => screen1.SuspendedTo == screen2); else AddUntilStep("screen1's entered and suspending fired", () => screen1.EnteredFrom != null); if (earlyExit) AddStep("early exit 2", () => screen2.Exit()); AddStep("allow load 2", () => screen2.AllowLoad.Set()); if (earlyExit) { AddAssert("screen2's entered did not fire", () => screen2.EnteredFrom == null); AddAssert("screen2's exited did not fire", () => screen2.ExitedTo == null); } else { AddUntilStep("ensure screen2 is current", () => screen2.IsCurrentScreen()); AddAssert("screen2's entered fired", () => screen2.EnteredFrom == screen1); AddStep("exit 2", () => screen2.Exit()); AddUntilStep("ensure screen1 is current", () => screen1.IsCurrentScreen()); AddAssert("screen2's exited fired", () => screen2.ExitedTo == screen1); } AddAssert("order is correct", () => order.SequenceEqual(order.OrderBy(i => i))); } [Test] public void TestEventsNotFiredBeforeScreenLoad() { Screen screen1 = null; bool wasLoaded = true; pushAndEnsureCurrent(() => screen1 = new TestScreen { // ReSharper disable once AccessToModifiedClosure Entered = () => wasLoaded &= screen1?.IsLoaded == true, // ReSharper disable once AccessToModifiedClosure Suspended = () => wasLoaded &= screen1?.IsLoaded == true, }); pushAndEnsureCurrent(() => new TestScreen(), () => screen1); AddAssert("was loaded before events", () => wasLoaded); } [Test] public void TestAsyncDoublePush() { TestScreenSlow screen1 = null; TestScreenSlow screen2 = null; AddStep("push slow", () => stack.Push(screen1 = new TestScreenSlow())); // important to note we are pushing to the stack here, unlike the failing case above. AddStep("push second slow", () => stack.Push(screen2 = new TestScreenSlow())); AddAssert("base screen registered suspend", () => baseScreen.SuspendedTo == screen1); AddAssert("screen1 is not current", () => !screen1.IsCurrentScreen()); AddAssert("screen2 is not current", () => !screen2.IsCurrentScreen()); AddAssert("screen2 is current to stack", () => stack.CurrentScreen == screen2); AddAssert("screen1 not registered suspend", () => screen1.SuspendedTo == null); AddAssert("screen2 not registered entered", () => screen2.EnteredFrom == null); AddStep("allow load 2", () => screen2.AllowLoad.Set()); // screen 2 won't actually be loading since the load is only triggered after screen1 is loaded. AddWaitStep("wait for load", 10); // furthermore, even though screen 2 is able to load, screen 1 has not yet so we shouldn't has received any events. AddAssert("screen1 is not current", () => !screen1.IsCurrentScreen()); AddAssert("screen2 is not current", () => !screen2.IsCurrentScreen()); AddAssert("screen1 not registered suspend", () => screen1.SuspendedTo == null); AddAssert("screen2 not registered entered", () => screen2.EnteredFrom == null); AddStep("allow load 1", () => screen1.AllowLoad.Set()); AddUntilStep("screen1 is loaded", () => screen1.LoadState == LoadState.Loaded); AddUntilStep("screen2 is loaded", () => screen2.LoadState == LoadState.Loaded); AddUntilStep("screen1 is expired", () => !screen1.IsAlive); AddUntilStep("screen1 is not current", () => !screen1.IsCurrentScreen()); AddUntilStep("screen2 is current", () => screen2.IsCurrentScreen()); AddAssert("screen1 registered suspend", () => screen1.SuspendedTo == screen2); AddAssert("screen2 registered entered", () => screen2.EnteredFrom == screen1); } [Test] public void TestAsyncPushWithNonImmediateSuspend() { AddStep("override stack", () => { // we can't use the [SetUp] screen stack as we need to change the ctor parameters. Clear(); Add(stack = new ScreenStack(baseScreen = new TestScreen(), false) { RelativeSizeAxes = Axes.Both }); }); TestScreenSlow screen1 = null; AddStep("push slow", () => baseScreen.Push(screen1 = new TestScreenSlow())); AddAssert("base screen not yet registered suspend", () => baseScreen.SuspendedTo == null); AddAssert("ensure notcurrent", () => !screen1.IsCurrentScreen()); AddStep("allow load", () => screen1.AllowLoad.Set()); AddUntilStep("ensure current", () => screen1.IsCurrentScreen()); AddAssert("base screen registered suspend", () => baseScreen.SuspendedTo == screen1); } [Test] public void TestMakeCurrent() { TestScreen screen1 = null; TestScreen screen2 = null; TestScreen screen3 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen()); pushAndEnsureCurrent(() => screen2 = new TestScreen(), () => screen1); pushAndEnsureCurrent(() => screen3 = new TestScreen(), () => screen2); AddStep("block exit", () => screen3.Exiting = () => true); AddStep("make screen 1 current", () => screen1.MakeCurrent()); AddAssert("screen 3 still current", () => screen3.IsCurrentScreen()); AddAssert("screen 3 exited fired", () => screen3.ExitedTo == screen2); AddAssert("screen 2 resumed not fired", () => screen2.ResumedFrom == null); AddAssert("screen 3 doesn't have lifetime end", () => screen3.LifetimeEnd == double.MaxValue); AddAssert("screen 2 valid for resume", () => screen2.ValidForResume); AddAssert("screen 1 valid for resume", () => screen1.ValidForResume); AddStep("don't block exit", () => screen3.Exiting = () => false); AddStep("make screen 1 current", () => screen1.MakeCurrent()); AddAssert("screen 1 current", () => screen1.IsCurrentScreen()); AddAssert("screen 3 exited fired", () => screen3.ExitedTo == screen2); AddAssert("screen 2 exited fired", () => screen2.ExitedTo == screen1); AddAssert("screen 1 resumed fired", () => screen1.ResumedFrom == screen2); AddAssert("screen 1 doesn't have lifetime end", () => screen1.LifetimeEnd == double.MaxValue); AddAssert("screen 3 has lifetime end", () => screen3.LifetimeEnd != double.MaxValue); AddAssert("screen 2 is not alive", () => !screen2.AsDrawable().IsAlive); } [Test] public void TestCallingExitFromBlockingExit() { TestScreen screen1 = null; TestScreen screen2 = null; int screen1ResumedCount = 0; bool blocking = true; pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1) { Resumed = () => screen1ResumedCount++ }); pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2) { Exiting = () => { if (blocking) { blocking = false; // ReSharper disable once AccessToModifiedClosure screen2.Exit(); return true; } // this call should fail in a way the user can understand. return false; } }, () => screen1); AddStep("make screen 1 current", () => screen1.MakeCurrent()); AddAssert("screen 1 resumed only once", () => screen1ResumedCount == 1); } [TestCase(false)] [TestCase(true)] public void TestMakeCurrentMidwayExitBlocking(bool validForResume) { TestScreen screen1 = null; TestScreen screen2 = null; TestScreen screen3 = null; TestScreen screen4 = null; int screen3ResumedCount = 0; pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2), () => screen1); pushAndEnsureCurrent(() => screen3 = new TestScreen(id: 3) { Resumed = () => screen3ResumedCount++ }, () => screen2); pushAndEnsureCurrent(() => screen4 = new TestScreen(id: 4), () => screen3); AddStep("block exit screen3", () => { screen3.Exiting = () => true; screen3.ValidForResume = validForResume; }); AddStep("make screen1 current", () => screen1.MakeCurrent()); // check the exit worked for one level AddUntilStep("screen4 is not alive", () => !screen4.AsDrawable().IsAlive); AddAssert("screen4 has lifetime end", () => screen4.LifetimeEnd != double.MaxValue); if (validForResume) { // check we blocked at screen 3 AddAssert("screen 3 valid for resume", () => screen3.ValidForResume); AddAssert("screen3 is current", () => screen3.IsCurrentScreen()); AddAssert("screen3 resumed", () => screen3ResumedCount == 1); // check the ValidForResume state wasn't changed on parents AddAssert("screen 1 still valid for resume", () => screen1.ValidForResume); AddAssert("screen 2 still valid for resume", () => screen2.ValidForResume); AddStep("make screen 1 current", () => screen1.MakeCurrent()); // check blocking is consistent on a second attempt AddAssert("screen3 not resumed again", () => screen3ResumedCount == 1); AddAssert("screen3 is still current", () => screen3.IsCurrentScreen()); AddStep("stop blocking exit", () => screen3.Exiting = () => false); AddStep("make screen1 current", () => screen1.MakeCurrent()); } else { AddAssert("screen 3 not valid for resume", () => !screen3.ValidForResume); AddAssert("screen3 not current", () => !screen3.IsCurrentScreen()); AddAssert("screen3 did not resume", () => screen3ResumedCount == 0); } AddAssert("screen1 current", () => screen1.IsCurrentScreen()); AddAssert("screen1 doesn't have lifetime end", () => screen1.LifetimeEnd == double.MaxValue); AddUntilStep("screen3 is not alive", () => !screen3.AsDrawable().IsAlive); } [Test] public void TestMakeCurrentUnbindOrder() { List screens = null; AddStep("Setup screens", () => { screens = new List(); for (int i = 0; i < 5; i++) { var screen = new TestScreen(); screen.OnUnbindAllBindables += () => { if (screens.Last() != screen) throw new InvalidOperationException("Unbind order was wrong"); screens.Remove(screen); }; screens.Add(screen); } }); for (int i = 0; i < 5; i++) { var local = i; // needed to store the correct value for our delegate pushAndEnsureCurrent(() => screens[local], () => local > 0 ? screens[local - 1] : null); } AddStep("make first screen current", () => screens.First().MakeCurrent()); AddUntilStep("All screens unbound in correct order", () => screens.Count == 1); } [Test] public void TestScreensUnboundAndDisposedOnStackDisposal() { const int screen_count = 5; const int exit_count = 2; List screens = null; int disposedScreens = 0; AddStep("Setup screens", () => { screens = new List(); disposedScreens = 0; for (int i = 0; i < screen_count; i++) { var screen = new TestScreen(id: i); screen.OnDispose += () => disposedScreens++; screen.OnUnbindAllBindables += () => { if (screens.Last() != screen) throw new InvalidOperationException("Unbind order was wrong"); screens.Remove(screen); }; screens.Add(screen); } }); for (int i = 0; i < screen_count; i++) { var local = i; // needed to store the correct value for our delegate pushAndEnsureCurrent(() => screens[local], () => local > 0 ? screens[local - 1] : null); } AddStep("remove and dispose stack", () => { // We must exit a few screens just before the stack is disposed, otherwise the stack will update for one more frame and dispose screens itself for (int i = 0; i < exit_count; i++) stack.Exit(); Remove(stack); stack.Dispose(); }); AddUntilStep("All screens unbound in correct order", () => screens.Count == 0); AddAssert("All screens disposed", () => disposedScreens == screen_count); } /// /// Make sure that all bindables are returned before OnResuming is called for the next screen. /// [Test] public void TestReturnBindsBeforeResume() { TestScreen screen1 = null, screen2 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen()); pushAndEnsureCurrent(() => screen2 = new TestScreen(true), () => screen1); AddStep("Exit screen", () => screen2.Exit()); AddUntilStep("Wait until base is current", () => screen1.IsCurrentScreen()); AddAssert("Bindables have been returned by new screen", () => !screen2.DummyBindable.Disabled && !screen2.LeasedCopy.Disabled); } [Test] public void TestMakeCurrentDuringLoad() { TestScreen screen1 = null; TestScreenSlow screen2 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen()); AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); AddStep("make screen1 current", () => screen1.MakeCurrent()); AddStep("allow load of screen2", () => screen2.AllowLoad.Set()); AddUntilStep("wait for screen2 to load", () => screen2.LoadState == LoadState.Ready); AddAssert("screen1 is current screen", () => screen1.IsCurrentScreen()); AddAssert("screen2 did not receive OnEntering", () => screen2.EnteredFrom == null); AddAssert("screen2 did not receive OnExiting", () => screen2.ExitedTo == null); } [Test] public void TestMakeCurrentDuringLoadOfMany() { TestScreen screen1 = null; TestScreenSlow screen2 = null; TestScreenSlow screen3 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); AddStep("push slow screen 2", () => stack.Push(screen2 = new TestScreenSlow(id: 2))); AddStep("push slow screen 3", () => stack.Push(screen3 = new TestScreenSlow(id: 3))); AddAssert("Screen 1 is not current", () => !screen1.IsCurrentScreen()); AddStep("Make current screen 1", () => screen1.MakeCurrent()); AddAssert("Screen 1 is current", () => screen1.IsCurrentScreen()); // Allow the screens to load out of order to test whether or not screen 3 tried to load. // The load should be blocked since screen 2 is already exited by MakeCurrent. AddStep("allow screen 3 to load", () => screen3.AllowLoad.Set()); AddStep("allow screen 2 to load", () => screen2.AllowLoad.Set()); AddAssert("Screen 1 is current", () => screen1.IsCurrentScreen()); AddAssert("Screen 2 did not load", () => !screen2.IsLoaded); AddAssert("Screen 3 did not load", () => !screen3.IsLoaded); } [Test] public void TestMakeCurrentOnSameScreen() { TestScreen screen1 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen()); AddStep("Make current the same screen", () => screen1.MakeCurrent()); AddAssert("Screen 1 is current", () => screen1.IsCurrentScreen()); } [Test] public void TestPushOnExiting() { TestScreen screen1 = null; pushAndEnsureCurrent(() => { screen1 = new TestScreen(id: 1); screen1.Exiting = () => { screen1.Push(new TestScreen(id: 2)); return true; }; return screen1; }); AddStep("Exit screen 1", () => screen1.Exit()); AddAssert("Screen 1 is not current", () => !screen1.IsCurrentScreen()); AddAssert("Stack is not empty", () => stack.CurrentScreen != null); } [Test] public void TestInvalidPushBlocksNonImmediateSuspend() { TestScreen screen1 = null; TestScreenSlow screen2 = null; AddStep("override stack", () => { // we can't use the [SetUp] screen stack as we need to change the ctor parameters. Clear(); Add(stack = new ScreenStack(baseScreen = new TestScreen(), false) { RelativeSizeAxes = Axes.Both }); }); pushAndEnsureCurrent(() => screen1 = new TestScreen()); AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); AddStep("exit slow", () => screen2.Exit()); AddStep("allow load", () => screen2.AllowLoad.Set()); AddUntilStep("wait for screen 2 to load", () => screen2.LoadState >= LoadState.Ready); AddAssert("screen 1 did not receive suspending", () => screen1.SuspendedTo == null); AddAssert("screen 1 did not receive resuming", () => screen1.ResumedFrom == null); } [Test] public void TestInvalidPushDoesNotBlockImmediateSuspend() { TestScreen screen1 = null; TestScreenSlow screen2 = null; AddStep("override stack", () => { // we can't use the [SetUp] screen stack as we need to change the ctor parameters. Clear(); Add(stack = new ScreenStack(baseScreen = new TestScreen(), true) { RelativeSizeAxes = Axes.Both }); }); pushAndEnsureCurrent(() => screen1 = new TestScreen()); AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); AddStep("exit slow", () => screen2.Exit()); AddStep("allow load", () => screen2.AllowLoad.Set()); AddUntilStep("wait for screen 2 to load", () => screen2.LoadState >= LoadState.Ready); AddAssert("screen 1 did receive suspending", () => screen1.SuspendedTo == screen2); AddAssert("screen 1 did receive resumed", () => screen1.ResumedFrom == screen2); } /// /// Push two screens and check that they only handle input when they are respectively loaded and current. /// [Test] public void TestNonCurrentScreenDoesNotAcceptInput() { ManualInputManager inputManager = null; AddStep("override stack", () => { // we can't use the [SetUp] screen stack as we need to change the ctor parameters. Clear(); Add(inputManager = new ManualInputManager { Child = stack = new ScreenStack(baseScreen = new TestScreen()) { RelativeSizeAxes = Axes.Both } }); }); TestScreen screen1 = null; TestScreenSlow screen2 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen()); AddStep("Click center of screen", () => clickScreen(inputManager, screen1)); AddAssert("screen 1 clicked", () => screen1.ClickCount == 1); AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); AddStep("Click center of screen", () => inputManager.Click(MouseButton.Left)); AddAssert("screen 1 not clicked", () => screen1.ClickCount == 1); AddAssert("Screen 2 not clicked", () => screen2.ClickCount == 0 && !screen2.IsLoaded); AddStep("Allow screen to load", () => screen2.AllowLoad.Set()); AddUntilStep("ensure current", () => screen2.IsCurrentScreen()); AddStep("Click center of screen", () => clickScreen(inputManager, screen2)); AddAssert("screen 1 not clicked", () => screen1.ClickCount == 1); AddAssert("Screen 2 clicked", () => screen2.ClickCount == 1 && screen2.IsLoaded); } [Test] public void TestMakeCurrentIntermediateResumes() { TestScreen screen1 = null; TestScreen screen2 = null; TestScreen screen3 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2) { Exiting = () => true }, () => screen1); pushAndEnsureCurrent(() => screen3 = new TestScreen(id: 3), () => screen2); AddStep("make screen1 current", () => screen1.MakeCurrent()); AddAssert("screen3 exited to screen2", () => screen3.ExitedTo == screen2); AddAssert("screen2 resumed from screen3", () => screen2.ResumedFrom == screen3); } [Test] public void TestGetChildScreenAndGetParentScreenReturnNullWhenNotInStack() { TestScreen screen1 = null; TestScreen screen2 = null; TestScreen screen3 = null; pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2), () => screen1); pushAndEnsureCurrent(() => screen3 = new TestScreen(id: 3), () => screen2); AddStep("exit from screen 3", () => screen3.Exit()); AddAssert("screen 3 parent is null", () => screen3.GetParentScreen() == null); AddAssert("screen 3 child is null", () => screen3.GetChildScreen() == null); } /// /// Ensure that an intermediary screen doesn't block and doesn't attempt to fire events when not loaded. /// [Test] public void TestMakeCurrentWhileScreensStillLoading() { TestScreen root = null; pushAndEnsureCurrent(() => root = new TestScreen(id: 1)); AddStep("push slow", () => stack.Push(new TestScreenSlow { Exiting = () => true })); AddStep("push second slow", () => stack.Push(new TestScreenSlow())); AddStep("make screen1 current", () => root.MakeCurrent()); } private void clickScreen(ManualInputManager inputManager, TestScreen screen) { inputManager.MoveMouseTo(screen); inputManager.Click(MouseButton.Left); } private void pushAndEnsureCurrent(Func screenCtor, Func target = null) { IScreen screen = null; AddStep("push", () => (target?.Invoke() ?? baseScreen).Push(screen = screenCtor())); AddUntilStep("ensure current", () => screen.IsCurrentScreen()); } private class TestScreenSlow : TestScreen { public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); public TestScreenSlow(int? id = null) : base(false, id) { } [BackgroundDependencyLoader] private void load() { if (!AllowLoad.Wait(TimeSpan.FromSeconds(10))) throw new TimeoutException(); } } private class TestScreen : Screen { public Func Exiting; public Action Entered; public Action Suspended; public Action Resumed; public Action Exited; public IScreen EnteredFrom; public IScreen ExitedTo; public IScreen SuspendedTo; public IScreen ResumedFrom; public static int Sequence; private BasicButton popButton; private const int transition_time = 500; public bool EagerFocus; public int ClickCount { get; private set; } public override bool RequestsFocus => EagerFocus; public override bool AcceptsFocus => EagerFocus; public override bool HandleNonPositionalInput => true; public LeasedBindable LeasedCopy; public readonly Bindable DummyBindable = new Bindable(); private readonly bool shouldTakeOutLease; public TestScreen(bool shouldTakeOutLease = false, int? id = null) { this.shouldTakeOutLease = shouldTakeOutLease; if (id != null) Name = id.ToString(); } [BackgroundDependencyLoader] private void load() { InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Size = new Vector2(1), Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = new Color4( Math.Max(0.5f, RNG.NextSingle()), Math.Max(0.5f, RNG.NextSingle()), Math.Max(0.5f, RNG.NextSingle()), 1), }, new SpriteText { Text = $@"Screen {Sequence++}", Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = new FontUsage(size: 50) }, popButton = new BasicButton { Text = @"Pop", RelativeSizeAxes = Axes.Both, Size = new Vector2(0.1f), Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, BackgroundColour = Color4.Red, Alpha = 0, Action = this.Exit }, new BasicButton { Text = @"Push", RelativeSizeAxes = Axes.Both, Size = new Vector2(0.1f), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, BackgroundColour = Color4.YellowGreen, Action = delegate { this.Push(new TestScreen { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); } } }; BorderColour = Color4.Red; Masking = true; } public override string ToString() => Name; protected override void OnFocus(FocusEvent e) { base.OnFocus(e); BorderThickness = 10; } protected override void OnFocusLost(FocusLostEvent e) { base.OnFocusLost(e); BorderThickness = 0; } public override void OnEntering(IScreen last) { attemptTransformMutation(); EnteredFrom = last; Entered?.Invoke(); if (shouldTakeOutLease) { DummyBindable.BindTo(((TestScreen)last).DummyBindable); LeasedCopy = DummyBindable.BeginLease(true); } base.OnEntering(last); if (last != null) { //only show the pop button if we are entered form another screen. popButton.Alpha = 1; } this.MoveTo(new Vector2(0, -DrawSize.Y)); this.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); this.FadeIn(1000); } public override bool OnExiting(IScreen next) { attemptTransformMutation(); ExitedTo = next; Exited?.Invoke(); if (Exiting?.Invoke() == true) return true; this.MoveTo(new Vector2(0, -DrawSize.Y), transition_time, Easing.OutQuint); return base.OnExiting(next); } public override void OnSuspending(IScreen next) { attemptTransformMutation(); SuspendedTo = next; Suspended?.Invoke(); base.OnSuspending(next); this.MoveTo(new Vector2(0, DrawSize.Y), transition_time, Easing.OutQuint); } public override void OnResuming(IScreen last) { attemptTransformMutation(); ResumedFrom = last; Resumed?.Invoke(); base.OnResuming(last); this.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); } private void attemptTransformMutation() { // all callbacks should be in a state where transforms are able to be run. this.FadeIn(); } protected override bool OnClick(ClickEvent e) { ClickCount++; return base.OnClick(e); } } } }