Edition 73

Working with Multile Webviews in Android Hybrid Apps

App with two webviews

It's not very common, but it can happen that an Android hybrid app has not just one but two or more webviews (for example, when advertisements are in one webview and app logic is in another). If you've ever found yourself in this situation, you may have been frustrated when using Appium's context API, specifically with code like this:

Set<String> handles = driver.getContextHandles();

If you have multiple webviews, you might have expected the Set to contain three items: NATIVE_APP, and two webview-looking strings. But instead you probably found only one webview context. If you were lucky, it was the one you wanted, but there was an equal chance you got the unimportant advertising webview (or whatever). So, how can we be sure to get inside the particular webview we want, even when there are multiple?

The answer lies in how Chromedriver works. Appium uses Chromedriver for Chrome automation, and Chromedriver handles multiple webviews in a single Android app by treating them as separate windows. What this means is that we have access to them using the built-in Webdriver window commands, like so:

Set<String> windowHandles = driver.getWindowHandles();

Then, we can switch into any window we want by picking a certain handle:

driver.switchTo().window(handle);

How do we know from the handle which window to choose? We don't, because it will be a random-looking string like CDwindow-A26869B5EDAEAA9D83947BB274F1D0C7, and it might change from test to test. So our best bet is to switch into each one and perform some validation on the window, to prove that we are in the correct one (for example, looking at the URL or title as a way of ensuring we're in the right webview, and short-circuiting our search when we find it).

Of course, to do any of this we actually have to be in the webview context first, otherwise the getWindowHandles command won't do anything. So to conclude, here's a full example that utilizes a new feature of The App, namely a view with two webviews. The idea behind this test is simply to make an assertion based on the contents of the page displayed in each webview, proving that we can indeed enter and automate both of them. As with Edition 17, we use a helper function getWebContext to get ourselves into the initial web context so that Chromedriver is operative. Without further ado, here's the full sample:

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 javax.annotation.Nullable;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class Edition073_Multiple_Webviews {

    private String APP_ANDROID = "https://github.com/cloudgrey-io/the-app/releases/download/v1.10.0/TheApp-v1.10.0.apk";

    private AndroidDriver driver;
    private static By hybridScreen = MobileBy.AccessibilityId("Dual Webview Demo");
    private static By webview = By.className("android.webkit.WebView");

    @Before
    public void setUp() throws MalformedURLException {
        DesiredCapabilities capabilities = new DesiredCapabilities();
        capabilities.setCapability("platformName", "Android");
        capabilities.setCapability("deviceName", "Android Emulator");
        capabilities.setCapability("automationName", "UiAutomator2");
        capabilities.setCapability("app", APP_ANDROID);

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

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

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

        // get to dual webview screen and make sure it's loaded
        wait.until(ExpectedConditions.presenceOfElementLocated(hybridScreen)).click();
        wait.until(ExpectedConditions.presenceOfElementLocated(webview));

        // navigate to the webview context
        String webContext = getWebContext(driver);
        driver.context(webContext);

        // go into each available window and get the text from the html page
        ArrayList<String> webviewTexts = new ArrayList<>();
        for (String handle : driver.getWindowHandles()) {
            System.out.println(handle);
            driver.switchTo().window(handle);
            webviewTexts.add(driver.findElement(By.tagName("body")).getText());
        }

        // assert that we got the correct text from each android webview
        Assert.assertThat(webviewTexts,
            Matchers.containsInAnyOrder("This is webview '1'", "This is webview '2'"));
    }

    @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;
    }
}

That's it! With judicious use of both context and window handles commands, automating multiple webviews is totally doable. Don't forget to check out the full code on GitHub.