1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-05 23:53:33 +00:00

Compare commits

...

6 Commits

Author SHA1 Message Date
Alison Fernandes
9cc1a501a5 Merge branch 'master' into uitests 2022-04-27 23:28:47 +01:00
Alison Fernandes
b0a8694801 Implemented AccountSwitching UI tests 2022-04-27 23:27:53 +01:00
Alison Fernandes
b9c1ab7c1d Added Id to the activity indicator 2022-04-10 00:03:40 +01:00
Alison Fernandes
4d96b091f7 Enabling screenshots temporarily. 2022-04-09 00:32:24 +01:00
Alison Fernandes
88fee155db Added UI test project 2022-04-05 00:35:48 +01:00
Alison Fernandes
f930028920 Added id's to views in preparation for UI tests 2022-04-05 00:29:35 +01:00
25 changed files with 1004 additions and 15 deletions

View File

@@ -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}

View File

@@ -64,10 +64,10 @@ namespace Bit.Droid
Intent?.Validate();
base.OnCreate(savedInstanceState);
if (!CoreHelpers.InDebugMode())
{
Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
}
//if (!CoreHelpers.InDebugMode())
//{
// Window.AddFlags(Android.Views.WindowManagerFlags.Secure);
//}
#if !DEBUG && !FDROID
var appCenterHelper = new AppCenterHelper(_appIdService, _stateService);

View File

@@ -14,7 +14,7 @@
<ContentPage.ToolbarItems>
<ToolbarItem Text="{u:I18n Cancel}" Clicked="Close_Clicked" Order="Primary" Priority="-1" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" />
<ToolbarItem Text="{u:I18n Save}" Clicked="Submit_Clicked" AutomationId="save_button"/>
</ContentPage.ToolbarItems>
<ScrollView>
@@ -34,7 +34,8 @@
Placeholder="ex. https://bitwarden.company.com"
StyleClass="box-value"
ReturnType="Go"
ReturnCommand="{Binding SubmitCommand}" />
ReturnCommand="{Binding SubmitCommand}"
AutomationId="server_input"/>
</StackLayout>
<Label
Text="{u:I18n SelfHostedEnvironmentFooter}"

View File

@@ -23,7 +23,7 @@
Priority="-1"
UseOriginalImage="True"
AutomationProperties.IsInAccessibleTree="True"
AutomationProperties.Name="{u:I18n Account}" />
AutomationProperties.Name="{u:I18n Account}"/>
<ToolbarItem
Icon="cog_environment.png" Clicked="Environment_Clicked" Order="Primary"
AutomationProperties.IsInAccessibleTree="True"
@@ -37,15 +37,18 @@
<Image
x:Name="_logo"
Source="logo.png"
VerticalOptions="Center" />
VerticalOptions="Center"
AutomationId="logo_image"
/>
<Label Text="{u:I18n LoginOrCreateNewAccount}"
StyleClass="text-lg"
HorizontalTextAlignment="Center">
</Label>
HorizontalTextAlignment="Center"/>
<StackLayout Spacing="5">
<Button Text="{u:I18n LogIn}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
Clicked="LogIn_Clicked"
AutomationId="homepage_login_button"/>
<Button Text="{u:I18n CreateAccount}"
Clicked="Register_Clicked" />
<Button Text="{u:I18n LogInSso}"

View File

@@ -56,7 +56,8 @@
x:Name="_email"
Text="{Binding Email}"
Keyboard="Email"
StyleClass="box-value">
StyleClass="box-value"
AutomationId="email_input">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Disabled">
@@ -92,7 +93,8 @@
Grid.Row="1"
Grid.Column="0"
ReturnType="Go"
ReturnCommand="{Binding LogInCommand}" />
ReturnCommand="{Binding LogInCommand}"
AutomationId="password_input"/>
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
Text="{Binding ShowPasswordIcon}"
@@ -107,10 +109,12 @@
<StackLayout Padding="10, 0">
<Button Text="{u:I18n LogIn}"
StyleClass="btn-primary"
Clicked="LogIn_Clicked" />
Clicked="LogIn_Clicked"
AutomationId="loginpage_login_button"/>
<Button Text="{u:I18n Cancel}"
IsVisible="{Binding ShowCancelButton}"
Clicked="Cancel_Clicked" />
Clicked="Cancel_Clicked"
AutomationId="cancel_button"/>
</StackLayout>
</StackLayout>
</ScrollView>

View File

@@ -70,6 +70,7 @@ namespace Bit.App.Pages
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.Center,
Color = ThemeManager.GetResourceColor("PrimaryColor"),
AutomationId = "activity_indicator"
};
if (targetView != null)
{

View File

@@ -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
{
}
}

View File

