Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7aa74c3d authored by Steve Block's avatar Steve Block Committed by Android (Google) Code Review
Browse files

Merge "First bits of WebDriver containing blocking function to load a page and...

Merge "First bits of WebDriver containing blocking function to load a page and fetching the page source."
parents 44329ad5 600780bb
Loading
Loading
Loading
Loading
+216 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 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 android.webkit.webdriver;

import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.webkit.ConsoleMessage;
import android.webkit.GeolocationPermissions;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebStorage;
import android.webkit.WebView;

/**
 * Drives a web application by controlling the WebView. This class
 * provides a DOM-like API allowing to get information about the page,
 * navigate, and interact with the web application. This is particularly useful
 * for testing a web application.
 *
 * <p/>{@link android.webkit.webdriver.WebDriver} should be created in the main
 * thread, and invoked from another thread. Here is a sample usage:
 *
 * public class WebDriverStubActivity extends Activity {
 *   private WebDriver mDriver;
 *
 *   public void onCreate(Bundle savedInstanceState) {
 *       super.onCreate(savedInstanceState);
 *       WebView view = new WebView(this);
 *       mDriver = new WebDriver(view);
 *       setContentView(view);
 *   }
 *
 *
 *   public WebDriver getDriver() {
 *       return mDriver;
 *   }
 *}
 *
 * public class WebDriverTest extends
 *       ActivityInstrumentationTestCase2<WebDriverStubActivity>{
 *   private WebDriver mDriver;
 *
 *   public WebDriverTest() {
 *       super(WebDriverStubActivity.class);
 *   }
 *
 *   protected void setUp() throws Exception {
 *       super.setUp();
 *       mDriver = getActivity().getDriver();
 *   }
 *
 *   public void testGetIsBlocking() {
 *       mDriver.get("http://google.com");
 *       assertTrue(mDriver.getPageSource().startsWith("<html"));
 *   }
 *}
 *
 * @hide
 */
public class WebDriver {
    // Timeout for page load in milliseconds
    private static final int LOADING_TIMEOUT = 30000;
    // Timeout for executing JavaScript in the WebView in milliseconds
    private static final int JS_EXECUTION_TIMEOUT = 10000;

    // Commands posted to the handler
    private static final int GET_URL = 1;
    private static final int EXECUTE_SCRIPT = 2;

    private static final long MAIN_THREAD = Thread.currentThread().getId();

    // This is updated by a callabck from JavaScript when the result is ready
    private String mJsResult;

    // Used for synchronization
    private final Object mSyncObject;

    // Updated when the command is done executing in the main thread.
    private volatile boolean mCommandDone;

    private WebView mWebView;

