GuiTestContext.cs 28 KB

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