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:
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.