diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Stepper_ChangeFlowDirection_RTL_VerifyVisualState.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Stepper_ChangeFlowDirection_RTL_VerifyVisualState.png
new file mode 100644
index 000000000000..4834c3caed21
Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/Stepper_ChangeFlowDirection_RTL_VerifyVisualState.png differ
diff --git a/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs b/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs
index ae058bc9bc44..ff9d36abbaf8 100644
--- a/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs
+++ b/src/Controls/tests/TestCases.HostApp/CoreViews/CorePageView.cs
@@ -71,6 +71,7 @@ public override string ToString()
new GalleryPageFactory(() => new ScrollViewCoreGalleryPage(), "ScrollView Gallery"),
new GalleryPageFactory(() => new SearchBarCoreGalleryPage(), "Search Bar Gallery"),
new GalleryPageFactory(() => new SliderCoreGalleryPage(), "Slider Gallery"),
+ new GalleryPageFactory(() => new StepperControlPage(), "Stepper Feature Matrix"),
new GalleryPageFactory(() => new StepperCoreGalleryPage(), "Stepper Gallery"),
new GalleryPageFactory(() => new SwitchCoreGalleryPage(), "Switch Gallery"),
new GalleryPageFactory(() => new SwipeViewCoreGalleryPage(), "SwipeView Gallery"),
@@ -79,7 +80,6 @@ public override string ToString()
new GalleryPageFactory(() => new SliderControlPage(), "Slider Feature Matrix"),
new GalleryPageFactory(() => new CollectionViewFeaturePage(), "CollectionView Feature Matrix"),
new GalleryPageFactory(() => new CarouselViewFeaturePage(), "CarouselView Feature Matrix"),
-
};
public CorePageView(Page rootPage)
diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperControlPage.xaml b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperControlPage.xaml
new file mode 100644
index 000000000000..fb58ba4e1fb9
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperControlPage.xaml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperControlPage.xaml.cs b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperControlPage.xaml.cs
new file mode 100644
index 000000000000..5836c5ccb0e9
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperControlPage.xaml.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.Maui.Controls;
+
+namespace Maui.Controls.Sample;
+
+public class StepperControlPage : NavigationPage
+{
+
+ public StepperControlPage()
+ {
+ PushAsync(new StepperControlMainPage());
+ }
+}
+public partial class StepperControlMainPage : ContentPage
+{
+ private StepperViewModel _viewModel;
+
+ public StepperControlMainPage()
+ {
+ InitializeComponent();
+ BindingContext = _viewModel = new StepperViewModel();
+ }
+
+ private async void NavigateToOptionsPage_Clicked(object sender, EventArgs e)
+ {
+ BindingContext = _viewModel = new StepperViewModel();
+ await Navigation.PushAsync(new StepperFeaturePage(_viewModel));
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperFeaturePage.xaml b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperFeaturePage.xaml
new file mode 100644
index 000000000000..fc92f4df2342
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperFeaturePage.xaml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperFeaturePage.xaml.cs b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperFeaturePage.xaml.cs
new file mode 100644
index 000000000000..d53c97f04004
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperFeaturePage.xaml.cs
@@ -0,0 +1,68 @@
+using System;
+using Microsoft.Maui.Controls;
+
+namespace Maui.Controls.Sample;
+
+public partial class StepperFeaturePage : ContentPage
+{
+ private StepperViewModel _viewModel;
+
+ public StepperFeaturePage(StepperViewModel viewModel)
+ {
+ InitializeComponent();
+ _viewModel = viewModel;
+ BindingContext = _viewModel;
+ }
+
+ private void ApplyButton_Clicked(object sender, EventArgs e)
+ {
+ Navigation.PopAsync();
+ }
+
+ private void OnMinimumChanged(object sender, TextChangedEventArgs e)
+ {
+ if (double.TryParse(MinimumEntry.Text, out double min))
+ {
+ _viewModel.Minimum = min;
+ }
+ }
+
+ private void OnMaximumChanged(object sender, TextChangedEventArgs e)
+ {
+ if (double.TryParse(MaximumEntry.Text, out double max))
+ {
+ _viewModel.Maximum = max;
+ }
+ }
+
+ private void OnIncrementChanged(object sender, TextChangedEventArgs e)
+ {
+ if (double.TryParse(IncrementEntry.Text, out double increment))
+ {
+ _viewModel.Increment = increment;
+ }
+ }
+
+ private void OnValueChanged(object sender, TextChangedEventArgs e)
+ {
+ if (double.TryParse(ValueEntry.Text, out double value))
+ {
+ _viewModel.Value = value;
+ }
+ }
+
+ private void OnIsEnabledCheckedChanged(object sender, CheckedChangedEventArgs e)
+ {
+ _viewModel.IsEnabled = IsEnabledTrueRadio.IsChecked;
+ }
+
+ private void OnIsVisibleCheckedChanged(object sender, CheckedChangedEventArgs e)
+ {
+ _viewModel.IsVisible = IsVisibleTrueRadio.IsChecked;
+ }
+
+ private void OnFlowDirectionChanged(object sender, CheckedChangedEventArgs e)
+ {
+ _viewModel.FlowDirection = FlowDirectionLTRRadio.IsChecked ? FlowDirection.LeftToRight : FlowDirection.RightToLeft;
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperViewModel.cs b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperViewModel.cs
new file mode 100644
index 000000000000..e51ff5f6fdfa
--- /dev/null
+++ b/src/Controls/tests/TestCases.HostApp/FeatureMatrix/Stepper/StepperViewModel.cs
@@ -0,0 +1,68 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Maui.Controls.Sample;
+
+public class StepperViewModel : INotifyPropertyChanged
+{
+ private double _minimum = 0;
+ private double _maximum = 10;
+ private double _increment = 1;
+ private double _value = 0;
+ private bool _isEnabled = true;
+ private bool _isVisible = true;
+ private FlowDirection _flowDirection = FlowDirection.LeftToRight;
+
+ public double Minimum
+ {
+ get => _minimum;
+ set => SetProperty(ref _minimum, value);
+ }
+
+ public double Maximum
+ {
+ get => _maximum;
+ set => SetProperty(ref _maximum, value);
+ }
+
+ public double Increment
+ {
+ get => _increment;
+ set => SetProperty(ref _increment, value);
+ }
+
+ public double Value
+ {
+ get => _value;
+ set => SetProperty(ref _value, value);
+ }
+
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set => SetProperty(ref _isEnabled, value);
+ }
+
+ public bool IsVisible
+ {
+ get => _isVisible;
+ set => SetProperty(ref _isVisible, value);
+ }
+
+ public FlowDirection FlowDirection
+ {
+ get => _flowDirection;
+ set => SetProperty(ref _flowDirection, value);
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected void SetProperty(ref T backingStore, T value, [CallerMemberName] string propertyName = "")
+ {
+ if (EqualityComparer.Default.Equals(backingStore, value))
+ return;
+
+ backingStore = value;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/StepperFeatureTests.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/StepperFeatureTests.cs
new file mode 100644
index 000000000000..20f23edbba98
--- /dev/null
+++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/FeatureMatrix/StepperFeatureTests.cs
@@ -0,0 +1,348 @@
+using NUnit.Framework;
+using UITest.Appium;
+using UITest.Core;
+
+namespace Microsoft.Maui.TestCases.Tests;
+
+[Category(UITestCategories.Stepper)]
+public class StepperFeatureTests : UITest
+{
+ public const string StepperFeatureMatrix = "Stepper Feature Matrix";
+
+ public StepperFeatureTests(TestDevice device)
+ : base(device)
+ {
+ }
+
+ protected override void FixtureSetup()
+ {
+ base.FixtureSetup();
+ App.NavigateToGallery(StepperFeatureMatrix);
+ }
+
+ [Test, Order(1)]
+ public void Stepper_ValidateDefaultValues_VerifyLabels()
+ {
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("MinimumLabel").GetText(), Is.EqualTo("0.00"));
+ Assert.That(App.FindElement("MaximumLabel").GetText(), Is.EqualTo("10.00"));
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("0.00"));
+ }
+
+ [Test]
+ public void Stepper_SetMinimumValue_VerifyMinimumLabel()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("MinimumEntry");
+ App.EnterText("MinimumEntry", "2");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("MinimumLabel").GetText(), Is.EqualTo("2.00"));
+ }
+
+ [Test]
+ public void Stepper_SetMaximumValue_VerifyMaximumLabel()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("MaximumEntry");
+ App.EnterText("MaximumEntry", "20");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("MaximumLabel").GetText(), Is.EqualTo("20.00"));
+ }
+
+ [Test]
+ public void Stepper_SetValueWithinRange_VerifyValueLabel()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("ValueEntry");
+ App.EnterText("ValueEntry", "5");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("5.00"));
+ }
+
+ [Test]
+ public void Stepper_SetIncrementValue_VerifyIncrement()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("IncrementEntry");
+ App.EnterText("IncrementEntry", "2");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ App.IncreaseStepper("StepperControl");
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("2.00"));
+ }
+
+ [Test]
+ public void Stepper_SetValueExceedsMaximum()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("MaximumEntry");
+ App.ClearText("MaximumEntry");
+ App.EnterText("MaximumEntry", "100");
+ App.PressEnter();
+ App.ClearText("ValueEntry");
+ App.EnterText("ValueEntry", "200");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("100.00"));
+ }
+
+#if TEST_FAILS_ON_ANDROID && TEST_FAILS_ON_CATALYST && TEST_FAILS_ON_IOS && TEST_FAILS_ON_WINDOWS
+// Related Issue Link : https://github.com/dotnet/maui/issues/12243
+ [Test]
+ public void Stepper_SetValueBelowMinimum()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("MinimumEntry");
+ App.ClearText("MinimumEntry");
+ App.EnterText("MinimumEntry", "10");
+ App.PressEnter();
+ App.ClearText("ValueEntry");
+ App.EnterText("ValueEntry", "5");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("10.00"));
+ }
+
+ [Test]
+ public void Stepper_MinimumExceedsMaximum_SetsMinimumToMaximum()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("MinimumEntry");
+ App.ClearText("MinimumEntry");
+ App.EnterText("MinimumEntry", "50");
+ App.PressEnter();
+ App.ClearText("MaximumEntry");
+ App.EnterText("MaximumEntry", "25");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("MinimumLabel").GetText(), Is.EqualTo("50.00"));
+ Assert.That(App.FindElement("MaximumLabel").GetText(), Is.EqualTo("50.00"));
+ }
+#endif
+
+ [Test]
+ public void Stepper_SetEnabledStateToFalse_VerifyVisualState()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("IsEnabledFalseRadio");
+ App.Tap("IsEnabledFalseRadio");
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ App.IncreaseStepper("StepperControl");
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("0.00"));
+ }
+
+ [Test]
+ public void Stepper_SetVisibilityToFalse_VerifyVisualState()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("IsVisibleFalseRadio");
+ App.Tap("IsVisibleFalseRadio");
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ App.WaitForNoElement("StepperControl");
+ }
+
+#if TEST_FAILS_ON_IOS && TEST_FAILS_ON_CATALYST //Related Issue Link : https://github.com/dotnet/maui/issues/29704
+ [Test]
+ public void Stepper_ChangeFlowDirection_RTL_VerifyVisualState()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("FlowDirectionRTLRadio");
+ App.Tap("FlowDirectionRTLRadio");
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ VerifyScreenshot();
+ }
+#endif
+ [Test]
+ public void Stepper_AtMinimumValue_DecrementButtonDisabled()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+
+ App.WaitForElement("MinimumEntry");
+ App.ClearText("MinimumEntry");
+ App.EnterText("MinimumEntry", "10");
+ App.PressEnter();
+
+ App.WaitForElement("ValueEntry");
+ App.ClearText("ValueEntry");
+ App.EnterText("ValueEntry", "10");
+ App.PressEnter();
+
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+
+ App.WaitForElement("Options");
+
+ var currentValue = App.FindElement("ValueLabel").GetText();
+ Assert.That(currentValue, Is.EqualTo("10.00"));
+
+ App.DecreaseStepper("StepperControl");
+
+ var newValue = App.FindElement("ValueLabel").GetText();
+ Assert.That(newValue, Is.EqualTo("10.00"));
+ }
+
+ [Test]
+ public void Stepper_AtMaximumValue_IncrementButtonDisabled()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+
+ App.WaitForElement("MaximumEntry");
+ App.EnterText("MaximumEntry", "10");
+ App.PressEnter();
+
+ App.WaitForElement("ValueEntry");
+ App.EnterText("ValueEntry", "10");
+ App.PressEnter();
+
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+
+ App.WaitForElement("Options");
+
+ var currentValue = App.FindElement("ValueLabel").GetText();
+ Assert.That(currentValue, Is.EqualTo("10.00"));
+
+ App.IncreaseStepper("StepperControl");
+
+ var newValue = App.FindElement("ValueLabel").GetText();
+ Assert.That(newValue, Is.EqualTo("10.00"));
+ }
+
+ [Test]
+ public void Stepper_SetIncrementAndVerifyValueChange()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("IncrementEntry");
+ App.EnterText("IncrementEntry", "5");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ App.IncreaseStepper("StepperControl");
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("5.00"));
+ App.IncreaseStepper("StepperControl");
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("10.00"));
+ }
+
+
+ [Test]
+ public void Stepper_ResetToInitialState_VerifyDefaultValues()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("MinimumEntry");
+ App.EnterText("MinimumEntry", "10");
+ App.PressEnter();
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ App.Tap("Options");
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+ App.WaitForElement("Options");
+ Assert.That(App.FindElement("MinimumLabel").GetText(), Is.EqualTo("0.00"));
+ Assert.That(App.FindElement("MaximumLabel").GetText(), Is.EqualTo("10.00"));
+ Assert.That(App.FindElement("ValueLabel").GetText(), Is.EqualTo("0.00"));
+ }
+
+#if TEST_FAILS_ON_WINDOWS // Related Issue Link : https://github.com/dotnet/maui/issues/29740
+ [Test]
+ public void Stepper_IncrementDoesNotExceedMaximum()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+
+ App.WaitForElement("MaximumEntry");
+ App.ClearText("MaximumEntry");
+ App.EnterText("MaximumEntry", "10");
+ App.PressEnter();
+
+ App.WaitForElement("IncrementEntry");
+ App.ClearText("IncrementEntry");
+ App.EnterText("IncrementEntry", "3");
+ App.PressEnter();
+
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+
+ App.WaitForElement("Options");
+
+ App.IncreaseStepper("StepperControl");
+ App.IncreaseStepper("StepperControl");
+ App.IncreaseStepper("StepperControl");
+ App.IncreaseStepper("StepperControl");
+
+ var currentValue = App.FindElement("ValueLabel").GetText();
+ Assert.That(currentValue, Is.EqualTo("10.00"));
+ }
+#endif
+
+ [Test]
+ public void Stepper_DecrementDoesNotGoBelowMinimum()
+ {
+ App.WaitForElement("Options");
+ App.Tap("Options");
+
+ App.WaitForElement("MinimumEntry");
+ App.ClearText("MinimumEntry");
+ App.EnterText("MinimumEntry", "0");
+ App.PressEnter();
+
+ App.WaitForElement("IncrementEntry");
+ App.ClearText("IncrementEntry");
+ App.EnterText("IncrementEntry", "2");
+ App.PressEnter();
+
+ App.WaitForElement("ValueEntry");
+ App.ClearText("ValueEntry");
+ App.EnterText("ValueEntry", "2");
+ App.PressEnter();
+
+ App.WaitForElement("Apply");
+ App.Tap("Apply");
+
+ App.WaitForElement("Options");
+
+ App.DecreaseStepper("StepperControl");
+ App.DecreaseStepper("StepperControl");
+
+ var currentValue = App.FindElement("ValueLabel").GetText();
+ Assert.That(currentValue, Is.EqualTo("0.00"));
+ }
+}
\ No newline at end of file
diff --git a/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Stepper_ChangeFlowDirection_RTL_VerifyVisualState.png b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Stepper_ChangeFlowDirection_RTL_VerifyVisualState.png
new file mode 100644
index 000000000000..56f3f4511f18
Binary files /dev/null and b/src/Controls/tests/TestCases.WinUI.Tests/snapshots/windows/Stepper_ChangeFlowDirection_RTL_VerifyVisualState.png differ