A game framework written with osu! in mind.
at master 46 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.Linq; 7using System.Threading; 8using NUnit.Framework; 9using osu.Framework.Allocation; 10using osu.Framework.Bindables; 11using osu.Framework.Graphics; 12using osu.Framework.Graphics.Shapes; 13using osu.Framework.Graphics.Sprites; 14using osu.Framework.Graphics.UserInterface; 15using osu.Framework.Input.Events; 16using osu.Framework.Screens; 17using osu.Framework.Testing; 18using osu.Framework.Testing.Input; 19using osu.Framework.Utils; 20using osuTK; 21using osuTK.Graphics; 22using osuTK.Input; 23 24namespace osu.Framework.Tests.Visual.UserInterface 25{ 26 public class TestSceneScreenStack : FrameworkTestScene 27 { 28 private TestScreen baseScreen; 29 private ScreenStack stack; 30 31 private readonly List<TestScreenSlow> slowLoaders = new List<TestScreenSlow>(); 32 33 [SetUp] 34 public void SetupTest() => Schedule(() => 35 { 36 Clear(); 37 38 Add(stack = new ScreenStack(baseScreen = new TestScreen()) 39 { 40 RelativeSizeAxes = Axes.Both 41 }); 42 43 stack.ScreenPushed += (last, current) => 44 { 45 if (current is TestScreenSlow slow) 46 slowLoaders.Add(slow); 47 }; 48 }); 49 50 [TearDownSteps] 51 public void Teardown() 52 { 53 AddStep("unblock any slow loaders", () => 54 { 55 foreach (var slow in slowLoaders) 56 slow.AllowLoad.Set(); 57 58 slowLoaders.Clear(); 59 }); 60 } 61 62 [Test] 63 public void TestPushFocusLost() 64 { 65 TestScreen screen1 = null; 66 67 pushAndEnsureCurrent(() => screen1 = new TestScreen { EagerFocus = true }); 68 AddUntilStep("wait for focus grab", () => GetContainingInputManager().FocusedDrawable == screen1); 69 70 pushAndEnsureCurrent(() => new TestScreen(), () => screen1); 71 72 AddUntilStep("focus lost", () => GetContainingInputManager().FocusedDrawable != screen1); 73 } 74 75 [Test] 76 public void TestPushFocusTransferred() 77 { 78 TestScreen screen1 = null, screen2 = null; 79 80 pushAndEnsureCurrent(() => screen1 = new TestScreen { EagerFocus = true }); 81 AddUntilStep("wait for focus grab", () => GetContainingInputManager().FocusedDrawable == screen1); 82 83 pushAndEnsureCurrent(() => screen2 = new TestScreen { EagerFocus = true }, () => screen1); 84 85 AddUntilStep("focus transferred", () => GetContainingInputManager().FocusedDrawable == screen2); 86 } 87 88 [Test] 89 public void TestPushStackTwice() 90 { 91 TestScreen testScreen = null; 92 93 AddStep("public push", () => stack.Push(testScreen = new TestScreen())); 94 AddStep("ensure succeeds", () => Assert.IsTrue(stack.CurrentScreen == testScreen)); 95 AddStep("ensure internal throws", () => Assert.Throws<InvalidOperationException>(() => stack.Push(null, new TestScreen()))); 96 } 97 98 [Test] 99 public void TestAddScreenWithoutStackFails() 100 { 101 AddStep("ensure throws", () => Assert.Throws<InvalidOperationException>(() => Add(new TestScreen()))); 102 } 103 104 [Test] 105 public void TestPushInstantExitScreen() 106 { 107 AddStep("push non-valid screen", () => baseScreen.Push(new TestScreen { ValidForPush = false })); 108 AddAssert("stack is single", () => stack.InternalChildren.Count == 1); 109 } 110 111 [Test] 112 public void TestPushInstantExitScreenEmpty() 113 { 114 AddStep("fresh stack with non-valid screen", () => 115 { 116 Clear(); 117 Add(stack = new ScreenStack(baseScreen = new TestScreen { ValidForPush = false }) 118 { 119 RelativeSizeAxes = Axes.Both 120 }); 121 }); 122 123 AddAssert("stack is empty", () => stack.InternalChildren.Count == 0); 124 } 125 126 [Test] 127 public void TestPushPop() 128 { 129 TestScreen screen1 = null, screen2 = null; 130 131 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 132 133 AddAssert("baseScreen suspended to screen1", () => baseScreen.SuspendedTo == screen1); 134 AddAssert("screen1 entered from baseScreen", () => screen1.EnteredFrom == baseScreen); 135 136 // we don't support pushing a screen that has been entered 137 AddStep("bad push", () => Assert.Throws(typeof(ScreenStack.ScreenAlreadyEnteredException), () => screen1.Push(screen1))); 138 139 pushAndEnsureCurrent(() => screen2 = new TestScreen(), () => screen1); 140 141 AddAssert("screen1 suspended to screen2", () => screen1.SuspendedTo == screen2); 142 AddAssert("screen2 entered from screen1", () => screen2.EnteredFrom == screen1); 143 144 AddAssert("ensure child", () => screen1.GetChildScreen() == screen2); 145 AddAssert("ensure parent 1", () => screen1.GetParentScreen() == baseScreen); 146 AddAssert("ensure parent 2", () => screen2.GetParentScreen() == screen1); 147 148 AddStep("pop", () => screen2.Exit()); 149 150 AddAssert("screen1 resumed from screen2", () => screen1.ResumedFrom == screen2); 151 AddAssert("screen2 exited to screen1", () => screen2.ExitedTo == screen1); 152 AddAssert("screen2 has lifetime end", () => screen2.LifetimeEnd != double.MaxValue); 153 154 AddAssert("ensure child gone", () => screen1.GetChildScreen() == null); 155 AddAssert("ensure parent gone", () => screen2.GetParentScreen() == null); 156 AddAssert("ensure not current", () => !screen2.IsCurrentScreen()); 157 158 AddStep("pop", () => screen1.Exit()); 159 160 AddAssert("baseScreen resumed from screen1", () => baseScreen.ResumedFrom == screen1); 161 AddAssert("screen1 exited to baseScreen", () => screen1.ExitedTo == baseScreen); 162 AddAssert("screen1 has lifetime end", () => screen1.LifetimeEnd != double.MaxValue); 163 AddUntilStep("screen1 is removed", () => screen1.Parent == null); 164 } 165 166 [Test] 167 public void TestMultiLevelExit() 168 { 169 TestScreen screen1 = null, screen2 = null, screen3 = null; 170 171 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 172 pushAndEnsureCurrent(() => screen2 = new TestScreen { ValidForResume = false }, () => screen1); 173 pushAndEnsureCurrent(() => screen3 = new TestScreen(), () => screen2); 174 175 AddStep("bad exit", () => Assert.Throws(typeof(ScreenStack.ScreenHasChildException), () => screen1.Exit())); 176 AddStep("exit", () => screen3.Exit()); 177 178 AddAssert("screen3 exited to screen2", () => screen3.ExitedTo == screen2); 179 AddAssert("screen2 not resumed from screen3", () => screen2.ResumedFrom == null); 180 AddAssert("screen2 exited to screen1", () => screen2.ExitedTo == screen1); 181 AddAssert("screen1 resumed from screen2", () => screen1.ResumedFrom == screen2); 182 183 AddAssert("screen3 has lifetime end", () => screen3.LifetimeEnd != double.MaxValue); 184 AddAssert("screen2 has lifetime end", () => screen2.LifetimeEnd != double.MaxValue); 185 AddAssert("screen 2 is not alive", () => !screen2.AsDrawable().IsAlive); 186 187 AddAssert("ensure child gone", () => screen1.GetChildScreen() == null); 188 AddAssert("ensure current", () => screen1.IsCurrentScreen()); 189 190 AddAssert("ensure not current", () => !screen2.IsCurrentScreen()); 191 AddAssert("ensure not current", () => !screen3.IsCurrentScreen()); 192 } 193 194 [Test] 195 public void TestAsyncPush() 196 { 197 TestScreenSlow screen1 = null; 198 199 AddStep("push slow", () => baseScreen.Push(screen1 = new TestScreenSlow())); 200 AddAssert("base screen registered suspend", () => baseScreen.SuspendedTo == screen1); 201 AddAssert("ensure not current", () => !screen1.IsCurrentScreen()); 202 AddStep("allow load", () => screen1.AllowLoad.Set()); 203 AddUntilStep("ensure current", () => screen1.IsCurrentScreen()); 204 } 205 206 [Test] 207 public void TestAsyncPreloadPush() 208 { 209 TestScreenSlow screen1 = null; 210 AddStep("preload slow", () => 211 { 212 screen1 = new TestScreenSlow(); 213 screen1.AllowLoad.Set(); 214 215 LoadComponentAsync(screen1); 216 }); 217 pushAndEnsureCurrent(() => screen1); 218 } 219 220 [Test] 221 public void TestExitBeforePush() 222 { 223 TestScreenSlow screen1 = null; 224 TestScreen screen2 = null; 225 226 AddStep("push slow", () => baseScreen.Push(screen1 = new TestScreenSlow())); 227 AddStep("exit slow", () => screen1.Exit()); 228 AddStep("allow load", () => screen1.AllowLoad.Set()); 229 AddUntilStep("wait for screen to load", () => screen1.LoadState >= LoadState.Ready); 230 AddAssert("ensure not current", () => !screen1.IsCurrentScreen()); 231 AddAssert("ensure base still current", () => baseScreen.IsCurrentScreen()); 232 AddStep("push fast", () => baseScreen.Push(screen2 = new TestScreen())); 233 AddUntilStep("ensure new current", () => screen2.IsCurrentScreen()); 234 } 235 236 [Test] 237 public void TestScreenPushedAfterExiting() 238 { 239 TestScreen screen1 = null; 240 241 AddStep("push", () => stack.Push(screen1 = new TestScreen())); 242 243 AddUntilStep("wait for current", () => screen1.IsCurrentScreen()); 244 AddStep("exit screen1", () => screen1.Exit()); 245 AddUntilStep("ensure exited", () => !screen1.IsCurrentScreen()); 246 247 AddStep("push again", () => Assert.Throws<InvalidOperationException>(() => stack.Push(screen1))); 248 } 249 250 [Test] 251 public void TestPushToNonLoadedScreenFails() 252 { 253 TestScreenSlow screen1 = null; 254 255 AddStep("push slow", () => stack.Push(screen1 = new TestScreenSlow())); 256 AddStep("push second slow", () => Assert.Throws<InvalidOperationException>(() => screen1.Push(new TestScreenSlow()))); 257 } 258 259 [Test] 260 public void TestPushAlreadyLoadedScreenFails() 261 { 262 TestScreen screen1 = null; 263 264 AddStep("push once", () => stack.Push(screen1 = new TestScreen())); 265 AddUntilStep("wait for screen to be loaded", () => screen1.IsLoaded); 266 AddStep("exit", () => screen1.Exit()); 267 AddStep("push again fails", () => Assert.Throws<InvalidOperationException>(() => stack.Push(screen1))); 268 AddAssert("stack in valid state", () => stack.CurrentScreen == baseScreen); 269 } 270 271 [Test] 272 public void TestEventOrder() 273 { 274 List<int> order = new List<int>(); 275 276 var screen1 = new TestScreen 277 { 278 Entered = () => order.Add(1), 279 Suspended = () => order.Add(2), 280 Resumed = () => order.Add(5), 281 }; 282 283 var screen2 = new TestScreen 284 { 285 Entered = () => order.Add(3), 286 Exited = () => order.Add(4), 287 }; 288 289 AddStep("push screen1", () => stack.Push(screen1)); 290 AddUntilStep("ensure current", () => screen1.IsCurrentScreen()); 291 292 AddStep("preload screen2", () => LoadComponentAsync(screen2)); 293 AddUntilStep("wait for load", () => screen2.LoadState == LoadState.Ready); 294 295 AddStep("push screen2", () => screen1.Push(screen2)); 296 AddUntilStep("ensure current", () => screen2.IsCurrentScreen()); 297 298 AddStep("exit screen2", () => screen2.Exit()); 299 AddUntilStep("ensure exited", () => !screen2.IsCurrentScreen()); 300 301 AddStep("push screen2", () => screen1.Exit()); 302 AddUntilStep("ensure exited", () => !screen1.IsCurrentScreen()); 303 304 AddAssert("order is correct", () => order.SequenceEqual(order.OrderBy(i => i))); 305 } 306 307 [Test] 308 public void TestComeVisibleFromHidden() 309 { 310 TestScreen screen1 = null; 311 pushAndEnsureCurrent(() => screen1 = new TestScreen { Alpha = 0 }); 312 313 AddUntilStep("screen1 is visible", () => screen1.Alpha > 0); 314 315 pushAndEnsureCurrent(() => new TestScreen { Alpha = 0 }, () => screen1); 316 } 317 318 [TestCase(false, false)] 319 [TestCase(false, true)] 320 [TestCase(true, false)] 321 [TestCase(true, true)] 322 public void TestAsyncEventOrder(bool earlyExit, bool suspendImmediately) 323 { 324 TestScreenSlow screen1 = null; 325 TestScreenSlow screen2 = null; 326 List<int> order = null; 327 328 if (!suspendImmediately) 329 { 330 AddStep("override stack", () => 331 { 332 // we can't use the [SetUp] screen stack as we need to change the ctor parameters. 333 Clear(); 334 Add(stack = new ScreenStack(baseScreen = new TestScreen(id: 0)) 335 { 336 RelativeSizeAxes = Axes.Both 337 }); 338 }); 339 } 340 341 AddStep("Perform setup", () => 342 { 343 order = new List<int>(); 344 screen1 = new TestScreenSlow(1) 345 { 346 Entered = () => order.Add(1), 347 Suspended = () => order.Add(2), 348 Resumed = () => order.Add(5), 349 }; 350 screen2 = new TestScreenSlow(2) 351 { 352 Entered = () => order.Add(3), 353 Exited = () => order.Add(4), 354 }; 355 }); 356 357 AddStep("push slow", () => stack.Push(screen1)); 358 AddStep("push second slow", () => stack.Push(screen2)); 359 360 AddStep("allow load 1", () => screen1.AllowLoad.Set()); 361 362 AddUntilStep("ensure screen1 not current", () => !screen1.IsCurrentScreen()); 363 AddUntilStep("ensure screen2 not current", () => !screen2.IsCurrentScreen()); 364 365 // but the stack has a different idea of "current" 366 AddAssert("ensure screen2 is current at the stack", () => stack.CurrentScreen == screen2); 367 368 if (suspendImmediately) 369 AddUntilStep("screen1's suspending fired", () => screen1.SuspendedTo == screen2); 370 else 371 AddUntilStep("screen1's entered and suspending fired", () => screen1.EnteredFrom != null); 372 373 if (earlyExit) 374 AddStep("early exit 2", () => screen2.Exit()); 375 376 AddStep("allow load 2", () => screen2.AllowLoad.Set()); 377 378 if (earlyExit) 379 { 380 AddAssert("screen2's entered did not fire", () => screen2.EnteredFrom == null); 381 AddAssert("screen2's exited did not fire", () => screen2.ExitedTo == null); 382 } 383 else 384 { 385 AddUntilStep("ensure screen2 is current", () => screen2.IsCurrentScreen()); 386 AddAssert("screen2's entered fired", () => screen2.EnteredFrom == screen1); 387 AddStep("exit 2", () => screen2.Exit()); 388 AddUntilStep("ensure screen1 is current", () => screen1.IsCurrentScreen()); 389 AddAssert("screen2's exited fired", () => screen2.ExitedTo == screen1); 390 } 391 392 AddAssert("order is correct", () => order.SequenceEqual(order.OrderBy(i => i))); 393 } 394 395 [Test] 396 public void TestEventsNotFiredBeforeScreenLoad() 397 { 398 Screen screen1 = null; 399 bool wasLoaded = true; 400 401 pushAndEnsureCurrent(() => screen1 = new TestScreen 402 { 403 // ReSharper disable once AccessToModifiedClosure 404 Entered = () => wasLoaded &= screen1?.IsLoaded == true, 405 // ReSharper disable once AccessToModifiedClosure 406 Suspended = () => wasLoaded &= screen1?.IsLoaded == true, 407 }); 408 409 pushAndEnsureCurrent(() => new TestScreen(), () => screen1); 410 411 AddAssert("was loaded before events", () => wasLoaded); 412 } 413 414 [Test] 415 public void TestAsyncDoublePush() 416 { 417 TestScreenSlow screen1 = null; 418 TestScreenSlow screen2 = null; 419 420 AddStep("push slow", () => stack.Push(screen1 = new TestScreenSlow())); 421 // important to note we are pushing to the stack here, unlike the failing case above. 422 AddStep("push second slow", () => stack.Push(screen2 = new TestScreenSlow())); 423 424 AddAssert("base screen registered suspend", () => baseScreen.SuspendedTo == screen1); 425 426 AddAssert("screen1 is not current", () => !screen1.IsCurrentScreen()); 427 AddAssert("screen2 is not current", () => !screen2.IsCurrentScreen()); 428 429 AddAssert("screen2 is current to stack", () => stack.CurrentScreen == screen2); 430 431 AddAssert("screen1 not registered suspend", () => screen1.SuspendedTo == null); 432 AddAssert("screen2 not registered entered", () => screen2.EnteredFrom == null); 433 434 AddStep("allow load 2", () => screen2.AllowLoad.Set()); 435 436 // screen 2 won't actually be loading since the load is only triggered after screen1 is loaded. 437 AddWaitStep("wait for load", 10); 438 439 // furthermore, even though screen 2 is able to load, screen 1 has not yet so we shouldn't has received any events. 440 AddAssert("screen1 is not current", () => !screen1.IsCurrentScreen()); 441 AddAssert("screen2 is not current", () => !screen2.IsCurrentScreen()); 442 AddAssert("screen1 not registered suspend", () => screen1.SuspendedTo == null); 443 AddAssert("screen2 not registered entered", () => screen2.EnteredFrom == null); 444 445 AddStep("allow load 1", () => screen1.AllowLoad.Set()); 446 AddUntilStep("screen1 is loaded", () => screen1.LoadState == LoadState.Loaded); 447 AddUntilStep("screen2 is loaded", () => screen2.LoadState == LoadState.Loaded); 448 449 AddUntilStep("screen1 is expired", () => !screen1.IsAlive); 450 451 AddUntilStep("screen1 is not current", () => !screen1.IsCurrentScreen()); 452 AddUntilStep("screen2 is current", () => screen2.IsCurrentScreen()); 453 454 AddAssert("screen1 registered suspend", () => screen1.SuspendedTo == screen2); 455 AddAssert("screen2 registered entered", () => screen2.EnteredFrom == screen1); 456 } 457 458 [Test] 459 public void TestAsyncPushWithNonImmediateSuspend() 460 { 461 AddStep("override stack", () => 462 { 463 // we can't use the [SetUp] screen stack as we need to change the ctor parameters. 464 Clear(); 465 Add(stack = new ScreenStack(baseScreen = new TestScreen(), false) 466 { 467 RelativeSizeAxes = Axes.Both 468 }); 469 }); 470 471 TestScreenSlow screen1 = null; 472 473 AddStep("push slow", () => baseScreen.Push(screen1 = new TestScreenSlow())); 474 AddAssert("base screen not yet registered suspend", () => baseScreen.SuspendedTo == null); 475 AddAssert("ensure notcurrent", () => !screen1.IsCurrentScreen()); 476 AddStep("allow load", () => screen1.AllowLoad.Set()); 477 AddUntilStep("ensure current", () => screen1.IsCurrentScreen()); 478 AddAssert("base screen registered suspend", () => baseScreen.SuspendedTo == screen1); 479 } 480 481 [Test] 482 public void TestMakeCurrent() 483 { 484 TestScreen screen1 = null; 485 TestScreen screen2 = null; 486 TestScreen screen3 = null; 487 488 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 489 pushAndEnsureCurrent(() => screen2 = new TestScreen(), () => screen1); 490 pushAndEnsureCurrent(() => screen3 = new TestScreen(), () => screen2); 491 492 AddStep("block exit", () => screen3.Exiting = () => true); 493 AddStep("make screen 1 current", () => screen1.MakeCurrent()); 494 AddAssert("screen 3 still current", () => screen3.IsCurrentScreen()); 495 AddAssert("screen 3 exited fired", () => screen3.ExitedTo == screen2); 496 AddAssert("screen 2 resumed not fired", () => screen2.ResumedFrom == null); 497 AddAssert("screen 3 doesn't have lifetime end", () => screen3.LifetimeEnd == double.MaxValue); 498 AddAssert("screen 2 valid for resume", () => screen2.ValidForResume); 499 AddAssert("screen 1 valid for resume", () => screen1.ValidForResume); 500 501 AddStep("don't block exit", () => screen3.Exiting = () => false); 502 AddStep("make screen 1 current", () => screen1.MakeCurrent()); 503 AddAssert("screen 1 current", () => screen1.IsCurrentScreen()); 504 AddAssert("screen 3 exited fired", () => screen3.ExitedTo == screen2); 505 AddAssert("screen 2 exited fired", () => screen2.ExitedTo == screen1); 506 AddAssert("screen 1 resumed fired", () => screen1.ResumedFrom == screen2); 507 AddAssert("screen 1 doesn't have lifetime end", () => screen1.LifetimeEnd == double.MaxValue); 508 AddAssert("screen 3 has lifetime end", () => screen3.LifetimeEnd != double.MaxValue); 509 AddAssert("screen 2 is not alive", () => !screen2.AsDrawable().IsAlive); 510 } 511 512 [Test] 513 public void TestCallingExitFromBlockingExit() 514 { 515 TestScreen screen1 = null; 516 TestScreen screen2 = null; 517 int screen1ResumedCount = 0; 518 519 bool blocking = true; 520 521 pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1) 522 { 523 Resumed = () => screen1ResumedCount++ 524 }); 525 526 pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2) 527 { 528 Exiting = () => 529 { 530 if (blocking) 531 { 532 blocking = false; 533 534 // ReSharper disable once AccessToModifiedClosure 535 screen2.Exit(); 536 return true; 537 } 538 539 // this call should fail in a way the user can understand. 540 return false; 541 } 542 }, () => screen1); 543 544 AddStep("make screen 1 current", () => screen1.MakeCurrent()); 545 AddAssert("screen 1 resumed only once", () => screen1ResumedCount == 1); 546 } 547 548 [TestCase(false)] 549 [TestCase(true)] 550 public void TestMakeCurrentMidwayExitBlocking(bool validForResume) 551 { 552 TestScreen screen1 = null; 553 TestScreen screen2 = null; 554 TestScreen screen3 = null; 555 TestScreen screen4 = null; 556 int screen3ResumedCount = 0; 557 558 pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); 559 pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2), () => screen1); 560 pushAndEnsureCurrent(() => screen3 = new TestScreen(id: 3) 561 { 562 Resumed = () => screen3ResumedCount++ 563 }, () => screen2); 564 pushAndEnsureCurrent(() => screen4 = new TestScreen(id: 4), () => screen3); 565 566 AddStep("block exit screen3", () => 567 { 568 screen3.Exiting = () => true; 569 screen3.ValidForResume = validForResume; 570 }); 571 572 AddStep("make screen1 current", () => screen1.MakeCurrent()); 573 574 // check the exit worked for one level 575 AddUntilStep("screen4 is not alive", () => !screen4.AsDrawable().IsAlive); 576 AddAssert("screen4 has lifetime end", () => screen4.LifetimeEnd != double.MaxValue); 577 578 if (validForResume) 579 { 580 // check we blocked at screen 3 581 AddAssert("screen 3 valid for resume", () => screen3.ValidForResume); 582 AddAssert("screen3 is current", () => screen3.IsCurrentScreen()); 583 AddAssert("screen3 resumed", () => screen3ResumedCount == 1); 584 585 // check the ValidForResume state wasn't changed on parents 586 AddAssert("screen 1 still valid for resume", () => screen1.ValidForResume); 587 AddAssert("screen 2 still valid for resume", () => screen2.ValidForResume); 588 589 AddStep("make screen 1 current", () => screen1.MakeCurrent()); 590 591 // check blocking is consistent on a second attempt 592 AddAssert("screen3 not resumed again", () => screen3ResumedCount == 1); 593 AddAssert("screen3 is still current", () => screen3.IsCurrentScreen()); 594 595 AddStep("stop blocking exit", () => screen3.Exiting = () => false); 596 597 AddStep("make screen1 current", () => screen1.MakeCurrent()); 598 } 599 else 600 { 601 AddAssert("screen 3 not valid for resume", () => !screen3.ValidForResume); 602 AddAssert("screen3 not current", () => !screen3.IsCurrentScreen()); 603 AddAssert("screen3 did not resume", () => screen3ResumedCount == 0); 604 } 605 606 AddAssert("screen1 current", () => screen1.IsCurrentScreen()); 607 AddAssert("screen1 doesn't have lifetime end", () => screen1.LifetimeEnd == double.MaxValue); 608 AddUntilStep("screen3 is not alive", () => !screen3.AsDrawable().IsAlive); 609 } 610 611 [Test] 612 public void TestMakeCurrentUnbindOrder() 613 { 614 List<TestScreen> screens = null; 615 616 AddStep("Setup screens", () => 617 { 618 screens = new List<TestScreen>(); 619 620 for (int i = 0; i < 5; i++) 621 { 622 var screen = new TestScreen(); 623 624 screen.OnUnbindAllBindables += () => 625 { 626 if (screens.Last() != screen) 627 throw new InvalidOperationException("Unbind order was wrong"); 628 629 screens.Remove(screen); 630 }; 631 632 screens.Add(screen); 633 } 634 }); 635 636 for (int i = 0; i < 5; i++) 637 { 638 var local = i; // needed to store the correct value for our delegate 639 pushAndEnsureCurrent(() => screens[local], () => local > 0 ? screens[local - 1] : null); 640 } 641 642 AddStep("make first screen current", () => screens.First().MakeCurrent()); 643 AddUntilStep("All screens unbound in correct order", () => screens.Count == 1); 644 } 645 646 [Test] 647 public void TestScreensUnboundAndDisposedOnStackDisposal() 648 { 649 const int screen_count = 5; 650 const int exit_count = 2; 651 652 List<TestScreen> screens = null; 653 int disposedScreens = 0; 654 655 AddStep("Setup screens", () => 656 { 657 screens = new List<TestScreen>(); 658 disposedScreens = 0; 659 660 for (int i = 0; i < screen_count; i++) 661 { 662 var screen = new TestScreen(id: i); 663 664 screen.OnDispose += () => disposedScreens++; 665 666 screen.OnUnbindAllBindables += () => 667 { 668 if (screens.Last() != screen) 669 throw new InvalidOperationException("Unbind order was wrong"); 670 671 screens.Remove(screen); 672 }; 673 674 screens.Add(screen); 675 } 676 }); 677 678 for (int i = 0; i < screen_count; i++) 679 { 680 var local = i; // needed to store the correct value for our delegate 681 pushAndEnsureCurrent(() => screens[local], () => local > 0 ? screens[local - 1] : null); 682 } 683 684 AddStep("remove and dispose stack", () => 685 { 686 // 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 687 for (int i = 0; i < exit_count; i++) 688 stack.Exit(); 689 690 Remove(stack); 691 stack.Dispose(); 692 }); 693 694 AddUntilStep("All screens unbound in correct order", () => screens.Count == 0); 695 AddAssert("All screens disposed", () => disposedScreens == screen_count); 696 } 697 698 /// <summary> 699 /// Make sure that all bindables are returned before OnResuming is called for the next screen. 700 /// </summary> 701 [Test] 702 public void TestReturnBindsBeforeResume() 703 { 704 TestScreen screen1 = null, screen2 = null; 705 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 706 pushAndEnsureCurrent(() => screen2 = new TestScreen(true), () => screen1); 707 AddStep("Exit screen", () => screen2.Exit()); 708 AddUntilStep("Wait until base is current", () => screen1.IsCurrentScreen()); 709 AddAssert("Bindables have been returned by new screen", () => !screen2.DummyBindable.Disabled && !screen2.LeasedCopy.Disabled); 710 } 711 712 [Test] 713 public void TestMakeCurrentDuringLoad() 714 { 715 TestScreen screen1 = null; 716 TestScreenSlow screen2 = null; 717 718 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 719 AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); 720 721 AddStep("make screen1 current", () => screen1.MakeCurrent()); 722 AddStep("allow load of screen2", () => screen2.AllowLoad.Set()); 723 AddUntilStep("wait for screen2 to load", () => screen2.LoadState == LoadState.Ready); 724 725 AddAssert("screen1 is current screen", () => screen1.IsCurrentScreen()); 726 AddAssert("screen2 did not receive OnEntering", () => screen2.EnteredFrom == null); 727 AddAssert("screen2 did not receive OnExiting", () => screen2.ExitedTo == null); 728 } 729 730 [Test] 731 public void TestMakeCurrentDuringLoadOfMany() 732 { 733 TestScreen screen1 = null; 734 TestScreenSlow screen2 = null; 735 TestScreenSlow screen3 = null; 736 737 pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); 738 AddStep("push slow screen 2", () => stack.Push(screen2 = new TestScreenSlow(id: 2))); 739 AddStep("push slow screen 3", () => stack.Push(screen3 = new TestScreenSlow(id: 3))); 740 741 AddAssert("Screen 1 is not current", () => !screen1.IsCurrentScreen()); 742 AddStep("Make current screen 1", () => screen1.MakeCurrent()); 743 AddAssert("Screen 1 is current", () => screen1.IsCurrentScreen()); 744 745 // Allow the screens to load out of order to test whether or not screen 3 tried to load. 746 // The load should be blocked since screen 2 is already exited by MakeCurrent. 747 AddStep("allow screen 3 to load", () => screen3.AllowLoad.Set()); 748 AddStep("allow screen 2 to load", () => screen2.AllowLoad.Set()); 749 750 AddAssert("Screen 1 is current", () => screen1.IsCurrentScreen()); 751 AddAssert("Screen 2 did not load", () => !screen2.IsLoaded); 752 AddAssert("Screen 3 did not load", () => !screen3.IsLoaded); 753 } 754 755 [Test] 756 public void TestMakeCurrentOnSameScreen() 757 { 758 TestScreen screen1 = null; 759 760 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 761 AddStep("Make current the same screen", () => screen1.MakeCurrent()); 762 AddAssert("Screen 1 is current", () => screen1.IsCurrentScreen()); 763 } 764 765 [Test] 766 public void TestPushOnExiting() 767 { 768 TestScreen screen1 = null; 769 770 pushAndEnsureCurrent(() => 771 { 772 screen1 = new TestScreen(id: 1); 773 screen1.Exiting = () => 774 { 775 screen1.Push(new TestScreen(id: 2)); 776 return true; 777 }; 778 return screen1; 779 }); 780 781 AddStep("Exit screen 1", () => screen1.Exit()); 782 AddAssert("Screen 1 is not current", () => !screen1.IsCurrentScreen()); 783 AddAssert("Stack is not empty", () => stack.CurrentScreen != null); 784 } 785 786 [Test] 787 public void TestInvalidPushBlocksNonImmediateSuspend() 788 { 789 TestScreen screen1 = null; 790 TestScreenSlow screen2 = null; 791 792 AddStep("override stack", () => 793 { 794 // we can't use the [SetUp] screen stack as we need to change the ctor parameters. 795 Clear(); 796 Add(stack = new ScreenStack(baseScreen = new TestScreen(), false) 797 { 798 RelativeSizeAxes = Axes.Both 799 }); 800 }); 801 802 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 803 AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); 804 AddStep("exit slow", () => screen2.Exit()); 805 AddStep("allow load", () => screen2.AllowLoad.Set()); 806 AddUntilStep("wait for screen 2 to load", () => screen2.LoadState >= LoadState.Ready); 807 AddAssert("screen 1 did not receive suspending", () => screen1.SuspendedTo == null); 808 AddAssert("screen 1 did not receive resuming", () => screen1.ResumedFrom == null); 809 } 810 811 [Test] 812 public void TestInvalidPushDoesNotBlockImmediateSuspend() 813 { 814 TestScreen screen1 = null; 815 TestScreenSlow screen2 = null; 816 817 AddStep("override stack", () => 818 { 819 // we can't use the [SetUp] screen stack as we need to change the ctor parameters. 820 Clear(); 821 Add(stack = new ScreenStack(baseScreen = new TestScreen(), true) 822 { 823 RelativeSizeAxes = Axes.Both 824 }); 825 }); 826 827 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 828 AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); 829 AddStep("exit slow", () => screen2.Exit()); 830 AddStep("allow load", () => screen2.AllowLoad.Set()); 831 AddUntilStep("wait for screen 2 to load", () => screen2.LoadState >= LoadState.Ready); 832 AddAssert("screen 1 did receive suspending", () => screen1.SuspendedTo == screen2); 833 AddAssert("screen 1 did receive resumed", () => screen1.ResumedFrom == screen2); 834 } 835 836 /// <summary> 837 /// Push two screens and check that they only handle input when they are respectively loaded and current. 838 /// </summary> 839 [Test] 840 public void TestNonCurrentScreenDoesNotAcceptInput() 841 { 842 ManualInputManager inputManager = null; 843 844 AddStep("override stack", () => 845 { 846 // we can't use the [SetUp] screen stack as we need to change the ctor parameters. 847 Clear(); 848 849 Add(inputManager = new ManualInputManager 850 { 851 Child = stack = new ScreenStack(baseScreen = new TestScreen()) 852 { 853 RelativeSizeAxes = Axes.Both 854 } 855 }); 856 }); 857 858 TestScreen screen1 = null; 859 TestScreenSlow screen2 = null; 860 861 pushAndEnsureCurrent(() => screen1 = new TestScreen()); 862 AddStep("Click center of screen", () => clickScreen(inputManager, screen1)); 863 AddAssert("screen 1 clicked", () => screen1.ClickCount == 1); 864 865 AddStep("push slow", () => screen1.Push(screen2 = new TestScreenSlow())); 866 AddStep("Click center of screen", () => inputManager.Click(MouseButton.Left)); 867 AddAssert("screen 1 not clicked", () => screen1.ClickCount == 1); 868 AddAssert("Screen 2 not clicked", () => screen2.ClickCount == 0 && !screen2.IsLoaded); 869 870 AddStep("Allow screen to load", () => screen2.AllowLoad.Set()); 871 AddUntilStep("ensure current", () => screen2.IsCurrentScreen()); 872 AddStep("Click center of screen", () => clickScreen(inputManager, screen2)); 873 AddAssert("screen 1 not clicked", () => screen1.ClickCount == 1); 874 AddAssert("Screen 2 clicked", () => screen2.ClickCount == 1 && screen2.IsLoaded); 875 } 876 877 [Test] 878 public void TestMakeCurrentIntermediateResumes() 879 { 880 TestScreen screen1 = null; 881 TestScreen screen2 = null; 882 TestScreen screen3 = null; 883 884 pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); 885 pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2) 886 { 887 Exiting = () => true 888 }, () => screen1); 889 pushAndEnsureCurrent(() => screen3 = new TestScreen(id: 3), () => screen2); 890 891 AddStep("make screen1 current", () => screen1.MakeCurrent()); 892 893 AddAssert("screen3 exited to screen2", () => screen3.ExitedTo == screen2); 894 AddAssert("screen2 resumed from screen3", () => screen2.ResumedFrom == screen3); 895 } 896 897 [Test] 898 public void TestGetChildScreenAndGetParentScreenReturnNullWhenNotInStack() 899 { 900 TestScreen screen1 = null; 901 TestScreen screen2 = null; 902 TestScreen screen3 = null; 903 904 pushAndEnsureCurrent(() => screen1 = new TestScreen(id: 1)); 905 pushAndEnsureCurrent(() => screen2 = new TestScreen(id: 2), () => screen1); 906 pushAndEnsureCurrent(() => screen3 = new TestScreen(id: 3), () => screen2); 907 908 AddStep("exit from screen 3", () => screen3.Exit()); 909 AddAssert("screen 3 parent is null", () => screen3.GetParentScreen() == null); 910 AddAssert("screen 3 child is null", () => screen3.GetChildScreen() == null); 911 } 912 913 /// <summary> 914 /// Ensure that an intermediary screen doesn't block and doesn't attempt to fire events when not loaded. 915 /// </summary> 916 [Test] 917 public void TestMakeCurrentWhileScreensStillLoading() 918 { 919 TestScreen root = null; 920 921 pushAndEnsureCurrent(() => root = new TestScreen(id: 1)); 922 AddStep("push slow", () => stack.Push(new TestScreenSlow { Exiting = () => true })); 923 AddStep("push second slow", () => stack.Push(new TestScreenSlow())); 924 925 AddStep("make screen1 current", () => root.MakeCurrent()); 926 } 927 928 private void clickScreen(ManualInputManager inputManager, TestScreen screen) 929 { 930 inputManager.MoveMouseTo(screen); 931 inputManager.Click(MouseButton.Left); 932 } 933 934 private void pushAndEnsureCurrent(Func<IScreen> screenCtor, Func<IScreen> target = null) 935 { 936 IScreen screen = null; 937 AddStep("push", () => (target?.Invoke() ?? baseScreen).Push(screen = screenCtor())); 938 AddUntilStep("ensure current", () => screen.IsCurrentScreen()); 939 } 940 941 private class TestScreenSlow : TestScreen 942 { 943 public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); 944 945 public TestScreenSlow(int? id = null) 946 : base(false, id) 947 { 948 } 949 950 [BackgroundDependencyLoader] 951 private void load() 952 { 953 if (!AllowLoad.Wait(TimeSpan.FromSeconds(10))) 954 throw new TimeoutException(); 955 } 956 } 957 958 private class TestScreen : Screen 959 { 960 public Func<bool> Exiting; 961 962 public Action Entered; 963 public Action Suspended; 964 public Action Resumed; 965 public Action Exited; 966 967 public IScreen EnteredFrom; 968 public IScreen ExitedTo; 969 970 public IScreen SuspendedTo; 971 public IScreen ResumedFrom; 972 973 public static int Sequence; 974 private BasicButton popButton; 975 976 private const int transition_time = 500; 977 978 public bool EagerFocus; 979 980 public int ClickCount { get; private set; } 981 982 public override bool RequestsFocus => EagerFocus; 983 984 public override bool AcceptsFocus => EagerFocus; 985 986 public override bool HandleNonPositionalInput => true; 987 988 public LeasedBindable<bool> LeasedCopy; 989 990 public readonly Bindable<bool> DummyBindable = new Bindable<bool>(); 991 992 private readonly bool shouldTakeOutLease; 993 994 public TestScreen(bool shouldTakeOutLease = false, int? id = null) 995 { 996 this.shouldTakeOutLease = shouldTakeOutLease; 997 998 if (id != null) 999 Name = id.ToString(); 1000 } 1001 1002 [BackgroundDependencyLoader] 1003 private void load() 1004 { 1005 InternalChildren = new Drawable[] 1006 { 1007 new Box 1008 { 1009 RelativeSizeAxes = Axes.Both, 1010 Size = new Vector2(1), 1011 Anchor = Anchor.Centre, 1012 Origin = Anchor.Centre, 1013 Colour = new Color4( 1014 Math.Max(0.5f, RNG.NextSingle()), 1015 Math.Max(0.5f, RNG.NextSingle()), 1016 Math.Max(0.5f, RNG.NextSingle()), 1017 1), 1018 }, 1019 new SpriteText 1020 { 1021 Text = $@"Screen {Sequence++}", 1022 Anchor = Anchor.Centre, 1023 Origin = Anchor.Centre, 1024 Font = new FontUsage(size: 50) 1025 }, 1026 popButton = new BasicButton 1027 { 1028 Text = @"Pop", 1029 RelativeSizeAxes = Axes.Both, 1030 Size = new Vector2(0.1f), 1031 Anchor = Anchor.TopLeft, 1032 Origin = Anchor.TopLeft, 1033 BackgroundColour = Color4.Red, 1034 Alpha = 0, 1035 Action = this.Exit 1036 }, 1037 new BasicButton 1038 { 1039 Text = @"Push", 1040 RelativeSizeAxes = Axes.Both, 1041 Size = new Vector2(0.1f), 1042 Anchor = Anchor.TopRight, 1043 Origin = Anchor.TopRight, 1044 BackgroundColour = Color4.YellowGreen, 1045 Action = delegate 1046 { 1047 this.Push(new TestScreen 1048 { 1049 Anchor = Anchor.Centre, 1050 Origin = Anchor.Centre, 1051 }); 1052 } 1053 } 1054 }; 1055 1056 BorderColour = Color4.Red; 1057 Masking = true; 1058 } 1059 1060 public override string ToString() => Name; 1061 1062 protected override void OnFocus(FocusEvent e) 1063 { 1064 base.OnFocus(e); 1065 BorderThickness = 10; 1066 } 1067 1068 protected override void OnFocusLost(FocusLostEvent e) 1069 { 1070 base.OnFocusLost(e); 1071 BorderThickness = 0; 1072 } 1073 1074 public override void OnEntering(IScreen last) 1075 { 1076 attemptTransformMutation(); 1077 1078 EnteredFrom = last; 1079 Entered?.Invoke(); 1080 1081 if (shouldTakeOutLease) 1082 { 1083 DummyBindable.BindTo(((TestScreen)last).DummyBindable); 1084 LeasedCopy = DummyBindable.BeginLease(true); 1085 } 1086 1087 base.OnEntering(last); 1088 1089 if (last != null) 1090 { 1091 //only show the pop button if we are entered form another screen. 1092 popButton.Alpha = 1; 1093 } 1094 1095 this.MoveTo(new Vector2(0, -DrawSize.Y)); 1096 this.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); 1097 this.FadeIn(1000); 1098 } 1099 1100 public override bool OnExiting(IScreen next) 1101 { 1102 attemptTransformMutation(); 1103 1104 ExitedTo = next; 1105 Exited?.Invoke(); 1106 1107 if (Exiting?.Invoke() == true) 1108 return true; 1109 1110 this.MoveTo(new Vector2(0, -DrawSize.Y), transition_time, Easing.OutQuint); 1111 return base.OnExiting(next); 1112 } 1113 1114 public override void OnSuspending(IScreen next) 1115 { 1116 attemptTransformMutation(); 1117 1118 SuspendedTo = next; 1119 Suspended?.Invoke(); 1120 1121 base.OnSuspending(next); 1122 this.MoveTo(new Vector2(0, DrawSize.Y), transition_time, Easing.OutQuint); 1123 } 1124 1125 public override void OnResuming(IScreen last) 1126 { 1127 attemptTransformMutation(); 1128 1129 ResumedFrom = last; 1130 Resumed?.Invoke(); 1131 1132 base.OnResuming(last); 1133 this.MoveTo(Vector2.Zero, transition_time, Easing.OutQuint); 1134 } 1135 1136 private void attemptTransformMutation() 1137 { 1138 // all callbacks should be in a state where transforms are able to be run. 1139 this.FadeIn(); 1140 } 1141 1142 protected override bool OnClick(ClickEvent e) 1143 { 1144 ClickCount++; 1145 return base.OnClick(e); 1146 } 1147 } 1148 } 1149}