Click or drag to resize
Setting Custom Fields

In this walkthrough you'll gain experience setting a custom field on a dialog and overriding a default implementations.

Caution note Caution

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.

Prerequisites
  • 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.

Adding Support For Custom Fields

Adding Support For Custom Fields - Overload Approach

  1. Identify Need for Custom Support

    This is the Enterprise CRM standard "Add an individual" dialog.
    Original Add Individual
    Here is a customized "Add an individual" dialog containing new fields.
    Custom Add Individual
    Note Note

    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.

    In our test project, if we create a scenario outlined and implemented like this...
    Feature Gherkin
    Core API used to implement steps.
    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.");
            }
    
        }
    }
    Note Note

    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.

    ...and run the test against the application with the new custom field, then we get an error indicating a need to add custom support for the new field.
    Not Implemeted Field Add Individual
  2. 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.

    Custom Individual Dialog Class Inheriting IndividualDialog
    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
        {
    
        }
    }
  3. Create Custom Supported Fields Mapping

    We need to map the custom field captions to their relevant XPath and Field Setter values.

    Custom Individual Dialog Class with mapped custom fields.
    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)}
            };
        }
    }
    Important note Important

    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.

  4. Pass Custom Supported Fields To Base.

    We need to map the custom field captions to their relevant XPath and Field Setter values.

    Custom SetIndividualFields()
    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.

    Note Note

    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.

  5. Modify Your Step Implementation.

    Modify your step definition to use the new CustomIndividualDIalog's SetIndividualFields() method.

    Modified Step Implementation
    [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();
        }
    }
    Caution note Caution

    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.

    The test passes now!
    Passing Test

Adding Support For Custom Fields - Custom Method Approach

  1. Alternative Gherkin Syntax

    An alternative Gherkin approach that drives a need for an entirely custom method.
    2nd Feature Gherkin
  2. 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.

    Custom method.
    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);
    }
    Note Note

    Notice how the custom method did not need the "new" attribute in the method declaration. "new" is only needed when overriding an inherited method.

  3. Implement The New Step Methods

    Implemetation of new steps.
    [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();
    }
    The test passes now!
    2nd Passing Test
    Note Note

    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.

Overriding And Overloading Implementations

Overloading An Implementation

  1. Identify Need To Overload Implementation

    Let's start with the following test case.
    Override Test Case
    Implemetation of steps.
    [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);
    }
    When run against the standard CRM application, the test passes.
    Passing Default Constituent Search
    The steps navigate to the Constituents functional area and click the "Constituent search" task.
    Default Constituent Search Task
    The "Last/Org/Group name" field is set and validated as containing the desired value.
    Default Constituent Search Dialog
    If we run the test against a custom application whose Constituent functional area looks like this...
    Custom Constituent Search Task
    ...we get the following error.
    Default On Custom Search Dialog Error

    This is resolved with the following code edit.

    Edited step
    [When(@"I open the constituent search dialog")]
    public void WhenIOpenTheConstituentSearchDialog()
    {
        BBCRMHomePage.OpenConstituentsFA();
        FunctionalArea.OpenLink("Searching", "Constituent search");
    }
    Running the test now we get a new error.
    Default On Custom Search Dialog Field Error
    Another must customization exist. The error stack trace indicates that the XPath constructor for the "Last/Org/Group name" field is not compatible with this application. NoSuchElementExceptions are thrown when Selenium's WebDriver times out looking for a web element using the XPath.
    Custom Constituent Search Dialog
  2. Identify The Customization

    Let's take a look at the search dialogs between the default and custom applications. Comparing the dialogs, clearly the dialog on the right has been customized. Inspecting the "Last/Org/Group name" field between the two applications, we can see they share the same unique field id.
    Comparing Field Id Search Dialog
    Note Note

    If you do not know how to identify the field's unique id, please review the XPath Guidelines

    Inspecting the unique dialog ids, we can see that they are different. The supported XPath constructs an XPath using the dialog id "ConstituentSearchbyNameorLookupID". We need to modify the dialog id to use the custom dialog id.
    Comparing Dialog Ids Search Dialog
  3. Edit Steps

    Update the step code so the XPath constructors use the custom dialog id.

    Edited steps for 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);
    }
    The test passes now on the custom application.
    Passing Default Constituent Search

Overriding An Implementation

  1. Identify Need For Overriding Implementation

    Let's start with the following test case that works against the standard CRM application.
    Override Working Feature Test
    Implemetation of steps.
    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);
                }
            }
        }
    }
    At some point in the test, the 'Related individual' field on the Add an individual dialog is set by using the associated searchlist.
    Record Search
    What if we wanted to set the field through the add button? This would require us to override the default implementation for how the 'Related individual' field is set.
    Add Icon
  2. 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.

    Selecting the right 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.

    Click the add button for the field
    public new static void SetHouseholdFields(TableRow fields)
    {
        OpenTab("Household");
        if (fields.ContainsKey("Related individual"))
        {
            WaitClick(getXInputNewFormTrigger(getXInput(GetDialogId(DialogIds), "_SPOUSEID_value")));
        }
    }
    The resuling dialog from clicking the add button on the 'Related individual' field.
    Trigger Dialog

    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.

    Set the 'Last name' field
    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.

    Set 'Related individual' to null and çall the base method.
    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.

    Remove the 'Related individual' key
    fields.Keys.Remove("Related individual");
  3. Update The Steps

    Change the step setting the household tab fields.

    Updated step
    [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.

See Also