From 3630cd8a0dd88d6af344dcf414d36ebd52551cef Mon Sep 17 00:00:00 2001 From: Martijn Vermaat <13309338+mVermaat@users.noreply.github.com> Date: Sun, 31 May 2020 11:03:23 +0200 Subject: [PATCH] Features/fieldrefactoring (#91) Updated EasyRepro to latest version. Workaround for #78 implemented until EasyRepro provides a fix Workaround for #88 implemented until EasyRepro provides a fix Fixed #89. Dialogs working again. --- .../Scripts/AddScreenshotsToTestResults.ps1 | 5 + .../Commands/AssertFormStateCommand.cs | 42 ++- .../Commands/ClickSubgridButtonCommand.cs | 2 +- Vermaat.Crm.Specflow/Constants.cs | 1 + .../EasyRepro/ContainerType.cs | 13 + Vermaat.Crm.Specflow/EasyRepro/Field.cs | 174 ----------- .../EasyRepro/FieldTypes/BooleanValue.cs | 27 ++ .../EasyRepro/FieldTypes/DateTimeValue.cs | 26 ++ .../EasyRepro/FieldTypes/DecimalValue.cs | 21 ++ .../EasyRepro/FieldTypes/DoubleValue.cs | 21 ++ .../EasyRepro/FieldTypes/IntegerValue.cs | 23 ++ .../EasyRepro/FieldTypes/LongValue.cs | 21 ++ .../EasyRepro/FieldTypes/LookupValue.cs | 26 ++ .../FieldTypes/MultiSelectOptionSetValue.cs | 29 ++ .../EasyRepro/FieldTypes/OptionSetValue.cs | 27 ++ .../EasyRepro/Fields/BodyFormField.cs | 119 ++++++++ .../EasyRepro/Fields/Field.cs | 118 ++++++++ .../EasyRepro/Fields/FormField.cs | 68 +++++ .../EasyRepro/Fields/HeaderFormField.cs | 96 ++++++ .../Fields/OpportunityCloseDialogField.cs | 99 +++++++ Vermaat.Crm.Specflow/EasyRepro/FormData.cs | 50 ++-- Vermaat.Crm.Specflow/EasyRepro/FormField.cs | 127 -------- Vermaat.Crm.Specflow/EasyRepro/FormState.cs | 40 ++- .../EasyRepro/OpportunityCloseDialog.cs | 3 +- .../EasyRepro/OpportunityCloseDialogField.cs | 34 --- .../EasyRepro/TemporaryFixes.cs | 275 ++++++++++++++---- Vermaat.Crm.Specflow/EasyRepro/UCIBrowser.cs | 2 +- Vermaat.Crm.Specflow/ErrorCodes.cs | 1 + Vermaat.Crm.Specflow/HelperMethods.cs | 2 +- .../Vermaat.Crm.Specflow.csproj | 2 +- 30 files changed, 1052 insertions(+), 442 deletions(-) create mode 100644 Vermaat.Crm.Specflow/EasyRepro/ContainerType.cs delete mode 100644 Vermaat.Crm.Specflow/EasyRepro/Field.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/BooleanValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DateTimeValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DecimalValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DoubleValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/IntegerValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LongValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LookupValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/MultiSelectOptionSetValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/FieldTypes/OptionSetValue.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/Fields/BodyFormField.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/Fields/Field.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/Fields/FormField.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/Fields/HeaderFormField.cs create mode 100644 Vermaat.Crm.Specflow/EasyRepro/Fields/OpportunityCloseDialogField.cs delete mode 100644 Vermaat.Crm.Specflow/EasyRepro/FormField.cs delete mode 100644 Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialogField.cs diff --git a/Deployment/Scripts/AddScreenshotsToTestResults.ps1 b/Deployment/Scripts/AddScreenshotsToTestResults.ps1 index 007356f0..47fba430 100644 --- a/Deployment/Scripts/AddScreenshotsToTestResults.ps1 +++ b/Deployment/Scripts/AddScreenshotsToTestResults.ps1 @@ -37,6 +37,11 @@ $resultData = Invoke-RestMethod -Uri "$baseUrl/$testRunId/results?api-version=5. Foreach ($result in $resultData.value) { $testName = $result.testCaseTitle.Replace(' ', '_').Replace(':','') + $comma = $testName.LastIndexOf(',') + if($comma -ne -1) { + $testName = $testName.Substring(0, $comma) + } + $search = "$testResultFolder\error_*_$($testName)_*" Write-Host "Processing $testName. Path: $search" diff --git a/Vermaat.Crm.Specflow/Commands/AssertFormStateCommand.cs b/Vermaat.Crm.Specflow/Commands/AssertFormStateCommand.cs index 20a57e8f..e43095ec 100644 --- a/Vermaat.Crm.Specflow/Commands/AssertFormStateCommand.cs +++ b/Vermaat.Crm.Specflow/Commands/AssertFormStateCommand.cs @@ -12,6 +12,13 @@ public class AssertFormStateCommand : BrowserOnlyCommand private readonly EntityReference _crmRecord; private readonly Table _visibilityCriteria; + private class ExpectedFormState + { + public FormVisibility? Visible { get; set; } + public bool? Locked { get; set; } + public RequiredState? Required { get; set; } + } + public AssertFormStateCommand(CrmTestingContext crmContext, SeleniumTestingContext seleniumContext, EntityReference crmRecord, Table visibilityCriteria) : base(crmContext, seleniumContext) { @@ -22,31 +29,20 @@ public AssertFormStateCommand(CrmTestingContext crmContext, SeleniumTestingConte public override void Execute() { var formData = _seleniumContext.GetBrowser().OpenRecord(new OpenFormOptions(_crmRecord)); + var formState = new FormState(_seleniumContext.GetBrowser().App); List errors = new List(); - string currentTab = null; foreach (TableRow row in _visibilityCriteria.Rows) { var expectedFormState = GetExpectedFormState(row[Constants.SpecFlow.TABLE_FORMSTATE]); var isOnForm = formData.ContainsField(row[Constants.SpecFlow.TABLE_KEY]); - if (isOnForm) - { - var field = formData[row[Constants.SpecFlow.TABLE_KEY]]; - var newTab = field.GetTabName(); - if (string.IsNullOrWhiteSpace(currentTab) || currentTab != newTab) - { - formData.ExpandTab(field.GetTabLabel()); - currentTab = newTab; - } - } - if (isOnForm || (!expectedFormState.Locked.HasValue && !expectedFormState.Required.HasValue)) { // Assert - AssertVisibility(formData, row[Constants.SpecFlow.TABLE_KEY], expectedFormState.Visible, errors, isOnForm); - AssertReadOnly(formData, row[Constants.SpecFlow.TABLE_KEY], expectedFormState.Locked, errors); - AssertRequirement(formData, row[Constants.SpecFlow.TABLE_KEY], expectedFormState.Required, errors); + AssertVisibility(formData, formState, row[Constants.SpecFlow.TABLE_KEY], expectedFormState.Visible, errors, isOnForm); + AssertReadOnly(formData, formState, row[Constants.SpecFlow.TABLE_KEY], expectedFormState.Locked, errors); + AssertRequirement(formData, formState, row[Constants.SpecFlow.TABLE_KEY], expectedFormState.Required, errors); } else { @@ -57,11 +53,11 @@ public override void Execute() Assert.AreEqual(0, errors.Count, string.Join(", ", errors)); } - private FormState GetExpectedFormState(string formStateString) + private ExpectedFormState GetExpectedFormState(string formStateString) { var splitted = formStateString.Split(','); - FormState result = new FormState(); + ExpectedFormState result = new ExpectedFormState(); foreach(string state in splitted) { switch(state.Trim().ToLower()) @@ -80,14 +76,14 @@ private FormState GetExpectedFormState(string formStateString) return result; } - private void AssertVisibility(FormData formData, string fieldName, FormVisibility? expected, List errors, bool isOnForm) + private void AssertVisibility(FormData formData, FormState formState, string fieldName, FormVisibility? expected, List errors, bool isOnForm) { if (!expected.HasValue) return; if(isOnForm) { - var isVisible = formData[fieldName].IsVisible(); + var isVisible = formData[fieldName].IsVisible(formState); if (expected == FormVisibility.Visible && !isVisible) { errors.Add($"{fieldName} was expected to be visible but it is invisible"); @@ -107,24 +103,24 @@ private void AssertVisibility(FormData formData, string fieldName, FormVisibilit } } - private void AssertReadOnly(FormData formData, string fieldName, bool? locked, List errors) + private void AssertReadOnly(FormData formData, FormState formState, string fieldName, bool? locked, List errors) { if (!locked.HasValue) return; - if (formData[fieldName].IsLocked() != locked.Value) + if (formData[fieldName].IsLocked(formState) != locked.Value) { errors.Add(string.Format("{0} was expected to be {1}locked but it is {2}locked", fieldName, locked.Value ? "" : "un", locked.Value ? "un" : "")); } } - private void AssertRequirement(FormData formData, string fieldName, RequiredState? expectedRequiredState, List errors) + private void AssertRequirement(FormData formData, FormState formState, string fieldName, RequiredState? expectedRequiredState, List errors) { if (!expectedRequiredState.HasValue) return; - var actualRequiredState = formData[fieldName].GetRequiredState(); + var actualRequiredState = formData[fieldName].GetRequiredState(formState); if (actualRequiredState != expectedRequiredState) { errors.Add($"{fieldName} was expected to be {expectedRequiredState} but it is {actualRequiredState}"); diff --git a/Vermaat.Crm.Specflow/Commands/ClickSubgridButtonCommand.cs b/Vermaat.Crm.Specflow/Commands/ClickSubgridButtonCommand.cs index 4d24a690..b716b9b0 100644 --- a/Vermaat.Crm.Specflow/Commands/ClickSubgridButtonCommand.cs +++ b/Vermaat.Crm.Specflow/Commands/ClickSubgridButtonCommand.cs @@ -30,7 +30,7 @@ public override void Execute() var browser = _seleniumContext.GetBrowser(); var record = browser.OpenRecord(new OpenFormOptions(parentRecord)); - record.ExpandTab(_tabName); + browser.App.App.Entity.SelectTab(_tabName); Logger.WriteLine($"Clicking button '{_gridButtonId} in grid {_subgridName}"); record.ClickSubgridButton(_subgridName, _gridButtonId); diff --git a/Vermaat.Crm.Specflow/Constants.cs b/Vermaat.Crm.Specflow/Constants.cs index 4da44fab..4ddb1289 100644 --- a/Vermaat.Crm.Specflow/Constants.cs +++ b/Vermaat.Crm.Specflow/Constants.cs @@ -66,6 +66,7 @@ public class ErrorCodes public const int ASYNC_TIMEOUT = 34; public const int MULTIPLE_ATTRIBUTES_FOUND = 35; public const int APPLICATIONUSER_CANNOT_LOGIN = 36; + public const int NO_CONTROL_FOUND = 37; } } } diff --git a/Vermaat.Crm.Specflow/EasyRepro/ContainerType.cs b/Vermaat.Crm.Specflow/EasyRepro/ContainerType.cs new file mode 100644 index 00000000..7ea0d457 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/ContainerType.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro +{ + public enum ContainerType + { + Body, Header, Dialog + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/Field.cs b/Vermaat.Crm.Specflow/EasyRepro/Field.cs deleted file mode 100644 index 1478b173..00000000 --- a/Vermaat.Crm.Specflow/EasyRepro/Field.cs +++ /dev/null @@ -1,174 +0,0 @@ -using Microsoft.Dynamics365.UIAutomation.Api.UCI; -using Microsoft.Dynamics365.UIAutomation.Browser; -using Microsoft.Xrm.Sdk; -using Microsoft.Xrm.Sdk.Metadata; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Vermaat.Crm.Specflow.EasyRepro -{ - public abstract class Field - { - protected AttributeMetadata Metadata { get; } - protected UCIApp App { get; } - - protected virtual string LogicalName => Metadata.LogicalName; - - public Field(UCIApp app, AttributeMetadata metadata) - { - Metadata = metadata; - App = app; - } - - public void SetValue(CrmTestingContext crmContext, string fieldValueText) - { - object fieldValue = ObjectConverter.ToCrmObject(Metadata.EntityLogicalName, Metadata.LogicalName, fieldValueText, crmContext); - - if (fieldValue != null) - { - Logger.WriteLine($"Setting field value"); - switch (Metadata.AttributeType.Value) - { - case AttributeTypeCode.Boolean: - SetTwoOptionField((bool)fieldValue, fieldValueText); - break; - case AttributeTypeCode.DateTime: - SetDateTimeField((DateTime)fieldValue, fieldValueText); - break; - case AttributeTypeCode.Customer: - case AttributeTypeCode.Lookup: - SetLookupValue((EntityReference)fieldValue, fieldValueText); - break; - case AttributeTypeCode.Picklist: - SetOptionSetField((OptionSetValue)fieldValue, fieldValueText); - break; - case AttributeTypeCode.Money: - SetMoneyField((Money)fieldValue, fieldValueText); - break; - case AttributeTypeCode.Virtual: - SetVirtualField(fieldValueText); - break; - case AttributeTypeCode.Integer: - SetIntegerField((int)fieldValue); - break; - case AttributeTypeCode.Double: - SetDoubleField((double)fieldValue); - break; - case AttributeTypeCode.BigInt: - SetLongField((long)fieldValue); - break; - case AttributeTypeCode.Decimal: - SetDecimalField((decimal)fieldValue); - break; - default: - SetTextField((string)fieldValue); - break; - } - } - else - { - Logger.WriteLine($"Clearing field value"); - ClearValue(); - } - } - - private void SetIntegerField(int fieldValue) - { - SetTextField(fieldValue.ToString( - GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat)); - } - - private void SetDoubleField(double fieldValue) - { - SetTextField(fieldValue.ToString( - GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat)); - } - - private void SetDecimalField(decimal fieldValue) - { - SetTextField(fieldValue.ToString( - GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat)); - } - - private void SetLongField(long fieldValue) - { - SetTextField(fieldValue.ToString( - GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat)); - } - - protected virtual void SetVirtualField(string fieldValueText) - { - if (Metadata.AttributeTypeName == AttributeTypeDisplayName.MultiSelectPicklistType) - App.App.Entity.SetValue(new MultiValueOptionSet { Name = LogicalName, Values = fieldValueText.Split(',').Select(v => v.Trim()).ToArray() }); - else - throw new NotImplementedException(string.Format("Virtual type {0} not implemented", Metadata.AttributeTypeName.Value)); - } - - protected virtual void ClearValue() - { - switch (Metadata.AttributeType.Value) - { - case AttributeTypeCode.Boolean: - throw new TestExecutionException(Constants.ErrorCodes.TWO_OPTION_FIELDS_CANT_BE_CLEARED); - case AttributeTypeCode.Customer: - case AttributeTypeCode.Lookup: - App.App.Entity.ClearValue(new LookupItem { Name = LogicalName }); - break; - case AttributeTypeCode.Picklist: - App.App.Entity.ClearValue(new OptionSet { Name = LogicalName }); - break; - case AttributeTypeCode.DateTime: - App.Client.SetValueFix(LogicalName, null, null, null); - break; - default: - SetTextField(null); - break; - } - } - - protected virtual void SetTwoOptionField(bool fieldValueBool, string fieldValueText) - { - App.App.Entity.SetValue(new BooleanItem { Name = LogicalName, Value = fieldValueBool }); - } - - protected virtual void SetDateTimeField(DateTime fieldValue, string fieldValueText) - { - DateTime dateTime = fieldValue; - - if (((DateTimeAttributeMetadata)Metadata).DateTimeBehavior == DateTimeBehavior.UserLocal) - { - var offset = GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.TimeZoneInfo.GetUtcOffset(fieldValue); - dateTime = dateTime.Add(offset); - } - - App.Client.SetValueFix(LogicalName, dateTime, GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.DateFormat, GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.TimeFormat); - } - - protected virtual void SetOptionSetField(OptionSetValue optionSetNumber, string optionSetLabel) - { - App.App.Entity.SetValue(new OptionSet { Name = LogicalName, Value = optionSetLabel }); - } - - protected virtual void SetMoneyField(Money fieldValue, string fieldValueText) - { - SetTextField(fieldValue?.Value.ToString()); - } - - protected virtual void SetTextField(string fieldValue) - { - if (string.IsNullOrWhiteSpace(fieldValue)) - App.App.Entity.ClearValue(LogicalName); - else - App.Client.SetValueFix(LogicalName, fieldValue); - } - - protected virtual void SetLookupValue(EntityReference fieldValue, string fieldValueText) - { - App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').setValue([ {{ id: '{fieldValue.Id}', name: '{fieldValue.Name.Replace("'", @"\'")}', entityType: '{fieldValue.LogicalName}' }} ])"); - App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').fireOnChange()"); - } - } -} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/BooleanValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/BooleanValue.cs new file mode 100644 index 00000000..e8c376f0 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/BooleanValue.cs @@ -0,0 +1,27 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class BooleanValue + { + public BooleanValue(bool? value) + { + Value = value; + } + + public bool? Value { get; } + + public string TextValue => Value?.ToString(CultureInfo.InvariantCulture); + + public BooleanItem ToBooleanItem(string logicalName) + { + return new BooleanItem { Name = logicalName, Value = Value.GetValueOrDefault() }; + } + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DateTimeValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DateTimeValue.cs new file mode 100644 index 00000000..ddc55cfa --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DateTimeValue.cs @@ -0,0 +1,26 @@ +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class DateTimeValue + { + public DateTimeValue(DateTimeAttributeMetadata metadata, DateTime? value) + { + if (value.HasValue && metadata.DateTimeBehavior == DateTimeBehavior.UserLocal) + { + var offset = GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.TimeZoneInfo.GetUtcOffset(value.Value); + Value = value.Value.Add(offset); + } + else + Value = value; + } + + public DateTime? Value { get; } + + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DecimalValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DecimalValue.cs new file mode 100644 index 00000000..d2de5bfc --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DecimalValue.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class DecimalValue + { + public DecimalValue(decimal? value) + { + Value = value; + } + + public decimal? Value { get; } + + public string TextValue => Value?.ToString( + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat); + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DoubleValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DoubleValue.cs new file mode 100644 index 00000000..745426f8 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/DoubleValue.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class DoubleValue + { + public DoubleValue(double? value) + { + Value = value; + } + + public double? Value { get; } + + public string TextValue => Value?.ToString( + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat); + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/IntegerValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/IntegerValue.cs new file mode 100644 index 00000000..0ad4acbf --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/IntegerValue.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class IntegerValue + { + + + public IntegerValue(int? value) + { + Value = value; + } + + public int? Value { get; } + + public string TextValue => Value?.ToString( + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat); + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LongValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LongValue.cs new file mode 100644 index 00000000..4a375306 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LongValue.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class LongValue + { + public LongValue(long? value) + { + Value = value; + } + + public long? Value { get; } + + public string TextValue => Value?.ToString( + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.NumberFormat); + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LookupValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LookupValue.cs new file mode 100644 index 00000000..88eacc00 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/LookupValue.cs @@ -0,0 +1,26 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class LookupValue + { + public LookupValue(EntityReference value) + { + Value = value; + } + + public EntityReference Value { get; } + + public LookupItem ToLookupItem(AttributeMetadata metadata) + { + return new LookupItem { Name = metadata.LogicalName, Value = Value?.Name }; + } + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/MultiSelectOptionSetValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/MultiSelectOptionSetValue.cs new file mode 100644 index 00000000..f5e6e614 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/MultiSelectOptionSetValue.cs @@ -0,0 +1,29 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class MultiSelectOptionSetValue + { + + + public MultiSelectOptionSetValue(string[] labelValues) + { + LabelValues = labelValues; + } + + public MultiValueOptionSet ToMultiValueOptionSet(string logicalName) + { + return new MultiValueOptionSet { Name = logicalName, Values = LabelValues }; + } + + public string[] LabelValues { get; } + + + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/OptionSetValue.cs b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/OptionSetValue.cs new file mode 100644 index 00000000..e1eb5c05 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/FieldTypes/OptionSetValue.cs @@ -0,0 +1,27 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Vermaat.Crm.Specflow.EasyRepro.FieldTypes +{ + public class OptionSetValue + { + public OptionSetValue(int? value, string label) + { + Value = value; + Label = label; + } + + public OptionSet ToOptionSet(string logicalName) + { + return new OptionSet { Name = logicalName, Value = Label }; + } + + public int? Value { get; } + public string Label { get; } + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/Fields/BodyFormField.cs b/Vermaat.Crm.Specflow/EasyRepro/Fields/BodyFormField.cs new file mode 100644 index 00000000..d26a557a --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/Fields/BodyFormField.cs @@ -0,0 +1,119 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using Microsoft.Dynamics365.UIAutomation.Api.UCI.DTO; +using Microsoft.Dynamics365.UIAutomation.Browser; +using Microsoft.Xrm.Sdk.Metadata; +using OpenQA.Selenium; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Vermaat.Crm.Specflow.EasyRepro.FieldTypes; + +namespace Vermaat.Crm.Specflow.EasyRepro.Fields +{ + public class BodyFormField : FormField + { + private string _tabLabel; + + public BodyFormField(UCIApp app, AttributeMetadata attributeMetadata, string control) + : base(app, attributeMetadata, control) + { + } + + public override bool IsVisible(FormState formState) + { + formState.ExpandTab(GetTabLabel()); + return base.IsVisible(formState); + } + + public override RequiredState GetRequiredState(FormState formState) + { + formState.ExpandTab(GetTabLabel()); + return base.GetRequiredState(formState); + } + + public override bool IsLocked(FormState formState) + { + formState.ExpandTab(GetTabLabel()); + return base.IsLocked(formState); + } + + private string GetTabLabel() + { + if (string.IsNullOrEmpty(_tabLabel)) + { + _tabLabel = App.WebDriver.ExecuteScript($"return Xrm.Page.getControl('{Control}').getParent().getParent().getLabel()")?.ToString(); + } + return _tabLabel; + } + + protected override void SetMultiSelectOptionSetField(MultiSelectOptionSetValue value) + { + App.App.Entity.SetValue(value.ToMultiValueOptionSet(LogicalName), true); + } + + protected override void SetIntegerField(IntegerValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetDoubleField(DoubleValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetDecimalField(DecimalValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetLongField(LongValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetTwoOptionField(BooleanValue value) + { + App.App.Entity.SetValue(value.ToBooleanItem(Metadata.LogicalName)); + } + + protected override void SetDateTimeField(DateTimeValue value) + { + App.Client.SetValueFix(LogicalName, value.Value, + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.DateFormat, + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.TimeFormat); + } + + protected override void SetOptionSetField(OptionSetValue value) + { + if (value.Value.HasValue) + App.App.Entity.SetValue(value.ToOptionSet(LogicalName)); + else + App.App.Entity.ClearValue(value.ToOptionSet(LogicalName)); + } + + protected override void SetMoneyField(DecimalValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetTextField(string fieldValue) + { + App.Client.SetValueFix(LogicalName, fieldValue, ContainerType.Body); + } + + protected override void SetLookupValue(LookupValue value) + { + if (value.Value != null) + { + App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').setValue([ {{ id: '{value.Value.Id}', name: '{value.Value.Name.Replace("'", @"\'")}', entityType: '{value.Value.LogicalName}' }} ])"); + App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').fireOnChange()"); + } + else + { + App.App.Entity.ClearValue(value.ToLookupItem(Metadata)); + } + } + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/Fields/Field.cs b/Vermaat.Crm.Specflow/EasyRepro/Fields/Field.cs new file mode 100644 index 00000000..9effc5ec --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/Fields/Field.cs @@ -0,0 +1,118 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using Microsoft.Dynamics365.UIAutomation.Api.UCI.DTO; +using Microsoft.Dynamics365.UIAutomation.Browser; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Vermaat.Crm.Specflow.EasyRepro.FieldTypes; + +namespace Vermaat.Crm.Specflow.EasyRepro.Fields +{ + public abstract class Field + { + protected AttributeMetadata Metadata { get; } + protected UCIApp App { get; } + + protected virtual string LogicalName => Metadata.LogicalName; + + public Field(UCIApp app, AttributeMetadata metadata) + { + Metadata = metadata; + App = app; + } + + public virtual void SetValue(CrmTestingContext crmContext, string fieldValueText) + { + object fieldValue = ObjectConverter.ToCrmObject(Metadata.EntityLogicalName, Metadata.LogicalName, fieldValueText, crmContext); + + Logger.WriteLine($"Setting field value"); + switch (Metadata.AttributeType.Value) + { + case AttributeTypeCode.Boolean: + SetTwoOptionField(new BooleanValue((bool?)fieldValue)); + break; + case AttributeTypeCode.DateTime: + SetDateTimeField(new DateTimeValue((DateTimeAttributeMetadata)Metadata, (DateTime?)fieldValue)); + break; + case AttributeTypeCode.Customer: + case AttributeTypeCode.Lookup: + SetLookupValue(new LookupValue((EntityReference)fieldValue)); + break; + case AttributeTypeCode.Picklist: + SetOptionSetField(new FieldTypes.OptionSetValue(((Microsoft.Xrm.Sdk.OptionSetValue)fieldValue)?.Value, fieldValueText)); + break; + case AttributeTypeCode.Money: + SetMoneyField(new DecimalValue(((Money)fieldValue)?.Value)); + break; + case AttributeTypeCode.Virtual: + SetVirtualField(fieldValueText); + break; + case AttributeTypeCode.Integer: + SetIntegerField(new IntegerValue((int?)fieldValue)); + break; + case AttributeTypeCode.Double: + SetDoubleField(new DoubleValue((double?)fieldValue)); + break; + case AttributeTypeCode.BigInt: + SetLongField(new LongValue((long?)fieldValue)); + break; + case AttributeTypeCode.Decimal: + SetDecimalField(new DecimalValue((decimal?)fieldValue)); + break; + default: + SetTextField((string)fieldValue); + break; + } + + } + + protected virtual void SetVirtualField(string fieldValueText) + { + if (Metadata.AttributeTypeName == AttributeTypeDisplayName.MultiSelectPicklistType) + SetMultiSelectOptionSetField(new MultiSelectOptionSetValue(fieldValueText.Split(',').Select(v => v.Trim()).ToArray())); + else + throw new NotImplementedException(string.Format("Virtual type {0} not implemented", Metadata.AttributeTypeName.Value)); + } + + + protected abstract void SetIntegerField(IntegerValue value); + + protected abstract void SetDoubleField(DoubleValue value); + + protected abstract void SetDecimalField(DecimalValue value); + + protected abstract void SetLongField(LongValue value); + + protected abstract void SetTwoOptionField(BooleanValue value); + + protected abstract void SetDateTimeField(DateTimeValue value); + + protected abstract void SetOptionSetField(FieldTypes.OptionSetValue value); + + protected abstract void SetMoneyField(DecimalValue value); + + protected abstract void SetTextField(string fieldValue); + + protected abstract void SetLookupValue(LookupValue value); + + protected abstract void SetMultiSelectOptionSetField(MultiSelectOptionSetValue value); + + + + + + + + + + + + + + + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/Fields/FormField.cs b/Vermaat.Crm.Specflow/EasyRepro/Fields/FormField.cs new file mode 100644 index 00000000..26201eaf --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/Fields/FormField.cs @@ -0,0 +1,68 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using Microsoft.Dynamics365.UIAutomation.Browser; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Metadata; +using OpenQA.Selenium; +using System; +using System.Collections.Generic; +using System.Linq; +using OpenQA.Selenium.Interactions; +using Microsoft.Dynamics365.UIAutomation.Api.UCI.DTO; + +namespace Vermaat.Crm.Specflow.EasyRepro.Fields +{ + public abstract class FormField : Field + { + + protected string Control { get; private set; } + + public FormField(UCIApp app, AttributeMetadata attributeMetadata, string control) + : base(app, attributeMetadata) + { + Control = control; + } + + public virtual RequiredState GetRequiredState(FormState formState) + { + BrowserCommandResult result = App.Client.Execute(BrowserOptionHelper.GetOptions($"Check field requirement"), driver => + { + IWebElement fieldContainer = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", LogicalName))); + if (fieldContainer == null) + throw new TestExecutionException(Constants.ErrorCodes.FIELD_NOT_ON_FORM, LogicalName); + + if (fieldContainer.TryFindElement(SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_FormState_RequiredOrRecommended, LogicalName), out IWebElement requiredElement)) + { + if (requiredElement.GetAttribute("innerText") == "*") + return RequiredState.Required; + else + return RequiredState.Recommended; + } + else + { + return RequiredState.Optional; + } + }); + + return result.Value; + } + + public virtual bool IsVisible(FormState formState) + { + return App.WebDriver.WaitUntilVisible( + SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_FieldContainer, Control), + TimeSpan.FromSeconds(5)) != null; + } + + public virtual bool IsLocked(FormState formState) + { + return App.Client.Execute(BrowserOptionHelper.GetOptions($"Check field locked state"), driver => + { + IWebElement fieldContainer = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", LogicalName))); + if (fieldContainer == null) + throw new TestExecutionException(Constants.ErrorCodes.FIELD_NOT_ON_FORM, LogicalName); + + return fieldContainer.TryFindElement(SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_FormState_LockedIcon, LogicalName), out IWebElement requiredElement); + }).Value; + } + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/Fields/HeaderFormField.cs b/Vermaat.Crm.Specflow/EasyRepro/Fields/HeaderFormField.cs new file mode 100644 index 00000000..068098d7 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/Fields/HeaderFormField.cs @@ -0,0 +1,96 @@ +using Microsoft.Dynamics365.UIAutomation.Api.UCI.DTO; +using Microsoft.Dynamics365.UIAutomation.Browser; +using Microsoft.Xrm.Sdk.Metadata; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Vermaat.Crm.Specflow.EasyRepro.FieldTypes; + +namespace Vermaat.Crm.Specflow.EasyRepro.Fields +{ + class HeaderFormField : FormField + { + + + public HeaderFormField(UCIApp app, AttributeMetadata attributeMetadata, string control) + : base(app, attributeMetadata, control) + { + } + + public override bool IsVisible(FormState formState) + { + formState.ExpandHeader(); + return base.IsVisible(formState); + } + + protected override void SetDateTimeField(DateTimeValue value) + { + App.Client.SetValueFix(LogicalName, value.Value, + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.DateFormat, + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.TimeFormat); + } + + protected override void SetDecimalField(DecimalValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetDoubleField(DoubleValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetIntegerField(IntegerValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetLongField(LongValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetLookupValue(LookupValue value) + { + if (value.Value != null) + { + App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').setValue([ {{ id: '{value.Value.Id}', name: '{value.Value.Name.Replace("'", @"\'")}', entityType: '{value.Value.LogicalName}' }} ])"); + App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').fireOnChange()"); + } + else + { + App.App.Entity.ClearValue(value.ToLookupItem(Metadata)); + } + } + + protected override void SetMoneyField(DecimalValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetMultiSelectOptionSetField(MultiSelectOptionSetValue value) + { + App.App.Entity.SetHeaderValue(value.ToMultiValueOptionSet(LogicalName)); + } + + protected override void SetOptionSetField(OptionSetValue value) + { + if (value.Value.HasValue) + App.App.Entity.SetHeaderValue(value.ToOptionSet(LogicalName)); + else + App.App.Entity.ClearValue(value.ToOptionSet(LogicalName)); + } + + protected override void SetTextField(string fieldValue) + { + App.Client.SetValueFix(LogicalName, fieldValue, ContainerType.Header); + } + + protected override void SetTwoOptionField(BooleanValue value) + { + App.App.Entity.SetHeaderValue(value.ToBooleanItem(LogicalName)); + } + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/Fields/OpportunityCloseDialogField.cs b/Vermaat.Crm.Specflow/EasyRepro/Fields/OpportunityCloseDialogField.cs new file mode 100644 index 00000000..76ea4690 --- /dev/null +++ b/Vermaat.Crm.Specflow/EasyRepro/Fields/OpportunityCloseDialogField.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Dynamics365.UIAutomation.Api.UCI.DTO; +using Microsoft.Dynamics365.UIAutomation.Browser; +using Microsoft.Xrm.Sdk.Metadata; +using Vermaat.Crm.Specflow.EasyRepro.FieldTypes; + +namespace Vermaat.Crm.Specflow.EasyRepro.Fields +{ + class OpportunityCloseDialogField : Field + { + protected override string LogicalName => GetLogicalName(); + + public OpportunityCloseDialogField(UCIApp app, AttributeMetadata metadata) + : base(app, metadata) + { + } + + private string GetLogicalName() + { + if (Metadata.LogicalName == "opportunitystatuscode") + return "statusreason_id"; + else + return base.LogicalName + "_id"; + } + + protected override void SetMultiSelectOptionSetField(MultiSelectOptionSetValue value) + { + App.App.Entity.SetValue(value.ToMultiValueOptionSet(LogicalName), true); + } + + protected override void SetIntegerField(IntegerValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetDoubleField(DoubleValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetDecimalField(DecimalValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetLongField(LongValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetTwoOptionField(BooleanValue value) + { + App.App.Entity.SetValue(value.ToBooleanItem(LogicalName)); + } + + protected override void SetDateTimeField(DateTimeValue value) + { + App.Client.SetValueFix(LogicalName, value.Value, + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.DateFormat, + GlobalTestingContext.ConnectionManager.CurrentConnection.UserSettings.TimeFormat); + } + + protected override void SetOptionSetField(FieldTypes.OptionSetValue value) + { + if (value.Value.HasValue) + App.Client.SetValueFix(value.ToOptionSet(LogicalName), ContainerType.Dialog); + else + App.App.Entity.ClearValue(value.ToOptionSet(LogicalName)); + } + + protected override void SetMoneyField(DecimalValue value) + { + SetTextField(value.TextValue); + } + + protected override void SetTextField(string fieldValue) + { + App.Client.SetValueFix(LogicalName, fieldValue, ContainerType.Dialog); + } + + protected override void SetLookupValue(LookupValue value) + { + if (value.Value != null) + { + App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').setValue([ {{ id: '{value.Value.Id}', name: '{value.Value.Name.Replace("'", @"\'")}', entityType: '{value.Value.LogicalName}' }} ])"); + App.WebDriver.ExecuteScript($"Xrm.Page.getAttribute('{LogicalName}').fireOnChange()"); + } + else + { + App.App.Entity.ClearValue(value.ToLookupItem(Metadata)); + } + } + + } +} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FormData.cs b/Vermaat.Crm.Specflow/EasyRepro/FormData.cs index 9d4d3d5c..68290c43 100644 --- a/Vermaat.Crm.Specflow/EasyRepro/FormData.cs +++ b/Vermaat.Crm.Specflow/EasyRepro/FormData.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using TechTalk.SpecFlow; +using Vermaat.Crm.Specflow.EasyRepro.Fields; namespace Vermaat.Crm.Specflow.EasyRepro { @@ -43,19 +44,6 @@ public bool ContainsField(string fieldLogicalName) return containsField; } - public void ExpandTab(string tabLabel) - { - Logger.WriteLine($"Expanding tab {tabLabel}"); - _app.App.Entity.SelectTab(tabLabel); - } - - public void ExpandHeader() - { - Logger.WriteLine("Expanding headers"); - var header = SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_Header, string.Empty); - _app.WebDriver.ClickWhenAvailable(header); - } - public string GetErrorDialogMessage() { Logger.WriteLine("Getting error dialog message"); @@ -93,21 +81,13 @@ public void Save(bool saveIfDuplicate) public void FillForm(CrmTestingContext crmContext, Table formData) { Logger.WriteLine($"Filling form"); - string currentTab = null; + var formState = new FormState(_app); foreach (var row in formData.Rows) { Assert.IsTrue(ContainsField(row[Constants.SpecFlow.TABLE_KEY]), $"Field {row[Constants.SpecFlow.TABLE_KEY]} isn't on the form"); var field = _formFields[row[Constants.SpecFlow.TABLE_KEY]]; - - var newTab = field.GetTabName(); - if (!field.IsFieldInHeaderOnly() && (string.IsNullOrWhiteSpace(currentTab) || currentTab != newTab)) - { - ExpandTab(field.GetTabLabel()); - currentTab = newTab; - } - - Assert.IsTrue(field.IsVisible(), $"Field {row[Constants.SpecFlow.TABLE_KEY]} isn't visible"); - Assert.IsFalse(field.IsLocked(), $"Field {row[Constants.SpecFlow.TABLE_KEY]} is read-only"); + Assert.IsTrue(field.IsVisible(formState), $"Field {row[Constants.SpecFlow.TABLE_KEY]} isn't visible"); + Assert.IsFalse(field.IsLocked(formState), $"Field {row[Constants.SpecFlow.TABLE_KEY]} is read-only"); field.SetValue(crmContext, row[Constants.SpecFlow.TABLE_VALUE]); } @@ -174,10 +154,30 @@ private Dictionary InitializeFormData() controls[i] = attribute["controls"][i]; } - formFields.Add(attribute["name"], new FormField(this, _app, metadataDic[attribute["name"]], controls)); + FormField field = CreateFormField(metadataDic[attribute["name"]], controls); + if (field != null) + formFields.Add(attribute["name"], field); + } return formFields; } + + private FormField CreateFormField(AttributeMetadata metadata, string[] controls) + { + if (controls.Length == 0) + return null; + + // Take the first control that isn't in the header + for(int i = 0; i < controls.Length; i++) + { + if(!controls[i].StartsWith("header")) + { + return new BodyFormField(_app, metadata, controls[i]); + } + } + // If all are in the header, take the first header control + return new HeaderFormField(_app, metadata, controls[0]); + } } } diff --git a/Vermaat.Crm.Specflow/EasyRepro/FormField.cs b/Vermaat.Crm.Specflow/EasyRepro/FormField.cs deleted file mode 100644 index 94881ebf..00000000 --- a/Vermaat.Crm.Specflow/EasyRepro/FormField.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Microsoft.Dynamics365.UIAutomation.Api.UCI; -using Microsoft.Dynamics365.UIAutomation.Browser; -using Microsoft.Xrm.Sdk; -using Microsoft.Xrm.Sdk.Metadata; -using OpenQA.Selenium; -using System; -using System.Collections.Generic; -using System.Linq; -using OpenQA.Selenium.Interactions; - -namespace Vermaat.Crm.Specflow.EasyRepro -{ - public class FormField : Field - { - - private readonly string[] _controls; - private readonly FormData _form; - - private string _tabLabel; - private string _tabName; - private bool? _isFieldInHeaderOnly; - - public IEnumerable Controls => _controls; - - public FormField(FormData form, UCIApp app, AttributeMetadata attributeMetadata, string[] controls) - : base(app, attributeMetadata) - { - _form = form; - _controls = controls; - } - - public string GetDefaultControl() - { - if (_controls.Length == 1) - return _controls[0]; - else - return _controls.FirstOrDefault(c => !c.StartsWith("header")); - } - - public string GetTabLabel() - { - if (string.IsNullOrEmpty(_tabLabel)) - { - if (IsFieldInHeaderOnly()) - _tabLabel = string.Empty; - else - _tabLabel = App.WebDriver.ExecuteScript($"return Xrm.Page.getControl('{GetDefaultControl()}').getParent().getParent().getLabel()")?.ToString(); - } - return _tabLabel; - } - - public RequiredState GetRequiredState() - { - BrowserCommandResult result = App.Client.Execute(BrowserOptionHelper.GetOptions($"Check field requirement"), driver => - { - IWebElement fieldContainer = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", LogicalName))); - if (fieldContainer == null) - throw new TestExecutionException(Constants.ErrorCodes.FIELD_NOT_ON_FORM, LogicalName); - - if (fieldContainer.TryFindElement(SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_FormState_RequiredOrRecommended, LogicalName), out IWebElement requiredElement)) - { - if (requiredElement.GetAttribute("innerText") == "*") - return RequiredState.Required; - else - return RequiredState.Recommended; - } - else - { - return RequiredState.Optional; - } - }); - - return result.Value; - } - - public bool IsVisible() - { - if (IsFieldInHeaderOnly()) - _form.ExpandHeader(); - else if (!IsTabOfFieldExpanded()) - _form.ExpandTab(GetTabLabel()); - - return App.WebDriver.WaitUntilVisible( - SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_FieldContainer, GetDefaultControl()), - TimeSpan.FromSeconds(5)); - } - - public bool IsLocked() - { - return App.Client.Execute(BrowserOptionHelper.GetOptions($"Check field locked state"), driver => - { - IWebElement fieldContainer = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", LogicalName))); - if (fieldContainer == null) - throw new TestExecutionException(Constants.ErrorCodes.FIELD_NOT_ON_FORM, LogicalName); - - return fieldContainer.TryFindElement(SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_FormState_LockedIcon, LogicalName), out IWebElement requiredElement); - }).Value; - } - - public string GetTabName() - { - if (string.IsNullOrEmpty(_tabName)) - { - if (IsFieldInHeaderOnly()) - _tabName = "Header"; - else - _tabName = App.WebDriver.ExecuteScript($"return Xrm.Page.getControl('{GetDefaultControl()}').getParent().getParent().getName()")?.ToString(); - } - return _tabName; - } - - public bool IsTabOfFieldExpanded() - { - - string result = App.WebDriver.ExecuteScript($"return Xrm.Page.ui.tabs.get('{GetTabName()}').getDisplayState()")?.ToString(); - return "expanded".Equals(result, StringComparison.CurrentCultureIgnoreCase); - } - - public bool IsFieldInHeaderOnly() - { - if (_isFieldInHeaderOnly == null) - _isFieldInHeaderOnly = _controls.Length == 1 && _controls[0].StartsWith("header"); - return _isFieldInHeaderOnly.Value; - } - - } -} diff --git a/Vermaat.Crm.Specflow/EasyRepro/FormState.cs b/Vermaat.Crm.Specflow/EasyRepro/FormState.cs index c39a5688..58847e4e 100644 --- a/Vermaat.Crm.Specflow/EasyRepro/FormState.cs +++ b/Vermaat.Crm.Specflow/EasyRepro/FormState.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Dynamics365.UIAutomation.Browser; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -6,10 +7,39 @@ namespace Vermaat.Crm.Specflow.EasyRepro { - class FormState + public class FormState { - public FormVisibility? Visible { get; set; } - public bool? Locked { get; set; } - public RequiredState? Required { get; set; } + private readonly UCIApp _app; + + public string CurrentTab { get; set; } + + public FormState(UCIApp app) + { + _app = app; + } + + public void ResetState() + { + CurrentTab = null; + } + + + public void ExpandHeader() + { + Logger.WriteLine("Expanding headers"); + var header = SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_Header, string.Empty); + _app.WebDriver.ClickWhenAvailable(header); + } + + public void ExpandTab(string tabLabel) + { + if (string.IsNullOrEmpty(CurrentTab) || !CurrentTab.Equals(tabLabel, StringComparison.OrdinalIgnoreCase)) + { + Logger.WriteLine($"Expanding tab {tabLabel}"); + _app.App.Entity.SelectTab(tabLabel); + CurrentTab = tabLabel; + } + + } } } diff --git a/Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialog.cs b/Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialog.cs index b06cad6f..84b6b938 100644 --- a/Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialog.cs +++ b/Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialog.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading.Tasks; using TechTalk.SpecFlow; +using Vermaat.Crm.Specflow.EasyRepro.Fields; using Vermaat.Crm.Specflow.FormLoadConditions; namespace Vermaat.Crm.Specflow.EasyRepro @@ -54,7 +55,7 @@ public void FinishDialog() driver.WaitUntilClickable(By.XPath(AppElements.Xpath[AppReference.Dialogs.CloseOpportunity.Ok]), new TimeSpan(0, 0, 5), d => { driver.ClickWhenAvailable(By.XPath(AppElements.Xpath[AppReference.Dialogs.CloseOpportunity.Ok])); }, - d => { throw new InvalidOperationException("The Close Opportunity dialog is not available."); }); + () => { throw new InvalidOperationException("The Close Opportunity dialog is not available."); }); HelperMethods.WaitForFormLoad(_app.WebDriver, new NoBusinessProcessError(), new RecordHasStatus(_closeAsWon ? "Won" : "Lost")); diff --git a/Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialogField.cs b/Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialogField.cs deleted file mode 100644 index 22032ac2..00000000 --- a/Vermaat.Crm.Specflow/EasyRepro/OpportunityCloseDialogField.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Dynamics365.UIAutomation.Api.UCI; -using Microsoft.Xrm.Sdk; -using Microsoft.Xrm.Sdk.Metadata; - -namespace Vermaat.Crm.Specflow.EasyRepro -{ - class OpportunityCloseDialogField : Field - { - protected override string LogicalName => GetLogicalName(); - - private string GetLogicalName() - { - if (Metadata.LogicalName == "opportunitystatuscode") - return "statusreason_id"; - else - return base.LogicalName + "_id"; - } - - public OpportunityCloseDialogField(UCIApp app, AttributeMetadata metadata) - : base(app, metadata) - { - } - - protected override void SetLookupValue(EntityReference fieldValue, string fieldValueText) - { - App.App.Entity.SetValue(new LookupItem { Name = LogicalName, Value = fieldValueText }); - } - } -} diff --git a/Vermaat.Crm.Specflow/EasyRepro/TemporaryFixes.cs b/Vermaat.Crm.Specflow/EasyRepro/TemporaryFixes.cs index 6a4c9d84..9e94b9ba 100644 --- a/Vermaat.Crm.Specflow/EasyRepro/TemporaryFixes.cs +++ b/Vermaat.Crm.Specflow/EasyRepro/TemporaryFixes.cs @@ -1,11 +1,14 @@ using Microsoft.Dynamics365.UIAutomation.Api.UCI; +using Microsoft.Dynamics365.UIAutomation.Api.UCI.DTO; using Microsoft.Dynamics365.UIAutomation.Browser; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using SeleniumExtras.WaitHelpers; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; +using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,55 +17,8 @@ namespace Vermaat.Crm.Specflow.EasyRepro { public static class TemporaryFixes { - /// - /// Set Value - /// - /// The field - /// The value - /// xrmApp.Entity.SetValue("firstname", "Test"); - public static BrowserCommandResult SetValueFix(this WebClient client, string field, string value) - { - return client.Execute(BrowserOptionHelper.GetOptions($"Set Value"), driver => - { - var query = By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", field)); - IWebElement fieldContainer = WaitUntilClickable(driver, query, TimeSpan.FromSeconds(5), null, null); - if (fieldContainer == null) - { - throw new TestExecutionException(Constants.ErrorCodes.ELEMENT_NOT_INTERACTABLE, $"Field {field} is probably locked or invisible"); - } - - IWebElement input; - if (fieldContainer.FindElements(By.TagName("input")).Count > 0) - { - input = fieldContainer.FindElement(By.TagName("input")); - - } - else if (fieldContainer.FindElements(By.TagName("textarea")).Count > 0) - { - input = fieldContainer.FindElement(By.TagName("textarea")); - } - else - { - throw new Exception($"Field with name {field} does not exist."); - } - - if (input != null) - { - input.SendKeys(Keys.Control + "a"); - input.SendKeys(Keys.Backspace); - - if (!string.IsNullOrWhiteSpace(value)) - { - input.SendKeys(value); - } - - input.SendKeys(Keys.Tab + Keys.Tab); - } - - return true; - }); - } + #region https://github.com/DynamicHands/Crm.Specflow/issues/44 /// /// Sets the value of a Date Field. @@ -166,7 +122,6 @@ public static bool RepeatUntil(this IWebDriver driver, Action action, Predicate< return success; } - private static IWebElement WaitUntilClickable(IWebDriver driver, By by, TimeSpan timeout, Action successCallback, Action failureCallback) { WebDriverWait wait = new WebDriverWait(driver, timeout); @@ -196,5 +151,227 @@ private static IWebElement WaitUntilClickable(IWebDriver driver, By by, TimeSpan return returnElement; } + + + #endregion + + #region https://github.com/DynamicHands/Crm.Specflow/issues/78 + + public static void Login(WebClient client, Uri orgUri, SecureString username, SecureString password) + { + client.Execute(BrowserOptionHelper.GetOptions("Login"), Login, client, orgUri, username, password); + } + + private static LoginResult Login(IWebDriver driver, WebClient client, Uri uri, SecureString username, SecureString password) + { + bool online = !(client.OnlineDomains != null && !client.OnlineDomains.Any(d => uri.Host.EndsWith(d))); + driver.Navigate().GoToUrl(uri); + + if (!online) + return LoginResult.Success; + + driver.WaitUntilClickable(By.Id("use_another_account_link"), TimeSpan.FromSeconds(1), e => e.Click()); + + bool success = EnterUserName(driver, username); + driver.ClickIfVisible(By.Id("aadTile")); + client.Browser.ThinkTime(1000); + + EnterPassword(driver, password); + client.Browser.ThinkTime(1000); + + int attempts = 0; + do + { + success = ClickStaySignedIn(driver); + attempts++; + } + while (!success && attempts <= 3); + + if (success) + WaitForMainPage(driver); + else + throw new InvalidOperationException("Login failed"); + + return success ? LoginResult.Success : LoginResult.Failure; + } + + private static bool EnterUserName(IWebDriver driver, SecureString username) + { + var input = driver.WaitUntilAvailable(By.XPath(Elements.Xpath[Reference.Login.UserId]), new TimeSpan(0, 0, 30)); + if (input == null) + return false; + + input.SendKeys(username.ToUnsecureString()); + input.SendKeys(Keys.Enter); + return true; + } + + private static void EnterPassword(IWebDriver driver, SecureString password) + { + var input = driver.FindElement(By.XPath(Elements.Xpath[Reference.Login.LoginPassword])); + input.SendKeys(password.ToUnsecureString()); + input.Submit(); + } + + private static bool ClickStaySignedIn(IWebDriver driver) + { + var xpath = By.XPath(Elements.Xpath[Reference.Login.StaySignedIn]); + var element = driver.ClickIfVisible(xpath, 5.Seconds()); + return element != null; + } + + internal static bool WaitForMainPage(IWebDriver driver) + { + Action successCallback = _ => + { + bool 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, TimeSpan.FromSeconds(30), successCallback); + return element != null; + } + + #endregion + + #region https://github.com/DynamicHands/Crm.Specflow/issues/88 + + /// + /// Set Value + /// + /// The field + /// The value + /// xrmApp.Entity.SetValue("firstname", "Test"); + public static BrowserCommandResult SetValueFix(this WebClient client, string field, string value, ContainerType formContextType) + { + return client.Execute(BrowserOptionHelper.GetOptions("Set Value"), driver => + { + + IWebElement fieldContainer = null; + + if (formContextType == ContainerType.Body) + { + // Initialize the entity form context + var formContext = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.FormContext])); + fieldContainer = formContext.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", field))); + } + else if (formContextType == ContainerType.Header) + { + // Initialize the Header context + var formContext = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.HeaderContext])); + fieldContainer = formContext.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", field))); + } + else if (formContextType == ContainerType.Dialog) + { + // Initialize the Dialog context + var formContext = driver.WaitUntilAvailable(SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Dialog_Container)); + fieldContainer = formContext.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", field))); + } + + IWebElement input; + bool found = fieldContainer.TryFindElement(By.TagName("input"), out input); + + if (!found) + found = fieldContainer.TryFindElement(By.TagName("textarea"), out input); + + if (!found) + throw new NoSuchElementException($"Field with name {field} does not exist."); + + SetInputValue(driver, input, value); + + return true; + }); + } + + private static void SetInputValue(IWebDriver driver, IWebElement input, string value) + { + + input.SendKeys(Keys.Control + "a"); + input.SendKeys(Keys.Backspace); + + if (!string.IsNullOrWhiteSpace(value)) + { + input.SendKeys(value); + } + + input.SendKeys(Keys.Tab + Keys.Tab); + } + + /// + /// Sets the value of a picklist or status field. + /// + /// The option you want to set. + /// xrmApp.Entity.SetValue(new OptionSet { Name = "preferredcontactmethodcode", Value = "Email" }); + public static BrowserCommandResult SetValueFix(this WebClient client, OptionSet control, ContainerType formContextType) + { + var controlName = control.Name; + return client.Execute(BrowserOptionHelper.GetOptions($"Set OptionSet Value: {controlName}"), driver => + { + IWebElement fieldContainer = null; + + if (formContextType == ContainerType.Body) + { + // Initialize the entity form context + var formContext = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.FormContext])); + fieldContainer = formContext.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", controlName))); + } + else if (formContextType == ContainerType.Header) + { + // Initialize the Header context + var formContext = driver.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.HeaderContext])); + fieldContainer = formContext.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", controlName))); + } + else if (formContextType == ContainerType.Dialog) + { + // Initialize the Dialog context + var formContext = driver.WaitUntilAvailable(SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Dialog_Container)); + fieldContainer = formContext.WaitUntilAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.TextFieldContainer].Replace("[NAME]", controlName))); + } + + TrySetValue(fieldContainer, control); + return true; + }); + } + + private static void TrySetValue(IWebElement fieldContainer, OptionSet control) + { + var value = control.Value; + bool success = fieldContainer.TryFindElement(By.TagName("select"), out IWebElement select); + if (success) + { + var options = select.FindElements(By.TagName("option")); + SelectOption(options, value); + return; + } + + var name = control.Name; + var hasStatusCombo = fieldContainer.HasElement(By.XPath(AppElements.Xpath[AppReference.Entity.EntityOptionsetStatusCombo].Replace("[NAME]", name))); + if (hasStatusCombo) + { + // This is for statuscode (type = status) that should act like an optionset doesn't doesn't follow the same pattern when rendered + fieldContainer.ClickWhenAvailable(By.XPath(AppElements.Xpath[AppReference.Entity.EntityOptionsetStatusComboButton].Replace("[NAME]", name))); + + var listBox = fieldContainer.FindElement(By.XPath(AppElements.Xpath[AppReference.Entity.EntityOptionsetStatusComboList].Replace("[NAME]", name))); + + var options = listBox.FindElements(By.TagName("li")); + SelectOption(options, value); + return; + } + + throw new InvalidOperationException($"OptionSet Field: '{name}' does not exist"); + } + + private static void SelectOption(ReadOnlyCollection options, string value) + { + var selectedOption = options.FirstOrDefault(op => op.Text == value || op.GetAttribute("value") == value); + selectedOption.Click(true); + } + + #endregion + + + } } diff --git a/Vermaat.Crm.Specflow/EasyRepro/UCIBrowser.cs b/Vermaat.Crm.Specflow/EasyRepro/UCIBrowser.cs index acb4cacd..27edfad8 100644 --- a/Vermaat.Crm.Specflow/EasyRepro/UCIBrowser.cs +++ b/Vermaat.Crm.Specflow/EasyRepro/UCIBrowser.cs @@ -39,7 +39,7 @@ public UCIBrowser(BrowserOptions browserOptions, ButtonTexts buttonTexts, CrmMod public void Login(BrowserLoginDetails loginDetails) { Logger.WriteLine("Logging in CRM"); - App.App.OnlineLogin.Login(new Uri(loginDetails.Url), loginDetails.Username.ToSecureString(), loginDetails.Password); + TemporaryFixes.Login(App.Client, new Uri(loginDetails.Url), loginDetails.Username.ToSecureString(), loginDetails.Password); } public void ChangeApp(string appUniqueName) diff --git a/Vermaat.Crm.Specflow/ErrorCodes.cs b/Vermaat.Crm.Specflow/ErrorCodes.cs index c33a8cae..89156d7e 100644 --- a/Vermaat.Crm.Specflow/ErrorCodes.cs +++ b/Vermaat.Crm.Specflow/ErrorCodes.cs @@ -54,6 +54,7 @@ private void FillDictionary() AddError(Constants.ErrorCodes.ASYNC_TIMEOUT, "Not all Asynchronous jobs are completed on time"); AddError(Constants.ErrorCodes.MULTIPLE_ATTRIBUTES_FOUND, "Multiple attributes found for {0}. Results: {1}"); AddError(Constants.ErrorCodes.APPLICATIONUSER_CANNOT_LOGIN, "An application user can't login via the browser"); + AddError(Constants.ErrorCodes.NO_CONTROL_FOUND, "No controls found for attribute {0}"); } public void AddError(int errorCode, string message) diff --git a/Vermaat.Crm.Specflow/HelperMethods.cs b/Vermaat.Crm.Specflow/HelperMethods.cs index 6469dfd9..d13177ac 100644 --- a/Vermaat.Crm.Specflow/HelperMethods.cs +++ b/Vermaat.Crm.Specflow/HelperMethods.cs @@ -105,7 +105,7 @@ public static void WaitForFormLoad(IWebDriver driver, params IFormLoadCondition[ driver.WaitUntilClickable(SeleniumFunctions.Selectors.GetXPathSeleniumSelector(SeleniumSelectorItems.Entity_FormLoad), timeLeft, null, - d => { throw new TestExecutionException(Constants.ErrorCodes.FORM_LOAD_TIMEOUT); } + () => { throw new TestExecutionException(Constants.ErrorCodes.FORM_LOAD_TIMEOUT); } ); if (additionalConditions != null) diff --git a/Vermaat.Crm.Specflow/Vermaat.Crm.Specflow.csproj b/Vermaat.Crm.Specflow/Vermaat.Crm.Specflow.csproj index bdf443f1..8c9a86b5 100644 --- a/Vermaat.Crm.Specflow/Vermaat.Crm.Specflow.csproj +++ b/Vermaat.Crm.Specflow/Vermaat.Crm.Specflow.csproj @@ -21,7 +21,7 @@ - +