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

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

Merge "Adding WebElement.java which provides DOM friendly API function to...

Merge "Adding WebElement.java which provides DOM friendly API function to lookup HTML elements in the page."
parents 50e657bb 3f7480dc
Loading
Loading
Loading
Loading
+209 −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;

/**
 * Mechanism to locate elements within the DOM of the page.
 * @hide
 */
public abstract class By {
    public abstract WebElement findElement(WebElement element);

    /**
     * Locates an element by its HTML id attribute.
     *
     * @param id The HTML id attribute to look for.
     * @return A By instance that locates elements by their HTML id attributes.
     */
    public static By id(final String id) {
        throwIfNull(id);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementById(id);
            }

            @Override
            public String toString() {
                return "By.id: " + id;
            }
        };
    }

    /**
     * Locates an element by the matching the exact text on the HTML link.
     *
     * @param linkText The exact text to match against.
     * @return A By instance that locates elements by the text displayed by
     * the link.
     */
    public static By linkText(final String linkText) {
        throwIfNull(linkText);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementByLinkText(linkText);
            }

            @Override
            public String toString() {
                return "By.linkText: " + linkText;
            }
        };
    }

    /**
     * Locates an element by matching partial part of the text displayed by an
     * HTML link.
     *
     * @param linkText The text that should be contained by the text displayed
     * on the link.
     * @return A By instance that locates elements that contain the given link
     * text.
     */
    public static By partialLinkText(final String linkText) {
        throwIfNull(linkText);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementByPartialLinkText(linkText);
            }

            @Override
            public String toString() {
                return "By.partialLinkText: " + linkText;
            }
        };
    }

    /**
     * Locates an element by matching its HTML name attribute.
     *
     * @param name The value of the HTML name attribute.
     * @return A By instance that locates elements by the HTML name attribute.
     */
    public static By name(final String name) {
        throwIfNull(name);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementByName(name);
            }

            @Override
            public String toString() {
                return "By.name: " + name;
            }
        };
    }

    /**
     * Locates an element by matching its class name.
     * @param className The class name
     * @return A By instance that locates elements by their class name attribute.
     */
    public static By className(final String className) {
        throwIfNull(className);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementByClassName(className);
            }

            @Override
            public String toString() {
                return "By.className: " + className;
            }
        };
    }

    /**
     * Locates an element by matching its css property.
     *
     * @param css The css property.
     * @return A By instance that locates elements by their css property.
     */
    public static By css(final String css) {
        throwIfNull(css);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementByCss(css);
            }

            @Override
            public String toString() {
                return "By.css: " + css;
            }
        };
    }

    /**
     * Locates an element by matching its HTML tag name.
     *
     * @param tagName The HTML tag name to look for.
     * @return A By instance that locates elements using the name of the
     * HTML tag.
     */
    public static By tagName(final String tagName) {
        throwIfNull(tagName);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementByTagName(tagName);
            }

            @Override
            public String toString() {
                return "By.tagName: " + tagName;
            }
        };
    }

    /**
     * Locates an element using an XPath expression.
     *
     * <p>When using XPath, be aware that this follows standard conventions: a
     * search prefixed with "//" will search the entire document, not just the
     * children of the current node. Use ".//" to limit your search to the
     * children of this {@link android.webkit.webdriver.WebElement}.
     *
     * @param xpath The XPath expression to use.
     * @return A By instance that locates elements using the given XPath.
     */
    public static By xpath(final String xpath) {
        throwIfNull(xpath);
        return new By() {
            @Override
            public WebElement findElement(WebElement element) {
                return element.findElementByXPath(xpath);
            }

            @Override
            public String toString() {
                return "By.xpath: " + xpath;
            }
        };
    }

    private static void throwIfNull(String argument) {
        if (argument == null) {
            throw new IllegalArgumentException(
                    "Cannot find elements with null locator.");
        }
    }
}
+340 −30
Original line number Diff line number Diff line
@@ -16,20 +16,27 @@

package android.webkit.webdriver;

import android.graphics.Bitmap;
import android.net.Uri;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;

import com.android.internal.R;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

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;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Drives a web application by controlling the WebView. This class
 * provides a DOM-like API allowing to get information about the page,
@@ -68,27 +75,34 @@ import android.webkit.WebView;
 *       mDriver = getActivity().getDriver();
 *   }
 *
 *   public void testGetIsBlocking() {
 *   public void testGoogle() {
 *       mDriver.get("http://google.com");
 *       assertTrue(mDriver.getPageSource().startsWith("<html"));
 *       WebElement searchBox = mDriver.findElement(By.name("q"));
 *       q.sendKeys("Cheese!");
 *       q.submit();
 *       assertTrue(mDriver.findElements(By.partialLinkText("Cheese")).size() > 0);
 *   }
 *}
 *
 * @hide
 */
