© Denys Zelenchuk 2019
Denys ZelenchukAndroid Espresso Revealedhttps://doi.org/10.1007/978-1-4842-4315-2_6

6. Testing Web Views

Denys Zelenchuk1 
(1)
Zürich, Switzerland
 

Today we can find mobile applications for almost everything—gaming, social networking, banking, music, etc. Such a variety of applications developed by a single developer, small startup, or solid company means different development approaches. These approaches are represented by native and hybrid applications. Native applications are developed for a mobile operating system following platform standards, user interface, and user experience guidelines and access mobile device capabilities like the camera, GPS, etc. Hybrid applications typically use websites that are in a native wrapper or container. On Android, this container is called the WebView .

However, there is a gray area. Even when a developer selects the native application development, there are many places in an application that may use the integrated Android WebView component . This makes sense, because web views represent features that should be controlled remotely without the need to create redundant application releases. The common areas where WebView components can be used are as follows:
  • Web browser applications

  • Registration or login forms with Google, Facebook, or Twitter accounts

  • Legal and privacy disclaimers

  • Application FAQs

  • Support contact forms

We already know that native Android applications can be tested by Espresso. This chapter presents Espresso-Web and shows how it can be used to test Android WebView UI components integrated into mobile applications. Both Espresso and Espresso-Web can be used in combination to fully interact with an application on its different levels.

Espresso-Web Basics

Similar to Espresso’s onData() method, a WebView interaction is comprised of several atoms. WebView interactions use a combination of the Java programming language and a JavaScript bridge to do their work. Because there is no chance of introducing race conditions by exposing data from the JavaScript environment—everything Espresso sees on the Java-based side is an isolated copy—returning data from Web.WebInteraction objects is fully supported, allowing you to verify all the data that’s returned from a request.

The WebDriver framework uses atoms to find and manipulate web elements programmatically. Atoms are used by WebDriver to accommodate browser manipulation. An atom is conceptually similar to a ViewAction. It’s a self-contained unit that performs an action in your UI. You expose atoms using a list of defined methods, such as findElement() and getElement(), to drive the browser from the user’s point of view. However, if you use the WebDriver framework directly, atoms need to be properly orchestrated, requiring logic that is quite verbose.

Within Espresso, the Web and Web.WebInteraction classes wrap this boilerplate and give an Espresso-like feel to interacting with WebView objects. So, in the context of a WebView, atoms are used as a substitution to traditional Espresso ViewMatchers and ViewActions.

The API then looks quite simple, as follows.

Espresso-Web API Usage Formula.
onWebView()
    .withElement(Atom)
    .perform(Atom)
    .check(WebAssertion)

To add Espresso-Web to a project, insert the following line of code into the application build.gradle file.

Espresso-Web Dependency in the Android Support Library.
androidTestImplementation 'com.android.support.test.espresso:espresso-web:3.0.2'

Or add the same dependency to the AndroidX Test Library.

Espresso-Web Dependency in the AndroidX Test Library.
androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'

Espresso-Web Building Blocks

Espresso-Web contains the following API components:
  • WebInteractions—An analogue to Espresso’s ViewInteraction or DataInteraction. Used to perform actions and call validation methods, locate web elements, and set WebView properties.

  • DriverAtoms—A collection of JavaScript atoms from the WebDriver project.

  • WebAssertions—Asserts that the given atom’s result is accepted by the provided matcher.

Web interactions:
  • reset()—Deletes the Element and Window references from the web interaction.

  • forceJavascriptEnabled()—Forces JavaScript usage on a WebView. Enabling JavaScript may reload the WebView under test.

  • withNoTimeout()—Disables all timeouts on this WebInteraction.

  • withTimelout()—Sets a defined timeout for current WebInteraction.

  • inWindow()—Causes this WebInteraction to perform JavaScript evaluation in a specific DOM window.

  • withElement()—Causes this WebInteraction to supply the given ElementReference to the atom prior to evaluation. After calling this method, it resets any previously selected ElementReference.

  • withContextualElement()—Evaluates this WebInteraction on the subview of the selected element. Similar to the Espresso withChild() method.

  • perform()—Executes the provided atom within the current context. This method blocks until the atom returns. Produces a new instance of WebInteraction that can be used in further interactions.

  • check()—Evaluates the given WebAssertion. After this method completes, the result of the atom’s evaluation is available via get.

  • get()—Returns the result of a previous call to perform or check.

For better understanding, web interactions can be split into different groups where each group represents some functional load, as shown in Figure 6-1.

