Skip to content

Commit dcc353e

Browse files
ewingjmmjahlvosagiestartdashworthleroi-douglas
authored
feat: 2021 release wave 2 (#119)
Co-authored-by: Mike Andrews <[email protected]> Co-authored-by: Osagie Okoedo <[email protected]> Co-authored-by: Tom Ashworth <[email protected]> Co-authored-by: Douglas <[email protected]>
1 parent 0dccf41 commit dcc353e

29 files changed

+18075
-395
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Installing the NuGet package creates a _power-apps-bindings.yml_ file in your pr
5555
```yaml
5656
url: SPECFLOW_POWERAPPS_URL # mandatory
5757
useProfiles: false # optional - defaults to false if not set
58+
deleteTestData: true # optional - defaults to true if not set
5859
browserOptions: # optional - will use default EasyRepro options if not set
5960
browserType: Chrome
6061
headless: true
@@ -138,6 +139,8 @@ These bindings look for a corresponding JSON file in a _data_ folder in the root
138139
with a difference.json
139140
```
140141

142+
The deleteTestData property in the power-apps-bindings.yml file can be set to specify whether you want records created via these bindings to be deleted after a scenario has ran. You may wish to override the default value and retain these e.g. to aid in diagnosing failures.
143+
141144
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:
142145

143146
- a user with a matching alias in the `users` array that has the `username` set

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Capgemini.PowerApps.SpecFlowBindings.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
</Target>
4646

4747
<ItemGroup>
48-
<PackageReference Include="Dynamics365.UIAutomation.Api" Version="9.2.21014.138" />
48+
<PackageReference Include="Dynamics365.UIAutomation.Api" Version="9.2.21101.119-RW2-Preview" />
4949
<PackageReference Include="FluentAssertions" Version="5.10.3" />
5050
<PackageReference Include="Microsoft.Build.Tasks.Git" Version="1.0.0">
5151
<PrivateAssets>all</PrivateAssets>

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Configuration/TestConfiguration.cs

+6
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ public TestConfiguration()
4545
[YamlMember(Alias = "useProfiles")]
4646
public bool UseProfiles { get; set; } = false;
4747

48+
/// <summary>
49+
/// Gets or sets a value indicating whether to delete test data.
50+
/// </summary>
51+
[YamlMember(Alias = "deleteTestData")]
52+
public bool DeleteTestData { get; set; } = true;
53+
4854
/// <summary>
4955
/// Gets or sets the base path where the user profiles are stored.
5056
/// </summary>

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Extensions/SubGridExtensions.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static int GetRecordIndexById(this SubGrid subGrid, string subgridName, I
2727
throw new ArgumentNullException(nameof(driver));
2828
}
2929

30-
driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Grid.Container]));
30+
driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridContents].Replace("[NAME]", subgridName)));
3131

3232
var index = (long)driver.ExecuteScript(
3333
$"return Xrm.Page.getControl(\"{subgridName}\").getGrid().getRows().get().findIndex(row => row.getData().getEntity().getId() == \"{recordId.ToString("B").ToUpper(CultureInfo.CurrentCulture)}\")");
@@ -52,8 +52,7 @@ public static void HighlightRecord(this SubGrid subGrid, string subgridName, IWe
5252

5353
var subGridElement = driver.FindElement(
5454
By.XPath(AppElements.Xpath[AppReference.Entity.SubGridContents].Replace("[NAME]", subgridName)));
55-
56-
var rows = subGridElement.FindElements(By.CssSelector("div.wj-row[role=row][data-lp-id]"));
55+
var rows = subGridElement.FindElements(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridRows]));
5756

