diff --git a/.github/workflows/pullrequest_android.yml b/.github/workflows/pullrequest_android.yml index ff8e3c61..867bc5c8 100644 --- a/.github/workflows/pullrequest_android.yml +++ b/.github/workflows/pullrequest_android.yml @@ -13,17 +13,26 @@ env: jobs: # MAUI Android Build build-android: - runs-on: windows-2022 + #runs-on: windows-2022 + runs-on: macos-12 name: Android Build steps: - name: Checkout uses: actions/checkout@v3 - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.1 + - uses: malinskiy/action-android/install-sdk@release/0.1.1 + - run: sdkmanager "platform-tools" "platforms;android-31" + - run: sdkmanager "build-tools;30.0.2" + - run: adb devices + + - name: Set up Node.js + uses: actions/setup-node@v1 with: - vs-prerelease: true - msbuild-architecture: x64 + node-version: '12.12.0' + + - name: Set up Appium + run: | + npm install -g appium --unsafe-perm=true --allow-root - name: Setup .NET 6 uses: actions/setup-dotnet@v2 @@ -49,6 +58,15 @@ jobs: - name: Run Unit Tests run: dotnet test TransactionMobile.Maui.BusinessLogic.Tests/TransactionMobile.Maui.BusinessLogic.Tests.csproj + + - name: Run Integration Tests - Android + uses: malinskiy/action-android/emulator-run-cmd@release/0.1.1 + with: + cmd: dotnet test TransactionMobile.Maui.UiTests/TransactionMobile.Maui.UiTests.csproj --filter (Category=PRTest)&(Category=Android) + api: 28 + tag: default + abi: x86 + verbose: true #- name: Upload Android Artifact # uses: actions/upload-artifact@v2.3.1 diff --git a/.github/workflows/pullrequest_ios.yml b/.github/workflows/pullrequest_ios.yml index 75969523..4173a5ab 100644 --- a/.github/workflows/pullrequest_ios.yml +++ b/.github/workflows/pullrequest_ios.yml @@ -23,6 +23,15 @@ jobs: with: xcode-version: '13.3' + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: '12.12.0' + + - name: Set up Appium + run: | + npm install -g appium --unsafe-perm=true --allow-root + - name: Setup .NET 6 uses: actions/setup-dotnet@v2 with: @@ -43,6 +52,9 @@ jobs: - name: Run Unit Tests run: dotnet test TransactionMobile.Maui.BusinessLogic.Tests/TransactionMobile.Maui.BusinessLogic.Tests.csproj + - name: Run Integration Tests - iOS + run: dotnet test TransactionMobile.Maui.UiTests/TransactionMobile.Maui.UiTests.csproj --filter "Category=PRTest&Category=iOS" + #- name: Upload iOS Artifact # uses: actions/upload-artifact@v2.3.1 # with: diff --git a/.github/workflows/pullrequest_maccatalyst.yml b/.github/workflows/pullrequest_maccatalyst.yml index 433d6dbe..fd706dcf 100644 --- a/.github/workflows/pullrequest_maccatalyst.yml +++ b/.github/workflows/pullrequest_maccatalyst.yml @@ -13,7 +13,7 @@ env: jobs: # MAUI MacCatalyst Build build-mac: - runs-on: macos-11 + runs-on: macos-12 name: MacCatalyst Build steps: - name: Checkout diff --git a/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj b/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj index 3501235e..cf0bf6ca 100644 --- a/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj +++ b/TransactionMobile.Maui.BusinessLogic/TransactionMobile.Maui.BusinessLogic.csproj @@ -24,9 +24,9 @@ - - - - + + + + diff --git a/TransactionMobile.Maui.BusinessLogic/UIServices/INavigationService.cs b/TransactionMobile.Maui.BusinessLogic/UIServices/INavigationService.cs index 515bd74f..5275ec54 100644 --- a/TransactionMobile.Maui.BusinessLogic/UIServices/INavigationService.cs +++ b/TransactionMobile.Maui.BusinessLogic/UIServices/INavigationService.cs @@ -36,6 +36,5 @@ Task GoToVoucherIssueVoucherPage(String operatorIdentifier, Guid productId, Decimal voucherAmount); - #endregion } \ No newline at end of file diff --git a/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs b/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs index 6a303ee6..39f9430c 100644 --- a/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs +++ b/TransactionMobile.Maui.BusinessLogic/ViewModels/LoginPageViewModel.cs @@ -122,6 +122,8 @@ private async Task LoginCommandExecute() // TODO: Need to set the application as in training mode somehow + this.MemoryCacheService.Set("IsLoggedIn", true); + await this.NavigationService.GoToHome(); } diff --git a/TransactionMobile.Maui.UITests - Copy/Common/BaseTestFixture.cs b/TransactionMobile.Maui.UITests - Copy/Common/BaseTestFixture.cs new file mode 100644 index 00000000..dfc69fc6 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Common/BaseTestFixture.cs @@ -0,0 +1,16 @@ +namespace TransactionMobile.Maui.UITests.Common +{ + using Drivers; + + public abstract class BaseTestFixture + { + #region Constructors + + protected BaseTestFixture(MobileTestPlatform mobileTestPlatform) + { + AppiumDriver.MobileTestPlatform = mobileTestPlatform; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Common/MobileTestPlatform.cs b/TransactionMobile.Maui.UITests - Copy/Common/MobileTestPlatform.cs new file mode 100644 index 00000000..8465ee22 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Common/MobileTestPlatform.cs @@ -0,0 +1,9 @@ +namespace TransactionMobile.Maui.UITests.Common; + +public enum MobileTestPlatform +{ + iOS, + Android, + Windows, + MacCatalyst +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Common/Retry.cs b/TransactionMobile.Maui.UITests - Copy/Common/Retry.cs new file mode 100644 index 00000000..c82c362a --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Common/Retry.cs @@ -0,0 +1,68 @@ +namespace TransactionMobile.Maui.UITests.Common; + +using System; +using System.Threading; +using System.Threading.Tasks; + +public static class Retry +{ + #region Fields + + /// + /// The default retry for + /// + private static readonly TimeSpan DefaultRetryFor = TimeSpan.FromSeconds(60); + + /// + /// The default retry interval + /// + private static readonly TimeSpan DefaultRetryInterval = TimeSpan.FromSeconds(5); + + #endregion + + #region Methods + + /// + /// Fors the specified action. + /// + /// The action. + /// The retry for. + /// The retry interval. + /// + public static async Task For(Func action, + TimeSpan? retryFor = null, + TimeSpan? retryInterval = null) + { + DateTime startTime = DateTime.Now; + Exception lastException = null; + + if (retryFor == null) + { + retryFor = Retry.DefaultRetryFor; + } + + while (DateTime.Now.Subtract(startTime).TotalMilliseconds < retryFor.Value.TotalMilliseconds) + { + try + { + await action().ConfigureAwait(false); + lastException = null; + break; + } + catch (Exception e) + { + lastException = e; + + // wait before retrying + Thread.Sleep(retryInterval ?? Retry.DefaultRetryInterval); + } + } + + if (lastException != null) + { + throw lastException; + } + } + + #endregion +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Common/SpecflowTableHelper.cs b/TransactionMobile.Maui.UITests - Copy/Common/SpecflowTableHelper.cs new file mode 100644 index 00000000..2e5e2846 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Common/SpecflowTableHelper.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.UITests.Common +{ + using TechTalk.SpecFlow; + + public static class SpecflowTableHelper + { + #region Methods + + /// + /// Gets the enum value. + /// + /// + /// The row. + /// The key. + /// + public static T GetEnumValue(TableRow row, + String key) where T : struct + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + Enum.TryParse(field, out T myEnum); + + return myEnum; + } + + /// + /// Gets the boolean value. + /// + /// The row. + /// The key. + /// + public static Boolean GetBooleanValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + return bool.TryParse(field, out Boolean value) && value; + } + + /// + /// Gets the date for date string. + /// + /// The date string. + /// The today. + /// + public static DateTime GetDateForDateString(String dateString, + DateTime today) + { + switch (dateString.ToUpper()) + { + case "TODAY": + return today.Date; + case "YESTERDAY": + return today.AddDays(-1).Date; + case "LASTWEEK": + return today.AddDays(-7).Date; + case "LASTMONTH": + return today.AddMonths(-1).Date; + case "LASTYEAR": + return today.AddYears(-1).Date; + case "TOMORROW": + return today.AddDays(1).Date; + default: + return DateTime.Parse(dateString); + } + } + + /// + /// Gets the decimal value. + /// + /// The row. + /// The key. + /// + public static Decimal GetDecimalValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + return decimal.TryParse(field, out Decimal value) ? value : 0; + } + + /// + /// Gets the int value. + /// + /// The row. + /// The key. + /// + public static Int32 GetIntValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + return int.TryParse(field, out Int32 value) ? value : -1; + } + + /// + /// Gets the short value. + /// + /// The row. + /// The key. + /// + public static Int16 GetShortValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + if (short.TryParse(field, out Int16 value)) + { + return value; + } + + return -1; + } + + /// + /// Gets the string row value. + /// + /// The row. + /// The key. + /// + public static String GetStringRowValue(TableRow row, + String key) + { + return row.TryGetValue(key, out String value) ? value : ""; + } + + #endregion + } +} diff --git a/TransactionMobile.Maui.UITests - Copy/Drivers/AppiumDriver.cs b/TransactionMobile.Maui.UITests - Copy/Drivers/AppiumDriver.cs new file mode 100644 index 00000000..262c8a8d --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Drivers/AppiumDriver.cs @@ -0,0 +1,111 @@ +namespace TransactionMobile.Maui.UITests.Drivers +{ + using System; + using System.IO; + using System.Reflection; + using Common; + using OpenQA.Selenium.Appium; + using OpenQA.Selenium.Appium.Android; + using OpenQA.Selenium.Appium.Enums; + using OpenQA.Selenium.Appium.iOS; + using OpenQA.Selenium.Appium.Mac; + using OpenQA.Selenium.Appium.Service; + using OpenQA.Selenium.Appium.Windows; + + public class AppiumDriver + { + #region Fields + + public static AndroidDriver AndroidDriver; + + public static IOSDriver iOSDriver; + + public static MacDriver MacDriver; + + public static MobileTestPlatform MobileTestPlatform; + + public static WindowsDriver WindowsDriver; + + #endregion + + #region Methods + + public void StartApp() + { + AppiumLocalService appiumService = new AppiumServiceBuilder().UsingPort(4723).Build(); + + if (appiumService.IsRunning == false) + { + appiumService.Start(); + } + + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + var driverOptions = new AppiumOptions(); + driverOptions.AddAdditionalCapability("adbExecTimeout", TimeSpan.FromMinutes(5).Milliseconds); + driverOptions.AddAdditionalCapability(MobileCapabilityType.AutomationName, "Espresso"); + // TODO: Only do this locally + driverOptions.AddAdditionalCapability(MobileCapabilityType.FullReset, true); + driverOptions.AddAdditionalCapability("forceEspressoRebuild", true); + driverOptions.AddAdditionalCapability("enforceAppInstall", true); + driverOptions.AddAdditionalCapability("noSign", true); + driverOptions.AddAdditionalCapability(MobileCapabilityType.PlatformName, "Android"); + driverOptions.AddAdditionalCapability(MobileCapabilityType.PlatformVersion, "9.0"); + driverOptions.AddAdditionalCapability(MobileCapabilityType.DeviceName, "emulator-5554"); + + String assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + String binariesFolder = Path.Combine(assemblyFolder, "..", "..", "..", "..", @"TransactionMobile.Maui/bin/Release/net6.0-android/"); + var apkPath = Path.Combine(binariesFolder, "com.transactionprocessing.pos-Signed.apk"); + driverOptions.AddAdditionalCapability(MobileCapabilityType.App, apkPath); + driverOptions.AddAdditionalCapability("espressoBuildConfig", + "{ \"additionalAppDependencies\": [ \"com.google.android.material:material:1.0.0\", \"androidx.lifecycle:lifecycle-extensions:2.1.0\" ] }"); + + AppiumDriver.AndroidDriver = new AndroidDriver(appiumService, driverOptions, TimeSpan.FromMinutes(10)); + } + + //if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + //{ + // var driverOptions = new AppiumOptions(); + // driverOptions.AddAdditionalCapability(MobileCapabilityType.PlatformName, "iOS"); + // driverOptions.AddAdditionalCapability(MobileCapabilityType.DeviceName, "iPhone 11"); + // driverOptions.AddAdditionalCapability(MobileCapabilityType.PlatformVersion, "14.4"); + + // String assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + // String binariesFolder = Path.Combine(assemblyFolder, "..", "..", "..", "..", @"TransactionMobile.iOS/bin/iPhoneSimulator/Release"); + // var apkPath = Path.Combine(binariesFolder, "TransactionMobile.iOS.app"); + // driverOptions.AddAdditionalCapability(MobileCapabilityType.App, apkPath); + // driverOptions.AddAdditionalCapability(MobileCapabilityType.FullReset, true); + // driverOptions.AddAdditionalCapability(MobileCapabilityType.AutomationName, "XCUITest"); + // driverOptions.AddAdditionalCapability("useNewWDA", true); + // driverOptions.AddAdditionalCapability("wdaLaunchTimeout", 999999999); + // driverOptions.AddAdditionalCapability("wdaConnectionTimeout", 999999999); + // driverOptions.AddAdditionalCapability("restart", true); + + // AppiumDriver.iOSDriver = new IOSDriver(appiumService, driverOptions, TimeSpan.FromMinutes(5)); + //} + + // TODO: Implement iOS Tests + // TODO: Implement Windows UI Tests + // TODO: Implement Mac UI Tests + } + + public void StopApp() + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + AppiumDriver.AndroidDriver.Quit(); + } + //else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + //{ + // AppiumDriver.iOSDriver.CloseApp(); + // AppiumDriver.iOSDriver.Quit(); + //} + + // TODO: Implement iOS Tests + // TODO: Implement Windows UI Tests + // TODO: Implement Mac UI Tests + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Features/Login.feature b/TransactionMobile.Maui.UITests - Copy/Features/Login.feature new file mode 100644 index 00000000..b2c6a049 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Features/Login.feature @@ -0,0 +1,32 @@ +@background @login +Feature: Login + +Background: + +# Given I have created the following estates +# | EstateName | +# | Test Estate 1 | +# +# Given I have created the following operators +# | EstateName | OperatorName | RequireCustomMerchantNumber | RequireCustomTerminalNumber | +# | Test Estate 1 | Safaricom | True | True | +# +# Given I create the following merchants +# | MerchantName | EstateName | EmailAddress | Password | GivenName | FamilyName | +# | Test Merchant 1 | Test Estate 1 | merchantuser@testmerchant1.co.uk | 123456 | TestMerchant | User1 | +# +# Given I make the following manual merchant deposits +# | Amount | DateTime | MerchantName | EstateName | +# | 1000.00 | Today | Test Merchant 1 | Test Estate 1 | +# | 1000.00 | Yesterday | Test Merchant 1 | Test Estate 1 | +# +# Given the application in in test mode + +@PRTest +Scenario: Login as Merchant + Given I am on the Login Screen + When I enter 'merchantuser@testmerchant1.co.uk' as the Email Address + And I enter '123456' as the Password + And I tap on Login + Then the Merchant Home Page is displayed + #And the available balance is shown as 2000.00 \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Features/Login.feature.cs b/TransactionMobile.Maui.UITests - Copy/Features/Login.feature.cs new file mode 100644 index 00000000..c2358355 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Features/Login.feature.cs @@ -0,0 +1,141 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace TransactionMobile.Maui.UITests.Features +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [NUnit.Framework.TestFixtureAttribute()] + [NUnit.Framework.DescriptionAttribute("Login")] + [NUnit.Framework.CategoryAttribute("background")] + [NUnit.Framework.CategoryAttribute("login")] + public partial class LoginFeature + { + + private TechTalk.SpecFlow.ITestRunner testRunner; + + private string[] _featureTags = new string[] { + "background", + "login"}; + +#line 1 "Login.feature" +#line hidden + + [NUnit.Framework.OneTimeSetUpAttribute()] + public virtual void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Login", null, ProgrammingLanguage.CSharp, new string[] { + "background", + "login"}); + testRunner.OnFeatureStart(featureInfo); + } + + [NUnit.Framework.OneTimeTearDownAttribute()] + public virtual void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [NUnit.Framework.SetUpAttribute()] + public virtual void TestInitialize() + { + } + + [NUnit.Framework.TearDownAttribute()] + public virtual void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public virtual void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext); + } + + public virtual void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public virtual void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + public virtual void FeatureBackground() + { +#line 4 +#line hidden + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("Login as Merchant")] + [NUnit.Framework.CategoryAttribute("PRTest")] + public virtual void LoginAsMerchant() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Login as Merchant", null, tagsOfScenario, argumentsOfScenario, this._featureTags); +#line 26 +this.ScenarioInitialize(scenarioInfo); +#line hidden + bool isScenarioIgnored = default(bool); + bool isFeatureIgnored = default(bool); + if ((tagsOfScenario != null)) + { + isScenarioIgnored = tagsOfScenario.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any(); + } + if ((this._featureTags != null)) + { + isFeatureIgnored = this._featureTags.Where(__entry => __entry != null).Where(__entry => String.Equals(__entry, "ignore", StringComparison.CurrentCultureIgnoreCase)).Any(); + } + if ((isScenarioIgnored || isFeatureIgnored)) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 +this.FeatureBackground(); +#line hidden +#line 27 + testRunner.Given("I am on the Login Screen", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 28 + testRunner.When("I enter \'merchantuser@testmerchant1.co.uk\' as the Email Address", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 29 + testRunner.And("I enter \'123456\' as the Password", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 30 + testRunner.And("I tap on Login", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 31 + testRunner.Then("the Merchant Home Page is displayed", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/TransactionMobile.Maui.UITests - Copy/Features/LoginFeature.cs b/TransactionMobile.Maui.UITests - Copy/Features/LoginFeature.cs new file mode 100644 index 00000000..b8f7f3a8 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Features/LoginFeature.cs @@ -0,0 +1,13 @@ +namespace TransactionMobile.Maui.UITests.Features; + +using Common; +using NUnit.Framework; + +[TestFixture(MobileTestPlatform.Android, Category = "Android")] +public partial class LoginFeature : BaseTestFixture +{ + public LoginFeature(MobileTestPlatform mobileTestPlatform) + : base(mobileTestPlatform) + { + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Hooks/AppiumHooks.cs b/TransactionMobile.Maui.UITests - Copy/Hooks/AppiumHooks.cs new file mode 100644 index 00000000..c0369f61 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Hooks/AppiumHooks.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.UITests.Hooks +{ + using Drivers; + using TechTalk.SpecFlow; + + [Binding] + public class AppiumHooks + { + private readonly AppiumDriver _appiumDriver; + + public AppiumHooks(AppiumDriver appiumDriver) + { + _appiumDriver = appiumDriver; + } + + [BeforeScenario()] + public void StartApp() + { + _appiumDriver.StartApp(); + } + + [AfterScenario()] + public void ShutdownApp() + { + _appiumDriver.StopApp(); + } + } +} diff --git a/TransactionMobile.Maui.UITests - Copy/Pages/BasePage.cs b/TransactionMobile.Maui.UITests - Copy/Pages/BasePage.cs new file mode 100644 index 00000000..09e86090 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Pages/BasePage.cs @@ -0,0 +1,147 @@ +using System; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.UITests +{ + using Common; + using Drivers; + using OpenQA.Selenium; + using Shouldly; + + public abstract class BasePage + { + protected abstract String Trait { get; } + + public async Task AssertOnPage(TimeSpan? timeout = null) + { + timeout = timeout ?? TimeSpan.FromSeconds(60); + + await Retry.For(async () => + { + String message = "Unable to verify on page: " + this.GetType().Name; + + Should.NotThrow(() => this.WaitForElementByAccessibilityId(this.Trait), message); + }, + TimeSpan.FromMinutes(1), + timeout).ConfigureAwait(false); + + } + + /// + /// 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 = null) + { + timeout = timeout ?? TimeSpan.FromSeconds(5); + var message = "Unable to verify *not* on page: " + this.GetType().Name; + + Should.NotThrow(() => this.WaitForNoElementByAccessibilityId(this.Trait), message); + } + + public async Task WaitForElementByAccessibilityId(String x, TimeSpan? timeout = null) + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + return await AppiumDriver.AndroidDriver.WaitForElementByAccessibilityId(x, timeout); + } + else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + { + return await AppiumDriver.iOSDriver.WaitForElementByAccessibilityId(x, timeout); + } + + return null; + } + + public async Task GetPageSource() + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + return await AppiumDriver.AndroidDriver.GetPageSource(); + } + else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + { + return await AppiumDriver.iOSDriver.GetPageSource(); + } + + return null; + } + + public async Task WaitForNoElementByAccessibilityId(String x) + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + await AppiumDriver.AndroidDriver.WaitForNoElementByAccessibilityId(x); + } + else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + { + await AppiumDriver.iOSDriver.WaitForNoElementByAccessibilityId(x); + } + } + + public async Task WaitForToastMessage(String x) + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + await AppiumDriver.AndroidDriver.WaitForToastMessage(x); + } + else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + { + await AppiumDriver.iOSDriver.WaitForToastMessage(x); + } + } + + public void HideKeyboard() + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + AppiumDriver.AndroidDriver.HideKeyboard(); + } + else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + { + //if (AppiumDriver.iOSDriver.IsKeyboardShown()) + // AppiumDriver.iOSDriver.HideKeyboard(); + //AppiumDriver.iOSDriver.FindElementByName("Done").Click(); + //AppiumDriver.iOSDriver.HideKeyboard(); + } + } + + public IWebElement GetAlert() + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + return AppiumDriver.AndroidDriver.FindElementByClassName("androidx.appcompat.widget.AppCompatTextView"); + } + else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + { + return AppiumDriver.iOSDriver.FindElement(By.Name("OK")); + } + + return null; + } + + public IAlert SwitchToAlert() + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + return AppiumDriver.AndroidDriver.SwitchTo().Alert(); + } + + return null; + } + + public void NavigateBack() + { + if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.Android) + { + AppiumDriver.AndroidDriver.Navigate().Back(); + } + else if (AppiumDriver.MobileTestPlatform == MobileTestPlatform.iOS) + { + AppiumDriver.iOSDriver.Navigate().Back(); + } + } + } +} diff --git a/TransactionMobile.Maui.UITests - Copy/Pages/Extenstions.cs b/TransactionMobile.Maui.UITests - Copy/Pages/Extenstions.cs new file mode 100644 index 00000000..09819a33 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Pages/Extenstions.cs @@ -0,0 +1,128 @@ +namespace TransactionMobile.Maui.UITests; + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Common; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium.Android; +using OpenQA.Selenium.Appium.iOS; +using Shouldly; + +public static class Extenstions +{ + // TODO: Mac & Windows Extensions + // TODO: May need a platform switch + public static AndroidElement GetAlert(this AndroidDriver driver) + { + return driver.FindElementByClassName("androidx.appcompat.widget.AppCompatTextView"); + } + + public static async Task WaitForElementByAccessibilityId(this AndroidDriver driver, + String selector, + TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromSeconds(60); + AndroidElement element = null; + await Retry.For(async () => + { + element = driver.FindElementByAccessibilityId(selector); + element.ShouldNotBeNull(); + }); + + return element; + } + + public static async Task WaitForElementByAccessibilityId(this IOSDriver driver, + String selector, + TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromSeconds(60); + IOSElement element = null; + await Retry.For(async () => + { + element = driver.FindElementByAccessibilityId(selector); + element.ShouldNotBeNull(); + }); + + return element; + + } + + public static async Task WaitForNoElementByAccessibilityId(this IOSDriver driver, + String selector, + TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromSeconds(60); + + await Retry.For(async () => + { + IOSElement? element = driver.FindElementByAccessibilityId(selector); + element.ShouldBeNull(); + }); + + } + + public static async Task WaitForNoElementByAccessibilityId(this AndroidDriver driver, + String selector, + TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromSeconds(60); + + await Retry.For(async () => + { + AndroidElement? element = driver.FindElementByAccessibilityId(selector); + element.ShouldBeNull(); + }); + + } + + public static async Task WaitForToastMessage(this AndroidDriver driver, + String expectedToast) + { + await Retry.For(async () => + { + Dictionary args = new Dictionary + { + {"text", expectedToast}, + {"isRegexp", false} + }; + driver.ExecuteScript("mobile: isToastVisible", args); + + }); + } + + public static async Task WaitForToastMessage(this IOSDriver driver, + String expectedToast) + { + Boolean isDisplayed = false; + int count = 0; + do + { + if (driver.PageSource.Contains(expectedToast)) + { + Console.WriteLine(driver.PageSource); + isDisplayed = true; + break; + } + + Thread.Sleep(200); //Add your custom wait if exists + count++; + + } while (count < 10); + + Console.WriteLine(driver.PageSource); + isDisplayed.ShouldBeTrue(); + } + + public static async Task GetPageSource(this AndroidDriver driver) + { + return driver.PageSource; + } + + public static async Task GetPageSource(this IOSDriver driver) + { + return driver.PageSource; + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Pages/LoginPage.cs b/TransactionMobile.Maui.UITests - Copy/Pages/LoginPage.cs new file mode 100644 index 00000000..9b047d5d --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Pages/LoginPage.cs @@ -0,0 +1,52 @@ +namespace TransactionMobile.Maui.UITests; + +using System; +using System.Threading.Tasks; +using OpenQA.Selenium; + +public class LoginPage : BasePage +{ + protected override String Trait => "LoginLabel"; + + //private readonly String EmailEntry; + //private readonly String PasswordEntry; + private readonly String LoginButton; + //private readonly String TestModeButton; + //private readonly String ErrorLabel; + + public LoginPage() + { + //this.EmailEntry = "EmailEntry"; + //this.PasswordEntry = "PasswordEntry"; + this.LoginButton = "LoginButton"; + //this.TestModeButton = "TestModeButton"; + //this.ErrorLabel = "ErrorLabel"; + } + + public async Task EnterEmailAddress(String emailAddress) + { + //IWebElement element = await this.WaitForElementByAccessibilityId(this.EmailEntry); + + //element.SendKeys(emailAddress); + } + + public async Task EnterPassword(String password) + { + //IWebElement element = await this.WaitForElementByAccessibilityId(this.PasswordEntry); + //element.SendKeys(password); + } + + public async Task ClickLoginButton() + { + this.HideKeyboard(); + IWebElement element = await this.WaitForElementByAccessibilityId(this.LoginButton); + element.Click(); + } + + public async Task ClickTestModeButton() + { + //this.HideKeyboard(); + //IWebElement element = await this.WaitForElementByAccessibilityId(this.TestModeButton); + //element.Click(); + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Pages/MainPage.cs b/TransactionMobile.Maui.UITests - Copy/Pages/MainPage.cs new file mode 100644 index 00000000..ce536f4e --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Pages/MainPage.cs @@ -0,0 +1,70 @@ +namespace TransactionMobile.Maui.UITests; + +using System; +using System.Threading.Tasks; + +public class MainPage : BasePage +{ + protected override String Trait => "Home"; + + private readonly String TransactionsButton; + + private readonly String ReportsButton; + + private readonly String ProfileButton; + + private readonly String SupportButton; + + private readonly String AvailableBalanceLabel; + + /// + /// Initializes a new instance of the class. + /// + public MainPage() + { + this.TransactionsButton = "TransactionsButton"; + this.ReportsButton = "ReportsButton"; + this.ProfileButton = "ProfileButton"; + this.SupportButton = "SupportButton"; + this.AvailableBalanceLabel = "AvailableBalanceValueLabel"; + } + + public async Task ClickTransactionsButton() + { + var element = await this.WaitForElementByAccessibilityId(this.TransactionsButton); + element.Click(); + } + + public async Task ClickReportsButton() + { + var element = await this.WaitForElementByAccessibilityId(this.ReportsButton); + element.Click(); + } + + public void ClickProfileButton() + { + //app.WaitForElement(this.ProfileButton); + //app.Tap(this.ProfileButton); + } + + public void ClickSupportButton() + { + //app.WaitForElement(this.SupportButton); + //app.Tap(this.SupportButton); + } + + public async Task GetAvailableBalanceValue(TimeSpan? timeout = default(TimeSpan?)) + { + //await this.ScrollTo(this.Trait, this.AvailableBalanceLabel); + var element = await this.WaitForElementByAccessibilityId(this.AvailableBalanceLabel, timeout: TimeSpan.FromSeconds(30)); + + String availableBalanceText = element.Text.Replace(" KES", String.Empty); + + if (Decimal.TryParse(availableBalanceText, out Decimal balanceValue) == false) + { + throw new Exception($"Failed to parse [{availableBalanceText}] as a Decimal"); + } + + return balanceValue; + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UITests - Copy/Steps/LoginSteps.cs b/TransactionMobile.Maui.UITests - Copy/Steps/LoginSteps.cs new file mode 100644 index 00000000..1011aeb1 --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/Steps/LoginSteps.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.UITests.Steps +{ + using Shouldly; + using TechTalk.SpecFlow; + + [Binding] + [Scope(Tag = "login")] + public class LoginSteps + { + LoginPage loginPage = new LoginPage(); + //MainPage mainPage = new MainPage(); + + [Given(@"I am on the Login Screen")] + public async Task GivenIAmOnTheLoginScreen() + { + await this.loginPage.AssertOnPage(); + } + + [When(@"I enter '(.*)' as the Email Address")] + public async Task WhenIEnterAsTheEmailAddress(String emailAddress) + { + await this.loginPage.EnterEmailAddress(emailAddress); + } + + [When(@"I enter '(.*)' as the Password")] + public async Task WhenIEnterAsThePassword(String password) + { + await this.loginPage.EnterPassword(password); + } + + [When(@"I tap on Login")] + public async Task WhenITapOnLogin() + { + await this.loginPage.ClickLoginButton(); + } + + [Then(@"the Merchant Home Page is displayed")] + public async Task ThenTheMerchantHomePageIsDisplayed() + { + //await this.mainPage.AssertOnPage(); + } + + [Then(@"the available balance is shown as (.*)")] + public async Task ThenTheAvailableBalanceIsShownAs(Decimal expectedAvailableBalance) + { + //Decimal availableBalance = await this.mainPage.GetAvailableBalanceValue(TimeSpan.FromSeconds(120)).ConfigureAwait(false); + //availableBalance.ShouldBe(expectedAvailableBalance); + } + } +} diff --git a/TransactionMobile.Maui.UITests - Copy/TransactionMobile.Maui.UITests.csproj b/TransactionMobile.Maui.UITests - Copy/TransactionMobile.Maui.UITests.csproj new file mode 100644 index 00000000..c22100dd --- /dev/null +++ b/TransactionMobile.Maui.UITests - Copy/TransactionMobile.Maui.UITests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + + false + + Debug;Release;TestAuomation + + + + + + + + + + + + + + + + + diff --git a/TransactionMobile.Maui.UiTests/Common/BaseTestFixture.cs b/TransactionMobile.Maui.UiTests/Common/BaseTestFixture.cs new file mode 100644 index 00000000..09271d7e --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Common/BaseTestFixture.cs @@ -0,0 +1,17 @@ +using OpenQA.Selenium.Appium; +using TransactionMobile.Maui.UiTests.Drivers; + +namespace TransactionMobile.Maui.UITests.Common +{ + public abstract class BaseTestFixture + { + #region Constructors + + protected BaseTestFixture(MobileTestPlatform mobileTestPlatform) + { + AppiumDriverWrapper.MobileTestPlatform = mobileTestPlatform; + } + + #endregion + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UiTests/Common/Retry.cs b/TransactionMobile.Maui.UiTests/Common/Retry.cs new file mode 100644 index 00000000..c82c362a --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Common/Retry.cs @@ -0,0 +1,68 @@ +namespace TransactionMobile.Maui.UITests.Common; + +using System; +using System.Threading; +using System.Threading.Tasks; + +public static class Retry +{ + #region Fields + + /// + /// The default retry for + /// + private static readonly TimeSpan DefaultRetryFor = TimeSpan.FromSeconds(60); + + /// + /// The default retry interval + /// + private static readonly TimeSpan DefaultRetryInterval = TimeSpan.FromSeconds(5); + + #endregion + + #region Methods + + /// + /// Fors the specified action. + /// + /// The action. + /// The retry for. + /// The retry interval. + /// + public static async Task For(Func action, + TimeSpan? retryFor = null, + TimeSpan? retryInterval = null) + { + DateTime startTime = DateTime.Now; + Exception lastException = null; + + if (retryFor == null) + { + retryFor = Retry.DefaultRetryFor; + } + + while (DateTime.Now.Subtract(startTime).TotalMilliseconds < retryFor.Value.TotalMilliseconds) + { + try + { + await action().ConfigureAwait(false); + lastException = null; + break; + } + catch (Exception e) + { + lastException = e; + + // wait before retrying + Thread.Sleep(retryInterval ?? Retry.DefaultRetryInterval); + } + } + + if (lastException != null) + { + throw lastException; + } + } + + #endregion +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UiTests/Common/SpecflowTableHelper.cs b/TransactionMobile.Maui.UiTests/Common/SpecflowTableHelper.cs new file mode 100644 index 00000000..2e5e2846 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Common/SpecflowTableHelper.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.UITests.Common +{ + using TechTalk.SpecFlow; + + public static class SpecflowTableHelper + { + #region Methods + + /// + /// Gets the enum value. + /// + /// + /// The row. + /// The key. + /// + public static T GetEnumValue(TableRow row, + String key) where T : struct + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + Enum.TryParse(field, out T myEnum); + + return myEnum; + } + + /// + /// Gets the boolean value. + /// + /// The row. + /// The key. + /// + public static Boolean GetBooleanValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + return bool.TryParse(field, out Boolean value) && value; + } + + /// + /// Gets the date for date string. + /// + /// The date string. + /// The today. + /// + public static DateTime GetDateForDateString(String dateString, + DateTime today) + { + switch (dateString.ToUpper()) + { + case "TODAY": + return today.Date; + case "YESTERDAY": + return today.AddDays(-1).Date; + case "LASTWEEK": + return today.AddDays(-7).Date; + case "LASTMONTH": + return today.AddMonths(-1).Date; + case "LASTYEAR": + return today.AddYears(-1).Date; + case "TOMORROW": + return today.AddDays(1).Date; + default: + return DateTime.Parse(dateString); + } + } + + /// + /// Gets the decimal value. + /// + /// The row. + /// The key. + /// + public static Decimal GetDecimalValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + return decimal.TryParse(field, out Decimal value) ? value : 0; + } + + /// + /// Gets the int value. + /// + /// The row. + /// The key. + /// + public static Int32 GetIntValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + return int.TryParse(field, out Int32 value) ? value : -1; + } + + /// + /// Gets the short value. + /// + /// The row. + /// The key. + /// + public static Int16 GetShortValue(TableRow row, + String key) + { + String field = SpecflowTableHelper.GetStringRowValue(row, key); + + if (short.TryParse(field, out Int16 value)) + { + return value; + } + + return -1; + } + + /// + /// Gets the string row value. + /// + /// The row. + /// The key. + /// + public static String GetStringRowValue(TableRow row, + String key) + { + return row.TryGetValue(key, out String value) ? value : ""; + } + + #endregion + } +} diff --git a/TransactionMobile.Maui.UiTests/Drivers/AppiumDriver.cs b/TransactionMobile.Maui.UiTests/Drivers/AppiumDriver.cs new file mode 100644 index 00000000..44b7823e --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Drivers/AppiumDriver.cs @@ -0,0 +1,108 @@ +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium.Enums; +using OpenQA.Selenium.Appium.Service; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.UiTests.Drivers +{ + public enum MobileTestPlatform + { + iOS, + Android, + Windows, + MacCatalyst + } + + public class AppiumDriverWrapper + { + public static MobileTestPlatform MobileTestPlatform; + public static AppiumDriver Driver; + + public void StartApp() + { + + var streamWriter = new StreamWriter("C:\\Temp\\Debugging.log", append:true); + try + { + + AppiumLocalService appiumService = new AppiumServiceBuilder().UsingPort(4723).Build(); + + if (appiumService.IsRunning == false) + { + appiumService.Start(); + appiumService.OutputDataReceived += (sender, args) => { Console.WriteLine(args.Data); }; + } + + if (AppiumDriverWrapper.MobileTestPlatform == MobileTestPlatform.Android) { + AppiumDriverWrapper.SetupAndroidDriver(appiumService); + } + else if (AppiumDriverWrapper.MobileTestPlatform == MobileTestPlatform.iOS) { + AppiumDriverWrapper.SetupiOSDriver(appiumService); + } + + } + catch (Exception e) + { + streamWriter.Close(); + throw; + } + } + + private static void SetupiOSDriver(AppiumLocalService appiumService) { + var driverOptions = new AppiumOptions(); + driverOptions.AutomationName = "XCUITest"; + driverOptions.PlatformName = "iOS"; + driverOptions.PlatformVersion = "15.4"; + driverOptions.DeviceName = "iPhone 11"; + + String assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + String binariesFolder = Path.Combine(assemblyFolder, "..", "..", "..", "..", @"TransactionMobile.Maui/bin/Release/net6.0-ios/iossimulator-x64/"); + var apkPath = Path.Combine(binariesFolder, "TransactionMobile.Maui.app"); + driverOptions.App = apkPath; + driverOptions.AddAdditionalAppiumOption(MobileCapabilityType.FullReset, true); + driverOptions.AddAdditionalAppiumOption("useNewWDA", true); + driverOptions.AddAdditionalAppiumOption("wdaLaunchTimeout", 999999999); + driverOptions.AddAdditionalAppiumOption("wdaConnectionTimeout", 999999999); + driverOptions.AddAdditionalAppiumOption("restart", true); + + AppiumDriverWrapper.Driver = new OpenQA.Selenium.Appium.iOS.IOSDriver(appiumService, driverOptions, TimeSpan.FromMinutes(10)); + } + + private static void SetupAndroidDriver(AppiumLocalService appiumService) { + // Do Android stuff to start up + var driverOptions = new AppiumOptions(); + driverOptions.AddAdditionalAppiumOption("adbExecTimeout", TimeSpan.FromMinutes(5).Milliseconds); + driverOptions.AutomationName = "UIAutomator2"; + driverOptions.PlatformName = "Android"; + driverOptions.PlatformVersion = "9.0"; + driverOptions.DeviceName = "emulator-5554"; + + // TODO: Only do this locally + //driverOptions.AddAdditionalAppiumOption(MobileCapabilityType.FullReset, true); + driverOptions.AddAdditionalAppiumOption("appPackage", "com.transactionprocessing.pos"); + //driverOptions.AddAdditionalAppiumOption("forceEspressoRebuild", true); + driverOptions.AddAdditionalAppiumOption("enforceAppInstall", true); + //driverOptions.AddAdditionalAppiumOption("noSign", true); + //driverOptions.AddAdditionalAppiumOption("espressoBuildConfig", + // "{ \"additionalAppDependencies\": [ \"com.google.android.material:material:1.0.0\", \"androidx.lifecycle:lifecycle-extensions:2.1.0\" ] }"); + + String assemblyFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + String binariesFolder = Path.Combine(assemblyFolder, "..", "..", "..", "..", @"TransactionMobile.Maui/bin/Release/net6.0-android/"); + + var apkPath = Path.Combine(binariesFolder, "com.transactionprocessing.pos.apk"); + driverOptions.App = apkPath; + AppiumDriverWrapper.Driver = new OpenQA.Selenium.Appium.Android.AndroidDriver(appiumService, driverOptions, TimeSpan.FromMinutes(5)); + } + + public void StopApp() + { + AppiumDriverWrapper.Driver?.CloseApp(); + } + } +} diff --git a/TransactionMobile.Maui.UiTests/Features/Login.feature b/TransactionMobile.Maui.UiTests/Features/Login.feature new file mode 100644 index 00000000..a0715d25 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Features/Login.feature @@ -0,0 +1,13 @@ +@background @login +Feature: Login + +Background: + +@PRTest +Scenario: Login as Merchant + Given I am on the Login Screen + And the application is in training mode + When I enter 'merchantuser@testmerchant1.co.uk' as the Email Address + And I enter '123456' as the Password + And I tap on Login + Then the Merchant Home Page is displayed \ No newline at end of file diff --git a/TransactionMobile.Maui.UiTests/Features/Login.feature.cs b/TransactionMobile.Maui.UiTests/Features/Login.feature.cs new file mode 100644 index 00000000..fb66a47b --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Features/Login.feature.cs @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------------ +// +// This code was generated by SpecFlow (https://www.specflow.org/). +// SpecFlow Version:3.9.0.0 +// SpecFlow Generator Version:3.9.0.0 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// ------------------------------------------------------------------------------ +#region Designer generated code +#pragma warning disable +namespace TransactionMobile.Maui.UiTests.Features +{ + using TechTalk.SpecFlow; + using System; + using System.Linq; + + + [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [NUnit.Framework.TestFixtureAttribute()] + [NUnit.Framework.DescriptionAttribute("Login")] + [NUnit.Framework.CategoryAttribute("background")] + [NUnit.Framework.CategoryAttribute("login")] + public partial class LoginFeature + { + + private TechTalk.SpecFlow.ITestRunner testRunner; + + private static string[] featureTags = new string[] { + "background", + "login"}; + +#line 1 "Login.feature" +#line hidden + + [NUnit.Framework.OneTimeSetUpAttribute()] + public virtual void FeatureSetup() + { + testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); + TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "Login", null, ProgrammingLanguage.CSharp, featureTags); + testRunner.OnFeatureStart(featureInfo); + } + + [NUnit.Framework.OneTimeTearDownAttribute()] + public virtual void FeatureTearDown() + { + testRunner.OnFeatureEnd(); + testRunner = null; + } + + [NUnit.Framework.SetUpAttribute()] + public void TestInitialize() + { + } + + [NUnit.Framework.TearDownAttribute()] + public void TestTearDown() + { + testRunner.OnScenarioEnd(); + } + + public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) + { + testRunner.OnScenarioInitialize(scenarioInfo); + testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(NUnit.Framework.TestContext.CurrentContext); + } + + public void ScenarioStart() + { + testRunner.OnScenarioStart(); + } + + public void ScenarioCleanup() + { + testRunner.CollectScenarioErrors(); + } + + public virtual void FeatureBackground() + { +#line 4 +#line hidden + } + + [NUnit.Framework.TestAttribute()] + [NUnit.Framework.DescriptionAttribute("Login as Merchant")] + [NUnit.Framework.CategoryAttribute("PRTest")] + public void LoginAsMerchant() + { + string[] tagsOfScenario = new string[] { + "PRTest"}; + System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary(); + TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Login as Merchant", null, tagsOfScenario, argumentsOfScenario, featureTags); +#line 7 +this.ScenarioInitialize(scenarioInfo); +#line hidden + if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags))) + { + testRunner.SkipScenario(); + } + else + { + this.ScenarioStart(); +#line 4 +this.FeatureBackground(); +#line hidden +#line 8 + testRunner.Given("I am on the Login Screen", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); +#line hidden +#line 9 + testRunner.And("the application is in training mode", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 10 + testRunner.When("I enter \'merchantuser@testmerchant1.co.uk\' as the Email Address", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); +#line hidden +#line 11 + testRunner.And("I enter \'123456\' as the Password", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 12 + testRunner.And("I tap on Login", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "And "); +#line hidden +#line 13 + testRunner.Then("the Merchant Home Page is displayed", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); +#line hidden + } + this.ScenarioCleanup(); + } + } +} +#pragma warning restore +#endregion diff --git a/TransactionMobile.Maui.UiTests/Features/LoginFeature.cs b/TransactionMobile.Maui.UiTests/Features/LoginFeature.cs new file mode 100644 index 00000000..498cf8ae --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Features/LoginFeature.cs @@ -0,0 +1,16 @@ +using TransactionMobile.Maui.UITests.Common; +using TransactionMobile.Maui.UiTests.Drivers; + +namespace TransactionMobile.Maui.UiTests.Features; + +using NUnit.Framework; + +[TestFixture(MobileTestPlatform.Android, Category = "Android")] +[TestFixture(MobileTestPlatform.iOS, Category = "iOS")] +public partial class LoginFeature : BaseTestFixture +{ + public LoginFeature(MobileTestPlatform mobileTestPlatform) + : base(mobileTestPlatform) + { + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UiTests/Hooks/AppiumHooks.cs b/TransactionMobile.Maui.UiTests/Hooks/AppiumHooks.cs new file mode 100644 index 00000000..7058b7b0 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Hooks/AppiumHooks.cs @@ -0,0 +1,34 @@ +using OpenQA.Selenium.Appium; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TechTalk.SpecFlow; +using TransactionMobile.Maui.UiTests.Drivers; + +namespace TransactionMobile.Maui.UiTests.Hooks +{ + [Binding] + public class AppiumHooks + { + private readonly AppiumDriverWrapper _appiumDriver; + + public AppiumHooks(AppiumDriverWrapper appiumDriver) + { + _appiumDriver = appiumDriver; + } + + [BeforeScenario()] + public void StartApp() + { + _appiumDriver.StartApp(); + } + + [AfterScenario()] + public void ShutdownApp() + { + _appiumDriver.StopApp(); + } + } +} diff --git a/TransactionMobile.Maui.UiTests/Pages/BasePage.cs b/TransactionMobile.Maui.UiTests/Pages/BasePage.cs new file mode 100644 index 00000000..84137de5 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Pages/BasePage.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TransactionMobile.Maui.UiTests.Drivers; + +namespace TransactionMobile.Maui.UITests +{ + using Common; + using OpenQA.Selenium; + using Shouldly; + + public abstract class BasePage + { + protected abstract String Trait { get; } + + public async Task AssertOnPage(TimeSpan? timeout = null) + { + timeout = timeout ?? TimeSpan.FromSeconds(60); + + await Retry.For(async () => + { + String message = $"Unable to verify on page: {this.GetType().Name} {Environment.NewLine} Source: {AppiumDriverWrapper.Driver.PageSource}"; + + Should.NotThrow(() => this.WaitForElementByAccessibilityId(this.Trait), message); + }, + TimeSpan.FromMinutes(1), + timeout).ConfigureAwait(false); + } + + /// + /// 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 = null) + { + timeout = timeout ?? TimeSpan.FromSeconds(5); + var message = "Unable to verify *not* on page: " + this.GetType().Name; + + Should.NotThrow(() => this.WaitForNoElementByAccessibilityId(this.Trait), message); + } + + public async Task WaitForElementByAccessibilityId(String accessibilityId, TimeSpan? timeout = null) + { + return await AppiumDriverWrapper.Driver.WaitForElementByAccessibilityId(accessibilityId, timeout); + } + + public async Task GetPageSource() + { + return await AppiumDriverWrapper.Driver.GetPageSource(); + } + + public async Task WaitForNoElementByAccessibilityId(String accessibilityId) + { + await AppiumDriverWrapper.Driver.WaitForNoElementByAccessibilityId(accessibilityId); + } + + public async Task WaitForToastMessage(String toastMessage) + { + await AppiumDriverWrapper.Driver.WaitForToastMessage(AppiumDriverWrapper.MobileTestPlatform, toastMessage); + } + + public void HideKeyboard() + { + //AppiumDriverWrapper.Driver.HideKeyboard(); + if (AppiumDriverWrapper.MobileTestPlatform == MobileTestPlatform.Android) + { + AppiumDriverWrapper.Driver.HideKeyboard(); + } + else if (AppiumDriverWrapper.MobileTestPlatform == MobileTestPlatform.iOS) + { + AppiumDriverWrapper.Driver.FindElement(By.Name("Done")).Click(); + } + } + + public IWebElement GetAlert() + { + return AppiumDriverWrapper.Driver.FindElement(By.Name("OK")); + } + + public IAlert SwitchToAlert() + { + return AppiumDriverWrapper.Driver.SwitchTo().Alert(); + } + + public void NavigateBack() + { + AppiumDriverWrapper.Driver.Navigate().Back(); + } + } +} diff --git a/TransactionMobile.Maui.UiTests/Pages/Extenstions.cs b/TransactionMobile.Maui.UiTests/Pages/Extenstions.cs new file mode 100644 index 00000000..72d87cb0 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Pages/Extenstions.cs @@ -0,0 +1,95 @@ +using OpenQA.Selenium.Appium; +using TransactionMobile.Maui.UiTests.Drivers; + +namespace TransactionMobile.Maui.UITests; + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Common; +using OpenQA.Selenium; +using OpenQA.Selenium.Appium.Android; +using OpenQA.Selenium.Appium.iOS; +using Shouldly; + +public static class Extenstions +{ + // TODO: Mac & Windows Extensions + // TODO: May need a platform switch + //public static AndroidElement GetAlert(this AndroidDriver driver) + //{ + // return driver.FindElementByClassName("androidx.appcompat.widget.AppCompatTextView"); + //} + + public static async Task WaitForElementByAccessibilityId(this AppiumDriver driver, + String selector, + TimeSpan? timeout = null) { + IWebElement? element = null; + timeout ??= TimeSpan.FromSeconds(60); + + await Retry.For(async () => + { + element = driver.FindElement(MobileBy.AccessibilityId(selector)); + element.ShouldNotBeNull(); + }); + return element; + } + + public static async Task WaitForNoElementByAccessibilityId(this AppiumDriver driver, + String selector, + TimeSpan? timeout = null) + { + timeout ??= TimeSpan.FromSeconds(60); + + await Retry.For(async () => + { + IWebElement? element = driver.FindElement(MobileBy.AccessibilityId(selector)); + element.ShouldBeNull(); + }); + + } + + public static async Task WaitForToastMessage(this AppiumDriver driver, MobileTestPlatform platform, String expectedToast) + { + if (platform == MobileTestPlatform.Android) + { + await Retry.For(async () => + { + Dictionary args = new Dictionary + { + {"text", expectedToast}, + {"isRegexp", false} + }; + driver.ExecuteScript("mobile: isToastVisible", args); + + }); + } + else if (platform == MobileTestPlatform.iOS) + { + Boolean isDisplayed = false; + int count = 0; + do + { + if (driver.PageSource.Contains(expectedToast)) + { + Console.WriteLine(driver.PageSource); + isDisplayed = true; + break; + } + + Thread.Sleep(200); //Add your custom wait if exists + count++; + + } while (count < 10); + + Console.WriteLine(driver.PageSource); + isDisplayed.ShouldBeTrue(); + } + } + + public static async Task GetPageSource(this AppiumDriver driver) + { + return driver.PageSource; + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UiTests/Pages/LoginPage.cs b/TransactionMobile.Maui.UiTests/Pages/LoginPage.cs new file mode 100644 index 00000000..db5b56c1 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Pages/LoginPage.cs @@ -0,0 +1,85 @@ +using TransactionMobile.Maui.UiTests.Drivers; + +namespace TransactionMobile.Maui.UITests; + +using System; +using System.Threading.Tasks; +using OpenQA.Selenium; + +public class LoginPage : BasePage +{ + protected override String Trait => "LoginLabel"; + + private readonly String UserNameEntry; + private readonly String PasswordEntry; + private readonly String LoginButton; + private readonly String UseTrainingModeSwitch; + //private readonly String TestModeButton; + //private readonly String ErrorLabel; + + public LoginPage() + { + this.UserNameEntry = "UserNameEntry"; + this.PasswordEntry = "PasswordEntry"; + this.LoginButton = "LoginButton"; + this.UseTrainingModeSwitch = "UseTrainingModeSwitch"; + //this.TestModeButton = "TestModeButton"; + //this.ErrorLabel = "ErrorLabel"; + } + + public async Task IsTrainingModeOn() + { + IWebElement element = await this.WaitForElementByAccessibilityId(this.UseTrainingModeSwitch); + String? text = element.Text; + if (AppiumDriverWrapper.MobileTestPlatform == MobileTestPlatform.Android) { + if (text == "OFF") { + return false; + } + + return true; + } + if (AppiumDriverWrapper.MobileTestPlatform == MobileTestPlatform.iOS) { + if (text == "0") { + return false; + } + + return true; + } + + return true; + } + + public async Task SetTrainingModeOn() + { + IWebElement element = await this.WaitForElementByAccessibilityId(this.UseTrainingModeSwitch); + var text = element.Text; + element.Click(); + } + + public async Task SetTrainingModeOff() + { + IWebElement element = await this.WaitForElementByAccessibilityId(this.UseTrainingModeSwitch); + + element.Click(); + } + + public async Task EnterEmailAddress(String emailAddress) + { + IWebElement element = await this.WaitForElementByAccessibilityId(this.UserNameEntry); + + element.SendKeys(emailAddress); + } + + public async Task EnterPassword(String password) + { + IWebElement element = await this.WaitForElementByAccessibilityId(this.PasswordEntry); + element.SendKeys(password); + } + + public async Task ClickLoginButton() + { + //this.HideKeyboard(); + IWebElement element = await this.WaitForElementByAccessibilityId(this.LoginButton); + element.Click(); + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UiTests/Pages/MainPage.cs b/TransactionMobile.Maui.UiTests/Pages/MainPage.cs new file mode 100644 index 00000000..472c283d --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Pages/MainPage.cs @@ -0,0 +1,71 @@ +namespace TransactionMobile.Maui.UITests; + +using System; +using System.Threading.Tasks; + +public class MainPage : BasePage +{ + protected override String Trait => "Home"; + + private readonly String TransactionsButton; + + private readonly String ReportsButton; + + private readonly String ProfileButton; + + private readonly String SupportButton; + + private readonly String AvailableBalanceLabel; + + /// + /// Initializes a new instance of the class. + /// + public MainPage() + { + this.TransactionsButton = "TransactionsButton"; + this.ReportsButton = "ReportsButton"; + this.ProfileButton = "ProfileButton"; + this.SupportButton = "SupportButton"; + this.AvailableBalanceLabel = "AvailableBalanceValueLabel"; + } + + public async Task ClickTransactionsButton() + { + //var element = await this.WaitForElementByAccessibilityId(this.TransactionsButton); + //element.Click(); + } + + public async Task ClickReportsButton() + { + //var element = await this.WaitForElementByAccessibilityId(this.ReportsButton); + //element.Click(); + } + + public void ClickProfileButton() + { + //app.WaitForElement(this.ProfileButton); + //app.Tap(this.ProfileButton); + } + + public void ClickSupportButton() + { + //app.WaitForElement(this.SupportButton); + //app.Tap(this.SupportButton); + } + + public async Task GetAvailableBalanceValue(TimeSpan? timeout = default(TimeSpan?)) + { + //await this.ScrollTo(this.Trait, this.AvailableBalanceLabel); + //var element = await this.WaitForElementByAccessibilityId(this.AvailableBalanceLabel, timeout: TimeSpan.FromSeconds(30)); + + //String availableBalanceText = element.Text.Replace(" KES", String.Empty); + + //if (Decimal.TryParse(availableBalanceText, out Decimal balanceValue) == false) + //{ + // throw new Exception($"Failed to parse [{availableBalanceText}] as a Decimal"); + //} + + //return balanceValue; + return 0; + } +} \ No newline at end of file diff --git a/TransactionMobile.Maui.UiTests/Steps/LoginSteps.cs b/TransactionMobile.Maui.UiTests/Steps/LoginSteps.cs new file mode 100644 index 00000000..9d26ad55 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/Steps/LoginSteps.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TransactionMobile.Maui.UITests.Steps +{ + using TechTalk.SpecFlow; + + [Binding] + [Scope(Tag = "login")] + public class LoginSteps + { + LoginPage loginPage = new LoginPage(); + MainPage mainPage = new MainPage(); + + [Given(@"I am on the Login Screen")] + public async Task GivenIAmOnTheLoginScreen() + { + await this.loginPage.AssertOnPage(); + } + + [Given(@"the application is in training mode")] + public async Task GivenTheApplicationIsInTrainingMode() { + var isTrainingModeOn = await this.loginPage.IsTrainingModeOn(); + + if (isTrainingModeOn == false) + await this.loginPage.SetTrainingModeOn(); + } + + + [When(@"I enter '(.*)' as the Email Address")] + public async Task WhenIEnterAsTheEmailAddress(String emailAddress) + { + await this.loginPage.EnterEmailAddress(emailAddress); + } + + [When(@"I enter '(.*)' as the Password")] + public async Task WhenIEnterAsThePassword(String password) + { + await this.loginPage.EnterPassword(password); + } + + [When(@"I tap on Login")] + public async Task WhenITapOnLogin() + { + await this.loginPage.ClickLoginButton(); + } + + [Then(@"the Merchant Home Page is displayed")] + public async Task ThenTheMerchantHomePageIsDisplayed() + { + await this.mainPage.AssertOnPage(); + } + + [Then(@"the available balance is shown as (.*)")] + public async Task ThenTheAvailableBalanceIsShownAs(Decimal expectedAvailableBalance) + { + //Decimal availableBalance = await this.mainPage.GetAvailableBalanceValue(TimeSpan.FromSeconds(120)).ConfigureAwait(false); + //availableBalance.ShouldBe(expectedAvailableBalance); + } + } +} diff --git a/TransactionMobile.Maui.UiTests/TransactionMobile.Maui.UiTests.csproj b/TransactionMobile.Maui.UiTests/TransactionMobile.Maui.UiTests.csproj new file mode 100644 index 00000000..6f8ea286 --- /dev/null +++ b/TransactionMobile.Maui.UiTests/TransactionMobile.Maui.UiTests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + diff --git a/TransactionMobile.Maui.sln b/TransactionMobile.Maui.sln index 5a1818ea..95de4fa9 100644 --- a/TransactionMobile.Maui.sln +++ b/TransactionMobile.Maui.sln @@ -18,10 +18,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Nuget.config = Nuget.config EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TransactionMobile.Maui.UiTests", "TransactionMobile.Maui.UiTests\TransactionMobile.Maui.UiTests.csproj", "{1F7CE7A2-6350-4F8A-B758-5E275C9C88C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU + TestAuomation|Any CPU = TestAuomation|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {73668181-7A26-435D-83E3-CF141AC8FD0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -30,14 +33,27 @@ Global {73668181-7A26-435D-83E3-CF141AC8FD0B}.Release|Any CPU.ActiveCfg = Release|Any CPU {73668181-7A26-435D-83E3-CF141AC8FD0B}.Release|Any CPU.Build.0 = Release|Any CPU {73668181-7A26-435D-83E3-CF141AC8FD0B}.Release|Any CPU.Deploy.0 = Release|Any CPU + {73668181-7A26-435D-83E3-CF141AC8FD0B}.TestAuomation|Any CPU.ActiveCfg = Release|Any CPU + {73668181-7A26-435D-83E3-CF141AC8FD0B}.TestAuomation|Any CPU.Build.0 = Release|Any CPU + {73668181-7A26-435D-83E3-CF141AC8FD0B}.TestAuomation|Any CPU.Deploy.0 = Release|Any CPU {0894F054-5C4D-4DDD-A8E9-636416189234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0894F054-5C4D-4DDD-A8E9-636416189234}.Debug|Any CPU.Build.0 = Debug|Any CPU {0894F054-5C4D-4DDD-A8E9-636416189234}.Release|Any CPU.ActiveCfg = Release|Any CPU {0894F054-5C4D-4DDD-A8E9-636416189234}.Release|Any CPU.Build.0 = Release|Any CPU + {0894F054-5C4D-4DDD-A8E9-636416189234}.TestAuomation|Any CPU.ActiveCfg = Release|Any CPU + {0894F054-5C4D-4DDD-A8E9-636416189234}.TestAuomation|Any CPU.Build.0 = Release|Any CPU {902D54CF-CD5F-4932-B1DC-01A3937AC054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {902D54CF-CD5F-4932-B1DC-01A3937AC054}.Debug|Any CPU.Build.0 = Debug|Any CPU {902D54CF-CD5F-4932-B1DC-01A3937AC054}.Release|Any CPU.ActiveCfg = Release|Any CPU {902D54CF-CD5F-4932-B1DC-01A3937AC054}.Release|Any CPU.Build.0 = Release|Any CPU + {902D54CF-CD5F-4932-B1DC-01A3937AC054}.TestAuomation|Any CPU.ActiveCfg = Release|Any CPU + {902D54CF-CD5F-4932-B1DC-01A3937AC054}.TestAuomation|Any CPU.Build.0 = Release|Any CPU + {1F7CE7A2-6350-4F8A-B758-5E275C9C88C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F7CE7A2-6350-4F8A-B758-5E275C9C88C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F7CE7A2-6350-4F8A-B758-5E275C9C88C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F7CE7A2-6350-4F8A-B758-5E275C9C88C9}.Release|Any CPU.Build.0 = Release|Any CPU + {1F7CE7A2-6350-4F8A-B758-5E275C9C88C9}.TestAuomation|Any CPU.ActiveCfg = Debug|Any CPU + {1F7CE7A2-6350-4F8A-B758-5E275C9C88C9}.TestAuomation|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +62,7 @@ Global {73668181-7A26-435D-83E3-CF141AC8FD0B} = {1CBEF4C1-7D90-4A78-AA55-D81F1447A70E} {0894F054-5C4D-4DDD-A8E9-636416189234} = {AB312EE3-CBA4-469A-8694-67C5466298C5} {902D54CF-CD5F-4932-B1DC-01A3937AC054} = {1CBEF4C1-7D90-4A78-AA55-D81F1447A70E} + {1F7CE7A2-6350-4F8A-B758-5E275C9C88C9} = {AB312EE3-CBA4-469A-8694-67C5466298C5} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {61F7FB11-1E47-470C-91E2-47F8143E1572} diff --git a/TransactionMobile.Maui/App.xaml.cs b/TransactionMobile.Maui/App.xaml.cs index d4fd107f..12cb5d6a 100644 --- a/TransactionMobile.Maui/App.xaml.cs +++ b/TransactionMobile.Maui/App.xaml.cs @@ -1,9 +1,13 @@ -using TransactionMobile.Maui.Pages.Reports; +using Microsoft.Maui.Platform; +using TransactionMobile.Maui.Pages.Reports; using TransactionMobile.Maui.Pages.Transactions.MobileTopup; using TransactionMobile.Maui.Pages.Transactions.Voucher; namespace TransactionMobile.Maui; +using BusinessLogic.ViewModels; +using Microsoft.Maui.Handlers; +using Pages; using Pages.AppHome; using Pages.Transactions.Admin; using TransactionMobile.Maui.BusinessLogic.Services; @@ -15,7 +19,7 @@ public App() { InitializeComponent(); - Microsoft.Maui.Handlers.EntryHandler.ElementMapper.AppendToMapping("TrainingMode", (handler, view) => + Microsoft.Maui.Handlers.LabelHandler.ElementMapper.AppendToMapping("TrainingMode", (handler, view) => { if (view is TitleLabel) { @@ -30,7 +34,109 @@ public App() } }); - MainPage = new AppShell(); +#if ANDROID + ViewHandler.ViewMapper.ModifyMapping("AutomationId", (handler, view, previousAction) => + { + if (handler.PlatformView is Android.Views.View androidView) + { + if (String.IsNullOrWhiteSpace(view.AutomationId)) + return; + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is not AutomationIdDelegate) + AndroidX.Core.View.ViewCompat.SetAccessibilityDelegate(androidView, new AutomationIdDelegate()); + + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is AutomationIdDelegate td) + td.AutomationId = view.AutomationId; + + androidView.ContentDescription = view.AutomationId; + } + }); + + EntryHandler.Mapper.ModifyMapping("AutomationId", (handler, view, previousAction) => + { + if (handler.PlatformView is Android.Views.View androidView) + { + if (String.IsNullOrWhiteSpace(view.AutomationId)) + return; + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is not AutomationIdDelegate) + AndroidX.Core.View.ViewCompat.SetAccessibilityDelegate(androidView, new AutomationIdDelegate()); + + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is AutomationIdDelegate td) + td.AutomationId = view.AutomationId; + + androidView.ContentDescription = view.AutomationId; + } + }); + + SwitchHandler.Mapper.ModifyMapping("AutomationId", (handler, view, previousAction) => + { + if (handler.PlatformView is Android.Views.View androidView) + { + if (String.IsNullOrWhiteSpace(view.AutomationId)) + return; + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is not AutomationIdDelegate) + AndroidX.Core.View.ViewCompat.SetAccessibilityDelegate(androidView, new AutomationIdDelegate()); + + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is AutomationIdDelegate td) + td.AutomationId = view.AutomationId; + + androidView.ContentDescription = view.AutomationId; + } + }); + + LabelHandler.Mapper.ModifyMapping("AutomationId", (handler, view, previousAction) => + { + if (handler.PlatformView is Android.Views.View androidView) + { + if (String.IsNullOrWhiteSpace(view.AutomationId)) + return; + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is not AutomationIdDelegate) + AndroidX.Core.View.ViewCompat.SetAccessibilityDelegate(androidView, new AutomationIdDelegate()); + + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is AutomationIdDelegate td) + td.AutomationId = view.AutomationId; + + androidView.ContentDescription = view.AutomationId; + } + }); + + ButtonHandler.Mapper.ModifyMapping("AutomationId", (handler, view, previousAction) => + { + if (handler.PlatformView is Android.Views.View androidView) + { + if (String.IsNullOrWhiteSpace(view.AutomationId)) + return; + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is not AutomationIdDelegate) + AndroidX.Core.View.ViewCompat.SetAccessibilityDelegate(androidView, new AutomationIdDelegate()); + + + if (AndroidX.Core.View.ViewCompat.GetAccessibilityDelegate(androidView) is AutomationIdDelegate td) + td.AutomationId = view.AutomationId; + + androidView.ContentDescription = view.AutomationId; + } + }); + +#endif + var memoryCache = MauiProgram.Container.Services.GetService(); + memoryCache.TryGetValue("isLoggedIn", out bool isLoggedIn); + + if (isLoggedIn) + { + MainPage = new AppShell(); + } + else { + var loginPageViewModel = MauiProgram.Container.Services.GetService(); + MainPage = new LoginPage(loginPageViewModel); + } Routing.RegisterRoute(nameof(MobileTopupSelectOperatorPage), typeof(MobileTopupSelectOperatorPage)); Routing.RegisterRoute(nameof(MobileTopupSelectProductPage), typeof(MobileTopupSelectProductPage)); @@ -45,6 +151,24 @@ public App() Routing.RegisterRoute(nameof(VoucherIssueFailedPage), typeof(VoucherIssueFailedPage)); Routing.RegisterRoute(nameof(AdminPage), typeof(AdminPage)); + Routing.RegisterRoute(nameof(LoginPage), typeof(LoginPage)); + } +} + +#if ANDROID +public class AutomationIdDelegate : MauiAccessibilityDelegateCompat +{ + public string AutomationId { get; internal set; } + + public override void OnInitializeAccessibilityNodeInfo(Android.Views.View host, AndroidX.Core.View.Accessibility.AccessibilityNodeInfoCompat info) + { + base.OnInitializeAccessibilityNodeInfo(host, info); + + if (!String.IsNullOrWhiteSpace(AutomationId)) + { + info.ViewIdResourceName = $"{host.Context.PackageName}:id/{AutomationId}"; + } } } +#endif diff --git a/TransactionMobile.Maui/AppShell.xaml b/TransactionMobile.Maui/AppShell.xaml index b77df859..1ae4af7c 100644 --- a/TransactionMobile.Maui/AppShell.xaml +++ b/TransactionMobile.Maui/AppShell.xaml @@ -14,13 +14,10 @@ --> - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TransactionMobile.Maui/AppShell.xaml.cs b/TransactionMobile.Maui/AppShell.xaml.cs index d169e3cd..c3345b52 100644 --- a/TransactionMobile.Maui/AppShell.xaml.cs +++ b/TransactionMobile.Maui/AppShell.xaml.cs @@ -1,9 +1,19 @@ namespace TransactionMobile.Maui; +using System.Diagnostics; + public partial class AppShell : Shell { public AppShell() { InitializeComponent(); } + + protected override void OnNavigating(ShellNavigatingEventArgs args) { + base.OnNavigating(args); + } + + protected override void OnNavigated(ShellNavigatedEventArgs args) { + base.OnNavigated(args); + } } \ No newline at end of file diff --git a/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs b/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs index 6c0b4d78..b9497e8f 100644 --- a/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs +++ b/TransactionMobile.Maui/Extensions/MauiAppBuilderExtensions.cs @@ -32,7 +32,7 @@ public static class MauiAppBuilderExtensions public static MauiAppBuilder ConfigureDatabase(this MauiAppBuilder builder) { - String connectionString = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "transactionpos.db"); + String connectionString = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "transactionpos1.db"); Func logLevelFunc = new Func( () => { return Database.LogLevel.Debug; diff --git a/TransactionMobile.Maui/MauiProgram.cs b/TransactionMobile.Maui/MauiProgram.cs index c8d6597b..9a1dc567 100644 --- a/TransactionMobile.Maui/MauiProgram.cs +++ b/TransactionMobile.Maui/MauiProgram.cs @@ -21,7 +21,6 @@ public static MauiApp CreateMauiApp() Platforms.Services.DangerousTrustProvider.Register(); #endif - //raw.SetProvider(new SQLite3Provider_sqlite3()); Builder = MauiApp.CreateBuilder(); Builder.UseMauiApp() .ConfigureRequestHandlers() diff --git a/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs b/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs index b90be145..eb959471 100644 --- a/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs +++ b/TransactionMobile.Maui/Pages/AppHome/HomePage.xaml.cs @@ -6,4 +6,8 @@ public HomePage() { InitializeComponent(); } + + protected override void OnAppearing() { + base.OnAppearing(); + } } \ No newline at end of file diff --git a/TransactionMobile.Maui/Pages/LoginPage.xaml b/TransactionMobile.Maui/Pages/LoginPage.xaml index 876d172b..57f69a14 100644 --- a/TransactionMobile.Maui/Pages/LoginPage.xaml +++ b/TransactionMobile.Maui/Pages/LoginPage.xaml @@ -4,7 +4,7 @@ x:Class="TransactionMobile.Maui.Pages.LoginPage" Title="LoginPage" Shell.NavBarIsVisible="False" - Shell.FlyoutItemIsVisible="False"> + Shell.FlyoutItemIsVisible="True"> @@ -12,7 +12,8 @@ -