Driver atoms:
  • webClick()—Simulates the JavaScript events to click on a particular element.

  • clearElement()—Clears content from an editable element.

  • webKeys()—Simulates JavaScript key events sent to a certain element.

  • findElement()—Finds an element using the provided locatorType strategy.

  • selectActiveElement()—Finds the currently active element in the document.

  • selectFrameByIndex()—Selects a subframe of the currently selected window by its index.

  • selectFrameByIdOrName()—Selects a subframe of the given window by its name or ID.

  • getText()—Returns the visible text beneath a given DOM element.

  • webScrollIntoView()—Returns true if the desired element is in view after scrolling.

../images/469090_1_En_6_Chapter/469090_1_En_6_Fig1_HTML.jpg
Figure 6-1

WebInteractions grouped by functional load

DriverAtoms can be grouped by the return type, which determines where a specific method will be used. See Figure 6-2.
../images/469090_1_En_6_Chapter/469090_1_En_6_Fig2_HTML.jpg
Figure 6-2

DriverAtoms grouped by the return type

Web assertions (see Figure 6-3):
  • webMatches()—A WebAssertion that asserts that the given atom’s result is accepted by the provided matcher.

  • webContent()—A WebAssertion that asserts that the document is matched by the provided matcher.

../images/469090_1_En_6_Chapter/469090_1_En_6_Fig3_HTML.jpg
Figure 6-3

WebAssertions methods

Now we can extend the Espresso-Web API usage formula with more detailed information, as shown in Figure 6-4.
../images/469090_1_En_6_Chapter/469090_1_En_6_Fig4_HTML.jpg
Figure 6-4

Extended Espresso-Web API usage formula

You might wonder why the onWebView() method shown in Figure 6-4 takes the Espresso ViewMatcher (discussed in Chapter 1) as a parameter. The WebView UI element is still an Android native component and can have its own ID, content description, and other element properties. If we have multiple WebView components inside the application screen, we have to specify which WebView we want to operate on.

Let’s take a look again at our sample application, where the Settings section contains a WebView sample entry with an integrated WebView component. Figure 6-5 shows the layout hierarchy in LayoutInspector.
../images/469090_1_En_6_Chapter/469090_1_En_6_Fig5_HTML.jpg
Figure 6-5

Application Settings subsection layout of the WebView component

As you can see, the WebView component can be identified using Espresso ViewMatcher based on the ID property web_view.

For your convenience, an Espresso-Web cheat sheet is included in Appendix A and as an addition to the sample application source code.

Exercise 17

Verifying Intents
  1. 1.

    Launch a sample application and navigate to Settings. Open the WebView sample section and do the layout dump with the LayoutInspector tool. Observe which WebView properties can be used in UI tests.

     
  2. 2.

    Similar to Step 1, do the layout dump using a monitor application. Observe which WebView properties can be analyzed using the monitor tool and compare it to the LayoutInspector results.

     

Writing Tests with Espresso-Web

We are now ready to dive into Espresso web tests. For better understanding, open the web_form.html and web_form_response.html files from the main application assets folder in any browser, open the browser developer tools, and then start to inspect the web pages. It is assumed that you have a basic understanding of HTML page structure and can inspect web page UI elements using browser developer tools.

With Espresso-Web, UI elements can be located in the layout with the following locator types:
  • CLASS_NAME("className")

  • CSS_SELECTOR("css")

  • ID("id")

  • LINK_TEXT("linkText")

  • NAME("name")

  • PARTIAL_LINK_TEXT("partialLinkText")

  • TAG_NAME("tagName")

  • XPATH("xpath")

Figure 6-6 shows the web_form.html page in the Chrome Developer Tools view.
../images/469090_1_En_6_Chapter/469090_1_En_6_Fig6_HTML.jpg
Figure 6-6

Chrome browser developer tools view

The web page is built in a way that allows you to showcase most of the Espresso-Web functionality. Open the chapter6.WebViewTest.kt class to see the implemented test cases. Here is the updatesLabelAndOpensNewPage() test case.

chapter6 . WebViewTest.updatesLabelAndOpensNewPage() .
@Test
fun updatesLabelAndOpensNewPage() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    onWebView()
            .forceJavascriptEnabled()
            // Find edit text and type text.
            .withElement(findElement(Locator.ID, "text_input"))
            .perform(webKeys("Espresso WebView testing"))
            // Find button by id and click.
            .withElement(findElement(Locator.ID, "submitBtn"))
            .perform(webClick())
            // Find element by id and check its text.
            .withElement(findElement(Locator.ID, "response"))
            .check(webMatches(getText(), containsString("Espresso+WebView+testing")))
}

Here, everything is simple. After navigating to the Settings section and clicking on the WebView sample item, the WebView is shown using Android WebViewClient. Espresso-Web handles web page loading, so there is no need to implement additional waiters. All the elements in this test case are located by their IDs, which is the ideal case.

The next test case shows how to find web elements by their CSS properties. This is the common case when element IDs are dynamically created and we cannot rely on them.

chapter6 . WebViewTest.selectsRadioButtonWithCss() .
@Test
fun selectsRadioButtonWithCss() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    onWebView()
            // Find radio button by CSS.
            .withElement(findElement(Locator.CSS_SELECTOR, "input[value="rb1"]"))
            .perform(webClick())
}

Another way a web element can be located is by the XPATH selector, as follows.

chapter6 . WebViewTest.findsElementsByXpath() .
@Test
fun findsElementsByXpath() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    onWebView()
            // Find label XPATH and check its text.
            .withElement(findElement(Locator.XPATH, "//label[@id="selection_result"]"))
            .perform(webScrollIntoView())
            .check(webMatches(getText(), equalTo("Select option")))
}

Note

Web browser developer tools can help locate elements by XPATH or CSS selectors. It is enough to use the CMD+F or CTRL+F shortcut and try expression on the search field. Elements are highlighted in the page layout.

The next sample test case shows how to operate on elements inside the dialog popup.

chapter6 . WebViewTest.opensModal() .
@Test
fun opensModal() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    onWebView()
            // Find button and click.
            .withElement(findElement(Locator.ID, "updateDetails"))
            .perform(webClick())
            // Find edit text field and input text in the popped up dialog.
            .withElement(findElement(Locator.ID, "modal_text_input"))
            .perform(webKeys("Text from modal"))
            // Find and click Confirm button.
            .withElement(findElement(Locator.ID, "confirm"))
            .perform(webClick())
            // Verify text from modal is set in label.
            .withElement(findElement(Locator.ID, "modal_message"))
            .check(webMatches(getText(), equalTo("Text from modal")))
}
In the current case, the dialog belongs to the HTML page and the elements inside can be easily found using the same onWebView() method , as shown in Figure 6-7.
../images/469090_1_En_6_Chapter/469090_1_En_6_Fig7_HTML.jpg
Figure 6-7

The HTML <dialog> shown inside the Android web view client

The next test case is about testing the interaction with the HTML <select> component. This turns out to be a problematic topic. To begin, the following test case was implemented.

chapter6 . WebViewTest.failsToClickSelectDropDown() .
@Test
fun failsToClickSelectDropDown() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    onWebView()
            // Supposed to click on select.
            .withElement(findElement(Locator.ID, "selection_id"))
            .perform(webClick())
            // Select list is not shown, so test fails.
            .check(webMatches(getText(), equalTo("Item 3")))
}

The thing is that this test case fails on the last check only because the HTML <select> component list is not shown, even though webClick() was sent to the found element. Changing the locator type doesn’t help in this case and it is not needed because the element was found. This leads to the fact that something is wrong with the webClick() action only for the HTML <select> element. And after a bit of research, it turned out to be a known problem and there is even a workaround to make it work with the additional button:

Browsers do not allow expanding <select> in pure JavaScript, that control can be expanded only by directly clicking on it using the mouse. The “select.click()” won't work. But there is a solution. We imitate expanded <select> control by creating another select with multiple options being displayed at once, this can be done by setting the “size” parameter. That multiselect will be positioned absolutely over the old single-option select control, and the old one will be hidden using style’s visibility. That way the layout is kept the same, and the new control is displayed seamlessly. The new control looks only little differently, but that shouldn't be a problem, see it for yourself in screenshots below.

https://code.google.com/archive/p/expandselect/

But there is a workaround from the testing side, without introducing UI components on the web page. Being on the screen with the web view shown, we can expand <select> by sending a ViewActions.pressKey(KeyEvent.KEYCODE_SPACE) event when it is focused. Just as if you do it via the browser. To move focus to the <select> element, we send as many tab actions as needed to navigate to the desired UI element—ViewActions.pressKey(KeyEvent.KEYCODE_TAB). Unfortunately, tests should sleep for a short amount of time, so the sent action can be applied in the web view. This is how it is done with our sample application.

chapter6 . WebViewTest.verifiesSelectDropDown() .
@Test
fun verifiesSelectDropDown() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    // Send TAB keys as many times as needed to reach the "select".
    Thread.sleep(300)
    onView(withId(R.id.web_view)).perform(pressKey(KeyEvent.KEYCODE_TAB))
    Thread.sleep(300)
    onView(withId(R.id.web_view)).perform(pressKey(KeyEvent.KEYCODE_TAB))
    Thread.sleep(300)
    onView(withId(R.id.web_view)).perform(pressKey(KeyEvent.KEYCODE_TAB))
    Thread.sleep(300)
    onView(withId(R.id.web_view)).perform(pressKey(KeyEvent.KEYCODE_TAB))
    Thread.sleep(300)
    onView(withId(R.id.web_view)).perform(pressKey(KeyEvent.KEYCODE_TAB))
    Thread.sleep(300)
    onView(withId(R.id.web_view)).perform(pressKey(KeyEvent.KEYCODE_TAB))
    Thread.sleep(300)
    // Send SPACE key to expand "select".
    onView(withId(R.id.web_view)).perform(pressKey(KeyEvent.KEYCODE_SPACE))
    /**
     * At this point android platform popup is shown.
     * Use Espresso native methods to select item from the list.
     */
    onView(withText("Item 3")).click()
    onWebView()
            // Check that text from select list is set into the label.
            .withElement(findElement(Locator.ID, "selection_result"))
            .check(webMatches(getText(), equalTo("Item 3")))
}