5857
if (rows.Count == 0)
5958
{
@@ -65,7 +64,7 @@ public static void HighlightRecord(this SubGrid subGrid, string subgridName, IWe
6564
throw new IndexOutOfRangeException($"Subgrid {subgridName} record count: {rows.Count}. Expected: {index + 1}");
6665
}
6766

68-
rows[index].FindElement(By.TagName("div")).Click();
67+
rows[index].FindElements(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridCells]))[0].Click();
6968
driver.WaitForTransaction();
7069
}
7170
}

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/AfterScenarioHooks.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ public static void TestCleanup()
3131
{
3232
try
3333
{
34-
TestDriver.DeleteTestData();
34+
if (TestConfig.DeleteTestData)
35+
{
36+
TestDriver.DeleteTestData();
37+
}
3538
}
3639
catch (WebDriverException)
3740
{

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Hooks/BeforeRunHooks.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Capgemini.PowerApps.SpecFlowBindings.Configuration;
88
using Capgemini.PowerApps.SpecFlowBindings.Steps;
99
using Microsoft.Dynamics365.UIAutomation.Api.UCI;
10+
using Microsoft.Dynamics365.UIAutomation.Browser;
1011
using TechTalk.SpecFlow;
1112

1213
/// <summary>
@@ -54,10 +55,10 @@ public static void BaseProfileSetup()
5455
userBrowserOptions.Headless = true;
5556

5657
var webClient = new WebClient(userBrowserOptions);
57-
using (new XrmApp(webClient))
58+
using (var app = new XrmApp(webClient))
5859
{
5960
var user = TestConfig.Users.First(u => u.Username == username);
60-
LoginSteps.Login(webClient.Browser.Driver, TestConfig.GetTestUrl(), user.Username, user.Password);
61+
app.OnlineLogin.Login(TestConfig.GetTestUrl(), user.Username.ToSecureString(), user.Password.ToSecureString());
6162
}
6263
}
6364
finally

bindings/src/Capgemini.PowerApps.SpecFlowBindings/PowerAppsStepDefiner.cs

+16-6
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ protected static string AccessToken
5252
{
5353
var hostSegments = TestConfig.GetTestUrl().Host.Split('.');
5454

55-
return GetApp().AcquireTokenForClient(new string[] { $"https://{hostSegments[0]}.api.{hostSegments[1]}.dynamics.com//.default" })
55+
return GetApp()
56+
.AcquireTokenForClient(new string[] { $"https://{hostSegments[0]}.api.{hostSegments[1]}.dynamics.com//.default" })
5657
.ExecuteAsync()
5758
.Result.AccessToken;
5859
}
@@ -212,12 +213,21 @@ protected static IDictionary<string, string> UserProfileDirectories
212213
/// </summary>
213214
protected static void Quit()
214215
{
215-
var driver = client?.Browser?.Driver;
216-
217-
xrmApp?.Dispose();
216+
// Try to dispose, and catch web driver errors that can occur on disposal. Retry the disposal if these occur. Trap the final exception and continue the disposal process.
217+
var polly = Policy
218+
.Handle<WebDriverException>()
219+
.Retry(3, (ex, i) =>
220+
{
221+
Console.WriteLine(ex.Message);
222+
})
223+
.ExecuteAndCapture(() =>
224+
{
225+
xrmApp?.Dispose();
218226

219-
// Ensuring that the driver gets disposed. Previously we were left with orphan processes and were unable to clean up profile folders.
220-
driver?.Dispose();
227+
// Ensuring that the driver gets disposed. Previously we were left with orphan processes and were unable to clean up profile folders. We cannot rely on xrmApp.Dispose to properly dispose of the web driver.
228+
var driver = client?.Browser?.Driver;
229+
driver?.Dispose();
230+
});
221231

222232
xrmApp = null;
223233
client = null;

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/EntitySteps.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ public static void ThenICanNotSeeTheField(string fieldName)
489489
[Then("the status of the record is (active|inactive)")]
490490
public static void ThenTheStatusOfTheRecordIs(string status)
491491
{
492-
XrmApp.Entity.GetFooterStatusValue().Should().BeEquivalentTo(status);
492+
XrmApp.Entity.GetFormState().Should().BeEquivalentTo(status);
493493
}
494494

495495
private static void SetFieldValue(string fieldName, string fieldValue, string fieldType)

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/EntitySubGridSteps.cs

+26-18
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ public class EntitySubGridSteps : PowerAppsStepDefiner
2525
[When(@"I click the '(.*)' command on the '(.*)' subgrid")]
2626
public static void WhenISelectTheCommandOnTheSubgrid(string commandName, string subGridName)
2727
{
28-
Driver.WaitUntilVisible(
29-
By.CssSelector($"div#dataSetRoot_{subGridName} button[aria-label=\"{commandName}\"]"));
28+
Driver.WaitUntilAvailable(
29+
By.XPath(AppElements.Xpath[AppReference.Entity.SubGridContents].Replace("[NAME]", subGridName)));
3030

3131
XrmApp.Entity.SubGrid.ClickCommand(subGridName, commandName);
3232
}
@@ -220,10 +220,13 @@ public static void ThenTheSubgridContainsRecordsWithInTheField(string subGridNam
220220
[Then(@"I can see the '(.*)' command on the '(.*)' subgrid")]
221221
public static void ThenICanSeeTheCommandOnTheSubgrid(string commandName, string subGridName)
222222
{
223-
Driver.WaitUntilVisible(
224-
By.CssSelector($"div#dataSetRoot_{subGridName} button[aria-label=\"{commandName}\"]"),
225-
new TimeSpan(0, 0, 5),
226-
$"Could not find the {commandName} command on the {subGridName} subgrid.");
223+
Driver
224+
.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridContents].Replace("[NAME]", subGridName)))
225+
.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridCommandBar]))
226+
.WaitUntilVisible(
227+
By.XPath(AppElements.Xpath[AppReference.Entity.SubGridCommandLabel].Replace("[NAME]", commandName)),
228+
new TimeSpan(0, 0, 5),
229+
$"Could not find the {commandName} command on the {subGridName} subgrid.");
227230
}
228231

