Contents
This article was originally posted on the ServiceNow Community.
The primary Use Case is for Regression Testing to speed up the time to upgrade ServiceNow.
The Automated Test Framework is great for creating automated regression tests, but someone still has to write them.
As I was writing more and more regression tests using ATF, I started to realize that, most of the time, I was just creating the same pattern of tests over and over:
- Impersonate a User
- Open a new Record
- Check which UI Actions are Visible
- Populate the Form
- Check field values that may have changed as a result of populating the form
- Check Field State (Mandatory, Read-Only & Hidden)
- Click UI Action
Being a ServiceNow developer, I thought to myself “surely there is a way to automate this!”.
Creating an ATF Recorder – Proof of Concept
These were equivalent to the following Test Steps:
- Impersonate
- Open a New Form
- UI Action Visibility
- Set Field Values
- Field Values Validation
- Field State Validation
- Click UI Action
Before wasting many hours in my home office study, I did what all good engineers do first – test the concept.
I needed to work out if I could get all the information I needed from ServiceNow to generate Test Steps for these repetitious actions.
Let’s go through each one:
Impersonate
Impersonates the specified user in the current session for the duration of the test or until another user is impersonated.
Inputs
- User (sys_id)
It is fairly trivial to get the sys_id of the currently logged in user:
var currentlyLoggedInUser = g_user.userID;
Easy.
Open a New Form
Opens a new form for the specified table.
Inputs
- Table (String)
- View (String)
Using Client Scripts, I know I can determine the table and view of the current form
Table
var tableName = g_form.getTableName();
View
var view = g_form.getViewName();
Check UI Action Visibility
Validates whether a UI Action is visible on the current form.
You can assert that any number of UI Actions are either visible or invisible.
Inputs
- Table
- Visible (GlideList, UI Action Reference)
- Not Visible (GlideList, UI Action Reference)
We’ve already worked out how to get the Table of the current form, but there isn’t any API to get visible UI Actions, so I had to write my own.
Visible
Luckily, ServiceNow has already done most the work for me in its Test Config code:
function checkVisibility(actionId) { var queryString = "[gsft_id='" + actionId + "']"; var actionIdExist = testFrameWindow.document.querySelector(queryString); if (actionIdExist == null) return false; if (!isVisible(actionIdExist)) return false; return true; } function isVisible(dom) { return !dom.hidden && dom.style.visibility != 'hidden'; }
I just needed to get a list of all possible UI Action ID’s, which was pretty easy to do based on the queryString that ServiceNow had already provided.
function getVisibleUIActions() { var visibleUIActions = []; var queryString = "[gsft_id]"; var uiActionElements = document.querySelectorAll(queryString); for (var i in uiActionElements ) { var uiActionElement = uiActionElements[i]; if (isElementVisible(uiActionElement)) { var sysId = uiActionElement.getAttribute("gsft_id"); visibleUIActions.push(sysId); } } return visibleUIActions; }
Not Visible
To determine the UI Actions that are not visible, I just need to have a list of every UI Action that could possibly be displayed, and remove all UI Actions that are visible.
When manually populated this Test Step, ServiceNow already has reference qualifiers in place that hides any UI Actions that wouldn’t be possible to display.
function getFormUIAction(tableName) { if (GlideStringUtil.nil(tableName)) { return "table=global^form_action=true^active=true"; } var currentAndParentTables = GlideDBObjectManager.get().getTables(tableName); var str = currentAndParentTables.toString(); var tables = str.substring(1, str.length() - 1); tables += ", global"; var stringQuery = "tableIN" + tables + "^form_action=true^active=true"; return stringQuery; }
This was almost perfect but was missing one thing.
To do full regression, you need to consider UI Actions of and Active value of False to be potentially visible, as upgrades may deprecate UI Actions that you want your test to know about, as this is a change in state.
To solve this, I just added an OR
var allUIActionsQuery = getFormUIAction(tableName) + "^ORactive=false";
Set Field Values
Sets field values on the current form.
Inputs
- Table
- Field Values (String, Encoded Query)
Field Values
There isn’t any documented Client Script API to get all populated fields on a form.
With a little digging in Xplore, I found that GlideForm had a property called ‘modifiedFields’ which tracked all fields that had changed value by means of manual changes:
var modifiedFields = g_form.modifiedFields;
Changing the Short Description field on an Incident would result in the contents of modifiedFields as follows:
{incident.short_description: true}
The values weren’t there, but I could get that just as easily:
g_form.getValue('incident.short_description')
Field Values Validation
Validates field values on the current form.
Inputs
- Table
- Conditions (String, Encoded Query)
If Set Field Values throws a fail if the field is not populated as defined in the Test Step, then why do I need to validate field values?
Take changing the value of Impact or Urgency as an example.
Changing these values may change the value of Priority – but the Priority field will not show up in g_form.modifiedFields – nor would you want it to.
The Priority field is read-only, and if your Test tried to set that field value, it would fail.
We need a way to find all field values that have changed as a result of changing other fields.
Conditions
Back in my System Administrator days, I created hundreds of Email Notifications via Notification records.
A great feature of Email Notifications was that you could adjust the record and click ‘Preview Notification’ to see what effect your changes would have, without committing to saving those changes.
With this in mind, I knew there had to be an existing way to get all fields that had changed value. Reverse engineering is your friend 🙂
The UI Action code revealed the following:
function showSimulator() { var generationType = g_form.getValue('generation_type'); if (!g_form.getValue('collection') && generationType == 'engine') g_form.addErrorMessage(new GwtMessage().getMessage("Table is required to preview this notification")); else { if (typeof rowSysId == 'undefined') sysId = gel('sys_uniqueValue').value; else sysId = rowSysId; var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow; var dd = new dialogClass("notification_preview"); dd.setTitle("Notification Preview"); dd.setWidth(800); dd.setPreference('sysparm_notification_id', sysId); dd.setPreference('sysparm_changed_fields',g_form.serializeChangedAll()); dd.render(); } }
The golden line:
dd.setPreference('sysparm_changed_fields',g_form.serializeChangedAll());
The output from serializeChangedAll() looks something like this:
&sys_target=incident&sys_uniqueValue=c8ef1aaedb76c8109e101461399619ea&sys_row=-1&sysparm_encoded_record=SBSvOy%2BGbxRnTUw297uYJlfHsT4CchYhL8qsQvVS4oYRqJf5GoVaR2BXDzla2tU8q3ug3aoDHV9t1DUxeof9%2BC1O6nUWGM99c5XsbmeFryD%2FqmZLEMKuybxG2rt59gLaa%2FL0qnX8xqsFbcVp2V8RIjM5P5v23ZrzyULjWCB18QEkKCv4xTRl7wxpg0NnJkLmjeLYPSlECZnCV3mhSUGxXpyyKC635YQFYbDz5N5yYSXjIc8yObiI2GdU3yzJwWEkDX3bpLCTIJJrIB%2FOmfg5YI0f%2F1iRgdGywLd%2F%2F1LVEV6dUdT33sgd0z82ASeObP%2FOeXZ6O1%2Fe5iXWrd9I7zw20SAk2JKEzBYQBgPBBDA42X%2BUZYlzv4dv4nlYITwcKdYXi1PYzruNthKzwKUWq4kdR6KJp7kErQ7xVzy5W5tTfqp500GkCke57BCUxrZZCFqJSwSTh2tB7lf7HJSArNOqeD19niysdPM4W%2BREKFGCwQdmrQtGvvFLq9oMWIT3Pjrz%2FjA8I9WF6s%2F%2B8OpMI7SlZRbYPbACZEhL6qVn5T8XdrMlhPFWxe3rPuIf%2BmYxminAf1uJmof%2BorxB9gPaNQfdZi2Uwj%2FCBMiK%2FeSKEwsZNz%2B3eFwnu3aMTg%3D%3D&incident.impact=1&incident.urgency=1&incident.priority=1
It’s ugly, but we can work with it.
Using some dirty Regular Expression, I was able to strip out everything except the changed fields then turn it into an encoded query:
impact=1^urgency=1^priority=1
Field State Validation
Validates states of the desired fields.
The field states can be one (or more) of mandatory, not mandatory, read only, not read only, visible and not visible.
Inputs
- Table
- Visible (GlideList, Field names)
- Not visible (GlideList, Field names)
- Read only (GlideList, Field names)
- Not read only (GlideList, Field names)
- Mandatory (GlideList, Field names)
- Not mandatory (GlideList, Field names)
All field names are contained in g_form.elements fieldName property. You can get all the field names if you like by looping through them:
var fieldElements = g_form.elements; var fieldNames = []; for (var f in fieldElements) { if (fieldElements.hasOwnProperty(f)) { var fieldElement = fieldElements[f]; fieldNames.push(fieldElement.fieldName); } }
Visible
The isVisible function takes an element (like those in g_form.elements) and does not work with a field name alone.
var uiElement= g_form.getGlideUIElement(fieldName); var visible = g_form.isVisible(uiElement);
Not Visible
var uiElement= g_form.getGlideUIElement(fieldName); var notVisible = !g_form.isVisible(uiElement);
Read Only
The isReadOnly() function is more complicated still:
var element = g_form.getElement(fieldName); var control = g_form.getControl(fieldName); var isReadOnly = g_form.isReadOnly(element,control);
Not Read Only
var element = g_form.getElement(fieldName); var control = g_form.getControl(fieldName); var isNotReadOnly = !g_form.isReadOnly(element,control);
Mandatory
Why is the last one the easiest?
var isMandatory = g_form.isMandatory(fieldName);
Not Mandatory
var isNotMandatory = !g_form.isMandatory(fieldName);
Click a UI Action
Clicks a UI Action on the current form.
Inputs
- Table
- UI action (Reference UI Action)
- Assert type
UI Action
I thought this one was going to be really simple.
It turned out this was the hardest one to do and has many limitations that I didn’t realize when I was first doing this proof of concept.
To get the UI Action just clicked, you can pop the following code into an onSubmit Client Script:
var actionName = g_form.getActionName();
If you click the OOTB Save button, you get an actionName of the following:
sysverb_update_and_stay
The Test Step requires a sys_id, so we need to work out how we can use an action name to get the sys_id.
I ended up reverse-engineering the code for getActionName() to get the sys_id:
function getUIActionSysID() { var uiActionSysID=''; var form = g_form.getFormElement(); if (form) { var theButton = form.sys_action; if (theButton) { var buttonName = theButton.value; //Try get UI Action Sys ID var actionElement = g_form.getElement(buttonName); if (actionElement) { uiActionSysID = actionElement.getAttribute("gsft_id"); jslog('Clicked UI action SYS_ID is ' + uiActionSysID); } } } return uiActionSysID; }
I soon discovered that g_form.getActionName() is not a reliable way to track the UI Action the was clicked.
Client-Side UI Actions that use g_form.save() or g_form.submit() without an action name as a parameter just result in the OOTB action names for save and submit being captured.
I’ll dig into this further at a later time. You can see how I worked around this for yourself.
Assert Type
We can really only test for success here, as onSubmit Client Scripts are never called if the UI Action result was not successful.
Conclusion
So, I had proof to myself that I could automate the recording of ATF Tests for forms.
I thought it would only take a few hours to build a simple framework to automate all the Test Steps above.
I wanted to make a simple ATF Recorder that could be used by a Process Coordinator, a non-technical role who knew how to use ServiceNow but not how to use ATF.
Tests needed to almost always pass after recording, with little to no modification required to the generated Test.
The following Use Cases were decided for my MVP, using an OOTB system:
- Record the following Form Actions
- Impersonate
- Open a New Form
- UI Action Visibility
- Set Field Values
- Field Values Validation
- Field State Validation
- Click UI Action
- Record the following use Cases End to End with a Pass on replay
- New Incident to Resolution
- Request Item Workflow
- New Normal Change to Closure including Approvals
Like a typical Developer, I grossly underestimated how much effort this would take.
Like a typical Engineer, my proof of concepts was not exhaustive enough to detect some limitation that almost made this impossible. I’ll address the Limitations in further blogs. Watch this space!
Weeks / Months later, I present to the Community my Application ⏺️ Regress – ATF Recorder for UI16 Forms, now available on Share (v0.99).
Thanks for reading and please bookmark and comment below.
If you enjoyed this, please check out my Technical Blog series Service-KnowHow.