MouseEventRoutingTests.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. using Terminal.Gui.App;
  2. using Xunit.Abstractions;
  3. namespace ApplicationTests;
  4. /// <summary>
  5. /// Parallelizable tests for mouse event routing and coordinate transformation.
  6. /// These tests validate mouse event handling without Application.Begin or global state.
  7. /// </summary>
  8. [Trait ("Category", "Input")]
  9. public class MouseEventRoutingTests (ITestOutputHelper output)
  10. {
  11. private readonly ITestOutputHelper _output = output;
  12. #region Mouse Event Routing to Views
  13. [Theory]
  14. [InlineData (5, 5, 5, 5, true)] // Click inside view
  15. [InlineData (0, 0, 0, 0, true)] // Click at origin
  16. [InlineData (9, 9, 9, 9, true)] // Click at far corner (view is 10x10)
  17. [InlineData (10, 10, -1, -1, false)] // Click outside view
  18. [InlineData (-1, -1, -1, -1, false)] // Click outside view
  19. public void View_NewMouseEvent_ReceivesCorrectCoordinates (int screenX, int screenY, int expectedViewX, int expectedViewY, bool shouldReceive)
  20. {
  21. // Arrange
  22. View view = new ()
  23. {
  24. X = 0,
  25. Y = 0,
  26. Width = 10,
  27. Height = 10
  28. };
  29. Point? receivedPosition = null;
  30. var eventReceived = false;
  31. view.MouseEvent += (sender, args) =>
  32. {
  33. eventReceived = true;
  34. receivedPosition = args.Position;
  35. };
  36. MouseEventArgs mouseEvent = new ()
  37. {
  38. Position = new Point (screenX, screenY),
  39. Flags = MouseFlags.Button1Clicked
  40. };
  41. // Act
  42. view.NewMouseEvent (mouseEvent);
  43. // Assert
  44. if (shouldReceive)
  45. {
  46. Assert.True (eventReceived);
  47. Assert.NotNull (receivedPosition);
  48. Assert.Equal (expectedViewX, receivedPosition.Value.X);
  49. Assert.Equal (expectedViewY, receivedPosition.Value.Y);
  50. }
  51. view.Dispose ();
  52. }
  53. [Theory]
  54. [InlineData (0, 0, 5, 5, 5, 5, true)] // View at origin, click at (5,5) in view
  55. [InlineData (10, 10, 5, 5, 5, 5, true)] // View offset, but we still pass view-relative coords
  56. [InlineData (0, 0, 0, 0, 0, 0, true)] // View at origin, click at origin
  57. [InlineData (5, 5, 9, 9, 9, 9, true)] // View offset, click at far corner (view-relative)
  58. [InlineData (0, 0, 10, 10, -1, -1, false)] // Click outside view bounds
  59. [InlineData (0, 0, -1, -1, -1, -1, false)] // Click outside view bounds
  60. public void View_WithOffset_ReceivesCorrectCoordinates (
  61. int viewX,
  62. int viewY,
  63. int viewRelativeX,
  64. int viewRelativeY,
  65. int expectedViewX,
  66. int expectedViewY,
  67. bool shouldReceive)
  68. {
  69. // Arrange
  70. // Note: When testing View.NewMouseEvent directly (without Application routing),
  71. // coordinates are already view-relative. The view's X/Y position doesn't affect
  72. // the coordinate transformation at this level.
  73. View view = new ()
  74. {
  75. X = viewX,
  76. Y = viewY,
  77. Width = 10,
  78. Height = 10
  79. };
  80. Point? receivedPosition = null;
  81. var eventReceived = false;
  82. view.MouseEvent += (sender, args) =>
  83. {
  84. eventReceived = true;
  85. receivedPosition = args.Position;
  86. };
  87. MouseEventArgs mouseEvent = new ()
  88. {
  89. Position = new Point (viewRelativeX, viewRelativeY),
  90. Flags = MouseFlags.Button1Clicked
  91. };
  92. // Act
  93. view.NewMouseEvent (mouseEvent);
  94. // Assert
  95. if (shouldReceive)
  96. {
  97. Assert.True (eventReceived, $"Event should be received at view-relative ({viewRelativeX},{viewRelativeY})");
  98. Assert.NotNull (receivedPosition);
  99. Assert.Equal (expectedViewX, receivedPosition.Value.X);
  100. Assert.Equal (expectedViewY, receivedPosition.Value.Y);
  101. }
  102. view.Dispose ();
  103. }
  104. #endregion
  105. #region View Hierarchy Mouse Event Routing
  106. [Fact]
  107. public void SubView_ReceivesMouseEvent_WithCorrectRelativeCoordinates ()
  108. {
  109. // Arrange
  110. View superView = new ()
  111. {
  112. X = 0,
  113. Y = 0,
  114. Width = 20,
  115. Height = 20
  116. };
  117. View subView = new ()
  118. {
  119. X = 5,
  120. Y = 5,
  121. Width = 10,
  122. Height = 10
  123. };
  124. superView.Add (subView);
  125. Point? subViewReceivedPosition = null;
  126. var subViewEventReceived = false;
  127. subView.MouseEvent += (sender, args) =>
  128. {
  129. subViewEventReceived = true;
  130. subViewReceivedPosition = args.Position;
  131. };
  132. // Click at position (2, 2) relative to subView (which is at 5,5 relative to superView)
  133. MouseEventArgs mouseEvent = new ()
  134. {
  135. Position = new Point (2, 2), // Relative to subView
  136. Flags = MouseFlags.Button1Clicked
  137. };
  138. // Act
  139. subView.NewMouseEvent (mouseEvent);
  140. // Assert
  141. Assert.True (subViewEventReceived);
  142. Assert.NotNull (subViewReceivedPosition);
  143. Assert.Equal (2, subViewReceivedPosition.Value.X);
  144. Assert.Equal (2, subViewReceivedPosition.Value.Y);
  145. subView.Dispose ();
  146. superView.Dispose ();
  147. }
  148. [Fact]
  149. public void MouseClick_OnSubView_RaisesMouseClickEvent ()
  150. {
  151. // Arrange
  152. View superView = new ()
  153. {
  154. Width = 20,
  155. Height = 20
  156. };
  157. View subView = new ()
  158. {
  159. X = 5,
  160. Y = 5,
  161. Width = 10,
  162. Height = 10
  163. };
  164. superView.Add (subView);
  165. var clickCount = 0;
  166. subView.MouseClick += (sender, args) => clickCount++;
  167. MouseEventArgs mouseEvent = new ()
  168. {
  169. Position = new Point (5, 5),
  170. Flags = MouseFlags.Button1Clicked
  171. };
  172. // Act
  173. subView.NewMouseEvent (mouseEvent);
  174. // Assert
  175. Assert.Equal (1, clickCount);
  176. subView.Dispose ();
  177. superView.Dispose ();
  178. }
  179. #endregion
  180. #region Mouse Event Propagation
  181. [Fact]
  182. public void View_HandledEvent_StopsPropagation ()
  183. {
  184. // Arrange
  185. View view = new () { Width = 10, Height = 10 };
  186. var handlerCalled = false;
  187. var clickHandlerCalled = false;
  188. view.MouseEvent += (sender, args) =>
  189. {
  190. handlerCalled = true;
  191. args.Handled = true; // Mark as handled
  192. };
  193. view.MouseClick += (sender, args) => { clickHandlerCalled = true; };
  194. MouseEventArgs mouseEvent = new ()
  195. {
  196. Position = new Point (5, 5),
  197. Flags = MouseFlags.Button1Clicked
  198. };
  199. // Act
  200. bool? result = view.NewMouseEvent (mouseEvent);
  201. // Assert
  202. Assert.True (result.HasValue && result.Value); // Event was handled
  203. Assert.True (handlerCalled);
  204. Assert.False (clickHandlerCalled); // Click handler should not be called when event is handled
  205. view.Dispose ();
  206. }
  207. [Fact]
  208. public void View_UnhandledEvent_ContinuesProcessing ()
  209. {
  210. // Arrange
  211. View view = new () { Width = 10, Height = 10 };
  212. var eventHandlerCalled = false;
  213. var clickHandlerCalled = false;
  214. view.MouseEvent += (sender, args) =>
  215. {
  216. eventHandlerCalled = true;
  217. // Don't set Handled = true
  218. };
  219. view.MouseClick += (sender, args) => { clickHandlerCalled = true; };
  220. MouseEventArgs mouseEvent = new ()
  221. {
  222. Position = new Point (5, 5),
  223. Flags = MouseFlags.Button1Clicked
  224. };
  225. // Act
  226. view.NewMouseEvent (mouseEvent);
  227. // Assert
  228. Assert.True (eventHandlerCalled);
  229. Assert.True (clickHandlerCalled); // Click handler should be called when event is not handled
  230. view.Dispose ();
  231. }
  232. #endregion
  233. #region Mouse Button Events
  234. [Theory]
  235. [InlineData (MouseFlags.Button1Pressed, 1, 0, 0)]
  236. [InlineData (MouseFlags.Button1Released, 0, 1, 0)]
  237. [InlineData (MouseFlags.Button1Clicked, 0, 0, 1)]
  238. public void View_MouseButtonEvents_RaiseCorrectHandlers (MouseFlags flags, int expectedPressed, int expectedReleased, int expectedClicked)
  239. {
  240. // Arrange
  241. View view = new () { Width = 10, Height = 10 };
  242. var pressedCount = 0;
  243. var releasedCount = 0;
  244. var clickedCount = 0;
  245. view.MouseEvent += (sender, args) =>
  246. {
  247. if (args.Flags.HasFlag (MouseFlags.Button1Pressed))
  248. {
  249. pressedCount++;
  250. }
  251. if (args.Flags.HasFlag (MouseFlags.Button1Released))
  252. {
  253. releasedCount++;
  254. }
  255. };
  256. view.MouseClick += (sender, args) => { clickedCount++; };
  257. MouseEventArgs mouseEvent = new ()
  258. {
  259. Position = new Point (5, 5),
  260. Flags = flags
  261. };
  262. // Act
  263. view.NewMouseEvent (mouseEvent);
  264. // Assert
  265. Assert.Equal (expectedPressed, pressedCount);
  266. Assert.Equal (expectedReleased, releasedCount);
  267. Assert.Equal (expectedClicked, clickedCount);
  268. view.Dispose ();
  269. }
  270. [Theory]
  271. [InlineData (MouseFlags.Button1Clicked)]
  272. [InlineData (MouseFlags.Button2Clicked)]
  273. [InlineData (MouseFlags.Button3Clicked)]
  274. [InlineData (MouseFlags.Button4Clicked)]
  275. public void View_AllMouseButtons_TriggerClickEvent (MouseFlags clickFlag)
  276. {
  277. // Arrange
  278. View view = new () { Width = 10, Height = 10 };
  279. var clickCount = 0;
  280. view.MouseClick += (sender, args) => clickCount++;
  281. MouseEventArgs mouseEvent = new ()
  282. {
  283. Position = new Point (5, 5),
  284. Flags = clickFlag
  285. };
  286. // Act
  287. view.NewMouseEvent (mouseEvent);
  288. // Assert
  289. Assert.Equal (1, clickCount);
  290. view.Dispose ();
  291. }
  292. #endregion
  293. #region Disabled View Tests
  294. [Fact]
  295. public void View_Disabled_DoesNotRaiseMouseEvent ()
  296. {
  297. // Arrange
  298. View view = new ()
  299. {
  300. Width = 10,
  301. Height = 10,
  302. Enabled = false
  303. };
  304. var eventCalled = false;
  305. view.MouseEvent += (sender, args) => { eventCalled = true; };
  306. MouseEventArgs mouseEvent = new ()
  307. {
  308. Position = new Point (5, 5),
  309. Flags = MouseFlags.Button1Clicked
  310. };
  311. // Act
  312. view.NewMouseEvent (mouseEvent);
  313. // Assert
  314. Assert.False (eventCalled);
  315. view.Dispose ();
  316. }
  317. [Fact]
  318. public void View_Disabled_DoesNotRaiseMouseClickEvent ()
  319. {
  320. // Arrange
  321. View view = new ()
  322. {
  323. Width = 10,
  324. Height = 10,
  325. Enabled = false
  326. };
  327. var clickCalled = false;
  328. view.MouseClick += (sender, args) => { clickCalled = true; };
  329. MouseEventArgs mouseEvent = new ()
  330. {
  331. Position = new Point (5, 5),
  332. Flags = MouseFlags.Button1Clicked
  333. };
  334. // Act
  335. view.NewMouseEvent (mouseEvent);
  336. // Assert
  337. Assert.False (clickCalled);
  338. view.Dispose ();
  339. }
  340. #endregion
  341. #region Focus and Selection Tests
  342. [Theory]
  343. [InlineData (true, true)]
  344. [InlineData (false, false)]
  345. public void MouseClick_SetsFocus_BasedOnCanFocus (bool canFocus, bool expectFocus)
  346. {
  347. // Arrange
  348. View superView = new () { CanFocus = true, Width = 20, Height = 20 };
  349. View subView = new ()
  350. {
  351. X = 5,
  352. Y = 5,
  353. Width = 10,
  354. Height = 10,
  355. CanFocus = canFocus
  356. };
  357. superView.Add (subView);
  358. superView.SetFocus (); // Give superView focus first
  359. MouseEventArgs mouseEvent = new ()
  360. {
  361. Position = new Point (2, 2),
  362. Flags = MouseFlags.Button1Clicked
  363. };
  364. // Act
  365. subView.NewMouseEvent (mouseEvent);
  366. // Assert
  367. Assert.Equal (expectFocus, subView.HasFocus);
  368. subView.Dispose ();
  369. superView.Dispose ();
  370. }
  371. [Fact]
  372. public void MouseClick_RaisesSelecting_WhenCanFocus ()
  373. {
  374. // Arrange
  375. View superView = new () { CanFocus = true, Width = 20, Height = 20 };
  376. View view = new ()
  377. {
  378. X = 5,
  379. Y = 5,
  380. Width = 10,
  381. Height = 10,
  382. CanFocus = true
  383. };
  384. superView.Add (view);
  385. var selectingCount = 0;
  386. view.Selecting += (sender, args) => selectingCount++;
  387. MouseEventArgs mouseEvent = new ()
  388. {
  389. Position = new Point (5, 5),
  390. Flags = MouseFlags.Button1Clicked
  391. };
  392. // Act
  393. view.NewMouseEvent (mouseEvent);
  394. // Assert
  395. Assert.Equal (1, selectingCount);
  396. view.Dispose ();
  397. superView.Dispose ();
  398. }
  399. #endregion
  400. }