Edition 114

How (And Why) To Create a Custom Expected Condition

Automating the Zoom app

One of the issues I ran into in the Automation Happy Hour featuring Zoom (part 2), was that the Zoom UI disappears during a meeting, making it hard to tap on buttons in the UI. The way that a user would get around this is of course to (petulantly) tap the middle of the screen, which triggers the app to re-show the UI.

Given that we don't know which state the UI will be in when we want to interact with it, we're in a bit of a pickle with our automation. If we try to use a simple WebDriverWait for a UI element, it will fail, because the UI will never show up unless we take an action. However, if we blindly take an action without checking the state first, then we run the risk of actually hiding the UI instead of showing it (because maybe it was already present!).

One solution to this problem would be to create a helper method that wraps the logic for checking if the UI is present, and performs the appropriate tap if not:

void ensureUIPresent() {
    try {
        driver.findElement(UI_ELEMENT);
    } catch (NoSuchElementException ign) {
        driver.findElement(SCREEN).click();
    }
}

In this simplistic logic, we first check if the appropriate UI element is present. In the Zoom app, this could be the "Leave" button, for example, which is always visible whenever the UI is present. If we can't find this element, then we find some large-scale screen element, and tap it (assuming we can find an element whose center is precisely in the middle of the screen). Then, we use this helper anywhere we want to be sure the UI is present! Pretty simple. It might be a little too simple, in fact--what if the UI is in the middle of transitioning? You could image we run into a situation where we tap the "SCREEN" element to show the UI, and pass control back to our caller, who then tries to use a UI element while the UI is still busy coming out of hiding.

And there is a more elegant way to wrap up this solution in general, which is to create something called an ExpectedCondition. You might be familiar with ExpectedConditions from using WebDriverWait. The Appium / Selenium Java client has a bunch of built in ExpectedConditions that you can use, the most common of which is presenceOfElementLocated:

wait.until(ExpectedConditions.presenceOfElementLocated(LOCATOR));

We can also make our own ExpectedConditions! All we have to do is return a class that overrides the appropriate method. Let's take our previous ensureUIPresent method and turn it into one of these ExpectedConditions.

private ExpectedCondition<Boolean> ZoomUIPresent() {
    return new ExpectedCondition<Boolean>() {
        @Override
        public Boolean apply(WebDriver driver) {
            try {
                driver.findElement(LEAVE_BTN);
                return true;
            } catch (NoSuchElementException ign) {
                driver.findElement(CONTENT).click();
            }
            return false;
        }
    };
}

Here I've created an ExpectedCondition wrapper called ZoomUIPresent. In the returned condition, we override the apply method to return a true or false value. The WebDriver wait object will know how to use these return values to keep trying to wait for the expected condition to return true! The logic here is pretty straightforward:

  1. Try and find the 'Leave' button (which, if found, proves the UI is present)
  2. If we can find it, return true because we have satisfied our condition
  3. If we can't find it, click the 'content' element (which should trigger a tap to the center of the screen), and return false
  4. If we return false, the condition will run again until we return true!

(Note that I have used findElement for simplicity, but we should probably be using a short WebDriverWait instead, to handle the potential race condition I described above).

Now, to use this new expected condition, we just toss it into WebDriverWait.until()!

// do some stuff
wait.until(ZoomUIPresent());
// do some other stuff now that we know the UI is present

So this is a real world example of how and why you might want to create your own app-specific expected condition! Have you created your own expected conditions in the past? Let me know how you used them to make your own automation framework more functional and elegant!