Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
namespace CommunityToolkit.Maui.Markup.UnitTests;
using CommunityToolkit.Maui.Markup.UnitTests.Mocks;
namespace CommunityToolkit.Maui.Markup.UnitTests;

public static class ApplicationTestHelpers
{
Expand All @@ -11,19 +12,28 @@ public static class ApplicationTestHelpers
public static void PerformAppThemeBasedTest<TBindable>(
AppTheme appTheme,
Func<TBindable> setAppThemeValue,
Action<TBindable> assertResult) where TBindable : BindableObject
Action<TBindable> assertResult) where TBindable : View
{
try
{
_ = new Application();
var appBuilder = MauiApp.CreateBuilder()
.UseMauiCommunityToolkit()
.UseMauiApp<MockApplication>();

var bindable = setAppThemeValue();
var mauiApp = appBuilder.Build();

var current = Application.Current;
var application = mauiApp.Services.GetRequiredService<IApplication>();

var bindable = setAppThemeValue();

ArgumentNullException.ThrowIfNull(current);
ArgumentNullException.ThrowIfNull(Application.Current);

Application.Current.MainPage = new ContentPage
{
Content = bindable
};

current.UserAppTheme = appTheme;
Application.Current.UserAppTheme = appTheme;

assertResult.Invoke(bindable);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Handlers;

namespace CommunityToolkit.Maui.Markup.UnitTests.Mocks;

class MockApplication : Application, IPlatformApplication
{
public MockApplication(IServiceProvider serviceProvider)
{
Services = serviceProvider;
#pragma warning disable CS0612 // Type or member is obsolete
DependencyService.Register<ISystemResourcesProvider, MockResourcesProvider>();
#pragma warning restore CS0612 // Type or member is obsolete
}

public IApplication Application => this;
public IServiceProvider Services { get; }
}

// Inspired by https://github.com/dotnet/maui/blob/main/src/Controls/tests/Core.UnitTests/TestClasses/ApplicationHandlerStub.cs
class ApplicationHandlerStub() : ElementHandler<IApplication, object>(Mapper)
{
public static IPropertyMapper<IApplication, ApplicationHandlerStub> Mapper = new PropertyMapper<IApplication, ApplicationHandlerStub>(ElementMapper);

protected override object CreatePlatformElement()
{
return new object();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.Maui.Controls.Internals;

namespace CommunityToolkit.Maui.Markup.UnitTests.Mocks;


// Inspired by https://github.com/dotnet/maui/blob/79695fbb7ba6517a334c795ecf0a1d6358ef309a/src/Controls/Foldable/test/MockPlatformServices.cs#L145-L176

#pragma warning disable CS0612 // Type or member is obsolete
class MockResourcesProvider : ISystemResourcesProvider
#pragma warning restore CS0612 // Type or member is obsolete
{
readonly ResourceDictionary dictionary = new();

public IResourceDictionary GetSystemResources() => dictionary;
}
71 changes: 65 additions & 6 deletions src/CommunityToolkit.Maui.Markup.UnitTests/StyleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,36 @@ namespace CommunityToolkit.Maui.Markup.UnitTests;
class StyleTests : BaseMarkupTestFixture
{
[Test]
public void ImplicitCast()
public void ImplicitCastToStyleT()
{
var formsStyle = new Style(typeof(Label));
var style = (Style<Label>)formsStyle;

Assert.That(ReferenceEquals(style.MauiStyle, formsStyle));
}

[Test]
public void ImplicitCastToStyleTUsingBaseClass()
{
var formsStyle = new Style(typeof(Label))
{
Behaviors =
{
new LabelBehavior()
}
};

var style = (Style<View>)formsStyle;

Assert.Multiple(() =>
{
Assert.That(formsStyle.Behaviors[0], Is.InstanceOf<LabelBehavior>());
Assert.That(style.MauiStyle.Behaviors[0], Is.InstanceOf<LabelBehavior>());
});
}

[Test]
public void ImplicitCastFromStyleT()
{
Style<Label> style = new();
Style formsStyle = style;
Expand Down Expand Up @@ -248,12 +277,15 @@ public void Fluent()
[TestCase(AppTheme.Unspecified)]
public void AddAppThemeBindingCorrectlySetsPropertyToChangeBasedOnApplicationsAppTheme(AppTheme appTheme)
{
var expectedColor = appTheme == AppTheme.Dark ? Colors.Orange : Colors.Purple;
var darkThemeColor = Colors.Orange;
var otherThemeColor = Colors.Purple;

var expectedColor = appTheme == AppTheme.Dark ? darkThemeColor : otherThemeColor;

ApplicationTestHelpers.PerformAppThemeBasedTest(
appTheme,
() => new Label()
.Style(new Style<Label>().AddAppThemeBinding(Label.TextColorProperty, Colors.Purple, Colors.Orange))
.Style(new Style<Label>().AddAppThemeBinding(Label.TextColorProperty, otherThemeColor, darkThemeColor))
.AppThemeBinding(Label.TextProperty, nameof(AppTheme.Light), nameof(AppTheme.Dark)),
(label) => Assert.That(label.TextColor, Is.EqualTo(expectedColor)));
}
Expand All @@ -263,13 +295,16 @@ public void AddAppThemeBindingCorrectlySetsPropertyToChangeBasedOnApplicationsAp
[TestCase(AppTheme.Unspecified)]
public void AddAppThemeBindingsCorrectlySetsPropertiesToChangeBasedOnApplicationsAppTheme(AppTheme appTheme)
{
var expectedColor = appTheme == AppTheme.Dark ? Colors.Orange : Colors.Purple;
var darkThemeColor = Colors.Orange;
var otherThemeColor = Colors.Purple;

var expectedColor = appTheme == AppTheme.Dark ? darkThemeColor : otherThemeColor;
var expectedText = appTheme == AppTheme.Dark ? nameof(AppTheme.Dark) : nameof(AppTheme.Light);

ApplicationTestHelpers.PerformAppThemeBasedTest(
appTheme,
() => new Label()
.Style(new Style<Label>().AddAppThemeBindings((Label.TextColorProperty, Colors.Purple, Colors.Orange),
.Style(new Style<Label>().AddAppThemeBindings((Label.TextColorProperty, otherThemeColor, darkThemeColor),
(Label.TextProperty, nameof(AppTheme.Light), nameof(AppTheme.Dark))))
.AppThemeBinding(Label.TextProperty, nameof(AppTheme.Light), nameof(AppTheme.Dark)),
(label) =>
Expand All @@ -282,5 +317,29 @@ public void AddAppThemeBindingsCorrectlySetsPropertiesToChangeBasedOnApplication
});
}

class LabelBehavior : Behavior<Label> { }
[Test]
public void InvalidMauiStyleInitializationShouldThrowException()
{
var buttonStyle = new Style(typeof(Button));
Assert.Throws<ArgumentException>(() => new Style<Label>(buttonStyle));
}

[Test]
public void InvalidMauiStyleCastShouldThrowException()
{
var buttonStyle = new Style(typeof(Button));
Assert.Throws<ArgumentException>(() =>
{
var style = (Style<Label>)buttonStyle;
});
}

[Test]
public void ValidMauiStyleInitializationDoesNotThrowException()
{
var buttonStyle = new Style(typeof(Button));
Assert.DoesNotThrow(() => new Style<Button>(buttonStyle));
}

class LabelBehavior : Behavior<Label>;
}
35 changes: 30 additions & 5 deletions src/CommunityToolkit.Maui.Markup/Style.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,22 @@
public class Style<T> where T : BindableObject
{
/// <summary>
/// FormsStyle
/// Converts <see cref="CommunityToolkit.Maui.Markup.Style{T}"/> to <see cref="Microsoft.Maui.Controls.Style"/>
/// </summary>
/// <param name="style"></param>
public static implicit operator Style(Style<T> style) => style.MauiStyle;

/// <summary>
/// Converts <see cref="Microsoft.Maui.Controls.Style"/> to <see cref="CommunityToolkit.Maui.Markup.Style{T}"/>
/// </summary>
/// <param name="style"></param>
/// <returns></returns>
public static implicit operator Style<T>(Style style) => new(style);

/// <summary>
/// Initialize Style
/// </summary>
/// <param name="property">"The <see cref="BindableProperty"/> to style </param>
/// <param name="property">"The <see cref="BindableProperty"/> to mauiStyle </param>
/// <param name="value">"The value for the <see cref="BindableProperty"/> </param>
public Style(BindableProperty property, object value) : this((property, value))
{
Expand All @@ -32,6 +39,20 @@ public Style(params (BindableProperty Property, object Value)[] setters)
Add(setters);
}

/// <summary>
/// Initialize Style
/// </summary>
/// <param name="mauiStyle"></param>
public Style(Style mauiStyle)
{
if (!mauiStyle.TargetType.IsAssignableTo(typeof(T)))
{
throw new ArgumentException($"Invalid type. The Type used in {nameof(mauiStyle)}.{nameof(mauiStyle.TargetType)} ({mauiStyle.TargetType.FullName}) must be assignable to the Type used in {nameof(Style<T>)} ({typeof(T).FullName})", nameof(mauiStyle));
}

MauiStyle = mauiStyle;
}

/// <summary>
/// Style(typeof(T))
/// </summary>
Expand Down Expand Up @@ -62,7 +83,7 @@ public Style<T> BasedOn(Style value)
/// <summary>
/// Add Setters
/// </summary>
/// <param name="property">"The <see cref="BindableProperty"/> to style </param>
/// <param name="property">"The <see cref="BindableProperty"/> to mauiStyle </param>
/// <param name="value">"The value for the <see cref="BindableProperty"/> </param>
/// <returns>Style with added setters</returns>
public Style<T> Add(BindableProperty property, object value)
Expand All @@ -89,13 +110,17 @@ public Style<T> Add(params (BindableProperty Property, object Value)[] setters)
/// <summary>
/// Adds the supplied <paramref name="light"/> and <paramref name="dark"/> values in an <see cref="AppThemeBinding"/>.
/// </summary>
/// <param name="property">"The <see cref="BindableProperty"/> to style </param>
/// <param name="property">"The <see cref="BindableProperty"/> to mauiStyle </param>
/// <param name="light">"The light value for the <see cref="BindableProperty"/> </param>
/// <param name="dark">"The dark value for the <see cref="BindableProperty"/> </param>
/// <returns>Style with added setters</returns>
public Style<T> AddAppThemeBinding(BindableProperty property, object light, object dark)
{
MauiStyle.Setters.Add(property, new AppThemeBinding { Light = light, Dark = dark });
MauiStyle.Setters.Add(property, new AppThemeBinding
{
Light = light,
Dark = dark
});
return this;
}

Expand Down