diff --git a/appium-dotnet-driver/Appium/Android/AndroidStartScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/Android/AndroidStartScreenRecordingOptions.cs new file mode 100644 index 00000000..c55c2b49 --- /dev/null +++ b/appium-dotnet-driver/Appium/Android/AndroidStartScreenRecordingOptions.cs @@ -0,0 +1,72 @@ +using System; +using OpenQA.Selenium.Appium.ScreenRecording; + +namespace OpenQA.Selenium.Appium.Android +{ + public class AndroidStartScreenRecordingOptions : BaseStartScreenRecordingOptions + { + public static AndroidStartScreenRecordingOptions GetAndroidStartScreenRecordingOptions() + { + return new AndroidStartScreenRecordingOptions(); + } + + /// + /// The video bit rate for the video, in megabits per second. + /// The default value is 4000000 (4 Mb/s) for Android API level below 27 + /// and 20000000 (20 Mb/s) for API level 27 and above. + /// You can increase the bit rate to improve video quality, + /// but doing so results in larger movie files. + /// + /// The actual bit rate (Mb/s). + /// self instance for chaining. + public AndroidStartScreenRecordingOptions WithBitRate(int bitRate) + { + Parameters["bitRate"] = bitRate; + return this; + } + + /// + /// The video size of the generated media file. The format is WIDTHxHEIGHT. + /// The default value is the device's native display resolution (if supported), + /// 1280x720 if not. For best results, + /// use a size supported by your device's Advanced Video Coding (AVC) encoder. + /// + /// The actual video size: WIDTHxHEIGHT. + /// self instance for chaining. + public AndroidStartScreenRecordingOptions WithVideoSize(string videoSize) + { + Parameters["videoSize"] = videoSize; + return this; + } + + /// + /// Makes the recorder to display an additional information on the video overlay, + /// such as a timestamp, that is helpful in videos captured to illustrate bugs. + /// This option is only supported since API level 27 (Android P). + /// + /// self instance for chaining. + public AndroidStartScreenRecordingOptions EnableBugReport() + { + Parameters["isBugReportEnabled"] = true; + return this; + } + + /// + /// The maximum recording time.The default and maximum value is 180 seconds (3 minutes). + /// Setting values greater than this or less than zero will cause an exception. The minimum + /// time resolution unit is one second. + /// + /// Since Appium 1.8.2 the time limit can be up to 1800 seconds (30 minutes). + /// Appium will automatically try to merge the 3-minutes chunks recorded + /// by the screenrecord utility, however, this requires FFMPEG utility + /// to be installed and available in PATH on the server machine. If the utility is not + /// present then the most recent screen recording chunk is going to be returned as the result. + /// + /// The actual time limit of the recorded video. + /// self instance for chaining. + public new AndroidStartScreenRecordingOptions WithTimeLimit(TimeSpan timeLimit) + { + return base.WithTimeLimit(timeLimit); + } + } +} diff --git a/appium-dotnet-driver/Appium/Android/AndroidStopScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/Android/AndroidStopScreenRecordingOptions.cs new file mode 100644 index 00000000..39f69ece --- /dev/null +++ b/appium-dotnet-driver/Appium/Android/AndroidStopScreenRecordingOptions.cs @@ -0,0 +1,12 @@ +using OpenQA.Selenium.Appium.ScreenRecording; + +namespace OpenQA.Selenium.Appium.Android +{ + public class AndroidStopScreenRecordingOptions : BaseStopScreenRecordingOptions + { + public static AndroidStopScreenRecordingOptions StopScreenRecordingOptions() + { + return new AndroidStopScreenRecordingOptions(); + } + } +} diff --git a/appium-dotnet-driver/Appium/AppiumCommand.cs b/appium-dotnet-driver/Appium/AppiumCommand.cs index 691594c2..0be5d7d1 100644 --- a/appium-dotnet-driver/Appium/AppiumCommand.cs +++ b/appium-dotnet-driver/Appium/AppiumCommand.cs @@ -151,10 +151,17 @@ public class AppiumCommand #region SeassionData - new AppiumCommand(CommandInfo.GetCommand, AppiumDriverCommand.GetSession, "/session/{sessionId}/") + new AppiumCommand(CommandInfo.GetCommand, AppiumDriverCommand.GetSession, "/session/{sessionId}/"), #endregion SeassionData + #region Recording Screen + + new AppiumCommand(CommandInfo.PostCommand, AppiumDriverCommand.StartRecordingScreen, "/session/{sessionId}/appium/start_recording_screen"), + new AppiumCommand(CommandInfo.PostCommand, AppiumDriverCommand.StopRecordingScreen, "/session/{sessionId}/appium/stop_recording_screen") + + #endregion Recording Screen + #endregion JSON Wire Protocol Commands }; diff --git a/appium-dotnet-driver/Appium/AppiumDriver.cs b/appium-dotnet-driver/Appium/AppiumDriver.cs index 6142d08b..d15d5e60 100644 --- a/appium-dotnet-driver/Appium/AppiumDriver.cs +++ b/appium-dotnet-driver/Appium/AppiumDriver.cs @@ -23,6 +23,8 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Castle.Core.Internal; +using OpenQA.Selenium.Appium.ScreenRecording; namespace OpenQA.Selenium.Appium { @@ -429,6 +431,26 @@ public object GetSessionDetail(string detail) #endregion Session Data + #region Recording Screen + + public string StartRecordingScreen() => ((IExecuteMethod)this).Execute(AppiumDriverCommand.StartRecordingScreen).Value.ToString(); + + public string StartRecordingScreen(IScreenRecordingOptions options) + { + var parameters = new Dictionary {{"options", options.GetParameters()} }; + return Execute(AppiumDriverCommand.StartRecordingScreen, parameters).Value.ToString(); + } + + public string StopRecordingScreen() => ((IExecuteMethod)this).Execute(AppiumDriverCommand.StopRecordingScreen).Value.ToString(); + + public string StopRecordingScreen(IScreenRecordingOptions options) + { + var parameters = new Dictionary { { "options", options.GetParameters() } }; + return Execute(AppiumDriverCommand.StopRecordingScreen, parameters).Value.ToString(); + } + + #endregion Recording Screen + #endregion Public Methods #region Support methods diff --git a/appium-dotnet-driver/Appium/AppiumDriverCommand.cs b/appium-dotnet-driver/Appium/AppiumDriverCommand.cs index fc02a3b5..dbba80b9 100644 --- a/appium-dotnet-driver/Appium/AppiumDriverCommand.cs +++ b/appium-dotnet-driver/Appium/AppiumDriverCommand.cs @@ -270,6 +270,10 @@ public class AppiumDriverCommand public const string GetSession = "getSession"; + public const string StartRecordingScreen = "startRecordingScreen"; + + public const string StopRecordingScreen = "stopRecordingScreen"; + #endregion JSON Wire Protocol } } \ No newline at end of file diff --git a/appium-dotnet-driver/Appium/Interfaces/IScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/Interfaces/IScreenRecordingOptions.cs new file mode 100644 index 00000000..a703673a --- /dev/null +++ b/appium-dotnet-driver/Appium/Interfaces/IScreenRecordingOptions.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace OpenQA.Selenium.Appium.Interfaces +{ + public interface IScreenRecordingOptions + { + Dictionary GetParameters(); + } +} \ No newline at end of file diff --git a/appium-dotnet-driver/Appium/ScreenRecording/BaseScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/ScreenRecording/BaseScreenRecordingOptions.cs new file mode 100644 index 00000000..15c20111 --- /dev/null +++ b/appium-dotnet-driver/Appium/ScreenRecording/BaseScreenRecordingOptions.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using OpenQA.Selenium.Appium.Interfaces; + +namespace OpenQA.Selenium.Appium.ScreenRecording +{ + public abstract class BaseScreenRecordingOptions : IScreenRecordingOptions where T : BaseScreenRecordingOptions + { + protected Dictionary Parameters; + + protected BaseScreenRecordingOptions() + { + Parameters = new Dictionary(); + } + + /// + /// Upload options set for the recorded screen capture. + /// + /// Upload options + /// + public T WithUploadOptions(ScreenRecordingUploadOptions uploadOptions) + { + foreach (var parameter in uploadOptions.GetParameters()) + { + Parameters[parameter.Key] = parameter.Value; + } + + return (T) this; + } + + /// + /// Get all setted parameters + /// + /// + public Dictionary GetParameters() + { + return Parameters; + } + } +} diff --git a/appium-dotnet-driver/Appium/ScreenRecording/BaseStartScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/ScreenRecording/BaseStartScreenRecordingOptions.cs new file mode 100644 index 00000000..7c34b105 --- /dev/null +++ b/appium-dotnet-driver/Appium/ScreenRecording/BaseStartScreenRecordingOptions.cs @@ -0,0 +1,49 @@ +using System; + +namespace OpenQA.Selenium.Appium.ScreenRecording +{ + public abstract class BaseStartScreenRecordingOptions : BaseScreenRecordingOptions where T : BaseStartScreenRecordingOptions + { + /// + /// The maximum recording time. + /// + /// The actual time limit of the recorded video. + /// self instance for chaining. + public T WithTimeLimit(TimeSpan timeLimit) + { + Parameters["timeLimit"] = (int) timeLimit.TotalSeconds; + return (T) this; + } + + /// + /// Whether to ignore the result of previous capture and start a new recording + /// immediately. By default the endpoint will try to catch and return the result of + /// the previous capture if it's still available. + /// + /// self instance for chaining. + public T EnableForcedRestart() + { + Parameters["forceRestart"] = true; + return (T) this; + } + + /// + /// The remotePath upload option is the path to the remote location, + /// where the resulting video should be uploaded. + /// The following protocols are supported: http/https (multipart), ftp. + /// + /// Missing value (the default setting) means the content of the resulting + /// file should be encoded as Base64 and passed as the endpoint response value, but + /// an exception will be thrown if the generated media file is too big to + /// fit into the available process memory. + /// This option only has an effect if there is a screen recording session in progress + /// and forced restart is not enabled (the default setting). + /// + /// Upload options + /// self instance for chaining. + public new T WithUploadOptions(ScreenRecordingUploadOptions uploadOptions) + { + return base.WithUploadOptions(uploadOptions); + } + } +} diff --git a/appium-dotnet-driver/Appium/ScreenRecording/BaseStopScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/ScreenRecording/BaseStopScreenRecordingOptions.cs new file mode 100644 index 00000000..83c9521d --- /dev/null +++ b/appium-dotnet-driver/Appium/ScreenRecording/BaseStopScreenRecordingOptions.cs @@ -0,0 +1,21 @@ +namespace OpenQA.Selenium.Appium.ScreenRecording +{ + public abstract class BaseStopScreenRecordingOptions : BaseScreenRecordingOptions where T : BaseStopScreenRecordingOptions + { + /// + /// The remotePath upload option is the path to the remote location, + /// where the resulting video should be uploaded. + /// The following protocols are supported: http/https (multipart), ftp. + /// Missing value (the default setting) means the content of resulting + /// file should be encoded as Base64 and passed as the endpoint response value, but + /// an exception will be thrown if the generated media file is too big to + /// fit into the available process memory. + /// + /// Upload options + /// self instance for chaining. + public new T WithUploadOptions(ScreenRecordingUploadOptions uploadOptions) + { + return base.WithUploadOptions(uploadOptions); + } + } +} diff --git a/appium-dotnet-driver/Appium/ScreenRecording/ScreenRecordingUploadOptions.cs b/appium-dotnet-driver/Appium/ScreenRecording/ScreenRecordingUploadOptions.cs new file mode 100644 index 00000000..aec5b34b --- /dev/null +++ b/appium-dotnet-driver/Appium/ScreenRecording/ScreenRecordingUploadOptions.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; + +namespace OpenQA.Selenium.Appium.ScreenRecording +{ + public class ScreenRecordingUploadOptions + { + protected Dictionary Parameters; + + /// + /// The path to the remote location, where the resulting video should be uploaded. + /// + /// The path to a writable remote location. + /// self instance for chaining. + public ScreenRecordingUploadOptions WithRemotePath(string remotePath) + { + Parameters["remotePath"] = remotePath; + return this; + } + + /// + /// Sets the credentials for remote ftp/http authentication (if needed). + /// This option only has an effect if remotePath is provided. + /// + /// The name of the user for the remote authentication. + /// The password for the remote authentication. + /// self instance for chaining. + public ScreenRecordingUploadOptions WithAuthCredentials(string user, string pass) + { + Parameters["user"] = user; + Parameters["pass"] = pass; + return this; + } + + public enum RequestMethod + { + POST, PUT + } + + /// + /// Sets the method name for http(s) upload. PUT is used by default. + /// This option only has an effect if remotePath is provided. + /// + /// method The HTTP method name. + /// self instance for chaining. + public ScreenRecordingUploadOptions WithHttpMethod(RequestMethod method) + { + Parameters["method"] = method.ToString(); + return this; + } + + /// + /// Get all setted parameters + /// + /// + public Dictionary GetParameters() + { + return Parameters; + } + } +} diff --git a/appium-dotnet-driver/Appium/iOS/IOSStartScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/iOS/IOSStartScreenRecordingOptions.cs new file mode 100644 index 00000000..69ed2de2 --- /dev/null +++ b/appium-dotnet-driver/Appium/iOS/IOSStartScreenRecordingOptions.cs @@ -0,0 +1,61 @@ +using System; +using OpenQA.Selenium.Appium.ScreenRecording; + +namespace OpenQA.Selenium.Appium.iOS +{ + public class IOSStartScreenRecordingOptions : BaseStartScreenRecordingOptions + { + public static IOSStartScreenRecordingOptions GetIosStartScreenRecordingOptions() + { + return new IOSStartScreenRecordingOptions(); + } + + public enum VideoType + { + H264, MP4, FMP4 + } + + /// + /// The format of the screen capture to be recorded. + /// Available formats: "h264", "mp4" or "fmp4". Default is "mp4". + /// Only works for Simulator. + /// + /// one of available format names. + /// self instance for chaining. + public IOSStartScreenRecordingOptions WithVideoType(VideoType videoType) + { + Parameters["videoType"] = videoType.ToString().ToLower(); + return this; + } + + public enum VideoQuality + { + LOW, MEDIUM, HIGH, PHOTO + } + + /// + /// The video encoding quality (low, medium, high, photo - defaults to medium). + /// Only works for real devices. + /// + /// + /// + public IOSStartScreenRecordingOptions WithVideoQuality(VideoQuality videoQuality) + { + Parameters["videoQuality"] = videoQuality.ToString().ToLower(); + return this; + } + + /// + /// The maximum recording time.The default value is 180 seconds (3 minutes). + /// The maximum value is 10 minutes. + /// Setting values greater than this or less than zero will cause an exception. The minimum + /// time resolution unit is one second. + /// + /// The actual time limit of the recorded video. + /// self instance for chaining. + public new IOSStartScreenRecordingOptions WithTimeLimit(TimeSpan timeLimit) + { + return base.WithTimeLimit(timeLimit); + } + } +} diff --git a/appium-dotnet-driver/Appium/iOS/IOSStopScreenRecordingOptions.cs b/appium-dotnet-driver/Appium/iOS/IOSStopScreenRecordingOptions.cs new file mode 100644 index 00000000..ff8f2a5e --- /dev/null +++ b/appium-dotnet-driver/Appium/iOS/IOSStopScreenRecordingOptions.cs @@ -0,0 +1,12 @@ +using OpenQA.Selenium.Appium.ScreenRecording; + +namespace OpenQA.Selenium.Appium.iOS +{ + public class IOSStopScreenRecordingOptions : BaseStopScreenRecordingOptions + { + public static IOSStopScreenRecordingOptions StopScreenRecordingOptions() + { + return new IOSStopScreenRecordingOptions(); + } + } +} diff --git a/integration_tests/Android/AndroidScreenRecordingTest.cs b/integration_tests/Android/AndroidScreenRecordingTest.cs new file mode 100644 index 00000000..bb8c005e --- /dev/null +++ b/integration_tests/Android/AndroidScreenRecordingTest.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading; +using Appium.Integration.Tests.Helpers; +using NUnit.Framework; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Android; +using static OpenQA.Selenium.Appium.Android.AndroidStartScreenRecordingOptions; + +namespace Appium.Integration.Tests.Android +{ + [TestFixture] + class AndroidScreenRecordingTest + { + private AppiumDriver driver; + + [OneTimeSetUp] + public void BeforeAll() + { + AppiumOptions capabilities = Caps.getAndroid27Caps(Apps.get("androidApiDemos")); + if (Env.isSauce()) + { + capabilities.AddAdditionalCapability("username", Env.getEnvVar("SAUCE_USERNAME")); + capabilities.AddAdditionalCapability("accessKey", Env.getEnvVar("SAUCE_ACCESS_KEY")); + capabilities.AddAdditionalCapability("name", "android - complex"); + capabilities.AddAdditionalCapability("tags", new string[] { "sample" }); + } + Uri serverUri = Env.isSauce() ? AppiumServers.sauceURI : AppiumServers.LocalServiceURIAndroid; + driver = new AndroidDriver(serverUri, capabilities, Env.INIT_TIMEOUT_SEC); + driver.Manage().Timeouts().ImplicitWait = Env.IMPLICIT_TIMEOUT_SEC; + } + + [OneTimeTearDown] + public void AfterAll() + { + if (driver != null) + { + driver.Quit(); + } + if (!Env.isSauce()) + { + AppiumServers.StopLocalService(); + } + } + + [Test] + public void ScreenRecordTest() + { + driver.StartRecordingScreen(); + Thread.Sleep(1000); + string result = driver.StopRecordingScreen(); + Assert.IsNotEmpty(result); + } + + [Test] + public void ScreenRecordWithOptionsTest() + { + driver.StartRecordingScreen( + GetAndroidStartScreenRecordingOptions() + .WithTimeLimit(TimeSpan.FromSeconds(10)) + .WithBitRate(500000) + .WithVideoSize("720x1280")); + Thread.Sleep(1000); + string result = driver.StopRecordingScreen(); + Assert.IsNotEmpty(result); + } + } +} diff --git a/integration_tests/helpers/Caps.cs b/integration_tests/helpers/Caps.cs index 32fa0350..c47fdba7 100644 --- a/integration_tests/helpers/Caps.cs +++ b/integration_tests/helpers/Caps.cs @@ -57,6 +57,15 @@ public static AppiumOptions getAndroid19Caps(string app) return capabilities; } + public static AppiumOptions getAndroid27Caps(string app) + { + AppiumOptions capabilities = new AppiumOptions(); + capabilities.AddAdditionalCapability(MobileCapabilityType.PlatformVersion, "8.1.0"); + capabilities.AddAdditionalCapability(MobileCapabilityType.DeviceName, "Android Emulator"); + capabilities.AddAdditionalCapability(MobileCapabilityType.App, app); + return capabilities; + } + public static AppiumOptions getSelendroid19Caps(string app) { AppiumOptions capabilities = new AppiumOptions(); diff --git a/integration_tests/iOS/IOSScreenRecordingTest.cs b/integration_tests/iOS/IOSScreenRecordingTest.cs new file mode 100644 index 00000000..e770b23f --- /dev/null +++ b/integration_tests/iOS/IOSScreenRecordingTest.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; +using Appium.Integration.Tests.Helpers; +using NUnit.Framework; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.iOS; +using static OpenQA.Selenium.Appium.iOS.IOSStartScreenRecordingOptions; + +namespace Appium.Integration.Tests.iOS +{ + [TestFixture] + class IOSScreenRecordingTest + { + private IOSDriver driver; + + [OneTimeSetUp] + public void beforeAll() + { + AppiumOptions capabilities = Caps.getIos102Caps(Apps.get("iosUICatalogApp")); + if (Env.isSauce()) + { + capabilities.AddAdditionalCapability("username", Env.getEnvVar("SAUCE_USERNAME")); + capabilities.AddAdditionalCapability("accessKey", Env.getEnvVar("SAUCE_ACCESS_KEY")); + capabilities.AddAdditionalCapability("name", "ios - complex"); + capabilities.AddAdditionalCapability("tags", new string[] { "sample" }); + } + Uri serverUri = Env.isSauce() ? AppiumServers.sauceURI : AppiumServers.LocalServiceURIForIOS; + driver = new IOSDriver(serverUri, capabilities, Env.INIT_TIMEOUT_SEC); + driver.Manage().Timeouts().ImplicitWait = Env.IMPLICIT_TIMEOUT_SEC; + } + + [OneTimeTearDown] + public void AfterEach() + { + if (driver != null) + { + driver.Quit(); + } + if (!Env.isSauce()) + { + AppiumServers.StopLocalService(); + } + } + + [Test] + public void ScreenRecordTest() + { + driver.StartRecordingScreen(); + Thread.Sleep(1000); + string result = driver.StopRecordingScreen(); + Assert.IsNotEmpty(result); + } + + [Test] + public void ScreenRecordWithOptionsTest() + { + driver.StartRecordingScreen( + GetIosStartScreenRecordingOptions() + .WithTimeLimit(TimeSpan.FromSeconds(10)) + .WithVideoType(VideoType.H264)); + Thread.Sleep(1000); + string result = driver.StopRecordingScreen(); + Assert.IsNotEmpty(result); + } + } +}