现在的 App 一般都是 Hybrid 的,常见的情况是有一个 Native 的按钮,点了之后会打开一个 Webview 界面,那么在 Espresso 里如何测试呢?现在我们从 0 开始,来分析此类 Webview 该如何测试.
正所谓不入虎穴焉得虎子!一个好的测试人员应该知其然,更知其所以然!不要害怕,难道写一个简单的 App 这么难吗?在你下定决心的时候,就已经赢了一半! 你不开始,就永远没有深入了解它是如何运作的机会!Never!
新建项目
打开 Android Studio, File>New Project, Application name 随便取,然后 Next>Next,选择默认的 Empty Activity, Next>Finish. 除了命名外,我们全部采用默认的配置,建了一个空的 Activity。什么是 Activity 呢? 你可以简单的理解为一个页面
,对,就是一个页面,就是你最常见到的页面,仅此而已,所有页面上的变化,不过是修改了部分显示控件的内容而已。就这么简单!我们建了一个 Activity,就可以在这个页面上随便绘制想要的内容:按钮啦,输入框啦等等等等……建完 Project 之后,AS(Android Studio) 会开始 build 你的项目 (没错,AS 就是这么严谨,哪怕是空的项目啥也没有,它也会默认帮你编译一遍,看是否缺什么依赖!Google 棒棒哒!),不出意外,build 完了之后大概是下面的样子 (以下是 Mac 系统下的目录,Windows 的请参照这个):
大概每个目录的意思:
.gradle
: Gradle 编译系统,版本由下面的gradle
>wrapper
指定.idea
:IDE 生成的专用工程配置文件,类似 Eclipse 的.project
, VSCode 的.vscode
一样app
:项目的所有代码和资源文件
app/build
:app 模块编译输出的文件app/libs
: 放置引用的类库文件app/src
: 放置应用的主要文件目录app/src/androidTest
:单元测试目录app/src/main
:主要的项目目录和代码app/src/main/assets
:放置原生文件,里面的文件会保留原有格式,文件的读取需要通过流app/src/main/java
:项目的源代码app/src/main/res
:项目的资源app/src/main/res/drawable
:存放各种位图文件 (.png,.jpg,.9png,.gif 等) 和 drawable 类型的 XML 文件app/src/main/res/drawable-v24
:存放自定义 Drawables 类(Android API 24 开始,可在 XML 中使用)app/src/main/res/layout
:存放布局文件app/src/main/res/menu
:存放菜单文件app/src/main/res/mipmap-hdpi
:存放高分辨率图片资源app/src/main/res/mipmap-mdpi
:存放中等分辨率图片资源app/src/main/res/mipmap-xdpi
:存放超高分辨率图片资源app/src/main/res/mipmap-xxdpi
:存放超超分辨率图片资源app/src/main/res/mipmap-xxxdpi
:存放超超超高分辨率图片资源app/src/main/res/raw
:存放各种原生资源 (音频,视频,一些 XML 文件等)app/src/main/res/values
: 存放各种配置资源(颜色,尺寸,样式,字符串等)app/src/main/res/values/attrs.xml
:自定义控件时用的较多,自定义控件的属性app/src/main/res/values/arrays.xml
:定义数组资源app/src/main/res/values/colors.xml
:定义颜色资源app/src/main/res/values/dimens.xml
:定义尺寸资源app/src/main/res/values/string.xml
:定义字符串资源app/src/main/res/values/styles.xml
:定义样式资源app/src/main/res/values-v11
:在 API 11+ 的设备上调用app/src/main/res/values-v14
:在 API 14+ 的设备上调用app/src/main/res/values-v21
:在 API 21+ 的设备上调用app/src/main/res/AndroidManifest.xml
:项目的清单文件(名称、版本、SDK、权限等配置信息)app/src/.gitignore
:忽略的文件或者目录app/app.iml
:app 模块的配置文件app/build.gradle
:app 模块的 gradle 编译文件app.*
:app 模块的代码混淆配置文件build
:系统生成的文件目录
gradle
: wrapper 的 jar 和配置文件所在的位置
.gitignore
: 忽略的文件或者目录
build.gradle
:项目的 gradle 编译文件
gradle.properties
: gradle 相关的全局属性设置
gradlew
: 编译脚本,可以在命令行执行打包
gradlew.bat
:windows 下的 gradle wrapper 可执行文件
local.properties
:配置 SDK/NDK 所在的路径
settings.gradle
:设置相关的 gradle 脚本
webViewDemo.iml
:项目模块的相关信息
External Libraries
:项目依赖的库,编译时自动下载
这些信息仅供参考,我们大概了解下就行,当然,以后深入了,我们会对以上目录或文件会有更深的理解!
编写源码
app 的源码在app/src/main/java
目录下,复制如下代码替换 MainActivity 里的内容 (注意替换为自己的包名,需要根据自己的情况进行替换):
MainActivity.java
:
package com.example.pis.webviewdemo;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.EditText;
public class MainActivity extends AppCompatActivity {
public static final String TAG_WEB_VIEW_EXAMPLE = "WEB_VIEW_EXAMPLE";
private EditText urlEditor;
private Button loadUrlButton;
private Button backButton;
private Button forwardButton;
private Button clearCacheButton;
private Button showSnippetButton;
private WebView webView;
private CustomWebviewClient customWebviewClient;
private CustomWebChromeClient customWebChromeClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Request show progress bar in the activity window title.
Window activityWindow = this.getWindow();
activityWindow.requestFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
setTitle("dev2qa.com - Android WebView Example.");
// Initialize this activity used controls.
initControl();
// Click this button to load user specified web url page.
loadUrlButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String url = urlEditor.getText().toString();
if(!TextUtils.isEmpty(url))
{
webView.loadUrl(url);
}else {
webView.loadData("<font color=red><b>Please input web page url start with http or https, then click load button.</b></font>", "text/html", "utf-8");
}
}
});
// Click this button to go to previous page.
backButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(webView.canGoBack())
{
webView.goBack();
}
}
});
// Click this button to go to next page.
forwardButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(webView.canGoForward())
{
webView.goForward();
}
}
});
// Click this button to clear webview cache, history and html form data.
clearCacheButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Clear cache page.
webView.clearCache(true);
// Clear webview history.
webView.clearHistory();
// Clear html form data.
webView.clearFormData();
}
});
showSnippetButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
webView.loadData("<strong><font color=red>Hello WebView</font></strong>", "text/html", "utf-8");
}
});
}
// Initialize the edit text, buttons and WebView component.
private void initControl()
{
urlEditor = (EditText)findViewById(R.id.web_view_url_editor);
loadUrlButton = (Button)findViewById(R.id.web_view_load_button);
backButton = (Button)findViewById(R.id.web_view_back_button);
forwardButton = (Button)findViewById(R.id.web_view_forward_button);
clearCacheButton = (Button)findViewById(R.id.web_view_clear_cache_button);
showSnippetButton = (Button)findViewById(R.id.web_view_show_snippet_html);
webView = (WebView)findViewById(R.id.web_view_component);
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
// Set custom webview client.
customWebviewClient = new CustomWebviewClient();
webView.setWebViewClient(customWebviewClient);
// Set custom web chrome client.
customWebChromeClient = new CustomWebChromeClient();
customWebChromeClient.setSourceActivity(this);
webView.setWebChromeClient(customWebChromeClient);
webView.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null);
}
@Override
protected void onDestroy() {
// Destroy WebView object when activity is destroyed.
if(webView!=null)
{
ViewGroup viewGroup = (ViewGroup) webView.getParent();
viewGroup.removeView(webView);
webView.destroy();
webView = null;
}
super.onDestroy();
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_BACK)
{
// Process android device back menu
if(webView!=null && webView.canGoBack())
{
webView.goBack();
return true;
}
}
return super.onKeyDown(keyCode, event);
}
}
CustomWebviewClient.java
:
package com.example.pis.webviewdemo;
/**
* Created by pis on 2019/2/19.
*/
import android.graphics.Bitmap;
import android.util.Log;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
/**
* Created by Jerry on 2/26/2018.
*/
public class CustomWebviewClient extends WebViewClient {
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onPageStarted url : " + url);
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onPageFinished url : " + url);
super.onPageFinished(view, url);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
if(request!=null) {
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "shouldOverrideUrlLoading request.toString() : " + request.toString());
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "shouldOverrideUrlLoading request.getUrl().toString() : " + request.getUrl().toString());
view.loadUrl(request.getUrl().toString());
}else
{
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "shouldOverrideUrlLoading request is null.");
}
return false;
}
}
CustomWebChromeClient
:
package com.example.pis.webviewdemo;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
/**
* Created by pis on 2019/2/19.
*/
public class CustomWebChromeClient extends WebChromeClient {
private AppCompatActivity sourceActivity;
private String webPageTitle = "";
public AppCompatActivity getSourceActivity() {
return sourceActivity;
}
public void setSourceActivity(AppCompatActivity sourceActivity) {
this.sourceActivity = sourceActivity;
}
// This method is invoked when webview load page.
@Override
public void onProgressChanged(WebView view, int newProgress) {
// set title and
sourceActivity.setTitle("Page Loading...... - dev2qa.com");
int showProgress = newProgress * 100;
sourceActivity.setProgress(showProgress);
if(newProgress == 100)
{
sourceActivity.setTitle(webPageTitle);
}
super.onProgressChanged(view, newProgress);
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onProgressChanged newProgress : " + newProgress);
}
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
webPageTitle = title;
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onReceivedTitle title : " + title);
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onJsAlert url : " + url + " , message : " + message);
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onJsConfirm url : " + url + " , message : " + message);
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
Log.d(MainActivity.TAG_WEB_VIEW_EXAMPLE, "onJsPrompt url : " + url + " , message : " + message);
return super.onJsPrompt(view, url, message, defaultValue, result);
}
}
以上三个文件都是放在app/src/main/java
目录下。
下面,我们还需要简单的布局文件,在 src>main>res 下面,有一个 layout 目录,目录下面默认有一个 activity_main.xml 文件,复制以下内容替换自己的文件内容:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.pis.webviewdemo.MainActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/web_view_url_editor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Input a url to browse."/>
<LinearLayout
android:id="@+id/web_view_button_layout"
android:layout_below="@id/web_view_url_editor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/web_view_load_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Load"/>
<Button
android:id="@+id/web_view_back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Prev"/>
<Button
android:id="@+id/web_view_forward_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Next"/>
<Button
android:id="@+id/web_view_clear_cache_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Clear"/>
<Button
android:id="@+id/web_view_show_snippet_html"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Hello" />
</LinearLayout>
<WebView
android:id="@+id/web_view_component"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/web_view_button_layout"
android:background="@color/colorPrimaryDark"></WebView>
</RelativeLayout>
</android.support.constraint.ConstraintLayout>
注意上面的tools:context
行,声明了当前这个布局文件是给哪个 Activity 服务的,你需要替换为自己的对应的 Activity 名,其他的保持不变.
另外,我们的 src>main 下面的AndroidMainfest.xml
如下供参考:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.pis.webviewdemo">
<!-- Play web url audio file required permission. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.example.pis.webviewdemo.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
现在,我们开始试着编译了!一般情况下,新手会遇到一些问题,都是一些简单的问题,相信你会很快修改完毕,并编译成功 (比如你可能会遇到最低版本要求问题:代码里有个函数 getUrl() 是在 sdk>21 才能使用的,因此你可能需要改一下 minSdkVersion 为 21,在哪儿改呢?相信你一定会找到~),有问题可以通过搜索引擎来搜索一下,以上问题都是基本的问题,很容易调试完成!调试成功运行的界面如下图:
输入一个 url,比如http://www.bing.com
就可以看到网址被 load 成功了,说明我们的 webview 起作用了!