@@ -0,0 +1,34 @@
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<AppQuery, AppQuery> elementQuery)
{
app.WaitForElement(elementQuery);
app.Tap(elementQuery);
}
public static void WaitAndTapElement(this IApp app, Func<AppQuery, AppWebQuery> elementQuery)
{
app.WaitForElement(elementQuery);
app.Tap(elementQuery);
}
public static void WaitAndScreenshot(this IApp app, string screenshotTitle)
{
app.Wait(1); //screenshots tend to be too fast and not capture the previous actions
app.Screenshot(screenshotTitle);
}
}
}

View File

@@ -0,0 +1,14 @@
using Xamarin.UITest;
namespace Bit.UITests.Helpers
{
public static class AppState
{
public static void EnableScreenshots(this IApp app)
{
//TODO placeholder, mobile app needs the service / setting to enable Android screenshots first
app.Invoke("Zamboni");
}
}
}

View File

@@ -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(10);
public CustomWaitTimes()
{
_timeout = DefaultCustomTimeout;
}
public CustomWaitTimes(TimeSpan timeoutTimeSpan)
{
_timeout = timeoutTimeSpan;
}
public TimeSpan GestureCompletionTimeout => _timeout;
public TimeSpan GestureWaitTimeout => _timeout;
public TimeSpan WaitForTimeout => _timeout;
}
}

View File

@@ -0,0 +1,52 @@
using Bit.UITests.Extensions;
using Bit.UITests.Setup;
using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;
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.ClearText(_serverUrlInput);
App.EnterText(_serverUrlInput, serverUrl);
App.DismissKeyboard();
App.WaitAndScreenshot("After inserting the server url, I can see the field filled");
return this;
}
}
}

View File

@@ -0,0 +1,50 @@
using Bit.UITests.Setup;
using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;
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;
}
}
}

View File

@@ -0,0 +1,90 @@
using System;
using Bit.UITests.Extensions;
using Bit.UITests.Setup;
using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;
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.ClearText(_emailInput);
App.EnterText(_emailInput, email);
App.DismissKeyboard();
return this;
}
public LoginPage InputPassword(string password)
{
App.Tap(_passwordInput);
App.EnterText(password);
App.DismissKeyboard();
App.WaitAndScreenshot("After I input the email and password fields, I can see both fields filled");
return this;
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using Bit.UITests.Extensions;
using Bit.UITests.Setup;
using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;
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.WaitAndScreenshot("After I input the email and password fields, I can see both fields filled");
return this;
}
}
}

View File

@@ -0,0 +1,70 @@
using System;
using Bit.UITests.Extensions;
using Bit.UITests.Helpers;
using Bit.UITests.Setup;
using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;
namespace Bit.UITests.Pages
{
public class TabsPage : BasePage
{
private readonly Query _vaultTab;
private readonly Query _sendTab;
private readonly Query _accountSwitchingAvatar;
private readonly Query _accountSwitchingAddAccount;
public TabsPage()
: base()
{
_vaultTab = x => x.Marked("My Vault");
_sendTab = x => x.Marked("Send");
_accountSwitchingAvatar = x => x.Marked("Account");
_accountSwitchingAddAccount = x => x.Marked("Add Account");
WaitForNoLoader();
}
protected override PlatformQuery Trait => new PlatformQuery
{
Android = x => x.Marked("Send"),
iOS = x => x.Marked("Send"),
};
public TabsPage WaitForNoLoader()
{
App.WaitForNoElement(LoadingIndicator, timeout: CustomWaitTimes.DefaultCustomTimeout);
App.WaitAndScreenshot("Page finished loading");
return this;
}
public TabsPage TapAccountSwitchingAvatar()
{
App.WaitForElement(_accountSwitchingAvatar);
App.Tap(_accountSwitchingAvatar);
App.WaitAndScreenshot("Tapping the avatar, I can see the account switching panel");
return this;
}
public TabsPage TapAccountSwitchingAddAccount()
{
App.Tap(_accountSwitchingAddAccount);
return this;
}
public TabsPage TapVaultTab()
{
App.Tap(_vaultTab);
App.WaitAndScreenshot("Tapping the Vault tab, I can see the Vault view");
return this;
}
public TabsPage TapSTab()
{
App.Tap(_sendTab);
App.WaitAndScreenshot("Tapping the Send tab, I can see the Send view");
return this;
}
}
}

View File

