/* MonkeyTalk - a cross-platform functional testing tool Copyright (C) 2012 Gorilla Logic, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.gorillalogic.fonemonkey.automators; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; import org.json.JSONException; import org.json.JSONObject; import android.webkit.WebView; import com.gorillalogic.fonemonkey.Log; import com.gorillalogic.fonemonkey.web.HtmlElement; import com.gorillalogic.fonemonkey.web.WebAutomationManager; import com.gorillalogic.fonemonkey.web.WebChromeClientWrapper; import com.gorillalogic.fonemonkey.web.WebViewRecorder; import com.gorillalogic.monkeytalk.automators.AutomatorConstants; public class WebViewAutomator extends ViewAutomator { private static long JS_TIMEOUT = 100; private static boolean jsPopupOpen; private WebViewRecorder recorder; private static ArrayBlockingQueue<Result> jsResult = new ArrayBlockingQueue<Result>(1); static { Log.log("Initializing WebViewAutomator"); } @Override public String getComponentType() { return "WebView"; } @Override public Class<?> getComponentClass() { return WebView.class; } public WebView getWebView() { return (WebView) getComponent(); } public WebViewRecorder getRecorder() { return recorder; } @Override public boolean installDefaultListeners() { getWebView().getSettings().setJavaScriptEnabled(true); getWebView().getSettings().setJavaScriptCanOpenWindowsAutomatically(true); recorder = new WebViewRecorder(getWebView()); // not supported in 4.2.2 // new WebViewClientWrapper(getWebView()); new WebChromeClientWrapper(this); return super.installDefaultListeners(); } @Override public boolean forSubtypeOf(String componentType) { // A webview might contain the componentType return true; } // private static final Object syncObject = new Object(); /** * Run the supplied Javascript statements in a WebView and (synchronously) return any returned * value as a string. * * @param script * @return */ public String runJavaScript(final String script) { if (isJsPopupOpen()) { return null; } Log.log("Running " + script); AutomationManager.runOnUIThread(new Runnable() { public void run() { final String lib = fileToString("monkeytalk.js"); // String s = // "javascript:window.location = \"http://mtdummy?monkeytalkresult=\" + (function(){" // + lib + script + "})()"; // runJS(script, lib); } }); long timeout = System.currentTimeMillis() + JS_TIMEOUT; String result = waitForJSResult(timeout); return result; } private void runJS(final String script, final String lib) { // The following 2 lines actually execute the function. (We monitor the console log // and execute log messages starting with "monkeytalk:") String s = "javascript:console.log('monkeytalk:' + (function(){" + lib + script + "})())"; getWebView().loadUrl(s); } private String waitForJSResult(long timeout) { Result result = null; try { result = jsResult.poll(JS_TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException e) { } if (result == null) { throw new IllegalArgumentException("Timed out waiting for javascript"); } if (result.error) { throw new IllegalArgumentException(result.result); } return result.result; } /** * Execute a JS expression that returns a list of html elements * * @param jsElemsExpr * @return HtmlElement represenations of the corresponding html elements in the webview */ public List<HtmlElement> findHtmlElements(String jsElemsExpr) { // String encodedResult = this.runJavaScript("return window.monkeytalk.encodeElements(" // + jsElemsExpr + ");"); String encodedResult = this.runJavaScript("return MonkeyTalk.getElement(" + jsElemsExpr + ");"); return this.decodeJsResult(encodedResult); } protected void assureMonkeyTalkInjected() { // make sure MonkeyTalk is linked into the webview String result = runJavaScript("return typeof MonkeyTalk != 'undefined'"); boolean isMonkeyTalkInjected = Boolean.valueOf(result); if (!isMonkeyTalkInjected) { WebViewRecorder.attachJs(getWebView()); } } public HtmlElement findElementByXpath(String xpathExpression) { return this.findElementByXpath(1, xpathExpression); } public HtmlElement findElementByXpath(int ordinal, String xpathExpression) { assureMonkeyTalkInjected(); String json = this.runJavaScript("return MonkeyTalk.getElementFromXpathWithOrdinal(\"" + xpathExpression + "\"," + ordinal + ");"); if (json == null || json.equalsIgnoreCase("null")) { Log.log("Unable to find " + componentType + " \"" + monkeyId + "\" with xpathExpression=" + xpathExpression); return null; } HtmlElement element = this.decodeJsonElement(json); element.putAttr("xpath", xpathExpression); return element; } public HtmlElement findElement(String jsElemsExpr, String componentType, String monkeyId) { // String encodedResult = this.runJavaScript("return window.monkeytalk.encodeElements(" // + jsElemsExpr + ");"); // wait until the webview has completely loaded to playback if (getWebView().getProgress() < 100) throw new IllegalArgumentException("Unable to find " + componentType + " \"" + monkeyId + "\""); Log.log("expr: " + jsElemsExpr); String json = this.runJavaScript("return MonkeyTalk.getElement(" + jsElemsExpr + ",'" + monkeyId + "','" + componentType + "');"); if (json == null || json.equalsIgnoreCase("null")) { Log.log("Unable to find " + componentType + " \"" + monkeyId + "\" with jsElemsExpr=" + jsElemsExpr); return null; } return this.decodeJsonElement(json); } public HtmlElement findCell(HtmlElement table, String cellId) { // String encodedResult = this.runJavaScript("return window.monkeytalk.encodeElements(" // + jsElemsExpr + ");"); // wait until the webview has completely loaded to playback String jsElemsExpr = "document.getElementsByTagName(\'" + table.getTagName() + "')"; if (getWebView().getProgress() < 100) throw new IllegalArgumentException("Unable to find " + componentType + " \"" + monkeyId + "\""); Log.log("expr: " + jsElemsExpr); String json = this.runJavaScript("return MonkeyTalk.getCell(" + jsElemsExpr + ",'" + table.getMonkeyId() + "','" + cellId + "');"); if (json == null || json.equalsIgnoreCase("null")) { Log.log("not found"); return null; } return this.decodeJsonElement(json); } public HtmlElement decodeJsonElement(String json) { HtmlElement element = null; try { JSONObject jsonObject = new JSONObject(json); element = new HtmlElement(this.getWebView()); Iterator<?> keys = jsonObject.keys(); while (keys.hasNext()) { String key = (String) keys.next(); element.putAttr(key, jsonObject.getString(key)); } } catch (JSONException e) { // throw new IllegalArgumentException("Unable to find " + componentType + " \"" + // monkeyId // + "\""); } return element; } public HtmlElement findNthElement(String jsElemsExpr, String componentType, int n) { // String encodedResult = this.runJavaScript("return window.monkeytalk.encodeElements(" // + jsElemsExpr + ");"); // wait until the webview has completely loaded to playback if (getWebView().getProgress() < 100) throw new IllegalArgumentException("Unable to find " + componentType); String json = this.runJavaScript("return MonkeyTalk.getElement(" + jsElemsExpr + ",'#" + n + "','" + componentType + "');"); HtmlElement element = null; try { JSONObject jsonObject = new JSONObject(json); element = new HtmlElement(this.getWebView()); for (String field : fields) { element.putAttr(field, jsonObject.getString(field)); } } catch (JSONException e) { throw new IllegalArgumentException("Unable to find " + componentType); } return element; } private static class Result { private String result = null; private boolean error = false; Result(String result) { this.result = result; } Result(String msg, boolean error) { result = msg; this.error = error; } } /** * Notify waiting caller that called JS is done * * @param result */ public static void reportResult(String result) { Log.log("putting result " + result); try { jsResult.put(new Result(result)); } catch (InterruptedException e) { Log.log(e); } } /** * Notify waiting caller that called JS errored out * * @param result */ public static void reportError(String result) { Log.log("Putting error " + result); try { jsResult.put(new Result(result, true)); } catch (InterruptedException e) { Log.log(e); } } public HtmlElement findHtmlElement(String componentType, String monkeyId, int index) { if (isJsPopupOpen()) { return null; } return WebAutomationManager.findHtmlElement(getWebView(), componentType, monkeyId, index); } /** * Include the JavaScript source in the webview * * @param fileName * name of resource file containing the source */ public void includeJs(String fileName) { final String s = fileToString(fileName); runJavaScript(s); } public static String fileToString(String fileName) { InputStream is = Thread.currentThread().getContextClassLoader() .getResourceAsStream(fileName); if (is == null) { String msg = "WebViewAutomator: Unable to read file " + fileName; Log.log(msg); throw new IllegalArgumentException(msg); } Writer writer = new StringWriter(); char[] buffer = new char[1024]; try { try { Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); int n = 0; while ((n = reader.read(buffer)) != -1) { writer.write(buffer, 0, n); } } finally { is.close(); } } catch (Exception e) { Log.log("Unexpected exception reading " + fileName + ": " + e.getMessage()); } return writer.toString(); } public void setJsPopupOpen(boolean b) { this.jsPopupOpen = b; } public boolean isJsPopupOpen() { return this.jsPopupOpen; } public static String[] getDefaultFields() { return fields; } private static String[] fields = { "monkeyId", "tagName", "id", "name", "className", "value", "textContent", "type", "x", "y", "width", "height", "title" }; private static String[] fields1 = { "tagName", "id", "name", "className", "value", "textContent", "type", "x", "y", "width", "height", "title" }; /** * Convert the delimited string representation of a list of HtmlElements to an actual list * * @param s * the delimited string encoded * @return the list of HtmlElements decoded from the string */ public List<HtmlElement> decodeJsResult(String s) { return decodeJsResult(s, fields1); } public List<HtmlElement> decodeJsResult(String s, String[] fields) { // getWebView().loadUrl("MonkeyTalkGetAllElements();"); List<HtmlElement> list = new ArrayList<HtmlElement>(); if (s == null) { return list; } String[] elems = s.split("<-mte->"); for (String elem : elems) { String[] attrs = elem.split("<-mtf->", fields.length); HtmlElement h = new HtmlElement(this.getWebView()); int i = 0; for (String field : fields) { if (attrs.length > i) { h.putAttr(field, attrs[i++]); } } Log.log("tag: " + h.getTagName() + "monkeyId: " + h.getMonkeyId() + "component: " + h.getClassName()); list.add(h); } return list; } // public List<HtmlElement> decodeJsResult(String s, String[] fields) { // getWebView().loadUrl("MonkeyTalkGetAllElements();"); // // runJavaScript("MonkeyTalkGetAllElements();"); // List<HtmlElement> list = new ArrayList<HtmlElement>(); // if (s == null) { // return list; // } // // String[] elems = s.split("<-mte->"); // for (String elem : elems) { // String[] attrs = elem.split("<-mtf->", fields.length); // HtmlElement h = new HtmlElement(this.getWebView()); // int i = 0; // for (String field : fields) { // if (attrs.length > i) { // h.putAttr(field, attrs[i++]); // } // } // // Log.log("tag: " + h.getTagName() + "monkeyId: " + h.getMonkeyId() + "component: " // + h.getClassName()); // list.add(h); // } // return list; // } @Override public String play(String action, String... args) { if (action.equalsIgnoreCase("dump")) { return (runJavaScript("return document.body.innerHTML")); } if (action.equalsIgnoreCase("execjs")) { assertArgCount("execJS", args, 1); return runJavaScript(args[0]); } if (action.equalsIgnoreCase(AutomatorConstants.ACTION_SCROLL)) { if (args.length < 2) { throw new IllegalArgumentException(AutomatorConstants.ACTION_SCROLL + " requires an X and Y value"); } scroll(Integer.parseInt(args[0]), Integer.parseInt(args[1])); return null; } return super.play(action, args); } protected void enterText(String s, HtmlElement element) { super.enterText(s); int x = element.getX(); int y = element.getY(); this.runJavaScript("document.elementFromPoint(" + x + "," + y + ").value = '" + s + "';"); } @Override public boolean canAutomate(String componentType, String monkeyID) { return componentType.equals(getComponentType()) ? super .canAutomate(componentType, monkeyID) : false; } public String getComponentTreeJson() { assureMonkeyTalkInjected(); long oldTimeout = JS_TIMEOUT; JS_TIMEOUT = 30000; try { String json = this.runJavaScript("return MonkeyTalk.getComponentTreeJson();"); return json; } finally { JS_TIMEOUT = oldTimeout; } } }