Element Types

Elements can have different types.

Basic Elements

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 Elements

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 Elements

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

There are certain requirements to declare a container element for a slot. For more information, see the guidelines in the slots guide.

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 on the scope element; 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 Elements

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.