io.selendroid.server.model.SelendroidWebDriver.java Source code

Java tutorial

Introduction

Here is the source code for io.selendroid.server.model.SelendroidWebDriver.java

Source

/*
 * Copyright 2012-2014 eBay Software Foundation and selendroid committers.
 * 
 * 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 io.selendroid.server.model;

import io.selendroid.server.ServerInstrumentation;
import io.selendroid.server.android.AndroidTouchScreen;
import io.selendroid.server.android.KeySender;
import io.selendroid.server.android.MotionSender;
import io.selendroid.server.android.WebViewKeySender;
import io.selendroid.server.android.WebViewMotionSender;
import io.selendroid.server.android.internal.DomWindow;
import io.selendroid.server.common.exceptions.SelendroidException;
import io.selendroid.server.common.exceptions.StaleElementReferenceException;
import io.selendroid.server.model.internal.WebViewHandleMapper;
import io.selendroid.server.model.js.AndroidAtoms;
import io.selendroid.server.util.SelendroidLogger;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import org.apache.cordova.CordovaChromeClient;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaWebView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;

public class SelendroidWebDriver {
    private static final String ELEMENT_KEY = "ELEMENT";
    private static final long FOCUS_TIMEOUT = 1000L;
    private static final long LOADING_TIMEOUT = 30000L;
    private static final long POLLING_INTERVAL = 50L;
    private static final long START_LOADING_TIMEOUT = 700L;
    static final long UI_TIMEOUT = 3000L;
    private volatile boolean pageDoneLoading;
    private volatile boolean pageStartedLoading;
    private volatile String result;
    private volatile WebView webview = null;
    private static final String WINDOW_KEY = "WINDOW";
    private volatile boolean editAreaHasFocus;
    private final Object syncObject = new Object();
    private boolean done = false;
    private ServerInstrumentation serverInstrumentation = null;
    private SessionCookieManager sm = new SessionCookieManager();
    private WebChromeClient chromeClient = null;
    private DomWindow currentWindowOrFrame;
    private Queue<String> currentAlertMessage = new LinkedList<String>();
    private TouchScreen touch;
    private KeySender keySender;
    private MotionSender motionSender;
    private long scriptTimeout = 60000L;
    private long asyncScriptTimeout = 0L;
    private final String contextHandle;

    public SelendroidWebDriver(ServerInstrumentation serverInstrumentation, String handle) {
        this.contextHandle = WebViewHandleMapper.normalizeHandle(handle);
        this.serverInstrumentation = serverInstrumentation;
        init(handle);
        keySender = new WebViewKeySender(serverInstrumentation, webview);
    }

    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();
    }

    @SuppressWarnings("unchecked")
    private String convertToJsArgs(JSONArray args, KnownElements ke) throws JSONException {
        StringBuilder toReturn = new StringBuilder();

        int length = args.length();
        for (int i = 0; i < length; i++) {
            toReturn.append((i > 0) ? "," : "");
            toReturn.append(convertToJsArgs(args.get(i), ke));
        }
        SelendroidLogger.info("convertToJsArgs: " + toReturn.toString());
        return toReturn.toString();
    }

    private String convertToJsArgs(Object obj, KnownElements ke) throws JSONException {
        StringBuilder toReturn = new StringBuilder();
        if (obj == null || obj.equals(null)) {
            return "null";
        }
        if (obj instanceof JSONArray) {
            return convertToJsArgs((JSONArray) obj, ke);
        }
        if (obj instanceof List<?>) {
            toReturn.append("[");
            List<Object> aList = (List<Object>) obj;
            for (int j = 0; j < aList.size(); j++) {
                String comma = ((j == 0) ? "" : ",");
                toReturn.append(comma + convertToJsArgs(aList.get(j), ke));
            }
            toReturn.append("]");
        } else if (obj instanceof Map<?, ?>) {
            Map<Object, Object> aMap = (Map<Object, Object>) obj;
            String toAdd = "{";
            for (Object key : aMap.keySet()) {
                toAdd += key + ":" + convertToJsArgs(aMap.get(key), ke) + ",";
            }
            toReturn.append(toAdd.substring(0, toAdd.length() - 1) + "}");
        } else if (obj instanceof AndroidWebElement) {
            // A WebElement is represented in JavaScript by an Object as
            // follow: {"ELEMENT":"id"} where "id" refers to the id
            // of the HTML element in the javascript cache that can
            // be accessed throught bot.inject.cache.getCache_()
            toReturn.append("{\"" + ELEMENT_KEY + "\":\"" + ((AndroidWebElement) obj).getId() + "\"}");
        } else if (obj instanceof DomWindow) {
            // A DomWindow is represented in JavaScript by an Object as
            // follow {"WINDOW":"id"} where "id" refers to the id of the
            // DOM window in the cache.
            toReturn.append("{\"" + WINDOW_KEY + "\":\"" + ((DomWindow) obj).getKey() + "\"}");
        } else if (obj instanceof Number || obj instanceof Boolean) {
            toReturn.append(String.valueOf(obj));
        } else if (obj instanceof String) {
            toReturn.append(escapeAndQuote((String) obj));
        } else if (obj instanceof JSONObject) {
            if (((JSONObject) obj).has(ELEMENT_KEY)) {
                try {
                    AndroidElement ae = ke.get(((JSONObject) obj).getString(ELEMENT_KEY));
                    toReturn.append(ae.toString());
                } catch (JSONException e) {
                    SelendroidLogger.info("exception getting the element id: " + e.toString());
                }
            } else {
                // send across the object since it's not a webelement
                toReturn.append(obj.toString());
            }
        } else {
            SelendroidLogger.info("failed to figure out what this is to convert to execute script:" + obj);
        }
        SelendroidLogger.info("convertToJsArgs: " + toReturn.toString());
        return toReturn.toString();
    }

    public String getContextHandle() {
        return contextHandle;
    }

    public Object executeAtom(AndroidAtoms atom, KnownElements ke, Object... args) {
        JSONArray array = new JSONArray();
        for (int i = 0; i < args.length; i++) {
            array.put(args[i]);
        }
        try {
            return executeAtom(atom, array, ke);
        } catch (JSONException je) {
            SelendroidLogger.error("Failed to execute atom", je);
            throw new RuntimeException(je);
        }
    }

    public Object executeAtom(AndroidAtoms atom, JSONArray args, KnownElements ke) throws JSONException {
        final String myScript = atom.getValue();
        String scriptInWindow = "(function(){ " + " var win; try{win=" + getWindowString()
                + "}catch(e){win=window;}" + "with(win){return (" + myScript + ")(" + convertToJsArgs(args, ke)
                + ")}})()";
        String jsResult = executeJavascriptInWebView(
                "alert('selendroid<' + document.charset + '>:'+" + scriptInWindow + ")");

        SelendroidLogger.info("jsResult: " + jsResult);
        if (jsResult == null || "undefined".equals(jsResult)) {
            return null;
        }

        try {
            JSONObject json = new JSONObject(jsResult);
            if (0 != json.optInt("status")) {
                Object value = json.get("value");
                if ((value instanceof String && value.equals("Element does not exist in cache"))
                        || (value instanceof JSONObject && ((JSONObject) value).getString("message")
                                .equals("Element does not exist in cache"))) {
                    throw new StaleElementReferenceException(json.optString("value"));
                }
                throw new SelendroidException(json.optString("value"));
            }
            if (json.isNull("value")) {
                return null;
            } else {
                return json.get("value");
            }
        } catch (JSONException e) {
            throw new SelendroidException(e);
        }
    }

    private String executeJavascriptInWebView(final String script) {
        result = null;
        ServerInstrumentation.getInstance().getCurrentActivity().runOnUiThread(new Runnable() {
            public void run() {
                if (webview.getUrl() == null) {
                    return;
                }
                // seems to be needed
                webview.setWebChromeClient(chromeClient);
                webview.loadUrl("javascript:" + script);
            }
        });
        long timeout = System.currentTimeMillis() + scriptTimeout;
        synchronized (syncObject) {
            while (result == null && (System.currentTimeMillis() < timeout)) {
                try {
                    syncObject.wait(2000);
                } catch (InterruptedException e) {
                    throw new SelendroidException(e);
                }
            }

            return result;
        }
    }

    public Object executeScript(String script) {
        return injectJavascript(script, new JSONArray(), null);
    }

    public Object executeScript(String script, JSONArray args, KnownElements ke) {
        return injectJavascript(script, args, ke);
    }

    public Object executeScript(String script, Object args, KnownElements ke) {
        return injectJavascript(script, args, ke);
    }

    public String getCurrentUrl() {
        if (webview == null) {
            throw new SelendroidException("No open web view.");
        }
        long end = System.currentTimeMillis() + UI_TIMEOUT;
        final String[] url = new String[1];
        done = false;
        Runnable r = new Runnable() {
            public void run() {
                url[0] = webview.getUrl();
                synchronized (this) {
                    this.notify();
                }
            }
        };
        runSynchronously(r, UI_TIMEOUT);
        return url[0];
    }

    public void get(final String url) {
        serverInstrumentation.getCurrentActivity().runOnUiThread(new Runnable() {
            public void run() {
                webview.loadUrl(url);
            }
        });
        waitForPageToLoad();
    }

    public String getWindowSource() throws JSONException {
        JSONObject source = new JSONObject((String) executeScript(
                "return (new XMLSerializer()).serializeToString(document.documentElement);"));
        return source.getString("value");
    }

    protected void init(String handle) {
        SelendroidLogger.info("Selendroid webdriver init");

        webview = WebViewHandleMapper.getWebViewByHandle(handle);

        if (webview == null) {
            throw new SelendroidException("No webview found on current activity.");
        }
        configureWebView(webview);
        currentWindowOrFrame = new DomWindow("");
        motionSender = new WebViewMotionSender(webview, serverInstrumentation);
        touch = new AndroidTouchScreen(serverInstrumentation, motionSender);
    }

    public TouchScreen getTouch() {
        return touch;
    }

    KeySender getKeySender() {
        return keySender;
    }

    MotionSender getMotionSender() {
        return motionSender;
    }

    private void configureWebView(final WebView view) {
        ServerInstrumentation.getInstance().getCurrentActivity().runOnUiThread(new Runnable() {

            @Override
            public void run() {
                try {
                    view.clearCache(true);
                    view.clearFormData();
                    view.clearHistory();
                    view.setFocusable(true);
                    view.setFocusableInTouchMode(true);
                    view.setNetworkAvailable(true);
                    // need to check the class name rather than checking instanceof
                    // since when it is not an instanceof, it likely means the app under test
                    // does not contain the Cordova project and this will cause a RuntimeException
                    if (view.getClass().getSimpleName().equalsIgnoreCase("CordovaWebView")) {
                        CordovaWebView webview = (CordovaWebView) view;
                        CordovaInterface ci = null;
                        chromeClient = new ExtendedCordovaChromeClient(null, webview);
                    } else {
                        chromeClient = new SelendroidWebChromeClient();
                    }
                    view.setWebChromeClient(chromeClient);

                    WebSettings settings = view.getSettings();
                    settings.setJavaScriptCanOpenWindowsAutomatically(true);
                    settings.setSupportMultipleWindows(true);
                    settings.setBuiltInZoomControls(true);
                    settings.setJavaScriptEnabled(true);
                    settings.setAppCacheEnabled(true);
                    settings.setAppCacheMaxSize(10 * 1024 * 1024);
                    settings.setAppCachePath("");
                    settings.setDatabaseEnabled(true);
                    settings.setDomStorageEnabled(true);
                    settings.setGeolocationEnabled(true);
                    settings.setSaveFormData(false);
                    settings.setSavePassword(false);
                    settings.setRenderPriority(WebSettings.RenderPriority.HIGH);
                    // Flash settings
                    settings.setPluginState(WebSettings.PluginState.ON);

                    // Geo location settings
                    settings.setGeolocationEnabled(true);
                    settings.setGeolocationDatabasePath("/data/data/selendroid");
                } catch (Exception e) {
                    SelendroidLogger.error("Error configuring web view", e);
                }
            }
        });
    }

    private String getWindowString() {
        String window = "";
        if (!currentWindowOrFrame.getKey().equals("")) {
            window = "document['$wdc_']['" + currentWindowOrFrame.getKey() + "'] ||";
        }
        return (window += "window");
    }

    Object injectJavascript(String toExecute, Object args, KnownElements ke) {
        try {
            String executeScript = AndroidAtoms.EXECUTE_SCRIPT.getValue();
            toExecute = "var win_context; try{win_context= " + getWindowString() + "}catch(e){"
                    + "win_context=window;}with(win_context){" + toExecute + "}";
            String wrappedScript = "(function(){ var win; try{win=" + getWindowString() + "}catch(e){win=window}"
                    + "with(win){return (" + executeScript + ")(" + escapeAndQuote(toExecute) + ", ["
                    + convertToJsArgs(args, ke) + "], true)}})()";
            return executeJavascriptInWebView(
                    "alert('selendroid<' + document.charset + '>:'+" + wrappedScript + ")");
        } catch (JSONException e) {
            SelendroidLogger.error("Failed to convert args to jsArgs", e);
            throw new RuntimeException(e);
        }
    }

    Object injectAtomJavascript(String toExecute, Object args, KnownElements ke) throws JSONException {
        return executeJavascriptInWebView("alert('selendroid<' + document.charset +'>:'+ (" + toExecute + ")("
                + convertToJsArgs(args, ke) + "))");
    }

    public Object executeAsyncJavascript(String toExecute, JSONArray args, KnownElements ke) {
        try {
            String callbackFunction = "function(result){alert('selendroid<' + document.charset + '>:'+result);}";
            String script = "try {(" + AndroidAtoms.EXECUTE_ASYNC_SCRIPT.getValue() + ")("
                    + escapeAndQuote(toExecute) + ", [" + convertToJsArgs(args, ke) + "], " + asyncScriptTimeout
                    + ", " + callbackFunction + "," + "true, " + getWindowString()
                    + ")}catch(e){alert('selendroid<' + document.charset + '>:{\"status\":13,\"value\":\"' + e + '\"}')}";
            return executeJavascriptInWebView(script);
        } catch (JSONException je) {
            SelendroidLogger.error("Failed convert JSONArray to jsArgs", je);
            throw new RuntimeException(je);
        }
    }

    Boolean isInFrame() {
        return !currentWindowOrFrame.getKey().equals("");
    }

    void resetPageIsLoading() {
        pageStartedLoading = false;
        pageDoneLoading = false;
    }

    void setEditAreaHasFocus(boolean focused) {
        editAreaHasFocus = focused;
    }

    void waitForPageToLoad() {
        synchronized (syncObject) {
            long timeout = System.currentTimeMillis() + START_LOADING_TIMEOUT;
            while (!pageStartedLoading && (System.currentTimeMillis() < timeout)) {
                try {
                    syncObject.wait(POLLING_INTERVAL);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
            }

            long end = System.currentTimeMillis() + LOADING_TIMEOUT;
            while (!pageDoneLoading && pageStartedLoading && (System.currentTimeMillis() < end)) {
                try {
                    syncObject.wait(LOADING_TIMEOUT);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    void waitUntilEditAreaHasFocus() {
        long timeout = System.currentTimeMillis() + FOCUS_TIMEOUT;
        while (!editAreaHasFocus && (System.currentTimeMillis() < timeout)) {
            try {
                Thread.sleep(POLLING_INTERVAL);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public class ExtendedCordovaChromeClient extends CordovaChromeClient {

        public ExtendedCordovaChromeClient(CordovaInterface ctx, CordovaWebView app) {
            super(ctx, app);
        }

        /**
         * Unconventional way of adding a Javascript interface but the main reason why I took this way
         * is that it is working stable compared to the webview.addJavascriptInterface way.
         */
        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult jsResult) {
            if (message != null && message.startsWith("selendroid<")) {
                jsResult.confirm();

                synchronized (syncObject) {
                    String res = message.replaceFirst("selendroid<", "");
                    int i = res.indexOf(">:");
                    String enc = res.substring(0, i);
                    res = res.substring(i + 2);
                    /*
                     * Workaround for Japanese character encodings: Replace U+00A5 with backslash so that we
                     * can properly parse JSON strings contains backslash escapes, since WebKit maps 0x5C
                     * (used for character escaping in all of the Japanses character encodings) to U+00A5 (YEN
                     * SIGN) and breaks escape characters.
                     */
                    if (("EUC-JP".equals(enc) || "Shift_JIS".equals(enc) || "ISO-2022-JP".equals(enc))
                            && res.contains("\u00a5")) {
                        SelendroidLogger.info("Perform workaround for japanese character encodings");
                        SelendroidLogger.debug("Original String: " + res);
                        res = res.replace("\u00a5", "\\");
                        SelendroidLogger.debug("Replaced result: " + res);
                    }
                    result = res;
                    syncObject.notify();
                }

                return true;
            } else {
                currentAlertMessage.add(message == null ? "null" : message);
                SelendroidLogger.info("new alert message: " + message);
                return super.onJsAlert(view, url, message, jsResult);
            }
        }
    }

    public class SelendroidWebChromeClient extends WebChromeClient {

        /**
         * Unconventional way of adding a Javascript interface but the main reason why I took this way
         * is that it is working stable compared to the webview.addJavascriptInterface way.
         */
        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult jsResult) {
            if (message != null && message.startsWith("selendroid<")) {
                jsResult.confirm();

                synchronized (syncObject) {
                    String res = message.replaceFirst("selendroid<", "");
                    int i = res.indexOf(">:");
                    String enc = res.substring(0, i);
                    res = res.substring(i + 2);
                    /*
                     * Workaround for Japanese character encodings: Replace U+00A5 with backslash so that we
                     * can properly parse JSON strings contains backslash escapes, since WebKit maps 0x5C
                     * (used for character escaping in all of the Japanses character encodings) to U+00A5 (YEN
                     * SIGN) and breaks escape characters.
                     */
                    if (("EUC-JP".equals(enc) || "Shift_JIS".equals(enc) || "ISO-2022-JP".equals(enc))
                            && res.contains("\u00a5")) {
                        SelendroidLogger.info("Perform workaround for japanese character encodings");
                        SelendroidLogger.debug("Original String: " + res);
                        res = res.replace("\u00a5", "\\");
                        SelendroidLogger.debug("Replaced result: " + res);
                    }
                    result = res;
                    syncObject.notify();
                }

                return true;
            } else {
                currentAlertMessage.add(message == null ? "null" : message);
                SelendroidLogger.info("new alert message: " + message);
                return super.onJsAlert(view, url, message, jsResult);
            }
        }

        @Override
        public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
            currentAlertMessage.add(message == null ? "null" : message);
            SelendroidLogger.info("new confirm message: " + message);
            return super.onJsConfirm(view, url, message, result);
        }

        @Override
        public boolean onJsPrompt(WebView view, String url, String message, String defaultValue,
                JsPromptResult result) {
            currentAlertMessage.add(message == null ? "null" : message);
            SelendroidLogger.info("new prompt message: " + message);
            return super.onJsPrompt(view, url, message, defaultValue, result);
        }

    }

    public String getTitle() {
        if (webview == null) {
            throw new SelendroidException("No open web view.");
        }
        long end = System.currentTimeMillis() + UI_TIMEOUT;
        final String[] title = new String[1];
        done = false;
        serverInstrumentation.getCurrentActivity().runOnUiThread(new Runnable() {
            public void run() {
                synchronized (syncObject) {
                    title[0] = webview.getTitle();
                    done = true;
                    syncObject.notify();
                }
            }
        });
        waitForDone(end, UI_TIMEOUT, "Failed to get title");
        return title[0];
    }

    private void waitForDone(long end, long timeout, String error) {
        synchronized (syncObject) {
            while (!done && System.currentTimeMillis() < end) {
                try {
                    syncObject.wait(timeout);
                } catch (InterruptedException e) {
                    throw new SelendroidException(error, e);
                }
            }
        }
    }

    private void runSynchronously(Runnable r, long timeout) {
        synchronized (r) {
            serverInstrumentation.getCurrentActivity().runOnUiThread(r);
            try {
                r.wait(timeout);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public WebView getWebview() {
        return webview;
    }

    public Set<Cookie> getCookies(String url) {

        return sm.getAllCookies(url);

    }

    public void removeAllCookie(String url) {

        sm.removeAllCookies(url);

    }

    public void remove(String url, String name) {

        sm.remove(url, name);

    }

    public void setCookies(String url, Cookie cookie) {

        sm.addCookie(url, cookie);

    }

    public void frame(int index) throws JSONException {
        currentWindowOrFrame = processFrameExecutionResult(
                injectAtomJavascript(AndroidAtoms.FRAME_BY_INDEX.getValue(), index, null));
    }

    public void frame(String frameNameOrId) throws JSONException {
        currentWindowOrFrame = processFrameExecutionResult(
                injectAtomJavascript(AndroidAtoms.FRAME_BY_ID_OR_NAME.getValue(), frameNameOrId, null));
    }

    public void frame(AndroidWebElement frameElement) {
        currentWindowOrFrame = processFrameExecutionResult(
                executeScript("return arguments[0].contentWindow;", frameElement, null));
    }

    public void switchToDefaultContent() {
        currentWindowOrFrame = new DomWindow("");
    }

    private DomWindow processFrameExecutionResult(Object result) {
        if (result == null || "undefined".equals(result)) {
            return null;
        }
        try {
            JSONObject json = new JSONObject((String) result);
            JSONObject value = json.getJSONObject("value");
            return new DomWindow(value.getString("WINDOW"));
        } catch (JSONException e) {
            throw new RuntimeException("Failed to parse JavaScript result: " + result.toString(), e);
        }
    }

    public void back() {
        pageDoneLoading = false;
        runSynchronously(new Runnable() {
            public void run() {
                webview.goBack();
            }
        }, 500);
        waitForPageToLoad();
    }

    public void forward() {
        pageDoneLoading = false;
        runSynchronously(new Runnable() {
            public void run() {
                webview.goForward();
            }
        }, 500);
        waitForPageToLoad();
    }

    public void refresh() {
        pageDoneLoading = false;
        runSynchronously(new Runnable() {
            public void run() {
                webview.reload();
            }
        }, 500);
        waitForPageToLoad();
    }

    public boolean isAlertPresent() {
        SelendroidLogger.info("checking currentAlertMessage: " + currentAlertMessage.size());
        return !currentAlertMessage.isEmpty();
    }

    public String getCurrentAlertMessage() {
        SelendroidLogger.info("getting currentAlertMessage: " + currentAlertMessage.peek());
        return currentAlertMessage.peek();
    }

    public void clearCurrentAlertMessage() {
        SelendroidLogger.info("clearing the current alert message: " + currentAlertMessage.remove());
    }

    public void setAsyncScriptTimeout(long timeout) {
        asyncScriptTimeout = timeout;
    }
}