diff --git a/bitwarden-mobile.sln b/bitwarden-mobile.sln index 07434b50a..7e636b99b 100644 --- a/bitwarden-mobile.sln +++ b/bitwarden-mobile.sln @@ -46,6 +46,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.ShareExtension", "src\i EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Autofill", "src\iOS.Autofill\iOS.Autofill.csproj", "{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UiTests", "src\UiTests\UiTests.csproj", "{23FB637B-1705-485F-9464-078FCAF361A8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Ad-Hoc|Any CPU = Ad-Hoc|Any CPU @@ -446,6 +448,36 @@ Global {8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhone.Build.0 = Release|iPhone {8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator {8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {23FB637B-1705-485F-9464-078FCAF361A8}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.AppStore|Any CPU.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.AppStore|iPhone.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Debug|iPhone.Build.0 = Debug|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.FDroid|Any CPU.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.FDroid|Any CPU.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.FDroid|iPhone.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.FDroid|iPhone.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.FDroid|iPhoneSimulator.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.FDroid|iPhoneSimulator.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Release|Any CPU.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Release|iPhone.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Release|iPhone.Build.0 = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {23FB637B-1705-485F-9464-078FCAF361A8}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -464,6 +496,7 @@ Global {8AE548D9-A567-4E97-995E-93EC7DB0FDE0} = {8904C536-C67D-420F-9971-51B26574C3AA} {F8C3F648-EA5A-4719-8005-85D1690B1655} = {D10CA4A9-F866-40E1-B658-F69051236C71} {8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A} = {D10CA4A9-F866-40E1-B658-F69051236C71} + {23FB637B-1705-485F-9464-078FCAF361A8} = {D10CA4A9-F866-40E1-B658-F69051236C71} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7D436EA3-8B7E-45D2-8D14-0730BD2E0410} diff --git a/src/UiTests/Categories/SmokeTest.cs b/src/UiTests/Categories/SmokeTest.cs new file mode 100755 index 000000000..79c28eabe --- /dev/null +++ b/src/UiTests/Categories/SmokeTest.cs @@ -0,0 +1,12 @@ +using System; +using NUnit.Framework; + +namespace Bit.UITests.Categories +{ + [AttributeUsage(AttributeTargets.Method)] +#pragma warning disable SA1649 // File name should match first type name + public class SmokeTestAttribute : CategoryAttribute +#pragma warning restore SA1649 // File name should match first type name + { + } +} diff --git a/src/UiTests/Extensions/IAppExtension.cs b/src/UiTests/Extensions/IAppExtension.cs new file mode 100644 index 000000000..e1f0ddfc4 --- /dev/null +++ b/src/UiTests/Extensions/IAppExtension.cs @@ -0,0 +1,28 @@ +using System; +using Xamarin.UITest; +using Xamarin.UITest.Queries; + +namespace Bit.UITests.Extensions +{ + public static class IAppExtension + { + public static void Wait(this IApp app, float seconds) + { + var waitTime = DateTime.Now + TimeSpan.FromSeconds(seconds); + + app.WaitFor(() => DateTime.Now > waitTime); + } + + public static void WaitAndTapElement(this IApp app, Func elementQuery) + { + app.WaitForElement(elementQuery); + app.Tap(elementQuery); + } + + public static void WaitAndTapElement(this IApp app, Func elementQuery) + { + app.WaitForElement(elementQuery); + app.Tap(elementQuery); + } + } +} diff --git a/src/UiTests/Helpers/AppState.cs b/src/UiTests/Helpers/AppState.cs new file mode 100644 index 000000000..7a5bb2030 --- /dev/null +++ b/src/UiTests/Helpers/AppState.cs @@ -0,0 +1,17 @@ +using Xamarin.UITest; + +namespace Bit.UITests.Helpers +{ + public static class AppState + { + public static void CallBackdoor(this IApp app, string paramExample) + { + var args = new object[] + { + paramExample, + }; + + app.Invoke("Zamboni", args); + } + } +} diff --git a/src/UiTests/Helpers/CustomWaitTimes.cs b/src/UiTests/Helpers/CustomWaitTimes.cs new file mode 100644 index 000000000..df797fd7b --- /dev/null +++ b/src/UiTests/Helpers/CustomWaitTimes.cs @@ -0,0 +1,27 @@ +using System; +using Xamarin.UITest.Utils; + +namespace Bit.UITests.Helpers +{ + public class CustomWaitTimes : IWaitTimes + { + private readonly TimeSpan _timeout; + public static readonly TimeSpan DefaultCustomTimeout = TimeSpan.FromSeconds(30); + + public CustomWaitTimes() + { + _timeout = DefaultCustomTimeout; + } + + public CustomWaitTimes(TimeSpan timeoutTimeSpan) + { + _timeout = timeoutTimeSpan; + } + + public TimeSpan GestureCompletionTimeout => _timeout; + + public TimeSpan GestureWaitTimeout => _timeout; + + public TimeSpan WaitForTimeout => _timeout; + } +} diff --git a/src/UiTests/Pages/Accounts/EnvironmentPage.cs b/src/UiTests/Pages/Accounts/EnvironmentPage.cs new file mode 100644 index 000000000..31bc73848 --- /dev/null +++ b/src/UiTests/Pages/Accounts/EnvironmentPage.cs @@ -0,0 +1,51 @@ +using Bit.UITests.Setup; +using Query = System.Func; + +namespace Bit.UITests.Pages.Accounts +{ + public class EnvironmentPage : BasePage + { + private readonly Query _saveButton; + private readonly Query _serverUrlInput; + + public EnvironmentPage() + : base() + { + if (OnAndroid) + { + _saveButton = x => x.Marked("save_button"); + _serverUrlInput = x => x.Marked("server_input"); + + return; + } + + if (OniOS) + { + _saveButton = x => x.Marked("save_button"); + _serverUrlInput = x => x.Marked("server_input"); + } + } + + protected override PlatformQuery Trait => new PlatformQuery + { + Android = x => x.Marked("server_input"), + iOS = x => x.Marked("server_input"), + }; + + public EnvironmentPage TapSaveAndNavigate() + { + App.Tap(_saveButton); + WaitForPageToLeave(); + return this; + } + + public EnvironmentPage InputServerUrl(string serverUrl) + { + App.Tap(_serverUrlInput); + App.EnterText(serverUrl); + App.DismissKeyboard(); + App.Screenshot("After inserting the server url, I can see the field filled"); + return this; + } + } +} diff --git a/src/UiTests/Pages/Accounts/HomePage.cs b/src/UiTests/Pages/Accounts/HomePage.cs new file mode 100644 index 000000000..ca3f8ef6d --- /dev/null +++ b/src/UiTests/Pages/Accounts/HomePage.cs @@ -0,0 +1,50 @@ +using Bit.UITests.Setup; +using Query = System.Func; + +namespace Bit.UITests.Pages.Accounts +{ + public class HomePage : BasePage + { + private readonly Query _loginButton; + private readonly Query _environmentButton; + + public HomePage() + : base() + { + if (OnAndroid) + { + _loginButton = x => x.Marked("homepage_login_button"); + + //TODO a11y uses the same fields as the UI tests and we're prioritising that + // improve this by getting the app runtime locale and use the i18n service here instead + _environmentButton = x => x.Marked("Options"); + //_environmentButton = x => x.Marked("environment_button"); + return; + } + + if (OniOS) + { + _loginButton = x => x.Marked("homepage_login_button"); + _environmentButton = x => x.Marked("Options"); + } + } + + protected override PlatformQuery Trait => new PlatformQuery + { + Android = x => x.Marked("logo_image"), + iOS = x => x.Marked("logo_image"), + }; + + public HomePage TapLoginAndNavigate() + { + App.Tap(_loginButton); + return this; + } + + public HomePage TapEnvironmentAndNavigate() + { + App.Tap(_environmentButton); + return this; + } + } +} diff --git a/src/UiTests/Pages/Accounts/LoginPage.cs b/src/UiTests/Pages/Accounts/LoginPage.cs new file mode 100644 index 000000000..87658a69e --- /dev/null +++ b/src/UiTests/Pages/Accounts/LoginPage.cs @@ -0,0 +1,90 @@ +using System; +using Bit.UITests.Setup; +using Query = System.Func; + +namespace Bit.UITests.Pages.Accounts +{ + public class LoginPage : BasePage + { + private readonly Query _loginButton; + private readonly Query _cancelButton; + private readonly Query _passwordVisibilityToggle; + + private readonly Query _emailInput; + private readonly Query _passwordInput; + + + public LoginPage() + : base() + { + if (OnAndroid) + { + _loginButton = x => x.Marked("loginpage_login_button"); + _cancelButton = x => x.Marked("cancel_button"); + + //TODO a11y uses the same fields as the UI tests and we're prioritising that + // improve this by getting the app runtime locale and use the i18n service here instead + _passwordVisibilityToggle = x => x.Marked("Toggle Visibility"); + + _emailInput = x => x.Marked("email_input"); + _passwordInput = x => x.Marked("password_input"); + + return; + } + + if (OniOS) + { + _loginButton = x => x.Marked("loginpage_login_button"); + _cancelButton = x => x.Marked("cancel_button"); + _passwordVisibilityToggle = x => x.Marked("Toggle Visibility"); + + _emailInput = x => x.Marked("email_input"); + _passwordInput = x => x.Marked("password_input"); + } + } + + protected override PlatformQuery Trait => new PlatformQuery + { + Android = x => x.Marked("email_input"), + iOS = x => x.Marked("email_input"), + }; + + public LoginPage TapLoginAndNavigate() + { + App.Tap(_loginButton); + return this; + } + + public LoginPage TapCancelAndNavigate() + { + App.Tap(_cancelButton); + return this; + } + + public LoginPage TapPasswordVisibilityToggle() + { + App.Tap(_passwordVisibilityToggle); + return this; + } + + public LoginPage InputEmail(string email) + { + App.Tap(_emailInput); + App.EnterText(email); + App.DismissKeyboard(); + return this; + } + + public LoginPage InputPassword(string password) + { + App.Tap(_passwordInput); + App.EnterText(password); + App.DismissKeyboard(); + + App.Screenshot("After I input the email and password fields, I can see both fields filled"); + + return this; + } + + } +} diff --git a/src/UiTests/Pages/ExamplePage.cs b/src/UiTests/Pages/ExamplePage.cs new file mode 100644 index 000000000..c1e63611b --- /dev/null +++ b/src/UiTests/Pages/ExamplePage.cs @@ -0,0 +1,54 @@ +using System; +using Bit.UITests.Setup; +using Query = System.Func; + +namespace Bit.UITests.Pages +{ + public class ExamplePage : BasePage + { + private readonly Query _loginButton; + private readonly Query _passwordInput; + + public ExamplePage() + : base() + { + if (OnAndroid) + { + _loginButton = x => x.Marked("loginpage_login_button"); + _passwordInput = x => x.Marked("password_input"); + + return; + } + + if (OniOS) + { + _loginButton = x => x.Marked("loginpage_login_button"); + _passwordInput = x => x.Marked("password_input"); + } + } + + protected override PlatformQuery Trait => new PlatformQuery + { + Android = x => x.Marked("password_input"), + iOS = x => x.Marked("password_input"), + }; + + public ExamplePage TapLogin() + { + App.Tap(_loginButton); + return this; + } + + public ExamplePage InputPassword(string password) + { + App.Tap(_passwordInput); + App.EnterText(password); + App.DismissKeyboard(); + + App.Screenshot("After I input the email and password fields, I can see both fields filled"); + + return this; + } + + } +} diff --git a/src/UiTests/Pages/TabsPage.cs b/src/UiTests/Pages/TabsPage.cs new file mode 100644 index 000000000..cad3b31e6 --- /dev/null +++ b/src/UiTests/Pages/TabsPage.cs @@ -0,0 +1,26 @@ +using System; +using Bit.UITests.Setup; +using Query = System.Func; + +namespace Bit.UITests.Pages +{ + public class TabsPage : BasePage + { + private readonly Query _vaultTab; + private readonly Query _sendTab; + + public TabsPage() + : base() + { + + _vaultTab = x => x.Marked("My Vault"); + _sendTab = x => x.Marked("Send"); + } + + protected override PlatformQuery Trait => new PlatformQuery + { + Android = x => x.Marked("Send"), + iOS = x => x.Marked("Send"), + }; + } +} diff --git a/src/UiTests/Setup/AppManager.cs b/src/UiTests/Setup/AppManager.cs new file mode 100644 index 000000000..034c9e679 --- /dev/null +++ b/src/UiTests/Setup/AppManager.cs @@ -0,0 +1,93 @@ +using System; +using System.IO; +using System.Reflection; +using Bit.UITests.Helpers; +using Bit.UITests.Setup.SimulatorManager; +using Xamarin.UITest; + +namespace Bit.UITests.Setup +{ + internal static class AppManager + { + private static readonly string _slnPath = GetSlnPath(); + + private const string AndroidPackageName = "com.x8bit.bitwarden"; + private const string IosBundleId = "com.x8bit.bitwarden"; + private static readonly string _apkPath = Path.Combine(_slnPath, "Android", "bin", "Debug", $"{AndroidPackageName}.apk"); + private static readonly string _iosPath = Path.Combine("..", "..", "..", $"{IosBundleId}.app"); + + private static IApp _app; + + private static Platform? _platform; + + public static IApp App + { + get + { + if (_app == null) + { + throw new NullReferenceException("'AppManager.App' not set. Call 'AppManager.StartApp()' before trying to access it."); + } + + return _app; + } + } + + + public static Platform Platform + { + get + { + if (_platform == null) + { + throw new NullReferenceException("'AppManager.Platform' not set."); + } + + return _platform.Value; + } + + set => _platform = value; + } + + public static void StartApp() + { + Console.WriteLine($"TestEnvironment.IsTestCloud: {TestEnvironment.IsTestCloud}"); + Console.WriteLine($"TestEnvironment.Platform: {TestEnvironment.Platform}"); + Console.WriteLine($"Platform: {Platform}"); + + switch (Platform, TestEnvironment.IsTestCloud) + { + case (Platform.Android, false): + _app = ConfigureApp + .Android + .InstalledApp(AndroidPackageName) + //.ApkFile(_apkPath) + .StartApp(); + break; + case (Platform.iOS, false): + _app = ConfigureApp + .iOS + .SetDeviceByName("iPhone X") //NOTE Get Devices name in terminal: xcrun instruments -s devices + .AppBundle(_iosPath) + // .InstalledApp(IosBundleId) + .StartApp(); + break; + case (Platform.Android, true): + _app = ConfigureApp.Android.WaitTimes(new CustomWaitTimes()).StartApp(); + break; + case (Platform.iOS, true): + _app = ConfigureApp.iOS.WaitTimes(new CustomWaitTimes()).StartApp(); + break; + } + } + + + private static string GetSlnPath() + { + string currentFile = new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath; + var fi = new FileInfo(currentFile); + string path = fi.Directory!.Parent!.Parent!.Parent!.FullName; + return path; + } + } +} diff --git a/src/UiTests/Setup/BasePage.cs b/src/UiTests/Setup/BasePage.cs new file mode 100644 index 000000000..cbac111d9 --- /dev/null +++ b/src/UiTests/Setup/BasePage.cs @@ -0,0 +1,70 @@ +using System; +using Bit.UITests.Extensions; +using Bit.UITests.Helpers; +using NUnit.Framework; +using Xamarin.UITest; + +namespace Bit.UITests.Setup +{ + public abstract class BasePage + { + + protected BasePage() + { + AssertOnPage(CustomWaitTimes.DefaultCustomTimeout); + App.Screenshot("On " + GetType().Name); + } + + protected IApp App => AppManager.App; + + protected bool OnAndroid => AppManager.Platform == Platform.Android; + + // ReSharper disable once InconsistentNaming + protected bool OniOS => AppManager.Platform == Platform.iOS; + + protected abstract PlatformQuery Trait { get; } + + /// + /// Verifies that the trait is still present. Defaults to no wait. + /// + /// Time to wait before the assertion fails + public void AssertOnPage(TimeSpan? timeout = default(TimeSpan?)) + { + var message = "Unable to verify on page: " + GetType().Name; + + if (timeout == null) + { + Assert.IsNotEmpty(App.Query(Trait.Current), message); + } + else + { + Assert.DoesNotThrow(() => App.WaitForElement(Trait.Current, timeout: timeout), message); + } + } + + /// + /// Verifies that the trait is no longer present. Defaults to a 5 second wait. + /// + /// Time to wait before the assertion fails + public void WaitForPageToLeave(TimeSpan? timeout = default(TimeSpan?)) + { + timeout ??= TimeSpan.FromSeconds(5); + var message = "Unable to verify *not* on page: " + GetType().Name; + + Assert.DoesNotThrow(() => App.WaitForNoElement(Trait.Current, timeout: timeout), message); + } + + + public BasePage Wait(int seconds) + { + App.Wait(seconds); + return this; + } + + public BasePage Back() + { + App.Back(); + return this; + } + } +} diff --git a/src/UiTests/Setup/BaseTestFixture.cs b/src/UiTests/Setup/BaseTestFixture.cs new file mode 100644 index 000000000..78bf17c48 --- /dev/null +++ b/src/UiTests/Setup/BaseTestFixture.cs @@ -0,0 +1,52 @@ +using Bit.UITests.Helpers; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using Xamarin.UITest; + +namespace Bit.UITests.Setup +{ + + [TestFixture(Platform.Android)] + [TestFixture(Platform.iOS)] + public abstract class BaseTestFixture + { + protected IApp App => AppManager.App; + + protected bool OnAndroid => AppManager.Platform == Platform.Android; + + protected bool OniOS => AppManager.Platform == Platform.iOS; + + protected BaseTestFixture(Platform platform) + { + AppManager.Platform = platform; + } + + [SetUp] + public virtual void BeforeEachTest() + { + AppManager.StartApp(); + } + + [TearDown] + public void TearDown() + { + if (TestContext.CurrentContext.Result.Outcome.Status != TestStatus.Failed) + { + return; + } + + if (App == null) + { + return; + } + + if (TestEnvironment.Platform != TestPlatform.Local) + { + return; + } + + // NOTE uncomment to help debug failing tests + //App.Repl(); + } + } +} diff --git a/src/UiTests/Setup/Csharp9Support.cs b/src/UiTests/Setup/Csharp9Support.cs new file mode 100644 index 000000000..a936391f5 --- /dev/null +++ b/src/UiTests/Setup/Csharp9Support.cs @@ -0,0 +1,9 @@ +// NOTE C# 9.0 support +// https://stackoverflow.com/a/64749403/859738 +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ +#pragma warning disable SA1649 // File name should match first type name + public class IsExternalInit { } +#pragma warning restore SA1649 // File name should match first type name +} diff --git a/src/UiTests/Setup/PlatformQuery.cs b/src/UiTests/Setup/PlatformQuery.cs new file mode 100755 index 000000000..48feb9abc --- /dev/null +++ b/src/UiTests/Setup/PlatformQuery.cs @@ -0,0 +1,39 @@ +using System; +using Xamarin.UITest; +using Xamarin.UITest.Queries; + +namespace Bit.UITests.Setup +{ + public class PlatformQuery + { + Func _current; + public Func Current + { + get + { + if (_current == null) + throw new NullReferenceException("Trait not set for current platform"); + + return _current; + } + } + + public Func Android + { + set + { + if (AppManager.Platform == Platform.Android) + _current = value; + } + } + + public Func iOS + { + set + { + if (AppManager.Platform == Platform.iOS) + _current = value; + } + } + } +} \ No newline at end of file diff --git a/src/UiTests/Setup/SimulatorManager/InstrumentsRunner.cs b/src/UiTests/Setup/SimulatorManager/InstrumentsRunner.cs new file mode 100755 index 000000000..11526733d --- /dev/null +++ b/src/UiTests/Setup/SimulatorManager/InstrumentsRunner.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Bit.UITests.Setup.SimulatorManager +{ + class InstrumentsRunner + { + static string[] GetInstrumentsOutput() + { + const string cmd = "/usr/bin/xcrun"; + + var startInfo = new ProcessStartInfo + { + FileName = cmd, + Arguments = "instruments -s devices", + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var proc = new Process(); + proc.StartInfo = startInfo; + proc.Start(); + var result = proc.StandardOutput.ReadToEnd(); + proc.WaitForExit(); + + var lines = result.Split('\n'); + return lines; + } + + public Simulator[] GetListOfSimulators() + { + var simulators = new List(); + var lines = GetInstrumentsOutput(); + + foreach (var line in lines) + { + var sim = new Simulator(line); + if (sim.IsValid()) + { + simulators.Add(sim); + } + } + + return simulators.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/UiTests/Setup/SimulatorManager/IosSimulatorsManager.cs b/src/UiTests/Setup/SimulatorManager/IosSimulatorsManager.cs new file mode 100755 index 000000000..62615bad7 --- /dev/null +++ b/src/UiTests/Setup/SimulatorManager/IosSimulatorsManager.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xamarin.UITest; +using Xamarin.UITest.Configuration; + +namespace Bit.UITests.Setup.SimulatorManager +{ + internal static class IosSimulatorsManager + { + + public static iOSAppConfigurator SetDeviceByName(this iOSAppConfigurator configurator, string simulatorName) + { + var deviceId = GetDeviceId(simulatorName); + return configurator.DeviceIdentifier(deviceId); + } + + public static string GetDeviceId(string simulatorName) + { + if (!TestEnvironment.Platform.Equals(TestPlatform.Local)) + { + return string.Empty; + } + + // See below for the InstrumentsRunner class. + IEnumerable simulators = new InstrumentsRunner().GetListOfSimulators(); + + var simulator = simulators.FirstOrDefault(x => x.Name.Contains(simulatorName)); + + if (simulator == null) + { + throw new ArgumentException("Could not find a device identifier for '" + simulatorName + "'.", "simulatorName"); + } + + return simulator.GUID; + } + } +} diff --git a/src/UiTests/Setup/SimulatorManager/Simulator.cs b/src/UiTests/Setup/SimulatorManager/Simulator.cs new file mode 100755 index 000000000..a4cfee518 --- /dev/null +++ b/src/UiTests/Setup/SimulatorManager/Simulator.cs @@ -0,0 +1,49 @@ +namespace Bit.UITests.Setup.SimulatorManager +{ + internal class Simulator + { + public Simulator(string line) + { + ParseLine(line); + } + + public string Line { get; private set; } + + public string GUID { get; private set; } + + public string Name { get; private set; } + + public bool IsValid() + { + return !string.IsNullOrWhiteSpace(GUID) && !(string.IsNullOrWhiteSpace(Name)); + } + + public override string ToString() + { + return Line; + } + + void ParseLine(string line) + { + GUID = string.Empty; + Name = string.Empty; + Line = string.Empty; + + if (string.IsNullOrWhiteSpace(line)) + { + return; + } + + Line = line.Trim(); + var idx1 = line.IndexOf(" ["); + + if (idx1 < 1) + { + return; + } + + Name = Line.Substring(0, idx1).Trim(); + GUID = Line.Substring(idx1 + 2, 36).Trim(); + } + } +} diff --git a/src/UiTests/Tests/LoginTests.cs b/src/UiTests/Tests/LoginTests.cs new file mode 100644 index 000000000..d2638e639 --- /dev/null +++ b/src/UiTests/Tests/LoginTests.cs @@ -0,0 +1,60 @@ +using Bit.UITests.Categories; +using Bit.UITests.Pages; +using Bit.UITests.Pages.Accounts; +using Bit.UITests.Setup; +using NUnit.Framework; +using Xamarin.UITest; + +namespace Bit.UiTests.Tests +{ + public class LoginTests : BaseTestFixture + { + private const string _testServerUrl = "zamboni"; + private const string _email = "zamboni"; + private const string _password = "zamboni"; + + public LoginTests(Platform platform) + : base(platform) + { + } + + //[Test] + public void OpenREPL() + { + App.Repl(); + } + + [Test] + [SmokeTest] + public void WaitForAppToLoad() + { + new HomePage(); + + App.Screenshot("App loaded with success!"); + } + + [Test] + public void LoginWithSuccess() + { + + new HomePage() + .TapEnvironmentAndNavigate(); + + new EnvironmentPage() + .InputServerUrl(_testServerUrl) + .TapSaveAndNavigate(); + + new HomePage() + .TapLoginAndNavigate(); + + new LoginPage() + .InputEmail(_email) + .InputPassword(_password) + .TapLoginAndNavigate(); + + new TabsPage(); + + App.Screenshot("After logging in with success, I can see the vault"); + } + } +} diff --git a/src/UiTests/UiTests.csproj b/src/UiTests/UiTests.csproj new file mode 100644 index 000000000..60d2b732a --- /dev/null +++ b/src/UiTests/UiTests.csproj @@ -0,0 +1,59 @@ + + + + Debug + AnyCPU + {23FB637B-1705-485F-9464-078FCAF361A8} + Library + Bit.UiTests + UiTests + v4.7.2 + + 9.0 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file