diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/MouseActionConverter.cs b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/MouseActionConverter.cs index 62f3ad65dc8..59fa1f97d6b 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/MouseActionConverter.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Input/Command/MouseActionConverter.cs @@ -87,7 +87,8 @@ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo cul if (value is null || destinationType != typeof(string)) throw GetConvertToException(value, destinationType); - return (MouseAction)value switch + MouseAction mouseAction = (MouseAction)value; + return mouseAction switch { MouseAction.None => string.Empty, MouseAction.LeftClick => "LeftClick", @@ -97,7 +98,7 @@ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo cul MouseAction.LeftDoubleClick => "LeftDoubleClick", MouseAction.RightDoubleClick => "RightDoubleClick", MouseAction.MiddleDoubleClick => "MiddleDoubleClick", - _ => throw new InvalidEnumArgumentException(nameof(value), (int)value, typeof(MouseAction)) + _ => throw new InvalidEnumArgumentException(nameof(value), (int)mouseAction, typeof(MouseAction)) }; } diff --git a/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Input/MouseActionConverter.Tests.cs b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Input/MouseActionConverter.Tests.cs new file mode 100644 index 00000000000..5fa2efad7d3 --- /dev/null +++ b/src/Microsoft.DotNet.Wpf/tests/UnitTests/PresentationCore.Tests/System/Windows/Input/MouseActionConverter.Tests.cs @@ -0,0 +1,197 @@ +// 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; + +public sealed class MouseActionConverterTests +{ + [Theory] + // Supported type + [InlineData(true, typeof(string))] + // Unsupported types + [InlineData(false, typeof(InstanceDescriptor))] + [InlineData(false, typeof(MouseAction))] + public void CanConvertFrom_ReturnsExpected(bool expected, Type sourceType) + { + MouseActionConverter converter = new(); + + Assert.Equal(expected, converter.CanConvertFrom(null, sourceType)); + } + + [Theory] + [MemberData(nameof(CanConvertTo_Data))] + public void CanConvertTo_ReturnsExpected(bool expected, bool passContext, object? value, Type? destinationType) + { + MouseActionConverter 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, MouseAction.None, typeof(string) }; + yield return new object[] { true, true, MouseAction.MiddleDoubleClick, typeof(string) }; + + // Unsupported case (Value is above MouseAction range) + yield return new object[] { false, true, MouseAction.MiddleDoubleClick + 1, typeof(string) }; + + // Unsupported cases + yield return new object[] { false, false, MouseAction.None, typeof(string) }; + yield return new object[] { false, false, MouseAction.MiddleDoubleClick, typeof(string) }; + yield return new object?[] { false, true, null, typeof(MouseAction) }; + yield return new object?[] { false, true, null, typeof(string) }; + yield return new object?[] { false, false, MouseAction.MiddleDoubleClick, typeof(string) }; + yield return new object?[] { false, false, null, typeof(string) }; + yield return new object?[] { false, false, null, null }; + + yield return new object[] { false, true, MouseAction.MiddleDoubleClick + 1, typeof(string) }; + } + } + + [Fact] + public void CanConvertTo_ThrowsInvalidCastException() + { + MouseActionConverter converter = new(); + StandardContextImpl context = new() { Instance = 10 }; + + // TODO: CanConvert* methods should not throw but the implementation is faulty + Assert.Throws(() => converter.CanConvertTo(context, typeof(string))); + } + + [Theory] + [MemberData(nameof(ConvertFrom_ReturnsExpected_Data))] + public void ConvertFrom_ReturnsExpected(MouseAction expected, ITypeDescriptorContext context, CultureInfo? cultureInfo, string value) + { + MouseActionConverter converter = new(); + + Assert.Equal(expected, (MouseAction)converter.ConvertFrom(context, cultureInfo, value)); + } + + public static IEnumerable ConvertFrom_ReturnsExpected_Data + { + get + { + // ConvertTo handles two different inputs as MouseAction.None + yield return new object?[] { MouseAction.None, null, CultureInfo.InvariantCulture, string.Empty }; + yield return new object?[] { MouseAction.None, null, CultureInfo.InvariantCulture, "None" }; + + // Supported cases (Culture must stay irrelevant) + yield return new object?[] { MouseAction.None, null, CultureInfo.InvariantCulture, string.Empty }; + yield return new object?[] { MouseAction.LeftClick, null, new CultureInfo("ru-RU"), "LeftClick" }; + yield return new object?[] { MouseAction.RightClick, null, CultureInfo.InvariantCulture, "RightClick" }; + yield return new object?[] { MouseAction.MiddleClick, null, CultureInfo.InvariantCulture, "MiddleClick" }; + yield return new object?[] { MouseAction.WheelClick, null, new CultureInfo("no-NO"), "WheelClick" }; + yield return new object?[] { MouseAction.LeftDoubleClick, null, CultureInfo.InvariantCulture, "LeftDoubleClick" }; + yield return new object?[] { MouseAction.RightDoubleClick, null, CultureInfo.InvariantCulture, "RightDoubleClick" }; + yield return new object?[] { MouseAction.MiddleDoubleClick, null, CultureInfo.InvariantCulture, "MiddleDoubleClick" }; + + // Supported cases (fuzzed via whitespace and casing) + yield return new object?[] { MouseAction.None, null, CultureInfo.InvariantCulture, " " }; + yield return new object?[] { MouseAction.None, null, new CultureInfo("ru-RU"), " NoNE " }; + yield return new object?[] { MouseAction.LeftClick, null, CultureInfo.InvariantCulture, " LeFTCliCK " }; + yield return new object?[] { MouseAction.WheelClick, null, CultureInfo.InvariantCulture, " WHEELCLICK" }; + yield return new object?[] { MouseAction.MiddleClick, null, new CultureInfo("no-NO"), " MiDDLeCliCK " }; + yield return new object?[] { MouseAction.LeftDoubleClick, null, CultureInfo.InvariantCulture, " leftdoubleclick " }; + yield return new object?[] { MouseAction.RightClick, null, CultureInfo.InvariantCulture, " rightclick" }; + } + } + + [Theory] + // Unsupported values (data type) + [InlineData(null)] + [InlineData(Key.VolumeDown)] + [InlineData(MouseAction.None)] + // Unsupported value (bad string) + [InlineData("BadString")] + [InlineData("MouseZClick")] + public void ConvertFrom_ThrowsNotSupportedException(object? value) + { + MouseActionConverter converter = new(); + + Assert.Throws(() => converter.ConvertFrom(null, null, value)); + } + + [Theory] + [MemberData(nameof(ConvertTo_ReturnsExpected_Data))] + public void ConvertTo_ReturnsExpected(string expected, ITypeDescriptorContext context, CultureInfo? cultureInfo, object? value) + { + MouseActionConverter converter = new(); + + // Culture and context must not have any meaning + Assert.Equal(expected, (string)converter.ConvertTo(context, cultureInfo, value, typeof(string))); + } + + public static IEnumerable ConvertTo_ReturnsExpected_Data + { + get + { + // Supported cases (Culture must stay irrelevant) + yield return new object?[] { string.Empty, null, CultureInfo.InvariantCulture, MouseAction.None }; + yield return new object?[] { "LeftClick", null, CultureInfo.InvariantCulture, MouseAction.LeftClick }; + yield return new object?[] { "RightClick", null, new CultureInfo("ru-RU"), MouseAction.RightClick }; + yield return new object?[] { "MiddleClick", null, CultureInfo.InvariantCulture, MouseAction.MiddleClick }; + yield return new object?[] { "WheelClick", null, new CultureInfo("no-NO"), MouseAction.WheelClick }; + yield return new object?[] { "LeftDoubleClick", null, CultureInfo.InvariantCulture, MouseAction.LeftDoubleClick }; + yield return new object?[] { "RightDoubleClick", null, null, MouseAction.RightDoubleClick }; + yield return new object?[] { "MiddleDoubleClick", null, null, MouseAction.MiddleDoubleClick }; + } + } + + [Fact] + public void ConvertTo_ThrowsArgumentNullException() + { + MouseActionConverter converter = new(); + + Assert.Throws(() => converter.ConvertTo(MouseAction.None, destinationType: null!)); + } + + [Theory] + // Unsupported value + [InlineData(null, typeof(string))] + // Unsupported destinationType + [InlineData(MouseAction.None, typeof(int))] + [InlineData(MouseAction.LeftClick, typeof(byte))] + public void ConvertTo_ThrowsNotSupportedException(object? value, Type destinationType) + { + MouseActionConverter converter = new(); + + Assert.Throws(() => converter.ConvertTo(value, destinationType)); + } + + [Fact] + public void ConvertTo_ThrowsInvalidCastException() + { + MouseActionConverter converter = new(); + + // TODO: This should not throw InvalidCastException but NotSupportedException + Assert.Throws(() => converter.ConvertTo(null, null, (int)(MouseAction.MiddleDoubleClick), typeof(string))); + } + + [Fact] + public void ConvertTo_ThrowsInvalidEnumArgumentException() + { + MouseActionConverter converter = new(); + + Assert.Throws(() => converter.ConvertTo(null, null, (MouseAction)(MouseAction.MiddleDoubleClick + 1), 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(); + } +}