From 1ece3d6cf887e7403459c0f367d7712a36e2bf3e Mon Sep 17 00:00:00 2001 From: Alex Bance Date: Thu, 24 Jun 2021 08:27:24 +0100 Subject: [PATCH] feat: bypass login with browser profiles (#87) Improves performance by allowing the use of Chrome and Firefox profiles for test users. +semver: minor --- README.md | 25 ++-- ...apgemini.PowerApps.SpecFlowBindings.csproj | 1 + .../BrowserOptionsWithProfileSupport.cs | 52 ++++++++ .../Configuration/ConfigHelper.cs | 5 + .../Configuration/TestConfiguration.cs | 19 ++- .../Extensions/BrowserTypeExtensions.cs | 34 +++++ .../Extensions/DirectoryInfoExtensions.cs | 30 +++++ .../Hooks/AfterScenarioHooks.cs | 2 + .../Hooks/BeforeRunHooks.cs | 49 ++++++++ .../PowerAppsStepDefiner.cs | 119 +++++++++++++++++- .../Steps/LoginSteps.cs | 79 ++++++++++-- .../content/power-apps-bindings.yml | 1 + .../Hooks/MockSolutionHooks.cs | 30 ++--- .../LookupSteps.feature | 2 + .../power-apps-bindings.yml | 1 + templates/include-build-and-test-steps.yml | 2 +- 16 files changed, 411 insertions(+), 40 deletions(-) create mode 100644 bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/BrowserOptionsWithProfileSupport.cs create mode 100644 bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/BrowserTypeExtensions.cs create mode 100644 bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/DirectoryInfoExtensions.cs create mode 100644 bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/BeforeRunHooks.cs diff --git a/README.md b/README.md index 5b1b05a..34786c4 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The aim of this project is to make Power Apps test automation easier, faster and ## Installation -Follow the guidance in the **Installation and Setup** section in https://specflow.org/getting-started/. After installing the IDE integration and setting up your project, install the NuGet package. +Follow the guidance in the **Installation and Setup** section in SpecFlow's [docs](https://specflow.org/getting-started/). After installing the IDE integration and setting up your project, install the NuGet package. ```shell PM> Install-Package Capgemini.PowerApps.SpecFlowBindings @@ -40,7 +40,7 @@ Once the NuGet package is installed, follow the SpecFlow [documentation](https:/ ### WebDrivers -We do not have a dependency on any specific WebDrivers. You will need to ensure that the correct WebDrivers are available in your project based on the browser that you are targetting. For example - if your configuration file is targetting Chrome - you can install the Chrome WebDriver via NuGet - +We do not have a dependency on any specific WebDrivers. You will need to ensure that the correct WebDrivers are available in your project based on the browser that you are targetting. For example - if your configuration file is targetting Chrome - you can install the Chrome WebDriver via NuGet - ```shell PM> Install-Package Selenium.Chrome.WebDriver @@ -54,6 +54,7 @@ Installing the NuGet package creates a _power-apps-bindings.yml_ file in your pr ```yaml url: SPECFLOW_POWERAPPS_URL # mandatory +useProfiles: false # optional - defaults to false if not set browserOptions: # optional - will use default EasyRepro options if not set browserType: Chrome headless: true @@ -73,15 +74,19 @@ users: # mandatory The URL, driversPath, usernames, passwords, and application user details will be set from environment variable (if found). Otherwise, the value from the config file will be used. The browserOptions node supports anything in the EasyRepro `BrowserOptions` class. +#### User profiles + +Setting the `useProfiles` property to true causes the solution to create and use a unique [profile](https://support.google.com/chrome/answer/2364824?co=GENIE.Platform%3DDesktop&hl=en) for each user listed in the config file. This currently only works in Chrome & Firefox and attempting to use it with Edge or IE will cause an exception to be thrown. By using profiles test runs for the same user will not be required to re-authenticate, this saves time during test runs. [To take full advantage of this you will need to have the "Stay Signed In" prompt enabled.](https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/keep-me-signed-in) + ### Writing feature files You can use the predefined step bindings to write your tests. ```gherkin Scenario: User can create a new account - Given I am logged in to the 'Sales Team Member' app as 'an admin' - When I open the 'Accounts' sub area of the 'Customers' group - Then I can see the 'New' command + Given I am logged in to the 'Sales Team Member' app as 'an admin' + When I open the 'Accounts' sub area of the 'Customers' group + Then I can see the 'New' command ``` Alternatively, write your own step bindings (see below). @@ -114,14 +119,16 @@ You can create test data by using the following _Given_ steps - ```gherkin Given I have created 'a record' ``` + or + ```gherkin Given 'someone' has created 'a record with a difference' ``` These bindings look for a corresponding JSON file in a _data_ folder in the root of your project. The file is resolved using a combination of the directory structure and the file name. For example, the bindings above could resolve the following files: -``` +```shell └───data │ a record.json │ @@ -132,11 +139,11 @@ These bindings look for a corresponding JSON file in a _data_ folder in the root If you are using the binding which creates data as someone other than the current user, you will need the following configuration to be present: - a user with a matching alias in the `users` array that has the `username` set -- an application user with sufficient privileges to impersonate the above user configured in the `applicationUser` property. +- an application user with sufficient privileges to impersonate the above user configured in the `applicationUser` property. Refer to the Microsoft documentation on creating an application user [here](https://docs.microsoft.com/en-us/power-platform/admin/create-users-assign-online-security-roles#create-an-application-user). -#### Data file syntax +#### Data file syntax The JSON is similar to that expected by Web API when creating records via a [deep insert](https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/webapi/create-entity-web-api#create-related-entities-in-one-operation). @@ -157,6 +164,7 @@ The JSON is similar to that expected by Web API when creating records via a [dee ] } ``` + The example above will create the following: - An account @@ -193,7 +201,6 @@ When using faker syntax, you must also annotate number or date fields using `@fa You can also dynamically set lookups by alias using `@alias.bind` (this is limited to aliased records in other files - not the current file). - ## Contributing Please refer to the [Contributing](./CONTRIBUTING.md) guide. diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj index 3f44608..2fae745 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj @@ -63,6 +63,7 @@ all + diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/BrowserOptionsWithProfileSupport.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/BrowserOptionsWithProfileSupport.cs new file mode 100644 index 0000000..1a4cdad --- /dev/null +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/BrowserOptionsWithProfileSupport.cs @@ -0,0 +1,52 @@ +namespace Capgemini.PowerApps.SpecFlowBindings.Configuration +{ + using System; + using System.IO; + using Microsoft.Dynamics365.UIAutomation.Browser; + using OpenQA.Selenium.Chrome; + using OpenQA.Selenium.Firefox; + + /// + /// Extends the EasyRepro class with additonal support for chrome profiles. + /// + public class BrowserOptionsWithProfileSupport : BrowserOptions, ICloneable + { + /// + /// Gets or sets the directory to use as the user profile. + /// + public string ProfileDirectory { get; set; } + + /// + public object Clone() + { + return this.MemberwiseClone(); + } + + /// + public override ChromeOptions ToChrome() + { + var options = base.ToChrome(); + + if (!string.IsNullOrEmpty(this.ProfileDirectory)) + { + options.AddArgument($"--user-data-dir={this.ProfileDirectory}"); + } + + return options; + } + + /// + public override FirefoxOptions ToFireFox() + { + var options = base.ToFireFox(); + + if (!string.IsNullOrEmpty(this.ProfileDirectory)) + { + this.ProfileDirectory = this.ProfileDirectory.EndsWith("firefox") ? this.ProfileDirectory : Path.Combine(this.ProfileDirectory, "firefox"); + options.AddArgument($"-profile \"{this.ProfileDirectory}\""); + } + + return options; + } + } +} diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/ConfigHelper.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/ConfigHelper.cs index 9aa5274..7d70c74 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/ConfigHelper.cs +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/ConfigHelper.cs @@ -14,6 +14,11 @@ public static class ConfigHelper /// The environment variable value (if found) or the passed in value. public static string GetEnvironmentVariableIfExists(string value) { + if (string.IsNullOrEmpty(value)) + { + return value; + } + var environmentVariableValue = Environment.GetEnvironmentVariable(value); if (!string.IsNullOrEmpty(environmentVariableValue)) diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/TestConfiguration.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/TestConfiguration.cs index 0c4a1b7..6fdd499 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/TestConfiguration.cs +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/TestConfiguration.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Configuration; using System.Linq; - using Microsoft.Dynamics365.UIAutomation.Browser; using YamlDotNet.Serialization; /// @@ -19,12 +18,14 @@ public class TestConfiguration private const string GetUserException = "Unable to retrieve user configuration. Please ensure a user with the given alias exists in the config."; + private string profilesBasePath; + /// /// Initializes a new instance of the class. /// public TestConfiguration() { - this.BrowserOptions = new BrowserOptions(); + this.BrowserOptions = new BrowserOptionsWithProfileSupport(); } /// @@ -33,11 +34,23 @@ public TestConfiguration() [YamlMember(Alias = "url")] public string Url { private get; set; } + /// + /// Gets or sets a value indicating whether to use profiles. + /// + [YamlMember(Alias = "useProfiles")] + public bool UseProfiles { get; set; } = false; + + /// + /// Gets or sets the base path where the user profiles are stored. + /// + [YamlMember(Alias = "profilesBasePath")] + public string ProfilesBasePath { get => ConfigHelper.GetEnvironmentVariableIfExists(this.profilesBasePath); set => this.profilesBasePath = value; } + /// /// Gets or sets the browser options to use for running tests. /// [YamlMember(Alias = "browserOptions")] - public BrowserOptions BrowserOptions { get; set; } + public BrowserOptionsWithProfileSupport BrowserOptions { get; set; } /// /// Gets or sets users that tests can be run as. diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/BrowserTypeExtensions.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/BrowserTypeExtensions.cs new file mode 100644 index 0000000..e1c1c0a --- /dev/null +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/BrowserTypeExtensions.cs @@ -0,0 +1,34 @@ +namespace Capgemini.PowerApps.SpecFlowBindings.Extensions +{ + using Microsoft.Dynamics365.UIAutomation.Browser; + + /// + /// Provides extension methods on . + /// + public static class BrowserTypeExtensions + { + /// + /// Determines if the given browser type supports profiles. + /// + /// The to check. + /// true if the browser supports profiles otherwise false. + public static bool SupportsProfiles(this BrowserType type) + { + switch (type) + { + case BrowserType.IE: + return false; + case BrowserType.Chrome: + return true; + case BrowserType.Firefox: + return true; + case BrowserType.Edge: + return false; + case BrowserType.Remote: + return false; + default: + return false; + } + } + } +} diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/DirectoryInfoExtensions.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/DirectoryInfoExtensions.cs new file mode 100644 index 0000000..8233b62 --- /dev/null +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/DirectoryInfoExtensions.cs @@ -0,0 +1,30 @@ +namespace Capgemini.PowerApps.SpecFlowBindings.Extensions +{ + using System.IO; + + /// + /// Extensions to the class. + /// + public static class DirectoryInfoExtensions + { + /// + /// Copies the directory recursively to the target directory. + /// + /// The source directory. + /// The target directory. + public static void CopyTo(this DirectoryInfo source, DirectoryInfo target) + { + Directory.CreateDirectory(target.FullName); + + foreach (var fileInfo in source.GetFiles()) + { + fileInfo.CopyTo(Path.Combine(target.FullName, fileInfo.Name), true); + } + + foreach (var subdirectory in source.GetDirectories()) + { + subdirectory.CopyTo(target.CreateSubdirectory(subdirectory.Name)); + } + } + } +} diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/AfterScenarioHooks.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/AfterScenarioHooks.cs index b84a5e8..8e00068 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/AfterScenarioHooks.cs +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/AfterScenarioHooks.cs @@ -1,5 +1,6 @@ namespace Capgemini.PowerApps.SpecFlowBindings.Hooks { + using System; using System.IO; using System.Reflection; using OpenQA.Selenium; @@ -52,6 +53,7 @@ public void ScreenshotFailedScenario() { var rootFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); var screenshotsFolder = Path.Combine(rootFolder, "screenshots"); + Console.WriteLine(screenshotsFolder); if (!Directory.Exists(screenshotsFolder)) { diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/BeforeRunHooks.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/BeforeRunHooks.cs new file mode 100644 index 0000000..a035b5b --- /dev/null +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/BeforeRunHooks.cs @@ -0,0 +1,49 @@ +namespace Capgemini.PowerApps.SpecFlowBindings.Hooks +{ + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using Capgemini.PowerApps.SpecFlowBindings.Configuration; + using Capgemini.PowerApps.SpecFlowBindings.Steps; + using Microsoft.Dynamics365.UIAutomation.Api.UCI; + using Microsoft.Dynamics365.UIAutomation.Browser; + using TechTalk.SpecFlow; + + /// + /// Hooks that run before the start of each run. + /// + [Binding] + public class BeforeRunHooks : PowerAppsStepDefiner + { + /// + /// Creates a new folder for the scenario and copies the session/cookies information from previous runs. + /// + [BeforeTestRun] + public static void BaseProfileSetup() + { + if (!TestConfig.UseProfiles) + { + return; + } + + Parallel.ForEach(UserProfileDirectories.Keys, (username) => + { + var profileDirectory = UserProfileDirectories[username]; + var baseDirectory = Path.Combine(profileDirectory, "base"); + + Directory.CreateDirectory(baseDirectory); + + var userBrowserOptions = (BrowserOptionsWithProfileSupport)TestConfig.BrowserOptions.Clone(); + userBrowserOptions.ProfileDirectory = baseDirectory; + userBrowserOptions.Headless = true; + + var webClient = new WebClient(userBrowserOptions); + using (new XrmApp(webClient)) + { + var user = TestConfig.Users.First(u => u.Username == username); + LoginSteps.Login(webClient.Browser.Driver, TestConfig.GetTestUrl(), user.Username, user.Password); + } + }); + } + } +} \ No newline at end of file diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/PowerAppsStepDefiner.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/PowerAppsStepDefiner.cs index dde6212..cf7eeea 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/PowerAppsStepDefiner.cs +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/PowerAppsStepDefiner.cs @@ -1,13 +1,18 @@ namespace Capgemini.PowerApps.SpecFlowBindings { using System; + using System.Collections.Generic; using System.Configuration; using System.IO; + using System.Linq; using System.Reflection; using Capgemini.PowerApps.SpecFlowBindings.Configuration; + using Capgemini.PowerApps.SpecFlowBindings.Extensions; + using FluentAssertions.Extensions; using Microsoft.Dynamics365.UIAutomation.Api.UCI; using Microsoft.Identity.Client; using OpenQA.Selenium; + using Polly; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -20,6 +25,9 @@ public abstract class PowerAppsStepDefiner private static IConfidentialClientApplication app; + [ThreadStatic] + private static string currentProfileDirectory; + [ThreadStatic] private static ITestDriver testDriver; @@ -32,6 +40,9 @@ public abstract class PowerAppsStepDefiner [ThreadStatic] private static XrmApp xrmApp; + private static IDictionary userProfilesDirectories; + private static object userProfilesDirectoriesLock = new object(); + /// /// Gets access token used to authenticate as the application user configured for testing. /// @@ -87,7 +98,25 @@ protected static TestConfiguration TestConfig /// /// Gets the EasyRepro WebClient. /// - protected static WebClient Client => client ?? (client = new WebClient(TestConfig.BrowserOptions)); + protected static WebClient Client + { + get + { + if (client == null) + { + var browserOptions = (BrowserOptionsWithProfileSupport)TestConfig.BrowserOptions.Clone(); + + if (TestConfig.UseProfiles) + { + browserOptions.ProfileDirectory = CurrentProfileDirectory; + } + + client = new WebClient(browserOptions); + } + + return client; + } + } /// /// Gets the EasyRepro XrmApp. @@ -99,6 +128,28 @@ protected static TestConfiguration TestConfig /// protected static IWebDriver Driver => Client.Browser.Driver; + /// + /// Gets the profile directory for the current scenario. + /// + protected static string CurrentProfileDirectory + { + get + { + if (!testConfig.BrowserOptions.BrowserType.SupportsProfiles()) + { + throw new NotSupportedException($"The {testConfig.BrowserOptions.BrowserType} does not support profiles."); + } + + if (string.IsNullOrEmpty(currentProfileDirectory)) + { + var basePath = string.IsNullOrEmpty(TestConfig.ProfilesBasePath) ? Path.GetTempPath() : TestConfig.ProfilesBasePath; + currentProfileDirectory = Path.Combine(basePath, "profiles", Guid.NewGuid().ToString()); + } + + return currentProfileDirectory; + } + } + /// /// Gets provides utilities for test setup/teardown. /// @@ -117,19 +168,75 @@ protected static ITestDriver TestDriver } /// - /// Performs any cleanup necessary when quitting the WebBrowser. + /// Gets the directories for the Chrome or Firefox profiles. /// - protected static void Quit() + protected static IDictionary UserProfileDirectories { - if (xrmApp == null) + get { - return; + if (!testConfig.BrowserOptions.BrowserType.SupportsProfiles()) + { + throw new NotSupportedException($"The {testConfig.BrowserOptions.BrowserType} does not support profiles."); + } + + lock (userProfilesDirectoriesLock) + { + if (userProfilesDirectories != null) + { + return userProfilesDirectories; + } + + var basePath = string.IsNullOrEmpty(TestConfig.ProfilesBasePath) ? Path.GetTempPath() : TestConfig.ProfilesBasePath; + var profilesDirectory = Path.Combine(basePath, "profiles"); + + Directory.CreateDirectory(profilesDirectory); + + userProfilesDirectories = TestConfig.Users + .Where(u => !string.IsNullOrEmpty(u.Password)) + .Select(u => u.Username) + .Distinct() + .ToDictionary(u => u, u => Path.Combine(profilesDirectory, u)); + + foreach (var dir in userProfilesDirectories.Values) + { + Directory.CreateDirectory(dir); + } + } + + return userProfilesDirectories; } + } + + /// + /// Performs any cleanup necessary when quitting the WebBrowser. + /// + protected static void Quit() + { + var driver = client?.Browser?.Driver; + + xrmApp?.Dispose(); + + // Ensuring that the driver gets disposed. Previously we were left with orphan processes and were unable to clean up profile folders. + driver?.Dispose(); - xrmApp.Dispose(); xrmApp = null; client = null; testDriver = null; + + if (!string.IsNullOrEmpty(currentProfileDirectory) && Directory.Exists(currentProfileDirectory)) + { + var directoryToDelete = currentProfileDirectory; + currentProfileDirectory = null; + + // CrashpadMetrics-active.pma file can continue to be locked even after quitting Chrome. Requires retries. + Policy + .Handle() + .WaitAndRetry(3, retry => (retry * 5).Seconds()) + .ExecuteAndCapture(() => + { + Directory.Delete(directoryToDelete, true); + }); + } } /// diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/LoginSteps.cs b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/LoginSteps.cs index f962108..0731d72 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/LoginSteps.cs +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/LoginSteps.cs @@ -1,5 +1,9 @@ namespace Capgemini.PowerApps.SpecFlowBindings.Steps { + using System; + using System.IO; + using Capgemini.PowerApps.SpecFlowBindings.Extensions; + using Microsoft.Dynamics365.UIAutomation.Api.UCI; using Microsoft.Dynamics365.UIAutomation.Browser; using OpenQA.Selenium; using TechTalk.SpecFlow; @@ -10,23 +14,62 @@ [Binding] public class LoginSteps : PowerAppsStepDefiner { + /// + /// Logs into the given instance with the given credentials. + /// + /// WebDriver used to imitate user actions. + /// The of the instance. + /// The username of the user. + /// The password of the user. + public static void Login(IWebDriver driver, Uri orgUrl, string username, string password) + { + driver.Navigate().GoToUrl(orgUrl); + driver.ClickIfVisible(By.Id("otherTile")); + + bool waitForMainPage = WaitForMainPage(driver); + + if (!waitForMainPage) + { + IWebElement usernameInput = driver.WaitUntilAvailable(By.XPath(Elements.Xpath[Reference.Login.UserId]), 30.Seconds()); + usernameInput.SendKeys(username); + usernameInput.SendKeys(Keys.Enter); + + IWebElement passwordInput = driver.WaitUntilClickable(By.XPath(Elements.Xpath[Reference.Login.LoginPassword]), 30.Seconds()); + passwordInput.SendKeys(password); + passwordInput.Submit(); + + var staySignedIn = driver.WaitUntilClickable(By.XPath(Elements.Xpath[Reference.Login.StaySignedIn]), 10.Seconds()); + if (staySignedIn != null) + { + staySignedIn.Click(); + } + + WaitForMainPage(driver, 30.Seconds()); + } + } + /// /// Logs in to a given app as a given user. /// /// The name of the app. /// The alias of the user. [Given("I am logged in to the '(.*)' app as '(.*)'")] - public static void GivenIAmLoggedInToTheAppAs(string appName, string userAlias) + public void GivenIAmLoggedInToTheAppAs(string appName, string userAlias) { var user = TestConfig.GetUser(userAlias); - XrmApp.OnlineLogin.Login( - TestConfig.GetTestUrl(), - user.Username.ToSecureString(), - user.Password.ToSecureString(), - string.Empty.ToSecureString()); + if (TestConfig.UseProfiles && TestConfig.BrowserOptions.BrowserType.SupportsProfiles()) + { + SetupScenarioProfile(user.Username); + } + + var url = TestConfig.GetTestUrl(); + Login(Driver, url, user.Username, user.Password); - XrmApp.Navigation.OpenApp(appName); + if (!url.Query.Contains("appid")) + { + XrmApp.Navigation.OpenApp(appName); + } CloseTeachingBubbles(); } @@ -38,5 +81,27 @@ private static void CloseTeachingBubbles() closeButton.Click(); } } + + private static bool WaitForMainPage(IWebDriver driver, TimeSpan? timeout = null) + { + timeout = timeout ?? 10.Seconds(); + + var isUCI = driver.HasElement(By.XPath(Elements.Xpath[Reference.Login.CrmUCIMainPage])); + if (isUCI) + { + driver.WaitForTransaction(); + } + + var xpathToMainPage = By.XPath(Elements.Xpath[Reference.Login.CrmMainPage]); + var element = driver.WaitUntilAvailable(xpathToMainPage, timeout); + + return element != null; + } + + private static void SetupScenarioProfile(string username) + { + var baseProfileDirectory = Path.Combine(UserProfileDirectories[username], "base"); + new DirectoryInfo(baseProfileDirectory).CopyTo(new DirectoryInfo(CurrentProfileDirectory)); + } } } diff --git a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/content/power-apps-bindings.yml b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/content/power-apps-bindings.yml index 185d7ec..ad7095b 100644 --- a/bindings/src/Capgemini.PowerApps.SpecFlowBindings/content/power-apps-bindings.yml +++ b/bindings/src/Capgemini.PowerApps.SpecFlowBindings/content/power-apps-bindings.yml @@ -1,4 +1,5 @@ url: +useProfiles: false browserOptions: browserType: users: diff --git a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Hooks/MockSolutionHooks.cs b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Hooks/MockSolutionHooks.cs index f37068d..5e84ee2 100644 --- a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Hooks/MockSolutionHooks.cs +++ b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Hooks/MockSolutionHooks.cs @@ -1,10 +1,10 @@ -using Microsoft.Xrm.Tooling.Connector; -using System.IO; -using System.Reflection; -using TechTalk.SpecFlow; - -namespace Capgemini.PowerApps.SpecFlowBindings.UiTests.Hooks +namespace Capgemini.PowerApps.SpecFlowBindings.UiTests.Hooks { + using Microsoft.Xrm.Tooling.Connector; + using System.IO; + using System.Reflection; + using TechTalk.SpecFlow; + /// /// Hooks related to the mock solution used for testing. /// @@ -37,14 +37,16 @@ private static CrmServiceClient GetServiceClient() { var admin = TestConfig.GetUser("an admin"); - return new CrmServiceClient( - $"AuthType=OAuth; " + - $"Username={admin.Username}; " + - $"Password={admin.Password}; " + - $"Url={TestConfig.GetTestUrl()}; " + - $"AppId=51f81489-12ee-4a9e-aaae-a2591f45987d; " + - $"RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97; " + - $"LoginPrompt=Auto"); + var connectionString = + $"AuthType=OAuth;" + + $"Username={admin.Username};" + + $"Password={admin.Password};" + + $"Url={TestConfig.GetTestUrl()};" + + $"AppId=51f81489-12ee-4a9e-aaae-a2591f45987d;" + + $"RedirectUri=app://58145B91-0C36-4500-8554-080854F2AC97;" + + $"LoginPrompt=Auto"; + + return new CrmServiceClient(connectionString); } } } \ No newline at end of file diff --git a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/LookupSteps.feature b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/LookupSteps.feature index e44e621..3c34c96 100644 --- a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/LookupSteps.feature +++ b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/LookupSteps.feature @@ -24,6 +24,8 @@ Scenario: Switch view in a lookup When I search for 'Some text' in the 'sb_lookup' lookup And I switch to the 'Inactive Secondary Mock Records' view in the lookup +# Selecting lookup entities in EasyRepro currently flaky. +@ignore Scenario: Select a related entity in a lookup When I search for '*' in the 'sb_customer' lookup And I select the related 'Contacts' entity in the lookup diff --git a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/power-apps-bindings.yml b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/power-apps-bindings.yml index 18cb9ef..bf0ddf6 100644 --- a/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/power-apps-bindings.yml +++ b/bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/power-apps-bindings.yml @@ -1,4 +1,5 @@ url: POWERAPPS_SPECFLOW_BINDINGS_TEST_URL +useProfiles: true browserOptions: browserType: Chrome headless: true diff --git a/templates/include-build-and-test-steps.yml b/templates/include-build-and-test-steps.yml index ee2f992..1b4e49b 100644 --- a/templates/include-build-and-test-steps.yml +++ b/templates/include-build-and-test-steps.yml @@ -92,7 +92,7 @@ jobs: POWERAPPS_SPECFLOW_BINDINGS_TEST_CLIENTSECRET: $(Application User Client Secret) POWERAPPS_SPECFLOW_BINDINGS_TEST_ADMIN_USERNAME: $(User ADO Integration Username) POWERAPPS_SPECFLOW_BINDINGS_TEST_ADMIN_PASSWORD: $(User ADO Integration Password) - POWERAPPS_SPECFLOW_BINDINGS_TEST_URL: $(URL) + POWERAPPS_SPECFLOW_BINDINGS_TEST_URL: $(URL) - task: SonarCloudAnalyze@1 displayName: Analyse with SonarCloud - task: SonarCloudPublish@1