codycotulla
Tera Guru

In working with the ServiceNow automated test framework (ATF), one of the things that has been a game-changer for me has been creating custom UI step configurations. I wanted to share how to do it with the community. In this article, I'll explain what needs to be done to create a custom UI step configuration and how to create a step configuration for clicking an element in the UI, specifically, how to click the Personalize List icon in a list view.

find_real_file.png

At a high level, you'll need to do the following

  • Create a business rule so that you can copy an existing UI step configuration.
  • Copy an existing UI step configuration.
  • "Code" your custom step configuration.
  • Create a UI Policy so that you can have access to the timeout field when you create steps from your new step configuration.

**Important Reloading the Client Test Runner**

Before we get started, here's something you need to know. It seems that the Client Test Runner may cache the step configuration definitions, which is great for performance, but not so great when you are trying to iteratively code your step configuration. So, if you are making changes to your step configuration, and those changes don't seem to have an effect, close and reload the Client Test Runner window that may solve the problem.

Now, let's go.

Create Business Rule

The UI step configurations are read-only. However, you can do an "insert" to create a new record. The insert will fail though because the name of the step configuration must be unique. The business rule you create will append "(Custom)" to the name and remove the read-only constraint.

Create a business rule with the following properties.

  • Name: Create Custom from Read Only
  • Table: Test Step Config [sys_atf_step_config]
  • Advanced: true
  • When: before
  • Order: 200
  • Insert: true
  • Filter Conditions: Protection policy is Read-Only -or- Proection Policy is Protected
  • Script
    (function executeRule(current, previous /*null when async*/) {
    /**
    	This business rule is used to create a custom version of a read-only or protected ATF step configuration.
    	It sets the protection policy to blank and appends " (Custom)" to the name.
    	With this business rule, we can create custom UI step configurations.
    */
    	var name = current.getValue("name");
    	current.setValue('name', name + " (Custom)"); 
    	current.setValue("sys_policy", "");
    })(current, previous);​

 Copy an existing UI Step Configuration

Now that you have your business rule, you can create a copy of an existing UI step configuration. You can copy any one of them. It probably makes sense to use one that does something similar to what you want to do, but it's not a requirement. For this we'll start with Click Component (Custom UI).

  1. Navigate to the Automated Test Framework (ATF) > Administration > Step Configurations module.
  2. Find and open the Click Component (Custom UI) step configuration.
    You'll see messages that the item is read-only.
  3. Right click the record header, and choose Insert and Stay from the context menu.
    A new record will be created with the name Click Component (Custom UI) (Custom).

You now have a custom UI Step Configuration to code as you want. We'll do that next.

 Code your UI Step Configuration

What needs to be done to code a UI step configuration is not documented. To learn how to do it yourself, you'll need to dig into the code of the existing UI step configurations--or other people can do it and post articles. I'm not going to explain how to code them here. Instead, I'm going to provide code for one that can be used to click a UI Element. It's one I've started using recently, and it's relatively simple. Some others like populating fields using JSON are more tricky, or they were more tricky for me.

Open the Click Component (Custom UI) (Custom) step configuration that you created and populate it with the following values:

  • Name: Click UI Element (Custom)
  • Step environment: UI
  • Category: Custom UI
  • Template reminder: Click a UI element that you find with a JQuery selector 
  • HTML description: Click a UI element that you find with a jQuery selector
  • Description generation script: -- see below --
  • Step execution script: -- see below --

 Description generation script

 This is the script that will generate the description that appears in steps created from the step configuration.

function generateDescription() {
    // the global variable 'step' represents the current glide record
    try {
        // the global variable 'step' represents the current glide record

        var tokens = [];

        var description = "";

        // your code here
        description = "Click on the UI element that matches the query selector '{0}'.";
        if (!gs.nil(step.inputs.u_query_selector.getDisplayValue())) {
            tokens.push(step.inputs.u_query_selector.getDisplayValue().trim());
        }
		description += "\nWhatever action would happen when the user clicked UI element will happen.";
       
        description = gs.getMessage(description, tokens);
        return description;
    } catch (e) {
        return description + "\n" + e;
    }

}
generateDescription();

