UI Test Automation of Maestro Forms

   MaestroThe UI design product.  |   Form Builder |   22.04 This feature was updated in 22.04.

As we explained in UI test automation overview, UI testing of application forms can be a daunting task. However, Journey Maestro can help you with that. Let's look at how you can achieve this.

Maestro is a design tool that generates single page web applicationsA single-page application (SPA) is a web application or website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of a web browser loading entire new pages.. These application are Maestro forms, which are packaged during a build process. How it does this is key to solving the test automation puzzle:

The machine readable file is called form.json and it’s packaged in the META-INF directory of the Form Archive (FAR) build artefact, which is itself packages within a ZIP file. This is illustrated below.

The form.json provides the Form View definition, as shown below:

This structure is unsterilized into a browser DOM:

Maestro Form.json

Look inside the form.json and you will see a structure that represents the rows and items that define the UI elements. However, this data exists in a complex structure, most of which is superfluous for our testing needs.

Three objects are of importance:

  • rows - contains the form UI page definitions, which are often deeply recursive - there are rows withing rows that represent the hierarchical structure of the <div> elements in the rendered form. The UI elements themselves are all leaf nodes and we will exploit this fact when extracting the definitions.
  • dialogs - contain dialog definitions, each of which is represented as a named object, each of which has a rows property which conforms to the structure discussed, above.
  • modals - contains modal definitions and has an identical structure to the dialogs object.

Writing code that parses this structure is challenging. Therefore, in our framework, we sidestepped this issue and used JsonPath. JsonPath expressions are analogous to XPath expressions, allowing the user to query complex structures using simple expressions. For more information, see https://goessner.net/articles/JsonPath/index.html#e2 and https://github.com/json-path/JsonPath.

Three expressions are needed to get you started, one for each of rows, dialogs and modals:

  • $.rows..[?([email protected] && @.id && @.type)] - This recursively searches all children of the root rows object, filtering children that do not have a rows element themselves, but do have both an id and a type property.
  • $.dialogs..[?([email protected] && @.id && @.type)] - Same as above but start the search at a different root location in the JSON structure.
  • $.modals..[?([email protected] && @.id && @.type)] - Same as above but start the search at a different root location in the JSON structure.

The result will be returned as an array of Form Item definitions.

Note

We use item and element interchangeably.

When testing these expressions, we found the JSONPath Online Evaluator to be invaluable. This tool is easy to use. What you need to do is to paste a form.json file on the left side of the main window, as shown below, and collapse all of the nodes except rows. We used the JSONPath expression to return form elements, namely, leaf elements under the rows object that have an id and a type.

Form Item Definition

The form item definition, shown in an image above, is replicated below verbatim. We scrolled the display to show a complete item and we will use this to explain the structure and how it maps to a Selenium object locator.

{
  "entityPath": "customers_map_primary_PhoneNumber",
  "exclude": false,
  "hasCustomId": true,
  "hasCustomPath": true,
  "htmlLabel": "Mobile Number",
  "icon": "services/formresources/146770/widgets/text-input/text-input.png",
  "id": "customers_map_primary_PhoneNumber",
  "label": "Mobile Number",
  "mandatory": true,
  "noData": false,
  "path": "PhoneNumber",
  "pathContext": "",
  "properties": {
    "inputMode": "tel",
    "maxLength": 15,
    "placeholder": "eg: +45 664 123 4567"
  },
  "rules": {
    "ok": "value = value.replace(/[\\s-]/g, \"\");\nvar regEx = /^\\d{12}/;\nvar regEx2 = /^\\d{11}/; \nvar regEx3 = /^\\d{10}/; \nvar number =  value.replace(/[\\s-+]/g, \"\");\nif(value.indexOf(\"+\") == 0 && (number.length >= 10 || number.length <= 12) && (regEx.test(number) || regEx2.test(number) || regEx3.test(number))) {\n\tdata[item.id] = value;\n\treturn true;\n} else {\n  if (data.selectLanguage == \"de\") {\n    return \"Bitte geben Sie Ihre \"+item.label+\" im Format +439999999999 ein.\";\n  } else {\n\treturn \"Please enter your \"+item.label+\" in the format +439999999999.\";\n  }\n}"
  },
  "styles": [
    "avalon-form-field"
  ],
  "type": "text-input"
}

The structure of a form definition is fairly self-explanatory, if we ignore the rules element, and consists of the following elements:

  • id - is the unique identity for the element.
  • type - is the Maestro type and, as we shall see, this type plus the ID is the key to test generation.
  • entityPath - is the entity path into the Maestro data model in the browser.
  • hasCustomId - is a Boolean indicating whether the system or the user has generated the Id (and is therefore useful in linting applications).
  • mandatory - is a Boolean indicating whether this item must be provided if the element is visible.
  • properties - provides a type specific container for additional properties. In the example above, the mobile keyboard type is set to telephone, the maximum number of characters that can be input is set to 15 and the placeholder for text used by a screen reader is set.

Item Definitions and Selenium Locators

We’re now in a position to bring together all of the threads in this discussion and explain how the item definition is used to generate Selenium identifiers.

Maestro generates HTML code, the code varies according to the item type, but is always predictable. Using the example of a text-field we see the element rendered in a form with the DOM Inspector showing:

