ApplicationImplTests.cs 15 KB

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