Compose Methods

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.

Method Properties

A compose method has these properties:

Method Descriptions

A compose method or element can have a description that explains its usage, return value, and parameters. The description is used to generate JavaDoc or JSDoc.

The simplified description format is one string property:

{
    "methods": [
        {
            "name": "myMethod",
            "description": "Gets an attribute of the root element",
            "compose": [
                {
                    "element": "root",
                    "apply": "getAttribute",
                    "args": [
                        {
                            "name": "attrName",
                            "type": "string"
                        }
                    ]
                }
            ]
        }
    ]
}

The generated method includes the following Javadoc or JSDoc. By default, the @return tag contains the inferred return type and the @param tag has the name and type of the parameter:

/**
 * Gets an attribute of the root element
 *
 * @return String
 * @param attrName String
 */

The extended object description format enables you to describe more information, such as the return value:

{
    "methods": [
        {
            "name": "myMethod",
            "description": {
                "text": ["Gets an attribute of the root element"],
                "return": "string with an attribute value",
                "throws": "NullPointerException if the attribute name is null",
                "deprecated": "in Summer '22 release"
            },
            "compose": [
                {
                    "element": "root",
                    "apply": "getAttribute",
                    "args": [
                        {
                            "name": "attrName",
                            "type": "string",
                            "description": "string with attribute name"
                        }
                    ]
                }
            ]
        }
    ]
}

To provide a description for the parameter, we added a description property for the attrName argument. The description is only possible for non-literal (not hardcoded by value) arguments.

The generated method has the following Javadoc or JSDoc:

/**
 * Gets an attribute of the root element
 *
 * @return string with attribute value
 * @param attrName string with attribute name to get
 * @throws NullPointerException if the attribute name is null
 * @deprecated in Summer '22 release
 */

In Java, the generated method is marked with an @Deprecated annotation.

The same description format can be added to any element:

{
    "elements": [
        {
            "public": true,
            "name": "custom",
            "type": "utam/pageObjects/MyCustomObject",
            "description": "get area inside table",
            "selector": {
                "css": "css%s",
                "args": [
                    {
                        "name": "selectorArg",
                        "type": "string",
                        "description": "parameter description"
                    }
                ]
            }
        }
    ]
}

Return Types

See Method Return Types.

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;
}

Matchers

Use a matcher to transform the return value of a compose statement.

This compose statement uses a matcher to return a non-null value.

{
    "name": "matcherNotNull",
    "compose": [
        {
            "element": "single",
            "apply": "getAttribute",
            "args": [
                {
                    "value": "\"readonly\""
                }
            ],
            "matcher": {
                "type": "notNull"
            }
        }
    ]
}

For more information on matchers, see Element Filters: Matchers.

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;
}

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",
                    "returnType": "utam-tests/pageObjects/frameArea",
                    "apply": "enterFrameAndLoad",
                    "args": [
                        {
                            "type": "elementReference",
                            "value": "frameElement"
                        },
                        {
                            "type": "rootPageObject",
                            "value": "utam-tests/pageObjects/frameArea"
                        }
                    ]
                }
            ]
        },
        {
            "name": "composeEnterFrame",
            "compose": [
                {
                    "element": "document",
                    "returnType": "rootPageObject",
                    "apply": "enterFrameAndLoad",
                    "args": [
                        {
                            "type": "frame",
                            "name": "frameElementParameter"
                        },
                        {
                            "type": "rootPageObject",
                            "name": "pageObjectCtor"
                        }
                    ]
                }
            ]
        }
    ]
}

Generated JavaScript code:

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

composeEnterFrameHardcoded(): Promise<_FrameArea>;
composeEnterFrame<T extends _UtamBaseRootPageObject>(frameElementParameter: _FrameUtamElement, pageObjectCtor: _PageObjectCtor<T>): Promise<T>;


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

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

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