GuiTestContext.cs 34 KB

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