Set Custom Fields

In this walkthrough, you'll gain experience setting a custom field on a dialog and overriding default implementations. The UAT Automation Kit provides multiple ways to achieve a result, and this walkthrough describes two potential implementations.

  • Complete the Get Started walkthrough. For each new project, you must:
    • Add the UAT Automation Kit NuGet packages.
    • Ensure that ChromeDriver is on your path.
    • Add target environment URLs to the App.config file.
  • Complete the Selenium WebDriver walkthrough.
  • Access to a Blackbaud CRM instance to test against.
  • Complete of the SpecFlow's Table and TableRow Guidelines walkthrough.
  • Familiarity with adding tests and step implementations to existing feature and step files.
  • Familiarity with accessing the UAT Automation Kit Core API.
  • Familiarity with modifying the App.config to change which application the tests run against.
  • Familiarity with identifying the unique attribute values for the XPath constructors in the Core API and completion of the XPath Guidelines walkthrough.

This tutorial guides you through the steps to create a test to add an individual when the Add an individual screen has been customized to include custom fields. It describes two potential implementations to handle a dialog with a custom field, but these are not the only approaches. In this walkthrough, you will:

  • Use the overload approach to add support for custom fields.
  • Use the custom method approach to add support for custom fields.
  1. Identify the need for custom support. For example, this is the standard Blackbaud CRM Add an individual screen.
  2. Add a custom individual.

    And this is a customized version of the Add an individual screen that includes additional fields.

    In our test project, we can create a feature file to create a behavior-driven development test with Gherkin.

    Test to add indiviudal and include custom fields

    Gherkin
    @DelvingDeeper
      Scenario: Add an individual on a dialog containing a custom field.
    Given I have logged into BBCRM
    When I add constituent
    | Last name | First name | Title | Nickname  | Information source | Country of Origin | Matriculation Year (Use) |
    | Parker    | Peter      | Mr.   | Spiderman | Friend             | United States     | 2014                     |
    Then a constituent is created

    Then we can create a step file and populate its steps.

    Core API to implement steps

    C#
    using System;
    using System.Collections.Generic;
    using Blackbaud.UAT.Base;
    using Blackbaud.UAT.Core.Base;
    using Blackbaud.UAT.Core.Crm;
    using TechTalk.SpecFlow;
    
    namespace Delving_Deeper
    
    {
      [ Binding]
        public class SampleTestsSteps : BaseSteps
        {
    
            [Given(@"I have logged into BBCRM")]
            public void GivenIHaveLoggedIntoBBCRM()
            {
                BBCRMHomePage.Login();
            }
    
            [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.");
            }
    
        }
    }

    If we run the test against the customization and its custom field at this point, an error indicates that we need custom support for the new field.

    Custom support for new field.
  3. Create a class that inherits a core class. To resolve the error in the previous step, we create a class in the project to add support for the additional custom fields.
  4. Custom Individual Dialog Class That Inherits IndividualDialog

    C#
    using System.Collections.Generic;
    using Blackbaud.UAT.Base;
    using Blackbaud.UAT.Core.Crm;
    using TechTalk.SpecFlow;
    
    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)}
            };
        }
    }
  5. Create custom-supported field mapping. Map the custom field captions to their relevant XPath and Field Setter values.
  6. C#
    using System.Collections.Generic;
    using Blackbaud.UAT.Base;
    using Blackbaud.UAT.Core.Crm;
    using TechTalk.SpecFlow;
    
    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)}
            };
        }
    }
  7. Pass custom-supported fields to the base. 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.

    Custom SetIndividualFields()

    C#
    {
    SetFields(GetDialogId(DialogIds), fields, SupportedFields, CustomSupportedFields);
    }
  8. Modify the step implementation. Update the step definition to use the CustomIndividualDIalog's SetIndividualFields() method.

    Modified step implementation

    C#
    [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();
        }
    }

    The test should pass now.

    Test pass
  1. You can also use an alternative Gherkin approach that drives a need for an entirely custom method.

    Alternative test to add individual and include custom fields

    Gherkin
    @DelvingDeeper
    Scenario: Add an individual on a dialog containing a custom field using a custom method
      Given I have logged into BBCRM
      When I start to add a constituent
      | Last name | First name | Title  | Nickname  | Information source |
      | Parker    | Peter      | Mr.    | Spiderman | Friend             |
      And I set the custom constituent field "Country of Origin" to "Argentina"
      And I set the custom constituent field "Matriculation Year (Use)" to "2012"
      And I save the add an individual dialog
      Then a constituent is created
  2. Add a method to a 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

    C#
    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);
    }
  3. Implement the new step methods.

    Implemetation of new steps

    C#
    [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 ""(.&#42;)"" to ""(.&#42;)""")]
    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 should pass now.

    Passing Test
  1. Identify the need to overload an implementation, then create a test case and step file.
    Gherkin
    @DelvingDeeper
    Scenario: Set the Last/Org/Group name field in a constituent search dialog
      Given I have logged into BBCRM
      When I open the constituent search dialog
      And set the Last/Org/Group name field value to "Smith"
      Then the Last/Org/Group name field is "Smith"

    Implemetation of steps

    C#
    [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 ""(.&#42;)""")]
    public void WhenSetTheLastOrgGroupNameFieldValueTo(string fieldValue)
    {
        SearchDialog.SetTextField(Dialog.getXInput("ConstituentSearchbyNameorLookupID", "KEYNAME"), fieldValue);
    }
    
    [Then(@"the Last/Org/Group name field is ""(.&#42;)""")]
    public void ThenTheLastOrgGroupNameFieldIs(string expectedValue)
    {
        SearchDialog.ElementValueIsSet(Dialog.getXInput("ConstituentSearchbyNameorLookupID", "KEYNAME"), expectedValue);
    }

    When run against a standard Blackbaud CRM application, the test passes.

    Passing default consituent search

    The steps navigate to the >Constituents functional area and click the Constituent search task.

    default consituent search

    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

    ... then we get the following error:

    Default Custom Search Dialog Error

    We can resolve this with the following code edit.

    Edited step

    C#
    When(@"I open the constituent search dialog")]
    public void WhenIOpenTheConstituentSearchDialog()
    {
        BBCRMHomePage.OpenConstituentsFA();
        FunctionalArea.OpenLink("Searching", "Constituent search");
    }

    If we run the test now, we get a new error.

    Default custom search dialog field error

    Another customization must 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 task
  2. Identify the customization. Let's look at the search dialogs between the default and custom applications. Comparing the dialogs, clearly the dialog on the right is customized. Inspecting the Last/Org/Group name field between the two applications, we can see they share the same unique field ID.
    Compare field search dialog

    Inspecting the XPaths to get to the customized dialog requires us to use a more direct XPath trace.

    Compare dialog IDs search dialog
  3. Edit the steps. Let's use the BaseComponent class that contains SetTextField() and ElementValueIsSet(), which accept XPaths.

    Edited steps for custom dialog ID

    C#
    [When(@"set the Last/Org/Group name field value to ""(.&#42;)""")]
    public void WhenSetTheLastOrgGroupNameFieldValueTo(string fieldValue)
    {
        BaseComponent.SetTextField("//div[contains@class, 'bbui-dialog') and contain(@style, 'visible')]//input[contains(@id, 'KEYNAME'))]", fieldValue);
    }
    
    [Then(@"the Last/Org/Group name field is ""(.&#42;)""")]
    public void ThenTheLastOrgGroupNameFieldIs(string expectedValue)
    {
        BaseComponent.ElementValueIsSet("//div[contains@class, 'bbui-dialog') and contain(@style, 'visible')]//input[contains(@id, 'KEYNAME'))]", expectedValue);
    }

    The test should pass now.

    Passing default constituent search
  1. Identify the need to override an implementation, then create a test case and step file that work against a standard Blackaud CRM instance.
    Gherkin
    @DelvingDeeper
    Scenario: Add an individual and set the related individual through the constituent search list
      Given I have logged into BBCRM
      And a constituent exists
      | Last name | First name |
      | LeBouf    | Shia       |
      When I start to add a constituent
        | Last name | First name |
        | Prime     | Optimus    |
      And set the household fields
      | Related individual | Individual is the | Related individual is the |
      | LeBouf             | Co-worker         | Co-worker                 |
      And I save the add an individual dialog
      Then a constituent is created

    Implemetation of steps

    C#
    using System;
    using System.Collections.Generic;
    using Blackbaud.UAT.Base;
    using Blackbaud.UAT.Core.Base;
    using Blackbaud.UAT.Core.Crm;
    using TechTalk.SpecFlow;
    
    namespace Delving_Deeper
    {
        [Binding]
        public class SampleTestsSteps : BaseSteps
        {
            [Given(@"I have logged into BBCRM")]
            public void GivenIHaveLoggedIntoBBCRM()
            {
                BBCRMHomePage.Login();
            }
    
            [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 want to set the field through the Add button? This requires us to override the default implementation for how the Related individual field is set.

    Add Icon
  2. Create a custom method.
  3. If you have not created the CustomIndividualDialog, add it to your project and implement it as follows. First, we make sure to select the Household tab.

    Selecting the right tab

    C#
    using System.Collections.Generic;
    using Blackbaud.UAT.Base;
    using Blackbaud.UAT.Core.Crm;
    using TechTalk.SpecFlow;
    
    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 is provided. If a value is 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 fields's Add button

    C#
    public new static void SetHouseholdFields(TableRow fields)
    {
        OpenTab("Household");
        if (fields["Related individual"] != null)
        {
            WaitClick(getXInputNewFormTrigger(getXInput(GetDialogId(DialogIds), "_SPOUSEID_value")));
        }
    }

    This is the resulting 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 clicking OK. We could have defined any logic and interactions involving this dialog, but let's keep it simple.

    Set the Last name field

    C#
    {
        OpenTab("Household");
        if (fields["Related individual"] != null)
        {
            WaitClick(getXInputNewFormTrigger(getXInput(GetDialogId(DialogIds), "_SPOUSEID_value")));
            SetTextField(getXInput("IndividualSpouseBusinessSpouseForm", "_SPOUSE_LASTNAME_value"), fields["Related individual"]);
            OK();
        }
    }

    Before we call the base implementation to set the rest of the fields, we set fields["Related individual"] = null. We do this because we want the base SetHouseholdFields to skip its handling of the Related individual field.

    Set Related individual to null and call the base method

    C#
    using System.Collections.Generic;
    using Blackbaud.UAT.Base;
    using Blackbaud.UAT.Core.Crm;
    using TechTalk.SpecFlow;
    
    namespace Delving_Deeper
    {
        public class CustomIndividualDialog : IndividualDialog
        {
            public new static void SetHouseholdFields(TableRow fields)
            {
                OpenTab("Household");
                if (fields["Related individual"] != null)
                {
                    WaitClick(getXInputNewFormTrigger(getXInput(GetDialogId(dialogIds), "_SPOUSEID_value")));
                    SetTextField(getXInput("IndividualSpouseBusinessSpouseForm", "_SPOUSE_LASTNAME_value"), fields["Related individual"]);
                    OK();
                    fields["Related individual"] = null;
                }
                IndividualDialog.SetHouseholdFields(fields);
            }
        }
    }

    An alternative solution would be to remove the Related individual key from the fields object.

    Remove the Related individual key

    C#
    fields.Keys.Remove("Related individual");
  4. Update the steps to change the step setting the household tab fields.

    Updated step

    C#
    [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

SpecFlow's Table and TableRow Guidelines