229232
/// <summary>
@@ -234,8 +237,6 @@ public static void ThenICanSeeTheCommandOnTheSubgrid(string commandName, string
234237
[When(@"I click the '([^']+)' flyout on the '([^']+)' subgrid")]
235238
public static void WhenIClickTheFlyoutOnTheSubgrid(string flyoutName, string subGridName)
236239
{
237-
Driver.WaitUntilVisible(By.CssSelector($"div#dataSetRoot_{subGridName} li[aria-label=\"{flyoutName}\"]"));
238-
239240
XrmApp.Entity.SubGrid.ClickCommand(subGridName, flyoutName);
240241
}
241242

@@ -247,8 +248,11 @@ public static void WhenIClickTheFlyoutOnTheSubgrid(string flyoutName, string sub
247248
[Then(@"I can not see the '(.*)' command on the '(.*)' subgrid")]
248249
public static void ThenICanNotSeeTheCommandOnTheSubgrid(string commandName, string subGridName)
249250
{
250-
Driver.WaitUntilVisible(
251-
By.CssSelector($"div#dataSetRoot_{subGridName} button[aria-label=\"{commandName}\"]"),
251+
Driver
252+
.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridContents].Replace("[NAME]", subGridName)))
253+
.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridCommandBar]))
254+
.WaitUntilVisible(
255+
By.XPath(AppElements.Xpath[AppReference.Entity.SubGridCommandLabel].Replace("[NAME]", commandName)),
252256
new TimeSpan(0, 0, 5))
253257
.Should().BeNull();
254258
}
@@ -260,10 +264,12 @@ public static void ThenICanNotSeeTheCommandOnTheSubgrid(string commandName, stri
260264
[Then(@"I can see the '(.*)' command on the flyout of the subgrid")]
261265
public static void ThenICanSeeTheCommandOnTheFlyoutOfTheSubgrid(string commandName)
262266
{
263-
Driver.WaitUntilVisible(
264-
By.CssSelector($"#__flyoutRootNode button[aria-label$='{commandName}']"),
265-
new TimeSpan(0, 0, 10),
266-
$"Could not find the {commandName} command on the flyout of the subgrid.");
267+
Driver
268+
.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridOverflowContainer]))
269+
.WaitUntilVisible(
270+
By.XPath(AppElements.Xpath[AppReference.Entity.SubGridOverflowButton].Replace("[NAME]", commandName)),
271+
new TimeSpan(0, 0, 10),
272+
$"Could not find the {commandName} command on the flyout of the subgrid.");
267273
}
268274

