TextViewNavigationTests.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. using UnitTests;
  2. namespace UnitTests_Parallelizable.ViewsTests;
  3. /// <summary>
  4. /// Tests for TextView navigation, tabs, and cursor positioning.
  5. /// These replace the old non-parallelizable tests that had hard-coded viewport dimensions.
  6. /// </summary>
  7. public class TextViewNavigationTests : FakeDriverBase
  8. {
  9. [Fact]
  10. public void Tab_And_BackTab_Navigation_Without_Text ()
  11. {
  12. IDriver driver = CreateFakeDriver ();
  13. var textView = new TextView
  14. {
  15. Width = 30,
  16. Height = 10,
  17. Text = "",
  18. Driver = driver
  19. };
  20. textView.BeginInit ();
  21. textView.EndInit ();
  22. // Add 100 tabs
  23. for (var i = 0; i < 100; i++)
  24. {
  25. textView.Text += "\t";
  26. }
  27. // Move to end
  28. textView.MoveEnd ();
  29. Assert.Equal (new Point (100, 0), textView.CursorPosition);
  30. // Test BackTab (Shift+Tab) navigation backwards
  31. for (var i = 99; i >= 0; i--)
  32. {
  33. Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
  34. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  35. }
  36. // Test Tab navigation forwards
  37. for (var i = 1; i <= 100; i++)
  38. {
  39. Assert.True (textView.NewKeyDownEvent (Key.Tab));
  40. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  41. }
  42. }
  43. [Fact]
  44. public void Tab_And_BackTab_Navigation_With_Text ()
  45. {
  46. IDriver driver = CreateFakeDriver ();
  47. var textView = new TextView
  48. {
  49. Width = 30,
  50. Height = 10,
  51. Text = "TAB to jump between text fields.",
  52. Driver = driver
  53. };
  54. textView.BeginInit ();
  55. textView.EndInit ();
  56. Assert.Equal (new Point (0, 0), textView.CursorPosition);
  57. // Navigate forward with Tab
  58. for (var i = 1; i <= 100; i++)
  59. {
  60. Assert.True (textView.NewKeyDownEvent (Key.Tab));
  61. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  62. }
  63. // Navigate backward with BackTab
  64. for (var i = 99; i >= 0; i--)
  65. {
  66. Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
  67. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  68. }
  69. }
  70. [Fact]
  71. public void Tab_With_CursorLeft_And_CursorRight_Without_Text ()
  72. {
  73. IDriver driver = CreateFakeDriver ();
  74. var textView = new TextView
  75. {
  76. Width = 30,
  77. Height = 10,
  78. Text = "",
  79. Driver = driver
  80. };
  81. textView.BeginInit ();
  82. textView.EndInit ();
  83. // Navigate forward with Tab
  84. for (var i = 1; i <= 100; i++)
  85. {
  86. Assert.True (textView.NewKeyDownEvent (Key.Tab));
  87. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  88. }
  89. // Navigate backward with CursorLeft
  90. for (var i = 99; i >= 0; i--)
  91. {
  92. Assert.True (textView.NewKeyDownEvent (Key.CursorLeft));
  93. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  94. }
  95. // Navigate forward with CursorRight
  96. for (var i = 1; i <= 100; i++)
  97. {
  98. Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
  99. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  100. }
  101. }
  102. [Fact]
  103. public void Tab_With_CursorLeft_And_CursorRight_With_Text ()
  104. {
  105. IDriver driver = CreateFakeDriver ();
  106. var textView = new TextView
  107. {
  108. Width = 30,
  109. Height = 10,
  110. Text = "TAB to jump between text fields.",
  111. Driver = driver
  112. };
  113. textView.BeginInit ();
  114. textView.EndInit ();
  115. Assert.Equal (32, textView.Text.Length);
  116. Assert.Equal (new Point (0, 0), textView.CursorPosition);
  117. // Navigate forward with Tab
  118. for (var i = 1; i <= 100; i++)
  119. {
  120. Assert.True (textView.NewKeyDownEvent (Key.Tab));
  121. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  122. }
  123. // Navigate backward with CursorLeft
  124. for (var i = 99; i >= 0; i--)
  125. {
  126. Assert.True (textView.NewKeyDownEvent (Key.CursorLeft));
  127. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  128. }
  129. // Navigate forward with CursorRight
  130. for (var i = 1; i <= 100; i++)
  131. {
  132. Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
  133. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  134. }
  135. }
  136. [Fact]
  137. public void Tab_With_Home_End_And_BackTab ()
  138. {
  139. IDriver driver = CreateFakeDriver ();
  140. var textView = new TextView
  141. {
  142. Width = 30,
  143. Height = 10,
  144. Text = "TAB to jump between text fields.",
  145. Driver = driver
  146. };
  147. textView.BeginInit ();
  148. textView.EndInit ();
  149. Assert.Equal (32, textView.Text.Length);
  150. Assert.Equal (new Point (0, 0), textView.CursorPosition);
  151. // Navigate forward with Tab to column 100
  152. for (var i = 1; i <= 100; i++)
  153. {
  154. Assert.True (textView.NewKeyDownEvent (Key.Tab));
  155. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  156. }
  157. // Test Length increased due to tabs
  158. Assert.Equal (132, textView.Text.Length);
  159. // Press Home to go to beginning
  160. Assert.True (textView.NewKeyDownEvent (Key.Home));
  161. Assert.Equal (new Point (0, 0), textView.CursorPosition);
  162. // Press End to go to end
  163. Assert.True (textView.NewKeyDownEvent (Key.End));
  164. Assert.Equal (132, textView.Text.Length);
  165. Assert.Equal (new Point (132, 0), textView.CursorPosition);
  166. // Find the position just before the last tab
  167. string txt = textView.Text;
  168. var col = txt.Length;
  169. // Find the last tab position
  170. while (col > 1 && txt [col - 1] != '\t')
  171. {
  172. col--;
  173. // Safety check to prevent infinite loop
  174. if (col == 0)
  175. {
  176. break;
  177. }
  178. }
  179. // Set cursor to that position
  180. textView.CursorPosition = new Point (col, 0);
  181. // Navigate backward with BackTab (removes tabs, going back to original text)
  182. while (col > 0)
  183. {
  184. col--;
  185. Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
  186. Assert.Equal (new Point (col, 0), textView.CursorPosition);
  187. }
  188. // Should be back at the original text
  189. Assert.Equal ("TAB to jump between text fields.", textView.Text);
  190. Assert.Equal (32, textView.Text.Length);
  191. }
  192. [Fact]
  193. public void BackTab_Then_Tab_Navigation ()
  194. {
  195. IDriver driver = CreateFakeDriver ();
  196. var textView = new TextView
  197. {
  198. Width = 30,
  199. Height = 10,
  200. Text = "",
  201. Driver = driver
  202. };
  203. textView.BeginInit ();
  204. textView.EndInit ();
  205. // Add 100 tabs at end
  206. for (var i = 0; i < 100; i++)
  207. {
  208. textView.Text += "\t";
  209. }
  210. textView.MoveEnd ();
  211. Assert.Equal (new Point (100, 0), textView.CursorPosition);
  212. // Navigate backward with BackTab
  213. for (var i = 99; i >= 0; i--)
  214. {
  215. Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
  216. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  217. }
  218. // Navigate forward with Tab
  219. for (var i = 1; i <= 100; i++)
  220. {
  221. Assert.True (textView.NewKeyDownEvent (Key.Tab));
  222. Assert.Equal (new Point (i, 0), textView.CursorPosition);
  223. }
  224. }
  225. [Fact]
  226. public void TabWidth_Setting_To_Zero_Keeps_AllowsTab ()
  227. {
  228. IDriver driver = CreateFakeDriver ();
  229. var textView = new TextView
  230. {
  231. Width = 30,
  232. Height = 10,
  233. Text = "TAB to jump between text fields.",
  234. Driver = driver
  235. };
  236. textView.BeginInit ();
  237. textView.EndInit ();
  238. // Verify initial state
  239. Assert.Equal (4, textView.TabWidth);
  240. Assert.True (textView.AllowsTab);
  241. Assert.True (textView.AllowsReturn);
  242. Assert.True (textView.Multiline);
  243. // Set TabWidth to -1 (should clamp to 0)
  244. textView.TabWidth = -1;
  245. Assert.Equal (0, textView.TabWidth);
  246. Assert.True (textView.AllowsTab);
  247. Assert.True (textView.AllowsReturn);
  248. Assert.True (textView.Multiline);
  249. // Insert a tab
  250. Assert.True (textView.NewKeyDownEvent (Key.Tab));
  251. Assert.Equal ("\tTAB to jump between text fields.", textView.Text);
  252. // Change TabWidth back to 4
  253. textView.TabWidth = 4;
  254. Assert.Equal (4, textView.TabWidth);
  255. // Remove the tab with BackTab
  256. Assert.True (textView.NewKeyDownEvent (Key.Tab.WithShift));
  257. Assert.Equal ("TAB to jump between text fields.", textView.Text);
  258. }
  259. [Fact]
  260. public void KeyBindings_Command_Navigation ()
  261. {
  262. IDriver driver = CreateFakeDriver ();
  263. var text = "This is the first line.\nThis is the second line.\nThis is the third line.";
  264. var textView = new TextView
  265. {
  266. Width = 10,
  267. Height = 2,
  268. Text = text,
  269. Driver = driver
  270. };
  271. textView.BeginInit ();
  272. textView.EndInit ();
  273. Assert.Equal (
  274. $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.",
  275. textView.Text
  276. );
  277. Assert.Equal (3, textView.Lines);
  278. Assert.Equal (Point.Empty, textView.CursorPosition);
  279. Assert.False (textView.ReadOnly);
  280. Assert.True (textView.CanFocus);
  281. Assert.False (textView.IsSelecting);
  282. // Test that CursorLeft doesn't move when at beginning
  283. textView.CanFocus = false;
  284. Assert.True (textView.NewKeyDownEvent (Key.CursorLeft));
  285. Assert.False (textView.IsSelecting);
  286. textView.CanFocus = true;
  287. Assert.False (textView.NewKeyDownEvent (Key.CursorLeft));
  288. Assert.False (textView.IsSelecting);
  289. // Move right
  290. Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
  291. Assert.Equal (new Point (1, 0), textView.CursorPosition);
  292. Assert.False (textView.IsSelecting);
  293. // Move to end of document
  294. Assert.True (textView.NewKeyDownEvent (Key.End.WithCtrl));
  295. Assert.Equal (2, textView.CurrentRow);
  296. Assert.Equal (23, textView.CurrentColumn);
  297. Assert.Equal (textView.CurrentColumn, textView.GetCurrentLine ().Count);
  298. Assert.Equal (new Point (23, 2), textView.CursorPosition);
  299. Assert.False (textView.IsSelecting);
  300. // Try to move right (should fail, at end)
  301. Assert.False (textView.NewKeyDownEvent (Key.CursorRight));
  302. Assert.False (textView.IsSelecting);
  303. // Type a character
  304. Assert.True (textView.NewKeyDownEvent (Key.F.WithShift));
  305. Assert.Equal (
  306. $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F",
  307. textView.Text
  308. );
  309. Assert.Equal (new Point (24, 2), textView.CursorPosition);
  310. Assert.False (textView.IsSelecting);
  311. // Undo
  312. Assert.True (textView.NewKeyDownEvent (Key.Z.WithCtrl));
  313. Assert.Equal (
  314. $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.",
  315. textView.Text
  316. );
  317. Assert.Equal (new Point (23, 2), textView.CursorPosition);
  318. Assert.False (textView.IsSelecting);
  319. // Redo
  320. Assert.True (textView.NewKeyDownEvent (Key.R.WithCtrl));
  321. Assert.Equal (
  322. $"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F",
  323. textView.Text
  324. );
  325. Assert.Equal (new Point (24, 2), textView.CursorPosition);
  326. Assert.False (textView.IsSelecting);
  327. }
  328. [Fact]
  329. public void UnwrappedCursorPosition_Event_Fires_Correctly ()
  330. {
  331. IDriver driver = CreateFakeDriver (25, 25);
  332. Point unwrappedPosition = Point.Empty;
  333. var textView = new TextView
  334. {
  335. Width = Dim.Fill (),
  336. Height = Dim.Fill (),
  337. Text = "This is the first line.\nThis is the second line.\n",
  338. Driver = driver
  339. };
  340. textView.UnwrappedCursorPosition += (s, e) => { unwrappedPosition = e; };
  341. textView.BeginInit ();
  342. textView.EndInit ();
  343. // Initially no word wrap
  344. Assert.False (textView.WordWrap);
  345. Assert.Equal (Point.Empty, textView.CursorPosition);
  346. Assert.Equal (Point.Empty, unwrappedPosition);
  347. // Enable word wrap and move cursor
  348. textView.WordWrap = true;
  349. textView.CursorPosition = new Point (12, 0);
  350. Assert.Equal (new Point (12, 0), textView.CursorPosition);
  351. Assert.Equal (new Point (12, 0), unwrappedPosition);
  352. // Resize to narrow width to force wrapping
  353. textView.Width = 6;
  354. textView.Height = 25;
  355. textView.SetRelativeLayout (new Size (6, 25));
  356. // The wrapped cursor position should differ from unwrapped
  357. // After wrap, column 12 might be on a different visual line
  358. // but unwrapped position should still be (12, 0)
  359. Assert.Equal (new Point (12, 0), unwrappedPosition);
  360. // Move right - the unwrapped position event may not fire immediately
  361. // or may be based on previous cursor position
  362. // Just verify the event mechanism works
  363. var currentUnwrapped = unwrappedPosition;
  364. Assert.True (textView.NewKeyDownEvent (Key.CursorRight));
  365. // The unwrapped position should have updated (either to 13 or still tracking properly)
  366. Assert.True (unwrappedPosition.X >= currentUnwrapped.X);
  367. }
  368. [Fact]
  369. public void Horizontal_Scrolling_Adjusts_LeftColumn ()
  370. {
  371. IDriver driver = CreateFakeDriver ();
  372. var textView = new TextView
  373. {
  374. Width = 20,
  375. Height = 5,
  376. Text = "This is a very long line that will require horizontal scrolling to see all of it",
  377. WordWrap = false,
  378. Driver = driver
  379. };
  380. textView.BeginInit ();
  381. textView.EndInit ();
  382. // Initially at the start
  383. Assert.Equal (0, textView.LeftColumn);
  384. Assert.Equal (new Point (0, 0), textView.CursorPosition);
  385. // Move to the end of the line
  386. textView.MoveEnd ();
  387. // LeftColumn should have adjusted to show the cursor
  388. Assert.True (textView.LeftColumn > 0);
  389. Assert.Equal (textView.Text.Length, textView.CurrentColumn);
  390. // Move back to the start
  391. textView.MoveHome ();
  392. // LeftColumn should be back to 0
  393. Assert.Equal (0, textView.LeftColumn);
  394. Assert.Equal (0, textView.CurrentColumn);
  395. }
  396. [Fact]
  397. public void Vertical_Scrolling_Adjusts_TopRow ()
  398. {
  399. IDriver driver = CreateFakeDriver ();
  400. var lines = string.Join ("\n", Enumerable.Range (1, 100).Select (i => $"Line {i}"));
  401. var textView = new TextView
  402. {
  403. Width = 20,
  404. Height = 5,
  405. Text = lines,
  406. Driver = driver
  407. };
  408. textView.BeginInit ();
  409. textView.EndInit ();
  410. // Initially at the top
  411. Assert.Equal (0, textView.TopRow);
  412. Assert.Equal (new Point (0, 0), textView.CursorPosition);
  413. // Move down many lines
  414. for (var i = 0; i < 50; i++)
  415. {
  416. textView.NewKeyDownEvent (Key.CursorDown);
  417. }
  418. // TopRow should have adjusted to show the cursor
  419. Assert.True (textView.TopRow > 0);
  420. Assert.Equal (50, textView.CurrentRow);
  421. // Move back to the top
  422. textView.NewKeyDownEvent (Key.Home.WithCtrl);
  423. // TopRow should be back to 0
  424. Assert.Equal (0, textView.TopRow);
  425. Assert.Equal (0, textView.CurrentRow);
  426. }
  427. }