public class WebDriver {
    // Timeout for page load in milliseconds
    // Timeout for page load in milliseconds.
    private static final int LOADING_TIMEOUT = 30000;
    // Timeout for executing JavaScript in the WebView in milliseconds
    // 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 int CMD_GET_URL = 1;
    private static final int CMD_EXECUTE_SCRIPT = 2;

    private static final String ELEMENT_KEY = "ELEMENT";
    private static final String STATUS = "status";
    private static final String VALUE = "value";

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

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

    // Used for synchronization
@@ -99,30 +113,84 @@ public class WebDriver {

    private WebView mWebView;

    // This Handler runs in the main UI thread
    // This WebElement represents the object document.documentElement
    private WebElement mDocumentElement;

    // 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) {
            if (msg.what == CMD_GET_URL) {
                final String url = (String) msg.obj;
                mWebView.loadUrl(url);
            } else if (msg.what == EXECUTE_SCRIPT) {
                executeScript((String) msg.obj);
            } else if (msg.what == CMD_EXECUTE_SCRIPT) {
                mWebView.loadUrl("javascript:" + (String) msg.obj);
            }
        }
    };

    /**
     * Error codes from the WebDriver wire protocol
     * http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
     */
    private enum ErrorCode {
        SUCCESS(0),
        NO_SUCH_ELEMENT(7),
        NO_SUCH_FRAME(8),
        UNKNOWN_COMMAND(9),
        UNSUPPORTED_OPERATION(9),  // Alias
        STALE_ELEMENT_REFERENCE(10),
        ELEMENT_NOT_VISISBLE(11),
        INVALID_ELEMENT_STATE(12),
        UNKNOWN_ERROR(13),
        ELEMENT_NOT_SELECTABLE(15),
        XPATH_LOOKUP_ERROR(19),
        NO_SUCH_WINDOW(23),
        INVALID_COOKIE_DOMAIN(24),
        UNABLE_TO_SET_COOKIE(25),
        MODAL_DIALOG_OPENED(26),
        MODAL_DIALOG_OPEN(27),
        SCRIPT_TIMEOUT(28);

        private final int mCode;
        private static ErrorCode[] values = ErrorCode.values();

        ErrorCode(int code) {
            this.mCode = code;
        }

        public int getCode() {
            return mCode;
        }

        public static ErrorCode get(final int intValue) {
            for (int i = 0; i < values.length; i++) {
                if (values[i].getCode() == intValue) {
                    return values[i];
                }
            }
            throw new IllegalArgumentException(intValue
                    + " does not map to any ErrorCode.");
        }
    }

    public WebDriver(WebView webview) {
        this.mWebView = webview;
        if (mWebView == null) {
            throw new IllegalArgumentException("WebView cannot be null");
        }
        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);
        mDocumentElement = new WebElement(this, "");
        mWebView.addJavascriptInterface(new JavascriptResultReady(),
                "webdriver");
    }
