ApplicationImplBeginEndTests.cs 13 KB

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