Nullable Versus isPresent

Consider these two cases that validate the absence of an element:

  1. Log in to an application with minimal access rights and check that a "Setup" button isn't present.
  2. After entering login credentials, check that the login form is no longer on the screen.

The key difference is that for the first use case, the button element was never in the DOM. We call it a nullable element. For the second use case, the login form element was present in the DOM but no longer is. We call it a stale element.

The following sections provide examples for both types of use cases.

Check nullable element presence

When a test invokes an element's getter method, the framework attempts to find the element and return its value. In the first use case, a test invokes the getter for the Setup button.

Unless the element is marked as nullable, an error is thrown if the element isn't found.

If the element is marked as nullable, the getter returns null if the element isn't found.

{
  "shadow": {
    "elements": [
      {
        "name": "setupButtonHTMLElement",
        "type" : "clickable",
        "public" : true,
        "nullable": true,
        "selector": { "css": "button.setup" }
      },
      {
        "name": "setupButtonCustomElement",
        "type": "utam-my/pageObjects/button",
        "public" : true,
        "nullable": true,
        "selector": { "css": "my-button" }
      }
    ]
  }
}

A test or an imperative extension can check or assert a getter's returned value for null and act accordingly.

JavaScript example:

const setupButton = await page.getSetupButtonHTMLElement();
const setupButtonCustom = await page.getSetupButtonCustomElement();
// assert for null
expect(setupButton).toBeNull();
expect(setupButtonCustom).toBeNull();
// OR check for null and apply action
if (setupButton) {
  // do something if element is present
  ...
}
if (setupButtonCustom) {
  // do something if element is present
  ...
}

Java example:

SetupButton setupButton = page.getSetupButtonHTMLElement();
Button setupButtonCustom = page.getSetupButtonCustomElement();
// assert for null
assert setupButton == null;
assert setupButtonCustom == null;
// OR check for null and pply action
if (setupButton != null) {
  // do something if element is present
  ...
}
if (setupButtonCustom != null) {
  // do something if element is present
  ...
}

If the elements weren't marked as "nullable": true, the page.getSetupButtonHTMLElement() or page.getSetupButtonCustomElement() getters would throw an exception if the element isn't present.

Check nullable list presence

The nullable behavior is the same for a list of elements.

{
  "shadow": {
    "elements": [
      {
        "name": "setupButtonHTMLElementsList",
        "type" : "clickable",
        "public" : true,
        "nullable": true,
        "selector": { "css": "button.setup", "returnAll" : true }
      },
      {
        "name": "setupButtonCustomElementsList",
        "type": "utam-my/pageObjects/button",
        "public" : true,
        "nullable": true,
        "selector": { "css": "my-button", "returnAll" : true }
      }
    ]
  }
}

A test or an imperative extension can check or assert a getter's returned value for null and act accordingly.

JavaScript example:

const setupButtons = await page.getSetupButtonHTMLElementsList();
const setupButtonsCustom = await page.getSetupButtonCustomElementsList();
// assert for null
expect(setupButtons).toBeNull();
expect(setupButtonsCustom).toBeNull();

Java example:

List<SetupButton> setupButtons = page.getSetupButtonHTMLElement();
List<Button> setupButtonsCustom = page.getSetupButtonCustomElement();
// assert for null
assert setupButtons == null;
assert setupButtonsCustom == null;

Compose checking presence of a nullable element

For better encapsulation, keep the element private and declare a compose method to check that its getter returns not null:

{
  "elements" : [
    {
      // same element, but not public
      "name": "setupButton",
      "nullable": true,
      "selector": { "css": "button.setup" }
    }
  ],
  "methods" : [
    {
      "name" : "isSetupButtonPresent",
      "compose" : [
        {
          "element" : "setupButton",
          "matcher" : {
            "type" : "notNull"
          }
        }
      ]
    }
  ]
}

Note the syntax in the compose method. We used element and omitted apply to call the getter method.

The generated isSetupButtonPresent method calls the getter for the setupButton element, checks if its value isn’t null, and returns a boolean value.

A test or an imperative extension can assert or check the element's presence and act accordingly.

if (await homePage.isSetupButtonPresent()) {
  // do something
}

Compose checking presence with containsElement

Another option is to check for an element's presence using the containsElement action first and then act accordingly.

{
  "elements" : [
    {
      // same element, but not public
      "name": "myButton",
      "type" : "clickable"
      "selector": { "css": "button.my" }
    }
  ],
  "methods" : [
    {
      "name" : "isMyButtonPresent",
      "compose" : [
        {
          "element" : "root",
          "apply" : "containsElement",
          "args" : [
            // first arg - selector to search for
            { "value" : { "css": "button.my" } },
            // second arg - expand or not the shadow root of the parent, false is default
            { "value" : false }
          ]
        }
      ]
    },
    {
      "name" : "clickMyButton",
      "compose" : [
        {
          "element" : "myButton",
          "apply" : "click"
        }
      ]
    }
  ]
}

