diff --git a/src/CommunityToolkit.Maui.UnitTests/Behaviors/BaseBehaviorTests.cs b/src/CommunityToolkit.Maui.UnitTests/Behaviors/BaseBehaviorTests.cs new file mode 100644 index 0000000000..f31d7a6d73 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Behaviors/BaseBehaviorTests.cs @@ -0,0 +1,47 @@ +using CommunityToolkit.Maui.UnitTests.Mocks; +using FluentAssertions; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Behaviors; + +public class BaseBehaviorTests +{ + [Fact] + public void AttachAndDetachCallsShouldCorrectlyAssignView() + { + var label = new Label(); + var mockBehavior = new MockBehavior(); + + mockBehavior.AssertViewIsNull(); + mockBehavior.IsAttached.Should().BeFalse(); + + label.Behaviors.Add(mockBehavior); + + mockBehavior.AssertViewIsEqual(label); + mockBehavior.IsAttached.Should().BeTrue(); + + label.Behaviors.Remove(mockBehavior); + + mockBehavior.AssertViewIsNull(); + mockBehavior.IsAttached.Should().BeFalse(); + } + + [Fact] + public void ViewPropertyChangesShouldBeHandledWithinBehavior() + { + var label = new Label(); + var mockBehavior = new MockBehavior(); + + label.Behaviors.Add(mockBehavior); + + mockBehavior.PropertyChanges.Should().BeEmpty(); + + label.Text = "Text"; + + mockBehavior.PropertyChanges.Should().ContainSingle(); + var change = mockBehavior.PropertyChanges.Single(); + + change.Should().NotBeNull(); + change.PropertyName.Should().Be(nameof(Label.Text)); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterOneWayTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterOneWayTests.cs new file mode 100644 index 0000000000..d016c205f3 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterOneWayTests.cs @@ -0,0 +1,103 @@ +using System.Globalization; +using CommunityToolkit.Maui.Converters; +using CommunityToolkit.Maui.UnitTests.Mocks; +using FluentAssertions; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Converters; + +public abstract class BaseConverterOneWayTests +{ + [Theory] + [InlineData(4.123, typeof(double))] + [InlineData(4, typeof(int))] + public abstract void Convert_WithMismatchedTargetType(object? inputValue, Type targetType); + + [Theory] + [InlineData(4.123, typeof(string))] + [InlineData(true, typeof(string))] + public abstract void Convert_WithInvalidValueType(object? inputValue, Type targetType); + + [Theory] + [InlineData(1, typeof(string))] + public void Convert_ShouldCorrectlyReturnValueWithMatchingTargetType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); + + Assert.Equal("Two", converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void Setting_DefaultConvertBackReturnValue_WillThrowNotSupportedException() + { + ICommunityToolkitValueConverter converter = CreateConverter(); + + Assert.Throws(() => new MockOneWayConverter(["One", "Two", "Three"]) + { + DefaultConvertReturnValue = "Three", + DefaultConvertBackReturnValue = 1 + }); + } + + protected static BaseConverter CreateConverter() => new MockOneWayConverter(["One", "Two", "Three"]) + { + DefaultConvertReturnValue = "Three" + }; + + protected BaseConverterOneWayTests(bool suppressExceptions) + { + new Options().SetShouldSuppressExceptionsInConverters(suppressExceptions); + } +} + +/// +/// Unit tests that target and their public APIs with == false. +/// +public class BaseConverterOneWayTestsWithExceptionsEnabled : BaseConverterOneWayTests +{ + public BaseConverterOneWayTestsWithExceptionsEnabled() : base(false) + { + } + + public override void Convert_WithMismatchedTargetType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); + + var exception = Assert.Throws(() => converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture)); + + exception.Message.Should().Be($"targetType needs to be assignable from {converter.ToType}. (Parameter 'targetType')"); + } + + public override void Convert_WithInvalidValueType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); + + var exception = Assert.Throws(() => converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture)); + + exception.Message.Should().Be($"Value needs to be of type {converter.FromType} (Parameter 'value')"); + } +} + +/// +/// Unit tests that target and their public APIs with == true. +/// +public class BaseConverterOneWayTestsWithExceptionsSuppressed : BaseConverterOneWayTests +{ + public BaseConverterOneWayTestsWithExceptionsSuppressed() : base(true) + { + } + + public override void Convert_WithMismatchedTargetType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); + + converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture).Should().Be(converter.DefaultConvertReturnValue); + } + + public override void Convert_WithInvalidValueType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); + + converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture).Should().Be(converter.DefaultConvertReturnValue); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterTest.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterTest.cs new file mode 100644 index 0000000000..edfd167b90 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterTest.cs @@ -0,0 +1,92 @@ +using System.Globalization; +using CommunityToolkit.Maui.Converters; +using Xunit; + +namespace CommunityToolkit.Maui.UnitTests.Converters; + +public abstract class BaseOneWayConverterTest : ConverterTest where TConverter : ICommunityToolkitValueConverter, new() +{ + [Fact] + public void ConvertBack_ShouldThrowNotSupportedException() + { + var options = new Options(); + options.SetShouldSuppressExceptionsInConverters(true); + + var converter = InitializeConverterForInvalidConverterTests(); + + Assert.ThrowsAny(() => converter.ConvertBack(GetInvalidConvertBackValue(), converter.FromType, null, CultureInfo.CurrentCulture)); + } +} + +public abstract class BaseConverterTest : ConverterTest where TConverter : ICommunityToolkitValueConverter, new() +{ + [Fact] + public void InvalidConvertBackValue_ShouldThrowException() + { + var options = new Options(); + options.SetShouldSuppressExceptionsInConverters(false); + + var converter = InitializeConverterForInvalidConverterTests(); + + Assert.ThrowsAny(() => converter.ConvertBack(GetInvalidConvertBackValue(), converter.FromType, null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void InvalidConvertBackValue_ShouldSuppressExceptionsInConverters_ShouldReturnDefaultConvertValue() + { + var options = new Options(); + options.SetShouldSuppressExceptionsInConverters(true); + + var converter = InitializeConverterForInvalidConverterTests(); + + var result = converter.ConvertBack(GetInvalidConvertBackValue(), converter.FromType, null, CultureInfo.CurrentCulture); + + Assert.Equal(converter.DefaultConvertBackReturnValue, result); + } +} + +public abstract class ConverterTest : BaseHandlerTest where TConverter : ICommunityToolkitValueConverter, new() +{ + [Fact] + public void InvalidConvertValue_ShouldThrowException() + { + var options = new Options(); + options.SetShouldSuppressExceptionsInConverters(false); + + var converter = InitializeConverterForInvalidConverterTests(); + + Assert.ThrowsAny(() => converter.Convert(GetInvalidConvertFromValue(), converter.ToType, null, CultureInfo.CurrentCulture)); + } + + [Fact] + public void InvalidConverterValue_ShouldSuppressExceptionsInConverters_ShouldReturnDefaultConvertValue() + { + var options = new Options(); + options.SetShouldSuppressExceptionsInConverters(true); + + var converter = InitializeConverterForInvalidConverterTests(); + + var result = converter.Convert(GetInvalidConvertFromValue(), converter.ToType, null, CultureInfo.CurrentCulture); + + Assert.Equal(converter.DefaultConvertReturnValue, result); + } + + protected virtual object? GetInvalidConvertBackValue() => GetInvalidValue(InitializeConverterForInvalidConverterTests().ToType); + protected virtual object? GetInvalidConvertFromValue() => GetInvalidValue(InitializeConverterForInvalidConverterTests().FromType); + protected virtual TConverter InitializeConverterForInvalidConverterTests() => new(); + + static object GetInvalidValue(Type type) + { + if (type != typeof(string)) + { + return string.Empty; + } + + if (type != typeof(bool)) + { + return true; + } + + throw new NotImplementedException($"Invalid value not valid for {typeof(TConverter).Name}. If {nameof(InvalidConvertValue_ShouldThrowException)} is failing, please override {nameof(GetInvalidConvertFromValue)} and provide an invalid value. Otherwise, override {nameof(GetInvalidConvertBackValue)} and provide an invalid value"); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterTests.cs index edfd167b90..8b92c2d723 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/BaseConverterTests.cs @@ -1,92 +1,146 @@ -using System.Globalization; +using System.Globalization; using CommunityToolkit.Maui.Converters; +using CommunityToolkit.Maui.UnitTests.Mocks; +using FluentAssertions; using Xunit; namespace CommunityToolkit.Maui.UnitTests.Converters; -public abstract class BaseOneWayConverterTest : ConverterTest where TConverter : ICommunityToolkitValueConverter, new() +/// +/// Unit tests that target and their public APIs. +/// +public abstract class BaseConverterTests { - [Fact] - public void ConvertBack_ShouldThrowNotSupportedException() + [Theory] + [InlineData(4.123, typeof(double))] + [InlineData(4, typeof(int))] + public abstract void Convert_WithMismatchedTargetType(object? inputValue, Type targetType); + + [Theory] + [InlineData(4.123, typeof(string))] + [InlineData(true, typeof(string))] + public abstract void Convert_WithInvalidValueType(object? inputValue, Type targetType); + + [Theory] + [InlineData(1, typeof(string))] + public void Convert_ShouldCorrectlyReturnValueWithMatchingTargetType(object? inputValue, Type targetType) { - var options = new Options(); - options.SetShouldSuppressExceptionsInConverters(true); + ICommunityToolkitValueConverter converter = CreateConverter(); - var converter = InitializeConverterForInvalidConverterTests(); + Assert.Equal("Two", converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture)); + } + + [Theory] + [InlineData("4.123", typeof(Color))] + [InlineData("4", typeof(Color))] + public abstract void ConvertBack_WithMismatchedTargetType(object? inputValue, Type targetType); + + [Theory] + [InlineData(4.123, typeof(string))] + [InlineData(true, typeof(string))] + public abstract void ConvertBack_WithInvalidValueType(object? inputValue, Type targetType); + + [Theory] + [InlineData("One", typeof(int))] + public void ConvertBack_ShouldCorrectlyReturnValueWithMatchingTargetType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); - Assert.ThrowsAny(() => converter.ConvertBack(GetInvalidConvertBackValue(), converter.FromType, null, CultureInfo.CurrentCulture)); + Assert.Equal(0, converter.ConvertBack(inputValue, targetType, null, CultureInfo.CurrentCulture)); + } + + protected static BaseConverter CreateConverter() => new MockConverter(["One", "Two", "Three"]) + { + DefaultConvertReturnValue = "Three", + DefaultConvertBackReturnValue = 42 + }; + + protected BaseConverterTests(bool suppressExceptions) + { + new Options().SetShouldSuppressExceptionsInConverters(suppressExceptions); } } -public abstract class BaseConverterTest : ConverterTest where TConverter : ICommunityToolkitValueConverter, new() +/// +/// Unit tests that target and their public APIs with == false. +/// +public class BaseConverterTestsWithExceptionsEnabled : BaseConverterTests { - [Fact] - public void InvalidConvertBackValue_ShouldThrowException() + public BaseConverterTestsWithExceptionsEnabled() : base(false) { - var options = new Options(); - options.SetShouldSuppressExceptionsInConverters(false); - - var converter = InitializeConverterForInvalidConverterTests(); + } + + public override void Convert_WithMismatchedTargetType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); - Assert.ThrowsAny(() => converter.ConvertBack(GetInvalidConvertBackValue(), converter.FromType, null, CultureInfo.CurrentCulture)); + var exception = Assert.Throws(() => converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture)); + + exception.Message.Should().Be($"targetType needs to be assignable from {converter.ToType}. (Parameter 'targetType')"); } - [Fact] - public void InvalidConvertBackValue_ShouldSuppressExceptionsInConverters_ShouldReturnDefaultConvertValue() + public override void Convert_WithInvalidValueType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); + + var exception = Assert.Throws(() => converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture)); + + exception.Message.Should().Be($"Value needs to be of type {converter.FromType} (Parameter 'value')"); + } + + public override void ConvertBack_WithMismatchedTargetType(object? inputValue, Type targetType) { - var options = new Options(); - options.SetShouldSuppressExceptionsInConverters(true); + ICommunityToolkitValueConverter converter = CreateConverter(); - var converter = InitializeConverterForInvalidConverterTests(); + var exception = Assert.Throws(() => converter.ConvertBack(inputValue, targetType, null, CultureInfo.CurrentCulture)); + + exception.Message.Should().Be($"targetType needs to be assignable from {converter.FromType}. (Parameter 'targetType')"); + } - var result = converter.ConvertBack(GetInvalidConvertBackValue(), converter.FromType, null, CultureInfo.CurrentCulture); + public override void ConvertBack_WithInvalidValueType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); - Assert.Equal(converter.DefaultConvertBackReturnValue, result); + var exception = Assert.Throws(() => converter.ConvertBack(inputValue, targetType, null, CultureInfo.CurrentCulture)); + + exception.Message.Should().Be($"Value needs to be of type {converter.ToType} (Parameter 'value')"); } } -public abstract class ConverterTest : BaseHandlerTest where TConverter : ICommunityToolkitValueConverter, new() +/// +/// Unit tests that target and their public APIs with == true. +/// +public class BaseConverterTestsWithExceptionsSuppressed : BaseConverterTests { - [Fact] - public void InvalidConvertValue_ShouldThrowException() + public BaseConverterTestsWithExceptionsSuppressed() : base(true) { - var options = new Options(); - options.SetShouldSuppressExceptionsInConverters(false); - - var converter = InitializeConverterForInvalidConverterTests(); - - Assert.ThrowsAny(() => converter.Convert(GetInvalidConvertFromValue(), converter.ToType, null, CultureInfo.CurrentCulture)); } - [Fact] - public void InvalidConverterValue_ShouldSuppressExceptionsInConverters_ShouldReturnDefaultConvertValue() + public override void Convert_WithMismatchedTargetType(object? inputValue, Type targetType) { - var options = new Options(); - options.SetShouldSuppressExceptionsInConverters(true); - - var converter = InitializeConverterForInvalidConverterTests(); - - var result = converter.Convert(GetInvalidConvertFromValue(), converter.ToType, null, CultureInfo.CurrentCulture); + ICommunityToolkitValueConverter converter = CreateConverter(); - Assert.Equal(converter.DefaultConvertReturnValue, result); + converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture).Should().Be(converter.DefaultConvertReturnValue); } + + public override void Convert_WithInvalidValueType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); - protected virtual object? GetInvalidConvertBackValue() => GetInvalidValue(InitializeConverterForInvalidConverterTests().ToType); - protected virtual object? GetInvalidConvertFromValue() => GetInvalidValue(InitializeConverterForInvalidConverterTests().FromType); - protected virtual TConverter InitializeConverterForInvalidConverterTests() => new(); - - static object GetInvalidValue(Type type) + converter.Convert(inputValue, targetType, null, CultureInfo.CurrentCulture).Should().Be(converter.DefaultConvertReturnValue); + } + + public override void ConvertBack_WithMismatchedTargetType(object? inputValue, Type targetType) { - if (type != typeof(string)) - { - return string.Empty; - } + ICommunityToolkitValueConverter converter = CreateConverter(); - if (type != typeof(bool)) - { - return true; - } + converter.ConvertBack(inputValue, targetType, null, CultureInfo.CurrentCulture).Should().Be(converter.DefaultConvertBackReturnValue); + } + + public override void ConvertBack_WithInvalidValueType(object? inputValue, Type targetType) + { + ICommunityToolkitValueConverter converter = CreateConverter(); - throw new NotImplementedException($"Invalid value not valid for {typeof(TConverter).Name}. If {nameof(InvalidConvertValue_ShouldThrowException)} is failing, please override {nameof(GetInvalidConvertFromValue)} and provide an invalid value. Otherwise, override {nameof(GetInvalidConvertBackValue)} and provide an invalid value"); + converter.ConvertBack(inputValue, targetType, null, CultureInfo.CurrentCulture).Should().Be(converter.DefaultConvertBackReturnValue); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockBehavior.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockBehavior.cs new file mode 100644 index 0000000000..e1d58e8823 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockBehavior.cs @@ -0,0 +1,47 @@ +using System.ComponentModel; +using CommunityToolkit.Maui.Behaviors; +using FluentAssertions; + +namespace CommunityToolkit.Maui.UnitTests.Mocks; + +public class NumericValidationBehavior : Behavior +{ + void OnViewPropertyChanged(Entry sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Entry.Text)) + { + bool isValid = double.TryParse(sender.Text, out double _); + sender.TextColor = isValid ? Colors.Black : Colors.Red; + } + } +} + +public class MockBehavior : BaseBehavior