269275
/// <summary>
@@ -274,10 +280,12 @@ public static void ThenICanSeeTheCommandOnTheFlyoutOfTheSubgrid(string commandNa
274280
public static void ThenICanNotSeeTheCommandOnTheFlyoutOfTheSubgrid(string commandName)
275281
{
276282
Driver
277-
.Invoking(d => d.WaitUntilVisible(
278-
By.CssSelector($"#__flyoutRootNode button[aria-label$=\"{commandName}\"]"),
279-
new TimeSpan(0, 0, 1),
280-
$"Could not find the {commandName} command on the flyout of the subgrid."))
283+
.Invoking(d => d
284+
.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.SubGridOverflowContainer]))
285+
.WaitUntilVisible(
286+
By.XPath(AppElements.Xpath[AppReference.Entity.SubGridOverflowButton].Replace("[NAME]", commandName)),
287+
new TimeSpan(0, 0, 1),
288+
$"Could not find the {commandName} command on the flyout of the subgrid."))
281289
.Should()
282290
.Throw<Exception>();
283291
}

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/GlobalSearchSteps.cs

-10
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,6 @@ public static void WhenIPerformASearch(string filterValue)
3939
XrmApp.GlobalSearch.Search(filterValue);
4040
}
4141

42-
/// <summary>
43-
/// Performs an advanced search using the filter attribute and a filter value.
44-
/// </summary>
45-
/// <param name="filterValue">Attribute filter value.</param>
46-
[When("I change the search type using the filter '(.*)'")]
47-
public static void WhenIChangeTheSearchType(string filterValue)
48-
{
49-
XrmApp.GlobalSearch.ChangeSearchType(filterValue);
50-
}
51-
5242
/// <summary>
5343
/// Open a record from a global search at a certain row.
5444
/// </summary>

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/LoginSteps.cs

+4-35
Original file line numberDiff line numberDiff line change
@@ -14,40 +14,6 @@
1414
[Binding]
1515
public class LoginSteps : PowerAppsStepDefiner
1616
{
17-
/// <summary>
18-
/// Logs into the given instance with the given credentials.
19-
/// </summary>
20-
/// <param name="driver">WebDriver used to imitate user actions.</param>
21-
/// <param name="orgUrl">The <see cref="Uri"/> of the instance.</param>
22-
/// <param name="username">The username of the user.</param>
23-
/// <param name="password">The password of the user.</param>
24-
public static void Login(IWebDriver driver, Uri orgUrl, string username, string password)
25-
{
26-
driver.Navigate().GoToUrl(orgUrl);
27-
driver.ClickIfVisible(By.Id("otherTile"));
28-
29-
bool waitForMainPage = WaitForMainPage(driver);
30-
31-
if (!waitForMainPage)
32-
{
33-
IWebElement usernameInput = driver.WaitUntilAvailable(By.XPath(Elements.Xpath[Reference.Login.UserId]), 30.Seconds());
34-
usernameInput.SendKeys(username);
35-
usernameInput.SendKeys(Keys.Enter);
36-
37-
IWebElement passwordInput = driver.WaitUntilClickable(By.XPath(Elements.Xpath[Reference.Login.LoginPassword]), 30.Seconds());
38-
passwordInput.SendKeys(password);
39-
passwordInput.Submit();
40-
41-
var staySignedIn = driver.WaitUntilClickable(By.XPath(Elements.Xpath[Reference.Login.StaySignedIn]), 10.Seconds());
42-
if (staySignedIn != null)
43-
{
44-
staySignedIn.Click();
45-
}
46-
47-
WaitForMainPage(driver, 30.Seconds());
48-
}
49-
}
50-
5117
/// <summary>
5218
/// Logs in to a given app as a given user.
5319
/// </summary>
@@ -64,13 +30,16 @@ public static void GivenIAmLoggedInToTheAppAs(string appName, string userAlias)
6430
}
6531

6632
var url = TestConfig.GetTestUrl();
67-
Login(Driver, url, user.Username, user.Password);
33+
34+
XrmApp.OnlineLogin.Login(url, user.Username.ToSecureString(), user.Password.ToSecureString());
6835

6936
if (!url.Query.Contains("appid"))
7037
{
7138
XrmApp.Navigation.OpenApp(appName);
7239
}
7340

