Explicit Waits

Many page objects and UI tests rely on large implicit timeout values to find an element that appears as the result of a user action. This approach can be problematic and can often lead to flappy tests due to changes in the testing environment or network delays. Instead, write more durable tests by waiting for certain actions to complete and yield until the conditions are met. Instead of waiting for an implicit time, wait for a condition to be ready (explicit wait) for the test to proceed.

Grammar

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 a truthy value is returned (no error, not null, and not false).

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

The second parameter "value" : "custom error message" is optional and represents an error message that will be thrown if the action times out. By default, the error message is "Timeout after 20 seconds". A custom message makes the error more meaningful if a UI test fails.

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.

{
    "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": [
                                {
                                    "apply": "getString"
                                }
                            ]
                        }
                    ],
                    "returnType": "string"
                }
            ]
        }
    ]
}

Explicit wait timeout

You can set timeout values in UTAM for implicit and explicit waits for Java and JavaScript.

An implicit timeout is a time duration used to find an element inside its parent element or inside the driver for a root page object.

Note: Because we feel strongly that implicit timeout should be zero, as of version 1.0.0, implicit timeouts in UTAM are set to 0. Although we can't prevent usage of implicit timeouts and we still support the ability to override them, we strongly discourage their usage.

An explicit wait timeout is used for any waitFor method inside UTAM framework, for example waitFor is used inside compose and beforeLoad in JSON, or can be invoked on a page object in a test. Explicit timeout duration depends on many factors such as application performance and network access to the environment where tests are being run. It should be set to the maximum reasonable duration for which a test should wait for the server to respond and for the browser to finish rendering the page. It can be as short as 5000 ms and as long as 90 seconds.

If timeouts aren’t set, the default for implicit timeouts is 0 and explicit timeouts is 20 seconds.

For more information on how implicit and explicit timeouts affect UI tests, see:

Here are some examples of using explicit waits.

Use Explicit Wait instead of a Retry

Consider this Java unit test code snippet that retries an attempt to click a button:

    try {
        button.click();
        Thread.sleep(1000);
    } catch (Exception e) {
        button.click();
    }

This code pattern usually means that the button isn't clickable initially and there’s a waiting period for an element to be in the state to accept such actions.

Here's a better pattern: wrap the click action into an explicit wait.

{
    "elements" : [
        {
            "name": "button",
            "type": "clickable",
            "selector": { "css": ".not-yet-clickable" }
        }
    ],
    "methods" : [
        {
            "name" : "waitAndClick",
            "compose" : [
                {
                    "apply" : "waitFor",
                    "args" : [
                        {
                        "type": "function",
                        "predicate": [
                                {
                                    "element": "button",
                                    "apply" : "click"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

Wait Before Assertion

Consider this Java unit test code snippet that repeatedly attempts to assert displayed text while the text is being loaded.

    while(iterations < 10) {
        try {
            Thread.sleep(1000);
            if(fieldWithText.getText().equals("my expected text")) { return; }
        } catch (Exception e) {
            iterations ++;
        }
    }

Instead of hardcoding retry and sleep steps in a loop, add a getLoadingText method that accepts expected text as a parameter and uses an explicit wait to wait for the text to match.

{
  "elements" : [
    {
      "name": "fieldWithText",
      "selector": { "css": ".loading" }
    }
  ],
  "methods" : [
    {
      "name" : "getLoadingText",
      "compose" : [
        {
          "apply" : "waitFor",
          "args" : [
            {
              "type": "function",
              "predicate": [
                {
                  "element": "fieldWithText",
                  "apply" : "getText",
                  "matcher" : {
                    "type" : "stringEquals",
                    "args" : [
                      {
                        "name" : "expectedText",
                        "type" : "string"
                      }
                    ]
                  }
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Now the Java test code can look like this:

assert pageObject.getLoadingText("my expected text");

Wait for a Container Element

What if we need to wait for a container element? A container element is a placeholder for any internal component known only at runtime. Here's a container declaration:

{
    "elements": [
        {
          "name": "panelContainer",
          "type": "container",
          "public": true
        }
    ]
}

To wait for a container element to be loaded, we can use one of the following approaches:

  1. Add a waitForAnyPanelContent compose method that waits for any element inside a container using containsElement. This method is universal because it waits for any child element, so it's the preferred option. If waiting for the universal selector doesn't work for some reason, the page object needs to wait for a particular selector, as described in the example for the next approach.
{
    "methods" : [
        {
            "name" : "waitForAnyPanelContent",
            "compose" : [
                {
                    "apply" : "waitFor",
                    "args" : [
                        {
                        "type": "function",
                        "predicate": [
                                {
                                    "element": "root",
                                    "apply" : "containsElement",
                                    "args" : [
                                        // this selector means any child
                                        {
                                            "type": "locator",
                                            "value" : { "css": ":scope > *:first-child" }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}
  1. Add a waitForMyComponentPanelContent compose method that waits for a particular selector using the same containsElement basic action.
{
    "methods" : [
        {
            "name" : "waitForMyComponentPanelContent",
            "compose" : [
                {
                    "apply" : "waitFor",
                    "args" : [
                        {
                        "type": "function",
                        "predicate": [
                                {
                                    "element": "root",
                                    "apply" : "containsElement",
                                    "args" : [
                                        // this selector is specific for MyComponent
                                        {
                                            "type": "locator",
                                            "value" : { "css": "my-component" }
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}
  1. If you need your method to return an instance of a particular page object inside a container, add a waitForMyComponentPanelContent compose method that invokes a container method wrapped in a wait with a hardcoded page object type. Then, the method will return an object of the given type:
{
    "elements": [
        {
          "name": "panelContainer",
          "type": "container"
        }
    ],
    "methods" : [
        {
            "name" : "waitForMyComponentPanelContent",
            "compose" : [
                {
                    "apply" : "waitFor",
                    "args" : [
                        {
                            "type": "function",
                            "predicate": [
                                {
                                    "returnType": "utam/pageObjects/MyComponent",
                                    "element": "panelContainer",
                                    "args": [
                                        {
                                            "type": "pageObject",
                                            "value": "utam/pageObjects/MyComponent"
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}