mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-24 18:23:01 +08:00
209 lines
7.6 KiB
C#
209 lines
7.6 KiB
C#
using System.Globalization;
|
|
using AtomUI.Theme.Algorithms;
|
|
using AtomUI.Theme.Configuration;
|
|
using AtomUI.Theme.Schema;
|
|
using AtomUI.Theme.DesignTokens;
|
|
using Avalonia.Media;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace AtomUI.Core.Tests.Theme;
|
|
|
|
public class ThemeConfigNormalizerTests
|
|
{
|
|
[Fact]
|
|
public void Normalize_Captures_Typed_Values_And_Classifies_Control_Tokens()
|
|
{
|
|
var schema = ThemeConfigTestSchema.Create();
|
|
var buttonConfig = new ControlThemeConfigBuilder()
|
|
.WithAlgorithm(ControlAlgorithmMode.Global)
|
|
.WithToken("Alpha", "2.5")
|
|
.WithToken("Height", "32")
|
|
.Build();
|
|
var input = new ThemeConfigBuilder()
|
|
.WithAlgorithms("Default")
|
|
.WithToken("Alpha", "1.5")
|
|
.WithControl(ThemeConfigTestSchema.ButtonIdentity, buttonConfig)
|
|
.Build();
|
|
|
|
var result = ThemeConfigNormalizer.Normalize(input, schema);
|
|
|
|
result.Success.ShouldBeTrue();
|
|
var normalized = result.Config.ShouldNotBeNull();
|
|
normalized.AlgorithmsSpecified.ShouldBeTrue();
|
|
normalized.Algorithms.Select(static item => item.Id).ShouldBe(["Default"]);
|
|
normalized.GlobalTokens.Single().Value.ShouldBe(1.5d);
|
|
var button = normalized.Controls.Single();
|
|
button.AlgorithmMode.ShouldBe(ControlAlgorithmMode.Global);
|
|
button.GlobalTokens.Single().Value.ShouldBe(2.5d);
|
|
button.OwnTokens.Single(item => item.Descriptor.Name == "Alpha").Value.ShouldBe(2.5d);
|
|
button.OwnTokens.Single(item => item.Descriptor.Name == "Height").Value.ShouldBe(32d);
|
|
}
|
|
|
|
[Fact]
|
|
public void Normalize_Produces_Order_And_Culture_Independent_Fingerprint()
|
|
{
|
|
var schema = ThemeConfigTestSchema.Create();
|
|
var first = new ThemeConfigBuilder()
|
|
.WithAlgorithms("Default", "Compact")
|
|
.WithToken("Beta", "2.50")
|
|
.WithToken("Alpha", "1.50")
|
|
.Build();
|
|
var second = new ThemeConfigBuilder()
|
|
.WithAlgorithms("Default", "Compact")
|
|
.WithToken("Alpha", "1.5")
|
|
.WithToken("Beta", "2.5")
|
|
.Build();
|
|
|
|
NormalizedThemeConfig firstNormalized;
|
|
using (new CultureScope("fr-FR"))
|
|
{
|
|
firstNormalized = ThemeConfigNormalizer.Normalize(first, schema).Config.ShouldNotBeNull();
|
|
}
|
|
|
|
var secondNormalized = ThemeConfigNormalizer.Normalize(second, schema).Config.ShouldNotBeNull();
|
|
firstNormalized.ShouldBe(secondNormalized);
|
|
firstNormalized.Fingerprint.ShouldBe(secondNormalized.Fingerprint);
|
|
}
|
|
|
|
[Fact]
|
|
public void Normalize_Returns_Diagnostics_Without_Publishing_Partial_Config()
|
|
{
|
|
var schema = ThemeConfigTestSchema.Create();
|
|
var input = new ThemeConfigBuilder()
|
|
.WithAlgorithms("Unknown")
|
|
.WithToken("Alpha", "not-a-number")
|
|
.WithToken("Missing", "1")
|
|
.WithControl(
|
|
new ControlTokenIdentity("AtomUI", "Missing"),
|
|
new ControlThemeConfigBuilder().Build())
|
|
.WithControl(
|
|
ThemeConfigTestSchema.ButtonIdentity,
|
|
new ControlThemeConfigBuilder()
|
|
.WithAlgorithm(ControlAlgorithmMode.Custom)
|
|
.Build())
|
|
.Build();
|
|
|
|
var result = ThemeConfigNormalizer.Normalize(input, schema);
|
|
|
|
result.Success.ShouldBeFalse();
|
|
result.Config.ShouldBeNull();
|
|
result.Diagnostics.Select(static item => item.Code).ShouldContain("ATMTHM4001");
|
|
result.Diagnostics.Select(static item => item.Code).ShouldContain("ATMTHM4002");
|
|
result.Diagnostics.Select(static item => item.Code).ShouldContain("ATMTHM4004");
|
|
result.Diagnostics.Select(static item => item.Code).ShouldContain("ATMTHM4005");
|
|
result.Diagnostics.Select(static item => item.Code).ShouldContain("ATMTHM4006");
|
|
}
|
|
|
|
[Fact]
|
|
public void Normalize_Fingerprint_Changes_With_Algorithm_Revision()
|
|
{
|
|
var config = new ThemeConfigBuilder()
|
|
.WithAlgorithms("Default")
|
|
.Build();
|
|
|
|
var first = ThemeConfigNormalizer.Normalize(config, ThemeConfigTestSchema.Create()).Config.ShouldNotBeNull();
|
|
var changed = ThemeConfigNormalizer.Normalize(
|
|
config,
|
|
ThemeConfigTestSchema.Create(defaultAlgorithmRevision: 2)).Config.ShouldNotBeNull();
|
|
|
|
changed.Fingerprint.ShouldNotBe(first.Fingerprint);
|
|
}
|
|
|
|
private sealed class CultureScope : IDisposable
|
|
{
|
|
private readonly CultureInfo _originalCulture = CultureInfo.CurrentCulture;
|
|
private readonly CultureInfo _originalUICulture = CultureInfo.CurrentUICulture;
|
|
|
|
internal CultureScope(string name)
|
|
{
|
|
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(name);
|
|
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(name);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
CultureInfo.CurrentCulture = _originalCulture;
|
|
CultureInfo.CurrentUICulture = _originalUICulture;
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static class ThemeConfigTestSchema
|
|
{
|
|
internal static readonly ControlTokenIdentity ButtonIdentity = new("AtomUI", "Button");
|
|
|
|
internal static ThemeSchemaRegistry Create(int defaultAlgorithmRevision = 1)
|
|
{
|
|
var globalTokens = new[]
|
|
{
|
|
Token("Alpha", 0, TokenStage.Seed),
|
|
Token("Beta", 1, TokenStage.Alias)
|
|
};
|
|
var ownTokens = new[]
|
|
{
|
|
Token("Height", 0, TokenStage.Control),
|
|
Token("Alpha", 1, TokenStage.Control)
|
|
};
|
|
var control = new ControlTokenDescriptor(
|
|
ButtonIdentity,
|
|
ownTokens,
|
|
static () => new TestControlToken(),
|
|
static (_, _) => { });
|
|
var algorithms = new[]
|
|
{
|
|
new ThemeAlgorithmDescriptor(
|
|
"Compact",
|
|
revision: 1,
|
|
ThemeAppearanceEffect.Preserve,
|
|
static () => new TestAlgorithm()),
|
|
new ThemeAlgorithmDescriptor(
|
|
"Default",
|
|
revision: defaultAlgorithmRevision,
|
|
ThemeAppearanceEffect.Light,
|
|
static () => new TestAlgorithm())
|
|
};
|
|
return new ThemeSchemaRegistry(globalTokens, [control], algorithms);
|
|
}
|
|
|
|
private static TokenDescriptor Token(string name, int slot, TokenStage stage)
|
|
{
|
|
return new TokenDescriptor(
|
|
name,
|
|
slot,
|
|
stage,
|
|
typeof(double),
|
|
name,
|
|
static value => ThemeTokenValueParser.Parse<double>(value),
|
|
static value => ThemeTokenValueFormatter.Format((double)value!),
|
|
static token => ((TestDesignToken)token).Value,
|
|
static (token, value) => ((TestDesignToken)token).Value = (double)value!,
|
|
static token => ((TestDesignToken)token).Value);
|
|
}
|
|
|
|
private sealed class TestDesignToken : AbstractDesignToken
|
|
{
|
|
internal double Value { get; set; }
|
|
}
|
|
|
|
private sealed class TestControlToken : AbstractControlDesignToken
|
|
{
|
|
internal TestControlToken()
|
|
: base("Button")
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class TestAlgorithm : IThemeAlgorithm
|
|
{
|
|
public void Evaluate(DesignToken effectiveSeed, DesignToken? previousMap, DesignToken nextMap)
|
|
{
|
|
}
|
|
}
|
|
|
|
private enum TestTokenKind
|
|
{
|
|
Value
|
|
}
|
|
}
|