JSON Grammar

Root Element

The root of a page object can contain these properties:

{
    "elements": [],
    "methods": [],
    "shadow": {
        "elements": []
    },
    "root": true,
    "selector": {
        "css": "one-record-home"
    }
}

Actionable Root Element

To make the root element actionable, expose it via a public method. Add these properties to the root element:

{
    "exposeRootElement": true,
    "type": ["editable"],
    "elements": [],
    "methods": []
}

The UTAM generator converts this JSON into a public method. The method returns an instance of the page object root element for the test to interact with.

async getRoot() {
    const driver = this.driver;
    const root = await this.getRootElement();
    return new _EditableUtamElement(driver, root);
}

Note: The word root is reserved and can't be used as an element name.

Basic Element

If a component has an HTML element that you need in the page object, declare a basic element as an object in an elements array. Add a basic element only if it’s needed for user interactions or to scope other elements.

You can nest a basic element object in any elements array: at the root, inside a shadow, or nested inside another basic element.

A basic element can have these properties:

{
    "elements": [
        {
            "name": "myElement",
            "type" : [ "clickable", "editable" ],
            "public": true,
            "selector": {
                "css": ".element"
            },
            "elements": []
        }
    ]
}

If an element has one basic type only, it can be defined as a string instead an array of strings. For example, this root element has the actionable type only:

{
    "exposeRootElement" : true,
    "type" : "actionable"
}

UTAM generates a public method that returns an instance of the element to interact with.

Java:

public MyElementElement getMyElement() {
    // return element
}

JavaScript:

//declaration
getMyElement(): Promise<_BaseUtamElement>;

// implementation
async getMyElement() {
    const driver = this.driver;
    const root = await this.getRootElement();
    let element = await _utam_get_myElement(driver, root, );
    return new _ActionableUtamElement(driver, element);
}

Custom Element

To represent a nested component, declare a custom element as an object in the elements array. A custom element has a type property that references another page object.

You can nest a custom element object in any elements array: at the root, inside a shadow, or nested inside a basic element.

A custom element can't have nested elements.

A custom element has the same properties as a basic element, except that the type property is required and must reference another page object.

This example declares a custom element called todo-item, which lives in the utam-tutorial package.

{
  "root": true,
  "selector": { "css": "body" },
  "elements": [
      {
          "name": "todoApp",
          "selector": { "css": "example-todo-app" },
          "public": true,
          "shadow": {
              "elements": [
                {
                  "name": "todoItem",
                  "selector": { "css": "example-todo-item" },
                  "public": true,
                  "type": "utam-tutorial/todoItem"
                }
            ]
          }
      }
  ]
}

The generated getTodoItem() method returns an object scoped inside its parent element.

JavaScript:

getTodoApp(): Promise<_ActionableUtamElement>;
getTodoItem(): Promise<_todoItem>;

Container Element

A component with a slot or div can act as a container for other components whose types are unknown to the developer.

For example, lightning-tabset has a placeholder for tab content. In component source code, a placeholder is a slot.

<template>
    <div class={computedClass} title={title}>
        <lightning-tab-bar variant={variant} onselect={handleTabSelected}></lightning-tab-bar>
        <slot></slot>
    </div>
</template>

Another example is a panel component that can hold any content and produces HTML like this:

<div class="actionBody">
    <!-- any component can be injected here -->
    <records-detail-panel>
        <!-- inner HTML -->
    </records-detail-panel>
<div>

In this case, declare an element with "type": "container". The compiler generates a method with a parameter for the type of the component being loaded.

You can nest a container object in any elements array: at the root, inside a shadow, or nested inside a basic element.