@@ -134,21 +202,263 @@ public class WebDriver {
     * @param url The URL to load.
     */
    public void get(String url) {
        executeCommand(GET_URL, url, LOADING_TIMEOUT);
        executeCommand(CMD_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);",
        return (String) executeScript("return new XMLSerializer()."
                + "serializeToString(document);");
    }

    /**
     * Find the first {@link android.webkit.webdriver.WebElement} using the
     * given method.
     *
     * @param by The locating mechanism to use.
     * @return The first matching element on the current context.
     * @throws {@link android.webkit.webdriver.WebElementNotFoundException} if
     * no matching element was found.
     */
    public WebElement findElement(By by) {
        return by.findElement(mDocumentElement);
    }

    /**
     * Clears the WebView.
     */
    public void quit() {
        mWebView.clearCache(true);
        mWebView.clearFormData();
        mWebView.clearHistory();
        mWebView.clearSslPreferences();
        mWebView.clearView();
    }

    /**
     * Executes javascript in the context of the main frame.
     *
     * If the script has a return value the following happens:
     * <ul>
     * <li>For an HTML element, this method returns a WebElement</li>
     * <li>For a decimal, a Double is returned</li>
     * <li>For non-decimal number, a Long is returned</li>
     * <li>For a boolean, a Boolean is returned</li>
     * <li>For all other cases, a String is returned</li>
     * <li>For an array, this returns a List<Object> with each object
     * following the rules above.</li>
     * <li>For an object literal this returns a Map<String, Object>. Note that
     * Object literals keys can only be Strings. Non Strings keys will
     * be filtered out.</li>
     * </ul>
     *
     * <p> Arguments must be a number, a boolean, a string a WebElement or
     * a list of any combination of the above. The arguments will be made
     * available to the javascript via the "arguments" magic variable,
     * as if the function was called via "Function.apply".
     *
     * @param script The JavaScript to execute.
     * @param args The arguments to the script. Can be any of a number, boolean,
     * string, WebElement or a List of those.
     * @return A Boolean, Long, Double, String, WebElement, List or null.
     */
    public Object executeScript(final String script, final Object... args) {
        String scriptArgs = "[" + convertToJsArgs(args) + "]";
        String injectScriptJs = getResourceAsString(R.raw.execute_script_android);
        return executeRawJavascript("(" + injectScriptJs +
                ")(" + escapeAndQuote(script) + ", " + scriptArgs + ", true)");
    }

    /**
     * Converts the arguments passed to a JavaScript friendly format.
     *
     * @param args The arguments to convert.
     * @return Comma separated Strings containing the arguments.
     */
    /*package*/ String convertToJsArgs(final Object... args) {
        StringBuilder toReturn = new StringBuilder();
        int length = args.length;
        for (int i = 0; i < length; i++) {
            toReturn.append((i > 0) ? "," : "");
            if (args[i] instanceof List<?>) {
                toReturn.append("[");
                List<Object> aList = (List<Object>) args[i];
                for (int j = 0 ; j < aList.size(); j++) {
                    String comma = ((j == 0) ? "" : ",");
                    toReturn.append(comma + convertToJsArgs(aList.get(j)));
                }
                toReturn.append("]");
            } else if (args[i] instanceof Map<?, ?>) {
                Map<Object, Object> aMap = (Map<Object, Object>) args[i];
                String toAdd = "{";
                for (Object key: aMap.keySet()) {
                    toAdd += key + ":"
                            + convertToJsArgs(aMap.get(key)) + ",";
                }
                toReturn.append(toAdd.substring(0, toAdd.length() -1) + "}");
            } else if (args[i] instanceof WebElement) {
                // WebElement are represented in JavaScript by Objects as
                // follow: {ELEMENT:"id"}
                toReturn.append("{" + ELEMENT_KEY + ":\""
                        + ((WebElement) args[i]).getId() + "\"}");
            } else if (args[i] instanceof Number || args[i] instanceof Boolean) {
                toReturn.append(String.valueOf(args[i]));
            } else if (args[i] instanceof String) {
                toReturn.append(escapeAndQuote((String) args[i]));
            } else {
                throw new IllegalArgumentException(
                        "Javascript arguments can be "
                            + "a Number, a Boolean, a String, a WebElement, "
                            + "or a List or a Map of those. Got: "
                            + ((args[i] == null) ? "null" : args[i].toString()));
            }
        }
        return toReturn.toString();
    }

    /*package*/ Object executeRawJavascript(final String script) {
        String result = executeCommand(CMD_EXECUTE_SCRIPT,
                "window.webdriver.resultReady(" + script + ")",
                JS_EXECUTION_TIMEOUT);
        return mJsResult;
        try {
            JSONObject json = new JSONObject(result);
            throwIfError(json);
            Object value = json.get(VALUE);
            return convertJsonToJavaObject(value);
        } catch (JSONException e) {
            throw new RuntimeException("Failed to parse JavaScript result: "
                    + result.toString(), e);
        }
    }

    /*package*/ String getResourceAsString(final int resourceId) {
        InputStream is = mWebView.getResources().openRawResource(resourceId);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        StringBuilder sb = new StringBuilder();
        String line = null;
        try {
            while ((line = br.readLine()) != null) {
                sb.append(line);
            }
            br.close();
            is.close();
        } catch (IOException e) {
            throw new RuntimeException("Failed to open JavaScript resource.", e);
        }
        return sb.toString();
    }

    /**
     * Wraps the given string into quotes and escape existing quotes
     * and backslashes.
     * "foo" -> "\"foo\""
     * "foo\"" -> "\"foo\\\"\""
     * "fo\o" -> "\"fo\\o\""
     *
     * @param toWrap The String to wrap in quotes
     * @return a String wrapping the original String in quotes
     */
    private static String escapeAndQuote(final String toWrap) {
        StringBuilder toReturn = new StringBuilder("\"");
        for (int i = 0; i < toWrap.length(); i++) {
            char c = toWrap.charAt(i);
            if (c == '\"') {
                toReturn.append("\\\"");
            } else if (c == '\\') {
                toReturn.append("\\\\");
            } else {
                toReturn.append(c);
            }
        }
        toReturn.append("\"");
        return toReturn.toString();
    }

    private void executeScript(String script) {
        mWebView.loadUrl("javascript:" + script);
    private Object convertJsonToJavaObject(final Object toConvert) {
        try {
            if (toConvert == null
                    || toConvert.equals(null)
                    || "undefined".equals(toConvert)
                    || "null".equals(toConvert)) {
                return null;
            } else if (toConvert instanceof Boolean) {
                return toConvert;
            } else if (toConvert instanceof Double
                    || toConvert instanceof Float) {
                return Double.valueOf(String.valueOf(toConvert));
            } else if (toConvert instanceof Integer
                    || toConvert instanceof Long) {
              return Long.valueOf(String.valueOf(toConvert));
            } else if (toConvert instanceof JSONArray) { // List
                return convertJsonArrayToList((JSONArray) toConvert);
            } else if (toConvert instanceof JSONObject) { // Map or WebElment
                JSONObject map = (JSONObject) toConvert;
                if (map.opt(ELEMENT_KEY) != null) { // WebElement
                    return new WebElement(this, (String) map.get(ELEMENT_KEY));
                } else { // Map
                    return convertJsonObjectToMap(map);
                }
            } else {
                return toConvert.toString();
            }
        } catch (JSONException e) {
            throw new RuntimeException("Failed to parse JavaScript result: "
                    + toConvert.toString(), e);
        }
    }

    private List<Object> convertJsonArrayToList(final JSONArray json) {
        List<Object> toReturn = Lists.newArrayList();
        for (int i = 0; i < json.length(); i++) {
            try {
                toReturn.add(convertJsonToJavaObject(json.get(i)));
            } catch (JSONException e) {
                throw new RuntimeException("Failed to parse JSON: "
                        + json.toString(), e);
            }
        }
        return toReturn;
    }

    private Map<Object, Object> convertJsonObjectToMap(final JSONObject json) {
        Map<Object, Object> toReturn = Maps.newHashMap();
        for (Iterator it = json.keys(); it.hasNext();) {
            String key = (String) it.next();
            try {
                Object value = json.get(key);
                toReturn.put(convertJsonToJavaObject(key),
                        convertJsonToJavaObject(value));
            } catch (JSONException e) {
                throw new RuntimeException("Failed to parse JSON:"
                        + json.toString(), e);
            }
        }
        return toReturn;
    }

    private void throwIfError(final JSONObject jsonObject) {
        ErrorCode status;
        String errorMsg;
        try {
            status = ErrorCode.get((Integer) jsonObject.get(STATUS));
            errorMsg  = String.valueOf(jsonObject.get(VALUE));
        } catch (JSONException e) {
            throw new RuntimeException("Failed to parse JSON Object: "
                    + jsonObject, e);
        }
        switch (status) {
            case SUCCESS:
                return;
            case NO_SUCH_ELEMENT:
                throw new WebElementNotFoundException("Could not find "
                        + "WebElement.");
            case STALE_ELEMENT_REFERENCE:
                throw new WebElementStaleException("WebElement is stale.");
            default:
                throw new RuntimeException("Error: " + errorMsg);
        }
    }

    private void shouldRunInMainThread(boolean value) {
@@ -167,7 +477,7 @@ public class WebDriver {
         *
         * @param result The result that should be sent to Java from Javascript.
         */
        public void resultReady(String result) {
        public void resultReady(final String result) {
            synchronized (mSyncObject) {
                mJsResult = result;
                mCommandDone = true;
@@ -191,9 +501,8 @@ public class WebDriver {
     * @param arg The argument for that command.
     * @param timeout A timeout in milliseconds.
     */
    private void executeCommand(int command, String arg, long timeout) {
    private String executeCommand(int command, final Object arg, long timeout) {
        shouldRunInMainThread(false);

        synchronized (mSyncObject) {
            mCommandDone = false;
            Message msg = mHandler.obtainMessage(command);
@@ -212,5 +521,6 @@ public class WebDriver {
                }
            }
        }
        return mJsResult;
    }
}
+136 −0

File added.

Preview size limit exceeded, changes collapsed.

+41 −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;

/**
 * Thrown when a {@link android.webkit.webdriver.WebElement} is not found in the
 * DOM of the page.
 * @hide
 */
public class WebElementNotFoundException extends RuntimeException {

    public WebElementNotFoundException() {
        super();
    }

    public WebElementNotFoundException(String reason) {
        super(reason);
    }

    public WebElementNotFoundException(String reason, Throwable cause) {
        super(reason, cause);
    }

    public WebElementNotFoundException(Throwable cause) {
        super(cause);
    }
}
+42 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading