Skip to content

Commit

Permalink
feat: bypass login with browser profiles (#87)
Browse files Browse the repository at this point in the history
Improves performance by allowing the use of Chrome and Firefox profiles for test users.

+semver: minor
  • Loading branch information
bancey authored Jun 24, 2021
1 parent d8df17a commit 1ece3d6
Show file tree
Hide file tree
Showing 16 changed files with 411 additions and 40 deletions.
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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).
Expand Down Expand Up @@ -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
Expand All @@ -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).

Expand All @@ -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
Expand Down Expand Up @@ -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 `<lookup>@alias.bind` (this is limited to aliased records in other files - not the current file).


## Contributing

Please refer to the [Contributing](./CONTRIBUTING.md) guide.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Polly" Version="7.2.2" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="SpecFlow" Version="3.5.14" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extends the EasyRepro <see cref="BrowserOptions"/> class with additonal support for chrome profiles.
/// </summary>
public class BrowserOptionsWithProfileSupport : BrowserOptions, ICloneable
{
/// <summary>
/// Gets or sets the directory to use as the user profile.
/// </summary>
public string ProfileDirectory { get; set; }

/// <inheritdoc/>
public object Clone()
{
return this.MemberwiseClone();
}

/// <inheritdoc/>
public override ChromeOptions ToChrome()
{
var options = base.ToChrome();

if (!string.IsNullOrEmpty(this.ProfileDirectory))
{
options.AddArgument($"--user-data-dir={this.ProfileDirectory}");
}

return options;
}

/// <inheritdoc/>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ public static class ConfigHelper
/// <returns>The environment variable value (if found) or the passed in value.</returns>
public static string GetEnvironmentVariableIfExists(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}

var environmentVariableValue = Environment.GetEnvironmentVariable(value);

if (!string.IsNullOrEmpty(environmentVariableValue))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using Microsoft.Dynamics365.UIAutomation.Browser;
using YamlDotNet.Serialization;

/// <summary>
Expand All @@ -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;

/// <summary>
/// Initializes a new instance of the <see cref="TestConfiguration"/> class.
/// </summary>
public TestConfiguration()
{
this.BrowserOptions = new BrowserOptions();
this.BrowserOptions = new BrowserOptionsWithProfileSupport();
}

/// <summary>
Expand All @@ -33,11 +34,23 @@ public TestConfiguration()
[YamlMember(Alias = "url")]
public string Url { private get; set; }

/// <summary>
/// Gets or sets a value indicating whether to use profiles.
/// </summary>
[YamlMember(Alias = "useProfiles")]
public bool UseProfiles { get; set; } = false;

/// <summary>
/// Gets or sets the base path where the user profiles are stored.
/// </summary>
[YamlMember(Alias = "profilesBasePath")]
public string ProfilesBasePath { get => ConfigHelper.GetEnvironmentVariableIfExists(this.profilesBasePath); set => this.profilesBasePath = value; }

/// <summary>
/// Gets or sets the browser options to use for running tests.
/// </summary>
[YamlMember(Alias = "browserOptions")]
public BrowserOptions BrowserOptions { get; set; }
public BrowserOptionsWithProfileSupport BrowserOptions { get; set; }

/// <summary>
/// Gets or sets users that tests can be run as.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Extensions
{
using Microsoft.Dynamics365.UIAutomation.Browser;

/// <summary>
/// Provides extension methods on <see cref="BrowserType"/>.
/// </summary>
public static class BrowserTypeExtensions
{
/// <summary>
/// Determines if the given browser type supports profiles.
/// </summary>
/// <param name="type">The <see cref="BrowserType"/> to check.</param>
/// <returns>true if the browser supports profiles otherwise false.</returns>
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;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Extensions
{
using System.IO;

/// <summary>
/// Extensions to the <see cref="DirectoryInfo"/> class.
/// </summary>
public static class DirectoryInfoExtensions
{
/// <summary>
/// Copies the directory recursively to the target directory.
/// </summary>
/// <param name="source">The source directory.</param>
/// <param name="target">The target directory.</param>
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));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
namespace Capgemini.PowerApps.SpecFlowBindings.Hooks
{
using System;
using System.IO;
using System.Reflection;
using OpenQA.Selenium;
Expand Down Expand Up @@ -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))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Hooks that run before the start of each run.
/// </summary>
[Binding]
public class BeforeRunHooks : PowerAppsStepDefiner
{
/// <summary>
/// Creates a new folder for the scenario and copies the session/cookies information from previous runs.
/// </summary>
[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);
}
});
}
}
}
Loading

0 comments on commit 1ece3d6

Please sign in to comment.