using System.Reflection; using AtomUI.Controls; using AtomUI.Controls.Primitives; using AtomUI.Theme; using AtomUI.Theme.Configuration; using AtomUI.Theme.Resources; using Avalonia; using Avalonia.Controls; using Avalonia.Headless; using Avalonia.Media; using Avalonia.Styling; using Avalonia.Threading; using Avalonia.VisualTree; using Shouldly; using Xunit; using AtomUIButton = AtomUI.Desktop.Controls.Button; using AvaloniaWindow = Avalonia.Controls.Window; namespace AtomUI.Desktop.Controls.Tests.Buttons; public class ButtonBehaviorTests { static ButtonBehaviorTests() { AvaloniaTestApp.EnsureInitialized(); } [Fact] public void Button_ApplyTemplate_Configures_WaveSpiritType_From_Current_Shape() { var button = new AtomUIButton { Shape = ButtonShape.Circle, IsMotionEnabled = false }; ShowInWindow(button, () => { GetInternalPropertyValue(button, "WaveSpiritType") .ToString() .ShouldBe("CircleWave"); }); } [Fact] public void Button_ApplyTemplate_Configures_EffectiveBorderThickness_From_Current_Type() { var borderThickness = new Thickness(4); var button = new AtomUIButton { ButtonType = ButtonType.Link, BorderThickness = borderThickness, IsMotionEnabled = false }; ShowInWindow(button, () => { GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(new Thickness(0)); }); } [Fact] public void Button_ButtonType_Change_Refreshes_EffectiveBorderThickness() { var borderThickness = new Thickness(3); var button = new AtomUIButton { ButtonType = ButtonType.Link, BorderThickness = borderThickness, IsMotionEnabled = false }; ShowInWindow(button, () => { GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(new Thickness(0)); button.ButtonType = ButtonType.Default; Dispatcher.UIThread.RunJobs(); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(borderThickness); button.ButtonType = ButtonType.Text; Dispatcher.UIThread.RunJobs(); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(new Thickness(0)); }); } [Theory] [InlineData(ButtonType.Default, false, "Default", "Outlined", true)] [InlineData(ButtonType.Dashed, false, "Default", "Dashed", true)] [InlineData(ButtonType.Primary, false, "Primary", "Solid", true)] [InlineData(ButtonType.Text, false, "Default", "Text", false)] [InlineData(ButtonType.Link, false, "Default", "Link", false)] [InlineData(ButtonType.Default, true, "Danger", "Outlined", true)] [InlineData(ButtonType.Primary, true, "Danger", "Solid", true)] [InlineData(ButtonType.Text, true, "Danger", "Text", false)] [InlineData(ButtonType.Link, true, "Danger", "Link", false)] public void Button_Compatibility_Apis_Configure_Effective_Color_And_Variant( ButtonType buttonType, bool isDanger, string expectedColor, string expectedVariant, bool expectedBordered) { var borderThickness = new Thickness(2); var button = new AtomUIButton { ButtonType = buttonType, IsDanger = isDanger, BorderThickness = borderThickness, IsMotionEnabled = false }; ShowInWindow(button, () => { GetInternalPropertyValue(button, "EffectiveColor") .ToString() .ShouldBe(expectedColor); GetInternalPropertyValue(button, "EffectiveVariant") .ToString() .ShouldBe(expectedVariant); GetInternalPropertyValue(button, "EffectiveIsBordered") .ShouldBe(expectedBordered); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(expectedBordered ? borderThickness : new Thickness(0)); }); } [Theory] [InlineData("default")] [InlineData("primary")] [InlineData("danger")] public void Button_Text_Variant_Uses_Expected_Semantic_State_Colors(string scenario) { var button = CreateTextVariantButton(scenario); ShowInWindow(button, () => { var expected = scenario switch { "default" => new TextVariantColors( SharedTokenKind.ColorText, SharedTokenKind.ColorText, SharedTokenKind.ColorText, SharedTokenKind.ColorFillTertiary, SharedTokenKind.ColorFill), "primary" => new TextVariantColors( SharedTokenKind.ColorPrimary, SharedTokenKind.ColorPrimaryHover, SharedTokenKind.ColorPrimaryActive, SharedTokenKind.ColorPrimaryBg, SharedTokenKind.ColorPrimaryBorder), "danger" => new TextVariantColors( SharedTokenKind.ColorError, SharedTokenKind.ColorErrorHover, SharedTokenKind.ColorErrorActive, SharedTokenKind.ColorErrorBg, SharedTokenKind.ColorErrorBgActive), _ => throw new ArgumentOutOfRangeException(nameof(scenario), scenario, null) }; BrushShouldHaveSameColor(button.Foreground, GetThemeResource(expected.Text)); BrushShouldBeTransparent(button.Background); SetPseudoClass(button, StdPseudoClass.PointerOver, true); Dispatcher.UIThread.RunJobs(); BrushShouldHaveSameColor(button.Foreground, GetThemeResource(expected.TextHover)); BrushShouldHaveSameColor(button.Background, GetThemeResource(expected.BackgroundHover)); SetPseudoClass(button, StdPseudoClass.Pressed, true); Dispatcher.UIThread.RunJobs(); BrushShouldHaveSameColor(button.Foreground, GetThemeResource(expected.TextPressed)); BrushShouldHaveSameColor(button.Background, GetThemeResource(expected.BackgroundPressed)); }); } [Theory] [InlineData(ButtonType.Default, false, false)] [InlineData(ButtonType.Dashed, false, false)] [InlineData(ButtonType.Primary, false, false)] [InlineData(ButtonType.Link, false, false)] [InlineData(ButtonType.Text, false, false)] [InlineData(ButtonType.Default, true, false)] [InlineData(ButtonType.Primary, true, false)] [InlineData(ButtonType.Text, true, false)] [InlineData(ButtonType.Default, false, true)] [InlineData(ButtonType.Primary, false, true)] public void Button_Compatibility_Visuals_Project_Effective_Variant_Colors( ButtonType buttonType, bool isDanger, bool isGhost) { var button = new AtomUIButton { ButtonType = buttonType, IsDanger = isDanger, IsGhost = isGhost, Content = "Button", IsMotionEnabled = false }; ShowInWindow(button, () => { BrushShouldHaveSameColor( button.Foreground, GetInternalPropertyValue(button, "VariantTextBrush")); BrushShouldHaveSameColor( button.Background, GetInternalPropertyValue(button, "VariantBackgroundBrush")); BrushShouldHaveSameColor( button.BorderBrush, GetInternalPropertyValue(button, "VariantBorderBrush")); SetPseudoClass(button, StdPseudoClass.PointerOver, true); Dispatcher.UIThread.RunJobs(); BrushShouldHaveSameColor( button.Foreground, GetInternalPropertyValue(button, "VariantTextHoverBrush")); BrushShouldHaveSameColor( button.Background, GetInternalPropertyValue(button, "VariantBackgroundHoverBrush")); BrushShouldHaveSameColor( button.BorderBrush, GetInternalPropertyValue(button, "VariantBorderHoverBrush")); SetPseudoClass(button, StdPseudoClass.Pressed, true); Dispatcher.UIThread.RunJobs(); BrushShouldHaveSameColor( button.Foreground, GetInternalPropertyValue(button, "VariantTextPressedBrush")); BrushShouldHaveSameColor( button.Background, GetInternalPropertyValue(button, "VariantBackgroundPressedBrush")); BrushShouldHaveSameColor( button.BorderBrush, GetInternalPropertyValue(button, "VariantBorderPressedBrush")); }); } [Fact] public void Button_Explicit_Color_And_Variant_Override_ButtonType_And_Danger() { var button = new AtomUIButton { ButtonType = ButtonType.Primary, IsDanger = true, Color = ButtonColor.Pink, Variant = ButtonVariant.Filled, BorderThickness = new Thickness(2), IsMotionEnabled = false }; ShowInWindow(button, () => { GetInternalPropertyValue(button, "EffectiveColor") .ShouldBe(ButtonColor.Pink); GetInternalPropertyValue(button, "EffectiveVariant") .ShouldBe(ButtonVariant.Filled); GetInternalPropertyValue(button, "EffectiveIsBordered") .ShouldBeFalse(); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(new Thickness(0)); }); } [Fact] public void Button_IconPlacement_Defaults_To_Start_And_Can_Move_Icon_To_End() { var iconPlacementProperty = typeof(AtomUIButton).GetProperty( "IconPlacement", BindingFlags.Instance | BindingFlags.Public); iconPlacementProperty.ShouldNotBeNull(); iconPlacementProperty!.PropertyType.IsEnum.ShouldBeTrue(); Enum.GetNames(iconPlacementProperty.PropertyType).ShouldBe(["Start", "End"], ignoreOrder: false); var startButton = CreateIconPlacementButton(); var endButton = CreateIconPlacementButton(); var endValue = Enum.Parse(iconPlacementProperty.PropertyType, "End"); iconPlacementProperty.SetValue(endButton, endValue); var host = new StackPanel { Children = { startButton, endButton } }; ShowInWindow(host, () => { iconPlacementProperty.GetValue(startButton)!.ToString().ShouldBe("Start"); var startIcon = FindTemplateIcon(startButton); var endIcon = FindTemplateIcon(endButton); DockPanel.GetDock(startIcon).ShouldBe(Dock.Left); DockPanel.GetDock(endIcon).ShouldBe(Dock.Right); endIcon.Margin.Left.ShouldBe(startIcon.Margin.Right); endIcon.Margin.Right.ShouldBe(startIcon.Margin.Left); }); } [Fact] public void Button_Solid_Variant_Without_Color_Uses_Primary_Color() { var button = new AtomUIButton { Variant = ButtonVariant.Solid, BorderThickness = new Thickness(2), IsMotionEnabled = false }; ShowInWindow(button, () => { GetInternalPropertyValue(button, "EffectiveColor") .ShouldBe(ButtonColor.Primary); GetInternalPropertyValue(button, "EffectiveVariant") .ShouldBe(ButtonVariant.Solid); GetInternalPropertyValue(button, "EffectiveIsBordered") .ShouldBeTrue(); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(button.BorderThickness); }); } [Fact] public void Button_Default_Color_And_Solid_Variant_Uses_Default_Solid_Surface() { var button = new AtomUIButton { Color = ButtonColor.Default, Variant = ButtonVariant.Solid, BorderThickness = new Thickness(2), IsMotionEnabled = false }; ShowInWindow(button, () => { button.GetValue(ThemeScope.ContextProperty).ShouldNotBeNull(); GetInternalPropertyValue(button, "EffectiveColor") .ShouldBe(ButtonColor.Default); GetInternalPropertyValue(button, "EffectiveVariant") .ShouldBe(ButtonVariant.Solid); GetInternalPropertyValue(button, "EffectiveIsBordered") .ShouldBeTrue(); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(button.BorderThickness); BrushShouldHaveSameColor(button.Foreground, GetInternalPropertyValue(button, "VariantTextBrush")); BrushShouldHaveSameColor(button.Background, GetInternalPropertyValue(button, "VariantBackgroundBrush")); var foreground = GetSolidBrushColor(button.Foreground); var background = GetSolidBrushColor(button.Background); background.A.ShouldBe((byte)255); foreground.ShouldNotBe(background); }); } [Theory] [InlineData("button-type-primary")] [InlineData("explicit-primary-solid")] public void Button_Primary_Variant_Background_Follows_Scoped_ColorPrimary( string scenario) { var scopedPrimaryColor = Color.Parse("#00A1D6"); var button = CreateScopedPrimaryButton(scenario); var provider = new ThemeConfigProvider { Config = new ThemeConfigBuilder() .WithToken("ColorPrimary", "#00A1D6") .Build(), Child = button }; ShowInWindow(provider, () => { BrushShouldHaveColor(button.Background, scopedPrimaryColor); BrushShouldHaveColor(GetInternalPropertyValue(button, "VariantBackgroundBrush"), scopedPrimaryColor); BrushShouldHaveSameColor(button.Background, GetInternalPropertyValue(button, "VariantBackgroundBrush")); }); } [Fact] public void Button_Ghost_Solid_Variant_Becomes_Outlined() { var button = new AtomUIButton { Color = ButtonColor.Primary, Variant = ButtonVariant.Solid, IsGhost = true, BorderThickness = new Thickness(2), IsMotionEnabled = false }; ShowInWindow(button, () => { GetInternalPropertyValue(button, "EffectiveColor") .ShouldBe(ButtonColor.Primary); GetInternalPropertyValue(button, "EffectiveVariant") .ShouldBe(ButtonVariant.Outlined); GetInternalPropertyValue(button, "EffectiveIsBordered") .ShouldBeTrue(); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(button.BorderThickness); }); } [Theory] [InlineData(ButtonVariant.Text)] [InlineData(ButtonVariant.Link)] public void Button_Text_And_Link_Variants_Keep_Normal_Surface_Transparent_And_Shadowless( ButtonVariant variant) { var button = new AtomUIButton { Color = ButtonColor.Pink, Variant = variant, BorderThickness = new Thickness(2), IsMotionEnabled = false }; ShowInWindow(button, () => { BrushShouldBeTransparent(button.Background); BrushShouldBeTransparent(GetInternalPropertyValue(button, "VariantBackgroundBrush")); GetInternalPropertyValue(button, "VariantShadow") .ShouldBe(new BoxShadows()); GetInternalPropertyValue(button, "EffectiveBorderThickness") .ShouldBe(new Thickness(0)); var shadowFrame = FindTemplateBorder(button, "ShadowsFrame"); BrushShouldBeTransparent(shadowFrame.Background); shadowFrame.BoxShadow.ShouldBe(new BoxShadows()); }); } [Theory] [InlineData("primary-solid", true)] [InlineData("primary-outlined", false)] [InlineData("primary-text", false)] [InlineData("danger-solid", false)] [InlineData("disabled-primary-solid", false)] public void Button_CustomBackground_Visibility_Follows_Effective_State( string scenario, bool expectedVisible) { var customBackground = CreateCustomBackground(); var button = CreateCustomBackgroundButton(scenario, customBackground); ShowInWindow(button, () => { GetInternalPropertyValue(button, "HasCustomBackground") .ShouldBe(expectedVisible); var customBackgroundLayer = FindTemplateBorder(button, "CustomBackgroundLayer"); customBackgroundLayer.Background.ShouldBeSameAs(customBackground); customBackgroundLayer.IsVisible.ShouldBe(expectedVisible); customBackgroundLayer.Opacity.ShouldBe(expectedVisible ? 1.0 : 0.0); }); } [Theory] [InlineData(ButtonColor.Pink, ButtonVariant.Solid, "VariantBackgroundBrush")] [InlineData(ButtonColor.Pink, ButtonVariant.Outlined, "VariantBorderBrush")] [InlineData(ButtonColor.Cyan, ButtonVariant.Dashed, "VariantBorderBrush")] public void Button_WaveSpiritBrush_Follows_Effective_Color_And_Variant( ButtonColor color, ButtonVariant variant, string expectedBrushProperty) { var button = new AtomUIButton { Color = color, Variant = variant, IsMotionEnabled = false }; ShowInWindow(button, () => { var waveSpiritDecorator = GetPrivateFieldValue(button, "_waveSpiritDecorator"); var waveBrush = GetPublicPropertyValue(waveSpiritDecorator, "WaveBrush"); var expectedBrush = GetInternalPropertyValue(button, expectedBrushProperty); BrushShouldHaveSameColor(waveBrush, expectedBrush); }); } [Fact] public void Button_CustomBackground_Does_Not_Change_WaveSpiritBrush() { var customBackground = CreateCustomBackground(); var button = new AtomUIButton { ButtonType = ButtonType.Primary, CustomBackground = customBackground, IsMotionEnabled = false }; ShowInWindow(button, () => { var waveSpiritDecorator = GetPrivateFieldValue(button, "_waveSpiritDecorator"); var waveBrush = GetPublicPropertyValue(waveSpiritDecorator, "WaveBrush"); var expectedBrush = GetInternalPropertyValue(button, "VariantBackgroundBrush"); waveBrush.ShouldNotBeSameAs(customBackground); BrushShouldHaveSameColor(waveBrush, expectedBrush); }); } [Fact] public void Button_WaveRange_Update_Reconfigures_Cached_WavePainter() { var button = new AtomUIButton { Width = 120, Height = 36, ButtonType = ButtonType.Primary, Content = "Save", IsMotionEnabled = true }; ShowInWindow(button, () => { var waveSpiritDecorator = GetPrivateFieldValue(button, "_waveSpiritDecorator"); SetPublicPropertyValue(waveSpiritDecorator, "SizeMotionDuration", TimeSpan.FromSeconds(1)); SetPublicPropertyValue(waveSpiritDecorator, "OpacityMotionDuration", TimeSpan.FromSeconds(1)); SetPublicPropertyValue(waveSpiritDecorator, "WaveRange", 0d); InvokePublicMethod(waveSpiritDecorator, "Play"); var wavePainter = GetPrivateFieldValue(waveSpiritDecorator, "_wavePainter"); GetPublicPropertyValue(wavePainter, "WaveRange").ShouldBe(0d); SetPublicPropertyValue(waveSpiritDecorator, "WaveRange", 6d); GetPublicPropertyValue(wavePainter, "WaveRange").ShouldBe(6d); }); } [Theory] [InlineData("motion")] [InlineData("wave-spirit")] public void Button_Disabling_Wave_Gates_Cancels_Active_Wave(string gate) { var button = new AtomUIButton { Width = 120, Height = 36, ButtonType = ButtonType.Primary, Content = "Save", IsMotionEnabled = true, IsWaveSpiritEnabled = true }; ShowInWindow(button, () => { var waveSpiritDecorator = GetPrivateFieldValue(button, "_waveSpiritDecorator"); SetPublicPropertyValue(waveSpiritDecorator, "SizeMotionDuration", TimeSpan.FromSeconds(1)); SetPublicPropertyValue(waveSpiritDecorator, "OpacityMotionDuration", TimeSpan.FromSeconds(1)); InvokePublicMethod(waveSpiritDecorator, "Play"); GetInternalPropertyValue(waveSpiritDecorator, "IsPlaying").ShouldBeTrue(); if (gate == "motion") { button.IsMotionEnabled = false; } else { button.IsWaveSpiritEnabled = false; } Dispatcher.UIThread.RunJobs(); GetInternalPropertyValue(waveSpiritDecorator, "IsPlaying").ShouldBeFalse(); }); } [Fact] public void Button_CustomBackground_Layer_Overlays_Frame_In_Normal_State() { var button = new AtomUIButton { ButtonType = ButtonType.Primary, CustomBackground = CreateCustomBackground(), IsMotionEnabled = false }; ShowInWindow(button, () => { var frame = FindTemplateDashedBorder(button, "Frame"); var customBackgroundLayer = FindTemplateBorder(button, "CustomBackgroundLayer"); var rootPanel = frame.GetVisualParent(); rootPanel.ShouldNotBeNull(); var rootChildren = rootPanel!.GetVisualChildren().ToList(); var frameIndex = rootChildren.IndexOf(frame); var customBackgroundLayerIndex = rootChildren.IndexOf(customBackgroundLayer); frameIndex.ShouldBeGreaterThanOrEqualTo(0); customBackgroundLayerIndex.ShouldBeGreaterThan(frameIndex); BrushShouldHaveSameColor(frame.Background, GetInternalPropertyValue(button, "VariantBackgroundBrush")); }); } [Fact] public void Button_Default_Frame_Uses_AtomUI_Border_Rendering() { var button = new AtomUIButton { ButtonType = ButtonType.Default, BorderThickness = new Thickness(1), BorderBrush = Brushes.Black, IsMotionEnabled = false }; ShowInWindow(button, window => { window.SetRenderScaling(1.5); Dispatcher.UIThread.RunJobs(); var frame = FindTemplateDashedBorder(button, "Frame"); RenderToDrawingGroup(frame); GetBorderRenderHelperThickness(frame).ShouldBe(new Thickness(2d / 3d)); }); } private static AtomUIButton CreateCustomBackgroundButton(string scenario, IBrush customBackground) { var button = new AtomUIButton { CustomBackground = customBackground, IsMotionEnabled = false }; switch (scenario) { case "primary-solid": button.ButtonType = ButtonType.Primary; break; case "primary-outlined": button.Color = ButtonColor.Primary; button.Variant = ButtonVariant.Outlined; break; case "primary-text": button.Color = ButtonColor.Primary; button.Variant = ButtonVariant.Text; break; case "danger-solid": button.ButtonType = ButtonType.Primary; button.IsDanger = true; break; case "disabled-primary-solid": button.ButtonType = ButtonType.Primary; button.IsEnabled = false; break; default: throw new ArgumentOutOfRangeException(nameof(scenario), scenario, null); } return button; } private static AtomUIButton CreateTextVariantButton(string scenario) { var button = new AtomUIButton { Content = "Text", IsMotionEnabled = false }; switch (scenario) { case "default": button.ButtonType = ButtonType.Text; break; case "primary": button.Color = ButtonColor.Primary; button.Variant = ButtonVariant.Text; break; case "danger": button.ButtonType = ButtonType.Text; button.IsDanger = true; break; default: throw new ArgumentOutOfRangeException(nameof(scenario), scenario, null); } return button; } private static AtomUIButton CreateScopedPrimaryButton(string scenario) { var button = new AtomUIButton { IsMotionEnabled = false }; switch (scenario) { case "button-type-primary": button.ButtonType = ButtonType.Primary; break; case "explicit-primary-solid": button.Color = ButtonColor.Primary; button.Variant = ButtonVariant.Solid; break; default: throw new ArgumentOutOfRangeException(nameof(scenario), scenario, null); } return button; } private static LinearGradientBrush CreateCustomBackground() { return new LinearGradientBrush { StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative), EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative), GradientStops = { new GradientStop { Color = Colors.MediumPurple, Offset = 0 }, new GradientStop { Color = Colors.DeepSkyBlue, Offset = 1 } } }; } private static AtomUIButton CreateIconPlacementButton() { return new AtomUIButton { Content = "Search", Icon = new PathIcon { Data = Geometry.Parse("M0,0 L10,0 L10,10 Z") }, IsMotionEnabled = false }; } private static object GetInternalPropertyValue(object target, string propertyName) { var property = target.GetType().GetProperty( propertyName, BindingFlags.Instance | BindingFlags.NonPublic); property.ShouldNotBeNull(); var value = property.GetValue(target); value.ShouldNotBeNull(); return value; } private static T GetInternalPropertyValue(object target, string propertyName) { return (T)GetInternalPropertyValue(target, propertyName); } private static object GetPrivateFieldValue(object target, string fieldName) { var field = target.GetType().GetField( fieldName, BindingFlags.Instance | BindingFlags.NonPublic); field.ShouldNotBeNull(); var value = field.GetValue(target); value.ShouldNotBeNull(); return value; } private static T GetPublicPropertyValue(object target, string propertyName) { var property = target.GetType().GetProperty( propertyName, BindingFlags.Instance | BindingFlags.Public); property.ShouldNotBeNull(); return (T)property.GetValue(target)!; } private static void SetPublicPropertyValue(object target, string propertyName, T value) { var property = target.GetType().GetProperty( propertyName, BindingFlags.Instance | BindingFlags.Public); property.ShouldNotBeNull(); property.SetValue(target, value); } private static void InvokePublicMethod(object target, string methodName) { var method = target.GetType().GetMethod( methodName, BindingFlags.Instance | BindingFlags.Public); method.ShouldNotBeNull(); method.Invoke(target, null); } private static Border FindTemplateBorder(Control control, string name) { var border = control.GetVisualDescendants() .OfType() .SingleOrDefault(item => item.Name == name); border.ShouldNotBeNull(); return border; } private static DashedBorder FindTemplateDashedBorder(Control control, string name) { var border = control.GetVisualDescendants() .OfType() .SingleOrDefault(item => item.Name == name); border.ShouldNotBeNull(); return border!; } private static DrawingGroup RenderToDrawingGroup(Control control) { var drawingGroup = new DrawingGroup(); using (var context = drawingGroup.Open()) { control.Render(context); } return drawingGroup; } private static Thickness GetBorderRenderHelperThickness(DashedBorder border) { var helperField = typeof(DashedBorder).GetField( "_borderRenderHelper", BindingFlags.Instance | BindingFlags.NonPublic); helperField.ShouldNotBeNull(); var helper = helperField!.GetValue(border); helper.ShouldNotBeNull(); var thicknessField = helper!.GetType().GetField( "_borderThickness", BindingFlags.Instance | BindingFlags.NonPublic); thicknessField.ShouldNotBeNull(); return (Thickness)thicknessField!.GetValue(helper)!; } private static IconPresenter FindTemplateIcon(Control control) { var icon = control.GetVisualDescendants() .OfType() .SingleOrDefault(item => item.Name == "PART_ButtonIcon"); icon.ShouldNotBeNull(); return icon!; } private static void BrushShouldBeTransparent(IBrush? brush) { if (brush is null) { return; } GetSolidBrushColor(brush).A.ShouldBe((byte)0); } private static void BrushShouldHaveSameColor(IBrush? actual, IBrush? expected) { GetSolidBrushColor(actual).ShouldBe(GetSolidBrushColor(expected)); } private static void BrushShouldHaveColor(IBrush? actual, Color expected) { GetSolidBrushColor(actual).ShouldBe(expected); } private static T GetThemeResource(object key) { var application = Application.Current; application.ShouldNotBeNull(); application!.TryGetResource(key, application.ActualThemeVariant, out var value).ShouldBeTrue(); value.ShouldBeAssignableTo(); return (T)value!; } private static void SetPseudoClass(Control control, string pseudoClass, bool value) { ((IPseudoClasses)control.Classes).Set(pseudoClass, value); } private static Color GetSolidBrushColor(IBrush? brush) { brush.ShouldNotBeNull(); brush.ShouldBeAssignableTo(); return ((ISolidColorBrush)brush!).Color; } private static void ShowInWindow(Control content, Action assertion) { ShowInWindow(content, _ => assertion()); } private static void ShowInWindow(Control content, Action assertion) { var window = new AvaloniaWindow { Width = 240, Height = 160, Content = content }; try { window.Show(); Dispatcher.UIThread.RunJobs(); assertion(window); } finally { window.Close(); } } private sealed record TextVariantColors( SharedTokenKind Text, SharedTokenKind TextHover, SharedTokenKind TextPressed, SharedTokenKind BackgroundHover, SharedTokenKind BackgroundPressed); }