Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Testing #158

Draft
wants to merge 58 commits into
base: webview
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ee10b26
setup
grcoleman Jul 14, 2020
6973b6b
adds night theme color scheme, adds setForceDarkStrategy, adds media …
grcoleman Jul 14, 2020
efcf673
setup
grcoleman Jul 14, 2020
d6836d6
adds night theme color scheme, adds setForceDarkStrategy, adds media …
grcoleman Jul 14, 2020
c2de65f
fix conflicts
grcoleman Jul 14, 2020
761ecf0
changes path, adds icon choosing logic
grcoleman Jul 15, 2020
6db9c65
changes initial image path, adds symbos values
grcoleman Jul 15, 2020
55a9608
adds meta tag, corrects media query, adds setDefaultNightMode
grcoleman Jul 15, 2020
3e2c009
adds media query
grcoleman Jul 15, 2020
d70a861
changes path, adds icon choosing logic
grcoleman Jul 15, 2020
47f53af
changes initial image path, adds symbos values
grcoleman Jul 15, 2020
f429a26
changes fetch to correct path
grcoleman Jul 16, 2020
bc9ae5f
changes fetch path
grcoleman Jul 16, 2020
f0b47b9
changes background color, adds clarification on ForceDarkStrategy
grcoleman Jul 20, 2020
5dc103e
add clarifying comments on location of data, change path to ensure co…
grcoleman Jul 20, 2020
2c773f8
adds testing outline and dependencies
grcoleman Jul 21, 2020
403446f
changes res path, fixes comment in main.js
grcoleman Jul 21, 2020
77ff7e0
adds tests for createJsObject and drop down menu
grcoleman Jul 21, 2020
7185787
add description to tests,add ids to menu
grcoleman Jul 21, 2020
61c895e
splits light and dark themes into seperate files, adds logic to check…
grcoleman Jul 22, 2020
66b3c49
removes static text color setting from style.css
grcoleman Jul 22, 2020
da570e4
Merge pull request #157 from gcoleman799/change-data-source
gcoleman799 Jul 22, 2020
4323afe
adds testing outline and dependencies
grcoleman Jul 21, 2020
aa3df09
adds tests for createJsObject and drop down menu
grcoleman Jul 21, 2020
6d218b7
add description to tests,add ids to menu
grcoleman Jul 21, 2020
f263800
adds set up for unit tests
grcoleman Jul 22, 2020
42111ec
setup
grcoleman Jul 14, 2020
d42ded6
adds night theme color scheme, adds setForceDarkStrategy, adds media …
grcoleman Jul 14, 2020
0d4fdf8
setup
grcoleman Jul 14, 2020
85d21b8
adds night theme color scheme, adds setForceDarkStrategy, adds media …
grcoleman Jul 14, 2020
637fec3
adds meta tag, corrects media query, adds setDefaultNightMode
grcoleman Jul 15, 2020
2c83302
adds media query
grcoleman Jul 15, 2020
3a9d8fa
changes background color, adds clarification on ForceDarkStrategy
grcoleman Jul 20, 2020
f8b0c11
splits light and dark themes into seperate files, adds logic to check…
grcoleman Jul 22, 2020
be2899c
removes static text color setting from style.css
grcoleman Jul 22, 2020
7c3b2c1
add comment about default reloading
grcoleman Jul 22, 2020
60105b6
fixes conflicts
grcoleman Jul 22, 2020
e9e7a72
thread test and callback test outline
grcoleman Jul 23, 2020
4934ed2
fixes merge conflicts
grcoleman Jul 23, 2020
d7806ed
revert to one css file
grcoleman Jul 24, 2020
13d0b17
puts createJsObject in seperate file, adds handlers and runnables to …
grcoleman Jul 24, 2020
ce63069
converts runable to lambda, fixes message val, fixes loadDataWithBase…
grcoleman Jul 27, 2020
fa60356
adds coroutines to tests
grcoleman Jul 27, 2020
b4c6e58
changes dark theme css formatting
grcoleman Jul 28, 2020
35b902d
Merge pull request #155 from gcoleman799/custom-dark-theme
gcoleman799 Jul 28, 2020
77350b7
changes initial image path, gets rid of unncessary logging, fixes all…
grcoleman Jul 29, 2020
00b047b
Merge pull request #160 from gcoleman799/share-feature
gcoleman799 Jul 29, 2020
048851d
adds testing outline and dependencies
grcoleman Jul 21, 2020
7f7152e
adds tests for createJsObject and drop down menu
grcoleman Jul 21, 2020
bbc334b
add description to tests,add ids to menu
grcoleman Jul 21, 2020
4528470
adds set up for unit tests
grcoleman Jul 22, 2020
fa580f4
thread test and callback test outline
grcoleman Jul 23, 2020
684f2ef
adds testing outline and dependencies
grcoleman Jul 21, 2020
016ac01
adds tests for createJsObject and drop down menu
grcoleman Jul 21, 2020
c920025
puts createJsObject in seperate file, adds handlers and runnables to …
grcoleman Jul 24, 2020
e7bb953
uses CompletableDeffered in tests
grcoleman Jul 28, 2020
84cea6b
uses Completable Deferredand run blocking
grcoleman Jul 29, 2020
5c447de
Fixes merge conflicts
grcoleman Jul 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion WebView/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ dependencies {
implementation "com.google.android.material:material:$material_components_version"

testImplementation 'junit:junit:4.13'
testImplementation 'androidx.test:core:1.0.0'
testImplementation 'org.mockito:mockito-core:1.10.19'

androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
androidTestImplementation 'androidx.test.espresso:espresso-web:3.1.0'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
androidTestImplementation 'androidx.test:rules:1.1.1'
androidTestImplementation 'androidx.test:runner:1.1.0'
androidTestImplementation "androidx.webkit:webkit:$webkit_version"
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.samples.webviewdemo
import android.content.Context
import android.os.Looper
import android.webkit.WebView
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches
import androidx.test.espresso.web.model.Atoms.castOrDie
import androidx.test.espresso.web.model.Atoms.script
import androidx.test.espresso.web.sugar.Web.onWebView
import androidx.test.espresso.web.webdriver.DriverAtoms.findElement
import androidx.test.espresso.web.webdriver.DriverAtoms.getText
import androidx.test.espresso.web.webdriver.DriverAtoms.webClick
import androidx.test.espresso.web.webdriver.Locator
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.CoreMatchers.containsString
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith


/**
* Launch, interact, and verify conditions in an activity that has a WebView instance.
*/
@RunWith(AndroidJUnit4::class)


class MainActivityTest {

val context = ApplicationProvider.getApplicationContext<Context>()

@Rule @JvmField
val mainActivityRule = ActivityTestRule(MainActivity::class.java)
fun afterActivityLaunched() {
// Technically we do not need to do this - MainActivity has javascript turned on.
// Other WebViews in your app may have javascript turned off, however since the only way
// to automate WebViews is through javascript, it must be enabled.
onWebView().forceJavascriptEnabled()
}

// Test to check that the drop down menu behaves as expected
@Test
fun dropDownMenu_SanFran() {
mainActivityRule.getActivity()
onWebView()
.withElement(findElement(Locator.ID, "location"))
.perform(webClick()) // Similar to perform(click())
.withElement(findElement(Locator.ID, "SF"))
.perform(webClick()) // Similar to perform(click())
.withElement(findElement(Locator.ID, "title"))
.check(webMatches(getText(), containsString("San Francisco")))
}

// Test for checking createJsObject
@Test
fun jsObjectIsInjectedAndContainsPostMessage() {
mainActivityRule.getActivity()
onWebView()
.check(
webMatches(
script("return jsObject && jsObject.postMessage != null;", castOrDie(Boolean::class.javaObjectType)),
`is`(true)
)
)
}

@Test
fun valueInCallback_compareValueInput_returnsTrue(){
mainActivityRule.getActivity()

// Setup
val webView = WebView(context)
val jsObjName = "jsObject"
val allowedOriginRules = setOf<String>("https://example.com")

// Create HTML
val htmlPage = "<!DOCTYPE html><html><body>" + " <script>" + " myObject.postMessage('hello');" + " </script>" + "</body></html>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Swap myObject with jsObject. Or even better, you could use string interpolation:

val htmlPage = """
<!DOCTYPE html>
<html>
<body>
<script>${jsObjName}.postMessage('hello');</script>
</body>
</html>
"""

See https://kotlinlang.org/docs/reference/idioms.html#string-interpolation and https://realkotlin.com/tutorials/2018-06-26-multiline-string-literals-in-kotlin/

Copy link
Contributor Author

@gcoleman799 gcoleman799 Jul 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is easier to read if we do something along these lines and get rid fo the htmlPage

webView.loadData("<html></html>","text/html", "UTF-8") webView.evaluateJavascript("myObject.postMessage(someMessage)", null)
But just to check my understanding, with consideration to this comment though the second line becomes
webView.evaluateJavascript("${jsObjName}.postMessage(someMessage)", null)

Is that correct?


// Create JsObject
MainActivity.createJsObject(
webView,
jsObjName,
allowedOriginRules
) { message -> MainActivity.invokeShareIntent(message) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll want to change this line. Instead of invoking the share intent, you should save message somewhere. I would use a SettableFuture<> for this (call .set() to save the value there, call .get() afterward to block and retrieve the value). Here's a good example test case.

I'm not sure if there's a coroutine-friendly way to do this.


//Inject JsObject into Html
webView.loadData(htmlPage, "text/html", "UTF-8")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Android Views are all single-threaded, so they should only be interacted with on the UI thread, not the test thread. In WebView team's own tests, we have our own helper class to post stuff to the UI thread. In your case, you could just copy the second suggestion from https://stackoverflow.com/a/11125271 (the one with new Handler(Looper.getMainLooper()).

Here's some resources:



//Call js code to invoke callback (in script tag of htmlPage)

// evaluate what comes out -> it should be hello
// *Note: "response from callback" is a place holder here I am unsure what should be placed there
assertEquals("response from callback", "hello")

}

@Test
// Checks that postMessage runs on the UI thread
fun checkingThreadCallbackRunsOn() {
mainActivityRule.getActivity()

// Setup
val webView = WebView(context)
val jsObjName = "jsObject"
val allowedOriginRules = setOf<String>("https://example.com")

// Create HTML
val htmlPage =
"<!DOCTYPE html><html><body>" + " <script>" + " jsObject.postMessage('hello');" + " </script>" + "</body></html>"


// Create JsObject
MainActivity.createJsObject(
webView,
jsObjName,
allowedOriginRules
) { message -> MainActivity.invokeShareIntent(message) }


// Inject JsObject into Html by loading it in the webview
webView.loadData(htmlPage, "text/html", "UTF-8")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should swap this out for loadDataWithBaseUrl and use https://example.com" as the baseUrl. Otherwise, the allowedOriginRules may prevent the object from being injected.



// Use coroutine to go onto UI thread here?
// Call js code to invoke callback (in script tag of htmlPage)


// check that method is running on the UI thread
assertTrue(isUiThread())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want this assert here because instrumentation tests run on the "test thread," not the UI thread. I wrote an internal google doc which goes into more details about this.

As a first step, try moving this into createJsObject's callback. That will return correct results, but comes with the drawback that failing assertion crashes the process instead of showing up as a failed test (the google doc has more context on this). If you can get that working, then you can improve this further by using a SettableFuture<Boolean> to plumb the value across threads (see my previous comment for pointers).

}

/**
* Returns true if the current thread is the UI thread based on the
* Looper.
*/
private fun isUiThread(): Boolean {
return Looper.myLooper() == Looper.getMainLooper()
}
}
8 changes: 4 additions & 4 deletions WebView/app/src/main/assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
<body onload="getData()">
<label for="location">Choose a location:</label>
<select name="location" id="location" onchange="getData()">
<option value="newYork">New York</option>
<option value="sanFrancisco">San Francisco</option>
<option value="london">London</option>
<option id= "NYC" value="newYork">New York</option>
<option id= "SF" value="sanFrancisco">San Francisco</option>
<option id= "LDN" value="london">London</option>
</select>
<h1 id="title">Location</h1>
<img alt="weather" class="icon" id="icon" src="https://gcoleman799.github.io/res/drawable/sunny.png" />
<img alt="weather" class="icon" id="icon" src="https://raw.githubusercontent.com/res/drawable/sunny.png" />
<h2 class="currentTemp" id="currentTemp">Current Temp</h2>
<h2 class="shortDescription" id="shortDescription">Short Description</h2>
<p class="longDescription" id="longDescription">Long Description</p>
Expand Down
30 changes: 22 additions & 8 deletions WebView/app/src/main/assets/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,40 @@ function sendAndroidMessage() {
* in the WebViewCompat reference doc, the second parameter, MessagePorts, is optional.
* Also note that onmessage, addEventListener and removeEventListener are not supported.
*/
// TODO: Change message to account for changes in data
jsObject.postMessage("The weather in " + `${document.getElementById("title").innerText}` + " today is " +
`${document.getElementById("shortDescription").innerText} `);
}


function getData() {
// TODO: Change the path to grab data from new location; Change longDescription and currentTemp to work with changes in data
fetch("https://gcoleman799.github.io/Asset-Loader/weather.json").then(function(resp) {
// This JSON files is hosted over the web
fetch("https://raw.githubusercontent.com/android/views-widgets-samples/webview/WebView/sampleData/weather.json").then(function(resp) {
return resp.json();
}).then(function(data) {
var form = document.getElementById("location");
var currentLocation = form.options[form.selectedIndex].value;
console.log(data[currentLocation].description);
document.getElementById("title").innerText = form.options[form.selectedIndex].text;
document.getElementById("currentTemp").innerText = data[currentLocation].currentTemp;
document.getElementById("currentTemp").innerText = `${data[currentLocation].currentTemp}`+ "\xB0 F";
document.getElementById("shortDescription").innerText = data[currentLocation].description;
document.getElementById("longDescription").innerText = "Today in " + `${form.options[form.selectedIndex].text}`
+ " there is a " + `${data[currentLocation].chancePrecip}` + " chance of precipitation and the humidity is "
+ `${data[currentLocation].humidity}.`;
document.getElementById("icon").src = data[currentLocation].icon;
+ " there is a " + `${data[currentLocation].chancePrecip}` + "% chance of precipitation and the humidity is "
+ `${data[currentLocation].humidity}` + "%.";
document.getElementById("icon").src = getIcon(data[currentLocation].description);
})
}

// TODO: Create getIcon() function to decide which icon to render.
/* These icons are hosted locally, in the res/drawable folder. However, we can call them using
* http(s):// URLs because we have configured AssetLoader in MainActivity. It is desirable to
* access the files in this way because it is compatible with the Same-Origin policy.
*/
function getIcon(description){
switch(description) {
case "Rainy":
return "https://raw.githubusercontent.com/views-widgets-samples/res/drawable/rain.png";
case "Clear Sky":
return "https://raw.githubusercontent.com/views-widgets-samples/res/drawable/sunny.png";
default:
return "https://raw.githubusercontent.com/views-widgets-samples/res/drawable/partly_cloudy.png";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,16 +145,16 @@ class MainActivity : AppCompatActivity() {

// Configure asset loader with custom domain
// * NOTE THAT *:
// The assets path handler is set with the sub path /Asset-Loader/ here because we are tyring to ensure
// that the address loaded with loadUrl("https://gcoleman799.github.io/Asset-Loader/assets/index.html") does
// The assets path handler is set with the sub path /views-widgets-samples/ here because we are tyring to ensure
// that the address loaded with loadUrl("https://raw.githubusercontent.com/views-widgets-samples/assets/index.html") does
// not conflict with a real web address. In this case, if the path were only /assests/ we would need to load
// "https://gcoleman799.github.io/assets/index.html" in order to access our local index.html file.
// However we cannot guarantee "https://gcoleman799.github.io/assets/index.html" is not a valid web address.
// Therefore we must let the AssetLoader know to expect the /Asset-Loader/ sub path as well as the /assets/.
// "https://raw.githubusercontent.com/assets/index.html" in order to access our local index.html file.
// However we cannot guarantee "https://raw.githubusercontent.com/assets/index.html" is not a valid web address.
// Therefore we must let the AssetLoader know to expect the /views-widgets-samples/ sub path as well as the /assets/.
val assetLoader = WebViewAssetLoader.Builder()
.setDomain("gcoleman799.github.io")
.addPathHandler("/Asset-Loader/assets/", WebViewAssetLoader.AssetsPathHandler(this))
.addPathHandler("/res/", WebViewAssetLoader.ResourcesPathHandler(this))
.setDomain("raw.githubusercontent.com")
.addPathHandler("/views-widgets-samples/assets/", WebViewAssetLoader.AssetsPathHandler(this))
.addPathHandler("/views-widgets-samples/res/", WebViewAssetLoader.ResourcesPathHandler(this))
.build()

// Set clients
Expand Down Expand Up @@ -182,6 +182,6 @@ class MainActivity : AppCompatActivity() {
) { message -> invokeShareIntent(message) }

// Load the content
binding.webview.loadUrl("https://gcoleman799.github.io/Asset-Loader/assets/index.html")
binding.webview.loadUrl("https://raw.githubusercontent.com/views-widgets-samples/assets/index.html")
}
}