From 0504ca2d4b4ce0c91d48bcb5c564093ec0d3d380 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Thu, 6 Jul 2023 20:30:35 +0200 Subject: [PATCH 01/15] Ensure "text boxes" preserve cursor location (#15099) * Ensure iOS "text boxes" preserve cursor location * Fix the Windows tests * Attach the controls to the UI for focus tests * Adding some more "text box" tests * todo * tests * Revert "todo" This reverts commit 328c6ba789cc40cf28e57b7606037055dbe4c115. # Conflicts: # src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.cs * this * move * Revert "this" This reverts commit d62b12af5cc999a126d1d333d493999b47891305. * moar tests * Revert "Revert "todo"" This reverts commit 6065a41d8a58bfdfdf4990680e0d51fe9936a7e3. * all working on windows * Merge the methods * Update src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs Co-authored-by: Rachel Kang * fix the variable names * fix * feedback --------- Co-authored-by: Rachel Kang --- .../Windows/Extensions/TextBoxExtensions.cs | 13 +- .../Platform/iOS/Extensions/TextExtensions.cs | 51 +-- .../DeviceTests/ControlsHandlerTestBase.cs | 47 +++ .../Elements/Editor/EditorTests.Android.cs | 10 +- .../Elements/Editor/EditorTests.Windows.cs | 10 +- .../Elements/Editor/EditorTests.cs | 223 +----------- .../Elements/Editor/EditorTests.iOS.cs | 10 +- .../Elements/Entry/EntryTests.Android.cs | 10 +- .../Elements/Entry/EntryTests.Windows.cs | 10 +- .../DeviceTests/Elements/Entry/EntryTests.cs | 222 +----------- .../Elements/Entry/EntryTests.iOS.cs | 10 +- .../SearchBar/SearchBarTests.Android.cs | 25 +- .../SearchBar/SearchBarTests.Windows.cs | 33 +- .../Elements/SearchBar/SearchBarTests.cs | 31 +- .../Elements/SearchBar/SearchBarTests.iOS.cs | 16 +- ...roid.cs => TextInputFocusTests.Android.cs} | 32 +- .../Elements/TextInput/TextInputFocusTests.cs | 12 + .../Elements/TextInput/TextInputTests.cs | 340 +++++++++++++++++- .../HandlerTests/HandlerTestBasement.cs | 14 + .../HandlerTests/TestBase.cs | 10 +- 20 files changed, 600 insertions(+), 529 deletions(-) rename src/Controls/tests/DeviceTests/Elements/TextInput/{TextInputTests.Android.cs => TextInputFocusTests.Android.cs} (52%) create mode 100644 src/Controls/tests/DeviceTests/Elements/TextInput/TextInputFocusTests.cs diff --git a/src/Controls/src/Core/Platform/Windows/Extensions/TextBoxExtensions.cs b/src/Controls/src/Core/Platform/Windows/Extensions/TextBoxExtensions.cs index eac0381aeb..c28eacde0d 100644 --- a/src/Controls/src/Core/Platform/Windows/Extensions/TextBoxExtensions.cs +++ b/src/Controls/src/Core/Platform/Windows/Extensions/TextBoxExtensions.cs @@ -27,12 +27,15 @@ namespace Microsoft.Maui.Controls.Platform var cursorOffset = newText.Length - oldText.Length; int cursorPosition = hasFocus ? platformControl.GetCursorPosition(cursorOffset) : newText.Length; - if (oldText != newText && passwordBox is not null) - passwordBox.Password = newText; - else if (oldText != newText) - platformControl.Text = newText; + if (oldText != newText) + { + if (passwordBox is not null) + passwordBox.Password = newText; + else + platformControl.Text = newText; - platformControl.Select(cursorPosition, 0); + platformControl.Select(cursorPosition, 0); + } } } diff --git a/src/Controls/src/Core/Platform/iOS/Extensions/TextExtensions.cs b/src/Controls/src/Core/Platform/iOS/Extensions/TextExtensions.cs index 5a38647b19..948c18531e 100644 --- a/src/Controls/src/Core/Platform/iOS/Extensions/TextExtensions.cs +++ b/src/Controls/src/Core/Platform/iOS/Extensions/TextExtensions.cs @@ -28,48 +28,35 @@ namespace Microsoft.Maui.Controls.Platform textField.AdjustsFontSizeToFitWidth = entry.OnThisPlatform().AdjustsFontSizeToFitWidth(); } - public static void UpdateText(this UITextView textView, InputView inputView) + public static void UpdateText(this UITextView textView, InputView inputView) => + UpdateText(textView, inputView, textView.IsFirstResponder); + + public static void UpdateText(this UITextField textField, InputView inputView) => + UpdateText(textField, inputView, textField.IsEditing); + + static void UpdateText(this IUITextInput textInput, InputView inputView, bool isEditing) { - // Setting the text causes the cursor to be reset to the end of the UITextView. + // Setting the text causes the cursor to be reset to the end of the IUITextInput. // So, let's set back the cursor to the last known position and calculate a new // position if needed when the text was modified by a Converter. - var oldText = textView.Text ?? string.Empty; + var textRange = textInput.GetTextRange(textInput.BeginningOfDocument, textInput.EndOfDocument); + var oldText = textInput.TextInRange(textRange) ?? string.Empty; var newText = TextTransformUtilites.GetTransformedText( inputView?.Text, - textView.SecureTextEntry ? TextTransform.Default : inputView.TextTransform + textInput.GetSecureTextEntry() ? TextTransform.Default : inputView.TextTransform ); - // Re-calculate the cursor offset position if the text was modified by a Converter. - // but if the text is being set by code, let's just move the cursor to the end. - var cursorOffset = newText.Length - oldText.Length; - var cursorPosition = textView.IsFirstResponder ? textView.GetCursorPosition(cursorOffset) : newText.Length; - if (oldText != newText) - textView.Text = newText; + { + // Re-calculate the cursor offset position if the text was modified by a Converter. + // but if the text is being set by code, let's just move the cursor to the end. + var cursorOffset = newText.Length - oldText.Length; + var cursorPosition = isEditing ? textInput.GetCursorPosition(cursorOffset) : newText.Length; - textView.SetTextRange(cursorPosition, 0); - } + textInput.ReplaceText(textRange, newText); - public static void UpdateText(this UITextField textField, InputView inputView) - { - // Setting the text causes the cursor to be reset to the end of the UITextView. - // So, let's set back the cursor to the last known position and calculate a new - // position if needed when the text was modified by a Converter. - var oldText = textField.Text ?? string.Empty; - var newText = TextTransformUtilites.GetTransformedText( - inputView?.Text, - textField.SecureTextEntry ? TextTransform.Default : inputView.TextTransform - ); - - // Re-calculate the cursor offset position if the text was modified by a Converter. - // but if the text is being set by code, let's just move the cursor to the end. - var cursorOffset = newText.Length - oldText.Length; - var cursorPosition = textField.IsEditing ? textField.GetCursorPosition(cursorOffset) : newText.Length; - - if (oldText != newText) - textField.Text = newText; - - textField.SetTextRange(cursorPosition, 0); + textInput.SetTextRange(cursorPosition, 0); + } } public static void UpdateLineBreakMode(this UILabel platformLabel, Label label) diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs index 44b3954398..726705ba37 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs @@ -231,6 +231,53 @@ namespace Microsoft.Maui.DeviceTests }); } + /// + /// This is more complicated as we have different logic depending on the view being focused or not. + /// When we attach to the UI, there is only a single control so sometimes it cannot unfocus. + /// + public async Task AttachAndRunFocusAffectedControl(TType control, Action action) + where TType : IView, new() + where THandler : class, IPlatformViewHandler, IElementHandler, new() + { + Func boop = (handler) => + { + action.Invoke(handler); + return Task.CompletedTask; + }; + + await AttachAndRunFocusAffectedControl(control, boop); + } + + /// + /// This is more complicated as we have different logic depending on the view being focused or not. + /// When we attach to the UI, there is only a single control so sometimes it cannot unfocus. + /// + public async Task AttachAndRunFocusAffectedControl(TType control, Func action) + where TType : IView, new() + where THandler : class, IPlatformViewHandler, IElementHandler, new() + { + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handler => + { + handler.AddHandler(); + handler.AddHandler(); + }); + }); + + var layout = new VerticalStackLayout + { + WidthRequest = 200, + HeightRequest = 200, + }; + + var placeholder = new TType(); + layout.Add(placeholder); + layout.Add(control); + + await AttachAndRun(layout, handler => action(control.Handler as THandler)); + } + async protected Task ValidatePropertyInitValue( IView view, Func GetValue, diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs index 18457f0e71..0be22351a2 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Android.cs @@ -9,18 +9,18 @@ namespace Microsoft.Maui.DeviceTests [Collection(ControlsHandlerTestBase.RunInNewWindowCollection)] public partial class EditorTests { - AppCompatEditText GetPlatformControl(EditorHandler handler) => + static AppCompatEditText GetPlatformControl(EditorHandler handler) => handler.PlatformView; - Task GetPlatformText(EditorHandler handler) + static Task GetPlatformText(EditorHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } - void SetPlatformText(EditorHandler editorHandler, string text) => + static void SetPlatformText(EditorHandler editorHandler, string text) => GetPlatformControl(editorHandler).SetTextKeepState(text); - int GetPlatformCursorPosition(EditorHandler editorHandler) + static int GetPlatformCursorPosition(EditorHandler editorHandler) { var textView = GetPlatformControl(editorHandler); @@ -30,7 +30,7 @@ namespace Microsoft.Maui.DeviceTests return -1; } - int GetPlatformSelectionLength(EditorHandler editorHandler) + static int GetPlatformSelectionLength(EditorHandler editorHandler) { var textView = GetPlatformControl(editorHandler); diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs index 9198c624d0..6d03406286 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.Windows.cs @@ -7,21 +7,21 @@ namespace Microsoft.Maui.DeviceTests { public partial class EditorTests { - TextBox GetPlatformControl(EditorHandler handler) => + static TextBox GetPlatformControl(EditorHandler handler) => handler.PlatformView; - Task GetPlatformText(EditorHandler handler) + static Task GetPlatformText(EditorHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } - void SetPlatformText(EditorHandler editorHandler, string text) => + static void SetPlatformText(EditorHandler editorHandler, string text) => GetPlatformControl(editorHandler).Text = text; - int GetPlatformCursorPosition(EditorHandler editorHandler) => + static int GetPlatformCursorPosition(EditorHandler editorHandler) => GetPlatformControl(editorHandler).SelectionStart; - int GetPlatformSelectionLength(EditorHandler editorHandler) => + static int GetPlatformSelectionLength(EditorHandler editorHandler) => GetPlatformControl(editorHandler).SelectionLength; } } diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.cs index 87e1943a28..08e0be5b1f 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.cs @@ -10,7 +10,6 @@ namespace Microsoft.Maui.DeviceTests [Category(TestCategory.Editor)] public partial class EditorTests : ControlsHandlerTestBase { - #if !IOS && !MACCATALYST // iOS is broken until this point // https://github.com/dotnet/maui/issues/3425 @@ -51,26 +50,6 @@ namespace Microsoft.Maui.DeviceTests } #endif - [Theory(DisplayName = "Text is Transformed Correctly at Initialization")] - [ClassData(typeof(TextTransformCases))] - public async Task InitialTextTransformApplied(string text, TextTransform transform, string expected) - { - var control = new Editor() { Text = text, TextTransform = transform }; - var platformText = await GetPlatformText(await CreateHandlerAsync(control)); - Assert.Equal(expected, platformText); - } - - [Theory(DisplayName = "Text is Transformed Correctly after Initialization")] - [ClassData(typeof(TextTransformCases))] - public async Task TextTransformUpdated(string text, TextTransform transform, string expected) - { - var control = new Editor() { Text = text }; - var handler = await CreateHandlerAsync(control); - await InvokeOnMainThreadAsync(() => control.TextTransform = transform); - var platformText = await GetPlatformText(handler); - Assert.Equal(expected, platformText); - } - #if WINDOWS // Only Windows needs the IsReadOnly workaround for MaxLength==0 to prevent text from being entered [Fact] @@ -96,198 +75,26 @@ namespace Microsoft.Maui.DeviceTests } #endif - [Theory(DisplayName = "CursorPosition Initializes Correctly")] - [InlineData(2)] - public async Task CursorPositionInitializesCorrectly(int initialPosition) + [Category(TestCategory.Editor)] + [Category(TestCategory.TextInput)] + [Collection(RunInNewWindowCollection)] + public class EditorTextInputTests : TextInputTests { - var editor = new Editor - { - Text = "This is TEXT!", - CursorPosition = initialPosition - }; + protected override int GetPlatformSelectionLength(EditorHandler handler) => + EditorTests.GetPlatformSelectionLength(handler); - await ValidatePropertyInitValue( - editor, - () => editor.CursorPosition, - GetPlatformCursorPosition, - initialPosition); + protected override int GetPlatformCursorPosition(EditorHandler handler) => + EditorTests.GetPlatformCursorPosition(handler); + + protected override Task GetPlatformText(EditorHandler handler) => + EditorTests.GetPlatformText(handler); } - [Theory(DisplayName = "CursorPosition Updates Correctly")] - [InlineData(2, 5)] - public async Task CursorPositionUpdatesCorrectly(int setValue, int unsetValue) + [Category(TestCategory.Editor)] + [Category(TestCategory.TextInput)] + [Collection(RunInNewWindowCollection)] + public class EditorTextInputFocusTests : TextInputFocusTests { - string text = "This is TEXT!"; - - var editor = new Editor - { - Text = text - }; - - await ValidatePropertyUpdatesValue( - editor, - nameof(ITextInput.CursorPosition), - GetPlatformCursorPosition, - setValue, - unsetValue - ); - } - - [Theory(DisplayName = "CursorPosition is Capped to Text's Length")] - [InlineData(30)] - public async Task CursorPositionIsCapped(int initialPosition) - { - string text = "This is TEXT!"; - - var editor = new Editor - { - Text = text, - CursorPosition = initialPosition - }; - - await ValidatePropertyInitValue( - editor, - () => editor.CursorPosition, - GetPlatformCursorPosition, - text.Length); - } - - [Theory(DisplayName = "Unset CursorPosition is kept at zero at initialization")] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task UnsetCursorPositionKeepsToZeroOnInitialization(string text) - { - var editor = new Editor - { - Text = text - }; - - await ValidatePropertyInitValue( - editor, - () => editor.CursorPosition, - GetPlatformCursorPosition, - 0); - } - - [Theory(DisplayName = "CursorPosition moves to the end on text change after initialization" -#if WINDOWS - , Skip = "For some reason, the PlatformView events are not being fired on tests after the handler is created, something is swallowing them. " + - "This was tested on a real app and it's working correctly." -#endif - )] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task CursorPositionMovesToTheEndOnTextChangeAfterInitialization(string text) - { - var editor = new Editor - { - Text = "Test" - }; - - await SetValueAsync(editor, text, (h, s) => h.VirtualView.Text = s); - - Assert.Equal(text.Length, editor.CursorPosition); - } - - [Theory(DisplayName = "SelectionLength Initializes Correctly")] - [InlineData(2)] - public async Task SelectionLengthInitializesCorrectly(int initialLength) - { - var editor = new Editor - { - Text = "This is TEXT!", - SelectionLength = initialLength - }; - - await ValidatePropertyInitValue( - editor, - () => editor.SelectionLength, - GetPlatformSelectionLength, - initialLength); - } - - [Theory(DisplayName = "SelectionLength Updates Correctly")] - [InlineData(2, 5)] - public async Task SelectionLengthUpdatesCorrectly(int setValue, int unsetValue) - { - string text = "This is TEXT!"; - - var editor = new Editor - { - Text = text, - }; - - await ValidatePropertyUpdatesValue( - editor, - nameof(IEditor.SelectionLength), - GetPlatformSelectionLength, - setValue, - unsetValue - ); - } - - [Theory(DisplayName = "SelectionLength is Capped to Text Length")] - [InlineData(30)] - public async Task SelectionLengthIsCapped(int selectionLength) - { - string text = "This is TEXT!"; - - var editor = new Editor - { - Text = text, - SelectionLength = selectionLength - }; - - await ValidatePropertyInitValue( - editor, - () => editor.SelectionLength, - GetPlatformSelectionLength, - text.Length); - } - - [Theory(DisplayName = "Unset SelectionLength is kept at zero at initialization")] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task UnsetSelectionLengthKeepsToZeroOnInitialization(string text) - { - var editor = new Editor - { - Text = text - }; - - await ValidatePropertyInitValue( - editor, - () => editor.SelectionLength, - GetPlatformSelectionLength, - 0); - } - - [Theory(DisplayName = "SelectionLength is kept at zero on text change after initialization" -#if WINDOWS - , Skip = "For some reason, the PlatformView events are not being fired on tests after the handler is created, something is swallowing them. " + - "This was tested on a real app and it's working correctly." -#endif - )] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task SelectionLengthMovesToTheEndOnTextChangeAfterInitialization(string text) - { - var editor = new Editor - { - Text = "Test" - }; - - await SetValueAsync(editor, text, (h, s) => h.VirtualView.Text = s); - - Assert.Equal(0, editor.SelectionLength); } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs index 49b678f948..346689122f 100644 --- a/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Editor/EditorTests.iOS.cs @@ -8,18 +8,18 @@ namespace Microsoft.Maui.DeviceTests [Collection(ControlsHandlerTestBase.RunInNewWindowCollection)] public partial class EditorTests { - MauiTextView GetPlatformControl(EditorHandler handler) => + static MauiTextView GetPlatformControl(EditorHandler handler) => handler.PlatformView; - Task GetPlatformText(EditorHandler handler) + static Task GetPlatformText(EditorHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } - void SetPlatformText(EditorHandler editorHandler, string text) => + static void SetPlatformText(EditorHandler editorHandler, string text) => GetPlatformControl(editorHandler).Text = text; - int GetPlatformCursorPosition(EditorHandler editorHandler) + static int GetPlatformCursorPosition(EditorHandler editorHandler) { var nativeEditor = GetPlatformControl(editorHandler); @@ -29,7 +29,7 @@ namespace Microsoft.Maui.DeviceTests return -1; } - int GetPlatformSelectionLength(EditorHandler editorHandler) + static int GetPlatformSelectionLength(EditorHandler editorHandler) { var nativeEditor = GetPlatformControl(editorHandler); diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Android.cs index 0c65b004c3..8d94ce5cc3 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Android.cs @@ -9,18 +9,18 @@ namespace Microsoft.Maui.DeviceTests { public partial class EntryTests { - AppCompatEditText GetPlatformControl(EntryHandler handler) => + static AppCompatEditText GetPlatformControl(EntryHandler handler) => handler.PlatformView; - Task GetPlatformText(EntryHandler handler) + static Task GetPlatformText(EntryHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } - void SetPlatformText(EntryHandler entryHandler, string text) => + static void SetPlatformText(EntryHandler entryHandler, string text) => GetPlatformControl(entryHandler).SetTextKeepState(text); - int GetPlatformCursorPosition(EntryHandler entryHandler) + static int GetPlatformCursorPosition(EntryHandler entryHandler) { var editText = GetPlatformControl(entryHandler); @@ -30,7 +30,7 @@ namespace Microsoft.Maui.DeviceTests return -1; } - int GetPlatformSelectionLength(EntryHandler entryHandler) + static int GetPlatformSelectionLength(EntryHandler entryHandler) { var editText = GetPlatformControl(entryHandler); diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs index 5116daaf01..341b96eb0a 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.Windows.cs @@ -7,21 +7,21 @@ namespace Microsoft.Maui.DeviceTests { public partial class EntryTests { - TextBox GetPlatformControl(EntryHandler handler) => + static TextBox GetPlatformControl(EntryHandler handler) => handler.PlatformView; - Task GetPlatformText(EntryHandler handler) + static Task GetPlatformText(EntryHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } - void SetPlatformText(EntryHandler entryHandler, string text) => + static void SetPlatformText(EntryHandler entryHandler, string text) => GetPlatformControl(entryHandler).Text = text; - int GetPlatformCursorPosition(EntryHandler entryHandler) => + static int GetPlatformCursorPosition(EntryHandler entryHandler) => GetPlatformControl(entryHandler).SelectionStart; - int GetPlatformSelectionLength(EntryHandler entryHandler) => + static int GetPlatformSelectionLength(EntryHandler entryHandler) => GetPlatformControl(entryHandler).SelectionLength; } } diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs index c4e527bf97..e1fccc7f35 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs @@ -12,26 +12,6 @@ namespace Microsoft.Maui.DeviceTests [Category(TestCategory.Entry)] public partial class EntryTests : ControlsHandlerTestBase { - [Theory(DisplayName = "Text is Transformed Correctly at Initialization")] - [ClassData(typeof(TextTransformCases))] - public async Task InitialTextTransformApplied(string text, TextTransform transform, string expected) - { - var control = new Entry() { Text = text, TextTransform = transform }; - var platformText = await GetPlatformText(await CreateHandlerAsync(control)); - Assert.Equal(expected, platformText); - } - - [Theory(DisplayName = "Text is Transformed Correctly after Initialization")] - [ClassData(typeof(TextTransformCases))] - public async Task TextTransformUpdated(string text, TextTransform transform, string expected) - { - var control = new Entry() { Text = text }; - var handler = await CreateHandlerAsync(control); - await InvokeOnMainThreadAsync(() => control.TextTransform = transform); - var platformText = await GetPlatformText(handler); - Assert.Equal(expected, platformText); - } - [Fact] public async Task MaxLengthTrims() { @@ -138,198 +118,26 @@ namespace Microsoft.Maui.DeviceTests } #endif - [Theory(DisplayName = "CursorPosition Initializes Correctly")] - [InlineData(2)] - public async Task CursorPositionInitializesCorrectly(int initialPosition) + [Category(TestCategory.Entry)] + [Category(TestCategory.TextInput)] + [Collection(RunInNewWindowCollection)] + public class EntryTextInputTests : TextInputTests { - var entry = new Entry - { - Text = "This is TEXT!", - CursorPosition = initialPosition - }; + protected override int GetPlatformSelectionLength(EntryHandler handler) => + EntryTests.GetPlatformSelectionLength(handler); - await ValidatePropertyInitValue( - entry, - () => entry.CursorPosition, - GetPlatformCursorPosition, - initialPosition); + protected override int GetPlatformCursorPosition(EntryHandler handler) => + EntryTests.GetPlatformCursorPosition(handler); + + protected override Task GetPlatformText(EntryHandler handler) => + EntryTests.GetPlatformText(handler); } - [Theory(DisplayName = "CursorPosition Updates Correctly")] - [InlineData(2, 5)] - public async Task CursorPositionUpdatesCorrectly(int setValue, int unsetValue) + [Category(TestCategory.Entry)] + [Category(TestCategory.TextInput)] + [Collection(RunInNewWindowCollection)] + public class EntryTextInputFocusTests : TextInputFocusTests { - string text = "This is TEXT!"; - - var entry = new Entry - { - Text = text - }; - - await ValidatePropertyUpdatesValue( - entry, - nameof(ITextInput.CursorPosition), - GetPlatformCursorPosition, - setValue, - unsetValue - ); - } - - [Theory(DisplayName = "CursorPosition is Capped to Text's Length")] - [InlineData(30)] - public async Task CursorPositionIsCapped(int initialPosition) - { - string text = "This is TEXT!"; - - var entry = new Entry - { - Text = text, - CursorPosition = initialPosition - }; - - await ValidatePropertyInitValue( - entry, - () => entry.CursorPosition, - GetPlatformCursorPosition, - text.Length); - } - - [Theory(DisplayName = "Unset CursorPosition is kept at zero at initialization")] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task UnsetCursorPositionIsKeptAtZeroAtInitialization(string text) - { - var entry = new Entry - { - Text = text - }; - - await ValidatePropertyInitValue( - entry, - () => entry.CursorPosition, - GetPlatformCursorPosition, - 0); - } - - [Theory(DisplayName = "CursorPosition moves to the end on text change by code after initialization" -#if WINDOWS - , Skip = "For some reason, the PlatformView events are not being fired on tests after the handler is created, something is swallowing them. " + - "This was tested on a real app and it's working correctly." -#endif - )] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task CursorPositionMovesToTheEndOnTextChangeAfterInitialization(string text) - { - var entry = new Entry - { - Text = "Test" - }; - - await SetValueAsync(entry, text, (h, s) => h.VirtualView.Text = s); - - Assert.Equal(text.Length, entry.CursorPosition); - } - - [Theory(DisplayName = "SelectionLength Initializes Correctly")] - [InlineData(2)] - public async Task SelectionLengthInitializesCorrectly(int initialLength) - { - var entry = new Entry - { - Text = "This is TEXT!", - SelectionLength = initialLength - }; - - await ValidatePropertyInitValue( - entry, - () => entry.SelectionLength, - GetPlatformSelectionLength, - initialLength); - } - - [Theory(DisplayName = "SelectionLength Updates Correctly")] - [InlineData(2, 5)] - public async Task SelectionLengthUpdatesCorrectly(int setValue, int unsetValue) - { - string text = "This is TEXT!"; - - var entry = new Entry - { - Text = text, - }; - - await ValidatePropertyUpdatesValue( - entry, - nameof(IEntry.SelectionLength), - GetPlatformSelectionLength, - setValue, - unsetValue - ); - } - - [Theory(DisplayName = "SelectionLength is Capped to Text Length")] - [InlineData(30)] - public async Task SelectionLengthIsCapped(int selectionLength) - { - string text = "This is TEXT!"; - - var entry = new Entry - { - Text = text, - SelectionLength = selectionLength - }; - - await ValidatePropertyInitValue( - entry, - () => entry.SelectionLength, - GetPlatformSelectionLength, - text.Length); - } - - [Theory(DisplayName = "Unset SelectionLength is kept at zero at initialization")] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task UnsetSelectionLengthIsKeptAtZeroAtInitialization(string text) - { - var entry = new Entry - { - Text = text - }; - - await ValidatePropertyInitValue( - entry, - () => entry.SelectionLength, - GetPlatformSelectionLength, - 0); - } - - [Theory(DisplayName = "SelectionLength is kept at zero on text change by code after initialization" -#if WINDOWS - , Skip = "For some reason, the PlatformView events are not being fired on tests after the handler is created, something is swallowing them. " + - "This was tested on a real app and it's working correctly." -#endif - )] - [InlineData("This is a test!!!")] - [InlineData("a")] - [InlineData("")] - [InlineData(" ")] - public async Task SelectionLengthMovesToTheEndOnTextChangeAfterInitialization(string text) - { - var entry = new Entry - { - Text = "Test" - }; - - await SetValueAsync(entry, text, (h, s) => h.VirtualView.Text = s); - - Assert.Equal(0, entry.SelectionLength); } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs index c2d1560259..5888410ca3 100644 --- a/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.iOS.cs @@ -13,18 +13,18 @@ namespace Microsoft.Maui.DeviceTests { public partial class EntryTests { - UITextField GetPlatformControl(EntryHandler handler) => + static UITextField GetPlatformControl(EntryHandler handler) => (UITextField)handler.PlatformView; - Task GetPlatformText(EntryHandler handler) + static Task GetPlatformText(EntryHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } - void SetPlatformText(EntryHandler entryHandler, string text) => + static void SetPlatformText(EntryHandler entryHandler, string text) => GetPlatformControl(entryHandler).Text = text; - int GetPlatformCursorPosition(EntryHandler entryHandler) + static int GetPlatformCursorPosition(EntryHandler entryHandler) { var textField = GetPlatformControl(entryHandler); @@ -34,7 +34,7 @@ namespace Microsoft.Maui.DeviceTests return -1; } - int GetPlatformSelectionLength(EntryHandler entryHandler) + static int GetPlatformSelectionLength(EntryHandler entryHandler) { var textField = GetPlatformControl(entryHandler); diff --git a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs index 2c65ba89a1..b60d23b139 100644 --- a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Android.cs @@ -1,21 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Linq; using System.Threading.Tasks; +using Android.Widget; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; using SearchView = AndroidX.AppCompat.Widget.SearchView; namespace Microsoft.Maui.DeviceTests { public partial class SearchBarTests { - SearchView GetPlatformControl(SearchBarHandler handler) => + static SearchView GetPlatformControl(SearchBarHandler handler) => handler.PlatformView; - Task GetPlatformText(SearchBarHandler handler) + static Task GetPlatformText(SearchBarHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Query); } + + static int GetPlatformSelectionLength(SearchBarHandler searchBarHandler) + { + var control = GetPlatformControl(searchBarHandler); + var editText = control.GetChildrenOfType().FirstOrDefault(); + return editText.SelectionEnd - editText.SelectionStart; + } + + static int GetPlatformCursorPosition(SearchBarHandler searchBarHandler) + { + var control = GetPlatformControl(searchBarHandler); + var editText = control.GetChildrenOfType().FirstOrDefault(); + return editText.SelectionStart; + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs index 69347773eb..1dc922d50c 100644 --- a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.Windows.cs @@ -1,18 +1,47 @@ #nullable enable using System.Threading.Tasks; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Platform; using Microsoft.UI.Xaml.Controls; namespace Microsoft.Maui.DeviceTests { public partial class SearchBarTests { - AutoSuggestBox GetPlatformControl(SearchBarHandler handler) => + static AutoSuggestBox GetPlatformControl(SearchBarHandler handler) => handler.PlatformView; - Task GetPlatformText(SearchBarHandler handler) + static Task GetPlatformText(SearchBarHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } + + static int GetPlatformSelectionLength(SearchBarHandler searchBarHandler) + { + var platformSearchBar = GetPlatformControl(searchBarHandler); + + var textBox = platformSearchBar.GetFirstDescendant(); + + if (textBox is not null) + { + return textBox.SelectionLength; + } + + return -1; + } + + static int GetPlatformCursorPosition(SearchBarHandler searchBarHandler) + { + var platformSearchBar = GetPlatformControl(searchBarHandler); + + var textBox = platformSearchBar.GetFirstDescendant(); + + if (textBox is not null) + { + return textBox.SelectionStart; + } + + return -1; + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.cs b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.cs index 697f8732c6..7982a2220c 100644 --- a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.cs @@ -10,7 +10,9 @@ namespace Microsoft.Maui.DeviceTests [Category(TestCategory.SearchBar)] public partial class SearchBarTests : ControlsHandlerTestBase { - [Theory] + // TODO: remove these 2 tests and use SearchBarTextInputTests below + + [Theory(DisplayName = "Text is Transformed Correctly at Initialization")] [ClassData(typeof(TextTransformCases))] public async Task InitialTextTransformApplied(string text, TextTransform transform, string expected) { @@ -19,7 +21,7 @@ namespace Microsoft.Maui.DeviceTests Assert.Equal(expected, platformText); } - [Theory] + [Theory(DisplayName = "Text is Transformed Correctly after Initialization")] [ClassData(typeof(TextTransformCases))] public async Task TextTransformUpdated(string text, TextTransform transform, string expected) { @@ -54,5 +56,30 @@ namespace Microsoft.Maui.DeviceTests }); } #endif + +#if false + // TODO: The search bar controls are composite controls and need to be attached to the UI to run + [Category(TestCategory.SearchBar)] + [Category(TestCategory.TextInput)] + [Collection(RunInNewWindowCollection)] + public class SearchBarTextInputTests : TextInputTests + { + protected override int GetPlatformSelectionLength(SearchBarHandler handler) => + SearchBarTests.GetPlatformSelectionLength(handler); + + protected override int GetPlatformCursorPosition(SearchBarHandler handler) => + SearchBarTests.GetPlatformCursorPosition(handler); + + protected override Task GetPlatformText(SearchBarHandler handler) => + SearchBarTests.GetPlatformText(handler); + } +#endif + + [Category(TestCategory.SearchBar)] + [Category(TestCategory.TextInput)] + [Collection(RunInNewWindowCollection)] + public class SearchBarTextInputFocusTests : TextInputFocusTests + { + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.iOS.cs index 1635bb2c83..d8ff640be2 100644 --- a/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/SearchBar/SearchBarTests.iOS.cs @@ -6,12 +6,24 @@ namespace Microsoft.Maui.DeviceTests { public partial class SearchBarTests { - MauiSearchBar GetPlatformControl(SearchBarHandler handler) => + static MauiSearchBar GetPlatformControl(SearchBarHandler handler) => handler.PlatformView; - Task GetPlatformText(SearchBarHandler handler) + static Task GetPlatformText(SearchBarHandler handler) { return InvokeOnMainThreadAsync(() => GetPlatformControl(handler).Text); } + + static int GetPlatformSelectionLength(SearchBarHandler searchBarHandler) + { + var control = searchBarHandler.QueryEditor; + return control.GetSelectedTextLength(); + } + + static int GetPlatformCursorPosition(SearchBarHandler searchBarHandler) + { + var control = searchBarHandler.QueryEditor; + return control.GetCursorPosition(); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputFocusTests.Android.cs similarity index 52% rename from src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.Android.cs rename to src/Controls/tests/DeviceTests/Elements/TextInput/TextInputFocusTests.Android.cs index ba8b71ef93..cb526e7f6e 100644 --- a/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputFocusTests.Android.cs @@ -6,39 +6,37 @@ using Xunit; namespace Microsoft.Maui.DeviceTests { - [Category(TestCategory.TextInput)] [Collection(RunInNewWindowCollection)] - public partial class TextInputTests + public abstract partial class TextInputFocusTests : ControlsHandlerTestBase + where THandler : class, IViewHandler, IPlatformViewHandler, new() + where TView : VisualElement, ITextInput, new() { - [Theory] - [InlineData(typeof(Editor))] - [InlineData(typeof(Entry))] - [InlineData(typeof(SearchBar))] - public async Task ShowsKeyboardOnFocus(Type controlType) + [Fact] + public async Task ShowsKeyboardOnFocus() { - SetupBuilder(); - var textInput = (View)Activator.CreateInstance(controlType); + var textInput = new TView() as VisualElement; textInput.HeightRequest = 100; textInput.WidthRequest = 100; - await AttachAndRun(textInput, async (handler) => + + await AttachAndRun(textInput, async (handler) => { try { var platformView = handler.PlatformView; - await AssertionExtensions.HideKeyboardForView(textInput, message: $"Make sure keyboard starts out closed {controlType}"); + await AssertionExtensions.HideKeyboardForView(textInput, message: $"Make sure keyboard starts out closed"); textInput.Focus(); - await AssertionExtensions.WaitForFocused(platformView, message: $"WaitForFocused failed after first focus on {controlType}"); - await AssertionExtensions.WaitForKeyboardToShow(platformView, message: $"WaitForKeyboardToShow failed after first focus on {controlType}"); + await AssertionExtensions.WaitForFocused(platformView, message: $"WaitForFocused failed after first focus"); + await AssertionExtensions.WaitForKeyboardToShow(platformView, message: $"WaitForKeyboardToShow failed after first focus"); // Test that keyboard reappears when refocusing on an already focused TextInput control - await AssertionExtensions.HideKeyboardForView(textInput, message: $"HideKeyboardForView failed after first keyboard show on {controlType}"); + await AssertionExtensions.HideKeyboardForView(textInput, message: $"HideKeyboardForView failed after first keyboard show"); textInput.Focus(); - await AssertionExtensions.WaitForFocused(platformView, message: $"WaitForFocused failed after second focus on {controlType}"); - await AssertionExtensions.WaitForKeyboardToShow(platformView, message: $"WaitForKeyboardToShow failed after second focus on {controlType}"); + await AssertionExtensions.WaitForFocused(platformView, message: $"WaitForFocused failed after second focus"); + await AssertionExtensions.WaitForKeyboardToShow(platformView, message: $"WaitForKeyboardToShow failed after second focus"); } finally { - await AssertionExtensions.HideKeyboardForView(textInput, message: $"HideKeyboardForView after test has finished {controlType}"); + await AssertionExtensions.HideKeyboardForView(textInput, message: $"HideKeyboardForView after test has finished"); } }); } diff --git a/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputFocusTests.cs b/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputFocusTests.cs new file mode 100644 index 0000000000..6db30c303c --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputFocusTests.cs @@ -0,0 +1,12 @@ +using Microsoft.Maui.Controls; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + [Category(TestCategory.TextInput)] + public abstract partial class TextInputFocusTests : ControlsHandlerTestBase + where THandler : class, IViewHandler, IPlatformViewHandler, new() + where TView : VisualElement, ITextInput, new() + { + } +} diff --git a/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.cs b/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.cs index 53833aeec5..23e1fb262b 100644 --- a/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/TextInput/TextInputTests.cs @@ -1,24 +1,338 @@ -using Microsoft.Maui.Controls; -using Microsoft.Maui.Handlers; -using Microsoft.Maui.Hosting; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Xunit; namespace Microsoft.Maui.DeviceTests { - [Category(TestCategory.TextInput)] - public partial class TextInputTests : ControlsHandlerTestBase + public abstract partial class TextInputTests : ControlsHandlerTestBase + where THandler : class, IViewHandler, IPlatformViewHandler, new() + where TView : InputView, IView, ITextInput, new() { - void SetupBuilder() + protected abstract int GetPlatformCursorPosition(THandler handler); + + protected abstract int GetPlatformSelectionLength(THandler handler); + + protected abstract Task GetPlatformText(THandler handler); + + [Theory(DisplayName = "Text is Transformed Correctly at Initialization")] + [ClassData(typeof(TextTransformCases))] + public async Task InitialTextTransformApplied(string text, TextTransform transform, string expected) { - EnsureHandlerCreated(builder => + var control = new TView() { Text = text, TextTransform = transform }; + var platformText = await GetPlatformText(await CreateHandlerAsync(control)); + Assert.Equal(expected, platformText); + } + + [Theory(DisplayName = "Text is Transformed Correctly after Initialization")] + [ClassData(typeof(TextTransformCases))] + public async Task TextTransformUpdated(string text, TextTransform transform, string expected) + { + var control = new TView() { Text = text }; + var handler = await CreateHandlerAsync(control); + await InvokeOnMainThreadAsync(() => control.TextTransform = transform); + var platformText = await GetPlatformText(handler); + Assert.Equal(expected, platformText); + } + + [Theory(DisplayName = "Unset CursorPosition is kept at zero at initialization with TextTransform")] + [InlineData("This is a test!!!")] + [InlineData("a")] + [InlineData("")] + [InlineData(" ")] + public async Task UnsetCursorPositionKeepsToZeroOnInitializationWithTextTransform(string text) + { + var control = new TView { - builder.ConfigureMauiHandlers(handlers => - { - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - }); + Text = text, + TextTransform = TextTransform.Uppercase + }; + + await ValidatePropertyInitValue( + control, + () => control.CursorPosition, + GetPlatformCursorPosition, + 0); + } + + [Theory(DisplayName = "Unset SelectionLength is kept at zero at initialization with TextTransform")] + [InlineData("This is a test!!!")] + [InlineData("a")] + [InlineData("")] + [InlineData(" ")] + public async Task UnsetSelectionLengthKeepsToZeroOnInitializationWithTextTransform(string text) + { + var control = new TView + { + Text = text, + TextTransform = TextTransform.Uppercase + }; + + await ValidatePropertyInitValue( + control, + () => control.SelectionLength, + GetPlatformSelectionLength, + 0); + } + + [Theory(DisplayName = "CursorPosition Initializes Correctly")] + [InlineData(2)] + public async Task CursorPositionInitializesCorrectly(int initialPosition) + { + var control = new TView + { + Text = "This is TEXT!", + CursorPosition = initialPosition + }; + + await ValidatePropertyInitValue( + control, + () => control.CursorPosition, + GetPlatformCursorPosition, + initialPosition); + } + + [Theory] + [InlineData(2)] + public async Task CursorPositionInitializesCorrectlyWithUpdateCursorPositionLast(int initialPosition) + { + var control = new TView + { + Text = "This is TEXT!", + CursorPosition = initialPosition + }; + + var value = await GetValueAsync(control, handler => + { + handler.UpdateValue(nameof(ITextInput.CursorPosition)); + return GetPlatformCursorPosition(handler); }); + + Assert.Equal(initialPosition, value); + } + + [Theory] + [InlineData(2)] + public async Task CursorPositionInitializesCorrectlyWithUpdateTextLast(int initialPosition) + { + var control = new TView + { + Text = "This is TEXT!", + CursorPosition = initialPosition + }; + + var value = await GetValueAsync(control, handler => + { + handler.UpdateValue(nameof(ITextInput.Text)); + return GetPlatformCursorPosition(handler); + }); + + Assert.Equal(initialPosition, value); + } + + [Theory(DisplayName = "SelectionLength Initializes Correctly")] + [InlineData(2)] + public async Task SelectionLengthInitializesCorrectly(int initialLength) + { + var control = new TView + { + Text = "This is TEXT!", + SelectionLength = initialLength + }; + + await ValidatePropertyInitValue( + control, + () => control.SelectionLength, + GetPlatformSelectionLength, + initialLength); + } + + [Theory] + [InlineData(2)] + public async Task SelectionLengthInitializesCorrectlyWithUpdateCursorPositionLast(int initialLength) + { + var control = new TView + { + Text = "This is TEXT!", + SelectionLength = initialLength + }; + + var value = await GetValueAsync(control, handler => + { + handler.UpdateValue(nameof(ITextInput.CursorPosition)); + return GetPlatformSelectionLength(handler); + }); + + Assert.Equal(initialLength, value); + } + + [Theory] + [InlineData(2)] + public async Task SelectionLengthInitializesCorrectlyWithUpdateTextLast(int initialLength) + { + var control = new TView + { + Text = "This is TEXT!", + SelectionLength = initialLength + }; + + var value = await GetValueAsync(control, handler => + { + handler.UpdateValue(nameof(ITextInput.Text)); + return GetPlatformSelectionLength(handler); + }); + + Assert.Equal(initialLength, value); + } + + [Theory(DisplayName = "CursorPosition Updates Correctly")] + [InlineData(2, 5)] + public async Task CursorPositionUpdatesCorrectly(int setValue, int unsetValue) + { + string text = "This is TEXT!"; + + var control = new TView + { + Text = text + }; + + await ValidatePropertyUpdatesValue( + control, + nameof(ITextInput.CursorPosition), + GetPlatformCursorPosition, + setValue, + unsetValue + ); + } + + [Theory(DisplayName = "CursorPosition is Capped to Text's Length")] + [InlineData(30)] + public async Task CursorPositionIsCapped(int initialPosition) + { + string text = "This is TEXT!"; + + var control = new TView + { + Text = text, + CursorPosition = initialPosition + }; + + await ValidatePropertyInitValue( + control, + () => control.CursorPosition, + GetPlatformCursorPosition, + text.Length); + } + + [Theory(DisplayName = "Unset CursorPosition is kept at zero at initialization")] + [InlineData("This is a test!!!")] + [InlineData("a")] + [InlineData("")] + [InlineData(" ")] + public async Task UnsetCursorPositionKeepsToZeroOnInitialization(string text) + { + var control = new TView + { + Text = text + }; + + await ValidatePropertyInitValue( + control, + () => control.CursorPosition, + GetPlatformCursorPosition, + 0); + } + + [Theory(DisplayName = "CursorPosition moves to the end on text change after initialization")] + [InlineData("This is a test!!!")] + [InlineData("a")] + [InlineData("")] + [InlineData(" ")] + public async Task CursorPositionMovesToTheEndOnTextChangeAfterInitialization(string text) + { + var control = new TView + { + Text = "Test" + }; + + await AttachAndRunFocusAffectedControl(control, handler => control.Text = text); + + Assert.Equal(text.Length, control.CursorPosition); + } + + [Theory(DisplayName = "SelectionLength Updates Correctly")] + [InlineData(2, 5)] + public async Task SelectionLengthUpdatesCorrectly(int setValue, int unsetValue) + { + string text = "This is TEXT!"; + + var control = new TView + { + Text = text, + }; + + await ValidatePropertyUpdatesValue( + control, + nameof(ITextInput.SelectionLength), + GetPlatformSelectionLength, + setValue, + unsetValue + ); + } + + [Theory(DisplayName = "SelectionLength is Capped to Text Length")] + [InlineData(30)] + public async Task SelectionLengthIsCapped(int selectionLength) + { + string text = "This is TEXT!"; + + var control = new TView + { + Text = text, + SelectionLength = selectionLength + }; + + await ValidatePropertyInitValue( + control, + () => control.SelectionLength, + GetPlatformSelectionLength, + text.Length); + } + + [Theory(DisplayName = "Unset SelectionLength is kept at zero at initialization")] + [InlineData("This is a test!!!")] + [InlineData("a")] + [InlineData("")] + [InlineData(" ")] + public async Task UnsetSelectionLengthKeepsToZeroOnInitialization(string text) + { + var control = new TView + { + Text = text + }; + + await ValidatePropertyInitValue( + control, + () => control.SelectionLength, + GetPlatformSelectionLength, + 0); + } + + [Theory(DisplayName = "SelectionLength is kept at zero on text change after initialization")] + [InlineData("This is a test!!!")] + [InlineData("a")] + [InlineData("")] + [InlineData(" ")] + public async Task SelectionLengthKeepsToZeroOnTextChangeAfterInitialization(string text) + { + var control = new TView + { + Text = "Test" + }; + + await AttachAndRunFocusAffectedControl(control, handler => control.Text = text); + + Assert.Equal(0, control.SelectionLength); } } } diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs index 150e1b5431..992b7f0bab 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBasement.cs @@ -210,6 +210,7 @@ namespace Microsoft.Maui.DeviceTests return Task.FromResult(handler); }); } + public Task AttachAndRun(IView view, Func action) where TPlatformHandler : IPlatformViewHandler, IElementHandler, new() => @@ -223,6 +224,19 @@ namespace Microsoft.Maui.DeviceTests return result; }); + public Task AttachAndRun(IView view, Action action) + where TPlatformHandler : IPlatformViewHandler, IElementHandler, new() + => + view.AttachAndRun((handler) => + { + action(handler); + return true; + }, MauiContext, async (view) => + { + var result = await InvokeOnMainThreadAsync(() => CreateHandler(view)); + return result; + }); + protected Task AssertColorAtPoint(IView view, Color color, Type handlerType, int x, int y) { return InvokeOnMainThreadAsync(async () => diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/TestBase.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/TestBase.cs index 772653d57a..d5eaf4fe5b 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/TestBase.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/TestBase.cs @@ -9,19 +9,19 @@ namespace Microsoft.Maui.DeviceTests { public const int EmCoefficientPrecision = 4; - protected Task InvokeOnMainThreadAsync(Func func) => + protected static Task InvokeOnMainThreadAsync(Func func) => TestDispatcher.Current.DispatchAsync(func); - protected Task InvokeOnMainThreadAsync(Action action) => + protected static Task InvokeOnMainThreadAsync(Action action) => TestDispatcher.Current.DispatchAsync(action); - protected Task InvokeOnMainThreadAsync(Func action) => + protected static Task InvokeOnMainThreadAsync(Func action) => TestDispatcher.Current.DispatchAsync(action); - public Task InvokeOnMainThreadAsync(Func> func) => + protected static Task InvokeOnMainThreadAsync(Func> func) => TestDispatcher.Current.DispatchAsync(func); - protected async Task WaitForGC() + protected static async Task WaitForGC() { GC.Collect(); GC.WaitForPendingFinalizers(); From 462ca409a0e924719c840901df753d0dc9a42926 Mon Sep 17 00:00:00 2001 From: Juan Diego Herrera Date: Thu, 6 Jul 2023 17:41:00 -0400 Subject: [PATCH 02/15] Fix IsSpellCheckEnabled (#15459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change API docs * Tentative changes * Add MapIsSpellCheckEnabled mapper This at the xplat layer. Implementations will come later * Add iOS IsSpellCheckEnabled mapper * Add Windows IsSpellCheckEnabled mapper * Add Android IsSpellCheckEnabled mapper * Add Windows searchbar mapper * Add iOS searchbar mapper * Add Android searchbar mapper * Add Windows editor mapper * Add iOS editor mapper * Add Android editor mapper * Add API changes to Unshipped.txt * Remove unecessary usings * More RS0016 * More RS0016 * Revise Android inputs & other small errors * Add IsSpellCheckEnabled to test stubs * Add Tizen mappers * Add xml comment * More tizen build fixes * More tizen RS0016 * More tizen RS0016 * Start work tests (missing Keyboard-related tests) * Fix iOS typo * Windows and Android test checkin * Fix iOS typo * Checkin iOS tests * Finish Tests! * Clear up documentation * Fix isLoaded typo * Undo binary breaks * Undo more binary breaks * Address PR comments Co-authored-by: Gerald Versluis * Add Mapper documentation Adding only docs for the standard implementation. A brief search into the handler folder showed that no mapper was documented 🥲 I'm hoping that documenting the standard implementation will be what's displayed in the MSLearn page * Updated samples --------- Co-authored-by: Gerald Versluis Co-authored-by: Javier Suárez --- .../Microsoft.Maui.Controls/InputView.xml | 40 ----- .../Pages/Controls/EditorPage.xaml | 28 ++++ .../Pages/Controls/EntryPage.xaml | 28 ++++ .../Pages/Controls/SearchBarPage.xaml | 30 +++- src/Controls/src/Core/Editor/Editor.cs | 13 +- src/Controls/src/Core/Entry/Entry.cs | 19 +-- src/Controls/src/Core/InputView.cs | 16 +- .../Extensions/AutoSuggestBoxExtensions.cs | 10 -- .../net-android/PublicAPI.Shipped.txt | 6 - .../net-android/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net-ios/PublicAPI.Shipped.txt | 8 +- .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 3 + .../net-maccatalyst/PublicAPI.Shipped.txt | 6 - .../net-maccatalyst/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net-tizen/PublicAPI.Shipped.txt | 6 - .../net-tizen/PublicAPI.Unshipped.txt | 3 + .../net-windows/PublicAPI.Shipped.txt | 6 - .../net-windows/PublicAPI.Unshipped.txt | 3 + .../Core/PublicAPI/net/PublicAPI.Shipped.txt | 6 - .../PublicAPI/net/PublicAPI.Unshipped.txt | 3 + .../netstandard/PublicAPI.Shipped.txt | 6 - .../netstandard/PublicAPI.Unshipped.txt | 3 + .../src/Core/SearchBar/SearchBar.Mapper.cs | 4 +- .../src/Core/SearchBar/SearchBar.Windows.cs | 15 +- src/Controls/src/Core/SearchBar/SearchBar.cs | 12 +- src/Core/src/Core/ITextInput.cs | 5 + .../Handlers/Editor/EditorHandler.Android.cs | 3 + .../Handlers/Editor/EditorHandler.Standard.cs | 13 ++ .../Handlers/Editor/EditorHandler.Tizen.cs | 3 + .../Handlers/Editor/EditorHandler.Windows.cs | 3 + src/Core/src/Handlers/Editor/EditorHandler.cs | 1 + .../src/Handlers/Editor/EditorHandler.iOS.cs | 3 + .../Handlers/Entry/EntryHandler.Android.cs | 3 + .../Handlers/Entry/EntryHandler.Standard.cs | 13 ++ .../src/Handlers/Entry/EntryHandler.Tizen.cs | 3 + .../Handlers/Entry/EntryHandler.Windows.cs | 3 + src/Core/src/Handlers/Entry/EntryHandler.cs | 1 + .../src/Handlers/Entry/EntryHandler.iOS.cs | 3 + .../SearchBar/SearchBarHandler.Android.cs | 5 + .../SearchBar/SearchBarHandler.Standard.cs | 13 ++ .../SearchBar/SearchBarHandler.Tizen.cs | 3 + .../SearchBar/SearchBarHandler.Windows.cs | 6 + .../Handlers/SearchBar/SearchBarHandler.cs | 1 + .../SearchBar/SearchBarHandler.iOS.cs | 5 + .../Platform/Android/EditTextExtensions.cs | 44 ++++-- .../Platform/Android/KeyboardExtensions.cs | 46 ++---- .../Platform/Android/SearchViewExtensions.cs | 15 +- .../Platform/Windows/SearchBarExtensions.cs | 10 ++ .../src/Platform/Windows/TextBoxExtensions.cs | 7 +- .../src/Platform/iOS/KeyboardExtensions.cs | 18 +-- .../src/Platform/iOS/SearchBarExtensions.cs | 16 ++ .../src/Platform/iOS/TextFieldExtensions.cs | 11 ++ .../src/Platform/iOS/TextViewExtensions.cs | 11 ++ .../net-android/PublicAPI.Unshipped.txt | 7 + .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 7 + .../net-maccatalyst/PublicAPI.Unshipped.txt | 7 + .../net-tizen/PublicAPI.Unshipped.txt | 4 + .../net-windows/PublicAPI.Unshipped.txt | 8 +- .../src/PublicAPI/net/PublicAPI.Unshipped.txt | 4 + .../netstandard/PublicAPI.Unshipped.txt | 4 + .../netstandard2.0/PublicAPI.Unshipped.txt | 4 + .../Editor/EditorHandlerTests.Android.cs | 7 +- .../Editor/EditorHandlerTests.Windows.cs | 3 + .../Handlers/Editor/EditorHandlerTests.cs | 111 ++++++++++++- .../Handlers/Editor/EditorHandlerTests.iOS.cs | 5 +- .../Entry/EntryHandlerTests.Android.cs | 7 +- .../Entry/EntryHandlerTests.Windows.cs | 3 + .../Handlers/Entry/EntryHandlerTests.cs | 146 +++++++++++++++--- .../Handlers/Entry/EntryHandlerTests.iOS.cs | 5 +- .../SearchBarHandlerTests.Android.cs | 30 +++- .../SearchBarHandlerTests.Windows.cs | 33 ++-- .../SearchBar/SearchBarHandlerTests.cs | 134 +++++++++++++++- .../SearchBar/SearchBarHandlerTests.iOS.cs | 23 ++- .../tests/DeviceTests/Stubs/EditorStub.cs | 2 + src/Core/tests/DeviceTests/Stubs/EntryStub.cs | 2 + .../tests/DeviceTests/Stubs/SearchBarStub.cs | 2 + 76 files changed, 847 insertions(+), 259 deletions(-) diff --git a/src/Controls/docs/Microsoft.Maui.Controls/InputView.xml b/src/Controls/docs/Microsoft.Maui.Controls/InputView.xml index b31b24db6a..393586bfeb 100644 --- a/src/Controls/docs/Microsoft.Maui.Controls/InputView.xml +++ b/src/Controls/docs/Microsoft.Maui.Controls/InputView.xml @@ -97,46 +97,6 @@ To be added. - - - - - - Property - - Microsoft.Maui.Controls.Core - 0.0.0.0 - 2.0.0.0 - - - System.Boolean - - - Gets or sets a value that controls whether spell checking is enabled. - - if spell checking is enabled. Otherwise . - To be added. - - - - - - - - Field - - Microsoft.Maui.Controls.Core - 0.0.0.0 - 2.0.0.0 - - - Microsoft.Maui.Controls.BindableProperty - - - Backing store for the property. - To be added. - - diff --git a/src/Controls/samples/Controls.Sample/Pages/Controls/EditorPage.xaml b/src/Controls/samples/Controls.Sample/Pages/Controls/EditorPage.xaml index fb276e6022..1a9a975608 100644 --- a/src/Controls/samples/Controls.Sample/Pages/Controls/EditorPage.xaml +++ b/src/Controls/samples/Controls.Sample/Pages/Controls/EditorPage.xaml @@ -54,6 +54,34 @@ +