Step execution script

This is the script that is executed by the client test runner.

(function(step, stepResult, assertionObject) {

    assertionObject.executeStep = function(step, stepResult) {
        var MESSAGE_KEY_NO_QUERY_SELECTOR_PROVIDED = "FAILURE: No query selector provided";
        var MESSAGE_KEY_WAITING_FOR_TIMEOUT = "Waiting {0} seconds before completing the step";
        var MESSAGE_KEY_UI_ELEMENT_NOT_FOUND = "Failure: No UI element matched the selector '{0}'.";
        var MESSAGE_KEY_NO_CLICK_FUNCTION = "Failure: The UI element that matched the selector '{0}' did not have a 'click' function.";
        var MESSAGE_KEY_CLICK_SUCCESS = "The UI element that matched the selector '{0}' was successfully clicked.";

        var messages = new GwtMessage().getMessages([MESSAGE_KEY_NO_QUERY_SELECTOR_PROVIDED, MESSAGE_KEY_WAITING_FOR_TIMEOUT, MESSAGE_KEY_UI_ELEMENT_NOT_FOUND, MESSAGE_KEY_NO_CLICK_FUNCTION, MESSAGE_KEY_CLICK_SUCCESS]);

        // Function for updating the message that will appear in the step result.
        function updateStepResultMessage(message) {
            stepResult.message += (stepResult.message ? ("\n") : "") + message;
        }

        // Check the input and fail the query selector is missing
        if (!step.inputs.u_query_selector) {
            failStep(messages[MESSAGE_KEY_NO_QUERY_SELECTOR_PROVIDED]);
            return;
        }

        // Get the query selector
        var querySelector = step.inputs.u_query_selector + "";
        querySelector = querySelector.trim();

        try {
            // Gets the window that's loaded into the Test Runner
            var testFrameWindow = g_ui_testing_util.getTestIFrameWindow();

            // Get the uiElement
			// Verify that only one element matches and that the element has a click function
			// Run the element's click function
            var uiElement = testFrameWindow.jQuery(querySelector);
            if (uiElement.length > 0) {
                if (uiElement.click != undefined) {
                    // Click the uiElement
                    uiElement[0].click();
                    passStep(querySelector);

                } else {
                    updateStepResultMessage(formatMessage(messages[MESSAGE_KEY_NO_CLICK_FUNCTION], querySelector));
                    failStep();
                }
            } else {
                updateStepResultMessage(formatMessage(messages[MESSAGE_KEY_UI_ELEMENT_NOT_FOUND], querySelector));
                failStep();
            }


        } catch (e) {
            var msg = (e && e.message) ? e.message : e;
            updateStepResultMessage("Failure: " + msg);
            failStep();
        }

        function passStep() {
            if (step.timeout > 0)
                g_ui_testing_util.setTestStepStatusMessage(formatMessage(messages[MESSAGE_KEY_WAITING_FOR_TIMEOUT], step.timeout));

            setTimeout(_passStep, step.timeout * 1000);

            function _passStep(selector) {
                stepResult.success = true;
                updateStepResultMessage(formatMessage(messages[MESSAGE_KEY_CLICK_SUCCESS], selector));
                step.defer.resolve();
            }
        }


        function failStep(msg) {
            stepResult.success = false;
            step.defer.reject();
        }
    };
    assertionObject.canMutatePage = step.can_mutate_page;
})(step, stepResult, assertionObject);

Input Variables

If you look at the scripts, you'll see that there is an input variable step.inputs.u_query_selector.

Add an input variable with the following properties

  • Type: String
  • Label: Query selector
  • Column name: u_query_selector
  • Max length: 100

Now you have a coded UI step configuration that you can use. We're going to do one more thing, and then we'll use it in a sample test.

Create UI Policy for Displaying the Timeout Field

Sometimes in your UI step configurations, you're going to want to specify a timeout. You can always do it by displaying the timeout field on the list view, but we'll create a UI policy to make it easier.