@@ -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", "release", $"{AndroidPackageName}-Signed.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;
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using Bit.UITests.Extensions;
using Bit.UITests.Helpers;
using NUnit.Framework;
using Xamarin.UITest;
using Query = System.Func<Xamarin.UITest.Queries.AppQuery, Xamarin.UITest.Queries.AppQuery>;
namespace Bit.UITests.Setup
{
public abstract class BasePage
{
protected BasePage()
{
AssertOnPage(CustomWaitTimes.DefaultCustomTimeout);
App.Screenshot("On " + GetType().Name);
}
protected readonly Query LoadingIndicator = x => x.Marked("activity_indicator");
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; }
/// <summary>
/// Verifies that the trait is still present. Defaults to no wait.
/// </summary>
/// <param name="timeout">Time to wait before the assertion fails</param>
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);
}
}
/// <summary>
/// Verifies that the trait is no longer present. Defaults to a 5 second wait.
/// </summary>
/// <param name="timeout">Time to wait before the assertion fails</param>
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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,39 @@
using System;
using Xamarin.UITest;
using Xamarin.UITest.Queries;
namespace Bit.UITests.Setup
{
public class PlatformQuery
{
Func<AppQuery, AppQuery> _current;
public Func<AppQuery, AppQuery> Current
{
get
{
if (_current == null)
throw new NullReferenceException("Trait not set for current platform");
return _current;
}
}
public Func<AppQuery, AppQuery> Android
{
set
{
if (AppManager.Platform == Platform.Android)
_current = value;
}
}
public Func<AppQuery, AppQuery> iOS
{
set
{
if (AppManager.Platform == Platform.iOS)
_current = value;
}
}
}
}

View File

@@ -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<Simulator>();
var lines = GetInstrumentsOutput();
foreach (var line in lines)
{
var sim = new Simulator(line);
if (sim.IsValid())
{
simulators.Add(sim);
}
}
return simulators.ToArray();
}
}
}

View File

@@ -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<Simulator> 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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,84 @@
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
{
record AccountCredentials(string ServerUrl, string Email, string Password);
private AccountCredentials[] _accounts =
{
new ("", "", ""),
new ("", "", ""),
new ("", "", ""),
};
public LoginTests(Platform platform)
: base(platform)
{
}
[Test]
[SmokeTest]
public void WaitForAppToLoad()
{
new HomePage();
App.Screenshot("App loaded with success!");
}
//[Test]
public void LoginWithSuccess()
{
Login(_accounts[0]);
new TabsPage();
App.Repl();
App.Screenshot("After logging in with success, I can see the vault");
}
[Test]
public void AccountSwitchWithSuccess()
{
Login(_accounts[0], true);
new TabsPage()
.TapAccountSwitchingAvatar()
.TapAccountSwitchingAddAccount();
Login(_accounts[1]);
new TabsPage()
.TapAccountSwitchingAvatar()
.TapAccountSwitchingAddAccount();
Login(_accounts[2]);
new TabsPage()
.TapAccountSwitchingAvatar();
}
private void Login(AccountCredentials account, bool changeEnvironment = false)
{
if (changeEnvironment)
{
new HomePage()
.TapEnvironmentAndNavigate();
new EnvironmentPage()
.InputServerUrl(account.ServerUrl)
.TapSaveAndNavigate();
}
new HomePage()
.TapLoginAndNavigate();
new LoginPage()
.InputEmail(account.Email)
.InputPassword(account.Password)
.TapLoginAndNavigate();
}
}
}

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{23FB637B-1705-485F-9464-078FCAF361A8}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>Bit.UiTests</RootNamespace>
<AssemblyName>UiTests</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<TargetFrameworkProfile />
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug</OutputPath>
<DefineConstants>DEBUG;</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<Optimize>true</Optimize>
<OutputPath>bin\Release</OutputPath>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Xamarin.UITest" Version="3.2.6" />
<PackageReference Include="NUnit" Version="3.13.3" />
</ItemGroup>
<ItemGroup>
<Compile Include="Categories\SmokeTest.cs" />
<Compile Include="Extensions\IAppExtension.cs" />
<Compile Include="Setup\SimulatorManager\InstrumentsRunner.cs" />
<Compile Include="Setup\SimulatorManager\IosSimulatorsManager.cs" />
<Compile Include="Setup\SimulatorManager\Simulator.cs" />
<Compile Include="Setup\AppManager.cs" />
<Compile Include="Setup\BasePage.cs" />
<Compile Include="Setup\BaseTestFixture.cs" />
<Compile Include="Setup\Csharp9Support.cs" />
<Compile Include="Setup\PlatformQuery.cs" />
<Compile Include="Helpers\CustomWaitTimes.cs" />
<Compile Include="Helpers\AppState.cs" />
<Compile Include="Tests\LoginTests.cs" />
<Compile Include="Pages\Accounts\EnvironmentPage.cs" />
<Compile Include="Pages\Accounts\HomePage.cs" />
<Compile Include="Pages\Accounts\LoginPage.cs" />
<Compile Include="Pages\TabsPage.cs" />
<Compile Include="Pages\ExamplePage.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>