MouseEventRoutingTests.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. using Terminal.Gui.App;
  2. using Xunit.Abstractions;
  3. namespace ApplicationTests.Mouse;
  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. bool eventReceived = false;
  31. view.MouseEvent += (_, 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. bool eventReceived = false;
  82. view.MouseEvent += (_, args) =>
  83. {
  84. eventReceived = true;
  85. receivedPosition = args.Position;
  86. };
  87. MouseEventArgs mouseEvent = new ()
  88. {
  89. Position = new (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. bool subViewEventReceived = false;
  127. subView.MouseEvent += (_, 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_RaisesSelectingEvent ()
  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. int selectingCount = 0;
  166. subView.Selecting += (_, _) => selectingCount++;
  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, selectingCount);
  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. bool handlerCalled = false;
  187. bool clickHandlerCalled = false;
  188. view.MouseEvent += (_, args) =>
  189. {
  190. handlerCalled = true;
  191. args.Handled = true; // Mark as handled
  192. };
  193. view.MouseEvent += (_, e) => { clickHandlerCalled = !e.IsSingleDoubleOrTripleClicked; ; };
  194. MouseEventArgs mouseEvent = new ()
  195. {
  196. Position = new (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. bool eventHandlerCalled = false;
  213. view.MouseEvent += (_, _) =>
  214. {
  215. eventHandlerCalled = true;
  216. // Don't set Handled = true
  217. };
  218. MouseEventArgs mouseEvent = new ()
  219. {
  220. Position = new (5, 5),
  221. Flags = MouseFlags.Button1Clicked
  222. };
  223. // Act
  224. view.NewMouseEvent (mouseEvent);
  225. // Assert
  226. Assert.True (eventHandlerCalled);
  227. view.Dispose ();
  228. }
  229. #endregion
  230. #region Mouse Button Events
  231. [Theory]
  232. [InlineData (MouseFlags.Button1Pressed, 1, 0)]
  233. [InlineData (MouseFlags.Button1Released, 0, 1)]
  234. [InlineData (MouseFlags.Button1Clicked, 0, 0)]
  235. public void View_MouseButtonEvents_RaiseCorrectHandlers (MouseFlags flags, int expectedPressed, int expectedReleased)
  236. {
  237. // Arrange
  238. View view = new () { Width = 10, Height = 10 };
  239. int pressedCount = 0;
  240. int releasedCount = 0;
  241. view.MouseEvent += (_, args) =>
  242. {
  243. if (args.Flags.HasFlag (MouseFlags.Button1Pressed))
  244. {
  245. pressedCount++;
  246. }
  247. if (args.Flags.HasFlag (MouseFlags.Button1Released))
  248. {
  249. releasedCount++;
  250. }
  251. };
  252. MouseEventArgs mouseEvent = new ()
  253. {
  254. Position = new (5, 5),
  255. Flags = flags
  256. };
  257. // Act
  258. view.NewMouseEvent (mouseEvent);
  259. // Assert
  260. Assert.Equal (expectedPressed, pressedCount);
  261. Assert.Equal (expectedReleased, releasedCount);
  262. view.Dispose ();
  263. }
  264. [Theory]
  265. [InlineData (MouseFlags.Button1Clicked)]
  266. [InlineData (MouseFlags.Button2Clicked)]
  267. [InlineData (MouseFlags.Button3Clicked)]
  268. [InlineData (MouseFlags.Button4Clicked)]
  269. public void View_AllMouseButtons_TriggerClickEvent (MouseFlags clickFlag)
  270. {
  271. // Arrange
  272. View view = new () { Width = 10, Height = 10 };
  273. int clickCount = 0;
  274. view.MouseEvent += (_, a) => clickCount += a.IsSingleDoubleOrTripleClicked ? 1 : 0;
  275. MouseEventArgs mouseEvent = new ()
  276. {
  277. Position = new (5, 5),
  278. Flags = clickFlag
  279. };
  280. // Act
  281. view.NewMouseEvent (mouseEvent);
  282. // Assert
  283. Assert.Equal (1, clickCount);
  284. view.Dispose ();
  285. }
  286. #endregion
  287. #region Disabled View Tests
  288. [Fact]
  289. public void View_Disabled_DoesNotRaiseMouseEvent ()
  290. {
  291. // Arrange
  292. View view = new ()
  293. {
  294. Width = 10,
  295. Height = 10,
  296. Enabled = false
  297. };
  298. bool eventCalled = false;
  299. view.MouseEvent += (_, _) => { eventCalled = true; };
  300. MouseEventArgs mouseEvent = new ()
  301. {
  302. Position = new (5, 5),
  303. Flags = MouseFlags.Button1Clicked
  304. };
  305. // Act
  306. view.NewMouseEvent (mouseEvent);
  307. // Assert
  308. Assert.False (eventCalled);
  309. view.Dispose ();
  310. }
  311. [Fact]
  312. public void View_Disabled_DoesNotRaiseSelectingEvent ()
  313. {
  314. // Arrange
  315. View view = new ()
  316. {
  317. Width = 10,
  318. Height = 10,
  319. Enabled = false
  320. };
  321. bool selectingCalled = false;
  322. view.Selecting += (_, _) => { selectingCalled = true; };
  323. MouseEventArgs mouseEvent = new ()
  324. {
  325. Position = new (5, 5),
  326. Flags = MouseFlags.Button1Clicked
  327. };
  328. // Act
  329. view.NewMouseEvent (mouseEvent);
  330. // Assert
  331. Assert.False (selectingCalled);
  332. view.Dispose ();
  333. }
  334. #endregion
  335. #region Focus and Selection Tests
  336. [Theory]
  337. [InlineData (true, true)]
  338. [InlineData (false, false)]
  339. public void MouseClick_SetsFocus_BasedOnCanFocus (bool canFocus, bool expectFocus)
  340. {
  341. // Arrange
  342. View superView = new () { CanFocus = true, Width = 20, Height = 20 };
  343. View subView = new ()
  344. {
  345. X = 5,
  346. Y = 5,
  347. Width = 10,
  348. Height = 10,
  349. CanFocus = canFocus
  350. };
  351. superView.Add (subView);
  352. superView.SetFocus (); // Give superView focus first
  353. MouseEventArgs mouseEvent = new ()
  354. {
  355. Position = new (2, 2),
  356. Flags = MouseFlags.Button1Clicked
  357. };
  358. // Act
  359. subView.NewMouseEvent (mouseEvent);
  360. // Assert
  361. Assert.Equal (expectFocus, subView.HasFocus);
  362. subView.Dispose ();
  363. superView.Dispose ();
  364. }
  365. [Fact]
  366. public void MouseClick_RaisesSelecting_WhenCanFocus ()
  367. {
  368. // Arrange
  369. View superView = new () { CanFocus = true, Width = 20, Height = 20 };
  370. View view = new ()
  371. {
  372. X = 5,
  373. Y = 5,
  374. Width = 10,
  375. Height = 10,
  376. CanFocus = true
  377. };
  378. superView.Add (view);
  379. int selectingCount = 0;
  380. view.Selecting += (_, _) => selectingCount++;
  381. MouseEventArgs mouseEvent = new ()
  382. {
  383. Position = new (5, 5),
  384. Flags = MouseFlags.Button1Clicked
  385. };
  386. // Act
  387. view.NewMouseEvent (mouseEvent);
  388. // Assert
  389. Assert.Equal (1, selectingCount);
  390. view.Dispose ();
  391. superView.Dispose ();
  392. }
  393. #endregion
  394. }