A game framework written with osu! in mind.
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}