Create a UI Policy record with the following properties

  • Table: Test Step [sys_atf_step]
  • Short description: Show timeout (Custom)
  • Conditions: Step config.Step environment is UI -and- Step config.Protection policy is --None--

    You will need to dot-walk to these fields. In the field list, click Show Related fields, which will be at the bottom of the list. Then in the field list, choose Step Config --> Test step config fields. The list will refresh and you can choose Step Environment field. Go through the same process to select the Step Config.Protection field.

  • Global: true
  • On load: true
  • Reverse if false: false
  • Inherit: false
  • Order: 500
    You'll need to set the order on the list view. We are using 500 so our UI policy doesn't conflict with the out of the box UI policy for showing/hiding the timeout field.

 Add a UI Policy Action with the following properties

  • Field name: Timeout
  • Mandatory: Leave alone
  • Visible: True
  • Read only: Leave alone

Now we'll be able to see the timeout field when we create test steps using our UI step configurations we create. We won't always need to use the field, but it will be available.

Create a test that uses your custom test step 

We are going to create a test that goes to the incident list and clicks the personalize list icon to display the personalize list dialog and then closes the dialog.

Create a test with the following properties:

  • Name: My personalize list.
  • Description: Demonstrates the custom UI step configuration

Add the following test steps:

  1. Create User
    Name it what you want, give it the itil role, and impersonate the user.
     
  2. Navigate to Module
    Module: My Incidents
     
  3. Click UI Element (Custom)
    Query selector: #hdr_incident> th:nth-child(1) > i
    (You can find the query selector for a UI element using the browser debugger. Inspect the element, you want, and then copy the elements JS Path)
    find_real_file.png
    That will give you something like this:
    document.querySelector("#x4e929bc7db93c010660658b8dc96193b > li:nth-child(2) > a > span")
    Take the part in quotes.
     
  4. Click UI Element (Custom)
    Query selector: #cancel_button

Run the test. You'll see the list appear. The Personalize List Columns dialog will appear and disappear, and the test will end. 

That's pretty much it. I hope that some people find this helpful and that they find creative ways to use custom UI step configurations that they share what they learn.

Thanks,

Cody

Comments
codycotulla
Tera Guru

Updated the script. Specifically changed line 36 from

uiElement.click();

to 

uiElement[0].click();

The jQuery selector always returns an array. By adding [0], we make sure that we are selecting an element that we can click.

Yaraslau
Tera Guru

Could you explain how to create input variables? When I tried it I saw the next:

find_real_file.png I try to create my custom step similar to Set Field Values with inputs table and field values.

codycotulla
Tera Guru

Hi Yaraslau,

I've seen what your experiencing. If it's what I think it is, the problem is that you came to your step configuration through a step, and that when you created the variable it initially opened looking like the image below, and you clicked the Advanced view link. 

find_real_file.png

If this is what you did, then the variable got created with a blank column name, and so when you give it a name (u_table) and save the record it fails because you can't change the table name. (Long explanation, but in case someone else has the same problem).

What to do

You need to have the step configuration itself in the advanced view.

You can do either of the following:

- Go to the Step configurations module and open the step configuration from the list view.
    OR

- Add the word advanced to the end of you URL when you load the step configuration and press Enter on your keyboard to reload the page. Assuming that the URL initially ends with "sysparm_view%3D", you'll add advanced and make it "sysparm_view%3Dadvanced".

Once you do that when you create a new variable it will be in the advanced view.
find_real_file.png

It's a long explanation, but I hope it helps.

Thanks,

Cody 

Clement Dos San
Tera Contributor

Thank you ! The part to copy a read-only Step Configuration was very helpful !

reynaldemilien
Kilo Contributor

Hi Cody,

   Thank you , this is a great article and has helped us in expanding our ATFs tests!

   Currently we are trying to use this concept to use MRVS on the native side. We are able to click on the 'Add' button to open the MRVS window but are experiencing difficulties interacting with the modal MRVS window taht gets opened. Is this custom step the coorexct  step configuration to use for dealing with the MRVS pop-up? Is it that the modal is not part of g_form?

