ApplicationImplTests.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using Moq;
  4. namespace UnitTests.ApplicationTests;
  5. public class ApplicationImplTests
  6. {
  7. /// <summary>
  8. /// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked.
  9. /// </summary>
  10. private IApplication? NewMockedApplicationImpl ()
  11. {
  12. Mock<INetInput> netInput = new ();
  13. SetupRunInputMockMethodToBlock (netInput);
  14. Mock<IComponentFactory<ConsoleKeyInfo>> m = new ();
  15. m.Setup (f => f.CreateInput ()).Returns (netInput.Object);
  16. m.Setup (f => f.CreateInputProcessor (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ())).Returns (Mock.Of<IInputProcessor> ());
  17. Mock<IOutput> consoleOutput = new ();
  18. var size = new Size (80, 25);
  19. consoleOutput.Setup (o => o.SetSize (It.IsAny<int> (), It.IsAny<int> ()))
  20. .Callback<int, int> ((w, h) => size = new (w, h));
  21. consoleOutput.Setup (o => o.GetSize ()).Returns (() => size);
  22. m.Setup (f => f.CreateOutput ()).Returns (consoleOutput.Object);
  23. m.Setup (f => f.CreateSizeMonitor (It.IsAny<IOutput> (), It.IsAny<IOutputBuffer> ())).Returns (Mock.Of<ISizeMonitor> ());
  24. // TODO: Move these tests to Parallelizable tests
  25. ApplicationImpl.SetInstance(new ApplicationImpl (m.Object));
  26. return ApplicationImpl.Instance;
  27. }
  28. [Fact]
  29. public void Init_CreatesKeybindings ()
  30. {
  31. IApplication? app = NewMockedApplicationImpl ();
  32. app?.Keyboard.KeyBindings.Clear ();
  33. Assert.Empty (app?.Keyboard?.KeyBindings.GetBindings ()!);
  34. app?.Init ("fake");
  35. Assert.NotEmpty (app?.Keyboard?.KeyBindings.GetBindings ()!);
  36. app?.Shutdown ();
  37. }
  38. private void SetupRunInputMockMethodToBlock (Mock<INetInput> netInput)
  39. {
  40. netInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
  41. .Callback<CancellationToken> (token =>
  42. {
  43. // Simulate an infinite loop that checks for cancellation
  44. while (!token.IsCancellationRequested)
  45. {
  46. // Perform the action that should repeat in the loop
  47. // This could be some mock behavior or just an empty loop depending on the context
  48. }
  49. })
  50. .Verifiable (Times.Once);
  51. }
  52. [Fact]
  53. public void NoInitThrowOnRun ()
  54. {
  55. IApplication? app = NewMockedApplicationImpl ();
  56. var ex = Assert.Throws<NotInitializedException> (() => app?.Run (new Window ()));
  57. Assert.Equal ("Run cannot be accessed before Initialization", ex.Message);
  58. app?.Shutdown ();
  59. }
  60. [Fact]
  61. public void InitRunShutdown_Top_Set_To_Null_After_Shutdown ()
  62. {
  63. IApplication? app = NewMockedApplicationImpl ();
  64. app?.Init ("fake");
  65. object? timeoutToken = app?.AddTimeout (
  66. TimeSpan.FromMilliseconds (150),
  67. () =>
  68. {
  69. if (app.TopRunnable is { })
  70. {
  71. app.RequestStop ();
  72. return false;
  73. }
  74. return false;
  75. }
  76. );
  77. Assert.Null (app?.TopRunnable);
  78. // Blocks until the timeout call is hit
  79. app?.Run (new Window ());
  80. // We returned false above, so we should not have to remove the timeout
  81. Assert.False (app?.RemoveTimeout (timeoutToken!));
  82. Assert.NotNull (app?.TopRunnable);
  83. app.TopRunnable?.Dispose ();
  84. app.Shutdown ();
  85. Assert.Null (app.TopRunnable);
  86. }
  87. [Fact]
  88. public void InitRunShutdown_Running_Set_To_False ()
  89. {
  90. IApplication app = NewMockedApplicationImpl ()!;
  91. app.Init ("fake");
  92. Toplevel top = new Window
  93. {
  94. Title = "InitRunShutdown_Running_Set_To_False"
  95. };
  96. object timeoutToken = app.AddTimeout (
  97. TimeSpan.FromMilliseconds (150),
  98. () =>
  99. {
  100. Assert.True (top!.Running);
  101. if (app.TopRunnable != null)
  102. {
  103. app.RequestStop ();
  104. return false;
  105. }
  106. return false;
  107. }
  108. );
  109. Assert.False (top!.Running);
  110. // Blocks until the timeout call is hit
  111. app.Run (top);
  112. // We returned false above, so we should not have to remove the timeout
  113. Assert.False (app.RemoveTimeout (timeoutToken));
  114. Assert.False (top!.Running);
  115. // BUGBUG: Shutdown sets Top to null, not End.
  116. //Assert.Null (Application.TopRunnable);
  117. app.TopRunnable?.Dispose ();
  118. app.Shutdown ();
  119. }
  120. [Fact]
  121. public void InitRunShutdown_StopAfterFirstIteration_Stops ()
  122. {
  123. IApplication app = NewMockedApplicationImpl ()!;
  124. Assert.Null (app.TopRunnable);
  125. Assert.Null (app.Driver);
  126. app.Init ("fake");
  127. Toplevel top = new Window ();
  128. app.TopRunnable = top;
  129. var closedCount = 0;
  130. top.Closed
  131. += (_, a) => { closedCount++; };
  132. var unloadedCount = 0;
  133. top.Unloaded
  134. += (_, a) => { unloadedCount++; };
  135. object timeoutToken = app.AddTimeout (
  136. TimeSpan.FromMilliseconds (150),
  137. () =>
  138. {
  139. Assert.Fail (@"Didn't stop after first iteration.");
  140. return false;
  141. }
  142. );
  143. Assert.Equal (0, closedCount);
  144. Assert.Equal (0, unloadedCount);
  145. app.StopAfterFirstIteration = true;
  146. app.Run (top);
  147. Assert.Equal (1, closedCount);
  148. Assert.Equal (1, unloadedCount);
  149. app.TopRunnable?.Dispose ();
  150. app.Shutdown ();
  151. Assert.Equal (1, closedCount);
  152. Assert.Equal (1, unloadedCount);
  153. }
  154. [Fact]
  155. public void InitRunShutdown_End_Is_Called ()
  156. {
  157. IApplication app = NewMockedApplicationImpl ()!;
  158. Assert.Null (app.TopRunnable);
  159. Assert.Null (app.Driver);
  160. app.Init ("fake");
  161. Toplevel top = new Window ();
  162. // BUGBUG: Both Closed and Unloaded are called from End; what's the difference?
  163. var closedCount = 0;
  164. top.Closed
  165. += (_, a) => { closedCount++; };
  166. var unloadedCount = 0;
  167. top.Unloaded
  168. += (_, a) => { unloadedCount++; };
  169. object timeoutToken = app.AddTimeout (
  170. TimeSpan.FromMilliseconds (150),
  171. () =>
  172. {
  173. Assert.True (top!.Running);
  174. if (app.TopRunnable != null)
  175. {
  176. app.RequestStop ();
  177. return false;
  178. }
  179. return false;
  180. }
  181. );
  182. Assert.Equal (0, closedCount);
  183. Assert.Equal (0, unloadedCount);
  184. // Blocks until the timeout call is hit
  185. app.Run (top);
  186. Assert.Equal (1, closedCount);
  187. Assert.Equal (1, unloadedCount);
  188. // We returned false above, so we should not have to remove the timeout
  189. Assert.False (app.RemoveTimeout (timeoutToken));
  190. app.TopRunnable?.Dispose ();
  191. app.Shutdown ();
  192. Assert.Equal (1, closedCount);
  193. Assert.Equal (1, unloadedCount);
  194. }
  195. [Fact]
  196. public void InitRunShutdown_QuitKey_Quits ()
  197. {
  198. IApplication app = NewMockedApplicationImpl ()!;
  199. app.Init ("fake");
  200. Toplevel top = new Window
  201. {
  202. Title = "InitRunShutdown_QuitKey_Quits"
  203. };
  204. object timeoutToken = app.AddTimeout (
  205. TimeSpan.FromMilliseconds (150),
  206. () =>
  207. {
  208. Assert.True (top!.Running);
  209. if (app.TopRunnable != null)
  210. {
  211. app.Keyboard.RaiseKeyDownEvent (app.Keyboard.QuitKey);
  212. }
  213. return false;
  214. }
  215. );
  216. Assert.False (top!.Running);
  217. // Blocks until the timeout call is hit
  218. app.Run (top);
  219. // We returned false above, so we should not have to remove the timeout
  220. Assert.False (app.RemoveTimeout (timeoutToken));
  221. Assert.False (top!.Running);
  222. Assert.NotNull (app.TopRunnable);
  223. top.Dispose ();
  224. app.Shutdown ();
  225. Assert.Null (app.TopRunnable);
  226. }
  227. [Fact]
  228. public void InitRunShutdown_Generic_IdleForExit ()
  229. {
  230. IApplication app = NewMockedApplicationImpl ()!;
  231. app.Init ("fake");
  232. app.AddTimeout (TimeSpan.Zero, () => IdleExit (app));
  233. Assert.Null (app.TopRunnable);
  234. // Blocks until the timeout call is hit
  235. app.Run<Window> ();
  236. Assert.NotNull (app.TopRunnable);
  237. app.TopRunnable?.Dispose ();
  238. app.Shutdown ();
  239. Assert.Null (app.TopRunnable);
  240. }
  241. [Fact]
  242. public void Shutdown_Closing_Closed_Raised ()
  243. {
  244. IApplication app = NewMockedApplicationImpl ()!;
  245. app.Init ("fake");
  246. var closing = 0;
  247. var closed = 0;
  248. var t = new Toplevel ();
  249. t.Closing
  250. += (_, a) =>
  251. {
  252. // Cancel the first time
  253. if (closing == 0)
  254. {
  255. a.Cancel = true;
  256. }
  257. closing++;
  258. Assert.Same (t, a.RequestingTop);
  259. };
  260. t.Closed
  261. += (_, a) =>
  262. {
  263. closed++;
  264. Assert.Same (t, a.Toplevel);
  265. };
  266. app.AddTimeout (TimeSpan.Zero, () => IdleExit (app));
  267. // Blocks until the timeout call is hit
  268. app.Run (t);
  269. app.TopRunnable?.Dispose ();
  270. app.Shutdown ();
  271. Assert.Equal (2, closing);
  272. Assert.Equal (1, closed);
  273. }
  274. private bool IdleExit (IApplication app)
  275. {
  276. if (app.TopRunnable != null)
  277. {
  278. app.RequestStop ();
  279. return true;
  280. }
  281. return true;
  282. }
  283. [Fact]
  284. public void Open_Calls_ContinueWith_On_UIThread ()
  285. {
  286. IApplication app = NewMockedApplicationImpl ()!;
  287. app.Init ("fake");
  288. var b = new Button ();
  289. var result = false;
  290. b.Accepting +=
  291. (_, _) =>
  292. {
  293. Task.Run (() => { Task.Delay (300).Wait (); })
  294. .ContinueWith (
  295. (t, _) =>
  296. {
  297. // no longer loading
  298. app.Invoke (() =>
  299. {
  300. result = true;
  301. app.RequestStop ();
  302. });
  303. },
  304. TaskScheduler.FromCurrentSynchronizationContext ());
  305. };
  306. app.AddTimeout (
  307. TimeSpan.FromMilliseconds (150),
  308. () =>
  309. {
  310. // Run asynchronous logic inside Task.Run
  311. if (app.TopRunnable != null)
  312. {
  313. b.NewKeyDownEvent (Key.Enter);
  314. b.NewKeyUpEvent (Key.Enter);
  315. }
  316. return false;
  317. });
  318. Assert.Null (app.TopRunnable);
  319. var w = new Window
  320. {
  321. Title = "Open_CallsContinueWithOnUIThread"
  322. };
  323. w.Add (b);
  324. // Blocks until the timeout call is hit
  325. app.Run (w);
  326. Assert.NotNull (app.TopRunnable);
  327. app.TopRunnable?.Dispose ();
  328. app.Shutdown ();
  329. Assert.Null (app.TopRunnable);
  330. Assert.True (result);
  331. }
  332. [Fact]
  333. public void ApplicationImpl_UsesInstanceFields_NotStaticReferences ()
  334. {
  335. // This test verifies that ApplicationImpl uses instance fields instead of static Application references
  336. IApplication v2 = NewMockedApplicationImpl ()!;
  337. // Before Init, all fields should be null/default
  338. Assert.Null (v2.Driver);
  339. Assert.False (v2.Initialized);
  340. //Assert.Null (v2.Popover);
  341. //Assert.Null (v2.Navigation);
  342. Assert.Null (v2.TopRunnable);
  343. Assert.Empty (v2.SessionStack);
  344. // Init should populate instance fields
  345. v2.Init ("fake");
  346. // After Init, Driver, Navigation, and Popover should be populated
  347. Assert.NotNull (v2.Driver);
  348. Assert.True (v2.Initialized);
  349. Assert.NotNull (v2.Popover);
  350. Assert.NotNull (v2.Navigation);
  351. Assert.Null (v2.TopRunnable); // Top is still null until Run
  352. // Shutdown should clean up instance fields
  353. v2.Shutdown ();
  354. Assert.Null (v2.Driver);
  355. Assert.False (v2.Initialized);
  356. //Assert.Null (v2.Popover);
  357. //Assert.Null (v2.Navigation);
  358. Assert.Null (v2.TopRunnable);
  359. Assert.Empty (v2.SessionStack);
  360. }
  361. }