diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AppThemeShouldChangeDarkTheme.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AppThemeShouldChangeDarkTheme.png new file mode 100644 index 000000000000..66c4af527fd2 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AppThemeShouldChangeDarkTheme.png differ diff --git a/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AppThemeShouldChangeLightTheme.png b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AppThemeShouldChangeLightTheme.png new file mode 100644 index 000000000000..c91623cc6593 Binary files /dev/null and b/src/Controls/tests/TestCases.Android.Tests/snapshots/android/AppThemeShouldChangeLightTheme.png differ diff --git a/src/Controls/tests/TestCases.HostApp/Issues/ThemeChange.xaml b/src/Controls/tests/TestCases.HostApp/Issues/ThemeChange.xaml new file mode 100644 index 000000000000..5721a4e8e688 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/ThemeChange.xaml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Issues/ThemeChange.xaml.cs b/src/Controls/tests/TestCases.HostApp/Issues/ThemeChange.xaml.cs new file mode 100644 index 000000000000..867f458ac0d5 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/ThemeChange.xaml.cs @@ -0,0 +1,15 @@ +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Maui.Controls.Sample.Issues +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + [Issue(IssueTracker.None, 2, "UI theme change during the runtime", PlatformAffected.Android | PlatformAffected.iOS)] + public partial class ThemeChange : ContentPage + { + public ThemeChange() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/ThemeChange.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/ThemeChange.cs new file mode 100644 index 000000000000..be0ba5dc1137 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/ThemeChange.cs @@ -0,0 +1,40 @@ +#if ANDROID || IOS +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues +{ + public class ThemeChange : _IssuesUITest + { + public override string Issue => "UI theme change during the runtime"; + + public ThemeChange(TestDevice device) : base(device) + { + } + + [Test] + [Category(UITestCategories.LifeCycle)] + public void AppThemeShouldChange() + { + try + { + App.SetLightTheme(); + _ = App.WaitForElement("labelVisibleOnlyInLightMode"); + + App.SetDarkTheme(); + _ = App.WaitForElement("labelVisibleOnlyInDarkMode"); + VerifyScreenshot("AppThemeShouldChangeDarkTheme"); + + App.SetLightTheme(); + _ = App.WaitForElement("labelVisibleOnlyInLightMode"); + VerifyScreenshot("AppThemeShouldChangeLightTheme"); + } + finally + { + App.SetLightTheme(); + } + } + } +} +#endif \ No newline at end of file diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/AppThemeShouldChangeDarkTheme.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/AppThemeShouldChangeDarkTheme.png new file mode 100644 index 000000000000..02f9c00d798c Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/AppThemeShouldChangeDarkTheme.png differ diff --git a/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/AppThemeShouldChangeLightTheme.png b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/AppThemeShouldChangeLightTheme.png new file mode 100644 index 000000000000..e15dc869a173 Binary files /dev/null and b/src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/AppThemeShouldChangeLightTheme.png differ diff --git a/src/TestUtils/src/UITest.Appium/Actions/AppiumAndroidThemeChangeAction.cs b/src/TestUtils/src/UITest.Appium/Actions/AppiumAndroidThemeChangeAction.cs new file mode 100644 index 000000000000..ecb720e359e7 --- /dev/null +++ b/src/TestUtils/src/UITest.Appium/Actions/AppiumAndroidThemeChangeAction.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using UITest.Core; + +namespace UITest.Appium +{ + public class AppiumAndroidThemeChangeAction : ICommandExecutionGroup + { + const string SetLightTheme = "setLightTheme"; + const string SetDarkTheme = "setDarkTheme"; + + readonly List _commands = new() + { + SetLightTheme, + SetDarkTheme + }; + + public CommandResponse Execute(string commandName, IDictionary parameters) + { + if (commandName == SetLightTheme) + { + ExecuteAdbCommand($"adb shell cmd uimode night no"); + return CommandResponse.SuccessEmptyResponse; + } + else if (commandName == SetDarkTheme) + { + ExecuteAdbCommand($"adb shell cmd uimode night yes"); + return CommandResponse.SuccessEmptyResponse; + } + + return CommandResponse.FailedEmptyResponse; + } + + public bool IsCommandSupported(string commandName) + { + return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase); + } + + private static void ExecuteAdbCommand(string command) + { + var shell = GetShell(); + var shellArgument = GetShellArgument(shell, command); + + var processInfo = new ProcessStartInfo(shell, shellArgument) + { + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + var process = new Process { StartInfo = processInfo }; + + process.Start(); + process.WaitForExit(); + } + + private static string GetShell() + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + return "cmd.exe"; + else + return "/bin/bash"; + } + + private static string GetShellArgument(string shell, string command) + { + if (shell == "cmd.exe") + return $"/C {command}"; + else + return $"-c \"{command}\""; + } + } +} diff --git a/src/TestUtils/src/UITest.Appium/Actions/AppiumIOSThemeChangeAction.cs b/src/TestUtils/src/UITest.Appium/Actions/AppiumIOSThemeChangeAction.cs new file mode 100644 index 000000000000..73c5b57f6ad1 --- /dev/null +++ b/src/TestUtils/src/UITest.Appium/Actions/AppiumIOSThemeChangeAction.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using UITest.Core; + +namespace UITest.Appium +{ + public class AppiumIOSThemeChangeAction : ICommandExecutionGroup + { + const string SetLightTheme = "setLightTheme"; + const string SetDarkTheme = "setDarkTheme"; + + protected readonly AppiumApp _app; + + public AppiumIOSThemeChangeAction(AppiumApp app) + { + _app = app; + } + + readonly List _commands = new() + { + SetLightTheme, + SetDarkTheme + }; + + public CommandResponse Execute(string commandName, IDictionary parameters) + { + if (commandName == SetLightTheme) + { + var args = new Dictionary { { "style", "light" } }; + _app.Driver.ExecuteScript("mobile: setAppearance", args); + return CommandResponse.SuccessEmptyResponse; + } + else if(commandName == SetDarkTheme) + { + var args = new Dictionary { { "style", "dark" } }; + _app.Driver.ExecuteScript("mobile: setAppearance", args); + return CommandResponse.SuccessEmptyResponse; + } + + return CommandResponse.FailedEmptyResponse; + } + + public bool IsCommandSupported(string commandName) + { + return _commands.Contains(commandName, StringComparer.OrdinalIgnoreCase); + } + } +} + diff --git a/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs b/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs index 6200d100fbe1..b1b4def20a45 100644 --- a/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs +++ b/src/TestUtils/src/UITest.Appium/AppiumAndroidApp.cs @@ -10,6 +10,7 @@ public class AppiumAndroidApp : AppiumApp, IAndroidApp private AppiumAndroidApp(Uri remoteAddress, IConfig config) : base(new AndroidDriver(remoteAddress, GetOptions(config)), config) { + _commandExecutor.AddCommandGroup(new AppiumAndroidThemeChangeAction()); _commandExecutor.AddCommandGroup(new AppiumAndroidVirtualKeyboardActions(this)); _commandExecutor.AddCommandGroup(new AppiumAndroidAlertActions(this)); } diff --git a/src/TestUtils/src/UITest.Appium/AppiumIOSApp.cs b/src/TestUtils/src/UITest.Appium/AppiumIOSApp.cs index 29ffba9ad99c..56c707dc6c63 100644 --- a/src/TestUtils/src/UITest.Appium/AppiumIOSApp.cs +++ b/src/TestUtils/src/UITest.Appium/AppiumIOSApp.cs @@ -14,7 +14,9 @@ public AppiumIOSApp(Uri remoteAddress, IConfig config) _commandExecutor.AddCommandGroup(new AppiumIOSMouseActions(this)); _commandExecutor.AddCommandGroup(new AppiumIOSTouchActions(this)); _commandExecutor.AddCommandGroup(new AppiumIOSVirtualKeyboardActions(this)); + _commandExecutor.AddCommandGroup(new AppiumIOSThemeChangeAction(this)); _commandExecutor.AddCommandGroup(new AppiumIOSAlertActions(this)); + _commandExecutor.AddCommandGroup(new AppiumIOSThemeChangeAction(this)); } public override ApplicationState AppState diff --git a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs index 8822c3cebf9d..5bd1060d55d3 100644 --- a/src/TestUtils/src/UITest.Appium/HelperExtensions.cs +++ b/src/TestUtils/src/UITest.Appium/HelperExtensions.cs @@ -831,6 +831,34 @@ public static TestDevice GetTestDevice(this IApp app) return aaa.Config.GetProperty("TestDevice"); } + /// + /// Sets light device's theme + /// + /// Represents the main gateway to interact with an app. + public static void SetLightTheme(this IApp app) + { + if (app is not AppiumAndroidApp && app is not AppiumIOSApp) + { + throw new InvalidOperationException($"SetLightTheme is not supported"); + } + + app.CommandExecutor.Execute("setLightTheme", ImmutableDictionary.Empty); + } + + /// + /// Sets dark device's theme + /// + /// Represents the main gateway to interact with an app. + public static void SetDarkTheme(this IApp app) + { + if (app is not AppiumAndroidApp && app is not AppiumIOSApp) + { + throw new InvalidOperationException($"SetDarkTheme is not supported"); + } + + app.CommandExecutor.Execute("setDarkTheme", ImmutableDictionary.Empty); + } + /// /// Check if element has focused ///