codycotulla
Tera Guru

Hi,

I believe the issue is that the the dialog is in an iFrame, so you'll need to do the following:

Add a new input variable name iFrame.

Update the execution script so that it will get the values from the iFrame if there is one.

// Gets the window that's loaded into the Test Runner
            var testFrameWindow = g_ui_testing_util.getTestIFrameWindow();

// new for using an iFrame
        try {
        var iFrame = step.inputs.u_iframe + "";
        iFrame = parseInt(iFrame);
        if (isNaN(iFrame) == false) {
            testFrameWindow = testFrameWindow[iFrame]
        }

        } catch (e) {
            // no action
        }

I believe you'll need to set the value of the iFrame parameter to 1 to handle the modal dialog. I've used this method to fill out risk assessments on change requests.

If this works, please mark this comment as helpful. 

Thanks,

Cody

Rob87
Tera Contributor

Thanks, this was really helpful. One slight deviation I had was that i had to 'copy selector' rather than 'Copy JS path' as it wasn't working with JS path, but after that it worked an absolute treat. Thanks a lot. 

 

Also, in case you're interested, I was using it to click the 'preview' button to preview a user's details when logging an incident, to prove that a Service Desk agent could easily see key user info to validate them over the phone.

codycotulla
Tera Guru

Rob, Thanks for the information on using 'copy selector' rather than 'Copy JS path'. And thanks for sharing how you are using it. Glad it's helpful! 

Cody

codycotulla
Tera Guru

Hi

For the Next Experience UI, you can use the following query selector for the Personalize List button:

  • i[data-original-title='Personalize List']

This selector will also work for UI16.

Thanks,

Cody

bert_n
Tera Contributor

Huge thanks for this! I've been pounding my head trying to click on a custom widget in the SP for the better part of an afternoon before finding this.

DLHill
Giga Guru

Hi

 

This is such a helpful article however I am struggling with elements that have id's that are numberic. JQuery doesn't allow selectors to start with a number so they escape it out:

 

document.querySelector("#\\38 c373ee61b39d8102a8763d07e4bcb75_add_row") or

#\38 c373ee61b39d8102a8763d07e4bcb75_add_row

 

I can't find a way to pass this in to the code to get it to find that element.

 

(For context I am trying to get around the fact that the AddRowToMRVS step doesn't seem to work - see https://support.servicenow.com/kb?id=kb_article_view&sysparm_article=KB0995008 - so I thought just pressing the button Add might work.

 

<button type="button" title="You can add only 5 rows" class="btn btn-primary m-r" ng-click="c.createRow()" ng-disabled="!c.canInsert()" id="8c373ee61b39d8102a8763d07e4bcb75_add_row" aria-label="Do you want to add a row for Contacts">Add</button>)

 

Anyone got any ideas? Or maybe I'm doing it right and my page hasn't loaded correctly?

 

thanks

 

Debbie

Shan C
Mega Guru

This is a life-saver post !

 

Also, appreciate anyone if you could shed some light on how to debug the script in code level. I have created a custom Step Config and it's working fine but at the end showing as failed with reason "Step execution failed with error: Cannot read properties of undefined (reading 'defer')"

So, trying to use the Script Debugger but couldn't figure out why it's not stopping at the breakpoint. Even I have a breakpoint in Step itself, Clicking Debug Test, check the code level in Debugger, not pausing inside code in debugger.

DLHill
Giga Guru

Hi Shan I'm not sure how familiar you are with ATF so excuse that this may be too obvious for you

 

I don't think you can debug the code the way you want but reasons are your friend here. Try forcing failures at each step through the code with helpful reason strings to mimic debug output? Also be aware that "undefined" means you are referencing something that isn't there. For instance you are clicking on something in an order guide that you haven't actually added to the order guide in the test steps (aka How the actual hell does Review Item in Order Guide mean add item to order***?! - it took me *days*). Maybe focus on checking that all the things you are trying to reference exist and using other test steps to check that out?

 

