GuiTestContext.cs 34 KB

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