Skip to content

06 WebView Support

Joshua Moody edited this page Apr 10, 2015 · 21 revisions

Calabash supports queries and gestures on UIWebViews and WKWebViews.

There are four APIs for interacting with and inspecting web views.

We have a nice sample project that highlights each of these APIs with cucumbers.

https://github.com/calabash/ios-webview-test-app

UIWebView vs. WKWebView

WKWebView was added in 0.14.0 (April 2015).

The query and gesture APIs are nearly identical except for two cases.

Case 1. 'webView' will only match UIWebViews.

# Matches UIWebView
> query('webView')
> query('UIWebView')

# Matches WKWebView
> query('WKWebView')

Case 2. JavaScript API

WKWebView does not respond to the stringByEvaluatingJavaScriptString: selector. Instead provide JavaScript evaluation with the async evaluateJavaScript:completionHandler: selector. It was tempting to implement stringByEvaluatingJavaScriptString: on WKWebView, but because Objective-C doesn't have namespaces it is too dangerous. It is very likely that client code or some other library will have done so already. Our implementation would either clobber or be clobbered. In practical terms this means that we needed to namespace our selector.

This is the new public API for evaluating JavaScript. It is backward compatible for UIWebView.

### Incorrect
> query("WKWebView", {stringByEvaluatingJavaScriptString:"<javascript>"})

### Correct
> query("UIWebView", {stringByEvaluatingJavaScriptString:"<javascript>"})
> query("UIWebView", {calabashStringByEvaluatingJavaScript:"<javascript>"})
> query("WKWebView", {calabashStringByEvaluatingJavaScript:"<javascript>"})

What is visible?

Queries will only return DOM nodes whose centers are visible on the screen and within the web view's viewport.

Consider this example of an xpath query on page whose body tag spans many screens. When the page loads, the center of the body element is a couple of swipes down. A query for the 'body' will return no results. The following step scrolls down until the center of the 'body' is visible.

And(/^I can query for the body with xpath$/) do
  page = page(WebViewApp::WKWebView).await
  qstr = "WKWebView xpath:'//body'"

  visible = lambda {
     query(qstr).count == 1
  }

  counter = 0
  loop do
    break if visible.call || counter == 4
    scroll('WKWebView', :down)
    sleep(0.4) # for the animation
    counter = counter + 1
  end
  
  res = query(qstr)
  expect(res.count).to be == 1
end

Entering text

We recommend touching an input field to show the keyboard and using keyboard_enter_text. This is what the user does and it represents an authentic test of your app.

Given(/^I have entered my email address$/) do
  page(WebViewApp::WKWebView).await
  qstr = "WKWebView css:'input#firstname'"
  
  wait_for { !query(qstr).empty? }
     
  touch(qstr)
  wait_for_keyboard
  
  keyboard_enter_text("launisch@example.com")
end

It is also possible to fill in a text field or text area directly using set_text.

Be aware, however, that this may or may not trigger event listeners which in turn might affect the way your app behaves. For example, if you have a button that appears only after a valid email is entered, calling set_text might not cause the button to appear because no event listeners are triggered.

> set_text("UIWebView css:'input.login'", "yolo@aarhus.com")

CSS API

Query for an element with id, class or tag name.

> query("webView css:'#header'")
> query("webView css:'.js-current-repository'")
> query("webView css:'a'")
> touch("webView css:'a#internal'")
> query("webView css:'ul#faq'")
> touch("webView css:'button#login'")

XPATH API

http://www.w3schools.com/xpath/

XPath is incredibly powerful, but difficult to understand until you get the hang of it.

> query("webView xpath:'//body'")
> touch("webView xpath:'//a[contains(@id,\"internal\")]')
> touch("webView xpath:'//span/a[contains(@id,\"faq\")]')

JavaScript API

Unlike the css, path, and marked APIs - JavaScript can be evaluated on elements that are not visible.

> js = "document.getElementsByTagName('h1').toString()"
> query("UIWebView", {calabashStringByEvaluatingJavaScript: js}) 
=> ["[object NodeList]"]

> js = "document.getElementById('watermelon').innerHTML"
> query("WKWebView", {calabashStringByEvaluatingJavaScript: js})
=> ["Wassermelone"]

> js = "document.getElementById('firstname').value"
> query("UIWebView", {calabashStringByEvaluatingJavaScript: js}) 
=> ["launisch@example.com"]

> js = "document.getElementById('show_secret').click()"
> query("UIWebView", {calabashStringByEvaluatingJavaScript: js}) 

> js = "document.getElementById('secret_message').getAttribute('style')"
> query("WKWebView", {calabashStringByEvaluatingJavaScript: js}) 
=> ['display: block;']

> js = "eval(\"javascript:window.parent.navigateToSequence('border-collie-mov');\")")
> query("WKWebView", {calabashStringByEvaluatingJavaScript: js})

> iframe_id = 'frame_movies'
> element_id = 'heart-transplant'
> js = document.getElementById('#{iframe_id}').contentDocument.getElementById('#{element_id}').innerText
> query("UIWebView", {calabashStringByEvaluatingJavaScript: js})
=> ["Latest Heart Transplant Techniques"]

Marked API

This is a free text matcher.

// html <h1>H1 Header!<h1>
> query("UIWebView marked:'H1'").count         => 1
> query("UIWebView marked:'H1 H'").count       => 1
> query("UIWebView marked:'H1 Head'").count    => 1
> query("UIWebView marked:'H1 Header!'").count => 1

marked: behaves differently on web views

You cannot match the web view itself using an accessibilityIdentifier or accessibilityLabel.

Consider this example where we've set an id and label on our Objective-C web view. Notice that the queries don't match even though the view is clearly visible to Calabash. This is because the behavior of marked: is overridden for web views.

# Objective-C
  _webView.accessibilityIdentifier = @"landing page";
  _webView.accessibilityLabel = NSLocalizedString(@"landing page",
                                                  @"The app's first page.");

# Console - app is running with German language                                       
> query("UIWebView marked:'landing page') => []
> query("UIWebView").first['id']          => "landing page"

> query("UIWebView marked:'Zielseite'")   => []
> query("UIWebView").first['label']       => "Zielseite"

The work around is to use a predicate to match on the id or label. This also illustrates why you should favor using accessibilityIdentifiers over accessibilityLabels for Calabash queries.

> query("UIWebView {accessibilityIdentifier LIKE 'landing page'}").count => 1

# Will fail when the app language is not German.
> query("UIWebView {accessibilityLabel LIKE 'Zielseite'}").count         => 1
Clone this wiki locally