Debbie

 

*** incidentally this was the reason why my MRVS step wasn't working, in case anyone is interested. 

Shan C
Mega Guru

Thanks, I found the issue and working now.

Just wondering if we have any reference of supported methods for ATF module or similar.

For example, just tried to use some methods from here - https://developer.servicenow.com/dev.do#!/reference/api/utah/client/c_GlideFormAPI but nothing seems exposed.

 

g_ui_testing_util.getTestIFrameWindow().document.GlideForm.getControl('table_name.field_name');

 

Seems GlideForm class is available in this context but no methods from the class; so getting the error Cannot read properties of undefined (reading 'getControl')

Appreciate if any one has any knowledge about g_ui_testing_util or why the methods are not exposed for us in Step Config Script.

 

Ananta1
Tera Expert

Appreciate if any one has any knowledge about g_ui_testing_util or why the methods are not exposed for us in Step Config Script.  That is the question for which I am also trying to find an answer for. 

 

Hi @DLHill , were you able to use jQuery for the selector or any other means to handle?

DLHill
Giga Guru

Hi @Ananta1 I did manage to use the script above with JQuery successfully but not for what I wanted because servicenow construct their pages using the sys ids as element ids. Not a daft idea but JQuery doesn't allow element ids to start with a number so they are escaped out to unicode. This massively limits what bits of the page you and pick up.

 

Last I looked the g_ui_testing_util is very hidden and I am hoping that is because they are working on a flow designer type upgrade to the testing process. It's got a lot going for it but has that "rough edges" feel still. Not holding my breath....

 

