diff --git a/src/Avalonia.Controls.ColorPicker/Automation/Peers/ColorSpectrumAutomationPeer.cs b/src/Avalonia.Controls.ColorPicker/Automation/Peers/ColorSpectrumAutomationPeer.cs new file mode 100644 index 0000000000..6e7e83a3a9 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Automation/Peers/ColorSpectrumAutomationPeer.cs @@ -0,0 +1,40 @@ +using Avalonia; +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace Avalonia.Automation.Peers; + +public class ColorSpectrumAutomationPeer : ControlAutomationPeer, IValueProvider +{ + public ColorSpectrumAutomationPeer(ColorSpectrum owner) + : base(owner) + { + owner.ColorChanged += OwnerOnColorChanged; + } + + public bool IsReadOnly => false; + public new ColorSpectrum Owner => (ColorSpectrum)base.Owner; + public string? Value => Owner.Color.ToString(); + + public void SetValue(string? value) + { + if (!Color.TryParse(value, out var color)) + { + throw new System.FormatException($"Invalid color string: '{value}'."); + } + + Owner.Color = color; + } + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Custom; + + protected override string GetClassNameCore() => nameof(ColorSpectrum); + + private void OwnerOnColorChanged(object? sender, ColorChangedEventArgs e) + { + RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, e.OldColor.ToString(), e.NewColor.ToString()); + } +} diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 39cd5f0c5b..bd8d3fe65f 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Collections.Pooled; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Shapes; using Avalonia.Input; @@ -197,6 +198,11 @@ namespace Avalonia.Controls.Primitives base.OnDetachedFromVisualTree(e); } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new ColorSpectrumAutomationPeer(this); + } + /// /// Explicitly unregisters all events connected in OnApplyTemplate(). /// @@ -397,6 +403,8 @@ namespace Avalonia.Controls.Primitives { if (change.Property == ColorProperty) { + _oldColor = change.GetOldValue(); + // If we're in the process of internally updating the color, // then we don't want to respond to the Color property changing. if (!_updatingColor) @@ -410,9 +418,8 @@ namespace Avalonia.Controls.Primitives UpdateEllipse(); UpdateBitmapSources(); + RaiseColorChanged(); } - - _oldColor = change.GetOldValue(); } else if (change.Property == HsvColorProperty) { diff --git a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs index fb5c959c90..f4132a5ced 100644 --- a/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox/AutoCompleteBox.cs @@ -15,6 +15,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls.Metadata; +using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; @@ -774,6 +775,8 @@ namespace Avalonia.Controls FocusChanged(HasFocus()); } + protected override AutomationPeer OnCreateAutomationPeer() => new AutoCompleteBoxAutomationPeer(this); + /// /// Determines whether the text box or drop-down portion of the /// control has diff --git a/src/Avalonia.Controls/Automation/Peers/AutoCompleteBoxAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/AutoCompleteBoxAutomationPeer.cs new file mode 100644 index 0000000000..cf25070eae --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/AutoCompleteBoxAutomationPeer.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class AutoCompleteBoxAutomationPeer : ControlAutomationPeer, IExpandCollapseProvider, IValueProvider + { + public AutoCompleteBoxAutomationPeer(AutoCompleteBox owner) + : base(owner) + { + owner.PropertyChanged += OwnerPropertyChanged; + } + + public new AutoCompleteBox Owner => (AutoCompleteBox)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen); + public bool ShowsMenu => true; + public void Collapse() => Owner.IsDropDownOpen = false; + public void Expand() => Owner.IsDropDownOpen = true; + public bool IsReadOnly => false; + + public string? Value + { + get => Owner.Text; + set + { + if (value == Owner.Text) + return; + + Owner.Text = value; + } + } + + void IValueProvider.SetValue(string? value) => Owner.Text = value; + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Group; + } + + protected override string GetClassNameCore() => nameof(AutoCompleteBox); + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == AutoCompleteBox.IsDropDownOpenProperty) + { + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + ToState(e.GetOldValue()), + ToState(e.GetNewValue())); + } + else if (e.Property == AutoCompleteBox.TextProperty) + { + RaisePropertyChangedEvent( + ValuePatternIdentifiers.ValueProperty, + e.OldValue, + e.NewValue); + } + } + + private static ExpandCollapseState ToState(bool value) + { + return value ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/CalendarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CalendarAutomationPeer.cs new file mode 100644 index 0000000000..a79b1de1bc --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/CalendarAutomationPeer.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; + +namespace Avalonia.Automation.Peers +{ + public class CalendarAutomationPeer : ControlAutomationPeer, ISelectionProvider, IValueProvider + { + public CalendarAutomationPeer(Calendar owner) + : base(owner) + { + owner.SelectedDatesChanged += OwnerSelectedDatesChanged; + } + + public new Calendar Owner => (Calendar)base.Owner; + + public bool CanSelectMultiple => + Owner.SelectionMode == CalendarSelectionMode.SingleRange || + Owner.SelectionMode == CalendarSelectionMode.MultipleRange; + + public bool IsSelectionRequired => false; + + public IReadOnlyList GetSelection() + { + return Owner.SelectedDates.Select(date => Owner.FindDayButtonFromDay(date)) + .OfType().Select(GetOrCreate).ToList(); + } + + public bool IsReadOnly => true; + + public string? Value => string.Join( + System.Globalization.CultureInfo.CurrentCulture.TextInfo.ListSeparator, + Owner.SelectedDates.Select(x => x.ToString(System.Globalization.CultureInfo.CurrentCulture))); + + public void SetValue(string? value) + { + throw new NotSupportedException(); + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Calendar; + } + + protected override string GetClassNameCore() => nameof(Calendar); + + private void OwnerSelectedDatesChanged(object? sender, SelectionChangedEventArgs e) + { + RaisePropertyChangedEvent(SelectionPatternIdentifiers.SelectionProperty, null, null); + RaisePropertyChangedEvent(ValuePatternIdentifiers.ValueProperty, null, null); + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/CalendarDatePickerAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/CalendarDatePickerAutomationPeer.cs new file mode 100644 index 0000000000..9fc8c49838 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/CalendarDatePickerAutomationPeer.cs @@ -0,0 +1,72 @@ +using System; +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class CalendarDatePickerAutomationPeer : ControlAutomationPeer, + IInvokeProvider, + IExpandCollapseProvider, + IValueProvider + { + public CalendarDatePickerAutomationPeer(CalendarDatePicker owner) + : base(owner) + { + Owner.PropertyChanged += OwnerPropertyChanged; + } + + public new CalendarDatePicker Owner => (CalendarDatePicker)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsDropDownOpen); + + public bool ShowsMenu => true; + + public bool IsReadOnly => false; + + public string? Value => Owner.Text; + + public void Invoke() + { + EnsureEnabled(); + Owner.IsDropDownOpen = true; + } + + public void Expand() => Owner.IsDropDownOpen = true; + + public void Collapse() => Owner.IsDropDownOpen = false; + + public void SetValue(string? value) => Owner.Text = value; + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Button; + + protected override string GetClassNameCore() => "CalendarDatePicker"; + + protected override bool IsContentElementCore() => true; + + protected override bool IsControlElementCore() => true; + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == CalendarDatePicker.TextProperty) + { + RaisePropertyChangedEvent( + ValuePatternIdentifiers.ValueProperty, + e.OldValue, + e.NewValue); + } + else if (e.Property == CalendarDatePicker.IsDropDownOpenProperty) + { + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + ToState(e.GetOldValue()), + ToState(e.GetNewValue())); + } + } + + private static ExpandCollapseState ToState(bool isExpanded) + { + return isExpanded ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NativeMenuBarAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NativeMenuBarAutomationPeer.cs new file mode 100644 index 0000000000..f39ba342fe --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NativeMenuBarAutomationPeer.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class NativeMenuBarAutomationPeer(NativeMenuBar owner) : ControlAutomationPeer(owner) + { + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.MenuBar; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/NumericUpDownAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/NumericUpDownAutomationPeer.cs new file mode 100644 index 0000000000..3c205785af --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/NumericUpDownAutomationPeer.cs @@ -0,0 +1,73 @@ +using System; +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class NumericUpDownAutomationPeer : ControlAutomationPeer, IRangeValueProvider + { + public NumericUpDownAutomationPeer(NumericUpDown owner) + : base(owner) + { + Owner.PropertyChanged += OwnerPropertyChanged; + } + + public new NumericUpDown Owner => (NumericUpDown)base.Owner; + + public bool IsReadOnly => Owner.IsReadOnly; + + public double Maximum => (double)Owner.Maximum; + + public double Minimum => (double)Owner.Minimum; + + public double Value => Owner.Value.HasValue + ? (double)Owner.Value.Value + : (double)Math.Clamp(0m, Owner.Minimum, Owner.Maximum); + + public double SmallChange => (double)Owner.Increment; + + public double LargeChange => (double)Owner.Increment; + + public void SetValue(double value) + { + Owner.Value = (decimal)value; + } + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Spinner; + + protected override string GetClassNameCore() => "NumericUpDown"; + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == NumericUpDown.MinimumProperty) + { + RaisePropertyChangedEvent( + RangeValuePatternIdentifiers.MinimumProperty, + e.OldValue, + e.NewValue); + } + else if (e.Property == NumericUpDown.MaximumProperty) + { + RaisePropertyChangedEvent( + RangeValuePatternIdentifiers.MaximumProperty, + e.OldValue, + e.NewValue); + } + else if (e.Property == NumericUpDown.ValueProperty) + { + RaisePropertyChangedEvent( + RangeValuePatternIdentifiers.ValueProperty, + e.OldValue, + e.NewValue); + } + else if (e.Property == NumericUpDown.IsReadOnlyProperty) + { + RaisePropertyChangedEvent( + RangeValuePatternIdentifiers.IsReadOnlyProperty, + e.OldValue, + e.NewValue); + } + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/SplitButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SplitButtonAutomationPeer.cs new file mode 100644 index 0000000000..a683ac1eb1 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SplitButtonAutomationPeer.cs @@ -0,0 +1,73 @@ +using Avalonia.Automation; +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class SplitButtonAutomationPeer : ContentControlAutomationPeer, IInvokeProvider, IExpandCollapseProvider + { + private ExpandCollapseState _expandCollapseState; + + public SplitButtonAutomationPeer(SplitButton owner) + : base(owner) + { + _expandCollapseState = ToState(owner.IsFlyoutOpen); + owner.FlyoutStateChanged += OwnerFlyoutStateChanged; + } + + public new SplitButton Owner => (SplitButton)base.Owner; + + public ExpandCollapseState ExpandCollapseState => ToState(Owner.IsFlyoutOpen); + + public bool ShowsMenu => true; + + public void Collapse() + { + Owner.CloseFlyoutForAutomation(); + } + + public void Expand() + { + Owner.OpenFlyoutForAutomation(); + } + + void IInvokeProvider.Invoke() + { + EnsureEnabled(); + Owner.InvokePrimary(); + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.SplitButton; + } + + protected override string GetClassNameCore() + { + return "SplitButton"; + } + + protected override bool IsContentElementCore() => true; + protected override bool IsControlElementCore() => true; + + private void OwnerFlyoutStateChanged(object? sender, System.EventArgs e) + { + var oldState = _expandCollapseState; + var newState = ToState(Owner.IsFlyoutOpen); + + if (oldState != newState) + { + _expandCollapseState = newState; + RaisePropertyChangedEvent( + ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, + oldState, + newState); + } + } + + private static ExpandCollapseState ToState(bool isOpen) + { + return isOpen ? ExpandCollapseState.Expanded : ExpandCollapseState.Collapsed; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ToggleSplitButtonAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToggleSplitButtonAutomationPeer.cs new file mode 100644 index 0000000000..7af8626e57 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ToggleSplitButtonAutomationPeer.cs @@ -0,0 +1,50 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ToggleSplitButtonAutomationPeer : SplitButtonAutomationPeer, IToggleProvider + { + public ToggleSplitButtonAutomationPeer(ToggleSplitButton owner) + : base(owner) + { + owner.PropertyChanged += OwnerPropertyChanged; + } + + public new ToggleSplitButton Owner => (ToggleSplitButton)base.Owner; + + ToggleState IToggleProvider.ToggleState => ToState(Owner.IsChecked); + + void IToggleProvider.Toggle() + { + EnsureEnabled(); + Owner.ToggleForAutomation(); + } + + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.SplitButton; + } + + protected override string GetClassNameCore() + { + return "ToggleSplitButton"; + } + + private void OwnerPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == ToggleSplitButton.IsCheckedProperty) + { + RaisePropertyChangedEvent( + TogglePatternIdentifiers.ToggleStateProperty, + ToState(e.GetOldValue()), + ToState(e.GetNewValue())); + } + } + + private static ToggleState ToState(bool value) + { + return value ? ToggleState.On : ToggleState.Off; + } + } +} diff --git a/src/Avalonia.Controls/Automation/Peers/ToolTipAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ToolTipAutomationPeer.cs new file mode 100644 index 0000000000..f3d1f1a260 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/ToolTipAutomationPeer.cs @@ -0,0 +1,14 @@ +using Avalonia.Automation.Provider; +using Avalonia.Controls; + +namespace Avalonia.Automation.Peers +{ + public class ToolTipAutomationPeer(ToolTip owner) : ControlAutomationPeer(owner) + { + public new ToolTip Owner => (ToolTip)base.Owner; + + protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.ToolTip; + + protected override string GetClassNameCore() => "ToolTip"; + } +} diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 819019a634..7062a9af81 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -6,6 +6,7 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -2251,5 +2252,6 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() => new CalendarAutomationPeer(this); } } diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index 097fd2fb85..6194d4c14e 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -7,6 +7,7 @@ using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; +using Avalonia.Automation.Peers; using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -195,6 +196,8 @@ namespace Avalonia.Controls UpdatePseudoClasses(); } + protected override AutomationPeer OnCreateAutomationPeer() => new CalendarDatePickerAutomationPeer(this); + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs index 022511bafa..ddb0b8136e 100644 --- a/src/Avalonia.Controls/NativeMenuBar.cs +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; @@ -70,5 +71,7 @@ namespace Avalonia.Controls menu.Bind(ItemsControl.ItemsSourceProperty, topLevel.GetBindingObservable(NativeMenu.MenuProperty) .Select(v => v.GetValueOrDefault()?.Items))); } + + protected override AutomationPeer OnCreateAutomationPeer() => new NativeMenuBarAutomationPeer(this); } } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 073a2f109a..86a96ae7ba 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using System.IO; using System.Linq; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -488,6 +489,8 @@ namespace Avalonia.Controls SetValidSpinDirection(); } + protected override AutomationPeer OnCreateAutomationPeer() => new NumericUpDownAutomationPeer(this); + /// protected override void OnKeyDown(KeyEventArgs e) { diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 390f679191..145df37e60 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -1,6 +1,7 @@ using System; using System.Windows.Input; using Avalonia.Controls.Metadata; +using Avalonia.Automation.Peers; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; @@ -74,6 +75,8 @@ namespace Avalonia.Controls private IDisposable? _flyoutPropertyChangedDisposable; + internal event EventHandler? FlyoutStateChanged; + /// /// Initializes a new instance of the class. /// @@ -256,6 +259,8 @@ namespace Avalonia.Controls UpdatePseudoClasses(); } + protected override AutomationPeer OnCreateAutomationPeer() => new SplitButtonAutomationPeer(this); + /// protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { @@ -418,6 +423,23 @@ namespace Avalonia.Controls } } + internal void InvokePrimary() + { + OnClickPrimary(null); + } + + internal bool IsFlyoutOpen => _isFlyoutOpen; + + internal void OpenFlyoutForAutomation() + { + OpenFlyout(); + } + + internal void CloseFlyoutForAutomation() + { + CloseFlyout(); + } + /// /// Invoked when the secondary button part is clicked. /// @@ -516,6 +538,7 @@ namespace Avalonia.Controls { _isFlyoutOpen = true; UpdatePseudoClasses(); + FlyoutStateChanged?.Invoke(this, EventArgs.Empty); OnFlyoutOpened(); } @@ -533,6 +556,7 @@ namespace Avalonia.Controls { _isFlyoutOpen = false; UpdatePseudoClasses(); + FlyoutStateChanged?.Invoke(this, EventArgs.Empty); OnFlyoutClosed(); } diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index c493445ba1..3bd56f69f5 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs @@ -1,5 +1,6 @@ using System; +using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -65,6 +66,8 @@ namespace Avalonia.Controls /// protected override Type StyleKeyOverride => typeof(SplitButton); + protected override AutomationPeer OnCreateAutomationPeer() => new ToggleSplitButtonAutomationPeer(this); + /// /// Toggles the property between true and false. /// @@ -73,6 +76,11 @@ namespace Avalonia.Controls SetCurrentValue(IsCheckedProperty, !IsChecked); } + internal void ToggleForAutomation() + { + OnClickPrimary(null); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index d2b3da81fe..433098e3f5 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using Avalonia.Automation.Peers; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -472,5 +473,8 @@ namespace Avalonia.Controls { PseudoClasses.Set(":open", newValue); } + + protected override AutomationPeer OnCreateAutomationPeer() + => new ToolTipAutomationPeer(this); } } diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ColorSpectrumAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ColorSpectrumAutomationPeerTests.cs new file mode 100644 index 0000000000..58c5b467a8 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/ColorSpectrumAutomationPeerTests.cs @@ -0,0 +1,91 @@ +using System; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Automation; + +public class ColorSpectrumAutomationPeerTests +{ + public class AutomationPeerTests : ScopedTestBase + { + [Fact] + public void Creates_ColorSpectrumAutomationPeer() + { + var spectrum = new ColorSpectrum(); + var peer = ControlAutomationPeer.CreatePeerForElement(spectrum); + + Assert.IsType(peer); + } + + [Fact] + public void ControlType_Is_Custom() + { + var spectrum = new ColorSpectrum(); + var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum); + + Assert.Equal(AutomationControlType.Custom, peer.GetAutomationControlType()); + } + + [Fact] + public void ClassName_Is_ColorSpectrum() + { + var spectrum = new ColorSpectrum(); + var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum); + + Assert.Equal("ColorSpectrum", peer.GetClassName()); + } + + [Fact] + public void Implements_IValueProvider() + { + var spectrum = new ColorSpectrum(); + var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum); + + Assert.Equal(Colors.White.ToString(), peer.Value); + + var valueProvider = Assert.IsAssignableFrom(peer); + valueProvider.SetValue("#00FF00"); + + Assert.Equal(Colors.Lime.ToString(), peer.Value); + Assert.Equal(Colors.Lime, spectrum.Color); + } + + [Fact] + public void SetValue_Uses_Color_Parse_And_Throws_FormatException_On_Invalid_Input() + { + var spectrum = new ColorSpectrum(); + var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum); + var valueProvider = Assert.IsAssignableFrom(peer); + + Assert.Throws(() => valueProvider.SetValue("not-a-color")); + } + + [Fact] + public void ValueProperty_Raises_AutomationPropertyChangedEvent_On_Color_Change() + { + var spectrum = new ColorSpectrum(); + var peer = (ColorSpectrumAutomationPeer)ControlAutomationPeer.CreatePeerForElement(spectrum); + AutomationPropertyChangedEventArgs? changed = null; + + peer.PropertyChanged += (_, e) => + { + if (e.Property == ValuePatternIdentifiers.ValueProperty) + { + changed = e; + } + }; + + spectrum.Color = Colors.Black; + + Assert.NotNull(changed); + Assert.Equal(ValuePatternIdentifiers.ValueProperty, changed!.Property); + Assert.Equal(Colors.White.ToString(), changed.OldValue); + Assert.Equal(Colors.Black.ToString(), changed.NewValue); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Automation/ComplexControlAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/ComplexControlAutomationPeerTests.cs new file mode 100644 index 0000000000..52f4845a0a --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/ComplexControlAutomationPeerTests.cs @@ -0,0 +1,292 @@ +using System; +using System.Globalization; +using System.Linq; +using Avalonia; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Automation; + +public class AutoCompleteBoxAutomationPeerTests : ScopedTestBase +{ + [Fact] + public void Creates_AutoCompleteBoxAutomationPeer() + { + var target = new AutoCompleteBox(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsType(peer); + } + + [Fact] + public void Implements_IExpandCollapse_And_IValue_Providers() + { + var target = new AutoCompleteBox(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsAssignableFrom(peer); + Assert.IsAssignableFrom(peer); + Assert.False(peer is IInvokeProvider); + } + + [Fact] + public void ControlType_Is_Group_And_ClassName_Is_AutoCompleteBox() + { + var target = new AutoCompleteBox(); + var peer = (AutoCompleteBoxAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal(AutomationControlType.Group, peer.GetAutomationControlType()); + Assert.Equal(nameof(AutoCompleteBox), peer.GetClassName()); + } + + [Fact] + public void ExpandCollapse_Tracks_IsDropDownOpen() + { + var target = new AutoCompleteBox + { + ItemsSource = new[] { "alpha" }, + Text = "a" + }; + var peer = (IExpandCollapseProvider)ControlAutomationPeer.CreatePeerForElement(target); + Assert.True(peer.ShowsMenu); + + target.IsDropDownOpen = false; + Assert.False(target.IsDropDownOpen); + + peer.Expand(); + Assert.True(target.IsDropDownOpen); + Assert.Equal(ExpandCollapseState.Expanded, peer.ExpandCollapseState); + + peer.Collapse(); + Assert.False(target.IsDropDownOpen); + Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState); + } + + [Fact] + public void Value_Tracks_And_Sets_Text() + { + var target = new AutoCompleteBox { Text = "one" }; + var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal("one", peer.Value); + peer.SetValue("two"); + Assert.Equal("two", target.Text); + Assert.Equal("two", ((IValueProvider)peer).Value); + } + + [Fact] + public void ValueProvider_IsMutable() + { + var target = new AutoCompleteBox(); + var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.False(peer.IsReadOnly); + } + + [Fact] + public void Property_Change_Events_Raise_For_DropDown_And_Text() + { + var target = new AutoCompleteBox(); + var peer = (AutoCompleteBoxAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + AutomationPropertyChangedEventArgs? expandCollapseChanged = null; + AutomationPropertyChangedEventArgs? valueChanged = null; + peer.PropertyChanged += (_, e) => + { + if (e.Property == ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty) + { + expandCollapseChanged = e; + } + else if (e.Property == ValuePatternIdentifiers.ValueProperty) + { + valueChanged = e; + } + }; + + target.IsDropDownOpen = true; + Assert.NotNull(expandCollapseChanged); + Assert.Equal(ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, expandCollapseChanged!.Property); + Assert.Equal(ExpandCollapseState.Collapsed, expandCollapseChanged.OldValue); + Assert.Equal(ExpandCollapseState.Expanded, expandCollapseChanged.NewValue); + + target.Text = "query"; + Assert.NotNull(valueChanged); + Assert.Equal(ValuePatternIdentifiers.ValueProperty, valueChanged!.Property); + Assert.Equal(string.Empty, valueChanged.OldValue); + Assert.Equal("query", valueChanged.NewValue); + } +} + +public class CalendarAutomationPeerTests : ScopedTestBase +{ + [Fact] + public void Creates_CalendarAutomationPeer() + { + var target = new Calendar(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsType(peer); + } + + [Fact] + public void Implements_ISelection_And_IValue_Providers() + { + var target = new Calendar(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsAssignableFrom(peer); + Assert.IsAssignableFrom(peer); + } + + [Fact] + public void ControlType_Is_Calendar_And_ClassName_Is_Calendar() + { + var target = new Calendar(); + var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal(AutomationControlType.Calendar, peer.GetAutomationControlType()); + Assert.Equal(nameof(Calendar), peer.GetClassName()); + } + + [Theory] + [InlineData(CalendarSelectionMode.SingleDate, false)] + [InlineData(CalendarSelectionMode.SingleRange, true)] + [InlineData(CalendarSelectionMode.MultipleRange, true)] + [InlineData(CalendarSelectionMode.None, false)] + public void CanSelectMultiple_Reflects_SelectionMode(CalendarSelectionMode selectionMode, bool canSelectMultiple) + { + var target = new Calendar { SelectionMode = selectionMode }; + var peer = (ISelectionProvider)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal(canSelectMultiple, peer.CanSelectMultiple); + Assert.False(peer.IsSelectionRequired); + } + + [Fact] + public void Selection_Events_Include_Selection_And_Value_Properties() + { + var target = new Calendar { SelectionMode = CalendarSelectionMode.SingleDate }; + var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + AutomationPropertyChangedEventArgs? selectionChanged = null; + AutomationPropertyChangedEventArgs? valueChanged = null; + peer.PropertyChanged += (_, e) => + { + if (e.Property == SelectionPatternIdentifiers.SelectionProperty) + { + selectionChanged = e; + } + else if (e.Property == ValuePatternIdentifiers.ValueProperty) + { + valueChanged = e; + } + }; + + target.SelectedDate = new DateTime(2010, 1, 1); + + Assert.NotNull(selectionChanged); + Assert.NotNull(valueChanged); + Assert.Equal(SelectionPatternIdentifiers.SelectionProperty, selectionChanged!.Property); + Assert.Equal(ValuePatternIdentifiers.ValueProperty, valueChanged!.Property); + } + + [Fact] + public void Value_Joins_Selected_Dates_With_CurrentCulture() + { + var previousCulture = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB"); + + var selectedDates = new[] { new DateTime(2026, 5, 7), new DateTime(2026, 5, 8) }; + var target = new Calendar + { + SelectionMode = CalendarSelectionMode.MultipleRange + }; + var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + foreach (var date in selectedDates) + { + target.SelectedDates.Add(date); + } + + Assert.Equal( + string.Join(CultureInfo.CurrentCulture.TextInfo.ListSeparator, selectedDates.Select(x => x.ToString(CultureInfo.CurrentCulture))), + peer.Value); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + } + + [Fact] + public void SetValue_Throws_NotSupported() + { + var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(new Calendar()); + + Assert.Throws(() => peer.SetValue("2026-01-01")); + } + + [Fact] + public void Value_Is_ReadOnly() + { + var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(new Calendar()); + + Assert.True(peer.IsReadOnly); + } + + [Fact] + public void GetSelection_Returns_Empty_When_DayButton_Not_Realized() + { + var target = new Calendar { SelectionMode = CalendarSelectionMode.SingleDate, SelectedDate = new DateTime(2026, 5, 7) }; + var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + var selection = peer.GetSelection(); + + Assert.Empty(selection); + } + + [Fact] + public void GetSelection_Returns_Realized_DayButton_Peers() + { + var selectedDate = new DateTime(2026, 5, 7); + var target = new Calendar + { + SelectionMode = CalendarSelectionMode.SingleDate, + DisplayDate = new DateTime(2026, 5, 1), + SelectedDate = selectedDate, + }; + var monthView = new Grid(); + var calendarItem = new CalendarItem + { + Owner = target, + MonthView = monthView + }; + target.Root = new Panel { Children = { calendarItem } }; + + for (var i = 0; i < Calendar.ColumnsPerMonth; i++) + { + monthView.Children.Add(new TextBlock()); + } + + for (var i = 0; i < Calendar.RowsPerMonth * Calendar.ColumnsPerMonth - Calendar.ColumnsPerMonth; i++) + { + monthView.Children.Add(new CalendarDayButton + { + Owner = target, + DataContext = i == 0 ? selectedDate : selectedDate.AddDays(i + 1) + }); + } + + var peer = (CalendarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + var selection = peer.GetSelection(); + + Assert.Single(selection); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Automation/FoundationAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/FoundationAutomationPeerTests.cs new file mode 100644 index 0000000000..29865d20e4 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/FoundationAutomationPeerTests.cs @@ -0,0 +1,266 @@ +using System; +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Xunit; + +#nullable enable + +namespace Avalonia.Controls.UnitTests.Automation; + +public class FoundationAutomationPeerTests +{ + public class ToolTipPeer : ScopedTestBase + { + [Fact] + public void Creates_ToolTipAutomationPeer() + { + var control = new ToolTip(); + var peer = ControlAutomationPeer.CreatePeerForElement(control); + + Assert.IsType(peer); + } + + [Fact] + public void ControlType_Is_ToolTip() + { + var control = new ToolTip(); + var peer = (ToolTipAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.Equal(AutomationControlType.ToolTip, peer.GetAutomationControlType()); + Assert.Equal("ToolTip", peer.GetClassName()); + } + } + + public class CalendarDatePickerPeer : ScopedTestBase + { + [Fact] + public void Creates_CalendarDatePickerAutomationPeer() + { + var control = new CalendarDatePicker(); + var peer = ControlAutomationPeer.CreatePeerForElement(control); + + Assert.IsType(peer); + } + + [Fact] + public void Implements_IInvoke_And_IValue_Providers() + { + var control = new CalendarDatePicker(); + var peer = ControlAutomationPeer.CreatePeerForElement(control); + + Assert.IsAssignableFrom(peer); + Assert.IsAssignableFrom(peer); + Assert.IsAssignableFrom(peer); + } + + [Fact] + public void Has_Button_ControlType() + { + var control = new CalendarDatePicker(); + var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.Equal(AutomationControlType.Button, peer.GetAutomationControlType()); + Assert.Equal("CalendarDatePicker", peer.GetClassName()); + } + + [Fact] + public void Invoke_Opens_DropDown() + { + var control = new CalendarDatePicker(); + var peer = (IInvokeProvider)ControlAutomationPeer.CreatePeerForElement(control); + + peer.Invoke(); + + Assert.True(control.IsDropDownOpen); + } + + [Fact] + public void ExpandCollapse_Tracks_IsDropDownOpen() + { + var control = new CalendarDatePicker(); + var peer = (IExpandCollapseProvider)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.True(peer.ShowsMenu); + Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState); + + peer.Expand(); + Assert.True(control.IsDropDownOpen); + Assert.Equal(ExpandCollapseState.Expanded, peer.ExpandCollapseState); + + peer.Collapse(); + Assert.False(control.IsDropDownOpen); + Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState); + } + + [Fact] + public void Value_Mirrors_Owner_Text() + { + var control = new CalendarDatePicker { Text = "typed value" }; + var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.Equal("typed value", peer.Value); + + control.Text = "updated typed value"; + + Assert.Equal("updated typed value", peer.Value); + } + + [Fact] + public void SetValue_Updates_Text() + { + var control = new CalendarDatePicker(); + var peer = (IValueProvider)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.False(peer.IsReadOnly); + + peer.SetValue("automation text"); + + Assert.Equal("automation text", control.Text); + } + + [Fact] + public void PropertyChanged_Raises_Value_When_Text_Changes() + { + var control = new CalendarDatePicker(); + var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + AutomationPropertyChangedEventArgs? changed = null; + + peer.PropertyChanged += (_, e) => + { + if (e.Property == ValuePatternIdentifiers.ValueProperty) + changed = e; + }; + + control.Text = "January"; + + Assert.NotNull(changed); + Assert.Equal(ValuePatternIdentifiers.ValueProperty, changed!.Property); + Assert.Null(changed.OldValue); + Assert.Equal("January", changed.NewValue); + } + + [Fact] + public void PropertyChanged_Raises_ExpandCollapseState_When_DropDown_Open_Changes() + { + var control = new CalendarDatePicker(); + var peer = (CalendarDatePickerAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + AutomationPropertyChangedEventArgs? changed = null; + + peer.PropertyChanged += (_, e) => + { + if (e.Property == ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty) + changed = e; + }; + + control.IsDropDownOpen = true; + + Assert.NotNull(changed); + Assert.Equal(ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty, changed!.Property); + Assert.Equal(ExpandCollapseState.Collapsed, changed.OldValue); + Assert.Equal(ExpandCollapseState.Expanded, changed.NewValue); + } + } + + public class NumericUpDownPeer : ScopedTestBase + { + [Fact] + public void Creates_NumericUpDownAutomationPeer() + { + var control = new NumericUpDown(); + var peer = ControlAutomationPeer.CreatePeerForElement(control); + + Assert.IsType(peer); + } + + [Fact] + public void Is_Spinner_ControlType() + { + var control = new NumericUpDown(); + var peer = (NumericUpDownAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.Equal(AutomationControlType.Spinner, peer.GetAutomationControlType()); + Assert.Equal("NumericUpDown", peer.GetClassName()); + } + + [Fact] + public void Implements_IRangeValueProvider() + { + var control = new NumericUpDown(); + var peer = ControlAutomationPeer.CreatePeerForElement(control); + + Assert.IsAssignableFrom(peer); + } + + [Fact] + public void Range_Values_Reflect_Owner() + { + var control = new NumericUpDown + { + Minimum = 10, + Maximum = 20, + Increment = 3, + IsReadOnly = true, + Value = 14, + }; + + var peer = (IRangeValueProvider)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.Equal(10d, peer.Minimum); + Assert.Equal(20d, peer.Maximum); + Assert.Equal(14d, peer.Value); + Assert.Equal(3d, peer.SmallChange); + Assert.Equal(3d, peer.LargeChange); + Assert.True(peer.IsReadOnly); + } + + [Fact] + public void Null_Value_Reports_Default_Clamped_To_Range() + { + var control = new NumericUpDown + { + Minimum = 10, + Maximum = 20, + Value = null, + }; + + var peer = (IRangeValueProvider)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.Equal(10d, peer.Value); + } + + [Fact] + public void SetValue_Updates_Owner_Value() + { + var control = new NumericUpDown(); + var peer = (IRangeValueProvider)ControlAutomationPeer.CreatePeerForElement(control); + + peer.SetValue(42.5); + + Assert.Equal(42.5m, control.Value); + } + + [Fact] + public void PropertyChanged_Raises_Range_When_Value_Changes() + { + var control = new NumericUpDown(); + var peer = (NumericUpDownAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + AutomationPropertyChangedEventArgs? changed = null; + + peer.PropertyChanged += (_, e) => + { + if (e.Property == RangeValuePatternIdentifiers.ValueProperty) + changed = e; + }; + + control.Value = 7.5m; + + Assert.NotNull(changed); + Assert.Equal(RangeValuePatternIdentifiers.ValueProperty, changed!.Property); + Assert.Null(changed.OldValue); + Assert.Equal(7.5m, changed.NewValue); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Automation/NativeMenuBarAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/NativeMenuBarAutomationPeerTests.cs new file mode 100644 index 0000000000..4f4eb2a484 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/NativeMenuBarAutomationPeerTests.cs @@ -0,0 +1,46 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Automation; + +public class NativeMenuBarAutomationPeerTests +{ + public class PeerCreation : ScopedTestBase + { + [Fact] + public void Creates_NativeMenuBarAutomationPeer() + { + var control = new NativeMenuBar { Template = CreateTemplate() }; + var peer = ControlAutomationPeer.CreatePeerForElement(control); + + Assert.IsType(peer); + } + + [Fact] + public void ControlType_Is_MenuBar() + { + var control = new NativeMenuBar { Template = CreateTemplate() }; + var peer = (NativeMenuBarAutomationPeer)ControlAutomationPeer.CreatePeerForElement(control); + + Assert.Equal(AutomationControlType.MenuBar, peer.GetAutomationControlType()); + } + } + + private static FuncControlTemplate CreateTemplate() + { + return new FuncControlTemplate((_, ns) => + new Menu + { + Name = "PART_NativeMenuPresenter", + Items = + { + new MenuItem { Header = "File" }, + new MenuItem { Header = "Edit" }, + }, + }.RegisterInNameScope(ns)); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Automation/SplitButtonAutomationPeerTests.cs b/tests/Avalonia.Controls.UnitTests/Automation/SplitButtonAutomationPeerTests.cs new file mode 100644 index 0000000000..9eb5e10ec9 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Automation/SplitButtonAutomationPeerTests.cs @@ -0,0 +1,196 @@ +using Avalonia.Automation; +using Avalonia.Automation.Peers; +using Avalonia.Automation.Provider; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Automation; + +public class SplitButtonAutomationPeerTests : ScopedTestBase +{ + [Fact] + public void Creates_SplitButtonAutomationPeer() + { + var target = new SplitButton(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsType(peer); + } + + [Fact] + public void Implements_IExpandCollapseProvider() + { + var target = new SplitButton(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsAssignableFrom(peer); + } + + [Fact] + public void Implements_IInvokeProvider() + { + var target = new SplitButton(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsAssignableFrom(peer); + } + + [Fact] + public void ControlType_Is_SplitButton() + { + var target = new SplitButton(); + var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal(AutomationControlType.SplitButton, peer.GetAutomationControlType()); + } + + [Fact] + public void ClassName_Is_SplitButton() + { + var target = new SplitButton(); + var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal("SplitButton", peer.GetClassName()); + } + + [Fact] + public void ShowsMenu_Is_True() + { + var target = new SplitButton(); + var peer = (IExpandCollapseProvider)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.True(peer.ShowsMenu); + } + + [Fact] + public void Invoke_Triggers_Click() + { + var clicked = 0; + var target = new SplitButton(); + var peer = (IInvokeProvider)ControlAutomationPeer.CreatePeerForElement(target); + + target.Click += (_, _) => clicked++; + peer.Invoke(); + + Assert.Equal(1, clicked); + } + + [Fact] + public void ExpandCollapse_State_Tracks_Flyout() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new SplitButton + { + Flyout = new Flyout() + }; + var window = new Window + { + Content = target, + }; + window.Show(); + + var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState); + + peer.Expand(); + Assert.Equal(ExpandCollapseState.Expanded, peer.ExpandCollapseState); + Assert.True(target.Flyout?.IsOpen); + + peer.Collapse(); + Assert.Equal(ExpandCollapseState.Collapsed, peer.ExpandCollapseState); + Assert.False(target.Flyout!.IsOpen); + } + } + + [Fact] + public void ExpandCollapse_Raises_PropertyChanged() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new SplitButton + { + Flyout = new Flyout() + }; + var window = new Window + { + Content = target, + }; + window.Show(); + + var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + AutomationPropertyChangedEventArgs? changed = null; + peer.PropertyChanged += (_, e) => + { + if (e.Property == ExpandCollapsePatternIdentifiers.ExpandCollapseStateProperty) + { + changed = e; + } + }; + + peer.Expand(); + + Assert.NotNull(changed); + Assert.Equal(ExpandCollapseState.Collapsed, changed!.OldValue); + Assert.Equal(ExpandCollapseState.Expanded, changed.NewValue); + } + } +} + +public class ToggleSplitButtonAutomationPeerTests : ScopedTestBase +{ + [Fact] + public void Creates_ToggleSplitButtonAutomationPeer() + { + var target = new ToggleSplitButton(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsType(peer); + } + + [Fact] + public void Implements_IToggleProvider() + { + var target = new ToggleSplitButton(); + var peer = ControlAutomationPeer.CreatePeerForElement(target); + + Assert.IsAssignableFrom(peer); + } + + [Fact] + public void ControlType_Is_SplitButton() + { + var target = new ToggleSplitButton(); + var peer = (SplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal(AutomationControlType.SplitButton, peer.GetAutomationControlType()); + } + + [Fact] + public void ClassName_Is_ToggleSplitButton() + { + var target = new ToggleSplitButton(); + var peer = (ToggleSplitButtonAutomationPeer)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal("ToggleSplitButton", peer.GetClassName()); + } + + [Fact] + public void Toggle_Changes_IsChecked_And_Fires_Click() + { + var clicked = 0; + var target = new ToggleSplitButton(); + var peer = (IToggleProvider)ControlAutomationPeer.CreatePeerForElement(target); + + Assert.Equal(ToggleState.Off, peer.ToggleState); + + target.Click += (_, _) => clicked++; + peer.Toggle(); + + Assert.True(target.IsChecked); + Assert.Equal(ToggleState.On, peer.ToggleState); + Assert.Equal(1, clicked); + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index a92ba0dc83..4fbbd4c4ab 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -21,6 +21,7 @@ +