    // This Handler runs in the main UI thread
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == GET_URL) {
                final String url = (String) msg.obj;
                mWebView.loadUrl(url);
            } else if (msg.what == EXECUTE_SCRIPT) {
                executeScript((String) msg.obj);
            }
        }
    };

    public WebDriver(WebView webview) {
        if (!mWebView.getSettings().getJavaScriptEnabled()) {
            throw new RuntimeException("Javascript is disabled in the WebView. "
                    + "Enable it to use WebDriver");
        }
        shouldRunInMainThread(true);
        mSyncObject = new Object();
        this.mWebView = webview;
        WebchromeClientWrapper chromeWrapper = new WebchromeClientWrapper(
                webview.getWebChromeClient(), this);
        mWebView.setWebChromeClient(chromeWrapper);
        mWebView.addJavascriptInterface(new JavascriptResultReady(),
                "webdriver");
    }

    /**
     * Loads a URL in the WebView. This function is blocking and will return
     * when the page has finished loading.
     *
     * @param url The URL to load.
     */
    public void get(String url) {
        executeCommand(GET_URL, url, LOADING_TIMEOUT);
    }

    /**
     * @return The source page of the currently loaded page in WebView.
     */
    public String getPageSource() {
        executeCommand(EXECUTE_SCRIPT, "return (new XMLSerializer())"
                + ".serializeToString(document.documentElement);",
                JS_EXECUTION_TIMEOUT);
        return mJsResult;
    }

    private void executeScript(String script) {
        mWebView.loadUrl("javascript:" + script);
    }

    private void shouldRunInMainThread(boolean value) {
        assert (value == (MAIN_THREAD == Thread.currentThread().getId()));
    }

    /**
     * Interface called from JavaScript when the result is ready.
     */
    private class JavascriptResultReady {

        /**
         * A callback from JavaScript to Java that passes the result as a
         * parameter. This method is available from the WebView's
         * JavaScript DOM as window.webdriver.resultReady().
         *
         * @param result The result that should be sent to Java from Javascript.
         */
        public void resultReady(String result) {
            synchronized (mSyncObject) {
                mJsResult = result;
                mCommandDone = true;
                mSyncObject.notify();
            }
        }
    }

    /* package */ void notifyCommandDone() {
        synchronized (mSyncObject) {
            mCommandDone = true;
            mSyncObject.notify();
        }
    }

    /**
     * Executes the given command by posting a message to mHandler. This thread
     * will block until the command which runs in the main thread is done.
     *
     * @param command The command to run.
     * @param arg The argument for that command.
     * @param timeout A timeout in milliseconds.
     */
    private void executeCommand(int command, String arg, long timeout) {
        shouldRunInMainThread(false);

        synchronized (mSyncObject) {
            mCommandDone = false;
            Message msg = mHandler.obtainMessage(command);
            msg.obj = arg;
            mHandler.sendMessage(msg);

            long end = System.currentTimeMillis() + timeout;
            while (!mCommandDone) {
                if (System.currentTimeMillis() >= end) {
                    throw new RuntimeException("Timeout executing command.");
                }
                try {
                    mSyncObject.wait(timeout);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
+193 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 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 android.webkit.webdriver;

import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Message;
import android.view.View;
import android.webkit.ConsoleMessage;
import android.webkit.GeolocationPermissions;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebStorage;
import android.webkit.WebView;

/* package */ class WebchromeClientWrapper extends WebChromeClient {

    private final WebChromeClient mDelegate;
    private final WebDriver mDriver;

    public WebchromeClientWrapper(WebChromeClient delegate, WebDriver driver) {
        if (delegate == null) {
            this.mDelegate = new WebChromeClient();
        } else {
            this.mDelegate = delegate;
        }
        this.mDriver = driver;
    }

    @Override
    public void onProgressChanged(WebView view, int newProgress) {
        if (newProgress == 100) {
            mDriver.notifyCommandDone();
        }
        mDelegate.onProgressChanged(view, newProgress);
    }

    @Override
    public void onReceivedTitle(WebView view, String title) {
        mDelegate.onReceivedTitle(view, title);
    }

    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
        mDelegate.onReceivedIcon(view, icon);
    }

    @Override
    public void onReceivedTouchIconUrl(WebView view, String url,
            boolean precomposed) {
        mDelegate.onReceivedTouchIconUrl(view, url, precomposed);
    }

    @Override
    public void onShowCustomView(View view,
            CustomViewCallback callback) {
        mDelegate.onShowCustomView(view, callback);
    }

    @Override
    public void onHideCustomView() {
        mDelegate.onHideCustomView();
    }

    @Override
    public boolean onCreateWindow(WebView view, boolean dialog,
            boolean userGesture, Message resultMsg) {
        return mDelegate.onCreateWindow(view, dialog, userGesture, resultMsg);
    }

    @Override
    public void onRequestFocus(WebView view) {
        mDelegate.onRequestFocus(view);
    }

    @Override
    public void onCloseWindow(WebView window) {
        mDelegate.onCloseWindow(window);
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message,
            JsResult result) {
        return mDelegate.onJsAlert(view, url, message, result);
    }

    @Override
    public boolean onJsConfirm(WebView view, String url, String message,
            JsResult result) {
        return mDelegate.onJsConfirm(view, url, message, result);
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, String message,
            String defaultValue, JsPromptResult result) {
        return mDelegate.onJsPrompt(view, url, message, defaultValue, result);
    }

    @Override
    public boolean onJsBeforeUnload(WebView view, String url, String message,
            JsResult result) {
        return mDelegate.onJsBeforeUnload(view, url, message, result);
    }

    @Override
    public void onExceededDatabaseQuota(String url, String databaseIdentifier,
            long currentQuota, long estimatedSize, long totalUsedQuota,
            WebStorage.QuotaUpdater quotaUpdater) {
        mDelegate.onExceededDatabaseQuota(url, databaseIdentifier, currentQuota,
                estimatedSize, totalUsedQuota, quotaUpdater);
    }

    @Override
    public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
            WebStorage.QuotaUpdater quotaUpdater) {
        mDelegate.onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota,
                quotaUpdater);
    }

    @Override
    public void onGeolocationPermissionsShowPrompt(String origin,
            GeolocationPermissions.Callback callback) {
        mDelegate.onGeolocationPermissionsShowPrompt(origin, callback);
    }

    @Override
    public void onGeolocationPermissionsHidePrompt() {
        mDelegate.onGeolocationPermissionsHidePrompt();
    }

    @Override
    public boolean onJsTimeout() {
        return mDelegate.onJsTimeout();
    }

    @Override
    public void onConsoleMessage(String message, int lineNumber,
            String sourceID) {
        mDelegate.onConsoleMessage(message, lineNumber, sourceID);
    }

    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
        return mDelegate.onConsoleMessage(consoleMessage);
    }

    @Override
    public Bitmap getDefaultVideoPoster() {
        return mDelegate.getDefaultVideoPoster();
    }

    @Override
    public View getVideoLoadingProgressView() {
        return mDelegate.getVideoLoadingProgressView();
    }

    @Override
    public void getVisitedHistory(ValueCallback<String[]> callback) {
        mDelegate.getVisitedHistory(callback);
    }

    @Override
    public void openFileChooser(ValueCallback<Uri> uploadFile,
            String acceptType) {
        mDelegate.openFileChooser(uploadFile, acceptType);
    }

    @Override
    public void setInstallableWebApp() {
        mDelegate.setInstallableWebApp();
    }

    @Override
    public void setupAutoFill(Message msg) {
        mDelegate.setupAutoFill(msg);
    }
}