(Btw I did look at Regress but our app is horribly bespoke so didn't fit. You are obvs way past basics however it might give some pointers?)

 

Debbie

Ananta1
Tera Expert

Hi @DLHill , like you mentioned, with the new UI pages the elements are in shadow DOM and on top of that the numbering/ids of the elements. I did install regress and tried for some of our use cases, it does help up to some extent for test design but we are not using it. Is there anything else available to find an element apart from jQuery which can be used for shadow dom js paths?

DLHill
Giga Guru

Sorry @Ananta1, you are going beyond my knowledge! I was desperately trying to find a solution to a problem and went down this rabbit hole briefly. I also dropped it when I worked out what my actual problem was with "undefined" within the test framework (a missing step further up which was not properly documented as needed). Good luck with it!

Ananta1
Tera Expert

@DLHill np, thanks for sharing. On my case I am trying to create some custom steps to handle UI builder pages. I am able to open the page and able to assert some text on the page. When I use OOB Set Component Values (Custom UI), sometime it complains that it's not testable during retrieve but sometime it works. Elements are showing as they are testable on the manual page inspector  though. When I am trying to set values on my own custom step jQuery doesn't work testFrameWindow.jQuery is not a function. I tried this -

var testFrameWindow = g_ui_testing_util.getTestIFrameWindow();
var uiElement = testFrameWindow.jQuery(querySelector); //query selector is passed from input as document.selector as nested shadownroot.
uiElement.val(value).change().blur();

 

When I just pass as testFrameWindow.querySelector that element isn't read and so I get Failure: Cannot read properties of undefined (reading &#39;val&#39;) in the subsequent step.

 

I also tried using Window.top_g_automate and setValue as its there in OOB step but that didn't help either.

 

Wondering if anyone has any solution for this.

 

Shan C
Mega Guru

@Ananta1 I have done like below for the Shadow root, and working fine for me. You can consider if interested.

 

 

//selectors - I will pass as a step config parameter like
//rootTag,insideTag1InShadowRoot1,insideTag2InShadowRoot2,input

//And, split and make it array
var selectors = step.inputs.u_query_selector.trim().split(",");
var tFrameDoc = g_ui_testing_util.getTestIFrameWindow().document;
var element = null;
			for(var i = 0; i < selectors.length; i++) {
				if(element== null)
					element= tFrameDoc.querySelector(selectors[i].trim());
				else
					element= element.shadowRoot.querySelector(selectors[i].trim());
				
				if(element== null) {
					failStep("Couldn't find this part: " + selectors[i].trim());
					return;
				}
			}

//Then you can operate on it...
element.value = 'abcd';

 

 

And, ServiceNow API handles the blur or change events differently, so even explicitly calling blur() or click() didn't work for me. But select did the job after setting the value.

 

element.setAttribute('value', 'yourValue');
element.select();

 

For, other type of elements, of course, you can just try to get the exact element and click like everyone would do in other APIs like selenium.

Ananta1
Tera Expert

Thanks @Shan C . How would you pass the input for an element like this for example ?

 

'document.querySelector("body > macroponent-b66bf1075b31101016fd7c1bf481c7a8").shadowRoot.querySelector("div > sn-canvas-root > sn-canvas-layout > sn-layout > sn-canvas-main").shadowRoot.querySelector("main > sn-canvas-screen").shadowRoot.querySelector("section > screen-action-transformer-cc74f43bdb4d9110265f30ceaa961932 > macroponent-8874f43bdb4d9110265f30ceaa96192g").shadowRoot.querySelector("#item-catalog_item_1").shadowRoot.querySelector("div > sn-catalog-form-connected > now-uxf-page > sn-layout > now-collapse:nth-child(1) > sn-layout > sn-catalog-form").shadowRoot.querySelector("div > sn-catalog-form-container-variable:nth-child(2)").shadowRoot.querySelector("div > sn-catalog-form-variable-section > div.sc-section-form-column > div:nth-child(3) > sn-catalog-form-variable").shadowRoot.querySelector("#fbd83e66db81d5d0265f30ceaa9619ca").shadowRoot.querySelector("now-select").shadowRoot.getInnerHTML().match("now-select-trigger-label")'.

Shan C
Mega Guru

@Ananta1 This part is completely your CSS selector strategy like everyone would do.

 

I can help if you provide the HTML tree view as I couldn't understand better with the way you have given. Always, better to avoid index based selectors like child(2) or div(1) or so. There should be a unique hierarchical way to get to this element.

Not sure if you can send private message in this community or if you're interested, you can contact me.

Ananta1
Tera Expert

@Shan C I tweaked the code you sent as below for it to run (as there were to if element ==null in your for loop). But I see error "Failure: Cannot read properties of null (reading 'shadowRoot') undefined"

 

var selectors = step.inputs.u_query_selector.trim().split(",");
var tFrameDoc = g_ui_testing_util.getTestIFrameWindow().document;
var element = null;
var i=0;
element= tFrameDoc.querySelector(selectors[i].trim());
for( i = 1; i < selectors.length; i++) {
element= element.shadowRoot.querySelector(selectors[i].trim());
passStep("Could find this part: " + selectors[i].trim());
}

element.value = value;

Shan C
Mega Guru

@Ananta1 Yes, my code will check like:

if if it's null very first time when it goes inside for loop, it would fetch first part of the element from the html tree, then further parts using shadow roots.

The code you tweaked is going straight into shadow root without a root html element as parent.

 

If the locator is correct, the code would work fine as I have shared initially. Please feel free to send html snapshot in private messages if you need help in getting the locators i.e. selector

 

Note: You can send private messages if you go to my profile page.

sam2076
Tera Expert

Hey, can someone help in writing a step execution script where I can get the related details of a record. Basically, I want to access the approver sys_id of a ritm.

 

Vadzim Surin1
Tera Expert

Thank you,  cody10,
Thank you very, very much for your guide.
Now it is possible to click a custom button even in our custom widget.
Your recommendations were very helpful.

EdwinCoronado
ServiceNow Employee
ServiceNow Employee

UI-based and Java-based Step configurations are not supported by ServiceNow as per the official docs:


You can define only step configurations that run on the server and not step configurations that run in the browser.

Bypassing security rules to enable such functionality may lead to undesired results, instability in tests, and such Step Configurations will not be supported by our Support Engineering team.

Version history
Last update:
‎09-15-2020 05:50 PM
Updated by: