Working with WebViews

Important

Visual Studio App Center is scheduled for retirement on March 31, 2025. While you can continue to use Visual Studio App Center until it is fully retired, there are several recommended alternatives that you may consider migrating to.

Learn more about support timelines and alternatives.

Xamarin.UITest provides APIs for locating and interaction with views in native mobile applications. However, some mobile apps are hybrid apps that use webviews to display HTML to the user. Android provides the android.webkit.WebView, while iOS applications may use either UIWebView or WKWebView. UIWebView is older and compatible with all versions of iOS, while WKWebView is for iOS 8 and higher.

Interacting with webviews is a bit more involved as UITest must first obtain a reference to the webview, and then to the DOM elements it contains.

WebView APIs in UITest

There are two APIs to obtain a reference to a webview control in UITest:

Each of these will be discussed in more detail below.

Locating the UIWebView control with AppQuery.WebView

UITest provides the AppQuery.WebView method to obtain a reference to a UIWebView control. The following snippet is an example:

>>> app.Query(c=>c.WebView())       

If there's only one webview on the screen, then WebView will default to that one webview. If there are multiple webviews on the screen, the WebView(Int32) can be used to isolate one by passing a zero-based index.

Locating the WKWebView control

UITest provides the AppQuery.Class method to obtain a reference to a WKWebView control. The following snippet is an example:

>>> app.Query(c=>c.Class("WKWebView"))

Interacting with DOM Elements in a webview control

Once the webview has been isolated, there are two main APIs for interacting with DOM elements:

  • CSS – This API uses CSS selectors to locate DOM elements displayed by the webview.
  • XPath – UITest can use XPath expressions to locate DOM elements in the webview.

The XPath API, while more powerful is also more complicated to use. In general, preference should be given to the CSS API, and the XPath API used when necessary.

CSS

It's possible to match HTML elements using the AppQuery.Css method. This method takes a CSS selector and will return an array of all HTML elements that match. Functionally, this API is the same on Android and iOS.

For example, the following Android activity contains an android.webkit.WebView:

WebView on an Android device

This snippet shows how to locate all H1 DOM elements in the webview:

>>> app.Query(c=>c.Css("H1"))                                                                                
Query for Css("H1") gave 1 results.
[
    [0] {
        Id => "",
        NodeType => "ELEMENT_NODE",
        NodeName => "H1",
        Class => "",
        Html => "<h1>H1 Header!</h1>",
        Value => null,
        WebView => "webView1",
        TextContent => "H1 Header!",
        Rect => {
            Width => 1032,
            Height => 117,
            CenterX => 540,
            CenterY => 408,
            Top => 44,
            Bottom => 83,
            Left => 8,
            Right => 352,
            X => 24,
            Y => 351
        }
    }
]

If there's more than one webview on a given screen, then UITest will automatically default to the first webview. If there are multiple webviews, then it's necessary to explicitly identify the webview using IApp.WebView(Int32). For example, if a screen had two webviews and we want to find all the H1 elements in the second webview:

>>> app.Query(c=>c.WebView(1).Css("H1"))                                                                                
Query for WebView().Css("H1") gave 1 results.
[
    [0] {
        Id => "",
        NodeType => "ELEMENT_NODE",
        NodeName => "H1",
        Class => "",
        Html => "<h1>H1 Header!</h1>",
        Value => null,
        WebView => "webView1",
        TextContent => "H1 Header!",
        Rect => {
            Width => 1032,
            Height => 117,
            CenterX => 540,
            CenterY => 408,
            Top => 44,
            Bottom => 83,
            Left => 8,
            Right => 352,
            X => 24,
            Y => 351
        }
    }
]

If more than one HTML element matches the CSS query, the Index method can be used to access an individual element in the result set. For example, the following snippet shows how to access the third element of HTML elements with the .user style:

>>> app.Tap(c => c.Css(".user").Index(2));

XPath

XPath is a powerful API for searching the DOM, but can be a bit more difficult to use compared to the CSS API. UITest can locate DOM elements by an XPath selector passed to the AppQuery.XPath method. This API is the same on Android and iOS.

The following snippet is an example of how to match the H1 DOM element from the previous section using XPath:

>>> app.Query(c=>c.XPath("//h1"))
Query for XPath("//h1") gave 1 results.
[
    [0] {
        Id => "",
        NodeType => "ELEMENT_NODE",
        NodeName => "H1",
        Class => "",                                                                                                   
        Html => "<h1>H1 Header!</h1>",
        Value => null,
        WebView => "webView1",
        TextContent => "H1 Header!",
        Rect => {
            Width => 1032,
            Height => 117,
            CenterX => 540,
            CenterY => 408,
            Top => 44,
            Bottom => 83,
            Left => 8,
            Right => 352,
            X => 24,
            Y => 351
        }
    }
]

