Testing toast messages with UI Automator

In this post I will show how to test toast messages in Android using bare UI Automator.

If you are using Appium then you can skip this post, because with Appium a toast should be easily found using xpath “/hierarchy/android.widget.Toast”.

Recently I had to test that our app shows a toast message. Having no prior experience with testing toast messages with UI Automator, I’ve googled it and a few answers on SO recommending to use xpath “/hierarchy/android.widget.Toast” for obtaining a toast element. Sadly, it didn’t work. I also couldn’t find the toast in UI Automator Viewer, which explained why the locator didn’t work.

By that time a had two guesses, both of which I found later to be correct – the first one was that Appium implements some special handler for toasts, and the second one was that UiAutomation::setOnAccessibilityEventListener() is used for that.

I found that Appium handles toasts in class NotificationListener and the way it is done is quite ingenious. They set a listener for accessibility events, and after event TYPE_NOTIFICATION_STATE_CHANGED is fired they add the toast to the DOM for 3.5 seconds (which is, non-coincedentally, a duration of toast created with duration LENGTH_LONG). That is why it can be found with the xpath above. After 3.5 s pass, they remove the toast from the DOM.

This solution would be impossible to use as-is in our framework, so I modified it to simply start a listener before a test scripts performs an action which should cause the toast message to be displayed, then wait for toast, check its contents and then remove the listener.

   private boolean isToastDisplayed(String text, Runnable action) {
        toastDisplayed = false;
        long startTimeMs = System.currentTimeMillis();
        Log.d(TAG, "isToastDisplayed: start waiting");
        InstrumentationRegistry.getInstrumentation().getUiAutomation()
                .setOnAccessibilityEventListener((event) -> {
                    Log.d(TAG, "isToastDisplayed: event");
                    if (event.getEventType() == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED
                            && PACKAGE_NAME.equals(event.getPackageName().toString())
                            && event.getClassName().toString().contains(android.widget.Toast.class.getName())
                            && ("[" + text + "]").equals(event.getText().toString())
                    )
                    {
                        Log.d(TAG, "isToastDisplayed: TOAST");
                        toastDisplayed = true;
                    }
                    event.recycle();
                });
        action.run();
        while (!toastDisplayed && System.currentTimeMillis() - startTimeMs < 10000) {
            Log.d(TAG, "isToastDisplayed: sleep");
            SystemClock.sleep(500); }
        InstrumentationRegistry.getInstrumentation().getUiAutomation().setOnAccessibilityEventListener(null);
        return toastDisplayed;
    }

And this is how an assertion looks like:

@Test
public void testToast() {
    launchApp();
    assertTrue("Toast was not displayed", isToastDisplayed("toast test", () -> {
        device.findObject(By.res(PACKAGE_NAME + ":id/fab")).click();
    }));

}

Full source code of the test class can be found here.

As always, don’t hesitate to ask me a question or just leave a comment below.

One thought on “Testing toast messages with UI Automator

  1. Mohit 2024-03-27 / 20:47

    it is not working for me

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.