Example of usage from a test:

if (await homePage.isMyButtonPresent()) {
  await clickMyButton();
}
if (homePage.isMyButtonPresent()) {
  clickMyButton();
}

isPresent returns false for a Stale Element

A getMyElement().isPresent() call returns true for an existing element or throws an error for a non-nullable absent element. The isPresent() call returns false when an element becomes absent or stale because the page was refreshed.

myForm = homePage.getForm();
// do something in the form until it's gone
assert myForm.isPresent() == false;

The isPresent call returns false when the form element became stale. Consider our second use case earlier where the login form is no longer on the screen after the login is complete.

Save an element instance in a variable

Remember that invoking an element's getter triggers finding the element inside its parent. To use isPresent, save an instance of the element before it becomes stale to avoid an error that the element isn't found when its getter is invoked. The myForm = homePage.getForm() calls saves the form before it becomes stale.

Calling isPresent on a nullable element can throw NullPointerException

If an element is marked as nullable, a test should check for a null value before calling any method, including isPresent, on it.

myForm = homePage.getForm();
// assuming the form is nullable
assert myForm != null && myForm.isPresent() == false;

Wait for absence of a stale element

If an element was present but it takes time for it to become absent, use waitForAbsence. For example, an application might do some processing after we save changes in a form but doesn't hide the form immediately.

const myForm = await homePage.getForm();
await myForm.waitForAbsence();
PageObject myForm = homePage.getForm();
myForm.waitForAbsence();

Wait for presence in beforeLoad

If a test must wait for a certain element to be present inside a page object root, it's a best practice to make the wait a part of loading the page object by overriding the page object's load method.

A page Object is "loaded" when a test either call UtamLoader.load(Class) or invokes a getter method that returns the page object from inside its enclosing page object.

In this example, the page object overrides page loading by waiting for an element with a given selector using the containsElement action:

{
  "beforeLoad" : [
    {
      "apply" : "waitFor",
      "args" : [
        {
          "type": "function",
          "predicate": [
            {
              "element" : "root",
              "apply" : "containsElement",
              "args" : [
                // first arg - selector to search for
                { "value" : { "css": "i-am-slow" } },
                // second arg - expand the shadow root of the parent
                { "value" : true }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Note: We can't use a nullable element in this example. A beforeLoad statement can invoke methods only against root or a document object because the page isn't fully loaded yet.

The predicate in the waitFor statement has to return a truthy value (not null, not false and doesn’t throw) for the method to complete without an error.

In our example, the root element should contain an element with the given selector inside its shadowRoot.

Wait for nullable element presence

In some cases, it can take some time for an element to render. Such a wait isn't always allowed in a beforeLoad override such as in the previous example. For example, the element might not be inside root or its loading might not be stateless.

{
  "elements" : [
    {
      "name": "parent",
      "selector": { "css": ".containsSlowElement" },
      "elements" : [
        {
          "name": "slowElement",
          "selector": { "css": "i-am-slow" }
        }
      ]
    }
  ],
  "methods" : [
    {
      "name" : "waitForSlowElement",
      "compose" : [
        {
          "apply" : "waitFor",
          "args" : [
            {
              "type": "function",
              "predicate": [
                {
                  "element": "slowElement"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

The predicate in the waitFor statement has to return a truthy value (not null, not false and doesn’t throw) for the method to complete without an error.

In this example, the method repeatedly calls the slowElement getter until it stops throwing an error that element isn't found or until a timeout is reached.

Wait for nullable list presence

To wait for a list of elements, we can use the same approach as in the previous example. The only exception is that the element is marked with returnAll. This example also uses a custom type:

{
  "elements" : [
    {
      "name": "parent",
      "selector": { "css": ".containsSlowElement", "returnAll" : true  },
      "elements" : [
        {
          "name": "slowCustomElements",
          "selector": { "css": "i-am-slow" },
          "type": "utam-my/pageObjects/custom",
        }
      ]
    }
  ],
  "methods" : [
    {
      "name" : "waitForSlowElementsList",
      "compose" : [
        {
          "apply" : "waitFor",
          "args" : [
            {
              "type": "function",
              "predicate": [
                {
                  "element": "slowCustomElements"
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

As in the previous example, this method repeatedly calls the slowCustomElements getter until it stops throwing an error that the element isn't found or until a timeout is reached.