We can see that the element id is exactly that from the item definition in the form.json. Because all text-field items are treated the same way by the Maestro compiler, we can safely and reliably generate locators:

Framework

Example code

Selenium Java

By.id("customers_map_primary_PhoneNumber");

Selenium C#

By.id("customers_map_primary_PhoneNumber");

Selenium JavaScript

By.id("customers_map_primary_PhoneNumber")

Protractor

by.id("#customers_map_primary_PhoneNumber")

Cypress

cy.get("#customers_map_primary_PhoneNumber")

Galen

phoneNumber id customers_map_primary_PhoneNumber

By generate, we mean that a piece of code reads the form.json definition and writes the Locator code itself by iterating over the array of items extracted from the form.json as explained earlier.

A simplistic implementation of a method that generates Java code for the text-field type is:

/**
 * Build a By statement for any of the Text Field types
 *
 * @param maestroElementDefinition The element definition from the form.json
 * @return Java source for a By selector that will locate this element
 */
private static String builtTextFieldSelector(final JSONObject maestroElementDefinition) {
  final String TEXT_FIELD_TEMPLATE = """
      // Maestro type: %s
      static final By %s = By.id(\"%s\");
      """;

  final String id = maestroElementDefinition.getAsString("id");
  final String type = maestroElementDefinition.getAsString("type");
  final String by = String.format(TEXT_FIELD_TEMPLATE, type, id, id);
  return by;
}

Given the JSON object used earlier as an argument, this would return:

// Maestro type: text-field
static final By customers_map_primary_PhoneNumber = By.id("customers_map_primary_PhoneNumber");

Sadly, different Maestro element types have different code generation strategies. Testers must also deal with Repeats, which are blocks containing Form elements that can be repeated an arbitrary number of times. Finally, all we have used is the id and type properties; we have ignored the rich source of metadata about the element including in the property object. We will deal with each of these issues later in this documentation.

Statically Typed Definitions

In the generated code example above, we used the property name as the member name and we made the field static final. This brings us to why a statically typed language is so valuable. If a developer changes the id of an element, then the test that relies on this element name will fail with a compilation error, immediately on regeneration of the locators.

It is hard to overstate how important this is to tester productivity. By generating code in a statically typed language, we eliminate the major cause of non-deterministic runtime test failures. However, as noted, we have not yet used all the metadata available to us to produce a truly strongly typed model. To do this, we would have to write a Maestro aware framework on top of Selenium.

Field Validation

Maestro provides validation rules and, where these fail, an error message is displayed.

Maestro form validation rules visualized

Once again, the same rule always applies for the generation of these messages. The id of the <span> containing the error message is created from {id}_error. Where {id} is the id of the Maestro element to which this message is bound, in this case it is customers_map_primary_PhoneNumber.

In testing terms, this grants us a major advantage - by checking whether the error block is displayed, we have a reliable means of testing whether the value input is valid. These selectors can be generated from our JSON.

Data Model Lookup

Thus far, we have concerned ourselves with the DOM elements for our form. In Maestro, each item has an associated entry in the form data model. In our item definition we have:

"entityPath": "customers_map_primary_PhoneNumber"

This key can be used to get or set the data value bound to the element. Maestro exposes the following API to access this data:

$("Form").scope().Form

You can check it in your browser’s console, as shown below. We have expanded the return value to show the data entry and highlighted the phone number field.

A test that exploit this by using the Selenium driver’s executeScript method of the JavaScriptExecutor class:

((JavaScriptExecutor)driver).executeScript(
  "return $('Form').scope().Form.data.customers_map_primary_PhoneNumber" );

You can see that an automated test can not only set and validate elements via the UI, it can also verify that the underlying data model is updated as expected for the element type, which is a vital step requirement when dealing with elements like Social Security Numbers that require masking.

Page Object Model Generation

A Page Object Model is a collection of the locators or higher-level elements, grouped logically by page. However, looking back, you can see that the item’s JSON definition doesn't contain any information identifying the parent page / dialog / modal.

To solve this problem, we need to resort to some coding so that an item is aware of where it sits in the hierarchy. As an aside, the runtime form engine provides this feature, decorating each item with a parent property. This is one of the few things done at runtime and so we must do this by hand in our code generation framework.

Once again, JSONPath can assist us. In this case, you use the Output Paths feature of the tool, which outputs the path to a JSON object matched by the query expression. This is illustrated below:

You use this information as follows:

gather the list of JSON Path elements for the page
gather the list of JSON elements for the page 
loop for each target JSON element
  get the matching JSON Path element for the target JSON element from the JSON Path list
  loop for each path element in the JSON Path
    remove the last path element from the JSON Path
    lookup the JSON element from this JSON Path
    if the type of this JSON Path is page, then break
  continue
  add the page name as a property on the JSON element
end loop
Note

You can implement this pseudo code in your preferred programming language.

Once you have the page information, you can output a true Page Object model.

Conclusion

You could rightfully ask - "Why doesn’t Temenos Provide a Testing Tool that does this for us?"

We asked our customers "What UI test framework and language do you use?" There were almost as many answers as we have customers.

For this reason, we’ve decided to publish this guidance for how to exploit the data we provide. We’re also looking at open sourcing our own internal Proof of Concept (POC) that we use to demonstrate the techniques we discuss here.

Next, learn how to debug Maestro forms.