From 5c03b96242af6116f61355b8e08155360c626596 Mon Sep 17 00:00:00 2001 From: BDisp Date: Thu, 24 Apr 2025 18:00:30 +0100 Subject: [PATCH] Fixes #3966. TextField crashes app when pasting unicode surrogate pair (#3982) * Add constructor Key(int) and operator for handled with non-Bmp. * Fix TextField non-BMP issues * Fix TextField PositionCursor. * Reformat * Add IsValidInput method to handle clipboard paste when pressing CTRL+V in WT * Add handle IsValidInput in FakeDriver and unit tests * Fixes #3984 - `Margin` w/out shadow should not force draw (#3985) * shortcut tests * Generic demos * Optimize Margin to not defer draw if there's no shadow * Fixes #4041. WSLClipboard doesn't handles well with surrogate pairs * Avoid running Clipboard.Contents twice * Fixes #4042. Microsoft.VisualStudio.TestPlatform.ObjectModel.TestPlatformException: Could not find testhost * Moving tests to the parallelizable unit tests * Remove unused folder * Prevent warnings about not installed nuget packages * Using Toplevel instead of Application.Top * Cleanup code --------- Co-authored-by: Tig --- Terminal.Gui/Clipboard/ClipboardBase.cs | 2 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 34 ++ .../CursesDriver/ClipboardImpl.cs | 2 +- .../CursesDriver/CursesDriver.cs | 7 +- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 8 +- .../ConsoleDrivers/NetDriver/NetDriver.cs | 7 +- .../WindowsDriver/WindowsDriver.cs | 9 +- Terminal.Gui/Input/Keyboard/Key.cs | 52 +- Terminal.Gui/Views/TextField.cs | 30 +- .../TerminalGuiFluentTesting.Xunit.csproj | 14 - .../XunitContextExtensions.cs | 25 - .../TerminalGuiFluentTestingXunit.csproj | 2 + .../ConsoleDrivers/ConsoleDriverTests.cs | 111 ++++ Tests/UnitTests/Views/TextFieldTests.cs | 520 ---------------- .../Input/Keyboard/KeyTests.cs | 53 +- .../Views/TextFieldTests.cs | 554 ++++++++++++++++++ 16 files changed, 831 insertions(+), 599 deletions(-) delete mode 100644 TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj delete mode 100644 TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs create mode 100644 Tests/UnitTestsParallelizable/Views/TextFieldTests.cs diff --git a/Terminal.Gui/Clipboard/ClipboardBase.cs b/Terminal.Gui/Clipboard/ClipboardBase.cs index 8c1c0a93a..2406cfe47 100644 --- a/Terminal.Gui/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/Clipboard/ClipboardBase.cs @@ -22,7 +22,7 @@ public abstract class ClipboardBase : IClipboard return string.Empty; } - return GetClipboardDataImpl (); + return result; } catch (NotSupportedException ex) { diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 8d418dddb..fb5136316 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -691,6 +691,40 @@ public abstract class ConsoleDriver : IConsoleDriver /// If simulates the Ctrl key being pressed. public abstract void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); + internal char _highSurrogate = '\0'; + + internal bool IsValidInput (KeyCode keyCode, out KeyCode result) + { + result = keyCode; + + if (char.IsHighSurrogate ((char)keyCode)) + { + _highSurrogate = (char)keyCode; + + return false; + } + + if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode)) + { + result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value; + _highSurrogate = '\0'; + + return true; + } + + if (char.IsSurrogate ((char)keyCode)) + { + return false; + } + + if (_highSurrogate > 0) + { + _highSurrogate = '\0'; + } + + return true; + } + #endregion private AnsiRequestScheduler? _scheduler; diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs index 0ec8efc05..85f9cd88c 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/ClipboardImpl.cs @@ -203,7 +203,7 @@ internal class WSLClipboard : ClipboardBase } (int exitCode, string output) = - ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"Get-Clipboard\""); + ClipboardProcessRunner.Process (_powershellPath, "-noprofile -command \"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard\""); if (exitCode == 0) { diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 48bcb713f..f8101d546 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -924,8 +924,11 @@ internal class CursesDriver : ConsoleDriver k &= ~KeyCode.Space; } - OnKeyDown (new Key (k)); - OnKeyUp (new Key (k)); + if (IsValidInput (k, out k)) + { + OnKeyDown (new (k)); + OnKeyUp (new (k)); + } } } diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 5e9a883d7..3dc008242 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -352,8 +352,12 @@ public class FakeDriver : ConsoleDriver } KeyCode map = MapKey (consoleKeyInfo); - OnKeyDown (new Key (map)); - OnKeyUp (new Key (map)); + + if (IsValidInput (map, out map)) + { + OnKeyDown (new (map)); + OnKeyUp (new (map)); + } //OnKeyPressed (new KeyEventArgs (map)); } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs index 2ede00a00..59d75b581 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs @@ -321,8 +321,11 @@ internal class NetDriver : ConsoleDriver break; } - OnKeyDown (new (map)); - OnKeyUp (new (map)); + if (IsValidInput (map, out map)) + { + OnKeyDown (new (map)); + OnKeyUp (new (map)); + } break; case EventType.Mouse: diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 1f74c0321..1fc1eb842 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -507,9 +507,12 @@ internal class WindowsDriver : ConsoleDriver break; } - // This follows convention in NetDriver - OnKeyDown (new Key (map)); - OnKeyUp (new Key (map)); + if (IsValidInput (map, out map)) + { + // This follows convention in NetDriver + OnKeyDown (new (map)); + OnKeyUp (new (map)); + } break; diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index e0d4119aa..dfcd34e67 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -138,6 +138,44 @@ public class Key : EventArgs, IEquatable KeyCode = key.KeyCode; } + /// + /// Constructs a new Key from an integer describing the key. + /// It parses the integer as Key by calling the constructor with a char or calls the constructor with a + /// KeyCode. + /// + /// + /// Don't rely on passed from to because + /// would not return the expected keys from 'a' to 'z'. + /// + /// The integer describing the key. + /// + /// + public Key (int value) + { + if (value < 0 || value > RuneExtensions.MaxUnicodeCodePoint) + { + throw new ArgumentOutOfRangeException (@$"Invalid key value: {value}", nameof (value)); + } + + if (char.IsSurrogate ((char)value)) + { + throw new ArgumentException (@$"Surrogate key not allowed: {value}", nameof (value)); + } + + Key key; + + if (((Rune)value).IsBmp) + { + key = new ((char)value); + } + else + { + key = new ((KeyCode)value); + } + + KeyCode = key.KeyCode; + } + /// /// The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers. /// Useful for determining if a key represents is a printable character. @@ -388,6 +426,11 @@ public class Key : EventArgs, IEquatable /// public static implicit operator Key (string str) { return new (str); } + /// Cast to a . + /// See for more information. + /// + public static implicit operator Key (int value) { return new (value); } + /// Cast a to a . /// See for more information. /// @@ -550,7 +593,7 @@ public class Key : EventArgs, IEquatable // "Ctrl+" (trim) // "Ctrl++" (trim) - if (input.Length > 1 && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator) + if (input.Length > 1 && !char.IsHighSurrogate (input [^2]) && new Rune (input [^1]) == separator && new Rune (input [^2]) != separator) { return input [..^1]; } @@ -640,6 +683,13 @@ public class Key : EventArgs, IEquatable return false; } + if (text.Length == 2 && char.IsHighSurrogate (text [^2]) && char.IsLowSurrogate (text [^1])) + { + // It's a surrogate pair and there is no modifiers + key = new (new Rune (text [^2], text [^1]).Value); + return true; + } + // e.g. "Ctrl++" if ((Rune)text [^1] != separator && parts.Any (string.IsNullOrEmpty)) { diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index a6e5d58c1..07adca219 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -729,21 +729,11 @@ public class TextField : View /// Use the previous cursor position. public void InsertText (string toAdd, bool useOldCursorPos = true) { - foreach (char ch in toAdd) + foreach (Rune rune in toAdd.EnumerateRunes ()) { - Key key; - - try - { - key = ch; - } - catch (Exception) - { - throw new ArgumentException ( - $"Cannot insert character '{ch}' because it does not map to a Key" - ); - } - + // All rune can be mapped to a Key and no exception will throw here because + // EnumerateRunes will replace a surrogate char with the Rune.ReplacementChar + Key key = rune.Value; InsertText (key, useOldCursorPos); } } @@ -1072,14 +1062,20 @@ public class TextField : View /// Paste the selected text from the clipboard. public virtual void Paste () { - if (ReadOnly || string.IsNullOrEmpty (Clipboard.Contents)) + if (ReadOnly) + { + return; + } + + string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? ""; + + if (string.IsNullOrEmpty (cbTxt)) { return; } SetSelectedStartSelectedLength (); int selStart = _start == -1 ? CursorPosition : _start; - string cbTxt = Clipboard.Contents.Split ("\n") [0] ?? ""; Text = StringExtensions.ToString (_text.GetRange (0, selStart)) + cbTxt @@ -1114,7 +1110,7 @@ public class TextField : View TextModel.SetCol (ref col, Viewport.Width - 1, cols); } - int pos = _cursorPosition - ScrollOffset + Math.Min (Viewport.X, 0); + int pos = col - ScrollOffset + Math.Min (Viewport.X, 0); Move (pos, 0); return new Point (pos, 0); diff --git a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj b/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj deleted file mode 100644 index 03c8b09d9..000000000 --- a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - diff --git a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs b/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs deleted file mode 100644 index 53f81e37b..000000000 --- a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Xunit; - -namespace TerminalGuiFluentTesting; - -public static class XunitContextExtensions -{ - public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) - { - context.Then ( - () => - { - Assert.True (condition); - }); - return context; - } - public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual) - { - context.Then ( - () => - { - Assert.Equal (expected,actual); - }); - return context; - } -} diff --git a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj index e9e661df2..2b556191b 100644 --- a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj +++ b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj @@ -11,7 +11,9 @@ + + diff --git a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs index fbede01b0..de2965278 100644 --- a/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/Tests/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -283,4 +283,115 @@ public class ConsoleDriverTests // Application.Run (win); // Application.Shutdown (); // } + + [Theory] + [InlineData ('\ud83d', '\udcc4')] // This seems right sequence but Stack is LIFO + [InlineData ('\ud83d', '\ud83d')] + [InlineData ('\udcc4', '\udcc4')] + public void FakeDriver_IsValidInput_Wrong_Surrogate_Sequence (char c1, char c2) + { + var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver)); + Application.Init (driver); + + Stack mKeys = new ( + [ + new ('a', ConsoleKey.A, false, false, false), + new (c1, ConsoleKey.None, false, false, false), + new (c2, ConsoleKey.None, false, false, false) + ]); + + Console.MockKeyPresses = mKeys; + + Toplevel top = new (); + var view = new View { CanFocus = true }; + var rText = ""; + var idx = 0; + + view.KeyDown += (s, e) => + { + Assert.Equal (new ('a'), e.AsRune); + Assert.Equal ("a", e.AsRune.ToString ()); + rText += e.AsRune; + e.Handled = true; + idx++; + }; + top.Add (view); + + Application.Iteration += (s, a) => + { + if (mKeys.Count == 0) + { + Application.RequestStop (); + } + }; + + Application.Run (top); + + Assert.Equal ("a", rText); + Assert.Equal (1, idx); + Assert.Equal (0, ((FakeDriver)driver)._highSurrogate); + + top.Dispose (); + + // Shutdown must be called to safely clean up Application if Init has been called + Application.Shutdown (); + } + + [Fact] + public void FakeDriver_IsValidInput_Correct_Surrogate_Sequence () + { + var driver = (IConsoleDriver)Activator.CreateInstance (typeof (FakeDriver)); + Application.Init (driver); + + Stack mKeys = new ( + [ + new ('a', ConsoleKey.A, false, false, false), + new ('\udcc4', ConsoleKey.None, false, false, false), + new ('\ud83d', ConsoleKey.None, false, false, false) + ]); + + Console.MockKeyPresses = mKeys; + + Toplevel top = new (); + var view = new View { CanFocus = true }; + var rText = ""; + var idx = 0; + + view.KeyDown += (s, e) => + { + if (idx == 0) + { + Assert.Equal (new (0x1F4C4), e.AsRune); + Assert.Equal ("📄", e.AsRune.ToString ()); + } + else + { + Assert.Equal (new ('a'), e.AsRune); + Assert.Equal ("a", e.AsRune.ToString ()); + } + + rText += e.AsRune; + e.Handled = true; + idx++; + }; + top.Add (view); + + Application.Iteration += (s, a) => + { + if (mKeys.Count == 0) + { + Application.RequestStop (); + } + }; + + Application.Run (top); + + Assert.Equal ("📄a", rText); + Assert.Equal (2, idx); + + top.Dispose (); + + // Shutdown must be called to safely clean up Application if Init has been called + Application.Shutdown (); + } } diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 76e970706..fd69ea289 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -78,27 +78,6 @@ public class TextFieldTests (ITestOutputHelper output) } } - [Fact] - public void Cancel_TextChanging_ThenBackspace () - { - var tf = new TextField (); - tf.SetFocus (); - tf.NewKeyDownEvent (Key.A.WithShift); - Assert.Equal ("A", tf.Text); - - // cancel the next keystroke - tf.TextChanging += (s, e) => e.Cancel = e.NewValue == "AB"; - tf.NewKeyDownEvent (Key.B.WithShift); - - // B was canceled so should just be A - Assert.Equal ("A", tf.Text); - - // now delete the A - tf.NewKeyDownEvent (Key.Backspace); - - Assert.Equal ("", tf.Text); - } - [Fact] [TextFieldTestsAutoInitShutdown] public void CanFocus_False_Wont_Focus_With_Mouse () @@ -506,77 +485,6 @@ public class TextFieldTests (ITestOutputHelper output) top.Dispose (); } - [Fact] - public void HistoryText_IsDirty_ClearHistoryChanges () - { - var text = "Testing"; - var tf = new TextField { Text = text }; - tf.BeginInit (); - tf.EndInit (); - - Assert.Equal (text, tf.Text); - tf.ClearHistoryChanges (); - Assert.False (tf.IsDirty); - - Assert.True (tf.NewKeyDownEvent (Key.A.WithShift)); - Assert.Equal ($"{text}A", tf.Text); - Assert.True (tf.IsDirty); - } - - [Fact] - public void Space_Does_Not_Raise_Selected () - { - TextField tf = new (); - - tf.Selecting += (sender, args) => Assert.Fail ("Selected should not be raied."); - - Application.Top = new (); - Application.Top.Add (tf); - tf.SetFocus (); - Application.RaiseKeyDownEvent (Key.Space); - - Application.Top.Dispose (); - Application.ResetState (true); - } - - [Fact] - public void Enter_Does_Not_Raise_Selected () - { - TextField tf = new (); - - var selectingCount = 0; - tf.Selecting += (sender, args) => selectingCount++; - - Application.Top = new (); - Application.Top.Add (tf); - tf.SetFocus (); - Application.RaiseKeyDownEvent (Key.Enter); - - Assert.Equal (0, selectingCount); - - Application.Top.Dispose (); - Application.ResetState (true); - } - - [Fact] - public void Enter_Raises_Accepted () - { - TextField tf = new (); - - var acceptedCount = 0; - tf.Accepting += (sender, args) => acceptedCount++; - - Application.Top = new (); - Application.Top.Add (tf); - tf.SetFocus (); - Application.RaiseKeyDownEvent (Key.Enter); - - Assert.Equal (1, acceptedCount); - - Application.Top.Dispose (); - Application.ResetState (true); - } - [Fact] [AutoInitShutdown (useFakeClipboard: true)] public void KeyBindings_Command () @@ -811,47 +719,6 @@ public class TextFieldTests (ITestOutputHelper output) Assert.Equal ("", tf.Text); } - [Fact] - public void HotKey_Command_SetsFocus () - { - var view = new TextField (); - - view.CanFocus = true; - Assert.False (view.HasFocus); - view.InvokeCommand (Command.HotKey); - Assert.True (view.HasFocus); - } - - [Fact] - public void HotKey_Command_Does_Not_Accept () - { - var view = new TextField (); - var accepted = false; - view.Accepting += OnAccept; - view.InvokeCommand (Command.HotKey); - - Assert.False (accepted); - - return; - - void OnAccept (object sender, CommandEventArgs e) { accepted = true; } - } - - [Fact] - public void Accepted_Command_Fires_Accept () - { - var view = new TextField (); - - var accepted = false; - view.Accepting += Accept; - view.InvokeCommand (Command.Accept); - Assert.True (accepted); - - return; - - void Accept (object sender, CommandEventArgs e) { accepted = true; } - } - [Theory] [InlineData (false, 1)] [InlineData (true, 0)] @@ -904,87 +771,6 @@ public class TextFieldTests (ITestOutputHelper output) void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; } } - [Fact] - public void Accepted_No_Handler_Enables_Default_Button_Accept () - { - var superView = new Window - { - Id = "superView" - }; - - var tf = new TextField - { - Id = "tf" - }; - - var button = new Button - { - Id = "button", - IsDefault = true - }; - - superView.Add (tf, button); - - var buttonAccept = 0; - button.Accepting += ButtonAccept; - - tf.SetFocus (); - Assert.True (tf.HasFocus); - - superView.NewKeyDownEvent (Key.Enter); - Assert.Equal (1, buttonAccept); - - button.SetFocus (); - superView.NewKeyDownEvent (Key.Enter); - Assert.Equal (2, buttonAccept); - - return; - - void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; } - } - - [Fact] - public void Accepted_Cancel_Event_HandlesCommand () - { - //var super = new View (); - var view = new TextField (); - - //super.Add (view); - - //var superAcceptedInvoked = false; - - var tfAcceptedInvoked = false; - var handle = false; - view.Accepting += TextViewAccept; - Assert.False (view.InvokeCommand (Command.Accept)); - Assert.True (tfAcceptedInvoked); - - tfAcceptedInvoked = false; - handle = true; - view.Accepting += TextViewAccept; - Assert.True (view.InvokeCommand (Command.Accept)); - Assert.True (tfAcceptedInvoked); - - return; - - void TextViewAccept (object sender, CommandEventArgs e) - { - tfAcceptedInvoked = true; - e.Cancel = handle; - } - } - - [Fact] - public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility () - { - var top = new Toplevel (); - var tf = new TextField { Width = 10 }; - top.Add (tf); - - Exception exception = Record.Exception (() => tf.SetFocus ()); - Assert.Null (exception); - } - [Fact] [TextFieldTestsAutoInitShutdown] public void Paste_Always_Clear_The_SelectedText () @@ -997,59 +783,6 @@ public class TextFieldTests (ITestOutputHelper output) Assert.Null (_textField.SelectedText); } - [Fact] - public void Backspace_From_End () - { - var tf = new TextField { Text = "ABC" }; - tf.SetFocus (); - Assert.Equal ("ABC", tf.Text); - tf.BeginInit (); - tf.EndInit (); - - Assert.Equal (3, tf.CursorPosition); - - // now delete the C - tf.NewKeyDownEvent (Key.Backspace); - Assert.Equal ("AB", tf.Text); - Assert.Equal (2, tf.CursorPosition); - - // then delete the B - tf.NewKeyDownEvent (Key.Backspace); - Assert.Equal ("A", tf.Text); - Assert.Equal (1, tf.CursorPosition); - - // then delete the A - tf.NewKeyDownEvent (Key.Backspace); - Assert.Equal ("", tf.Text); - Assert.Equal (0, tf.CursorPosition); - } - - [Fact] - public void Backspace_From_Middle () - { - var tf = new TextField { Text = "ABC" }; - tf.SetFocus (); - tf.CursorPosition = 2; - Assert.Equal ("ABC", tf.Text); - - // now delete the B - tf.NewKeyDownEvent (Key.Backspace); - Assert.Equal ("AC", tf.Text); - - // then delete the A - tf.NewKeyDownEvent (Key.Backspace); - Assert.Equal ("C", tf.Text); - - // then delete nothing - tf.NewKeyDownEvent (Key.Backspace); - Assert.Equal ("C", tf.Text); - - // now delete the C - tf.CursorPosition = 1; - tf.NewKeyDownEvent (Key.Backspace); - Assert.Equal ("", tf.Text); - } - [Fact] [AutoInitShutdown] public void ScrollOffset_Initialize () @@ -1142,36 +875,6 @@ public class TextFieldTests (ITestOutputHelper output) Assert.Null (_textField.SelectedText); } - [Fact] - public void KeyDown_Handled_Prevents_Input () - { - var tf = new TextField (); - tf.KeyDown += HandleJKey; - - tf.NewKeyDownEvent (Key.A); - Assert.Equal ("a", tf.Text); - - // SuppressKey suppresses the 'j' key - tf.NewKeyDownEvent (Key.J); - Assert.Equal ("a", tf.Text); - - tf.KeyDown -= HandleJKey; - - // Now that the delegate has been removed we can type j again - tf.NewKeyDownEvent (Key.J); - Assert.Equal ("aj", tf.Text); - - return; - - void HandleJKey (object s, Key arg) - { - if (arg.AsRune == new Rune ('j')) - { - arg.Handled = true; - } - } - } - [Fact] [AutoInitShutdown] public void MouseEvent_Handled_Prevents_RightClick () @@ -1222,33 +925,6 @@ public class TextFieldTests (ITestOutputHelper output) } } - [InlineData ("a")] // Lower than selection - [InlineData ("aaaaaaaaaaa")] // Greater than selection - [InlineData ("aaaa")] // Equal than selection - [Theory] - public void SetTextAndMoveCursorToEnd_WhenExistingSelection (string newText) - { - var tf = new TextField (); - tf.Text = "fish"; - tf.CursorPosition = tf.Text.Length; - - tf.NewKeyDownEvent (Key.CursorLeft); - - tf.NewKeyDownEvent (Key.CursorLeft.WithShift); - tf.NewKeyDownEvent (Key.CursorLeft.WithShift); - - Assert.Equal (1, tf.CursorPosition); - Assert.Equal (2, tf.SelectedLength); - Assert.Equal ("is", tf.SelectedText); - - tf.Text = newText; - tf.CursorPosition = tf.Text.Length; - - Assert.Equal (newText.Length, tf.CursorPosition); - Assert.Equal (0, tf.SelectedLength); - Assert.Null (tf.SelectedText); - } - [Fact] [TextFieldTestsAutoInitShutdown] public void Text_Replaces_Tabs_With_Empty_String () @@ -1296,22 +972,6 @@ public class TextFieldTests (ITestOutputHelper output) Assert.Equal ("changing", _textField.Text); } - [Fact] - public void SpaceHandling () - { - var tf = new TextField { Width = 10, Text = " " }; - - var ev = new MouseEventArgs { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked }; - - tf.NewMouseEvent (ev); - Assert.Equal (1, tf.SelectedLength); - - ev = new () { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked }; - - tf.NewMouseEvent (ev); - Assert.Equal (1, tf.SelectedLength); - } - [Fact] [TextFieldTestsAutoInitShutdown] public void Used_Is_False () @@ -1631,77 +1291,6 @@ public class TextFieldTests (ITestOutputHelper output) } } - [Fact] - public void WordBackward_WordForward_Mixed () - { - var tf = new TextField { Width = 30, Text = "Test with0. and!.?;-@+" }; - tf.BeginInit (); - tf.EndInit (); - - tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); - Assert.Equal (15, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); - Assert.Equal (12, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); - Assert.Equal (10, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); - Assert.Equal (5, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); - Assert.Equal (0, tf.CursorPosition); - - tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); - Assert.Equal (5, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); - Assert.Equal (10, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); - Assert.Equal (12, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); - Assert.Equal (15, tf.CursorPosition); - tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); - Assert.Equal (22, tf.CursorPosition); - } - - [Fact] - public void WordBackward_WordForward_SelectedText_With_Accent () - { - var text = "Les Misérables movie."; - var tf = new TextField { Width = 30, Text = text }; - - Assert.Equal (21, text.Length); - Assert.Equal (21, tf.Text.GetRuneCount ()); - Assert.Equal (21, tf.Text.GetColumns ()); - - List runes = tf.Text.ToRuneList (); - Assert.Equal (21, runes.Count); - Assert.Equal (21, tf.Text.Length); - - for (var i = 0; i < runes.Count; i++) - { - char cs = text [i]; - var cus = (char)runes [i].Value; - Assert.Equal (cs, cus); - } - - var idx = 15; - Assert.Equal ('m', text [idx]); - Assert.Equal ('m', (char)runes [idx].Value); - Assert.Equal ("m", runes [idx].ToString ()); - - Assert.True ( - tf.NewMouseEvent ( - new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf } - ) - ); - Assert.Equal ("movie.", tf.SelectedText); - - Assert.True ( - tf.NewMouseEvent ( - new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf } - ) - ); - Assert.Equal ("movie.", tf.SelectedText); - } - [Fact] [TextFieldTestsAutoInitShutdown] public void WordForward_With_No_Selection () @@ -2056,115 +1645,6 @@ Les Miśerables", } } - [Fact] - public void Autocomplete_Popup_Added_To_SuperView_On_Init () - { - View superView = new () - { - CanFocus = true - }; - - TextField t = new (); - - superView.Add (t); - Assert.Single (superView.SubViews); - - superView.BeginInit (); - superView.EndInit (); - - Assert.Equal (2, superView.SubViews.Count); - } - - [Fact] - public void Autocomplete__Added_To_SuperView_On_Add () - { - View superView = new () - { - CanFocus = true, - Id = "superView" - }; - - superView.BeginInit (); - superView.EndInit (); - Assert.Empty (superView.SubViews); - - TextField t = new () - { - Id = "t" - }; - - superView.Add (t); - - Assert.Equal (2, superView.SubViews.Count); - } - - [Fact] - public void Right_CursorAtEnd_WithSelection_ShouldClearSelection () - { - var tf = new TextField - { - Text = "Hello", - }; - tf.SetFocus (); - tf.SelectAll (); - tf.CursorPosition = 5; - - // When there is selected text and the cursor is at the end of the text field - Assert.Equal ("Hello", tf.SelectedText); - - // Pressing right should not move focus, instead it should clear selection - Assert.True (tf.NewKeyDownEvent (Key.CursorRight)); - Assert.Null (tf.SelectedText); - - // Now that the selection is cleared another right keypress should move focus - Assert.False (tf.NewKeyDownEvent (Key.CursorRight)); - } - [Fact] - public void Left_CursorAtStart_WithSelection_ShouldClearSelection () - { - var tf = new TextField - { - Text = "Hello", - }; - tf.SetFocus (); - - tf.CursorPosition = 2; - Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift)); - Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift)); - - // When there is selected text and the cursor is at the start of the text field - Assert.Equal ("He", tf.SelectedText); - - // Pressing left should not move focus, instead it should clear selection - Assert.True (tf.NewKeyDownEvent (Key.CursorLeft)); - Assert.Null (tf.SelectedText); - - // When clearing selected text with left the cursor should be at the start of the selection - Assert.Equal (0, tf.CursorPosition); - - // Now that the selection is cleared another left keypress should move focus - Assert.False (tf.NewKeyDownEvent (Key.CursorLeft)); - } - [Fact] - public void Autocomplete_Visible_False_By_Default () - { - View superView = new () - { - CanFocus = true - }; - - TextField t = new (); - - superView.Add (t); - superView.BeginInit (); - superView.EndInit (); - - Assert.Equal (2, superView.SubViews.Count); - - Assert.True (t.Visible); - Assert.False (t.Autocomplete.Visible); - } - [Fact] [AutoInitShutdown] public void Draw_Esc_Rune () diff --git a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs index 0fcb4bead..26605314a 100644 --- a/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs +++ b/Tests/UnitTestsParallelizable/Input/Keyboard/KeyTests.cs @@ -52,7 +52,8 @@ public class KeyTests { "Ctrl-A", Key.A.WithCtrl }, { "Alt-A", Key.A.WithAlt }, { "A-Ctrl", Key.A.WithCtrl }, - { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt } + { "Alt-A-Ctrl", Key.A.WithCtrl.WithAlt }, + { "📄", (KeyCode)0x1F4C4 } }; [Theory] @@ -120,10 +121,13 @@ public class KeyTests [InlineData ('\'', (KeyCode)'\'')] [InlineData ('\xFFFF', (KeyCode)0xFFFF)] [InlineData ('\x0', (KeyCode)0x0)] - public void Cast_Char_To_Key (char ch, KeyCode expectedKeyCode) + public void Cast_Char_Int_To_Key (char ch, KeyCode expectedKeyCode) { var key = (Key)ch; Assert.Equal (expectedKeyCode, key.KeyCode); + + key = (int)ch; + Assert.Equal (expectedKeyCode, key.KeyCode); } [Fact] @@ -140,23 +144,25 @@ public class KeyTests [InlineData (KeyCode.A | KeyCode.ShiftMask, KeyCode.A | KeyCode.ShiftMask)] [InlineData (KeyCode.Z, KeyCode.Z)] [InlineData (KeyCode.Space, KeyCode.Space)] - public void Cast_KeyCode_To_Key (KeyCode cdk, KeyCode expected) + public void Cast_KeyCode_Int_To_Key (KeyCode cdk, KeyCode expected) { - // explicit + // KeyCode var key = (Key)cdk; Assert.Equal (((Key)expected).ToString (), key.ToString ()); - // implicit - key = cdk; + // Int + key = key.AsRune.Value; Assert.Equal (((Key)expected).ToString (), key.ToString ()); } // string cast operators - [Fact] - public void Cast_String_To_Key () + [Theory] + [InlineData ("Ctrl+Q", KeyCode.Q | KeyCode.CtrlMask)] + [InlineData ("📄", (KeyCode)0x1F4C4)] + public void Cast_String_To_Key (string str, KeyCode expectedKeyCode) { - var key = (Key)"Ctrl+Q"; - Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, key.KeyCode); + var key = (Key)str; + Assert.Equal (expectedKeyCode, key.KeyCode); } [Theory] @@ -190,12 +196,37 @@ public class KeyTests [InlineData ('\'', (KeyCode)'\'')] [InlineData ('\xFFFF', (KeyCode)0xFFFF)] [InlineData ('\x0', (KeyCode)0x0)] - public void Constructor_Char (char ch, KeyCode expectedKeyCode) + public void Constructor_Char_Int (char ch, KeyCode expectedKeyCode) { var key = new Key (ch); Assert.Equal (expectedKeyCode, key.KeyCode); + + key = new ((int)ch); + Assert.Equal (expectedKeyCode, key.KeyCode); } + [Theory] + [InlineData (0x1F4C4, (KeyCode)0x1F4C4, "📄")] + [InlineData (0x1F64B, (KeyCode)0x1F64B, "🙋")] + [InlineData (0x1F46A, (KeyCode)0x1F46A, "👪")] + public void Constructor_Int_Non_Bmp (int value, KeyCode expectedKeyCode, string expectedString) + { + var key = new Key (value); + Assert.Equal (expectedKeyCode, key.KeyCode); + Assert.Equal (expectedString, key.AsRune.ToString ()); + Assert.Equal (expectedString, key.ToString ()); + } + + [Theory] + [InlineData (-1)] + [InlineData (0x11FFFF)] + public void Constructor_Int_Invalid_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); } + + [Theory] + [InlineData ('\ud83d')] + [InlineData ('\udcc4')] + public void Constructor_Int_Surrogate_Throws (int keyInt) { Assert.Throws (() => new Key (keyInt)); } + [Fact] public void Constructor_Default_ShouldSetKeyToNull () { diff --git a/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs new file mode 100644 index 000000000..d9019dfff --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/TextFieldTests.cs @@ -0,0 +1,554 @@ +using System.Text; + +namespace Terminal.Gui.ViewsTests; + +public class TextFieldTests +{ + [Fact] + public void Cancel_TextChanging_ThenBackspace () + { + var tf = new TextField (); + tf.SetFocus (); + tf.NewKeyDownEvent (Key.A.WithShift); + Assert.Equal ("A", tf.Text); + + // cancel the next keystroke + tf.TextChanging += (s, e) => e.Cancel = e.NewValue == "AB"; + tf.NewKeyDownEvent (Key.B.WithShift); + + // B was canceled so should just be A + Assert.Equal ("A", tf.Text); + + // now delete the A + tf.NewKeyDownEvent (Key.Backspace); + + Assert.Equal ("", tf.Text); + } + + [Fact] + public void HistoryText_IsDirty_ClearHistoryChanges () + { + var text = "Testing"; + var tf = new TextField { Text = text }; + tf.BeginInit (); + tf.EndInit (); + + Assert.Equal (text, tf.Text); + tf.ClearHistoryChanges (); + Assert.False (tf.IsDirty); + + Assert.True (tf.NewKeyDownEvent (Key.A.WithShift)); + Assert.Equal ($"{text}A", tf.Text); + Assert.True (tf.IsDirty); + } + + [Fact] + public void Space_Does_Not_Raise_Selected () + { + TextField tf = new (); + + tf.Selecting += (sender, args) => Assert.Fail ("Selected should not be raied."); + + Toplevel top = new (); + top.Add (tf); + tf.SetFocus (); + top.NewKeyDownEvent (Key.Space); + + top.Dispose (); + } + + [Fact] + public void Enter_Does_Not_Raise_Selected () + { + TextField tf = new (); + + var selectingCount = 0; + tf.Selecting += (sender, args) => selectingCount++; + + Toplevel top = new (); + top.Add (tf); + tf.SetFocus (); + top.NewKeyDownEvent (Key.Enter); + + Assert.Equal (0, selectingCount); + + top.Dispose (); + } + + [Fact] + public void Enter_Raises_Accepted () + { + TextField tf = new (); + + var acceptedCount = 0; + tf.Accepting += (sender, args) => acceptedCount++; + + Toplevel top = new (); + top.Add (tf); + tf.SetFocus (); + top.NewKeyDownEvent (Key.Enter); + + Assert.Equal (1, acceptedCount); + + top.Dispose (); + } + + [Fact] + public void HotKey_Command_SetsFocus () + { + var view = new TextField (); + + view.CanFocus = true; + Assert.False (view.HasFocus); + view.InvokeCommand (Command.HotKey); + Assert.True (view.HasFocus); + } + + [Fact] + public void HotKey_Command_Does_Not_Accept () + { + var view = new TextField (); + var accepted = false; + view.Accepting += OnAccept; + view.InvokeCommand (Command.HotKey); + + Assert.False (accepted); + + return; + + void OnAccept (object sender, CommandEventArgs e) { accepted = true; } + } + + [Fact] + public void Accepted_Command_Fires_Accept () + { + var view = new TextField (); + + var accepted = false; + view.Accepting += Accept; + view.InvokeCommand (Command.Accept); + Assert.True (accepted); + + return; + + void Accept (object sender, CommandEventArgs e) { accepted = true; } + } + + [Fact] + public void Accepted_No_Handler_Enables_Default_Button_Accept () + { + var superView = new Window + { + Id = "superView" + }; + + var tf = new TextField + { + Id = "tf" + }; + + var button = new Button + { + Id = "button", + IsDefault = true + }; + + superView.Add (tf, button); + + var buttonAccept = 0; + button.Accepting += ButtonAccept; + + tf.SetFocus (); + Assert.True (tf.HasFocus); + + superView.NewKeyDownEvent (Key.Enter); + Assert.Equal (1, buttonAccept); + + button.SetFocus (); + superView.NewKeyDownEvent (Key.Enter); + Assert.Equal (2, buttonAccept); + + return; + + void ButtonAccept (object sender, CommandEventArgs e) { buttonAccept++; } + } + + [Fact] + public void Accepted_Cancel_Event_HandlesCommand () + { + //var super = new View (); + var view = new TextField (); + + //super.Add (view); + + //var superAcceptedInvoked = false; + + var tfAcceptedInvoked = false; + var handle = false; + view.Accepting += TextViewAccept; + Assert.False (view.InvokeCommand (Command.Accept)); + Assert.True (tfAcceptedInvoked); + + tfAcceptedInvoked = false; + handle = true; + view.Accepting += TextViewAccept; + Assert.True (view.InvokeCommand (Command.Accept)); + Assert.True (tfAcceptedInvoked); + + return; + + void TextViewAccept (object sender, CommandEventArgs e) + { + tfAcceptedInvoked = true; + e.Cancel = handle; + } + } + + [Fact] + public void OnEnter_Does_Not_Throw_If_Not_IsInitialized_SetCursorVisibility () + { + var top = new Toplevel (); + var tf = new TextField { Width = 10 }; + top.Add (tf); + + Exception exception = Record.Exception (() => tf.SetFocus ()); + Assert.Null (exception); + } + + [Fact] + public void Backspace_From_End () + { + var tf = new TextField { Text = "ABC" }; + tf.SetFocus (); + Assert.Equal ("ABC", tf.Text); + tf.BeginInit (); + tf.EndInit (); + + Assert.Equal (3, tf.CursorPosition); + + // now delete the C + tf.NewKeyDownEvent (Key.Backspace); + Assert.Equal ("AB", tf.Text); + Assert.Equal (2, tf.CursorPosition); + + // then delete the B + tf.NewKeyDownEvent (Key.Backspace); + Assert.Equal ("A", tf.Text); + Assert.Equal (1, tf.CursorPosition); + + // then delete the A + tf.NewKeyDownEvent (Key.Backspace); + Assert.Equal ("", tf.Text); + Assert.Equal (0, tf.CursorPosition); + } + + [Fact] + public void Backspace_From_Middle () + { + var tf = new TextField { Text = "ABC" }; + tf.SetFocus (); + tf.CursorPosition = 2; + Assert.Equal ("ABC", tf.Text); + + // now delete the B + tf.NewKeyDownEvent (Key.Backspace); + Assert.Equal ("AC", tf.Text); + + // then delete the A + tf.NewKeyDownEvent (Key.Backspace); + Assert.Equal ("C", tf.Text); + + // then delete nothing + tf.NewKeyDownEvent (Key.Backspace); + Assert.Equal ("C", tf.Text); + + // now delete the C + tf.CursorPosition = 1; + tf.NewKeyDownEvent (Key.Backspace); + Assert.Equal ("", tf.Text); + } + + [Fact] + public void KeyDown_Handled_Prevents_Input () + { + var tf = new TextField (); + tf.KeyDown += HandleJKey; + + tf.NewKeyDownEvent (Key.A); + Assert.Equal ("a", tf.Text); + + // SuppressKey suppresses the 'j' key + tf.NewKeyDownEvent (Key.J); + Assert.Equal ("a", tf.Text); + + tf.KeyDown -= HandleJKey; + + // Now that the delegate has been removed we can type j again + tf.NewKeyDownEvent (Key.J); + Assert.Equal ("aj", tf.Text); + + return; + + void HandleJKey (object s, Key arg) + { + if (arg.AsRune == new Rune ('j')) + { + arg.Handled = true; + } + } + } + + [InlineData ("a")] // Lower than selection + [InlineData ("aaaaaaaaaaa")] // Greater than selection + [InlineData ("aaaa")] // Equal than selection + [Theory] + public void SetTextAndMoveCursorToEnd_WhenExistingSelection (string newText) + { + var tf = new TextField (); + tf.Text = "fish"; + tf.CursorPosition = tf.Text.Length; + + tf.NewKeyDownEvent (Key.CursorLeft); + + tf.NewKeyDownEvent (Key.CursorLeft.WithShift); + tf.NewKeyDownEvent (Key.CursorLeft.WithShift); + + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (2, tf.SelectedLength); + Assert.Equal ("is", tf.SelectedText); + + tf.Text = newText; + tf.CursorPosition = tf.Text.Length; + + Assert.Equal (newText.Length, tf.CursorPosition); + Assert.Equal (0, tf.SelectedLength); + Assert.Null (tf.SelectedText); + } + + [Fact] + public void SpaceHandling () + { + var tf = new TextField { Width = 10, Text = " " }; + + var ev = new MouseEventArgs { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked }; + + tf.NewMouseEvent (ev); + Assert.Equal (1, tf.SelectedLength); + + ev = new () { Position = new (1, 0), Flags = MouseFlags.Button1DoubleClicked }; + + tf.NewMouseEvent (ev); + Assert.Equal (1, tf.SelectedLength); + } + + [Fact] + public void WordBackward_WordForward_Mixed () + { + var tf = new TextField { Width = 30, Text = "Test with0. and!.?;-@+" }; + tf.BeginInit (); + tf.EndInit (); + + tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); + Assert.Equal (15, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); + Assert.Equal (12, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); + Assert.Equal (10, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); + Assert.Equal (5, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorLeft.WithCtrl); + Assert.Equal (0, tf.CursorPosition); + + tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); + Assert.Equal (5, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); + Assert.Equal (10, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); + Assert.Equal (12, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); + Assert.Equal (15, tf.CursorPosition); + tf.NewKeyDownEvent (Key.CursorRight.WithCtrl); + Assert.Equal (22, tf.CursorPosition); + } + + [Fact] + public void WordBackward_WordForward_SelectedText_With_Accent () + { + var text = "Les Misérables movie."; + var tf = new TextField { Width = 30, Text = text }; + + Assert.Equal (21, text.Length); + Assert.Equal (21, tf.Text.GetRuneCount ()); + Assert.Equal (21, tf.Text.GetColumns ()); + + List runes = tf.Text.ToRuneList (); + Assert.Equal (21, runes.Count); + Assert.Equal (21, tf.Text.Length); + + for (var i = 0; i < runes.Count; i++) + { + char cs = text [i]; + var cus = (char)runes [i].Value; + Assert.Equal (cs, cus); + } + + var idx = 15; + Assert.Equal ('m', text [idx]); + Assert.Equal ('m', (char)runes [idx].Value); + Assert.Equal ("m", runes [idx].ToString ()); + + Assert.True ( + tf.NewMouseEvent ( + new () { Position = new (idx, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf } + ) + ); + Assert.Equal ("movie.", tf.SelectedText); + + Assert.True ( + tf.NewMouseEvent ( + new () { Position = new (idx + 1, 1), Flags = MouseFlags.Button1DoubleClicked, View = tf } + ) + ); + Assert.Equal ("movie.", tf.SelectedText); + } + + [Fact] + public void Autocomplete_Popup_Added_To_SuperView_On_Init () + { + View superView = new () + { + CanFocus = true + }; + + TextField t = new (); + + superView.Add (t); + Assert.Single (superView.SubViews); + + superView.BeginInit (); + superView.EndInit (); + + Assert.Equal (2, superView.SubViews.Count); + } + + [Fact] + public void Autocomplete__Added_To_SuperView_On_Add () + { + View superView = new () + { + CanFocus = true, + Id = "superView" + }; + + superView.BeginInit (); + superView.EndInit (); + Assert.Empty (superView.SubViews); + + TextField t = new () + { + Id = "t" + }; + + superView.Add (t); + + Assert.Equal (2, superView.SubViews.Count); + } + + [Fact] + public void Right_CursorAtEnd_WithSelection_ShouldClearSelection () + { + var tf = new TextField + { + Text = "Hello" + }; + tf.SetFocus (); + tf.SelectAll (); + tf.CursorPosition = 5; + + // When there is selected text and the cursor is at the end of the text field + Assert.Equal ("Hello", tf.SelectedText); + + // Pressing right should not move focus, instead it should clear selection + Assert.True (tf.NewKeyDownEvent (Key.CursorRight)); + Assert.Null (tf.SelectedText); + + // Now that the selection is cleared another right keypress should move focus + Assert.False (tf.NewKeyDownEvent (Key.CursorRight)); + } + + [Fact] + public void Left_CursorAtStart_WithSelection_ShouldClearSelection () + { + var tf = new TextField + { + Text = "Hello" + }; + tf.SetFocus (); + + tf.CursorPosition = 2; + Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift)); + Assert.True (tf.NewKeyDownEvent (Key.CursorLeft.WithShift)); + + // When there is selected text and the cursor is at the start of the text field + Assert.Equal ("He", tf.SelectedText); + + // Pressing left should not move focus, instead it should clear selection + Assert.True (tf.NewKeyDownEvent (Key.CursorLeft)); + Assert.Null (tf.SelectedText); + + // When clearing selected text with left the cursor should be at the start of the selection + Assert.Equal (0, tf.CursorPosition); + + // Now that the selection is cleared another left keypress should move focus + Assert.False (tf.NewKeyDownEvent (Key.CursorLeft)); + } + + [Fact] + public void Autocomplete_Visible_False_By_Default () + { + View superView = new () + { + CanFocus = true + }; + + TextField t = new (); + + superView.Add (t); + superView.BeginInit (); + superView.EndInit (); + + Assert.Equal (2, superView.SubViews.Count); + + Assert.True (t.Visible); + Assert.False (t.Autocomplete.Visible); + } + + [Fact] + public void InsertText_Bmp_SurrogatePair_Non_Bmp_Invalid_SurrogatePair () + { + var tf = new TextField (); + + //📄 == \ud83d\udcc4 == \U0001F4C4 + // � == Rune.ReplacementChar + tf.InsertText ("aA,;\ud83d\udcc4\U0001F4C4\udcc4\ud83d"); + Assert.Equal ("aA,;📄📄��", tf.Text); + } + + [Fact] + public void PositionCursor_Respect_GetColumns () + { + var tf = new TextField { Width = 5 }; + tf.BeginInit (); + tf.EndInit (); + + tf.NewKeyDownEvent (new ("📄")); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new (2, 0), tf.PositionCursor ()); + Assert.Equal ("📄", tf.Text); + + tf.NewKeyDownEvent (new (KeyCode.A)); + Assert.Equal (2, tf.CursorPosition); + Assert.Equal (new (3, 0), tf.PositionCursor ()); + Assert.Equal ("📄a", tf.Text); + } +}