Edition 38

Capturing Browser Errors and Logs in Android Web/Hybrid Apps

We've already seen how to capture browser logs for iOS; the same can be done for Android! The technique is largely similar but varies in some respects. As a reminder of why we might want to capture logs and errors from a browser, we could:

  1. Get under-the-hood information from our app that modulates the way our test works.
  2. Store the logs for future reference; we could give them to an app dev if a test fails, and they might be able to use the logs to pinpoint the nature of an otherwise confusing bug.

And of course, just as in the iOS world, we're not limited to capturing console logs; we could make sure any JavaScript errors that are thrown get attached to the console as well, so we track those too (using a little hack like window.onerror = console.error.bind(console) or similar).

But let's skip to the action. With iOS there was some complexity where we used a non-standard safariConsole log type, and had to parse some JSON to get the actual data, with Android we can take advantage of the fact that Chrome automation happens via Chromedriver, which supports the full WebDriver protocol all on its own. Given that, we can simply make a request for the browser log type:

driver.manage().logs().get("browser");

This returns a list of LogEntry objects, and for each one we can retrieve its message (and, say, print it to the terminal):

for (LogEntry entry : driver.manage().logs().get("browser")) {
    System.out.println(entry.getMessage());
}

That's all there is to it, in terms of the test code itself. However, we do need to use a special loggingPrefs capability to convince Chromedriver to turn on capturing of the browser logs for us. In it simplest form, the loggingPrefs capability is an object specifying log types and log levels. For our purposes it ends up looking like:

{"loggingPrefs": {"browser": "ALL"}}

But in Java-client-world, we can build up this little structure using classes and constants instead! That way our IDE can guide us to the correct structure without having to remember strings like "loggingPrefs":

LoggingPreferences logPrefs = new LoggingPreferences();
logPrefs.enable(LogType.BROWSER, Level.ALL);
capabilities.setCapability(CapabilityType.LOGGING_PREFS, logPrefs);

With this capability set, browser log retrieval will work, regardless of whether you're automating Chrome itself or a hybrid app. The code sample for this edition is basically the same as for the iOS equivalent, so I added a hybrid test just to show that it works equally well with hybrid apps (though of course we have to switch into the web context before making the call to get the browser logs). Here's the full example (which you can also view on GitHub):

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.MobileBy;
import io.appium.java_client.android.AndroidDriver;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.logging.Level;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.openqa.selenium.logging.LoggingPreferences;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class Edition038_Android_Web_Console {

    private AndroidDriver driver;
    private String APP_ANDROID = "https://github.com/cloudgrey-io/the-app/releases/download/v1.7.1/TheApp-v1.7.1.apk";
    private static By hybridScreen = MobileBy.AccessibilityId("Webview Demo");
    private static By urlInput = MobileBy.AccessibilityId("urlInput");

    @After
    public void tearDown() {
        if (driver != null) {
            driver.quit();
        }
    }

    @Nullable
    private String getWebContext(AppiumDriver driver) {
        ArrayList<String> contexts = new ArrayList(driver.getContextHandles());
        for (String context : contexts) {
            if (!context.equals("NATIVE_APP")) {
                return context;
            }
        }
        return null;
    }

    @Test
    public void testLogging_Chrome() throws MalformedURLException {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability("platformName", "Android");
        capabilities.setCapability("automationName", "UiAutomator2");
        capabilities.setCapability("deviceName", "Android Emulator");
        capabilities.setCapability("browserName", "Chrome");
        LoggingPreferences logPrefs = new LoggingPreferences();
        logPrefs.enable(LogType.BROWSER, Level.ALL);
        capabilities.setCapability(CapabilityType.LOGGING_PREFS, logPrefs);

        driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), capabilities);
        loggingRoutine(driver);
    }

    @Test
    public void testLogging_Hybrid() throws MalformedURLException {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability("platformName", "Android");
        capabilities.setCapability("automationName", "UiAutomator2");
        capabilities.setCapability("deviceName", "Android Emulator");
        capabilities.setCapability("app", APP_ANDROID);
        LoggingPreferences logPrefs = new LoggingPreferences();
        logPrefs.enable(LogType.BROWSER, Level.ALL);
        capabilities.setCapability(CapabilityType.LOGGING_PREFS, logPrefs);

        driver = new AndroidDriver<>(new URL("http://localhost:4723/wd/hub"), capabilities);
        WebDriverWait wait = new WebDriverWait(driver, 10);

        // get to webview screen and enter webview mode
        wait.until(ExpectedConditions.presenceOfElementLocated(hybridScreen)).click();
        wait.until(ExpectedConditions.presenceOfElementLocated(urlInput));
        driver.context(getWebContext(driver));

        // now we can run the same routine as for the browser
        loggingRoutine(driver);
    }

    public void loggingRoutine(AndroidDriver driver) {
        driver.get("https://appiumpro.com/test");
        driver.executeScript("window.onerror=console.error.bind(console)");
        driver.executeScript("console.log('foo.');");
        driver.executeScript("console.warn('bar?');");
        driver.findElementById("jsErrLink").click();

        for (LogEntry entry : driver.manage().logs().get("browser")) {
            System.out.println(entry.getMessage());
        }
    }

}

(NB: you'll need to make sure you're using Appium 1.9.2-beta or greater, since before this, the loggingPrefs capability was not passed correctly to Chromedriver).