Edition 64

Validating Android Toast Messages

If you've used Android apps for any length of time, you've no doubt noticed these little notifications that pop up and fade away with time:

Hello, Toast

These are called toast messages, and are an important tool for Android app designers, because they don't steal focus from the current activity. Your app might complete a background task while the user is playing a game, and with toasts you are able to convey this information without taking the user away from their present context.

Of course, toast messages can prove a challenge for automation, not just because of their ephemeral nature. From the perspective of the Android Accessibility layer, toast messages aren't visible! If you try to get the XML source from an Appium session while a toast is present on screen, you won't find its text anywhere. Luckily, with the advent of the Espresso driver, we have the ability to match text against on-screen toasts! Let's see how it all works.

First, we need a way to actually produce toast messages we can use for testing. I could add some behavior in my app that produces toasts, like a real app would, but instead I'm going to rely on another cool feature of the Espresso driver we've covered in the past -- calling app-internal methods. Since I've already got the plumbing hooked up in The App, I can just write myself a handy little helper method that will display toasts for me from my test code:

private void raiseToast(String text) {
    ImmutableMap<String, Object> scriptArgs = ImmutableMap.of(
        "target", "application",
        "methods", Arrays.asList(ImmutableMap.of(
            "name", "raiseToast",
            "args", Arrays.asList(ImmutableMap.of(
                "value", text,
                "type", "String"
            ))
        ))
    );

    driver.executeScript("mobile: backdoor", scriptArgs);
}

(Of course, in a real testing scenario, the toasts would be generated as a result of some app behavior). Once I've got toasts showing up, I need a way to check what they say for verification. Unfortunately, we don't have a method for getting the text from a toast message. What we have instead is a method which takes a string and tells us whether the on-screen toast matches that string or not. This is enough for our purposes of verification. So let's check out how to use the mobile: isToastVisible method:

ImmutableMap<String, Object> args = ImmutableMap.of(
    "text", "toast text to match",
    "isRegexp", false
);
driver.executeScript("mobile: isToastVisible", args);

Like all mobile: methods, we first need to construct a map of our arguments. This method takes two parameters: the text we want to look for, and a flag which tells Appium whether this text is in the form of a bare string or a regular expression. If we set isRegexp to true, then we can look for toast messages using more advanced criteria, limited only by what we can express in a regular expression. Finally, we call executeScript as the way of accessing the mobile: method.

This is great, but as we've mentioned already, toasts are a time-sensitive phenomenon. So we probably want to start looking for a matching toast before it pops up, so we're sure we don't miss it. To this end, we can use a custom Explicit Wait. It's possible to use the Java client's ExpectedCondition interface to define our own custom expected conditions, so that's what we'll do. Here's a helper method that defines a new ExpectedCondition called toastMatches:

public static ExpectedCondition<Boolean> toastMatches(String matchText, Boolean isRegexp) {
    return new ExpectedCondition<Boolean>() {
        @Override
        public Boolean apply(WebDriver driver) {
            ImmutableMap<String, Object> args = ImmutableMap.of(
                "text", matchText,
                "isRegexp", isRegexp
            );
            return (Boolean) ((JavascriptExecutor)driver).executeScript("mobile: isToastVisible", args);
        }

        @Override
        public String toString() {
            return "toast to be present";
        }
    };
}

All we do is override the appropriate methods of the ExpectedCondition class, and ensure we have appropriate typing in a few places, and we've got ourselves a nice self-contained way of waiting for toast messages, in conjunction with (for example) WebDriverWait. Since all the pieces are now in places, let's take a look at what our test method itself could look like:

@Test
public void testToast() {
    WebDriverWait wait = new WebDriverWait(driver, 10);

    final String toastText = "Catch me if you can!";
    raiseToast(toastText);

    wait.until(toastMatches(toastText, false));

    raiseToast(toastText);
    wait.until(toastMatches("^Catch.+!", true));
}

In this test, we define a WebDriverWait and use it with our toastMatches condition. You can see that we perform a match with both available modes, first by matching the exact toast string, and secondly by using a regular expression, highlighting how we could verify the presence of a valid toast message even if it contains dynamically generated content.

That's it! If you haven't checked out Appium's Espresso driver for Android, validating toast messages is a good reason to give it a try. And if you want to see a working example with all the boilerplate, you can find it on GitHub as always.