From 684caa797feab386f9c641c6a63569a2cb7ba639 Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Wed, 3 Sep 2025 14:46:52 +0200 Subject: [PATCH 01/10] ValidateOnValueChanged fails randomly sometimes --- .../Behaviors/ValidationBehaviorTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Behaviors/ValidationBehaviorTests.cs b/src/CommunityToolkit.Maui.UnitTests/Behaviors/ValidationBehaviorTests.cs index 834e3dbb1a..0882e97780 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Behaviors/ValidationBehaviorTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Behaviors/ValidationBehaviorTests.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Maui.Behaviors; +using System.Threading.Tasks; +using CommunityToolkit.Maui.Behaviors; using CommunityToolkit.Maui.UnitTests.Mocks; using Xunit; using Xunit.v3; @@ -8,7 +9,7 @@ namespace CommunityToolkit.Maui.UnitTests.Behaviors; public class ValidationBehaviorTests(ITestOutputHelper testOutputHelper) : BaseBehaviorTest(new MockValidationBehavior(), new View()) { [Fact] - public void ValidateOnValueChanged() + public async Task ValidateOnValueChanged() { // Arrange var entry = new Entry @@ -18,6 +19,7 @@ public void ValidateOnValueChanged() var behavior = new MockValidationBehavior() { ExpectedValue = "321", + SimulateValidationDelay = false, Flags = ValidationFlags.ValidateOnValueChanged }; @@ -26,6 +28,9 @@ public void ValidateOnValueChanged() // Act entry.Text = "321"; + // Fails sometimes randomly without delay + await Task.Delay(10, TestContext.Current.CancellationToken); + // Assert Assert.True(behavior.IsValid); } From 7916d903cec8e883ebb0218806bc5bc7185b498e Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Wed, 3 Sep 2025 14:53:23 +0200 Subject: [PATCH 02/10] Use cultured tests for failing converter tests --- .../ColorToCmykStringConverterTests.cs | 6 ++--- .../ColorToCmykaStringConverterTests.cs | 23 ++++--------------- .../ColorToHslStringConverterTests.cs | 6 ++--- .../ColorToHslaStringConverterTests.cs | 20 +++------------- 4 files changed, 14 insertions(+), 41 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToCmykStringConverterTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToCmykStringConverterTests.cs index 0e42a110db..7d0e58295d 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToCmykStringConverterTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToCmykStringConverterTests.cs @@ -135,15 +135,15 @@ public class ColorToCmykStringConverterTests : BaseOneWayConverterTest Date: Wed, 3 Sep 2025 16:00:17 +0200 Subject: [PATCH 03/10] Invariant color conversion extensions Wrap template string in FormattableString.Invariant Mark culture dependend versions from #552 obsolete --- .../ColorConversionExtensions.shared.cs | 56 ++++++++++++++++--- .../ColorToStringConverter.shared.cs | 6 +- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs index 7005dab0bf..9a05aebd4a 100644 --- a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs @@ -19,7 +19,7 @@ public static class ColorConversionExtensions public static string ToRgbString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return $"RGB({color.GetByteRed()},{color.GetByteGreen()},{color.GetByteBlue()})"; + return FormattableString.Invariant($"RGB({color.GetByteRed()},{color.GetByteGreen()},{color.GetByteBlue()})"); } /// @@ -32,10 +32,23 @@ public static string ToRgbString(this Color color) /// and alpha is a value between 0 and 1. (e.g. RGBA(255,0,0,1) for ). /// /// Thrown when is null. - public static string ToRgbaString(this Color color, CultureInfo? cultureInfo = null) + [Obsolete("Dont use CultureInfo this method should be culture invariant")] + public static string ToRgbaString(this Color color, CultureInfo? cultureInfo) => ToRgbaString(color); + + + /// + /// Converts this to a containing the red, green, blue and alpha components. + /// + /// The to convert. + /// + /// A in the format: RGBA(red,green,blue,alpha) where red, green and blue will be a value between 0 and 255, + /// and alpha is a value between 0 and 1. (e.g. RGBA(255,0,0,1) for ). + /// + /// Thrown when is null. + public static string ToRgbaString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return $"RGBA({color.GetByteRed()},{color.GetByteGreen()},{color.GetByteBlue()},{color.Alpha.ToString(cultureInfo)})"; + return FormattableString.Invariant($"RGBA({color.GetByteRed()},{color.GetByteGreen()},{color.GetByteBlue()},{color.Alpha})"); } /// @@ -50,7 +63,7 @@ public static string ToRgbaString(this Color color, CultureInfo? cultureInfo = n public static string ToCmykString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return $"CMYK({color.GetPercentCyan():P0},{color.GetPercentMagenta():P0},{color.GetPercentYellow():P0},{color.GetPercentBlackKey():P0})"; + return FormattableString.Invariant($"CMYK({color.GetPercentCyan():P0},{color.GetPercentMagenta():P0},{color.GetPercentYellow():P0},{color.GetPercentBlackKey():P0})"); } /// @@ -63,10 +76,23 @@ public static string ToCmykString(this Color color) /// 0% and 100% and alpha will be a value between 0 and 1. (e.g. CMYKA(100%,100%,0%,100%,1) for ). /// /// Thrown when is null. - public static string ToCmykaString(this Color color, CultureInfo? cultureInfo = null) + + [Obsolete("Dont use CultureInfo this method should be culture invariant")] + public static string ToCmykaString(this Color color, CultureInfo? cultureInfo) => ToCmykaString(color); + + /// + /// Converts this to a containing the cyan, magenta, yellow, key and alpha components. + /// + /// The to convert. + /// + /// A in the format: CMYKA(cyan,magenta,yellow,key,alpha) where cyan, magenta, yellow and key will be a value between + /// 0% and 100% and alpha will be a value between 0 and 1. (e.g. CMYKA(100%,100%,0%,100%,1) for ). + /// + /// Thrown when is null. + public static string ToCmykaString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return $"CMYKA({color.GetPercentCyan():P0},{color.GetPercentMagenta():P0},{color.GetPercentYellow():P0},{color.GetPercentBlackKey():P0},{color.Alpha.ToString(cultureInfo)})"; + return FormattableString.Invariant($"CMYKA({color.GetPercentCyan():P0},{color.GetPercentMagenta():P0},{color.GetPercentYellow():P0},{color.GetPercentBlackKey():P0},{color.Alpha})"); } /// @@ -81,7 +107,7 @@ public static string ToCmykaString(this Color color, CultureInfo? cultureInfo = public static string ToHslString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return $"HSL({color.GetDegreeHue():0},{color.GetSaturation():P0},{color.GetLuminosity():P0})"; + return FormattableString.Invariant($"HSL({color.GetDegreeHue():0},{color.GetSaturation():P0},{color.GetLuminosity():P0})"); } /// @@ -94,10 +120,22 @@ public static string ToHslString(this Color color) /// will be a value between 0% and 100%, and alpha will be a value between 0 and 1. (e.g. HSLA(0,100%,50%,1) for ). /// /// Thrown when is null. - public static string ToHslaString(this Color color, CultureInfo? cultureInfo = null) + [Obsolete("Dont use CultureInfo this method should be culture invariant")] + public static string ToHslaString(this Color color, CultureInfo? cultureInfo) => ToHslaString(color); + + /// + /// Converts this to a containing the hue, saturation, lightness and alpha components. + /// + /// The to convert. + /// + /// A in the format: HSLA(hue,saturation,lightness,alpha) where hue will be a value between 0 and 360, saturation and lightness + /// will be a value between 0% and 100%, and alpha will be a value between 0 and 1. (e.g. HSLA(0,100%,50%,1) for ). + /// + /// Thrown when is null. + public static string ToHslaString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return $"HSLA({color.GetDegreeHue():0},{color.GetSaturation():P0},{color.GetLuminosity():P0},{color.Alpha.ToString(cultureInfo)})"; + return FormattableString.Invariant($"HSLA({color.GetDegreeHue():0},{color.GetSaturation():P0},{color.GetLuminosity():P0},{color.Alpha})"); } /// diff --git a/src/CommunityToolkit.Maui/Converters/ColorToStringConverter.shared.cs b/src/CommunityToolkit.Maui/Converters/ColorToStringConverter.shared.cs index f09148cebe..92ff2ceb03 100644 --- a/src/CommunityToolkit.Maui/Converters/ColorToStringConverter.shared.cs +++ b/src/CommunityToolkit.Maui/Converters/ColorToStringConverter.shared.cs @@ -57,7 +57,7 @@ public override Color ConvertBackTo(string value, CultureInfo? culture) public override string ConvertFrom(Color value, CultureInfo? culture = null) { ArgumentNullException.ThrowIfNull(value); - return value.ToRgbaString(culture); + return value.ToRgbaString(); } } @@ -172,7 +172,7 @@ public partial class ColorToCmykaStringConverter : BaseConverterOneWay Date: Wed, 3 Sep 2025 16:38:43 +0200 Subject: [PATCH 04/10] Use `MidpointRounding.ToEven` and `"%0"` for color converter extensions --- .../ColorConversionExtensions.shared.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs index 9a05aebd4a..61b44e5dea 100644 --- a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs @@ -7,6 +7,18 @@ namespace CommunityToolkit.Maui.Core.Extensions; /// public static class ColorConversionExtensions { + /// + /// Converts the value to percentage. + /// Uses and "0%" format string to emulate the behavior of "en-US" locale. + /// If only "0%" is used to get rid of the space between the value and the percent sign, 0.625f would become 63% instead of 62%. + /// + /// percentage + /// + static string ToPercentage(this float percentage) + { + return Math.Round(percentage, 2, MidpointRounding.ToEven).ToString("0%"); + } + /// /// Converts this to a containing the red, green and blue components. /// @@ -63,7 +75,7 @@ public static string ToRgbaString(this Color color) public static string ToCmykString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"CMYK({color.GetPercentCyan():P0},{color.GetPercentMagenta():P0},{color.GetPercentYellow():P0},{color.GetPercentBlackKey():P0})"); + return FormattableString.Invariant($"CMYK({color.GetPercentCyan().ToPercentage()},{color.GetPercentMagenta().ToPercentage()},{color.GetPercentYellow().ToPercentage()},{color.GetPercentBlackKey().ToPercentage()})"); } /// @@ -92,7 +104,7 @@ public static string ToCmykString(this Color color) public static string ToCmykaString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"CMYKA({color.GetPercentCyan():P0},{color.GetPercentMagenta():P0},{color.GetPercentYellow():P0},{color.GetPercentBlackKey():P0},{color.Alpha})"); + return FormattableString.Invariant($"CMYKA({color.GetPercentCyan().ToPercentage()},{color.GetPercentMagenta().ToPercentage()},{color.GetPercentYellow().ToPercentage()},{color.GetPercentBlackKey().ToPercentage()},{color.Alpha})"); } /// @@ -107,7 +119,7 @@ public static string ToCmykaString(this Color color) public static string ToHslString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"HSL({color.GetDegreeHue():0},{color.GetSaturation():P0},{color.GetLuminosity():P0})"); + return FormattableString.Invariant($"HSL({color.GetDegreeHue():0},{color.GetSaturation().ToPercentage()},{color.GetLuminosity().ToPercentage()})"); } /// @@ -135,7 +147,7 @@ public static string ToHslString(this Color color) public static string ToHslaString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"HSLA({color.GetDegreeHue():0},{color.GetSaturation():P0},{color.GetLuminosity():P0},{color.Alpha})"); + return FormattableString.Invariant($"HSLA({color.GetDegreeHue():0},{color.GetSaturation().ToPercentage()},{color.GetLuminosity().ToPercentage()},{color.Alpha})"); } /// From dcf82248a9e24b5367a86e69322e5fe040fec4ea Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Wed, 3 Sep 2025 17:03:51 +0200 Subject: [PATCH 05/10] Run ColorConversionExtensionTests using multiple cultures --- .../ColorConversionExtensionsTests.cs | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs index d87ebba9ea..834ec357ea 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs @@ -288,7 +288,7 @@ public class ColorConversionExtensionsTests : BaseTest new ColorTestDefinition(1, 1, 1, (float)0.84705883, 255, 255, 255, 216, 0, 0, 0, 0, 0, 0, 0, 1, false, false, Colors.White, Colors.Black) ]; - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToRgbString(ColorTestDefinition testDef) { @@ -305,7 +305,7 @@ public void ToRgbStringNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToRgbaString(ColorTestDefinition testDef) { @@ -322,7 +322,7 @@ public void ToRgbaStringNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToCmykString(ColorTestDefinition testDef) { @@ -339,7 +339,7 @@ public void ToCmykStringNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToCmykaString(ColorTestDefinition testDef) { @@ -356,7 +356,7 @@ public void ToCmykaStringNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToHslString(ColorTestDefinition testDef) { @@ -373,7 +373,7 @@ public void ToHslStringNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToHslaString(ColorTestDefinition testDef) { @@ -390,7 +390,7 @@ public void ToHslaStringNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetByteRed(ColorTestDefinition testDef) { @@ -407,7 +407,7 @@ public void GetByteRedNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetByteGreen(ColorTestDefinition testDef) { @@ -424,7 +424,7 @@ public void GetByteGreenNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetByteBlue(ColorTestDefinition testDef) { @@ -441,7 +441,7 @@ public void GetByteBlueNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetByteAlpha(ColorTestDefinition testDef) { @@ -458,7 +458,7 @@ public void GetByteAlphaNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetPctBlackKey(ColorTestDefinition def) { @@ -475,7 +475,7 @@ public void GetPercentBlackKeyNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetDegreeHue(ColorTestDefinition def) { @@ -492,7 +492,7 @@ public void GetDegreeHueNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetPctCyan(ColorTestDefinition testDef) { @@ -509,7 +509,7 @@ public void GetPercentCyanNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetPctMagenta(ColorTestDefinition testDef) { @@ -526,7 +526,7 @@ public void GetPercentMagentaNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void GetPctYellow(ColorTestDefinition testDef) { @@ -543,7 +543,7 @@ public void GetPercentYellowNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToInverseColor(ColorTestDefinition testDef) { @@ -564,7 +564,7 @@ public void ToInverseColorNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToGrayScale(ColorTestDefinition testDef) { @@ -585,7 +585,7 @@ public void ToGrayScaleNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void IsDark(ColorTestDefinition testDef) { @@ -602,7 +602,7 @@ public void IsDarkNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void IsDarkForTheEye(ColorTestDefinition testDef) { @@ -643,7 +643,7 @@ public void GetPctYellowShouldNotCrashOnBlackColor() Assert.True(true); } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToBlackOrWhite(ColorTestDefinition testDef) { @@ -660,7 +660,7 @@ public void ToBlackOrWhiteNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void ToBlackOrWhiteForText(ColorTestDefinition testDef) { @@ -677,7 +677,7 @@ public void ToBlackOrWhiteForTextNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithRed_Double(ColorTestDefinition testDef) { @@ -713,7 +713,7 @@ public void WithRedNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithGreen_Double(ColorTestDefinition testDef) { @@ -749,7 +749,7 @@ public void WithGreenNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithBlue_Double(ColorTestDefinition testDef) { @@ -787,7 +787,7 @@ public void WithBlueNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithRed_Byte(ColorTestDefinition testDef) { @@ -797,7 +797,7 @@ public void WithRed_Byte(ColorTestDefinition testDef) Assert.Equal(red, result.GetByteRed()); } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithGreen_Byte(ColorTestDefinition testDef) { @@ -807,7 +807,7 @@ public void WithGreen_Byte(ColorTestDefinition testDef) Assert.Equal(green, result.GetByteGreen()); } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithBlue_Byte(ColorTestDefinition testDef) { @@ -817,7 +817,7 @@ public void WithBlue_Byte(ColorTestDefinition testDef) Assert.Equal(blue, result.GetByteBlue()); } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithCyan(ColorTestDefinition testDef) { @@ -840,7 +840,7 @@ public void WithCyanNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithMagenta(ColorTestDefinition testDef) { @@ -863,7 +863,7 @@ public void WithMagentaNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithYellow(ColorTestDefinition testDef) { @@ -886,7 +886,7 @@ public void WithYellowNullInput() #pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Theory] + [CulturedTheory(cultures: ["en-US", "uk-UA", "de-DE"])] [MemberData(nameof(ColorTestData))] public void WithBlackKey(ColorTestDefinition testDef) { From 2e655fc30425230a6931b9a10080a72dd9639b67 Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Wed, 3 Sep 2025 17:15:26 +0200 Subject: [PATCH 06/10] Build ColorTestDefinition using en-US locale --- .../Extensions/ColorConversionExtensionsTests.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs b/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs index 834ec357ea..298f3ced73 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Extensions/ColorConversionExtensionsTests.cs @@ -932,6 +932,8 @@ internal ColorTestDefinition(float r, float g, float b, float a, float expectedAvgColor, bool expectedIsDark, bool expectedIsDarkForEye, Color expectedToBlackOrWhite, Color expectedToBlackOrWhiteForText) { + var culture = new System.Globalization.CultureInfo("en-US"); + R = r; G = g; B = b; @@ -956,17 +958,17 @@ internal ColorTestDefinition(float r, float g, float b, float a, ExpectedGreyScale = new Color(expectedAvgColor, expectedAvgColor, expectedAvgColor); ExpectedInverse = new Color(expectedInverseR, expectedInverseG, expectedInverseB); ExpectedRgb = $"RGB({expectedByteR},{expectedByteG},{expectedByteB})"; - ExpectedRgba = $"RGBA({expectedByteR},{expectedByteG},{expectedByteB},{A})"; + ExpectedRgba = $"RGBA({expectedByteR},{expectedByteG},{expectedByteB},{A.ToString(culture)})"; ExpectedHexrgb = $"#{expectedByteR:X2}{expectedByteG:X2}{expectedByteB:X2}"; ExpectedHexrgba = $"#{expectedByteR:X2}{expectedByteG:X2}{expectedByteB:X2}{expectedByteA:X2}"; ExpectedHexargb = $"#{expectedByteA:X2}{expectedByteR:X2}{expectedByteG:X2}{expectedByteB:X2}"; - ExpectedCmyk = $"CMYK({expectedPctCyan:P0},{expectedPctMagenta:P0},{expectedPctYellow:P0},{expectedPctBlack:P0})"; - ExpectedCmyka = $"CMYKA({expectedPctCyan:P0},{expectedPctMagenta:P0},{expectedPctYellow:P0},{expectedPctBlack:P0},{a})"; + ExpectedCmyk = $"CMYK({expectedPctCyan.ToString("P0", culture)},{expectedPctMagenta.ToString("P0", culture)},{expectedPctYellow.ToString("P0", culture)},{expectedPctBlack.ToString("P0", culture)})"; + ExpectedCmyka = $"CMYKA({expectedPctCyan.ToString("P0", culture)},{expectedPctMagenta.ToString("P0", culture)},{expectedPctYellow.ToString("P0", culture)},{expectedPctBlack.ToString("P0", culture)},{a.ToString(culture)})"; Color = new Color(r, g, b, a); ExpectedDegreeHue = Color.GetHue() * 360; - ExpectedHslString = $"HSL({ExpectedDegreeHue:0},{Color.GetSaturation():P0},{Color.GetLuminosity():P0})"; - ExpectedHslaString = $"HSLA({ExpectedDegreeHue:0},{Color.GetSaturation():P0},{Color.GetLuminosity():P0},{a})"; + ExpectedHslString = $"HSL({ExpectedDegreeHue:0},{Color.GetSaturation().ToString("P0", culture)},{Color.GetLuminosity().ToString("P0", culture)})"; + ExpectedHslaString = $"HSLA({ExpectedDegreeHue:0},{Color.GetSaturation().ToString("P0", culture)},{Color.GetLuminosity().ToString("P0", culture)},{a.ToString(culture)})"; } internal float R { get; } From ea6ce57e19671bfda08ee5ce0ebe14b70281388f Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Wed, 3 Sep 2025 17:17:21 +0200 Subject: [PATCH 07/10] Remove wrong test case --- .../Converters/ColorToRgbaStringConverterTests.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToRgbaStringConverterTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToRgbaStringConverterTests.cs index 4cfabd94ce..5ced164ec0 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToRgbaStringConverterTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToRgbaStringConverterTests.cs @@ -163,20 +163,6 @@ public void ColorToRgbStringConverterConvertBackValidInputTest(float red, float AssertColorComparison(expectedResult, resultConvertBackTo); } - [Fact] - public void ColorToRgbStringConverterCultureTest() - { - var expectedResult = "RGBA(0,0,0,0,5)"; - var converter = new ColorToRgbaStringConverter(); - var color = new Color(0, 0, 0, 0.5f); - - var resultConvert = ((ICommunityToolkitValueConverter)converter).Convert(color, typeof(string), null, new System.Globalization.CultureInfo("uk-UA")); - var resultConvertFrom = converter.ConvertFrom(color, new System.Globalization.CultureInfo("uk-UA")); - - Assert.Equal(expectedResult, resultConvert); - Assert.Equal(expectedResult, resultConvertFrom); - } - [Fact] public void ColorToRgbStringConverterNullInputTest() { From 7cabea1cb28fa5e2b9b19e5833e44121a60aee5d Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Sat, 6 Sep 2025 14:39:02 +0200 Subject: [PATCH 08/10] Invariant percentage in color conversion --- .../Extensions/ColorConversionExtensions.shared.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs index 61b44e5dea..b653567595 100644 --- a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs @@ -16,7 +16,8 @@ public static class ColorConversionExtensions /// static string ToPercentage(this float percentage) { - return Math.Round(percentage, 2, MidpointRounding.ToEven).ToString("0%"); + var toEvenRounded = Math.Round(percentage, 2, MidpointRounding.ToEven); + return FormattableString.Invariant($"{toEvenRounded:0%}"); } /// From c9975b75dda6b31faee46b7a6d1a7019a336f9cd Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Sat, 6 Sep 2025 23:14:42 +0200 Subject: [PATCH 09/10] Typo and add the attribute to hide this method from intellisense Co-authored-by: Pedro Jesus --- .../Extensions/ColorConversionExtensions.shared.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs index b653567595..cf7c4d1d0f 100644 --- a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.ComponentModel; +using System.Globalization; namespace CommunityToolkit.Maui.Core.Extensions; @@ -45,7 +46,8 @@ public static string ToRgbString(this Color color) /// and alpha is a value between 0 and 1. (e.g. RGBA(255,0,0,1) for ). /// /// Thrown when is null. - [Obsolete("Dont use CultureInfo this method should be culture invariant")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Do not use CultureInfo, this method should be culture invariant.")] public static string ToRgbaString(this Color color, CultureInfo? cultureInfo) => ToRgbaString(color); @@ -90,7 +92,8 @@ public static string ToCmykString(this Color color) /// /// Thrown when is null. - [Obsolete("Dont use CultureInfo this method should be culture invariant")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Do not use CultureInfo, this method should be culture invariant.")] public static string ToCmykaString(this Color color, CultureInfo? cultureInfo) => ToCmykaString(color); /// @@ -133,7 +136,8 @@ public static string ToHslString(this Color color) /// will be a value between 0% and 100%, and alpha will be a value between 0 and 1. (e.g. HSLA(0,100%,50%,1) for ). /// /// Thrown when is null. - [Obsolete("Dont use CultureInfo this method should be culture invariant")] + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Do not use CultureInfo, this method should be culture invariant.")] public static string ToHslaString(this Color color, CultureInfo? cultureInfo) => ToHslaString(color); /// From fce54510b2d5bfb4b892946297a2aa662f13016f Mon Sep 17 00:00:00 2001 From: Matz Reckeweg Date: Wed, 8 Oct 2025 11:03:17 +0200 Subject: [PATCH 10/10] Change color conversion to `AwayFromZero` rounding --- .../ColorConversionExtensions.shared.cs | 21 ++++--------------- .../ColorToHslStringConverterTests.cs | 12 +++++------ .../ColorToHslaStringConverterTests.cs | 12 +++++------ 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs index cf7c4d1d0f..80bbf975ef 100644 --- a/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Extensions/ColorConversionExtensions.shared.cs @@ -8,19 +8,6 @@ namespace CommunityToolkit.Maui.Core.Extensions; /// public static class ColorConversionExtensions { - /// - /// Converts the value to percentage. - /// Uses and "0%" format string to emulate the behavior of "en-US" locale. - /// If only "0%" is used to get rid of the space between the value and the percent sign, 0.625f would become 63% instead of 62%. - /// - /// percentage - /// - static string ToPercentage(this float percentage) - { - var toEvenRounded = Math.Round(percentage, 2, MidpointRounding.ToEven); - return FormattableString.Invariant($"{toEvenRounded:0%}"); - } - /// /// Converts this to a containing the red, green and blue components. /// @@ -78,7 +65,7 @@ public static string ToRgbaString(this Color color) public static string ToCmykString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"CMYK({color.GetPercentCyan().ToPercentage()},{color.GetPercentMagenta().ToPercentage()},{color.GetPercentYellow().ToPercentage()},{color.GetPercentBlackKey().ToPercentage()})"); + return FormattableString.Invariant($"CMYK({color.GetPercentCyan():0%},{color.GetPercentMagenta():0%},{color.GetPercentYellow():0%},{color.GetPercentBlackKey():0%})"); } /// @@ -108,7 +95,7 @@ public static string ToCmykString(this Color color) public static string ToCmykaString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"CMYKA({color.GetPercentCyan().ToPercentage()},{color.GetPercentMagenta().ToPercentage()},{color.GetPercentYellow().ToPercentage()},{color.GetPercentBlackKey().ToPercentage()},{color.Alpha})"); + return FormattableString.Invariant($"CMYKA({color.GetPercentCyan():0%},{color.GetPercentMagenta():0%},{color.GetPercentYellow():0%},{color.GetPercentBlackKey():0%},{color.Alpha})"); } /// @@ -123,7 +110,7 @@ public static string ToCmykaString(this Color color) public static string ToHslString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"HSL({color.GetDegreeHue():0},{color.GetSaturation().ToPercentage()},{color.GetLuminosity().ToPercentage()})"); + return FormattableString.Invariant($"HSL({color.GetDegreeHue():0},{color.GetSaturation():0%},{color.GetLuminosity():0%})"); } /// @@ -152,7 +139,7 @@ public static string ToHslString(this Color color) public static string ToHslaString(this Color color) { ArgumentNullException.ThrowIfNull(color); - return FormattableString.Invariant($"HSLA({color.GetDegreeHue():0},{color.GetSaturation().ToPercentage()},{color.GetLuminosity().ToPercentage()},{color.Alpha})"); + return FormattableString.Invariant($"HSLA({color.GetDegreeHue():0},{color.GetSaturation():0%},{color.GetLuminosity():0%},{color.Alpha})"); } /// diff --git a/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToHslStringConverterTests.cs b/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToHslStringConverterTests.cs index d3405f23a0..569b39a4c6 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToHslStringConverterTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Converters/ColorToHslStringConverterTests.cs @@ -108,22 +108,22 @@ public class ColorToHslStringConverterTests : BaseOneWayConverterTest