using UICatalog; using Xunit.Abstractions; namespace Terminal.Gui.ApplicationTests; /// /// Application tests for keyboard support. /// public class KeyboardTests { private readonly ITestOutputHelper _output; public KeyboardTests (ITestOutputHelper output) { _output = output; #if DEBUG_IDISPOSABLE Responder.Instances.Clear (); RunState.Instances.Clear (); #endif } [Fact] [AutoInitShutdown] public void QuitKey_Getter_Setter () { Toplevel top = new (); var isQuiting = false; top.Closing += (s, e) => { isQuiting = true; e.Cancel = true; }; Application.Begin (top); top.Running = true; Key prevKey = Application.QuitKey; Application.OnKeyDown (Application.QuitKey); Assert.True (isQuiting); isQuiting = false; Application.OnKeyDown (Application.QuitKey); Assert.True (isQuiting); isQuiting = false; Application.QuitKey = Key.C.WithCtrl; Application.OnKeyDown (prevKey); // Should not quit Assert.False (isQuiting); Application.OnKeyDown (Key.Q.WithCtrl);// Should not quit Assert.False (isQuiting); Application.OnKeyDown (Application.QuitKey); Assert.True (isQuiting); // Reset the QuitKey to avoid throws errors on another tests Application.QuitKey = prevKey; top.Dispose (); } [Fact] public void QuitKey_Default_Is_Esc () { Application.ResetState (true); // Before Init Assert.Equal (Key.Empty, Application.QuitKey); Application.Init (new FakeDriver ()); // After Init Assert.Equal (Key.Esc, Application.QuitKey); Application.Shutdown (); } private object _timeoutLock; [Fact] public void QuitKey_Quits () { Assert.Null (_timeoutLock); _timeoutLock = new object (); uint abortTime = 500; bool initialized = false; int iteration = 0; bool shutdown = false; object timeout = null; Application.InitializedChanged += OnApplicationOnInitializedChanged; Application.Init (new FakeDriver ()); Assert.True (initialized); Assert.False (shutdown); _output.WriteLine ("Application.Run ().Dispose ().."); Application.Run ().Dispose (); _output.WriteLine ("Back from Application.Run ().Dispose ()"); Assert.True (initialized); Assert.False (shutdown); Assert.Equal (1, iteration); Application.Shutdown (); Application.InitializedChanged -= OnApplicationOnInitializedChanged; lock (_timeoutLock) { if (timeout is { }) { Application.RemoveTimeout (timeout); timeout = null; } } Assert.True (initialized); Assert.True (shutdown); #if DEBUG_IDISPOSABLE Assert.Empty (Responder.Instances); #endif lock (_timeoutLock) { _timeoutLock = null; } return; void OnApplicationOnInitializedChanged (object s, EventArgs a) { _output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.CurrentValue); if (a.CurrentValue) { Application.Iteration += OnApplicationOnIteration; initialized = true; lock (_timeoutLock) { timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback); } } else { Application.Iteration -= OnApplicationOnIteration; shutdown = true; } } bool ForceCloseCallback () { lock (_timeoutLock) { _output.WriteLine ($"ForceCloseCallback. iteration: {iteration}"); if (timeout is { }) { timeout = null; } } Application.ResetState (true); Assert.Fail ($"Failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit."); return false; } void OnApplicationOnIteration (object s, IterationEventArgs a) { _output.WriteLine ("Iteration: {0}", iteration); iteration++; Assert.True (iteration < 2, "Too many iterations, something is wrong."); if (Application.IsInitialized) { _output.WriteLine (" Pressing QuitKey"); Application.OnKeyDown (Application.QuitKey); } } } [Fact (Skip = "Replace when new key statics are added.")] public void AlternateForwardKey_AlternateBackwardKey_Tests () { Application.Init (new FakeDriver ()); Toplevel top = new (); var w1 = new Window (); var v1 = new TextField (); var v2 = new TextView (); w1.Add (v1, v2); var w2 = new Window (); var v3 = new CheckBox (); var v4 = new Button (); w2.Add (v3, v4); top.Add (w1, w2); Application.Iteration += (s, a) => { Assert.True (v1.HasFocus); // Using default keys. Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v2.HasFocus); Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v3.HasFocus); Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v4.HasFocus); Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (v1.HasFocus); Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v4.HasFocus); Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v3.HasFocus); Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v2.HasFocus); Application.OnKeyDown (Key.Tab.WithShift.WithCtrl); Assert.True (v1.HasFocus); Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v2.HasFocus); Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v3.HasFocus); Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v4.HasFocus); Application.OnKeyDown (Key.PageDown.WithCtrl); Assert.True (v1.HasFocus); Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v4.HasFocus); Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v3.HasFocus); Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v2.HasFocus); Application.OnKeyDown (Key.PageUp.WithCtrl); Assert.True (v1.HasFocus); // Using another's alternate keys. Application.AlternateForwardKey = Key.F7; Application.AlternateBackwardKey = Key.F6; Application.OnKeyDown (Key.F7); Assert.True (v2.HasFocus); Application.OnKeyDown (Key.F7); Assert.True (v3.HasFocus); Application.OnKeyDown (Key.F7); Assert.True (v4.HasFocus); Application.OnKeyDown (Key.F7); Assert.True (v1.HasFocus); Application.OnKeyDown (Key.F6); Assert.True (v4.HasFocus); Application.OnKeyDown (Key.F6); Assert.True (v3.HasFocus); Application.OnKeyDown (Key.F6); Assert.True (v2.HasFocus); Application.OnKeyDown (Key.F6); Assert.True (v1.HasFocus); Application.RequestStop (); }; Application.Run (top); // Replacing the defaults keys to avoid errors on others unit tests that are using it. Application.AlternateForwardKey = Key.PageDown.WithCtrl; Application.AlternateBackwardKey = Key.PageUp.WithCtrl; Application.QuitKey = Key.Q.WithCtrl; Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.AlternateForwardKey.KeyCode); Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.AlternateBackwardKey.KeyCode); Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); top.Dispose (); // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); } [Fact] [AutoInitShutdown] public void EnsuresTopOnFront_CanFocus_False_By_Keyboard () { Toplevel top = new (); var win = new Window { Title = "win", X = 0, Y = 0, Width = 20, Height = 10 }; var tf = new TextField { Width = 10 }; win.Add (tf); var win2 = new Window { Title = "win2", X = 22, Y = 0, Width = 20, Height = 10 }; var tf2 = new TextField { Width = 10 }; win2.Add (tf2); top.Add (win, win2); Application.Begin (top); Assert.True (win.CanFocus); Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); win.CanFocus = false; Assert.False (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (win2.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); Application.OnKeyDown (Key.Tab.WithCtrl); Assert.False (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); top.Dispose (); } [Fact] [AutoInitShutdown] public void EnsuresTopOnFront_CanFocus_True_By_Keyboard () { Toplevel top = new (); var win = new Window { Title = "win", X = 0, Y = 0, Width = 20, Height = 10 }; var tf = new TextField { Width = 10 }; win.Add (tf); var win2 = new Window { Title = "win2", X = 22, Y = 0, Width = 20, Height = 10 }; var tf2 = new TextField { Width = 10 }; win2.Add (tf2); top.Add (win, win2); Application.Begin (top); Assert.True (win.CanFocus); Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (win.CanFocus); Assert.False (win.HasFocus); Assert.True (win2.CanFocus); Assert.True (win2.HasFocus); Assert.Equal ("win2", ((Window)top.Subviews [^1]).Title); Application.OnKeyDown (Key.Tab.WithCtrl); Assert.True (win.CanFocus); Assert.True (win.HasFocus); Assert.True (win2.CanFocus); Assert.False (win2.HasFocus); Assert.Equal ("win", ((Window)top.Subviews [^1]).Title); top.Dispose (); } [Fact] public void KeyUp_Event () { Application.Init (new FakeDriver ()); // Setup some fake keypresses (This) var input = "Tests"; Key originalQuitKey = Application.QuitKey; Application.QuitKey = Key.Q.WithCtrl; // Put a control-q in at the end FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo ('Q', ConsoleKey.Q, false, false, true)); foreach (char c in input.Reverse ()) { if (char.IsLetter (c)) { FakeConsole.MockKeyPresses.Push ( new ConsoleKeyInfo ( c, (ConsoleKey)char.ToUpper (c), char.IsUpper (c), false, false ) ); } else { FakeConsole.MockKeyPresses.Push ( new ConsoleKeyInfo ( c, (ConsoleKey)c, false, false, false ) ); } } int stackSize = FakeConsole.MockKeyPresses.Count; var iterations = 0; Application.Iteration += (s, a) => { iterations++; // Stop if we run out of control... if (iterations > 10) { Application.RequestStop (); } }; var keyUps = 0; var output = string.Empty; var top = new Toplevel (); top.KeyUp += (sender, args) => { if (args.KeyCode != (KeyCode.CtrlMask | KeyCode.Q)) { output += args.AsRune; } keyUps++; }; Application.Run (top); Application.QuitKey = originalQuitKey; // Input string should match output Assert.Equal (input, output); // # of key up events should match stack size //Assert.Equal (stackSize, keyUps); // We can't use numbers variables on the left side of an Assert.Equal/NotEqual, // it must be literal (Linux only). Assert.Equal (6, keyUps); // # of key up events should match # of iterations Assert.Equal (stackSize, iterations); top.Dispose (); Application.Shutdown (); Assert.Null (Application.Current); Assert.Null (Application.Top); Assert.Null (Application.MainLoop); Assert.Null (Application.Driver); } [Fact] [AutoInitShutdown] public void KeyBinding_OnKeyDown () { var view = new ScopedKeyBindingView (); var invoked = false; view.InvokingKeyBindings += (s, e) => invoked = true; var top = new Toplevel (); top.Add (view); Application.Begin (top); Application.OnKeyDown (Key.A); Assert.False (invoked); Assert.True (view.ApplicationCommand); invoked = false; view.ApplicationCommand = false; Application.KeyBindings.Remove (KeyCode.A); Application.OnKeyDown (Key.A); // old Assert.False (invoked); Assert.False (view.ApplicationCommand); Application.KeyBindings.Add (Key.A.WithCtrl, view, Command.Save); Application.OnKeyDown (Key.A); // old Assert.False (invoked); Assert.False (view.ApplicationCommand); Application.OnKeyDown (Key.A.WithCtrl); // new Assert.False (invoked); Assert.True (view.ApplicationCommand); invoked = false; Application.OnKeyDown (Key.H); Assert.True (invoked); invoked = false; Assert.False (view.HasFocus); Application.OnKeyDown (Key.F); Assert.False (invoked); Assert.True (view.ApplicationCommand); Assert.True (view.HotKeyCommand); Assert.False (view.FocusedCommand); top.Dispose (); } [Fact] [AutoInitShutdown] public void KeyBinding_OnKeyDown_Negative () { var view = new ScopedKeyBindingView (); var invoked = false; view.InvokingKeyBindings += (s, e) => invoked = true; var top = new Toplevel (); top.Add (view); Application.Begin (top); Application.OnKeyDown (Key.A.WithCtrl); Assert.False (invoked); Assert.False (view.ApplicationCommand); Assert.False (view.HotKeyCommand); Assert.False (view.FocusedCommand); invoked = false; Assert.False (view.HasFocus); Application.OnKeyDown (Key.Z); Assert.False (invoked); Assert.False (view.ApplicationCommand); Assert.False (view.HotKeyCommand); Assert.False (view.FocusedCommand); top.Dispose (); } [Fact] [AutoInitShutdown] public void KeyBinding_Application_KeyBindings_Add_Adds () { Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); Application.KeyBindings.Add (Key.B, KeyBindingScope.Application, Command.Accept); Assert.True (Application.KeyBindings.TryGet (Key.A, out var binding)); Assert.Null (binding.BoundView); Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); Assert.Null (binding.BoundView); } [Fact] [AutoInitShutdown] public void KeyBinding_View_KeyBindings_Add_Adds () { View view1 = new (); Application.KeyBindings.Add (Key.A, view1, Command.Accept); View view2 = new (); Application.KeyBindings.Add (Key.B, view2, Command.Accept); Assert.True (Application.KeyBindings.TryGet (Key.A, out var binding)); Assert.Equal (view1, binding.BoundView); Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); Assert.Equal (view2, binding.BoundView); } [Fact] [AutoInitShutdown] public void KeyBinding_Application_RemoveKeyBinding_Removes () { Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); Application.KeyBindings.Remove (Key.A); Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); } [Fact] [AutoInitShutdown] public void KeyBinding_View_KeyBindings_RemoveKeyBinding_Removes () { View view1 = new (); Application.KeyBindings.Add (Key.A, view1, Command.Accept); View view2 = new (); Application.KeyBindings.Add (Key.B, view1, Command.Accept); Application.KeyBindings.Remove (Key.A, view1); Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); } // Test View for testing Application key Bindings public class ScopedKeyBindingView : View { public ScopedKeyBindingView () { AddCommand (Command.Save, () => ApplicationCommand = true); AddCommand (Command.HotKey, () => HotKeyCommand = true); AddCommand (Command.Left, () => FocusedCommand = true); Application.KeyBindings.Add (Key.A, this, Command.Save); HotKey = KeyCode.H; KeyBindings.Add (Key.F, Command.Left); } public bool ApplicationCommand { get; set; } public bool FocusedCommand { get; set; } public bool HotKeyCommand { get; set; } } }