ApplicationImplTests.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using Moq;
  4. using TerminalGuiFluentTesting;
  5. namespace UnitTests.ApplicationTests;
  6. public class ApplicationImplTests
  7. {
  8. /// <summary>
  9. /// Crates a new ApplicationImpl instance for testing. The input, output, and size monitor components are mocked.
  10. /// </summary>
  11. private ApplicationImpl NewMockedApplicationImpl ()
  12. {
  13. Mock<INetInput> netInput = new ();
  14. SetupRunInputMockMethodToBlock (netInput);
  15. Mock<IComponentFactory<ConsoleKeyInfo>> m = new ();
  16. m.Setup (f => f.CreateInput ()).Returns (netInput.Object);
  17. m.Setup (f => f.CreateInputProcessor (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ())).Returns (Mock.Of<IInputProcessor> ());
  18. Mock<IOutput> consoleOutput = new ();
  19. var size = new Size (80, 25);
  20. consoleOutput.Setup (o => o.SetSize (It.IsAny<int> (), It.IsAny<int> ()))
  21. .Callback<int, int> ((w, h) => size = new Size (w, h));
  22. consoleOutput.Setup (o => o.GetSize ()).Returns (() => size);
  23. m.Setup (f => f.CreateOutput ()).Returns (consoleOutput.Object);
  24. m.Setup (f => f.CreateSizeMonitor (It.IsAny<IOutput> (), It.IsAny<IOutputBuffer> ())).Returns (Mock.Of<ISizeMonitor> ());
  25. return new (m.Object);
  26. }
  27. [Fact]
  28. public void Init_CreatesKeybindings ()
  29. {
  30. IApplication orig = ApplicationImpl.Instance;
  31. ApplicationImpl v2 = NewMockedApplicationImpl ();
  32. ApplicationImpl.ChangeInstance (v2);
  33. Application.KeyBindings.Clear ();
  34. Assert.Empty (Application.KeyBindings.GetBindings ());
  35. v2.Init (null, "fake");
  36. Assert.NotEmpty (Application.KeyBindings.GetBindings ());
  37. v2.Shutdown ();
  38. ApplicationImpl.ChangeInstance (orig);
  39. }
  40. /*
  41. [Fact]
  42. public void Init_ExplicitlyRequestWin ()
  43. {
  44. var orig = ApplicationImpl.Instance;
  45. Assert.Null (Application.Driver);
  46. var netInput = new Mock<INetInput> (MockBehavior.Strict);
  47. var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  48. var winInput = new Mock<IWindowsInput> (MockBehavior.Strict);
  49. var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  50. winInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<WindowsConsole.InputRecord>> ()))
  51. .Verifiable (Times.Once);
  52. SetupRunInputMockMethodToBlock (winInput);
  53. winInput.Setup (i => i.Dispose ())
  54. .Verifiable (Times.Once);
  55. winOutput.Setup (i => i.Dispose ())
  56. .Verifiable (Times.Once);
  57. var v2 = new ApplicationV2 (
  58. () => netInput.Object,
  59. () => netOutput.Object,
  60. () => winInput.Object,
  61. () => winOutput.Object);
  62. ApplicationImpl.ChangeInstance (v2);
  63. Assert.Null (Application.Driver);
  64. v2.Init (null, "v2win");
  65. Assert.NotNull (Application.Driver);
  66. var type = Application.Driver.GetType ();
  67. Assert.True (type.IsGenericType);
  68. Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
  69. v2.Shutdown ();
  70. Assert.Null (Application.Driver);
  71. winInput.VerifyAll ();
  72. ApplicationImpl.ChangeInstance (orig);
  73. }
  74. [Fact]
  75. public void Init_ExplicitlyRequestNet ()
  76. {
  77. var orig = ApplicationImpl.Instance;
  78. var netInput = new Mock<INetInput> (MockBehavior.Strict);
  79. var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  80. var winInput = new Mock<IWindowsInput> (MockBehavior.Strict);
  81. var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict);
  82. netInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ()))
  83. .Verifiable (Times.Once);
  84. SetupRunInputMockMethodToBlock (netInput);
  85. netInput.Setup (i => i.Dispose ())
  86. .Verifiable (Times.Once);
  87. netOutput.Setup (i => i.Dispose ())
  88. .Verifiable (Times.Once);
  89. var v2 = new ApplicationV2 (
  90. () => netInput.Object,
  91. () => netOutput.Object,
  92. () => winInput.Object,
  93. () => winOutput.Object);
  94. ApplicationImpl.ChangeInstance (v2);
  95. Assert.Null (Application.Driver);
  96. v2.Init (null, "v2net");
  97. Assert.NotNull (Application.Driver);
  98. var type = Application.Driver.GetType ();
  99. Assert.True (type.IsGenericType);
  100. Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>));
  101. v2.Shutdown ();
  102. Assert.Null (Application.Driver);
  103. netInput.VerifyAll ();
  104. ApplicationImpl.ChangeInstance (orig);
  105. }
  106. */
  107. private void SetupRunInputMockMethodToBlock (Mock<IInput<WindowsConsole.InputRecord>> winInput)
  108. {
  109. winInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
  110. .Callback<CancellationToken> (token =>
  111. {
  112. // Simulate an infinite loop that checks for cancellation
  113. while (!token.IsCancellationRequested)
  114. {
  115. // Perform the action that should repeat in the loop
  116. // This could be some mock behavior or just an empty loop depending on the context
  117. }
  118. })
  119. .Verifiable (Times.Once);
  120. }
  121. private void SetupRunInputMockMethodToBlock (Mock<INetInput> netInput)
  122. {
  123. netInput.Setup (r => r.Run (It.IsAny<CancellationToken> ()))
  124. .Callback<CancellationToken> (token =>
  125. {
  126. // Simulate an infinite loop that checks for cancellation
  127. while (!token.IsCancellationRequested)
  128. {
  129. // Perform the action that should repeat in the loop
  130. // This could be some mock behavior or just an empty loop depending on the context
  131. }
  132. })
  133. .Verifiable (Times.Once);
  134. }
  135. [Fact]
  136. public void NoInitThrowOnRun ()
  137. {
  138. IApplication orig = ApplicationImpl.Instance;
  139. Assert.Null (Application.Driver);
  140. ApplicationImpl app = NewMockedApplicationImpl ();
  141. ApplicationImpl.ChangeInstance (app);
  142. var ex = Assert.Throws<NotInitializedException> (() => app.Run (new Window ()));
  143. Assert.Equal ("Run cannot be accessed before Initialization", ex.Message);
  144. app.Shutdown ();
  145. ApplicationImpl.ChangeInstance (orig);
  146. }
  147. [Fact]
  148. public void InitRunShutdown_Top_Set_To_Null_After_Shutdown ()
  149. {
  150. IApplication orig = ApplicationImpl.Instance;
  151. ApplicationImpl v2 = NewMockedApplicationImpl ();
  152. ApplicationImpl.ChangeInstance (v2);
  153. v2.Init (null, "fake");
  154. object timeoutToken = v2.AddTimeout (
  155. TimeSpan.FromMilliseconds (150),
  156. () =>
  157. {
  158. if (Application.Current != null)
  159. {
  160. Application.RequestStop ();
  161. return false;
  162. }
  163. return false;
  164. }
  165. );
  166. Assert.Null (Application.Current);
  167. // Blocks until the timeout call is hit
  168. v2.Run (new Window ());
  169. // We returned false above, so we should not have to remove the timeout
  170. Assert.False (v2.RemoveTimeout (timeoutToken));
  171. Assert.NotNull (Application.Current);
  172. Application.Current?.Dispose ();
  173. v2.Shutdown ();
  174. Assert.Null (Application.Current);
  175. ApplicationImpl.ChangeInstance (orig);
  176. }
  177. [Fact]
  178. public void InitRunShutdown_Running_Set_To_False ()
  179. {
  180. IApplication orig = ApplicationImpl.Instance;
  181. ApplicationImpl v2 = NewMockedApplicationImpl ();
  182. ApplicationImpl.ChangeInstance (v2);
  183. v2.Init (null, "fake");
  184. Toplevel top = new Window
  185. {
  186. Title = "InitRunShutdown_Running_Set_To_False"
  187. };
  188. object timeoutToken = v2.AddTimeout (
  189. TimeSpan.FromMilliseconds (150),
  190. () =>
  191. {
  192. Assert.True (top!.Running);
  193. if (Application.Current != null)
  194. {
  195. Application.RequestStop ();
  196. return false;
  197. }
  198. return false;
  199. }
  200. );
  201. Assert.False (top!.Running);
  202. // Blocks until the timeout call is hit
  203. v2.Run (top);
  204. // We returned false above, so we should not have to remove the timeout
  205. Assert.False (v2.RemoveTimeout (timeoutToken));
  206. Assert.False (top!.Running);
  207. // BUGBUG: Shutdown sets Top to null, not End.
  208. //Assert.Null (Application.Current);
  209. Application.Current?.Dispose ();
  210. v2.Shutdown ();
  211. ApplicationImpl.ChangeInstance (orig);
  212. }
  213. [Fact]
  214. public void InitRunShutdown_StopAfterFirstIteration_Stops ()
  215. {
  216. IApplication orig = ApplicationImpl.Instance;
  217. ApplicationImpl v2 = NewMockedApplicationImpl ();
  218. ApplicationImpl.ChangeInstance (v2);
  219. Assert.Null (Application.Current);
  220. Assert.Null (Application.Driver);
  221. v2.Init (null, "fake");
  222. Toplevel top = new Window ();
  223. var closedCount = 0;
  224. top.Closed
  225. += (_, a) => { closedCount++; };
  226. var unloadedCount = 0;
  227. top.Unloaded
  228. += (_, a) => { unloadedCount++; };
  229. object timeoutToken = v2.AddTimeout (
  230. TimeSpan.FromMilliseconds (150),
  231. () =>
  232. {
  233. Assert.Fail (@"Didn't stop after first iteration.");
  234. return false;
  235. }
  236. );
  237. Assert.Equal (0, closedCount);
  238. Assert.Equal (0, unloadedCount);
  239. v2.StopAfterFirstIteration = true;
  240. v2.Run (top);
  241. Assert.Equal (1, closedCount);
  242. Assert.Equal (1, unloadedCount);
  243. Application.Current?.Dispose ();
  244. v2.Shutdown ();
  245. Assert.Equal (1, closedCount);
  246. Assert.Equal (1, unloadedCount);
  247. ApplicationImpl.ChangeInstance (orig);
  248. }
  249. [Fact]
  250. public void InitRunShutdown_End_Is_Called ()
  251. {
  252. IApplication orig = ApplicationImpl.Instance;
  253. ApplicationImpl v2 = NewMockedApplicationImpl ();
  254. ApplicationImpl.ChangeInstance (v2);
  255. Assert.Null (Application.Current);
  256. Assert.Null (Application.Driver);
  257. v2.Init (null, "fake");
  258. Toplevel top = new Window ();
  259. // BUGBUG: Both Closed and Unloaded are called from End; what's the difference?
  260. var closedCount = 0;
  261. top.Closed
  262. += (_, a) => { closedCount++; };
  263. var unloadedCount = 0;
  264. top.Unloaded
  265. += (_, a) => { unloadedCount++; };
  266. object timeoutToken = v2.AddTimeout (
  267. TimeSpan.FromMilliseconds (150),
  268. () =>
  269. {
  270. Assert.True (top!.Running);
  271. if (Application.Current != null)
  272. {
  273. Application.RequestStop ();
  274. return false;
  275. }
  276. return false;
  277. }
  278. );
  279. Assert.Equal (0, closedCount);
  280. Assert.Equal (0, unloadedCount);
  281. // Blocks until the timeout call is hit
  282. v2.Run (top);
  283. Assert.Equal (1, closedCount);
  284. Assert.Equal (1, unloadedCount);
  285. // We returned false above, so we should not have to remove the timeout
  286. Assert.False (v2.RemoveTimeout (timeoutToken));
  287. Application.Current?.Dispose ();
  288. v2.Shutdown ();
  289. Assert.Equal (1, closedCount);
  290. Assert.Equal (1, unloadedCount);
  291. ApplicationImpl.ChangeInstance (orig);
  292. }
  293. [Fact]
  294. public void InitRunShutdown_QuitKey_Quits ()
  295. {
  296. IApplication orig = ApplicationImpl.Instance;
  297. ApplicationImpl v2 = NewMockedApplicationImpl ();
  298. ApplicationImpl.ChangeInstance (v2);
  299. v2.Init (null, "fake");
  300. Toplevel top = new Window
  301. {
  302. Title = "InitRunShutdown_QuitKey_Quits"
  303. };
  304. object timeoutToken = v2.AddTimeout (
  305. TimeSpan.FromMilliseconds (150),
  306. () =>
  307. {
  308. Assert.True (top!.Running);
  309. if (Application.Current != null)
  310. {
  311. Application.RaiseKeyDownEvent (Application.QuitKey);
  312. }
  313. return false;
  314. }
  315. );
  316. Assert.False (top!.Running);
  317. // Blocks until the timeout call is hit
  318. v2.Run (top);
  319. // We returned false above, so we should not have to remove the timeout
  320. Assert.False (v2.RemoveTimeout (timeoutToken));
  321. Assert.False (top!.Running);
  322. Assert.NotNull (Application.Current);
  323. top.Dispose ();
  324. v2.Shutdown ();
  325. Assert.Null (Application.Current);
  326. ApplicationImpl.ChangeInstance (orig);
  327. }
  328. [Fact]
  329. public void InitRunShutdown_Generic_IdleForExit ()
  330. {
  331. IApplication orig = ApplicationImpl.Instance;
  332. ApplicationImpl v2 = NewMockedApplicationImpl ();
  333. ApplicationImpl.ChangeInstance (v2);
  334. v2.Init (null, "fake");
  335. v2.AddTimeout (TimeSpan.Zero, IdleExit);
  336. Assert.Null (Application.Current);
  337. // Blocks until the timeout call is hit
  338. v2.Run<Window> ();
  339. Assert.NotNull (Application.Current);
  340. Application.Current?.Dispose ();
  341. v2.Shutdown ();
  342. Assert.Null (Application.Current);
  343. ApplicationImpl.ChangeInstance (orig);
  344. }
  345. [Fact]
  346. public void Shutdown_Closing_Closed_Raised ()
  347. {
  348. IApplication orig = ApplicationImpl.Instance;
  349. ApplicationImpl v2 = NewMockedApplicationImpl ();
  350. ApplicationImpl.ChangeInstance (v2);
  351. v2.Init (null, "fake");
  352. var closing = 0;
  353. var closed = 0;
  354. var t = new Toplevel ();
  355. t.Closing
  356. += (_, a) =>
  357. {
  358. // Cancel the first time
  359. if (closing == 0)
  360. {
  361. a.Cancel = true;
  362. }
  363. closing++;
  364. Assert.Same (t, a.RequestingTop);
  365. };
  366. t.Closed
  367. += (_, a) =>
  368. {
  369. closed++;
  370. Assert.Same (t, a.Toplevel);
  371. };
  372. v2.AddTimeout (TimeSpan.Zero, IdleExit);
  373. // Blocks until the timeout call is hit
  374. v2.Run (t);
  375. Application.Current?.Dispose ();
  376. v2.Shutdown ();
  377. ApplicationImpl.ChangeInstance (orig);
  378. Assert.Equal (2, closing);
  379. Assert.Equal (1, closed);
  380. }
  381. private bool IdleExit ()
  382. {
  383. if (Application.Current != null)
  384. {
  385. Application.RequestStop ();
  386. return true;
  387. }
  388. return true;
  389. }
  390. /*
  391. [Fact]
  392. public void Shutdown_Called_Repeatedly_DoNotDuplicateDisposeOutput ()
  393. {
  394. var orig = ApplicationImpl.Instance;
  395. var netInput = new Mock<INetInput> ();
  396. SetupRunInputMockMethodToBlock (netInput);
  397. Mock<IConsoleOutput>? outputMock = null;
  398. var v2 = new ApplicationV2 (
  399. () => netInput.Object,
  400. () => (outputMock = new Mock<IConsoleOutput> ()).Object,
  401. Mock.Of<IWindowsInput>,
  402. Mock.Of<IConsoleOutput>);
  403. ApplicationImpl.ChangeInstance (v2);
  404. v2.Init (null, "v2net");
  405. v2.Shutdown ();
  406. outputMock!.Verify (o => o.Dispose (), Times.Once);
  407. ApplicationImpl.ChangeInstance (orig);
  408. }
  409. */
  410. [Fact]
  411. public void Open_Calls_ContinueWith_On_UIThread ()
  412. {
  413. IApplication orig = ApplicationImpl.Instance;
  414. ApplicationImpl v2 = NewMockedApplicationImpl ();
  415. ApplicationImpl.ChangeInstance (v2);
  416. v2.Init (null, "fake");
  417. var b = new Button ();
  418. var result = false;
  419. b.Accepting +=
  420. (_, _) =>
  421. {
  422. Task.Run (() => { Task.Delay (300).Wait (); })
  423. .ContinueWith (
  424. (t, _) =>
  425. {
  426. // no longer loading
  427. Application.Invoke (() =>
  428. {
  429. result = true;
  430. Application.RequestStop ();
  431. });
  432. },
  433. TaskScheduler.FromCurrentSynchronizationContext ());
  434. };
  435. v2.AddTimeout (
  436. TimeSpan.FromMilliseconds (150),
  437. () =>
  438. {
  439. // Run asynchronous logic inside Task.Run
  440. if (Application.Current != null)
  441. {
  442. b.NewKeyDownEvent (Key.Enter);
  443. b.NewKeyUpEvent (Key.Enter);
  444. }
  445. return false;
  446. });
  447. Assert.Null (Application.Current);
  448. var w = new Window
  449. {
  450. Title = "Open_CallsContinueWithOnUIThread"
  451. };
  452. w.Add (b);
  453. // Blocks until the timeout call is hit
  454. v2.Run (w);
  455. Assert.NotNull (Application.Current);
  456. Application.Current?.Dispose ();
  457. v2.Shutdown ();
  458. Assert.Null (Application.Current);
  459. ApplicationImpl.ChangeInstance (orig);
  460. Assert.True (result);
  461. }
  462. [Fact]
  463. public void ApplicationImpl_UsesInstanceFields_NotStaticReferences ()
  464. {
  465. // This test verifies that ApplicationImpl uses instance fields instead of static Application references
  466. IApplication orig = ApplicationImpl.Instance;
  467. ApplicationImpl v2 = NewMockedApplicationImpl ();
  468. ApplicationImpl.ChangeInstance (v2);
  469. // Before Init, all fields should be null/default
  470. Assert.Null (v2.Driver);
  471. Assert.False (v2.Initialized);
  472. Assert.Null (v2.Popover);
  473. //Assert.Null (v2.Navigation);
  474. Assert.Null (v2.Current);
  475. Assert.Empty (v2.SessionStack);
  476. // Init should populate instance fields
  477. v2.Init (null, "fake");
  478. // After Init, Driver, Navigation, and Popover should be populated
  479. Assert.NotNull (v2.Driver);
  480. Assert.True (v2.Initialized);
  481. Assert.NotNull (v2.Popover);
  482. Assert.NotNull (v2.Navigation);
  483. Assert.Null (v2.Current); // Top is still null until Run
  484. // Verify that static Application properties delegate to instance
  485. Assert.Equal (v2.Driver, Application.Driver);
  486. Assert.Equal (v2.Initialized, Application.Initialized);
  487. Assert.Equal (v2.Popover, Application.Popover);
  488. Assert.Equal (v2.Navigation, Application.Navigation);
  489. Assert.Equal (v2.Current, Application.Current);
  490. Assert.Same (v2.SessionStack, Application.SessionStack);
  491. // Shutdown should clean up instance fields
  492. v2.Shutdown ();
  493. Assert.Null (v2.Driver);
  494. Assert.False (v2.Initialized);
  495. Assert.Null (v2.Popover);
  496. //Assert.Null (v2.Navigation);
  497. Assert.Null (v2.Current);
  498. Assert.Empty (v2.SessionStack);
  499. ApplicationImpl.ChangeInstance (orig);
  500. }
  501. }