To declare a container element:

  1. If needed, add a private basic element that serves as a scope. (Don't set "public": true; the test writer doesn't use this element in a test and doesn't need to know about it.)

In our examples:

  1. Add a nested public container element with these properties:
    • name (Required) String. An element name that's unique to this JSON file.
    • type (Required) String. The value must be container.
    • public (Required) Boolean. Must be set to true so that UTAM generates a public method.
    • selector (Optional, default is "css": ":scope > *:first-child"). A selector injected as a root for the container content.
  • As with any other element, if there's a #shadow-root between the basic element and the nested container element, enclose the container element in a shadow object.
  • Most containers should have a hardcoded selector. In most cases, the hardcoded selector should be *, which represents all the direct children of container's enclosing parent element.
  • If a selector is omitted, a default CSS selector of :scope > *:first-child will be used in the generated code.

Let's take our panel, which has body and footer areas where a developer can render any application component. In the page object, add a private HTML element contentScope that serves as a scope. Then add a nested public panelContent container element.

{
    "elements": [
        {
            "name": "contentScope",
            "selector": {
                "css": ".actionBody"
            },
            "elements": [
                {
                    "name": "panelContent",
                    "type": "container",
                    "public": true,
                    "selector": {
                        "css": "*"
                    }
                }
            ]
        }
    ]
}

UTAM generates this JavaScript code.

async function _utam_get_actionBody(driver, root) {
    let _element = root;
    const _locator = core.By.css(`.actionBody`);
    return _element.findElement(_locator);
}

async function _utam_get_detailsPanelContainer(driver, root) {
    let _element = await _utam_get_actionBody(driver, root, );
    const _locator = core.By.css(`*`);
    return _element.findElement(_locator);
}

async getPanelContent(ContainerCtor) {
        const driver = this.driver;
        const root = await this.getRootElement();
        let element = await _utam_get_detailsPanelContainer(driver, root, );
        element = new ContainerCtor(driver, element);
        return element;
}

From the test, to load DetailsPanel inside our panel, call the generated container method.

const recordModal = await utam.load(MyPanel);
const detailPanel = await recordModal.getPanelContent(DetailsPanel);

Sometimes the container selector can be more specific than *. For the lightning-tabset component we know that each tab has an attribute role, so the selector can be [role="tabpanel"]. But if the container is placed correctly inside its immediate parent, * always works.

If the container declaration doesn't have a selector, the generated method has a second parameter for a selector to be hardcoded from test code. We consider this a bad practice because it exposes internals of the component, but it is possible.

const detailPanel = await recordModal.getPanelContent(DetailsPanel, utam.By.css('records-detail-panel'));

If the selector inside the container is marked with "returnAll": true, the generated container method returns an array (in JavaScript) or a list (in Java) of objects.

For our tabset example it makes sense to return all tabs:

{
    "elements": [
        {
            "type": "container",
            "name": "tabs",
            "public": true,
            "selector": {
                "css": "[role='tabpanel']",
                "returnAll": true
            }
        }
    ]
}

UTAM generates this JavaScript code.

// declaration returns array
getTabs<T extends _UtamBasePageObject>(ContainerCtor: _ContainerCtor<T>): Promise<T[]>;

// implementation
async getTabs(ContainerCtor) {
        const driver = this.driver;
        const root = await this.getRootElement();
        let elements = await _utam_get_slots(driver, root, );
        elements = elements.map(function _createUtamElement(element) {
            return new ContainerCtor(driver, element);
        });
        return elements;
}

The test method loads all the existing tabs and returns an array with 3 tabs:

const tabs = await tabset.getTabs(Tab);
expect(labs.length).toBe(3);

Frame element

To support a frame or an iframe, use a separate type of element with the following properties:

Here's an example of a frame element:

{
    "name": "myPublicFrame",
    "public": true,
    "type": "frame",
    "selector": {
      "css": "iframe"
    }
}

The generated code returns an object of the special FrameElement type that can be used as a parameter in methods to switch between frames.

Element Selector

Every basic element and custom element requires a selector object that locates the element at run time. A container element doesn't require a selector, because it's scoped inside a basic element.

When a selector is used for a custom element, the selector is reserved for the custom element’s type and can’t be reused for any other type.

UTAM supports all valid CSS selectors.

Selector Properties

A selector object has these properties:

{
    "elements": [
        {
            "name": "myDiv",
            "selector": {
                "css": ".myDiv"
            }
        },
        {
            "name": "listItems",
            "selector": {
                "css": "li a",
                "returnAll": true
            }
        }
    ]
}

Selector Parameters

A selector can depend on run-time information from the test, like a label or an index. To access this run-time information, use these format indicators inside a CSS selector. At run time, they’re replaced by values passed as parameters to the element’s public method.

{
    "elements": [
        {
            "name": "listItemWithTitle",
            "selector": {
                "css": "li a[title='%s']",
                "args": [
                    {
                        "name": "titleString",
                        "type": "string"
                    }
                ]
            },
            "public": true
        }
    ]
}

UTAM generates this code from the JSON.

  public Actionable getListItemWithTitle(String titleString) {
    // return instance of the element using selector with replaced parameter
  }

A nested element can inherit parameters from its parents. In this example, the nested input needs two integer parameters for the enclosing table cell.

{
    "elements": [
        {
            "name": "tableCell",
            "selector": {
                "css": "tr:nth-of-type(%d) td:nth-of-type(%d)",
                "args": [
                    {
                        "name": "rowIndex",
                        "type": "number"
                    },
                    {
                        "name": "colIndex",
                        "type": "number"
                    }
                ]
            },
            "elements": [
                {
                    "name": "inputInsideCell",
                    "selector": {
                        "css": "input"
                    },
                    "type": ["editable"],
                    "public": true
                }
            ]
        }
    ]
}

UTAM collects parameter names for every element from root that leads to the current element and adds them in order of appearance.

  public Editable getInputInsideCell(int rowIndex, int colIndex) {
    // Apply the provided parameters,
    // then find an input element inside a table cell.
  }

Mobile Selectors

The Salesforce mobile app is a mobile hybrid application. Some pages are WebView pages, and some pages are native pages. For a native page, CSS selectors aren't sufficient. To find elements in a mobile environment, Appium implements a number of locator strategies for specific mobile devices. For more information, see appium.io: Find Elements.

Appium is an open-source tool for automating native, mobile web, and hybrid applications. Appium allows you to write tests against multiple platforms, such as iOS and Android, using the same API.

Accessibility ID Selector

This selector is for Mobile Platform and UTAM Java only.

Use the accessibility ID selector, which is a unique identifier for a UI element. This example uses an accessibility ID selector of searchCell.ViewAllCell.

{
  ...
  	"name": "searchCell",
    "selector": {
        "accessid": "searchCell.ViewAllCell"
    },
  ...
}

Generated Java code:

@Selector.Find(accessid = "searchCell.ViewAllCell")
private ElementLocation searchCell;

iOS Class Chain Selector

This selector is for iOS Platform and UTAM Java only.

The iOS class chain selector finds a window element from the root of the page:

Page object JSON:

{
  ...
    "name": "appWindow",
    "selector": {
        "classchain": "XCUIElementTypeWindow"
    },
  ...
}

Generated Java code:

@Selector.Find(classchain = "XCUIElementTypeWindow")
private ElementLocation appWindow;

You can use double star and slash (**/) to define the next item descendant from the root. This strategy makes the configured locator not sensitive to the hierarchy of the page.

Page object json:

{
  ...
    "name": "button",
    "selector": {
        "classchain": "**/XCUIElementTypeButton"
    },
  ...
}

Generated Java code:

@Selector.Find(classchain = "**/XCUIElementTypeButton")
private ElementLocation button;

Sometimes, the element type isn't unique enough to identify a specific element. In those scenarios, you can combine the element type with an attribute using a predicate. The predicate string should always be enclosed in ` marks or $ characters inside square brackets. Use `` or $$ to escape a single ` or $ character inside a predicate expression.

A single backtick means the predicate expression is applied to the current children. A single dollar sign means the predicate expression is applied to all the descendants of the current element(s). String values, not the key words, can be enclosed by ' or ".

Page object JSON:

{
  ...
    "name": "newButton",
    "selector": {
        "classchain": "**/XCUIElementTypeButton[`name BEGINSWITH 'New'`]",
    },
  ...
}

Generated Java code:

@Selector.Find(classchain = "**/XCUIElementTypeButton[`name BEGINSWITH 'New'`])
private ElementLocation newButton;

This selector uses the $ character to apply the attribute to all the descendants of the current element(s).

Page object JSON:

{
  ...
    "name": "newButton",
    "selector": {
        "classchain": "**/XCUIElementTypeCell[$name == \"My Accounts\"$]",
    },
  ...
}

Generated Java code:

@Selector.Find(classchain = "**/XCUIElementTypeCell[$name == \"My Accounts\"$])
private ElementLocation newButton;

The following attributes are supported:

The following comparison types for an attribute are supported:

You can use the following operators to set conditions for multiple attributes in one expression.

For example:

{
  ...
    "name": "newButton",
    "selector": {
        "classchain": "**/XCUIElementTypeBotton[`name BEGINSWITH 'New' AND visible == true`]",
    },
  ...
}

Generated Java code:

@Selector.Find(
    classchain = "**/XCUIElementTypeBotton[`name BEGINSWITH 'New' AND visible == true`]"
)
private ElementLocation newButton;

Similarly to CSS selectors, you can use string or integer parameters. This example uses a string parameter.

{
  ...
    "name": "tabBarBtn",
    "selector": {
        "classchain": "**/XCUIElementTypeButton[`name == '%s'`]",
        "args": [
            {
                "name": "item",
                "type": "string"
            }
        ]
    }
}

Generated Java code:

@ElementMarker.Find(classchain = "**/XCUIElementTypeButton[`name == '%s'`]")
private ElementLocation tabBarBtn;

@Override
final TabBarBtnElement getTabBarBtnElement(String item) {
  return element(this.tabBarBtn).build(TabBarBtnElement.class, TabBarBtnElementImpl.class, item);
}

Here's more detailed examples for iOS Class Chain.

Android UIAutomator Selector

This selector is for Android Platform and UTAM Java only.

Appium uses the UIAutomator selector to enable searching using UiSelectors. We support the following methods only from UiSelector to find elements:

This example uses the clickable method:

{
  ...
    "name": "acceptButton",
    "selector": {
        "uiautomator": "new UiSelector().clickable(true)"
    },
  ...
}

Generated Java code:

@Selector.Find(uiautomator = "new UiSelector().clickable(true)")
private ElementLocation acceptButton;

This example uses the className method:

{
  ...
  	"name": "newButton",
    "selector": {
        "uiautomator": "new UiSelector().className(\"android.widget.TextView\")"
    },
  ...
}

Generated Java code:

@Selector.Find(
    uiautomator = "new UiSelector().className(\"android.widget.TextView\")"
)
private ElementLocation newButton;

This example uses the descriptionContains method:

{
  ...
    "name": "vfLink",
    "selector": {
        "uiautomator": "new UiSelector().descriptionContains(\"VfPage Link\")"
    },
  ...
}

Generated Java code:

@Selector.Find(
    uiautomator = "new UiSelector().descriptionContains(\"VfPage Link\")"
)
private ElementLocation vfLink;

UIScrollable is a powerful Android class that performs element lookups in scrollable layouts. In most cases, you should use the scrollIntoView method, which performs a scroll action until the destination element is found on the screen. You can use UIScrollable swipe to:

The scrollIntoView method has UiSelector as search criteria input to allow you to find elements by supported methods. For example:

{
    "implements": "utam-salesforceapp/pageObjects/navigation/navItemsList",
    "elements": [
        {
            "name": "navItemWithLabel",
            "selector": {
                "uiautomator": "new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(className(\"android.widget.TextView\").text(\"%s\"))",
                "args": [
                    {
                        "name": "item",
                        "type": "string"
                    }
                ]
            }
        }

The generated Java code is:

  @ElementMarker.Find(
    uiautomator =
        "new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(className(\"android.widget.TextView\").text(\"%s\"))",
    nullable = true
  )
  private ElementLocation navItemWithLabel;

Here's more details for UIAutomator UiSelector and UiScrollable.

List and Index

When a selector includes "returnAll": true, the generated method returns a list.

In the DOM, a parent can contain multiple instances of a custom element, like lightning-tab components inside a lightning-tabset. If the selector has a returnAll property set to true, the method returns a list of instances found at run time.

{
    "elements": [
        {
            "name": "allTabs",
            "type": "utam-lightning/pageObjects/lightning/tab",
            "selector": {
                "css": "lightning-tab",
                "returnAll": true
            },
            "public": true
        }
    ]
}

From the previous JSON, UTAM generates this public method, which returns a list of the page objects of the given type.

public List<Tab> getAllTabs() {
    // return list of instances found in runtime
    // throw exception if nothing found
}

To get one of the instances by its index, add :nth-of-type(%d) to the injected selector and the args property with an index parameter. :nth-of-type(%d) is 1-based, not 0-based.

{
    "elements": [
        {
            "name": "myComponent",
            "type": "utam-lightning/pageObjects/lightning/myComponent",
            "selector": {
                "css": "lightning-my-component:nth-of-type(%d)",
                "args": [
                    {
                        "name": "index",
                        "type": "number"
                    }
                ]
            },
            "public": true
        }
    ]
}

The generated method finds all the custom elements inside the parent and returns one by index.

public MyComponent getMyComponent(int index) {
    // return nth instance
    // if nothing found, or index is out of bounds, throw exception
}

Element Filter

To pick an element or a list of elements based on a condition at run time, add a filter inside an element node. An element with a filter must have a selector with returnAll set to true.

A filter object inside an element has these properties:

This example uses a filter to pick one of the lightning-menu-item instances inside a lightning-button-menu based on the item text.

{
    "elements": [
        {
            "name": "menuItemByText",
            "type": "utam-lightning/pageObjects/lightning/menuItem",
            "selector": {
                "css": "lightning-menu-item",
                "returnAll": true
            },
            "filter": {
                "apply": "getItemText",
                "findFirst": true,
                "matcher": {
                    "type": "stringContains",
                    "args": [
                        {
                            "name": "itemText",
                            "type": "string"
                        }
                    ]
                }
            },
            "public": true
        }
    ]
}

For the example to work, the lightning-menu-item page object must declare a public method getItemText that returns menu item text.

The generated method finds all lightning-menu-item instances inside a lightning-button-menu based on the item text. For each item, getItemText executes and picks the first item that contains the parameter value as a substring. If no match is found, it returns null.

public MenuItem getMenuItemByText(String itemText) {
    // find all available menu items
    // get text for each item
    // pick first item that contains value provided in parameter
}

Example of a basic element filter that returns a list:

{
    "elements": [
        {
            "name": "myElement",
            "selector": {
                "css": ".myClass",
                "returnAll": true
            },
            "filter": {
                "apply": "getText",
                "matcher": {
                    "type": "stringContains",
                    "args": [
                        {
                            "name": "matcherArg",
                            "type": "string"
                        }
                    ]
                }
            },
            "public": true
        }
    ]
}

UTAM generates this compiled type:

getMyElement(matcherArg: string): Promise<_ActionableUtamElement[]>;

UTAM generates this compiled JavaScript:

async getMyElement(matcherArg) {
    const driver = this.driver;
    const root = await this.getRootElement();
    let elements = await _utam_get_myElements(driver, root, );
    elements = elements.map(function _createUtamElement(element) {
        return new _ActionableUtamElement(driver, element);
    });
    const appliedFilter = await Promise.all(elements.map(el => _utam_filter_myElement(el, matcherArg)));
    elements = elements.filter((_, i) => appliedFilter[i]);
    return elements;
}

Matchers

Use a matcher to apply a condition at run time:

A matcher is an object with these properties.

This matcher uses the stringContains matcher type.

"matcher": {
    "type": "stringContains",
    "args": [
        {
            "name": "itemText",
            "type": "string"
        }
    ]
}

Compose Method

To combine several element actions, such as a method for login that sets a username, sets a password, and clicks a button, declare a compose method.

A compose method has these properties:

Here are some examples of compose methods:

Invoke action for a basic element

This compose method sets text on the root element and clicks a submit button.

{
    "type": ["editable"],
    "elements": [
        {
            "name": "submitBtn",
            "type": ["clickable"],
            "selector": {
                "css": ".submit"
            }
        }
    ],
    "methods": [
        {
            "name": "submitForm",
            "compose": [
                {
                    "element": "root",
                    "apply": "setText",
                    "args": [
                        {
                            "type": "string",
                            "name": "stringToEnter"
                        }
                    ]
                },
                {
                    "element": "submitBtn",
                    "apply": "click"
                }
            ]
        }
    ]
}

Here's the generated Java code:

public void submitForm(String stringToEnter) {
    getRoot().setText(stringToEnter);
    submitBtn.click();
  }

Here's the generated JavaScript code:

async submitForm(stringToEnter) {
    const _statement0 = await this.__getRoot();
    await _statement0.setText(stringToEnter);
    const _statement1 = await this.__getSubmitBtn();
    const _result1 = await _statement1.click();
    return _result1;
}

Invoke method from the same page object

The invokeSubmitForm method simply invokes the submitForm method declared in the same page object.

{
    "elements": [
        // ...
    ],
    "methods": [
        {
            "name": "submitForm",
            "compose": [
                // ...
            ]
        },
        {
            "name": "invokeSubmitForm",
            "compose": [
                {
                    "apply": "submitForm",
                    "args": [
                        {
                            "type": "string",
                            "name": "stringToEnter"
                        }
                    ]
                }
            ]
        }
    ]
}

This pattern is useful if you want to reuse the same method in multiple compose statements. For example, you could call submitForm from login and loginWithDeepLink methods.

Here's the generated JavaScript code:

async invokeSubmitForm(stringToEnter) {
    const _result0 = await this.submitForm(stringToEnter);
    return _result0;
}

Invoke an element's getter

This compose method invokes the getter for the myCustomComponent element.

{
    "elements": [
        {
            "name": "myCustomComponent",
            "type": "my/custom/component",
            "selector": {
                "css": "custom-component"
            }
        }
    ],
    "methods": [
        {
            "name": "composeGettingCustomElement",
            "compose": [
                {
                    "element": "myCustomComponent"
                }
            ]
        }
    ]
}

Here's the generated JavaScript code.

async composeGettingCustomElement() {
    const _result0 = await this.__getMyCustomComponent();
    return _result0;
}

Invoke method from a different page object

This compose method applies the someUnknownPublicMethod method to the myCustomComponent custom element.

{
    "elements": [
        {
            "name": "myCustomComponent",
            "type": "my/custom/component",
            "selector": {
                "css": "custom-component"
            }
        }
    ],
    "methods": [
        {
            "name": "invokeCustomElementMethod",
            "compose": [
                {
                    "element": "myCustomComponent",
                    "apply": "someUnknownPublicMethod"
                }
            ]
        }
    ]
}

Note that the compiler can't know if the someUnknownPublicMethod method actually exists, and what are its return value or parameters. The responsibility to validate those things belongs to the developer or a preruntime compilation step (depending on the setup).

Here's the generated JavaScript code:

async invokeCustomElementMethod() {
    const _statement0 = await this.__getMyCustomComponent();
    const _result0 = await _statement0.someUnknownPublicMethod();
    return _result0;
}

Return Type for a Compose Statement

The return type of a compose method is based on what the last action returns, and the parameters are listed in the order of actions.

public void submitForm(String stringToEnter) {
    getRoot().setText(stringToEnter);
    submitBtn.click();
  }

If the element referenced in compose is marked as a list (returnAll is set to true inside the selector), the action is applied to each element in the list. Depending on the last return statement, the method returns void or a list of objects returned by the last statement.

For JavaScript, the generated method always returns Promise<unknown>. The declaration looks like this:

submitForm(stringToEnter: string): Promise<unknown>;

The return type for a compose method can be inferred by the UTAM compiler if the last statement is one of:

  1. An element getter invocation for an element from the same JSON (not chain!).

Note: The compiler can identify a statement as a getter invocation only when the statement uses an "element" property:

{
  "methods": [
       {
           "name": "composeGettingCustomElement",
           "compose": [
               {
                   // we know it's a getter and can infer return type
                   "element": "myCustomElement"
               },
               {
                   // we don't know it's a getter
                   // can be regular public method
                   "apply": "getMyCustomElement",
                   "returnType": "my/custom/component"
               }
           ]
       }
  ]
}
  1. A basic action invocation. In this example, the method's return type is String because it's a known return type of the getText action.
{
  "methods": [
    {
      "name": "myMethod",
      "compose": [
        {
          "element": "root",
          "apply": "getText"
        }
      ]
    }
  ]
}
  1. If a statement has a matcher, the statement returns a Boolean regardless of the matcher type:
{
    "methods": [
    {
      "name": "myMethodWithMatcher",
      "compose": [
        {
          "element": "root",
          "apply": "getText",
          "matcher": {
              "type": "stringEquals",
              "args": [
                  {
                      "value": "extectedValue"
                  }
              ]
          }
        }
      ]
    }
  ]
}

If the compiler can't infer a statement's return type, it defaults to void or can be set explicitly as one of:

{
  "elements": [
    {
      "name": "myCustomElement",
      "selector": {
        "css": "custom-element"
      },
      "type": "my/page/object"
    },
    {
      "name": "myCustomListElement",
      "selector": {
        "css": "custom-element",
        "returnAll": true
      },
      "type": "my/page/object"
    }
  ],
  "methods": [
    {
      "name": "myMethodReturnsString",
      "compose": [
        {
          "element": "myCustomElement",
          "apply": "getCustomText",
          "returnType": "string"
        }
      ]
    },
    {
      "name": "myMethodReturnsManyString",
      "compose": [
        {
          "element": "myCustomListElement",
          "apply": "getCustomText",
          "returnType": "string",
          "returnAll": true
        }
      ]
    }
  ]
}
{
  "elements": [
    {
      "name": "myCustomElement",
      "selector": {
        "css": "custom-element"
      },
      "type": "my/page/object"
    },
    {
      "name": "myCustomListElement",
      "selector": {
        "css": "custom-element",
        "returnAll": true
      },
      "type": "my/page/object"
    }
  ],
  "methods": [
    {
      "name": "myMethodReturnsCustom",
      "compose": [
        {
          "element": "myCustomElement",
          "apply": "getComponentInsideCustomElement",
          "returnType": "my/page/component"
        }
      ]
    },
    {
      "name": "myMethodReturnsCustomList",
      "compose": [
        {
          "element": "myCustomListElement",
          "apply": "getComponentInsideCustomElement",
          "returnType": "my/page/component",
          "returnAll": true
        }
      ]
    }
  ]
}
{
  "elements": [
    {
      "name": "myElement",
      "selector": {
        "css": ".css"
      },
      "type": [ "clickable" ]
    }
  ],
  "methods": [
    {
      "name": "clickAndReturnSelf",
      "compose": [
        {
          "element": "myElement",
          "apply": "click",
          "returnType": "self"
        }
      ]
    }
  ]
}

Chain Compose Statements

It's possible to "chain" compose statements. A chain applies a method or a getter to the result of the previous statement.

Important: Chains are supported only if the previous statement returns a custom type (another page object).

To apply a method or a getter from the current statement to the result of the previous statement, use the "chain": true property inside a statement.

The chain methods approach has some disadvantages and limitations:

Here are some examples of chaining compose statements.

Compose container element with getter and public action

Consider the following test code samples:

const myModal = await utam.load(MyModalWithDynamicContent);

// long version
const footerArea = await myModal.getContent(FooterAreaWrapper);
const footerButtonsPanel = await footerArea.getFooterButtonsPanel();
await footerButtonsPanel.clickButtonByIndex(1);

// short version
await myModal.clickSave();

It's possible to compose the long version into a short version using a chain.

{
    "elements": [
    {
      "name": "content",
      "type": "container"
    }
  ],
  "methods": [
    {
      "name": "clickSave",
      "compose": [
        {
          // invoke container method with hardcoded type
          "element": "content",
          "args": [
            {
              "type": "pageObject",
              "value": "my/pageObjects/FooterAreaWrapper"
            }
          ],
          "returnType": "my/pageObjects/FooterAreaWrapper"
        },
        {
          // invoke getter
          "chain": true,
          "element": "footerButtonsPanel",
          "returnType": "my/pageObjects/FooterButtonsPanel"
        },
        {
          // invoke public method
          "chain": true,
          "apply": "clickButtonByIndex",
          "args": [
              {
                  "value": 1
              }
          ]
        }
      ]
    }
  ]
}

Chaining list to a list

If both previous and current statements return lists ("returnAll": true), the method is applied to each returned element using flatMap.

Consider the use case of a table with multiple rows and cells. Assume that each row and cell is a separate component. Let's write a method that returns all cells inside a table.

{
    "elements": [
        {
            "type": "my/pageObjects/tableCell",
            "name": "tableCells",
            "selector": {
                "css": "table-cell",
                "returnAll": true
            }
        }
    ]
{
    "elements": [
        {
            "type": "my/pageObjects/tableRow",
            "name": "tableRows",
            "selector": {
                "css": "table-row",
                "returnAll": true
            }
        }
    ],
    "methods": [
        {
            "name": "getAllCells",
            "compose": [
                {
                    "returnType": "my/pageObjects/tableRow",
                    "returnAll": true,
                    "element": "tableRows"
                },
                {
                    "chain": true,
                    "returnType": "my/pageObjects/tableCell",
                    "returnAll": true,
                    "element": "tableCells"
                }
            ]
        }
    ]
}

Generated Java code:

public final List<TableCell> getAllCells() {
    List<TableRow> statement0 = this.getTableRowsElement();
    List<TableCell> statement1 =
        statement0
            .stream()
            .flatMap(element -> element.getTableCells().stream())
            .collect(Collectors.toList());
    return statement1;
}

Argument Reference

To access an argument passed to a compose method in multiple statements, declare a reusable argument at the method level. The args property must be a sibling of the name property. For example, we declare a reusable passwordStr argument at the method level:

{
    "name": "passwordStr",
    "type": "string"
}

Then in each compose statement where you want to access the reusable argument, set the argument's type to argumentReference, the name of an argument with "type": "argumentReference" must match the name of one of the reusable arguments declared at the method level:

{
    "name": "passwordStr",
    "type": "argumentReference"
}

Here's the full method declaration. The statements that apply actions to the password and passwordConfirmation elements both declare a reference to the reusable passwordStr argument. When test code invokes this method, the passwordStr argument is passed to both statements:

{
    "name": "confirmPassword",
    "args": [
        {
            "name": "passwordStr",
            "type": "string"
        }
    ],
    "compose": [
        {
            "element": "password",
            "apply": "clearAndType",
            "args": [
                {
                    "name": "passwordStr",
                    "type": "argumentReference"
                }
            ]
        },
        {
            "element": "passwordConfirmation",
            "apply": "clearAndType",
            "args": [
                {
                    "name": "passwordStr",
                    "type": "argumentReference"
                }
            ]
        }
    ]
 }

A reusable argument can't be an argument with a hardcoded value, also known as a literal type.

For a working example of argument references, see this tutorial.

Basic Actions

Basic elements support different public methods depending on the array of type options set for the element.

The following sections list the methods available for each type.

Base Element Actions

These methods are available by default for all elements.

ActionSummary
containsElementReturns true is current element contains element with selector provided as a parameter. Second optional parameter is a boolean to expand element's shadowRoot when looking for the element.
getAttributeReturns the string value of a given attribute.
getClassAttributeShortcut for getAttribute("class"), in Java actual method is called getClassAttribute().
getTextReturns a string with the innerText of an element.
getTitleShortcut for getAttribute("title").
getValueReturns the value of an input element's value property.
isEnabledReturns true if the element is enabled.
isFocusedReturns true if the element has focus at the moment.
isPresentReturns true if the element is found in the DOM.
isVisibleReturns true if the element is visible.
waitForWaits for function that is provided as a parameter to return truthy value. UTAM version of Selenium wait
waitForAbsencePolling wait action—waits for an element to be absent (not-attached to the DOM) within a timeout. Throws an exception after the timeout.
waitForInvisiblePolling wait action—waits for the element to become not displayed within the timeout. Expects the element to be present while checking its visibility.
waitForVisiblePolling wait action—waits for the element to be visible within a timeout. Throws an error if the element isn’t present.

Actionable Type Actions

These methods are available if the element's type array includes actionable.

ActionSummary
blurApplies a blur action to the element.
focusApplies a focus action to the element. The focused element is the default element to receive keyboard or similar events.
moveToMoves the mouse to the middle of the element—also known as mouseover.
scrollToCenterScrolls the current element to the center of the screen by executing JavaScript arguments[0].scrollIntoView({block:'center'})
scrollToTopScroll to the element by executing JavaScript return arguments[0].scrollIntoView(true);

Clickable Type Actions

The clickable type enables a click event on the element.

Editable Type Actions

These methods are available if the element's type array includes editable.

ActionSummary
clearIf this element is a text entry element, the method clears the value. Otherwise it throws an exception.
clearAndTypeCombines clear and setText: clears an element's value and replaces it with a provided string.
pressPresses a key. Pass a key name: press('Enter').
setTextSimulate typing text inside an element, which may set its value.

Draggable Type Actions

These methods are available if the element's type array includes draggable.

ActionSummary
dragAndDropClicks on an element, and holds and drops it at the target location. The target location is defined by another basic element. If the second optional durationSec parameter is set, pause during the hold. Example: dragAndDrop(myElement, 10).
dragAndDropByOffsetClicks on an element, and holds and drops it at the target location. The target location is determined by x and y offset coordinates relative to the current element. If the third optional durationSec parameter is set, pause during the hold. Example: dragAndDropByOffset(400, 300, 10).

Touchable Type Actions

Supported only for Mobile platform and in UTAM Java only.

These methods are available if the element's type array includes touchable.

ActionSummary
flickFlicks on the touch screen using finger motion events. The start point is the middle of the element. The end point is determined by x and y offset coordinates relative to the current element. Example: flick(100, 100).

Supported Argument Types

This table shows the syntax for declaring a non-literal argument with a anme and type. Instead of using a combination of name and type, it's also possible to use a hardcoded (literal) value for an argument of these types.

Name/type (non literal)Value/type (literal)
string primitive{ "name": "myArg", "type": "string" }{ "value": "myString" }
integer primitive{ "name": "myArg", "type": "number" }{ "value": 1024 }
boolean primitive{ "name": "myArg", "type": "boolean" }{ "value": false }
locator{ "name": "myArg", "type": "locator" }{ "value": { "css": ".myClass" }, "type": "locator" }
element referencenot supported{ "type": "elementReference", "value": "myElement" }
page object type{ "name": "myArg", "type": "pageObject" }{ "value": "my/page/object", "type": "pageObject" }
root page object type{ "name": "myArg", "type": "rootPageObject" }{ "value": "my/page/object", "type": "rootPageObject" }
frame element{ "name": "myArg", "type": "frame" }same as element reference
functionnot supported{ "type": "function", "predicate": [...] }

Compose Container Invocation

A pageObject type parameter can be used to invoke a container method inside a compose statement.

{
    "elements": [
        {
            "name": "containerElement",
            "type": "container"
        }
    ],
    "methods": [
        {
            "name": "composeContainerHardcoded",
            "compose": [
                {
                    "element": "containerElement",
                    "args": [
                        {
                            "type": "pageObject",
                            "value": "utam-tests/pageObjects/myPageObject"
                        }
                    ],
                    "returnType": "utam-tests/pageObjects/myPageObject"
                }
            ]
        },
        {
            "name": "composeContainer",
            "compose": [
                {
                    "element": "containerElement",
                    "args": [
                        {
                            "type": "pageObject",
                            "name": "pageObjectCtor"
                        }
                    ]
                }
            ]
        }
    ]
}

Generated JavaScript code:

// declaration
composeContainerHardcoded(): Promise<unknown>;
composeContainer<T extends _UtamBasePageObject>(pageObjectCtor: _PageObjectCtor<T>): Promise<unknown>;

// implementation
import _MyPageObject from 'utam-tests/pageObjects/myPageObject';

async composeContainerHardcoded() {
    const _result0 = await this.__getContainer(_MyPageObject);
    return _result0;
}

async composeContainer(pageObjectCtor) {
    const _result0 = await this.__getContainer(pageObjectCtor);
    return _result0;
}

Compose Entering Frame

Frame and Root Page Object type parameters can be used to compose entering a frame:

{
    "elements": [
        {
            "name": "frameElement",
            "type": "frame",
            "selector": {
                "css": "#frame"
            }
        }
    ],
    "methods": [
        {
            "name": "composeEnterFrameHardcoded",
            "compose": [
                {
                    "element": "document",
                    "apply": "enterFrameAndLoad",
                    "args": [
                        {
                            "type": "elementReference",
                            "value": "frameElement"
                        },
                        {
                            "type": "rootPageObject",
                            "value": "utam-tests/pageObjects/frameArea",
                        }
                    ]
                }
            ]
        },
        {
            "name": "composeEnterFrame",
            "compose": [
                {
                    "element": "document",
                    "apply": "enterFrameAndLoad",
                    "args": [
                        {
                            "type": "frame",
                            "name": "frameElementParameter"
                        },
                        {
                            "type": "rootPageObject",
                            "name": "pageObjectCtor",
                        }
                    ]
                }
            ]
        }
    ]
}

Generated JavaScript code:

// declaration
composeEnterFrame<T extends _UtamBaseRootPageObject>(frameElementParameter: _FrameUtamElement, pageObjectCtor: _RootPageObjectCtor<T>): Promise<unknown>;
composeEnterFrameHardcoded(): Promise<unknown>;

// implementation
import _FrameArea from 'utam-tests/pageObjects/frameArea';

async composeEnterFrame(frameElementParameter, pageObjectCtor) {
    const _statement0 = await this.getDocument();
    const _result0 = await _statement0.enterFrameAndLoad(frameElementParameter, pageObjectCtor);
    return _result0;
}

async composeEnterFrameHardcoded() {
    const _statement0 = await this.getDocument();
    const _result0 = await _statement0.enterFrameAndLoad(await this.getFrameElement(), _FrameArea);
    return _result0;
}

Element Reference

The elementReference type in an argument allows you to reference an existing element as a parameter value. This example uses an elementReference to a targetElement element.

{
    "elements": [
        {
            "name": "sourceElement",
            "selector": {
                "css": ".source"
            },
            "type": ["draggable"]
        },
        {
            "name": "targetElement",
            "selector": {
                "css": ".source"
            }
        }
    ],
    "methods": [
        {
            "name": "composeDragAndDrop",
            "compose": [
                {
                    "element": "sourceElement",
                    "apply": "dragAndDrop",
                    "args": [
                        {
                            "type": "elementReference",
                            "value": "targetElement"
                        }
                    ]
                }
            ]
        }
    ]
}

Generated JavaScript code:

// declaration
composeDragAndDrop(): Promise<unknown>;

// implementation
async composeDragAndDrop() {
    const _statement0 = await this.__getSourceElement();
    const _result0 = await _statement0.dragAndDrop(await this.__getTargetElement());
    return _result0;
}

If a referenced element has a selector or filter arguments, those values are automatically inferred and added to the method declaration.

{
      "name": "target",
      "selector": {
        "css": "foo:nth-child(%d)",
        "args": [
            {
                "name": "index",
                "type": "number"
            }
        ]
      }
}

When the element is referenced in a compose statement, this page object hard codes the value of 1 for an index.

{
    "element": "source",
    "apply": "dragAndDrop",
    "args": [
        {
            "value": "target",
            "type": "elementReference",
            "args": [
                {
                    "value": 1
                }
            ]
        }
    ]
}

Explicit Waits

We define an explicit wait by setting the apply value to waitFor, which is a public method that exercises a fluent wait for an element. The wait wraps one or more functions and repeatedly invokes them within a timeout until they return a truthy value (boolean true or any value that isn't null or undefined).

The function invoked inside waitFor is defined in the args array as a parameter for a method used in a compose statement:

The predicate value is a list of compose statements that will be executed until truthy value is returned (no error, not null and not false).

{
    "apply": "waitFor",
    "args": [
        {
            "type": "function",
            "predicate": [
                {
                  "element": "foo",
                  "apply": "click"
                }
            ]
        }
    ]
}

Return type inside predicate

If a compose statement is a "waitFor" action or an explicit wait, the return type is the return type of the last predicate statement. Similarly to a regular statement, the return type can be inferred, void, or set explicitly. Here are some examples.

{
    "methods": [
        {
            "name": "returnSelf",
            "compose": [
                {
                    "apply": "waitFor",
                    "args": [
                        {
                            "type": "function",
                            "predicate": [
                                {
                                    "element": "root",
                                    "apply": "isPresent",
                                    "returnType": "self"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}
{
    "elements": [
        {
            "name": "custom",
            "selector": {
                "css": "my-custom-element"
            },
            "type": "my/custom/element"
        }
    ],
    "methods": [
        {
            "name": "returnCustomElement",
            "compose": [
                {
                    "apply": "waitFor",
                    "args": [
                        {
                            "type": "function",
                            "predicate": [
                                {
                                    "element": "custom"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}
{
    "elements": [
        {
            "name": "custom",
            "selector": {
                "css": "my-custom-element"
            },
            "type": "my/custom/element"
        }
    ],
    "methods": [
        {
            "name": "returnCustomElement",
            "compose": [
                {
                    "apply": "waitFor",
                    "args": [
                        {
                            "type": "function",
                            "predicate": [
                                {
                                    "element": "custom"
                                },
                                {
                                    "chain": true,
                                    "apply": "customElementPublicMethod",
                                    "returnType": "string",
                                    "returnAll": true
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

Important: setting a returnType outside a predicate isn't allowed. This page object throws an error:

{
    "methods": [
        {
            "name": "clickAndReturn",
            "compose": [
                {
                    "apply": "waitFor",
                    "args": [
                        {
                            "type": "function",
                            "predicate": [
                                {
                                    "element": "root",
                                    "apply": "click"
                                }
                            ]
                        }
                    ],
                    "returnType": "self"
                }
            ]
        }
    ]
}

beforeLoad

The beforeLoad array sets the criteria to be satisfied before the load method completes. If you don't specify a beforeLoad array, the load method finds a root element for a regular page object, or waits for the root element to be present for a root page object), by default.

Note: Statements inside the beforeLoad array can reference only root or document elements because other elements are not loaded yet.

This ApplicationHome page object waits for the DOM to finish rendering before loading is complete and then waits for the root and indicator elements:

{
  "selector": {
    "css": "application-home"
  },
  "root": true,
  "beforeLoad": [
    {
      "element": "document",
      "apply": "waitForDocumentReady"
    },
    {
      "apply": "waitFor",
      "args": [
        {
          "type": "function",
          "predicate": [
            {
              "element": "root",
              "apply": "isPresent"
            },
            {
              "element": "root",
              "apply": "containsElement",
              "args": [
                {
                  "type": "locator",
                  "value": {
                    "css": ".indicator"
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

document Object

The document object exposes methods to interact with the DOM, such as waiting until its state is ready or checking the URL. And methods for frames.

Reference the document object inside a compose statement in JSON using this syntax.

{
    "element": "document",
    "apply": "getUrl"
}

Here are the supported methods for the document object. The return value is asynchronous. For example, in JavaScript, a Promise is returned.

MethodDescriptionReturnsExample
waitFor<T>Waits for specified criteria to be satisfied.<T>waitFor Indicator Element
waitForDocumentReady()Waits for the document to be ready for JavaScript code to execute.booleanbeforeLoad
containsElement(locator: Locator)Checks whether the document contains an element with a matching selector.booleanwaitFor Indicator Element
getUrl()Gets the URL of the document.stringwaitFor DOM State
enterFrameenters a frame or iframe elementframe
enterFrameAndLoadenters a frame or iframe element and then loads a Page Object that is a new rootframe, pageObject
exitFrameexits focus from a current frame or iframe
exitToParentFrameexits focus from a frame or iframe to the immediate parent frame

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.

Declare imperative utility code in a compose statement. A page object can reference multiple utilities.

{
  "methods": [
    {
      "name": "myMethod",
      "compose": [
        {
          "applyExternal": {
            "type": "utam-lst/utils/lst/relatedListContainerUtils",
            "invoke": "cardUtility1",
            "args": [
              {
                "name": "stringArg",
                "type": "string"
              }
            ]
          }
        },
        {
          "applyExternal": {
            "type": "utam-lst/utils/lst/relatedListContainerUtils",
            "invoke": "cardUtility2",
            "args": [
              {
                "name": "booleanArg",
                "type": "boolean"
              }
            ]
          }
        }
      ]
    }
  ]
}

An imperative extension in a compose statement has these properties:

For examples of Java and JavaScript code implementations, see Imperative Extensions.

Platform Context

In a hybrid mobile application, some pages are WebView and some pages are native. WebView allows you to display web content inside a native app. A WebView page leverages the device's browser engine to render HTML and process JavaScript locally. A native page is built with a platform SDK such as iOS or Android, using tools and languages for the specific platform.

Appium is an open-source tool for automating native, mobile web, and hybrid applications. Appium allows you to write tests against multiple platforms, such as iOS and Android, using the same API. For a hybrid mobile application test using Appium, the driver needs to enter the appropriate Appium context: WebView context for a WebView page or native context for a native page. For more details, see appium.io: Appium Context.

The platform field at the root of a page object defines the page context type. The value can be:

The UTAM framework uses the platform property to set WebView or native context at the page object bootstrap stage. Then, a test can interact appropriately with elements on the page.

Here's the JSON for a WebView page object (platform defaults to web):

{
  "selector": {
    "css": "div#main"
  },
  "elements": []
}

Here's the JSON for a native page object on Android:

{
  "platform": "native",
   "selector": {
      // native selector here
   },
  "elements": []
}

Interfaces

UTAM use interfaces to abstract page object APIs (declared public methods) from the implementation. Interfaces are for Java only.

Interface Declaration

Declare an interface in a separate JSON file.

To distinguish an interface from a regular page object, set the interface property to true.

{
  "interface": true,
  "methods": []
}

Declare an interface's API as an array of objects in the methods property. Each method object has the following properties:

Here's an interface with four methods.

{
  "interface": true,
  "methods": [
    {
        "name": "openMenuSection"
    },
    {
        "name": "clickTabBarItem",
        "args": [
            {
                "name": "item",
                "type": "string"
            }
        ]
    },
    {
        "name": "hasTabBarItem",
        "returnType": "boolean",
        "args": [
            {
                "name": "item",
                "type": "string"
            }
        ]
    },
    {
        "name": "getTabBarItems",
        "returnType": ["clickable"],
        "returnAll": true
    }
  ]
}

For every declared interface, the UTAM Java compiler generates a Java interface with method declarations:

public interface NavTabBar extends RootPageObject {

  void openMenuSection();

  void clickTabBarItem(String item);

  Boolean hasTabBarItem(String item);

  List<GetTabBarItemsElement> getTabBarItems();

  interface GetTabBarItemsElement extends Clickable {}
}

Interface Implementation

A page object declares that it implements a declarative interface by using the implements property.

{
  "implements": "utam-lightning/pageObjects/global/navigationMenu",
  "elements": [],
  "methods": []
}

The page object must implement all methods declared in the original interface.

To support multiple implementing JSON page objects for a single UTAM interface, use the profile property. This property enables UTAM to choose the appropriate implementation depending on the test context. Profile is an array of objects. Set a platform property in profile to define the supported platforms. The supported values for platform are:

For example, addConnAndroidImpl.utam.json defines a shared implementation for Android phone and tablet.

{
   "implements":"utam-salesforceapp/pageObjects/authentication/addConn",
   "profile":[
      {
         "platform": ["android_phone", "android_tablet"]
      }
   ],
   ....
}

addConnAndroidPhoneImpl.utam.json is for Android phone only.

{
   "implements":"utam-salesforceapp/pageObjects/authentication/addConn",
   "profile":[
      {
         "platform": "android_phone"
      }
   ],
   ....
}

addConniOSImpl.utam.json is for iOS phone and tablet.

{
   "implements":"utam-salesforceapp/pageObjects/authentication/addConn",
   "profile":[
      {
         "platform": ["ios_phone", "ios_tablet"]
      }
   ],
   ...
}

addConniOSTabletImpl.utam.json is for iPad only.

{
   "implements":"utam-salesforceapp/pageObjects/authentication/addConn",
   "profile":[
      {
         "platform": "ios_tablet"
      }
   ],
   ...
}

In the compiler configuration file, you must configure all possible profile values for your module. Otherwise, the compiler throws an error, such as:

utam.core.framework.consumer.UtamError: profile 'platform' does not support value 'android_tablet'

Here's an example of a Java compiler configuration file in compiler.config.json with a profiles property. The configuration file name can be anything. It must just match the name passed as a runtime parameter to the compiler.

{
  ....
  "profiles": [
    {
      "name": "platform",
      "values": [
        "ios_phone",
        "ios_tablet",
        "android_phone",
        "android_tablet"
      ]
    }
  ]
}

The compiler configuration file defines the supported platform values.

For more details on Java compiler configuration, see Configure the Java Compiler.