![]() | Setting Custom Fields |
In this walkthrough you'll gain experience setting a custom field on a dialog and overriding a default implementations.
![]() |
---|
Examples follow customizations that you likely will not have on your application. Either follow along against your own customizations and modify steps accordingly, or follow the screenshots below. |
You have completed the "Using the Selenium WebDriver" walkthrough and have access to an Enterprise CRM application.
You have completed the "SpecFlow's Table and TableRow Guidelines" walkthrough.
You are comfortable adding tests and step implementations to existing feature and step files.
You are comfortable accessing the existing UAT SDK (Project Blue) Core API.
You are comfortable modifying the app.config to change which application the tests run against.
You are comfortable identifying the unique attribute values for the XPath constructors in the Core API and have completed the "XPath Guidelines" walkthrough.
Identify Need for Custom Support
![]() |
---|
Notice that there are also custom required fields as well. We need to consider that when setting the fields for adding an individual on this application. |
using System; using Blackbaud.UAT.Base; using Blackbaud.UAT.Core.Base; using TechTalk.SpecFlow; using System.Collections.Generic; namespace Delving_Deeper { [Binding] public class SampleTestsSteps : BaseSteps { [Given(@"I have logged into BBCRM")] public void GivenIHaveLoggedIntoBBCRM() { BBCRMHomePage.Logon(); } [When(@"I add constituent")] public void WhenIAddConstituent(Table constituents) { foreach (var constituent in constituents.Rows) { BBCRMHomePage.OpenConstituentsFA(); ConstituentsFunctionalArea.AddAnIndividual(groupCaption: "Add Records"); IndividualDialog.SetIndividualFields(constituent); IndividualDialog.Save(); } } [Then(@"a constituent is created")] public void ThenAConstituentIsCreated() { if (!BaseComponent.Exists(Panel.getXPanelHeader("individual"))) FailTest("A constituent panel did not load."); } } }
![]() |
---|
The application with the custom field also had a custom group caption for the "Add an individual" task. This is why the ConstituentsFunctionalArea.AddAnIndividual() call overloads the 'groupCaption' parameter. |
Create Class Inheriting Core Class
To resolve this failure, we need to add support for the additional custom fields. Create a new class in your project.
using System; using Blackbaud.UAT.Base; using Blackbaud.UAT.Core.Base; using TechTalk.SpecFlow; using System.Collections.Generic; namespace Delving_Deeper { public class CustomIndividualDialog : IndividualDialog { } }
Create Custom Supported Fields Mapping
We need to map the custom field captions to their relevant XPath and Field Setter values.
using System; using Blackbaud.UAT.Base; using Blackbaud.UAT.Core.Base; using TechTalk.SpecFlow; using System.Collections.Generic; namespace Delving_Deeper { public class CustomIndividualDialog : IndividualDialog { private static readonly IDictionary<string, CrmField> CustomSupportedFields = new Dictionary<string, CrmField> { {"Country of Origin", new CrmField("_ATTRIBUTECATEGORYVALUE0_value", FieldType.Dropdown)}, {"Matriculation Year (Use)", new CrmField("_ATTRIBUTECATEGORYVALUE1_value", FieldType.Dropdown)} }; } }
![]() |
---|
You should be comfortable understanding how the unique id attributes for the fields were gathered from the UI. These values are used for the XPath constructors that locate and interact with the fields. Review the XPath Guidelines if you do not follow where the values "_ATTRIBUTECATEGORYVALUE0_value" and "_ATTRIBUTECATEGORYVALUE1_value" come from. |
Pass Custom Supported Fields To Base.
We need to map the custom field captions to their relevant XPath and Field Setter values.
public new static void SetIndividualFields(TableRow fields) { SetFields(GetDialogId(DialogIds), fields, SupportedFields, CustomSupportedFields); }
The custom class uses its inherited IndividualDialog values to pass the required values to the Dialog's SetFields() method. SetFields has an overload that takes in a second IDictionary mapping of field captions to CrmFields. We can pass our dictionary of custom fields to add additional support for custom fields.
![]() |
---|
If a mapping exists in our CustomSupportedFields where the string key is an already existing key for SupportedFields, the mapped values for CustomSupportedFields is used. |
Modify Your Step Implementation.
Modify your step definition to use the new CustomIndividualDIalog's SetIndividualFields() method.
[When(@"I add constituent")] public void WhenIAddConstituent(Table constituents) { foreach (var constituent in constituents.Rows) { BBCRMHomePage.OpenConstituentsFA(); constituent["Last name"] += uniqueStamp; ConstituentsFunctionalArea.AddAnIndividual(groupCaption: "Add Records"); CustomIndividualDialog.SetIndividualFields(constituent); IndividualDialog.Save(); } }
![]() |
---|
Depending on your application, the Save step may cause a duplicate entry or error dialog to appear in the application. If this occurs, we advise adding a unique stamp to the last name of your constituent's values as shown above. |
Alternative Gherkin Syntax
Add Method to Custom Class
In this approach we describe setting a single field's value for a step. Add the following method to your CustomIndividualDialog class.
public static void SetCustomField(string fieldCaption, string value) { //Use the same IDictionary<string, CrmField> CustomSupportedFields from the Overload Approach SetField(GetDialogId(DialogIds), fieldCaption, value, CustomSupportedFields); }
![]() |
---|
Notice how the custom method did not need the "new" attribute in the method declaration. "new" is only needed when overriding an inherited method. |
Implement The New Step Methods
[When(@"I start to add a constituent")] public void WhenIStartToAddAConstituent(Table constituents) { foreach (var constituent in constituents.Rows) { BBCRMHomePage.OpenConstituentsFA(); constituent["Last name"] += uniqueStamp; ConstituentsFunctionalArea.AddAnIndividual(groupCaption: "Add Records"); IndividualDialog.SetIndividualFields(constituent); } } [When(@"I set the custom constituent field ""(.*)"" to ""(.*)""")] public void WhenISetTheCustomConstituentFieldTo(string fieldCaption, string value) { CustomIndividualDialog.SetCustomField(fieldCaption, value); } [When(@"I save the add an individual dialog")] public void WhenISaveTheAddAnIndividualDialog() { IndividualDialog.Save(); }
![]() |
---|
There are many ways to use the UAT SDK (Project Blue) API in order to achieve the same result. Above are two potential implementations to handle a dialog with a custom field, but these are not the only approaches. The methods and their underlying logic are totally defined by the user. You are free to create whatever helper methods you see fit. Look into the API documentation and see if you can come up with a different solution. |
Identify Need To Overload Implementation
[When(@"I open the constituent search dialog")] public void WhenIOpenTheConstituentSearchDialog() { BBCRMHomePage.OpenConstituentsFA(); FunctionalArea.OpenLink("Constituents", "Constituent search"); } [When(@"set the Last/Org/Group name field value to ""(.*)""")] public void WhenSetTheLastOrgGroupNameFieldValueTo(string fieldValue) { SearchDialog.SetTextField(Dialog.getXInput("ConstituentSearchbyNameorLookupID", "KEYNAME"), fieldValue); } [Then(@"the Last/Org/Group name field is ""(.*)""")] public void ThenTheLastOrgGroupNameFieldIs(string expectedValue) { SearchDialog.ElementValueIsSet(Dialog.getXInput("ConstituentSearchbyNameorLookupID", "KEYNAME"), expectedValue); }
This is resolved with the following code edit.
[When(@"I open the constituent search dialog")] public void WhenIOpenTheConstituentSearchDialog() { BBCRMHomePage.OpenConstituentsFA(); FunctionalArea.OpenLink("Searching", "Constituent search"); }
Identify The Customization
![]() |
---|
If you do not know how to identify the field's unique id, please review the XPath Guidelines |
Edit Steps
Update the step code so the XPath constructors use the custom dialog id.
[When(@"set the Last/Org/Group name field value to ""(.*)""")] public void WhenSetTheLastOrgGroupNameFieldValueTo(string fieldValue) { SearchDialog.SetTextField(Dialog.getXInput("UniversityofOxfordConstituentSearch", "KEYNAME"), fieldValue); } [Then(@"the Last/Org/Group name field is ""(.*)""")] public void ThenTheLastOrgGroupNameFieldIs(string expectedValue) { SearchDialog.ElementValueIsSet(Dialog.getXInput("UniversityofOxfordConstituentSearch", "KEYNAME"), expectedValue); }
Identify Need For Overriding Implementation
using System; using Blackbaud.UAT.Base; using Blackbaud.UAT.Core.Base; using TechTalk.SpecFlow; using System.Collections.Generic; namespace Delving_Deeper { [Binding] public class SampleTestsSteps : BaseSteps { [Given(@"I have logged into BBCRM")] public void GivenIHaveLoggedIntoBBCRM() { BBCRMHomePage.Logon(); } [Then(@"a constituent is created")] public void ThenAConstituentIsCreated() { if (!BaseComponent.Exists(Panel.getXPanelHeader("individual"))) FailTest("A constituent panel did not load."); } [When(@"I start to add a constituent")] public void WhenIStartToAddAConstituent(Table constituents) { foreach (var constituent in constituents.Rows) { BBCRMHomePage.OpenConstituentsFA(); constituent["Last name"] += uniqueStamp; ConstituentsFunctionalArea.AddAnIndividual(); IndividualDialog.SetIndividualFields(constituent); } } [When(@"I save the add an individual dialog")] public void WhenISaveTheAddAnIndividualDialog() { IndividualDialog.Save(); } [Given(@"a constituent exists")] public void GivenAConstituentExists(Table constituents) { foreach (var constituent in constituents.Rows) { BBCRMHomePage.OpenConstituentsFA(); constituent["Last name"] += uniqueStamp; ConstituentsFunctionalArea.AddAnIndividual(constituent); } } [When(@"set the household fields")] public void WhenSetTheHouseholdFields(Table fieldsTable) { foreach (var fieldValues in fieldsTable.Rows) { IndividualDialog.SetHouseholdFields(fieldValues); } } } }
Create A Custom Method
If you do not have a CustomIndividualDialog class created yet, add a new class to your project and implement it as follows.
First we make sure to select the 'Household' tab.
using System; using Blackbaud.UAT.Base; using Blackbaud.UAT.Core.Base; using TechTalk.SpecFlow; using System.Collections.Generic; namespace Delving_Deeper { public class CustomIndividualDialog : IndividualDialog { public new static void SetHouseholdFields(TableRow fields) { OpenTab("Household"); } } }
Next we specify custom logic if a value for the 'Related individual' field has been provided. If a value has been provided for this field, we click the button that brings up the add dialog. Be sure to read the API documentation for the XPath constructors.
public new static void SetHouseholdFields(TableRow fields) { OpenTab("Household"); if (fields.ContainsKey("Related individual")) { WaitClick(getXInputNewFormTrigger(getXInput(GetDialogId(DialogIds), "_SPOUSEID_value"))); } }
We then set the 'Last name' field value to the value provided for 'Related individual' before hitting Ok. We could have defined any logic and interactions involving this dialog, but let's keep it simple.
public new static void SetHouseholdFields(TableRow fields) { OpenTab("Household"); if (fields.ContainsKey("Related individual")) { WaitClick(getXInputNewFormTrigger(getXInput(GetDialogId(DialogIds), "_SPOUSEID_value"))); SetTextField(getXInput("IndividualSpouseBusinessSpouseForm", "_SPOUSE_LASTNAME_value"), fields["Related individual"]); OK(); } }
Before we call the base implementation to handle setting the rest of the fields, we set fields["Related individual"] to equal null. We do this because we want the base SetHouseholdFields to skip it's handling of the 'Related individual' field.
using Blackbaud.UAT.Base; using Blackbaud.UAT.Core.Base; using TechTalk.SpecFlow; using System.Collections.Generic; namespace Delving_Deeper { public class CustomIndividualDialog : IndividualDialog { public new static void SetHouseholdFields(TableRow fields) { OpenTab("Household"); if (fields.ContainsKey("Related individual")) { WaitClick(getXInputNewFormTrigger(getXInput(dialogId, "_SPOUSEID_value"))); SetTextField(getXInput("IndividualSpouseBusinessSpouseForm", "_SPOUSE_LASTNAME_value"), fields["Related individual"]); OK(); fields["Related individual"] = null; } IndividualDialog.SetHouseholdFields(fields); } } }
Another solution would have been to remove the 'Related individual' key from the fields object.
fields.Keys.Remove("Related individual");
Update The Steps
Change the step setting the household tab fields.
[When(@"set the household fields")] public void WhenSetTheHouseholdFields(Table fieldsTable) { foreach (var fieldValues in fieldsTable.Rows) { CustomIndividualDialog.SetHouseholdFields(fieldValues); } }
The test now sets the 'Related individual' field through the add button and not the search dialog.