ApplicationImplTests.cs 18 KB

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