This test case is fully functional but doesn’t look good. By adding an additional expand function to ViewInteraction, we can clean up our test code.

Test Case chapter6 . WebViewTest.verifiesSelectDropDown() .
/**
 * Expand function for web view test case.
 * It contains a Thread.sleep() each time key event is sent.
 *
 * @param key - keycode from {@link KeyEvent}
 * @param milliseconds - milliseconds to sleep
 * @param count - amount of times {@link KeyEvent} should be executed
 */
fun ViewInteraction.pressKeyAndSleep(key: Int, milliseconds: Long, count: Int = 1): ViewInteraction {
    for (i in 1..count) {
        /**
         * Having Thread.sleep() in tests is a bad practice.
         * Here we are using it just to solve specific issue and nothing more.
         */
        Thread.sleep(milliseconds)
        perform(ViewActions.pressKey(key))
    }
    return this
}

The verifiesSelectDropDown() test case becomes much more readable.

Test Case chapter6 . WebViewTest.verifiesSelectDropDown() .
@Test
fun verifiesSelectDropDown() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    onView(withId(R.id.web_view))
            // Send TAB keys as many times as needed to reach the "select".
            .pressKeyAndSleep(KeyEvent.KEYCODE_TAB, 500, 6)
            // Send SPACE key to expand "select".
            .perform(ViewActions.pressKey(KeyEvent.KEYCODE_SPACE))
    /**
     * At this point android platform popup is shown.
     * Use Espresso native methods to select item from the list.
     */
    onView(withText("Item 3")).click()
    onWebView()
            // Check that text from select list is set into the label.
            .withElement(findElement(Locator.ID, "selection_result"))
            .check(webMatches(getText(), equalTo("Item 3")))
}
A small note about the <select> drop-down . It is presented to the user as a native platform popup that can be interacted with via the Espresso methods, as shown in Figure 6-8.
../images/469090_1_En_6_Chapter/469090_1_En_6_Fig8_HTML.jpg
Figure 6-8

HTML <select> drop-down options shown inside the Android web view client

The last test case in the WebViewTest.kt class contains the rest of the Locator type’s usage samples.

Test Case chapter6 . WebViewTest.showsOtherLocatorsSample() .
@Test
fun showsOtherLocatorsSample() {
    openDrawer()
    onView(allOf(withId(R.id.design_menu_item_text),
            withText(R.string.settings_title))).perform(click())
    onData(instanceOf(PreferenceActivity.Header::class.java))
            .inAdapterView(withId(android.R.id.list))
            .atPosition(3)
            .perform(click())
    onWebView()
            // Find element by Locator.NAME
            .withElement(findElement(Locator.NAME, "text_input"))
            .perform(webScrollIntoView())
            // Find element by Locator.LINK_TEXT
            .withElement(findElement(Locator.LINK_TEXT, "Espresso Web."))
            .perform(webScrollIntoView())
            // Find element by Locator.PARTIAL_LINK_TEXT
            .withElement(findElement(Locator.PARTIAL_LINK_TEXT, "Espresso"))
            .perform(webScrollIntoView())
            // Find element by Locator.CLASS_NAME
            .withElement(findElement(Locator.CLASS_NAME, "header"))
            .check(webMatches(webScrollIntoView(), `is`(true)))
}

This test case shows how the webScrollIntoView() action can be used as a parameter to the WebAssertion.webMatches() method . This approach provides a more readable error description when the element we operate on is not found.

Exercise 18

Writing Web View Tests
  1. 1.

    Open the web_form.html page in the web browser and analyze the page structure. Search the elements by XPATH and CSS selectors.

     
  2. 2.

    Update the selectsRadioButtonWithCss() test so that the radio button with “Option 2” label is selected.

     
  3. 3.

    Write a test that finds all the elements by their XPATHs only.

     
  4. 4.

    Write a test that finds all the elements by their CSS locators only.

     

Summary

Espresso-Web is a nice addition to the Espresso APIs. It allows you to test hybrid applications with WebView components. Yes, it is not perfect and can’t be used for purely web application testing, but it does its job quite well in an Espresso-like manner.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.38.176