Invoking JavaScript

It's also possible to locate webviews using the AppQuery.InvokeJS method. This method takes a query in the form of a string, and invokes JavaScript on the view elements matched by the query. If view elements other than WebViews are found, the execution will halt and an Exception will be thrown.

InvokeJS on Android

Generally speaking Android requires that the JavaScript returns the value; otherwise the query will return null:

>>> app.Query (w => w.WebView ().InvokeJS ("document.getElementById('lastname').value"))
[
[0] "null"
]
>>> app.Query (w => w.WebView ().InvokeJS ("return document.getElementById('lastname').value"))
[
[0] "Smith"
]

InvokeJS on iOS

Generally speaking, on iOS the return isn't used:

>>> app.Query (w => w.WebView ().InvokeJS ("return document.getElementById('lastname').value"))
[
[0] ""
]

>>> app.Query (w => w.WebView ().InvokeJS("document.getElementById('lastname').value"))
[
[0] "Smith"
]

Examples

This section will cover some common use cases that may be found when writing Calabash tests involving webviews.

Scrolling

It's possible to scroll a webview to bring DOM elements onto the screen. This is accomplished using the IApp.ScrollDownTo or IApp.ScrollUpTo methods. The signature of these methods is similar:

IApp.ScrollDownTo(Func<AppQuery, AppWebQuery> toQuery,
                  string withinMarked,
                  ScrollStrategy strategy,
                  Nullable<TimeSpan> timeout)

Here is a quick description of the parameters:

  • toQuery - this is a UITest web query that will locate a DOM element in the webview.
  • withinMarked - this is a string that will located the webview on the screen. This parameter is optional if there's only one webview on the screen. This string will used by IApp.Marked to locate the webview on the screen.
  • strategy - this optional parameter tells UITest how to scroll within the webview. ScrollStrategy.Gesture will try to emulate how a user would scroll, by dragging the screen. ScrollStrategy.Programatic frees up UITest to scroll in the quickest way possible. ScrollStrategy.Auto tells UITest to use any combination of Gesture and Programatic to scroll (with a preference to Programatic).
  • timeout - an optional parameter that specifies how long UITest should wait before timing out the query.

As an example of using these APIs, consider the screenshots of a webview embedded in an Android application:

Screenshot showing the WebView used in the following example.

If we examine the view hierarchy of this Activity in the REPL, we can see the following:

>>> tree                                                                                                                
[[object CalabashRootView] > PhoneWindow$DecorView]                                                                     
  [ActionBarOverlayLayout] id: "decor_content_parent"
    [FrameLayout > LinearLayout] id: "content"
      [WebView] id: "webView1",  label: "WebView"
        [dom] id: "show_secret"
        [dom] id: "firstname"
        [dom] id: "lastname"
    [ActionBarContainer] id: "action_bar_container"
      [Toolbar] id: "action_bar"
        [TextView] text: "TestingWebviews.Droid"
  [View] id: "statusBarBackground"
  [View] id: "navigationBarBackground"

The output from the tree command tells us that the webview has an the ID webView1, and contains three DOM elements:

  • an HTML button called show_secret with the markup <button id="show_secret">Toggle the Secret</button>
  • an HTML input field firstname, with the markup <input type="text" name="firstname" id='firstname'>
  • an HTML input field lastname, with the markup <input type="text" name="lastname" id='lastname'>

The show_secret button is visible on the screen, but firstname and lastname aren't. Before a UITest may interact with those DOM elements, it's necessary to use the scrolling APIs to bring those fields into view. The following snippet from the REPL shows how to scroll the HTML input elements into view using the ScrollDownTo API:

>>> app.ScrollDownTo(c=>c.Css("#firstname"))                              
Scrolling down to Css("#firstname")

Entering Text

Entering text into an HTML input element is accomplished by providing text and an AppWebQuery using AppQuery.EnterText API . Given the following screenshot of a webview in an Android application:

Screenshot showing the WebView used in the following example

The following code snippet will show to enter text into the firstname HTML input element:

[Test]
public void ScrollDownAndEnterName()
{
  app.ScrollDownTo(c => c.Css("input#firstname"), );
  app.EnterText(c => c.Css("input#firstname"), "Monkey");		

  // A more verbose way to enter text, explicitly identifying the webview and ScrollStrategy
  app.ScrollDownTo(c => c.Css("input#lastname"), "webView1", ScrollStrategy.Gesture);
  app.EnterText(c => c.Css("input#lastname"), "Chimpanzee");

}