Imperative Extensions

Imperative extensions enable you to create reusable utility code to address complex use cases that aren't supported in UTAM's JSON grammar. For example, you can't use the JSON grammar to navigate a data table where each column shows data from a different type of component with different markup.

Note: Imperative extensions are powerful because you can use the full capabilities of a programming language. However, we strongly discourage their usage except as a last resort because they violate the declarative nature of UTAM. If you use an imperative extension, you must implement and maintain it in each programming language that you use with UTAM.

To author an imperative extension:

The utility code is referenced from a page object's JSON file so that a test writer can call the code.

Reference Imperative Code from a Page Object

Declare an imperative utility method in a compose statement in a page object. A page object can reference any number of utility classes.

This page object declares a compose method, myMethod, that calls two utility methods, utilityA and utilityB. There's a separate applyExternal statement for each utility method.

{
    "methods": [
        {
            "name": "myMethod",
            "return": "actionable",
            "returnList": true,
            "compose": [
                {
                    "applyExternal": {
                        "type": "utam-impext/utils/impext/exampleUtils",
                        "invoke": "utilityA"
                    }
                },
                {
                    "applyExternal": {
                        "type": "utam-impext/utils/impext/exampleUtils",
                        "invoke": "utilityB",
                        "args": [
                            {
                                "name": "stringArg",
                                "type": "string"
                            }
                        ]
                    }
                }
            ]
        }
    ]
}

The applyExternal property declares an imperative extension. For a full explanation of the syntax, see Grammar: imperative extensions.

JavaScript Example

A JavaScript imperative extension must:

Additionally, the first parameter of each exported function must be a context object that represents the context that an imperative extension requires to interact with the page object. The context object holds a single pageObject property, which is a reference to the page object.

This example implements the utilityA and utilityB functions declared in the page object.

// An imperative extension is authored as a JavaScript ES module.
// It exports a set of named functions.

// The first parameter of every function is a context object.
// It has a single pageObject property that holds a reference
// to the current page object in which the function has been referenced.
export async function utilityA(context) {
    const { pageObject } = context;
    // add logic for utility method here
}

// Destructure the context parameter to access the pageObject
// as alternative syntax with less code
export async function utilityB({ pageObject }, stringArg) {
    // add logic for utility method here
}

The return value is based on what the last action (utilityB) in the compose statement returns.

To call the myMethod method declared in the page object from test code, use await context.pageObject.myMethod() or await pageObject.myMethod() if you use destructuring.

For an interactive JavaScript example, see this tutorial.

Java Example

To author a Java imperative extension, use a class with a set of static public methods.

The first parameter of each static public must be a context object that's an instance of UtamUtilitiesContext, which is responsible for passing the page object's context to the static method.

This example implements the utilityA and utilityB methods declared in the page object.

package utam.utils.impext;

import utam.examples.pageobjects.ExamplePageObject;

public class ExampleUtils {

  public static void utilityA(UtamUtilitiesContext context) {
    // Explicit casting to the page object type
    ExamplePageObject examplePO =
        (ExamplePageObject)context.getPageObject();

    // call methods on page object here
  }

  public static List<Actionable> utilityB(UtamUtilitiesContext context,
                                          String stringArg) {
    // Explicit casting to the page object type
    ExamplePageObject examplePO =
        (ExamplePageObject)context.getPageObject();

    // call methods on page object here

    // return List<Actionable>
  }
}

The UtamUtilitiesContext object exposes the getPageObject() getter that returns an object of type PageObject. You must explicitly cast the type of the object returned by getPageObject() to the type of the current page object instance, which is a subclass of the PageObject type. In this example, the page object instance is ExamplePageObject.

Note: The method implementation shouldn't interact with Driver or Element directly.

Return Value

By default, the return value of a compose statement is the return value of the last action in the statement. The framework can infer the return value from the apply value in the page object. The framework can't infer the return type of an imperative extension so you sometimes have to explicitly declare the return type if you write Java utility code.

A compose method has two optional properties where you can explicitly declare the return type.

These properties are declared at the method level, which means they are siblings of the name and compose properties. The page object for our example declares that the method returns a list of actionable elements.

{
  "methods": [
    {
      "name": "myMethod",
      "return": "actionable",
      "returnAll": true,
      "compose": [ ...
      ]
    }
  ]
}

For JavaScript, you don't have to declare the return or returnAll properties because the default behavior suffices. However, for Java, you must declare the return property if the last action in the compose statement invokes utility code that has a non-void return type.

In Java, if the last action in the compose statement invokes utility code, these rules apply for the method's return type:

A utility method should return a type that can be referenced from JSON, which is one of: