GuiTestContext.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. using System.Drawing;
  2. using System.Text;
  3. using Microsoft.Extensions.Logging;
  4. using Terminal.Gui;
  5. using Terminal.Gui.ConsoleDrivers;
  6. namespace TerminalGuiFluentTesting;
  7. /// <summary>
  8. /// Fluent API context for testing a Terminal.Gui application. Create
  9. /// an instance using <see cref="With"/> static class.
  10. /// </summary>
  11. public class GuiTestContext : IDisposable
  12. {
  13. private readonly CancellationTokenSource _cts = new ();
  14. private readonly CancellationTokenSource _hardStop = new (With.Timeout);
  15. private readonly Task _runTask;
  16. private Exception _ex;
  17. private readonly FakeOutput _output = new ();
  18. private readonly FakeWindowsInput _winInput;
  19. private readonly FakeNetInput _netInput;
  20. private View? _lastView;
  21. private readonly StringBuilder _logsSb;
  22. private readonly V2TestDriver _driver;
  23. private bool _finished;
  24. internal GuiTestContext (Func<Toplevel> topLevelBuilder, int width, int height, V2TestDriver driver)
  25. {
  26. IApplication origApp = ApplicationImpl.Instance;
  27. ILogger? origLogger = Logging.Logger;
  28. _logsSb = new ();
  29. _driver = driver;
  30. _netInput = new (_cts.Token);
  31. _winInput = new (_cts.Token);
  32. _output.Size = new (width, height);
  33. var v2 = new ApplicationV2 (
  34. () => _netInput,
  35. () => _output,
  36. () => _winInput,
  37. () => _output);
  38. var booting = new SemaphoreSlim (0, 1);
  39. // Start the application in a background thread
  40. _runTask = Task.Run (
  41. () =>
  42. {
  43. try
  44. {
  45. ApplicationImpl.ChangeInstance (v2);
  46. ILogger logger = LoggerFactory.Create (
  47. builder =>
  48. builder.SetMinimumLevel (LogLevel.Trace)
  49. .AddProvider (new TextWriterLoggerProvider (new StringWriter (_logsSb))))
  50. .CreateLogger ("Test Logging");
  51. Logging.Logger = logger;
  52. v2.Init (null, GetDriverName ());
  53. booting.Release ();
  54. Toplevel t = topLevelBuilder ();
  55. t.Closed += (s, e) => { _finished = true; };
  56. Application.Run (t); // This will block, but it's on a background thread now
  57. t.Dispose ();
  58. Application.Shutdown ();
  59. }
  60. catch (OperationCanceledException)
  61. { }
  62. catch (Exception ex)
  63. {
  64. _ex = ex;
  65. }
  66. finally
  67. {
  68. ApplicationImpl.ChangeInstance (origApp);
  69. Logging.Logger = origLogger;
  70. _finished = true;
  71. }
  72. },
  73. _cts.Token);
  74. // Wait for booting to complete with a timeout to avoid hangs
  75. if (!booting.WaitAsync (TimeSpan.FromSeconds (5)).Result)
  76. {
  77. throw new TimeoutException ("Application failed to start within the allotted time.");
  78. }
  79. WaitIteration ();
  80. }
  81. private string GetDriverName ()
  82. {
  83. return _driver switch
  84. {
  85. V2TestDriver.V2Win => "v2win",
  86. V2TestDriver.V2Net => "v2net",
  87. _ =>
  88. throw new ArgumentOutOfRangeException ()
  89. };
  90. }
  91. /// <summary>
  92. /// Stops the application and waits for the background thread to exit.
  93. /// </summary>
  94. public GuiTestContext Stop ()
  95. {
  96. if (_runTask.IsCompleted)
  97. {
  98. return this;
  99. }
  100. Application.Invoke (() => { Application.RequestStop (); });
  101. // Wait for the application to stop, but give it a 1-second timeout
  102. if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000)))
  103. {
  104. _cts.Cancel ();
  105. // Timeout occurred, force the task to stop
  106. _hardStop.Cancel ();
  107. throw new TimeoutException ("Application failed to stop within the allotted time.");
  108. }
  109. _cts.Cancel ();
  110. if (_ex != null)
  111. {
  112. throw _ex; // Propagate any exception that happened in the background task
  113. }
  114. return this;
  115. }
  116. /// <summary>
  117. /// Hard stops the application and waits for the background thread to exit.
  118. /// </summary>
  119. public void HardStop ()
  120. {
  121. _hardStop.Cancel ();
  122. Stop ();
  123. }
  124. /// <summary>
  125. /// Cleanup to avoid state bleed between tests
  126. /// </summary>
  127. public void Dispose ()
  128. {
  129. Stop ();
  130. if (_hardStop.IsCancellationRequested)
  131. {
  132. throw new (
  133. "Application was hard stopped, typically this means it timed out or did not shutdown gracefully. Ensure you call Stop in your test");
  134. }
  135. _hardStop.Cancel ();
  136. }
  137. /// <summary>
  138. /// Adds the given <paramref name="v"/> to the current top level view
  139. /// and performs layout.
  140. /// </summary>
  141. /// <param name="v"></param>
  142. /// <returns></returns>
  143. public GuiTestContext Add (View v)
  144. {
  145. WaitIteration (
  146. () =>
  147. {
  148. Toplevel top = Application.Top ?? throw new ("Top was null so could not add view");
  149. top.Add (v);
  150. top.Layout ();
  151. _lastView = v;
  152. });
  153. return this;
  154. }
  155. /// <summary>
  156. /// Simulates changing the console size e.g. by resizing window in your operating system
  157. /// </summary>
  158. /// <param name="width">new Width for the console.</param>
  159. /// <param name="height">new Height for the console.</param>
  160. /// <returns></returns>
  161. public GuiTestContext ResizeConsole (int width, int height)
  162. {
  163. _output.Size = new (width, height);
  164. return WaitIteration ();
  165. }
  166. public GuiTestContext ScreenShot (string title, TextWriter writer)
  167. {
  168. writer.WriteLine (title + ":");
  169. var text = Application.ToString ();
  170. writer.WriteLine (text);
  171. return this; //WaitIteration();
  172. }
  173. /// <summary>
  174. /// Writes all Terminal.Gui engine logs collected so far to the <paramref name="writer"/>
  175. /// </summary>
  176. /// <param name="writer"></param>
  177. /// <returns></returns>
  178. public GuiTestContext WriteOutLogs (TextWriter writer)
  179. {
  180. writer.WriteLine (_logsSb.ToString ());
  181. return this; //WaitIteration();
  182. }
  183. /// <summary>
  184. /// Waits until the end of the current iteration of the main loop. Optionally
  185. /// running a given <paramref name="a"/> action on the UI thread at that time.
  186. /// </summary>
  187. /// <param name="a"></param>
  188. /// <returns></returns>
  189. public GuiTestContext WaitIteration (Action? a = null)
  190. {
  191. // If application has already exited don't wait!
  192. if (_finished || _cts.Token.IsCancellationRequested || _hardStop.Token.IsCancellationRequested)
  193. {
  194. return this;
  195. }
  196. a ??= () => { };
  197. var ctsLocal = new CancellationTokenSource ();
  198. Application.Invoke (
  199. () =>
  200. {
  201. a ();
  202. ctsLocal.Cancel ();
  203. });
  204. // Blocks until either the token or the hardStopToken is cancelled.
  205. WaitHandle.WaitAny (
  206. new []
  207. {
  208. _cts.Token.WaitHandle,
  209. _hardStop.Token.WaitHandle,
  210. ctsLocal.Token.WaitHandle
  211. });
  212. return this;
  213. }
  214. /// <summary>
  215. /// Performs the supplied <paramref name="doAction"/> immediately.
  216. /// Enables running commands without breaking the Fluent API calls.
  217. /// </summary>
  218. /// <param name="doAction"></param>
  219. /// <returns></returns>
  220. public GuiTestContext Then (Action doAction)
  221. {
  222. try
  223. {
  224. doAction ();
  225. }
  226. catch (Exception)
  227. {
  228. HardStop ();
  229. throw;
  230. }
  231. return this;
  232. }
  233. /// <summary>
  234. /// Simulates a right click at the given screen coordinates on the current driver.
  235. /// This is a raw input event that goes through entire processing pipeline as though
  236. /// user had pressed the mouse button physically.
  237. /// </summary>
  238. /// <param name="screenX">0 indexed screen coordinates</param>
  239. /// <param name="screenY">0 indexed screen coordinates</param>
  240. /// <returns></returns>
  241. public GuiTestContext RightClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button3Pressed, screenX, screenY); }
  242. /// <summary>
  243. /// Simulates a left click at the given screen coordinates on the current driver.
  244. /// This is a raw input event that goes through entire processing pipeline as though
  245. /// user had pressed the mouse button physically.
  246. /// </summary>
  247. /// <param name="screenX">0 indexed screen coordinates</param>
  248. /// <param name="screenY">0 indexed screen coordinates</param>
  249. /// <returns></returns>
  250. public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); }
  251. public GuiTestContext LeftClick<T> (Func<T, bool> evaluator) where T : View { return Click (WindowsConsole.ButtonState.Button1Pressed, evaluator); }
  252. private GuiTestContext Click<T> (WindowsConsole.ButtonState btn, Func<T, bool> evaluator) where T : View
  253. {
  254. T v = Find (evaluator);
  255. Point screen = v.ViewportToScreen (new Point (0, 0));
  256. return Click (btn, screen.X, screen.Y);
  257. }
  258. private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY)
  259. {
  260. switch (_driver)
  261. {
  262. case V2TestDriver.V2Win:
  263. _winInput.InputBuffer.Enqueue (
  264. new ()
  265. {
  266. EventType = WindowsConsole.EventType.Mouse,
  267. MouseEvent = new ()
  268. {
  269. ButtonState = btn,
  270. MousePosition = new ((short)screenX, (short)screenY)
  271. }
  272. });
  273. _winInput.InputBuffer.Enqueue (
  274. new ()
  275. {
  276. EventType = WindowsConsole.EventType.Mouse,
  277. MouseEvent = new ()
  278. {
  279. ButtonState = WindowsConsole.ButtonState.NoButtonPressed,
  280. MousePosition = new ((short)screenX, (short)screenY)
  281. }
  282. });
  283. break;
  284. case V2TestDriver.V2Net:
  285. int netButton = btn switch
  286. {
  287. WindowsConsole.ButtonState.Button1Pressed => 0,
  288. WindowsConsole.ButtonState.Button2Pressed => 1,
  289. WindowsConsole.ButtonState.Button3Pressed => 2,
  290. WindowsConsole.ButtonState.RightmostButtonPressed => 2,
  291. _ => throw new ArgumentOutOfRangeException (nameof (btn))
  292. };
  293. foreach (ConsoleKeyInfo k in NetSequences.Click (netButton, screenX, screenY))
  294. {
  295. SendNetKey (k);
  296. }
  297. break;
  298. default:
  299. throw new ArgumentOutOfRangeException ();
  300. }
  301. return WaitIteration ();
  302. ;
  303. }
  304. public GuiTestContext Down ()
  305. {
  306. switch (_driver)
  307. {
  308. case V2TestDriver.V2Win:
  309. SendWindowsKey (ConsoleKeyMapping.VK.DOWN);
  310. break;
  311. case V2TestDriver.V2Net:
  312. foreach (ConsoleKeyInfo k in NetSequences.Down)
  313. {
  314. SendNetKey (k);
  315. }
  316. break;
  317. default:
  318. throw new ArgumentOutOfRangeException ();
  319. }
  320. return WaitIteration ();
  321. ;
  322. }
  323. /// <summary>
  324. /// Simulates the Right cursor key
  325. /// </summary>
  326. /// <returns></returns>
  327. /// <exception cref="ArgumentOutOfRangeException"></exception>
  328. public GuiTestContext Right ()
  329. {
  330. switch (_driver)
  331. {
  332. case V2TestDriver.V2Win:
  333. SendWindowsKey (ConsoleKeyMapping.VK.RIGHT);
  334. break;
  335. case V2TestDriver.V2Net:
  336. foreach (ConsoleKeyInfo k in NetSequences.Right)
  337. {
  338. SendNetKey (k);
  339. }
  340. WaitIteration ();
  341. break;
  342. default:
  343. throw new ArgumentOutOfRangeException ();
  344. }
  345. return WaitIteration ();
  346. }
  347. /// <summary>
  348. /// Simulates the Left cursor key
  349. /// </summary>
  350. /// <returns></returns>
  351. /// <exception cref="ArgumentOutOfRangeException"></exception>
  352. public GuiTestContext Left ()
  353. {
  354. switch (_driver)
  355. {
  356. case V2TestDriver.V2Win:
  357. SendWindowsKey (ConsoleKeyMapping.VK.LEFT);
  358. break;
  359. case V2TestDriver.V2Net:
  360. foreach (ConsoleKeyInfo k in NetSequences.Left)
  361. {
  362. SendNetKey (k);
  363. }
  364. break;
  365. default:
  366. throw new ArgumentOutOfRangeException ();
  367. }
  368. return WaitIteration ();
  369. }
  370. /// <summary>
  371. /// Simulates the up cursor key
  372. /// </summary>
  373. /// <returns></returns>
  374. /// <exception cref="ArgumentOutOfRangeException"></exception>
  375. public GuiTestContext Up ()
  376. {
  377. switch (_driver)
  378. {
  379. case V2TestDriver.V2Win:
  380. SendWindowsKey (ConsoleKeyMapping.VK.UP);
  381. break;
  382. case V2TestDriver.V2Net:
  383. foreach (ConsoleKeyInfo k in NetSequences.Up)
  384. {
  385. SendNetKey (k);
  386. }
  387. break;
  388. default:
  389. throw new ArgumentOutOfRangeException ();
  390. }
  391. return WaitIteration ();
  392. }
  393. /// <summary>
  394. /// Simulates pressing the Return/Enter (newline) key.
  395. /// </summary>
  396. /// <returns></returns>
  397. /// <exception cref="ArgumentOutOfRangeException"></exception>
  398. public GuiTestContext Enter ()
  399. {
  400. switch (_driver)
  401. {
  402. case V2TestDriver.V2Win:
  403. SendWindowsKey (
  404. new WindowsConsole.KeyEventRecord
  405. {
  406. UnicodeChar = '\r',
  407. dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
  408. wRepeatCount = 1,
  409. wVirtualKeyCode = ConsoleKeyMapping.VK.RETURN,
  410. wVirtualScanCode = 28
  411. });
  412. break;
  413. case V2TestDriver.V2Net:
  414. SendNetKey (new ('\r', ConsoleKey.Enter, false, false, false));
  415. break;
  416. default:
  417. throw new ArgumentOutOfRangeException ();
  418. }
  419. return WaitIteration ();
  420. }
  421. /// <summary>
  422. /// Simulates pressing the Esc (Escape) key.
  423. /// </summary>
  424. /// <returns></returns>
  425. /// <exception cref="ArgumentOutOfRangeException"></exception>
  426. public GuiTestContext Escape ()
  427. {
  428. switch (_driver)
  429. {
  430. case V2TestDriver.V2Win:
  431. SendWindowsKey (
  432. new WindowsConsole.KeyEventRecord
  433. {
  434. UnicodeChar = '\u001b',
  435. dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
  436. wRepeatCount = 1,
  437. wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE,
  438. wVirtualScanCode = 1
  439. });
  440. break;
  441. case V2TestDriver.V2Net:
  442. // Note that this accurately describes how Esc comes in. Typically, ConsoleKey is None
  443. // even though you would think it would be Escape - it isn't
  444. SendNetKey (new ('\u001b', ConsoleKey.None, false, false, false));
  445. break;
  446. default:
  447. throw new ArgumentOutOfRangeException ();
  448. }
  449. return this;
  450. }
  451. /// <summary>
  452. /// Simulates pressing the Tab key.
  453. /// </summary>
  454. /// <returns></returns>
  455. /// <exception cref="ArgumentOutOfRangeException"></exception>
  456. public GuiTestContext Tab ()
  457. {
  458. switch (_driver)
  459. {
  460. case V2TestDriver.V2Win:
  461. SendWindowsKey (
  462. new WindowsConsole.KeyEventRecord
  463. {
  464. UnicodeChar = '\t',
  465. dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed,
  466. wRepeatCount = 1,
  467. wVirtualKeyCode = 0,
  468. wVirtualScanCode = 0
  469. });
  470. break;
  471. case V2TestDriver.V2Net:
  472. // Note that this accurately describes how Tab comes in. Typically, ConsoleKey is None
  473. // even though you would think it would be Tab - it isn't
  474. SendNetKey (new ('\t', ConsoleKey.None, false, false, false));
  475. break;
  476. default:
  477. throw new ArgumentOutOfRangeException ();
  478. }
  479. return this;
  480. }
  481. /// <summary>
  482. /// Registers a right click handler on the <see cref="LastView"/> added view (or root view) that
  483. /// will open the supplied <paramref name="contextMenu"/>.
  484. /// </summary>
  485. /// <param name="contextMenu"></param>
  486. /// <returns></returns>
  487. public GuiTestContext WithContextMenu (PopoverMenu? contextMenu)
  488. {
  489. LastView.MouseEvent += (s, e) =>
  490. {
  491. if (e.Flags.HasFlag (MouseFlags.Button3Clicked))
  492. {
  493. // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused
  494. // and the context menu is disposed when it is closed.
  495. Application.Popover?.Register (contextMenu);
  496. contextMenu?.MakeVisible (e.ScreenPosition);
  497. }
  498. };
  499. return this;
  500. }
  501. /// <summary>
  502. /// The last view added (e.g. with <see cref="Add"/>) or the root/current top.
  503. /// </summary>
  504. public View LastView => _lastView ?? Application.Top ?? throw new ("Could not determine which view to add to");
  505. /// <summary>
  506. /// Send a full windows OS key including both down and up.
  507. /// </summary>
  508. /// <param name="fullKey"></param>
  509. private void SendWindowsKey (WindowsConsole.KeyEventRecord fullKey)
  510. {
  511. WindowsConsole.KeyEventRecord down = fullKey;
  512. WindowsConsole.KeyEventRecord up = fullKey; // because struct this is new copy
  513. down.bKeyDown = true;
  514. up.bKeyDown = false;
  515. _winInput.InputBuffer.Enqueue (
  516. new ()
  517. {
  518. EventType = WindowsConsole.EventType.Key,
  519. KeyEvent = down
  520. });
  521. _winInput.InputBuffer.Enqueue (
  522. new ()
  523. {
  524. EventType = WindowsConsole.EventType.Key,
  525. KeyEvent = up
  526. });
  527. WaitIteration ();
  528. }
  529. private void SendNetKey (ConsoleKeyInfo consoleKeyInfo) { _netInput.InputBuffer.Enqueue (consoleKeyInfo); }
  530. /// <summary>
  531. /// Sends a special key e.g. cursor key that does not map to a specific character
  532. /// </summary>
  533. /// <param name="specialKey"></param>
  534. private void SendWindowsKey (ConsoleKeyMapping.VK specialKey)
  535. {
  536. _winInput.InputBuffer.Enqueue (
  537. new ()
  538. {
  539. EventType = WindowsConsole.EventType.Key,
  540. KeyEvent = new ()
  541. {
  542. bKeyDown = true,
  543. wRepeatCount = 0,
  544. wVirtualKeyCode = specialKey,
  545. wVirtualScanCode = 0,
  546. UnicodeChar = '\0',
  547. dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
  548. }
  549. });
  550. _winInput.InputBuffer.Enqueue (
  551. new ()
  552. {
  553. EventType = WindowsConsole.EventType.Key,
  554. KeyEvent = new ()
  555. {
  556. bKeyDown = false,
  557. wRepeatCount = 0,
  558. wVirtualKeyCode = specialKey,
  559. wVirtualScanCode = 0,
  560. UnicodeChar = '\0',
  561. dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed
  562. }
  563. });
  564. WaitIteration ();
  565. }
  566. /// <summary>
  567. /// Sends a key to the application. This goes directly to Application and does not go through
  568. /// a driver.
  569. /// </summary>
  570. /// <param name="key"></param>
  571. /// <returns></returns>
  572. public GuiTestContext RaiseKeyDownEvent (Key key)
  573. {
  574. Application.RaiseKeyDownEvent (key);
  575. return this; //WaitIteration();
  576. }
  577. /// <summary>
  578. /// Sets the input focus to the given <see cref="View"/>.
  579. /// Throws <see cref="ArgumentException"/> if focus did not change due to system
  580. /// constraints e.g. <paramref name="toFocus"/>
  581. /// <see cref="View.CanFocus"/> is <see langword="false"/>
  582. /// </summary>
  583. /// <param name="toFocus"></param>
  584. /// <returns></returns>
  585. /// <exception cref="ArgumentException"></exception>
  586. public GuiTestContext Focus (View toFocus)
  587. {
  588. toFocus.FocusDeepest (NavigationDirection.Forward, TabBehavior.TabStop);
  589. if (!toFocus.HasFocus)
  590. {
  591. throw new ArgumentException ("Failed to set focus, FocusDeepest did not result in HasFocus becoming true. Ensure view is added and focusable");
  592. }
  593. return WaitIteration ();
  594. }
  595. /// <summary>
  596. /// Tabs through the UI until a View matching the <paramref name="evaluator"/>
  597. /// is found (of Type T) or all views are looped through (back to the beginning)
  598. /// in which case triggers hard stop and Exception
  599. /// </summary>
  600. /// <param name="evaluator">Delegate that returns true if the passed View is the one
  601. /// you are trying to focus. Leave <see langword="null"/> to focus the first view of type
  602. /// <typeparamref name="T"/></param>
  603. /// <returns></returns>
  604. /// <exception cref="ArgumentException"></exception>
  605. public GuiTestContext Focus<T> (Func<T, bool>? evaluator = null) where T : View
  606. {
  607. evaluator ??= _ => true;
  608. Toplevel? t = Application.Top;
  609. HashSet<View> seen = new ();
  610. if (t == null)
  611. {
  612. Fail ("Application.Top was null when trying to set focus");
  613. return this;
  614. }
  615. do
  616. {
  617. View? next = t.MostFocused;
  618. // Is view found?
  619. if (next is T v && evaluator (v))
  620. {
  621. return this;
  622. }
  623. // No, try tab to the next (or first)
  624. Tab ();
  625. WaitIteration ();
  626. next = t.MostFocused;
  627. if (next is null)
  628. {
  629. Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null");
  630. return this;
  631. }
  632. // Track the views we have seen
  633. // We have looped around to the start again if it was already there
  634. if (!seen.Add (next))
  635. {
  636. Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test before looping back to the original View");
  637. return this;
  638. }
  639. }
  640. while (true);
  641. }
  642. private T Find<T> (Func<T, bool> evaluator) where T : View
  643. {
  644. Toplevel? t = Application.Top;
  645. if (t == null)
  646. {
  647. Fail ("Application.Top was null when attempting to find view");
  648. }
  649. T? f = FindRecursive (t!, evaluator);
  650. if (f == null)
  651. {
  652. Fail ("Failed to tab to a view which matched the Type and evaluator constraints in any SubViews of top");
  653. }
  654. return f!;
  655. }
  656. private T? FindRecursive<T> (View current, Func<T, bool> evaluator) where T : View
  657. {
  658. foreach (View subview in current.SubViews)
  659. {
  660. if (subview is T match && evaluator (match))
  661. {
  662. return match;
  663. }
  664. // Recursive call
  665. T? result = FindRecursive (subview, evaluator);
  666. if (result != null)
  667. {
  668. return result;
  669. }
  670. }
  671. return null;
  672. }
  673. private void Fail (string reason)
  674. {
  675. Stop ();
  676. throw new (reason);
  677. }
  678. public GuiTestContext Send (Key key)
  679. {
  680. if (Application.Driver is IConsoleDriverFacade facade)
  681. {
  682. facade.InputProcessor.OnKeyDown (key);
  683. facade.InputProcessor.OnKeyUp (key);
  684. }
  685. else
  686. {
  687. Fail ("Expected Application.Driver to be IConsoleDriverFacade");
  688. }
  689. return this;
  690. }
  691. /// <summary>
  692. /// Returns the last set position of the cursor.
  693. /// </summary>
  694. /// <returns></returns>
  695. public Point GetCursorPosition ()
  696. {
  697. return _output.CursorPosition;
  698. }
  699. }