ApplicationImplTests.cs 21 KB

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