ApplicationImplBeginEndTests.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. #nullable enable
  2. using Xunit;
  3. using Xunit.Abstractions;
  4. namespace UnitTests.ApplicationTests;
  5. /// <summary>
  6. /// Comprehensive tests for ApplicationImpl.Begin/End logic that manages Current and SessionStack.
  7. /// These tests ensure the fragile state management logic is robust and catches regressions.
  8. /// Tests work directly with ApplicationImpl instances to avoid global Application state issues.
  9. /// </summary>
  10. public class ApplicationImplBeginEndTests
  11. {
  12. private readonly ITestOutputHelper _output;
  13. public ApplicationImplBeginEndTests (ITestOutputHelper output)
  14. {
  15. _output = output;
  16. }
  17. private ApplicationImpl NewApplicationImpl ()
  18. {
  19. var app = new ApplicationImpl ();
  20. return app;
  21. }
  22. [Fact]
  23. public void Begin_WithNullToplevel_ThrowsArgumentNullException ()
  24. {
  25. ApplicationImpl app = NewApplicationImpl ();
  26. try
  27. {
  28. Assert.Throws<ArgumentNullException> (() => app.Begin (null!));
  29. }
  30. finally
  31. {
  32. app.Shutdown ();
  33. }
  34. }
  35. [Fact]
  36. public void Begin_SetsCurrent_WhenCurrentIsNull ()
  37. {
  38. ApplicationImpl app = NewApplicationImpl ();
  39. Toplevel? toplevel = null;
  40. try
  41. {
  42. toplevel = new Toplevel ();
  43. Assert.Null (app.Current);
  44. app.Begin (toplevel);
  45. Assert.NotNull (app.Current);
  46. Assert.Same (toplevel, app.Current);
  47. Assert.Single (app.SessionStack);
  48. }
  49. finally
  50. {
  51. toplevel?.Dispose ();
  52. app.Shutdown ();
  53. }
  54. }
  55. [Fact]
  56. public void Begin_PushesToSessionStack ()
  57. {
  58. ApplicationImpl app = NewApplicationImpl ();
  59. Toplevel? toplevel1 = null;
  60. Toplevel? toplevel2 = null;
  61. try
  62. {
  63. toplevel1 = new Toplevel { Id = "1" };
  64. toplevel2 = new Toplevel { Id = "2" };
  65. app.Begin (toplevel1);
  66. Assert.Single (app.SessionStack);
  67. Assert.Same (toplevel1, app.Current);
  68. app.Begin (toplevel2);
  69. Assert.Equal (2, app.SessionStack.Count);
  70. Assert.Same (toplevel2, app.Current);
  71. }
  72. finally
  73. {
  74. toplevel1?.Dispose ();
  75. toplevel2?.Dispose ();
  76. app.Shutdown ();
  77. }
  78. }
  79. [Fact]
  80. public void Begin_SetsUniqueToplevelId_WhenIdIsEmpty ()
  81. {
  82. ApplicationImpl app = NewApplicationImpl ();
  83. Toplevel? toplevel1 = null;
  84. Toplevel? toplevel2 = null;
  85. Toplevel? toplevel3 = null;
  86. try
  87. {
  88. toplevel1 = new Toplevel ();
  89. toplevel2 = new Toplevel ();
  90. toplevel3 = new Toplevel ();
  91. Assert.Empty (toplevel1.Id);
  92. Assert.Empty (toplevel2.Id);
  93. Assert.Empty (toplevel3.Id);
  94. app.Begin (toplevel1);
  95. app.Begin (toplevel2);
  96. app.Begin (toplevel3);
  97. Assert.NotEmpty (toplevel1.Id);
  98. Assert.NotEmpty (toplevel2.Id);
  99. Assert.NotEmpty (toplevel3.Id);
  100. // IDs should be unique
  101. Assert.NotEqual (toplevel1.Id, toplevel2.Id);
  102. Assert.NotEqual (toplevel2.Id, toplevel3.Id);
  103. Assert.NotEqual (toplevel1.Id, toplevel3.Id);
  104. }
  105. finally
  106. {
  107. toplevel1?.Dispose ();
  108. toplevel2?.Dispose ();
  109. toplevel3?.Dispose ();
  110. app.Shutdown ();
  111. }
  112. }
  113. [Fact]
  114. public void End_WithNullSessionToken_ThrowsArgumentNullException ()
  115. {
  116. ApplicationImpl app = NewApplicationImpl ();
  117. try
  118. {
  119. Assert.Throws<ArgumentNullException> (() => app.End (null!));
  120. }
  121. finally
  122. {
  123. app.Shutdown ();
  124. }
  125. }
  126. [Fact]
  127. public void End_PopsSessionStack ()
  128. {
  129. ApplicationImpl app = NewApplicationImpl ();
  130. Toplevel? toplevel1 = null;
  131. Toplevel? toplevel2 = null;
  132. try
  133. {
  134. toplevel1 = new Toplevel { Id = "1" };
  135. toplevel2 = new Toplevel { Id = "2" };
  136. SessionToken token1 = app.Begin (toplevel1);
  137. SessionToken token2 = app.Begin (toplevel2);
  138. Assert.Equal (2, app.SessionStack.Count);
  139. app.End (token2);
  140. Assert.Single (app.SessionStack);
  141. Assert.Same (toplevel1, app.Current);
  142. app.End (token1);
  143. Assert.Empty (app.SessionStack);
  144. }
  145. finally
  146. {
  147. toplevel1?.Dispose ();
  148. toplevel2?.Dispose ();
  149. app.Shutdown ();
  150. }
  151. }
  152. [Fact]
  153. public void End_ThrowsArgumentException_WhenNotBalanced ()
  154. {
  155. ApplicationImpl app = NewApplicationImpl ();
  156. Toplevel? toplevel1 = null;
  157. Toplevel? toplevel2 = null;
  158. try
  159. {
  160. toplevel1 = new Toplevel { Id = "1" };
  161. toplevel2 = new Toplevel { Id = "2" };
  162. SessionToken token1 = app.Begin (toplevel1);
  163. SessionToken token2 = app.Begin (toplevel2);
  164. // Trying to end token1 when token2 is on top should throw
  165. // NOTE: This throws but has the side effect of popping token2 from the stack
  166. Assert.Throws<ArgumentException> (() => app.End (token1));
  167. // Don't try to clean up with more End calls - the state is now inconsistent
  168. // Let Shutdown/ResetState handle cleanup
  169. }
  170. finally
  171. {
  172. // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions
  173. toplevel1?.Dispose ();
  174. toplevel2?.Dispose ();
  175. // Shutdown will call ResetState which clears any remaining state
  176. app.Shutdown ();
  177. }
  178. }
  179. [Fact]
  180. public void End_RestoresCurrentToPreviousToplevel ()
  181. {
  182. ApplicationImpl app = NewApplicationImpl ();
  183. Toplevel? toplevel1 = null;
  184. Toplevel? toplevel2 = null;
  185. Toplevel? toplevel3 = null;
  186. try
  187. {
  188. toplevel1 = new Toplevel { Id = "1" };
  189. toplevel2 = new Toplevel { Id = "2" };
  190. toplevel3 = new Toplevel { Id = "3" };
  191. SessionToken token1 = app.Begin (toplevel1);
  192. SessionToken token2 = app.Begin (toplevel2);
  193. SessionToken token3 = app.Begin (toplevel3);
  194. Assert.Same (toplevel3, app.Current);
  195. app.End (token3);
  196. Assert.Same (toplevel2, app.Current);
  197. app.End (token2);
  198. Assert.Same (toplevel1, app.Current);
  199. app.End (token1);
  200. }
  201. finally
  202. {
  203. toplevel1?.Dispose ();
  204. toplevel2?.Dispose ();
  205. toplevel3?.Dispose ();
  206. app.Shutdown ();
  207. }
  208. }
  209. [Fact]
  210. public void MultipleBeginEnd_MaintainsStackIntegrity ()
  211. {
  212. ApplicationImpl app = NewApplicationImpl ();
  213. var toplevels = new List<Toplevel> ();
  214. var tokens = new List<SessionToken> ();
  215. try
  216. {
  217. // Begin multiple toplevels
  218. for (int i = 0; i < 5; i++)
  219. {
  220. var toplevel = new Toplevel { Id = $"toplevel-{i}" };
  221. toplevels.Add (toplevel);
  222. tokens.Add (app.Begin (toplevel));
  223. }
  224. Assert.Equal (5, app.SessionStack.Count);
  225. Assert.Same (toplevels [4], app.Current);
  226. // End them in reverse order (LIFO)
  227. for (int i = 4; i >= 0; i--)
  228. {
  229. app.End (tokens [i]);
  230. if (i > 0)
  231. {
  232. Assert.Equal (i, app.SessionStack.Count);
  233. Assert.Same (toplevels [i - 1], app.Current);
  234. }
  235. else
  236. {
  237. Assert.Empty (app.SessionStack);
  238. }
  239. }
  240. }
  241. finally
  242. {
  243. foreach (var toplevel in toplevels)
  244. {
  245. toplevel.Dispose ();
  246. }
  247. app.Shutdown ();
  248. }
  249. }
  250. [Fact]
  251. public void End_UpdatesCachedSessionTokenToplevel ()
  252. {
  253. ApplicationImpl app = NewApplicationImpl ();
  254. Toplevel? toplevel = null;
  255. try
  256. {
  257. toplevel = new Toplevel ();
  258. SessionToken token = app.Begin (toplevel);
  259. Assert.Null (app.CachedSessionTokenToplevel);
  260. app.End (token);
  261. Assert.Same (toplevel, app.CachedSessionTokenToplevel);
  262. }
  263. finally
  264. {
  265. toplevel?.Dispose ();
  266. app.Shutdown ();
  267. }
  268. }
  269. [Fact]
  270. public void End_NullsSessionTokenToplevel ()
  271. {
  272. ApplicationImpl app = NewApplicationImpl ();
  273. Toplevel? toplevel = null;
  274. try
  275. {
  276. toplevel = new Toplevel ();
  277. SessionToken token = app.Begin (toplevel);
  278. Assert.Same (toplevel, token.Toplevel);
  279. app.End (token);
  280. Assert.Null (token.Toplevel);
  281. }
  282. finally
  283. {
  284. toplevel?.Dispose ();
  285. app.Shutdown ();
  286. }
  287. }
  288. [Fact]
  289. public void ResetState_ClearsSessionStack ()
  290. {
  291. ApplicationImpl app = NewApplicationImpl ();
  292. Toplevel? toplevel1 = null;
  293. Toplevel? toplevel2 = null;
  294. try
  295. {
  296. toplevel1 = new Toplevel { Id = "1" };
  297. toplevel2 = new Toplevel { Id = "2" };
  298. app.Begin (toplevel1);
  299. app.Begin (toplevel2);
  300. Assert.Equal (2, app.SessionStack.Count);
  301. Assert.NotNull (app.Current);
  302. }
  303. finally
  304. {
  305. // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions
  306. toplevel1?.Dispose ();
  307. toplevel2?.Dispose ();
  308. // Shutdown calls ResetState, which will clear SessionStack and set Current to null
  309. app.Shutdown ();
  310. // Verify cleanup happened
  311. Assert.Empty (app.SessionStack);
  312. Assert.Null (app.Current);
  313. Assert.Null (app.CachedSessionTokenToplevel);
  314. }
  315. }
  316. [Fact]
  317. public void ResetState_StopsAllRunningToplevels ()
  318. {
  319. ApplicationImpl app = NewApplicationImpl ();
  320. Toplevel? toplevel1 = null;
  321. Toplevel? toplevel2 = null;
  322. try
  323. {
  324. toplevel1 = new Toplevel { Id = "1", Running = true };
  325. toplevel2 = new Toplevel { Id = "2", Running = true };
  326. app.Begin (toplevel1);
  327. app.Begin (toplevel2);
  328. Assert.True (toplevel1.Running);
  329. Assert.True (toplevel2.Running);
  330. }
  331. finally
  332. {
  333. // Dispose toplevels BEFORE Shutdown to satisfy DEBUG_IDISPOSABLE assertions
  334. toplevel1?.Dispose ();
  335. toplevel2?.Dispose ();
  336. // Shutdown calls ResetState, which will stop all running toplevels
  337. app.Shutdown ();
  338. // Verify toplevels were stopped
  339. Assert.False (toplevel1!.Running);
  340. Assert.False (toplevel2!.Running);
  341. }
  342. }
  343. [Fact]
  344. public void Begin_ActivatesNewToplevel_WhenCurrentExists ()
  345. {
  346. ApplicationImpl app = NewApplicationImpl ();
  347. Toplevel? toplevel1 = null;
  348. Toplevel? toplevel2 = null;
  349. try
  350. {
  351. toplevel1 = new Toplevel { Id = "1" };
  352. toplevel2 = new Toplevel { Id = "2" };
  353. bool toplevel1Deactivated = false;
  354. bool toplevel2Activated = false;
  355. toplevel1.Deactivate += (s, e) => toplevel1Deactivated = true;
  356. toplevel2.Activate += (s, e) => toplevel2Activated = true;
  357. app.Begin (toplevel1);
  358. app.Begin (toplevel2);
  359. Assert.True (toplevel1Deactivated);
  360. Assert.True (toplevel2Activated);
  361. Assert.Same (toplevel2, app.Current);
  362. }
  363. finally
  364. {
  365. toplevel1?.Dispose ();
  366. toplevel2?.Dispose ();
  367. app.Shutdown ();
  368. }
  369. }
  370. [Fact]
  371. public void Begin_DoesNotDuplicateToplevel_WhenIdAlreadyExists ()
  372. {
  373. ApplicationImpl app = NewApplicationImpl ();
  374. Toplevel? toplevel = null;
  375. try
  376. {
  377. toplevel = new Toplevel { Id = "test-id" };
  378. app.Begin (toplevel);
  379. Assert.Single (app.SessionStack);
  380. // Calling Begin again with same toplevel should not duplicate
  381. app.Begin (toplevel);
  382. Assert.Single (app.SessionStack);
  383. }
  384. finally
  385. {
  386. toplevel?.Dispose ();
  387. app.Shutdown ();
  388. }
  389. }
  390. [Fact]
  391. public void SessionStack_ContainsAllBegunToplevels ()
  392. {
  393. ApplicationImpl app = NewApplicationImpl ();
  394. var toplevels = new List<Toplevel> ();
  395. try
  396. {
  397. for (int i = 0; i < 10; i++)
  398. {
  399. var toplevel = new Toplevel { Id = $"toplevel-{i}" };
  400. toplevels.Add (toplevel);
  401. app.Begin (toplevel);
  402. }
  403. // All toplevels should be in the stack
  404. Assert.Equal (10, app.SessionStack.Count);
  405. // Verify stack contains all toplevels
  406. var stackList = app.SessionStack.ToList ();
  407. foreach (var toplevel in toplevels)
  408. {
  409. Assert.Contains (toplevel, stackList);
  410. }
  411. }
  412. finally
  413. {
  414. foreach (var toplevel in toplevels)
  415. {
  416. toplevel.Dispose ();
  417. }
  418. app.Shutdown ();
  419. }
  420. }
  421. }