From a7acd6a90b7f30507e14c148953d1b20e4ed6eaf Mon Sep 17 00:00:00 2001 From: h3xds1nz Date: Tue, 21 Jan 2025 07:00:19 +0100 Subject: [PATCH] Add test coverage for public API surface of KeyGestureConverter (#10259) * Add test coverage for public surface of KeyGestureConverter * Add additional case that must pass (plus in a display string) * Add tests for wrong type of context instance --- .../Command/KeyGestureConverter.Tests.cs | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Input/Command/KeyGestureConverter.Tests.cs diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Input/Command/KeyGestureConverter.Tests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Input/Command/KeyGestureConverter.Tests.cs new file mode 100644 index 00000000000..61e3269d722 --- /dev/null +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Input/Command/KeyGestureConverter.Tests.cs @@ -0,0 +1,229 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.Design.Serialization; +using System.ComponentModel; +using System.Globalization; + +namespace System.Windows.Input.Command; + +public sealed class KeyGestureConverterTests +{ + [Theory] + // Valid type + [InlineData(true, typeof(string))] + // Invalid types + [InlineData(false, typeof(Key))] + [InlineData(false, typeof(ModifierKeys))] + [InlineData(false, typeof(KeyGesture))] + [InlineData(false, typeof(MouseGesture))] + [InlineData(false, typeof(InstanceDescriptor))] + public void CanConvertFrom_ReturnsExpected(bool expected, Type sourceType) + { + KeyGestureConverter converter = new(); + + Assert.Equal(expected, converter.CanConvertFrom(sourceType)); + } + + [Theory] + [MemberData(nameof(CanConvertTo_Data))] + public void CanConvertTo_ReturnsExpected(bool expected, bool passContext, object? value, Type? destinationType) + { + KeyGestureConverter converter = new(); + StandardContextImpl context = new() { Instance = value }; + + Assert.Equal(expected, converter.CanConvertTo(passContext ? context : null, destinationType)); + } + + public static IEnumerable CanConvertTo_Data + { + get + { + // Supported cases + yield return new object[] { true, true, new KeyGesture(Key.NumLock, ModifierKeys.Control), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.F1, ModifierKeys.Alt, "displayString"), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.G, ModifierKeys.None, validateGesture: false), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.Insert, ModifierKeys.Control | ModifierKeys.Windows | ModifierKeys.Alt), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.NumLock, ModifierKeys.Control | ModifierKeys.Windows), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.F21, ModifierKeys.Alt | ModifierKeys.Windows, "displayString"), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.F8, ModifierKeys.Alt | ModifierKeys.Control, "Two Modifiers"), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.A, ModifierKeys.Alt | ModifierKeys.Windows | ModifierKeys.Control, "Test String"), typeof(string) }; + yield return new object[] { true, true, new KeyGesture(Key.Z, ModifierKeys.None, validateGesture: false), typeof(string) }; + + // Unsupported cases (Null Context) + yield return new object?[] { false, false, null, typeof(string) }; + // Unsupported cases (Null Context/Destination Type) + yield return new object?[] { false, false, null, null }; + // Unsupported cases (Null Instance) + yield return new object?[] { false, true, null, typeof(string) }; + // Unsupported cases (Null Instance/Destination Type) + yield return new object?[] { false, true, null, null }; + // Unsupported cases (Wrong destination type) + yield return new object?[] { false, true, new KeyGesture(Key.D1, ModifierKeys.Control), null }; + yield return new object?[] { false, true, new KeyGesture(Key.A, ModifierKeys.Alt), typeof(KeyGesture) }; + yield return new object?[] { false, true, new KeyGesture(Key.F5, ModifierKeys.Windows), typeof(MouseGesture) }; + yield return new object?[] { false, true, new KeyGesture(Key.A, ModifierKeys.Alt), typeof(Key) }; + yield return new object?[] { false, true, new KeyGesture(Key.F5, ModifierKeys.Windows), typeof(ModifierKeys) }; + // Unsupported cases (Wrong Context Instance) + yield return new object?[] { false, true, new MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt), typeof(string) }; + yield return new object?[] { false, true, MouseAction.WheelClick, typeof(string) }; + yield return new object?[] { false, true, Key.F1, typeof(string) }; + + // We do not test for malformed KeyGesture as KeyGesture has to perform its own validation and shall be enforced via its own unit tests + } + } + + [Theory] + [MemberData(nameof(ConvertFrom_ReturnsExpected_Data))] + public void ConvertFrom_ReturnsExpected(KeyGesture expected, ITypeDescriptorContext context, CultureInfo? cultureInfo, string value) + { + KeyGestureConverter converter = new(); + + KeyGesture converted = (KeyGesture)converter.ConvertFrom(context, cultureInfo, value); + Assert.Equal(expected.Key, converted.Key); + Assert.Equal(expected.Modifiers, converted.Modifiers); + Assert.Equal(expected.DisplayString, converted.DisplayString); + } + + public static IEnumerable ConvertFrom_ReturnsExpected_Data + { + get + { + // Supported special case + yield return new object?[] { new KeyGesture(Key.None, ModifierKeys.None, validateGesture: false), null, CultureInfo.InvariantCulture, string.Empty }; + + // Supported cases (Culture must stay irrelevant, Key/ModifierKeys also do not care) + yield return new object?[] { new KeyGesture(Key.NumLock, ModifierKeys.Control), null, CultureInfo.InvariantCulture, "Ctrl+NumLock" }; + yield return new object?[] { new KeyGesture(Key.A, ModifierKeys.Alt), null, CultureInfo.InvariantCulture, "Alt+A" }; + yield return new object?[] { new KeyGesture(Key.Back, ModifierKeys.Windows, "Massive Test"), null, CultureInfo.InvariantCulture, "Windows+Backspace,Massive Test" }; + yield return new object?[] { new KeyGesture(Key.F1, ModifierKeys.Alt, "displayString"), null, CultureInfo.InvariantCulture, "Alt+F1,displayString" }; + yield return new object?[] { new KeyGesture(Key.Insert, ModifierKeys.Control | ModifierKeys.Windows | ModifierKeys.Alt), null, new CultureInfo("de-DE"), "Ctrl+Alt+Windows+Insert", }; + yield return new object?[] { new KeyGesture(Key.Insert, ModifierKeys.Control | ModifierKeys.Windows | ModifierKeys.Alt), null, CultureInfo.InvariantCulture, "Ctrl+Alt+Windows+Insert" }; + yield return new object?[] { new KeyGesture(Key.NumLock, ModifierKeys.Control | ModifierKeys.Windows), null, CultureInfo.InvariantCulture, "Ctrl+Windows+NumLock" }; + yield return new object?[] { new KeyGesture(Key.F21, ModifierKeys.Alt | ModifierKeys.Windows, "displayString"), null, CultureInfo.InvariantCulture, "Alt+Windows+F21,displayString" }; + yield return new object?[] { new KeyGesture(Key.F21, ModifierKeys.Alt | ModifierKeys.Windows, "displayString"), null, new CultureInfo("ru-RU"), "Alt+Windows+F21,displayString" }; + yield return new object?[] { new KeyGesture(Key.F8, ModifierKeys.Alt | ModifierKeys.Control, "Two Modifiers"), null, CultureInfo.InvariantCulture, "Ctrl+Alt+F8,Two Modifiers" }; + yield return new object?[] { new KeyGesture(Key.A, ModifierKeys.Alt | ModifierKeys.Windows | ModifierKeys.Control, "Test String"), null, CultureInfo.InvariantCulture, "Ctrl+Alt+Windows+A,Test String" }; + + // Supported cases (fuzzed) + yield return new object?[] { new KeyGesture(Key.A, ModifierKeys.Alt, "Accept+Plus"), null, CultureInfo.InvariantCulture, "Alt+A,Accept+Plus" }; + yield return new object?[] { new KeyGesture(Key.NumLock, ModifierKeys.Control), null, CultureInfo.InvariantCulture, " Ctrl + NumLock " }; + yield return new object?[] { new KeyGesture(Key.A, ModifierKeys.Alt), null, CultureInfo.InvariantCulture, "Alt+A " }; + yield return new object?[] { new KeyGesture(Key.Back, ModifierKeys.Windows, "Massive Test"), null, CultureInfo.InvariantCulture, "Windows+ Backspace, Massive Test" }; + yield return new object?[] { new KeyGesture(Key.F1, ModifierKeys.Alt, ",,,,,,,,displayString"), null, CultureInfo.InvariantCulture, "Alt+F1,,,,,,,,,displayString" }; + yield return new object?[] { new KeyGesture(Key.Insert, ModifierKeys.Control | ModifierKeys.Windows | ModifierKeys.Alt), null, new CultureInfo("de-DE"), "Ctrl+Alt+Windows+Insert ", }; + yield return new object?[] { new KeyGesture(Key.F24, ModifierKeys.Alt | ModifierKeys.Windows, ",,, displayString"), null, CultureInfo.InvariantCulture, " Alt+Windows+ F24 ,,,, displayString" }; + yield return new object?[] { new KeyGesture(Key.F8, ModifierKeys.Alt | ModifierKeys.Control, "Two,,, Modifiers"), null, CultureInfo.InvariantCulture, "Ctrl+Alt+F8,Two,,, Modifiers" }; + yield return new object?[] { new KeyGesture(Key.D8, ModifierKeys.Alt | ModifierKeys.Windows | ModifierKeys.Control, ",, Test String,"), null, CultureInfo.InvariantCulture, "Ctrl+Alt+Windows+8 ,,, Test String, " }; + } + } + + [Theory] + [MemberData(nameof(ConvertFrom_ThrowsNotSupportedException_Data))] + public void ConvertFrom_ThrowsNotSupportedException(CultureInfo? cultureInfo, object value) + { + KeyGestureConverter converter = new(); + + Assert.Throws(() => converter.ConvertFrom(null, cultureInfo, value)); + } + + public static IEnumerable ConvertFrom_ThrowsNotSupportedException_Data + { + get + { + // This one actually comes from KeyGesture (see https://github.com/dotnet/wpf/issues/8639) [possibly TODO] + yield return new object?[] { CultureInfo.InvariantCulture, "Z" }; + // Nulls are not supported + yield return new object?[] { CultureInfo.InvariantCulture, null }; + // Anything that isn't a string ain't supported + yield return new object?[] { CultureInfo.InvariantCulture, new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control) }; + yield return new object?[] { CultureInfo.InvariantCulture, new KeyGesture(Key.V, ModifierKeys.Control) }; + yield return new object?[] { CultureInfo.InvariantCulture, ModifierKeys.Control }; + yield return new object?[] { CultureInfo.InvariantCulture, Key.V }; + } + } + + [Theory] + [MemberData(nameof(ConvertTo_ReturnsExpected_Data))] + public void ConvertTo_ReturnsExpected(string expected, ITypeDescriptorContext context, CultureInfo? cultureInfo, object? value) + { + KeyGestureConverter converter = new(); + + // Culture and context must not have any meaning + Assert.Equal(expected, converter.ConvertTo(context, cultureInfo, value, typeof(string))); + } + + public static IEnumerable ConvertTo_ReturnsExpected_Data + { + get + { + // Supported null value case that returns string.Empty + yield return new object?[] { string.Empty, null, CultureInfo.InvariantCulture, null }; + + // Supported special cases + yield return new object?[] { string.Empty, null, CultureInfo.InvariantCulture, new KeyGesture(Key.None, ModifierKeys.None, validateGesture: false) }; + yield return new object?[] { string.Empty, null, CultureInfo.InvariantCulture, new KeyGesture(Key.None, ModifierKeys.Control, validateGesture: false) }; + yield return new object?[] { string.Empty, null, new CultureInfo("de-DE"), new KeyGesture(Key.None, ModifierKeys.Windows, validateGesture: false) }; + yield return new object?[] { string.Empty, null, new CultureInfo("ru-RU"), new KeyGesture(Key.None, ModifierKeys.Alt, validateGesture: false) }; + + // Supported cases (Culture must stay irrelevant, Key/ModifierKeys also do not care) + yield return new object?[] { "Z", null, CultureInfo.InvariantCulture, new KeyGesture(Key.Z, ModifierKeys.None, validateGesture: false) }; + yield return new object?[] { "Ctrl+NumLock", null, CultureInfo.InvariantCulture, new KeyGesture(Key.NumLock, ModifierKeys.Control) }; + yield return new object?[] { "Alt+A", null, CultureInfo.InvariantCulture, new KeyGesture(Key.A, ModifierKeys.Alt) }; + yield return new object?[] { "Windows+Backspace,Massive Test", null, CultureInfo.InvariantCulture, new KeyGesture(Key.Back, ModifierKeys.Windows, "Massive Test") }; + yield return new object?[] { "Alt+F1,displayString", null, CultureInfo.InvariantCulture, new KeyGesture(Key.F1, ModifierKeys.Alt, "displayString") }; + yield return new object?[] { "Ctrl+Alt+Windows+Insert", null, new CultureInfo("de-DE"), new KeyGesture(Key.Insert, ModifierKeys.Control | ModifierKeys.Windows | ModifierKeys.Alt) }; + yield return new object?[] { "Ctrl+Alt+Windows+Insert", null, CultureInfo.InvariantCulture, new KeyGesture(Key.Insert, ModifierKeys.Control | ModifierKeys.Windows | ModifierKeys.Alt) }; + yield return new object?[] { "Ctrl+Windows+NumLock", null, CultureInfo.InvariantCulture, new KeyGesture(Key.NumLock, ModifierKeys.Control | ModifierKeys.Windows) }; + yield return new object?[] { "Alt+Windows+F21,displayString", null, CultureInfo.InvariantCulture, new KeyGesture(Key.F21, ModifierKeys.Alt | ModifierKeys.Windows, "displayString") }; + yield return new object?[] { "Alt+Windows+F21,displayString", null, new CultureInfo("ru-RU"), new KeyGesture(Key.F21, ModifierKeys.Alt | ModifierKeys.Windows, "displayString") }; + yield return new object?[] { "Ctrl+Alt+F8,Two Modifiers", null, CultureInfo.InvariantCulture, new KeyGesture(Key.F8, ModifierKeys.Alt | ModifierKeys.Control, "Two Modifiers") }; + yield return new object?[] { "Ctrl+Alt+Windows+A,Test String", null, CultureInfo.InvariantCulture, new KeyGesture(Key.A, ModifierKeys.Alt | ModifierKeys.Windows | ModifierKeys.Control, "Test String") }; + } + } + + [Fact] + public void ConvertTo_ThrowsArgumentNullException() + { + KeyGestureConverter converter = new(); + + Assert.Throws(() => converter.ConvertTo(null, CultureInfo.InvariantCulture, new KeyGesture(Key.C, ModifierKeys.Control), null)); + } + + [Theory] + [MemberData(nameof(ConvertTo_ThrowsNotSupportedException_Data))] + public void ConvertTo_ThrowsNotSupportedException(object? value, Type? destinationType) + { + KeyGestureConverter converter = new(); + + Assert.Throws(() => converter.ConvertTo(null, CultureInfo.InvariantCulture, value, destinationType)); + } + + public static IEnumerable ConvertTo_ThrowsNotSupportedException_Data + { + get + { + // Wrong destination types + yield return new object?[] { new KeyGesture(Key.V, ModifierKeys.Control), typeof(MouseGesture) }; + yield return new object?[] { new KeyGesture(Key.V, ModifierKeys.Control), typeof(KeyGesture) }; + yield return new object?[] { new KeyGesture(Key.V, ModifierKeys.Control), typeof(Key) }; + yield return new object?[] { new KeyGesture(Key.V, ModifierKeys.Control), typeof(ModifierKeys) }; + // Wrong value types + yield return new object?[] { new MouseGesture(MouseAction.LeftClick, ModifierKeys.Control), typeof(string) }; + yield return new object?[] { ModifierKeys.Control, typeof(string) }; + yield return new object?[] { Key.V, typeof(string) }; + } + } + + public sealed class StandardContextImpl : ITypeDescriptorContext + { + public IContainer? Container => throw new NotImplementedException(); + + public object? Instance { get; set; } + + public PropertyDescriptor? PropertyDescriptor => throw new NotImplementedException(); + public object? GetService(Type serviceType) => throw new NotImplementedException(); + public void OnComponentChanged() => throw new NotImplementedException(); + public bool OnComponentChanging() => throw new NotImplementedException(); + } +}