41+
Driver.WaitForTransaction();
42+
7443
CloseTeachingBubbles();
7544
}
7645

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/LookupDialogSteps.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static void WhenISelectInTheLookupDialog(string searchTerm)
3333
[When("I click Add in the lookup dialog")]
3434
public static void WhenIClickAddInTheLookupDialog()
3535
{
36-
var container = Driver.WaitUntilAvailable(By.CssSelector("div[id=\"lookupDialogContainer\"]"));
36+
var container = Driver.WaitUntilAvailable(By.CssSelector("div[id=\"lookupDialogFooterContainer\"]"));
3737

3838
container.FindElement(By.CssSelector("button[data-id*=\"lookupDialogSaveBtn\"]"))
3939
.Click();

bindings/src/Capgemini.PowerApps.SpecFlowBindings/Steps/NavigationSteps.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public static void ThenICanSeeTheGroup(string groupName)
107107
var groupNameWithoutWhitespace = groupName?.Replace(" ", string.Empty);
108108

109109
Driver
110-
.WaitUntilAvailable(By.XPath($"//span[@data-id='sitemap-sitemapAreaGroup-{groupNameWithoutWhitespace}']"))
110+
.WaitUntilAvailable(By.XPath($"//h3[@data-id='sitemap-sitemapAreaGroup-{groupNameWithoutWhitespace}']"))
111111
.Text
112112
.Should().Contain(groupName);
113113
}

bindings/src/Capgemini.PowerApps.SpecFlowBindings/TestDriver.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -39,23 +39,23 @@ public void InjectOnPage(string authToken)
3939
{
4040
var scriptBuilder = new StringBuilder();
4141
scriptBuilder.AppendLine(File.ReadAllText(this.FilePath));
42-
scriptBuilder.AppendLine($@"var recordRepository = new {LibraryNamespace}.CurrentUserRecordRepository(Xrm.WebApi.online);
43-
var metadataRepository = new {LibraryNamespace}.MetadataRepository(Xrm.WebApi.online);
44-
var deepInsertService = new {LibraryNamespace}.DeepInsertService(metadataRepository, recordRepository);");
42+
scriptBuilder.AppendLine($@"top.recordRepository = new {LibraryNamespace}.CurrentUserRecordRepository(Xrm.WebApi.online);
43+
top.metadataRepository = new {LibraryNamespace}.MetadataRepository(Xrm.WebApi.online);
44+
top.deepInsertService = new {LibraryNamespace}.DeepInsertService(top.metadataRepository, top.recordRepository);");
4545

4646
if (!string.IsNullOrEmpty(authToken))
4747
{
4848
scriptBuilder.AppendLine(
49-
$@"var appUserRecordRepository = new {LibraryNamespace}.AuthenticatedRecordRepository(metadataRepository, '{authToken}');
50-
var dataManager = new {LibraryNamespace}.DataManager(recordRepository, deepInsertService, [new {LibraryNamespace}.FakerPreprocessor()], appUserRecordRepository);");
49+
$@"top.appUserRecordRepository = new {LibraryNamespace}.AuthenticatedRecordRepository(top.metadataRepository, '{authToken}');
50+
top.dataManager = new {LibraryNamespace}.DataManager(top.recordRepository, top.deepInsertService, [new {LibraryNamespace}.FakerPreprocessor()], top.appUserRecordRepository);");
5151
}
5252
else
5353
{
5454
scriptBuilder.AppendLine(
55-
$"var dataManager = new {LibraryNamespace}.DataManager(recordRepository, deepInsertService, [new {LibraryNamespace}.FakerPreprocessor()]);");
55+
$"top.dataManager = new {LibraryNamespace}.DataManager(top.recordRepository, top.deepInsertService, [new {LibraryNamespace}.FakerPreprocessor()]);");
5656
}
5757

58-
scriptBuilder.AppendLine($"{TestDriverReference} = new {LibraryNamespace}.Driver(dataManager);");
58+
scriptBuilder.AppendLine($"{TestDriverReference} = new {LibraryNamespace}.Driver(top.dataManager);");
5959

6060
this.javascriptExecutor.ExecuteScript(scriptBuilder.ToString());
6161
}

bindings/tests/Capgemini.PowerApps.SpecFlowBindings.UiTests/Capgemini.PowerApps.SpecFlowBindings.UiTests.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<PackageReference Include="Microsoft.CrmSdk.XrmTooling.CoreAssembly" Version="9.1.0.64" />
3939
<PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
4040
<PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
41-
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="91.0.4472.10100" />
41+
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="103.0.5060.5300" />
4242
<PackageReference Include="SpecFlow" Version="3.5.14" />
4343
<PackageReference Include="SpecFlow.MsTest" Version="3.5.14" />
4444
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.5.14" />

0 commit comments

Comments
 (0)