From 7040eedb07f997c812a66b7bc590918265131c46 Mon Sep 17 00:00:00 2001 From: Joel Maher Date: Thu, 22 Mar 2012 11:19:57 -0400 Subject: [PATCH] Bug 712643 - land Marionette on m-c. r=mossop,robcee --- allmakefiles.sh | 8 + b2g/app/b2g.js | 4 + b2g/confvars.sh | 2 +- b2g/installer/package-manifest.in | 4 + config/autoconf.mk.in | 1 + configure.in | 9 + testing/marionette/Makefile.in | 19 + testing/marionette/components/Makefile.in | 17 + .../components/MarionetteComponents.manifest | 4 + .../components/marionettecomponent.js | 89 ++ testing/marionette/jar.mn | 7 + testing/marionette/marionette-actors.js | 1050 +++++++++++++++++ testing/marionette/marionette-elements.js | 403 +++++++ testing/marionette/marionette-listener.js | 534 +++++++++ testing/marionette/marionette-log-obj.js | 45 + testing/marionette/marionette-simpletest.js | 131 ++ testing/marionette/tests/Makefile.in | 17 + testing/marionette/tests/unit/head_mar.js | 11 + .../tests/unit/test_marionette_err.js | 53 + .../tests/unit/test_marionette_exec.js | 75 ++ .../tests/unit/test_marionette_execAsync.js | 193 +++ .../tests/unit/test_marionette_execjs.js | 365 ++++++ testing/marionette/tests/unit/xpcshell.ini | 8 + testing/xpcshell/xpcshell.ini | 1 + toolkit/toolkit-tiers.mk | 4 + 25 files changed, 3053 insertions(+), 1 deletion(-) create mode 100644 testing/marionette/Makefile.in create mode 100644 testing/marionette/components/Makefile.in create mode 100644 testing/marionette/components/MarionetteComponents.manifest create mode 100644 testing/marionette/components/marionettecomponent.js create mode 100644 testing/marionette/jar.mn create mode 100644 testing/marionette/marionette-actors.js create mode 100644 testing/marionette/marionette-elements.js create mode 100644 testing/marionette/marionette-listener.js create mode 100644 testing/marionette/marionette-log-obj.js create mode 100644 testing/marionette/marionette-simpletest.js create mode 100644 testing/marionette/tests/Makefile.in create mode 100644 testing/marionette/tests/unit/head_mar.js create mode 100644 testing/marionette/tests/unit/test_marionette_err.js create mode 100644 testing/marionette/tests/unit/test_marionette_exec.js create mode 100644 testing/marionette/tests/unit/test_marionette_execAsync.js create mode 100644 testing/marionette/tests/unit/test_marionette_execjs.js create mode 100644 testing/marionette/tests/unit/xpcshell.ini diff --git a/allmakefiles.sh b/allmakefiles.sh index d0325ff973d..7edae292440 100755 --- a/allmakefiles.sh +++ b/allmakefiles.sh @@ -122,6 +122,14 @@ if [ "$COMPILER_DEPEND" = "" -a "$MOZ_NATIVE_MAKEDEPEND" = "" ]; then " fi +if [ "$ENABLE_MARIONETTE" ]; then + add_makefiles " + testing/marionette/Makefile + testing/marionette/components/Makefile + testing/marionette/tests/Makefile + " +fi + if [ "$ENABLE_TESTS" ]; then add_makefiles " build/autoconf/test/Makefile diff --git a/b2g/app/b2g.js b/b2g/app/b2g.js index ec15b189121..06bcedfd141 100644 --- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -455,6 +455,10 @@ pref("ril.data.apn", ""); pref("ril.data.user", ""); pref("ril.data.passwd", ""); +//Enable/disable marionette server, set listening port +pref("marionette.defaultPrefs.enabled", true); +pref("marionette.defaultPrefs.port", 2828); + #ifdef MOZ_UPDATER pref("app.update.enabled", true); pref("app.update.auto", true); diff --git a/b2g/confvars.sh b/b2g/confvars.sh index 4aadc68767e..8ac2ae546a0 100644 --- a/b2g/confvars.sh +++ b/b2g/confvars.sh @@ -46,7 +46,7 @@ MOZ_OFFICIAL_BRANDING_DIRECTORY=b2g/branding/official # MOZ_APP_DISPLAYNAME is set by branding/configure.sh MOZ_SAFE_BROWSING= -MOZ_SERVICES_SYNC= +MOZ_SERVICES_SYNC=1 MOZ_WEBSMS_BACKEND=1 MOZ_DISABLE_DOMCRYPTO=1 diff --git a/b2g/installer/package-manifest.in b/b2g/installer/package-manifest.in index ae2569fa381..ee1a2ee0e45 100644 --- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -617,6 +617,10 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@ @BINPATH@/components/B2GComponents.manifest @BINPATH@/components/B2GComponents.xpt @BINPATH@/components/CameraContent.js +@BINPATH@/chrome/marionette@JAREXT@ +@BINPATH@/chrome/marionette.manifest +@BINPATH@/components/MarionetteComponents.manifest +@BINPATH@/components/marionettecomponent.js @BINPATH@/components/AlertsService.js @BINPATH@/components/ContentPermissionPrompt.js #ifdef MOZ_UPDATER diff --git a/config/autoconf.mk.in b/config/autoconf.mk.in index 4e87fbcaaff..1f48e4b6374 100644 --- a/config/autoconf.mk.in +++ b/config/autoconf.mk.in @@ -135,6 +135,7 @@ MOZ_LIBSTDCXX_HOST_VERSION=@MOZ_LIBSTDCXX_HOST_VERSION@ INCREMENTAL_LINKER = @INCREMENTAL_LINKER@ MACOSX_DEPLOYMENT_TARGET = @MACOSX_DEPLOYMENT_TARGET@ ENABLE_TESTS = @ENABLE_TESTS@ +ENABLE_MARIONETTE = @ENABLE_MARIONETTE@ IBMBIDI = @IBMBIDI@ MOZ_UNIVERSALCHARDET = @MOZ_UNIVERSALCHARDET@ ACCESSIBILITY = @ACCESSIBILITY@ diff --git a/configure.in b/configure.in index 9f67e760b50..0c2dfefb3f2 100644 --- a/configure.in +++ b/configure.in @@ -6437,6 +6437,14 @@ MOZ_ARG_DISABLE_BOOL(tests, ENABLE_TESTS=, ENABLE_TESTS=1 ) +dnl ======================================================== +dnl Marionette +dnl ======================================================== +MOZ_ARG_ENABLE_BOOL(marionette, +[ --enable-marionette Enable Marionette for remote testing and control], + ENABLE_MARIONETTE=1, + ENABLE_MARIONETTE) + dnl ======================================================== dnl parental controls (for Windows Vista) dnl ======================================================== @@ -8381,6 +8389,7 @@ AC_SUBST(JAR) AC_SUBST(MOZ_PROFILELOCKING) AC_SUBST(ENABLE_TESTS) +AC_SUBST(ENABLE_MARIONETTE) AC_SUBST(IBMBIDI) AC_SUBST(MOZ_UNIVERSALCHARDET) AC_SUBST(ACCESSIBILITY) diff --git a/testing/marionette/Makefile.in b/testing/marionette/Makefile.in new file mode 100644 index 00000000000..eb58bf38ca2 --- /dev/null +++ b/testing/marionette/Makefile.in @@ -0,0 +1,19 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +ifdef ENABLE_MARIONETTE + DIRS += components + ifdef ENABLE_TESTS + DIRS += tests + endif +endif + +include $(topsrcdir)/config/rules.mk diff --git a/testing/marionette/components/Makefile.in b/testing/marionette/components/Makefile.in new file mode 100644 index 00000000000..80beda579db --- /dev/null +++ b/testing/marionette/components/Makefile.in @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +EXTRA_PP_COMPONENTS = \ + MarionetteComponents.manifest \ + marionettecomponent.js \ + $(NULL) + +include $(topsrcdir)/config/rules.mk diff --git a/testing/marionette/components/MarionetteComponents.manifest b/testing/marionette/components/MarionetteComponents.manifest new file mode 100644 index 00000000000..3720f7f3362 --- /dev/null +++ b/testing/marionette/components/MarionetteComponents.manifest @@ -0,0 +1,4 @@ +# Marionette +component {786a1369-dca5-4adc-8486-33d23c88010a} marionettecomponent.js +contract @mozilla.org/marionette;1 {786a1369-dca5-4adc-8486-33d23c88010a} +category profile-after-change MarionetteComponent @mozilla.org/marionette;1 diff --git a/testing/marionette/components/marionettecomponent.js b/testing/marionette/components/marionettecomponent.js new file mode 100644 index 00000000000..3277391b447 --- /dev/null +++ b/testing/marionette/components/marionettecomponent.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; + +const MARIONETTE_CONTRACTID = "@mozilla.org/marionette;1"; +const MARIONETTE_CID = Components.ID("{786a1369-dca5-4adc-8486-33d23c88010a}"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +function MarionetteComponent() { + this._loaded = false; +} + +MarionetteComponent.prototype = { + classDescription: "Marionette component", + classID: MARIONETTE_CID, + contractID: MARIONETTE_CONTRACTID, + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), + _xpcom_categories: [{category: "profile-after-change", service: true}], + + observe: function mc_observe(aSubject, aTopic, aData) { + let observerService = Services.obs; + switch (aTopic) { + case "profile-after-change": + if (Services.prefs.getBoolPref('marionette.defaultPrefs.enabled')) { + // set up the logger + Cu.import("resource://gre/modules/FileUtils.jsm"); + Cu.import("resource://gre/modules/services-sync/log4moz.js"); + + let logger = Log4Moz.repository.getLogger("Marionette"); + logger.level = Log4Moz.Level["All"]; + let logf = FileUtils.getFile('ProfD', ['marionette.log']); + + let formatter = new Log4Moz.BasicFormatter(); + logger.addAppender(new Log4Moz.FileAppender(logf, formatter)); + logger.info("MarionetteComponent loaded"); + + //add observers + observerService.addObserver(this, "final-ui-startup", false); + observerService.addObserver(this, "xpcom-shutdown", false); + } + else { + logger.info("marionette not enabled"); + } + break; + case "final-ui-startup": + observerService.removeObserver(this, "final-ui-startup"); + this.init(); + break; + case "xpcom-shutdown": + observerService.removeObserver(this, "xpcom-shutdown"); + this.uninit(); + break; + } + }, + + init: function mc_init() { + if (!this._loaded) { + this._loaded = true; + let port; + try { + port = Services.prefs.getIntPref('marionette.defaultPrefs.port'); + } + catch(e) { + port = 2828; + } + try { + Cu.import('resource:///modules/devtools/dbg-server.jsm'); + DebuggerServer.addActors('chrome://marionette/content/marionette-actors.js'); + DebuggerServer.initTransport(); + DebuggerServer.openListener(port, true); + } + catch(e) { + logger.error('exception: ' + e.name + ', ' + e.message); + } + } + }, + + uninit: function mc_uninit() { + DebuggerServer.closeListener(); + this._loaded = false; + }, + +}; + +const NSGetFactory = XPCOMUtils.generateNSGetFactory([MarionetteComponent]); diff --git a/testing/marionette/jar.mn b/testing/marionette/jar.mn new file mode 100644 index 00000000000..2a3a0701c07 --- /dev/null +++ b/testing/marionette/jar.mn @@ -0,0 +1,7 @@ +marionette.jar: +% content marionette %content/ + content/marionette-actors.js (marionette-actors.js) + content/marionette-listener.js (marionette-listener.js) + content/marionette-elements.js (marionette-elements.js) + content/marionette-log-obj.js (marionette-log-obj.js) + content/marionette-simpletest.js (marionette-simpletest.js) diff --git a/testing/marionette/marionette-actors.js b/testing/marionette/marionette-actors.js new file mode 100644 index 00000000000..34316f74c4e --- /dev/null +++ b/testing/marionette/marionette-actors.js @@ -0,0 +1,1050 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; +/** + * Gecko-specific actors. + */ + +let Ci = Components.interfaces; +let Cc = Components.classes; +let Cu = Components.utils; + +let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader); +loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js"); +loader.loadSubScript("chrome://marionette/content/marionette-log-obj.js"); +Cu.import("chrome://marionette/content/marionette-elements.js"); + +let prefs = Cc["@mozilla.org/preferences-service;1"] + .getService(Ci.nsIPrefBranch); +prefs.setBoolPref("marionette.contentListener", false); + +let xulAppInfo = Cc["@mozilla.org/xre/app-info;1"] + .getService(Ci.nsIXULAppInfo); +let appName = xulAppInfo.name; + +// import logger +Cu.import("resource://gre/modules/services-sync/log4moz.js"); +let logger = Log4Moz.repository.getLogger("Marionette"); +logger.info('marionette-actors.js loaded'); + +/** + * Creates the root actor once a connection is established + */ + +function createRootActor(aConnection) +{ + return new MarionetteRootActor(aConnection); +} + +/** + * Root actor for Marionette. Add any future actors to its actor pool. + * Implements methods needed by resource:///modules/devtools/dbg-server.jsm + */ + +function MarionetteRootActor(aConnection) +{ + this.conn = aConnection; + this._marionetteActor = new MarionetteDriverActor(this.conn); + this._marionetteActorPool = null; //hold future actors + + this._marionetteActorPool = new ActorPool(this.conn); + this._marionetteActorPool.addActor(this._marionetteActor); + this.conn.addActorPool(this._marionetteActorPool); +} + +MarionetteRootActor.prototype = { + /** + * Called when a client first makes a connection + * + * @return object + * returns the name of the actor, the application type, and any traits + */ + sayHello: function MRA_sayHello() { + return { from: "root", + applicationType: "gecko", + traits: [] }; + }, + + /** + * Called when client disconnects. Cleans up marionette driver actions. + */ + disconnect: function MRA_disconnect() { + this._marionetteActor.deleteSession(); + }, + + /** + * Used to get the running marionette actor, so we can issue commands + * + * @return object + * Returns the ID the client can use to communicate with the + * MarionetteDriverActor + */ + getMarionetteID: function MRA_getMarionette() { + return { "from": "root", + "id": this._marionetteActor.actorID } ; + }, +} + +// register the calls +MarionetteRootActor.prototype.requestTypes = { + "getMarionetteID": MarionetteRootActor.prototype.getMarionetteID, + "sayHello": MarionetteRootActor.prototype.sayHello +}; + +/** + * This actor is responsible for all marionette API calls. It gets created + * for each connection and manages all chrome and browser based calls. It + * mediates content calls by issuing appropriate messages to the content process. + */ +function MarionetteDriverActor(aConnection) +{ + this.uuidGen = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + + this.conn = aConnection; + this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. + getService(Ci.nsIChromeFrameMessageManager); + this.windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].getService(Ci.nsIWindowMediator); + this.browsers = {}; //holds list of BrowserObjs + this.curBrowser = null; // points to current browser + this.context = "content"; + this.scriptTimeout = null; + this.elementManager = new ElementManager([SELECTOR, NAME, LINK_TEXT, PARTIAL_LINK_TEXT]); + this.timer = null; + this.marionetteLog = new MarionetteLogObj(); + this.command_id = null; + + //register all message listeners + this.messageManager.addMessageListener("Marionette:ok", this); + this.messageManager.addMessageListener("Marionette:done", this); + this.messageManager.addMessageListener("Marionette:error", this); + this.messageManager.addMessageListener("Marionette:log", this); + this.messageManager.addMessageListener("Marionette:testLog", this); + this.messageManager.addMessageListener("Marionette:register", this); + this.messageManager.addMessageListener("Marionette:goUrl", this); +} + +MarionetteDriverActor.prototype = { + + //name of the actor + actorPrefix: "marionette", + + /** + * Helper method to send async messages to the content listener + * + * @param string name + * Suffix of the targetted message listener (Marionette:) + * @param object values + * Object to send to the listener + */ + sendAsync: function MDA_sendAsync(name, values) { + this.messageManager.sendAsyncMessage("Marionette:" + name + this.browsers[this.curBrowser].curFrameId, values); + }, + + /** + * Helper methods: + */ + + /** + * Generic method to pass a response to the client + * + * @param object msg + * Response to send back to client + * @param string command_id + * Unique identifier assigned to the client's request. + * Used to distinguish the asynchronous responses. + */ + sendToClient: function MDA_sendToClient(msg, command_id) { + logger.info("sendToClient: " + JSON.stringify(msg) + ", " + command_id + ", " + this.command_id); + if (command_id == undefined || command_id == this.command_id) { + this.conn.send(msg); + this.command_id = null; + } + }, + + /** + * Send a value to client + * + * @param object value + * Value to send back to client + * @param string command_id + * Unique identifier assigned to the client's request. + * Used to distinguish the asynchronous responses. + */ + sendResponse: function MDA_sendResponse(value, command_id) { + if (typeof(value) == 'undefined') + value = null; + this.sendToClient({from:this.actorID, value: value}, command_id); + }, + + /** + * Send ack to client + * + * @param string command_id + * Unique identifier assigned to the client's request. + * Used to distinguish the asynchronous responses. + */ + sendOk: function MDA_sendOk(command_id) { + this.sendToClient({from:this.actorID, ok: true}, command_id); + }, + + /** + * Send error message to client + * + * @param string message + * Error message + * @param number status + * Status number + * @param string trace + * Stack trace + * @param string command_id + * Unique identifier assigned to the client's request. + * Used to distinguish the asynchronous responses. + */ + sendError: function MDA_sendError(message, status, trace, command_id) { + let error_msg = {message: message, status: status, stacktrace: trace}; + this.sendToClient({from:this.actorID, error: error_msg}, command_id); + }, + + /** + * Gets the current active window + * + * @return nsIDOMWindow + */ + getCurrentWindow: function MDA_getCurrentWindow() { + let type = null; + if (appName != "B2G") { + type = 'navigator:browser'; + } + return this.windowMediator.getMostRecentWindow(type); + }, + + /** + * Gets the the window enumerator + * + * @return nsISimpleEnumerator + */ + getWinEnumerator: function MDA_getWinEnumerator() { + let type = null; + if (appName != "B2G") { + type = 'navigator:browser'; + } + return this.windowMediator.getEnumerator(type); + }, + + /** + * Create a new BrowserObj for window and add to known browsers + * + * @param nsIDOMWindow win + * Window for which we will create a BrowserObj + * + * @return string + * Returns the unique server-assigned ID of the window + */ + addBrowser: function MDA_addBrowser(win) { + let browser = new BrowserObj(win); + let winId = win.QueryInterface(Ci.nsIInterfaceRequestor). + getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + winId = winId + ((appName == "B2G") ? '-b2g' : ''); + if (this.elementManager.seenItems[winId] == undefined) { + //add this to seenItems so we can guarantee the user will get winId as this window's id + this.elementManager.seenItems[winId] = win; + } + this.browsers[winId] = browser; + return winId; + }, + + /** + * Start a new session in a new browser. + * + * If newSession is true, we will switch focus to the start frame + * when it registers. Also, if it is in desktop, then a new tab + * with the start page uri (about:blank) will be opened. + * + * @param nsIDOMWindow win + * Window whose browser we need to access + * @param boolean newSession + * True if this is the first time we're talking to this browser + */ + startBrowser: function MDA_startBrowser(win, newSession) { + let winId = this.addBrowser(win); + this.curBrowser = winId; + this.browsers[this.curBrowser].newSession = newSession; + this.browsers[this.curBrowser].startSession(newSession); + this.browsers[this.curBrowser].loadFrameScript("chrome://marionette/content/marionette-listener.js", win); + }, + + /** + * Marionette API: + */ + + /** + * Create a new session. This creates a BrowserObj. + * + * In a desktop environment, this opens a new 'about:blank' tab for + * the client to test in. + * + */ + newSession: function MDA_newSession() { + if (!prefs.getBoolPref("marionette.contentListener")) { + this.startBrowser(this.getCurrentWindow(), true); + } + else if ((appName == "B2G")&& (this.curBrowser == null)) { + //if there is a content listener, then we just wake it up + let winId = this.addBrowser(this.getCurrentWindow()); + this.curBrowser = winId; + this.browsers[this.curBrowser].startSession(false); + this.messageManager.sendAsyncMessage("Marionette:restart", {}); + } + else { + this.sendError("Session already running", 500, null); + } + }, + + /** + * Log message. Accepts user defined log-level. + * + * @param object aRequest + * 'value' member holds log message + * 'level' member hold log level + */ + log: function MDA_log(aRequest) { + this.marionetteLog.log(aRequest.value, aRequest.level); + this.sendOk(); + }, + + /** + * Return all logged messages. + */ + getLogs: function MDA_getLogs() { + this.sendResponse(this.marionetteLog.getLogs()); + }, + + /** + * Sets the context of the subsequent commands to be either 'chrome' or 'content' + * + * @param object aRequest + * 'value' member holds the name of the context to be switched to + */ + setContext: function MDA_setContext(aRequest) { + let context = aRequest.value; + if (context != "content" && context != "chrome") { + this.sendError("invalid context", 500, null); + } + else { + this.context = context; + this.sendOk(); + } + }, + + /** + * Returns a chrome sandbox that can be used by the execute_foo functions. + * + * @param nsIDOMWindow aWindow + * Window in which we will execute code + * @param Marionette marionette + * Marionette test instance + * @param object args + * Client given args + * @return Sandbox + * Returns the sandbox + */ + createExecuteSandbox: function MDA_createExecuteSandbox(aWindow, marionette, args) { + try { + args = this.elementManager.convertWrappedArguments(args, aWindow); + } + catch(e) { + this.sendError(e.message, e.num, e.stack); + return; + } + + let _chromeSandbox = new Cu.Sandbox(aWindow, + { sandboxPrototype: aWindow, wantXrays: false, sandboxName: ''}); + _chromeSandbox.__namedArgs = this.elementManager.applyNamedArgs(args); + _chromeSandbox.__marionetteParams = args; + + marionette.exports.forEach(function(fn) { + _chromeSandbox[fn] = marionette[fn].bind(marionette); + }); + + return _chromeSandbox; + }, + + /** + * Executes a script in the given sandbox. + * + * @param Sandbox sandbox + * Sandbox in which the script will run + * @param string script + * The script to run + * @param boolean directInject + * If true, then the script will be run as is, + * and not as a function body (as you would + * do using the WebDriver spec) + * @param boolean async + * True if the script is asynchronous + */ + executeScriptInSandbox: function MDA_executeScriptInSandbox(sandbox, script, + directInject, async) { + try { + if (directInject && async && + (this.scriptTimeout == null || this.scriptTimeout == 0)) { + this.sendError("Please set a timeout", 21, null); + return; + } + + let res = Cu.evalInSandbox(script, sandbox, "1.8"); + + if (directInject && !async && + (res == undefined || res.passed == undefined)) { + this.sendError("finish() not called", 500, null); + return; + } + + if (!async) { + this.sendResponse(this.elementManager.wrapValue(res)); + } + } + catch (e) { + this.sendError(e.name + ': ' + e.message, 17, e.stack); + } + }, + + /** + * Execute the given script either as a function body (executeScript) + * or directly (for 'mochitest' like JS Marionette tests) + * + * @param object aRequest + * 'value' member is the script to run + * 'args' member holds the arguments to the script + * @param boolean directInject + * if true, it will be run directly and not as a + * function body + */ + execute: function MDA_execute(aRequest, directInject) { + if (this.context == "content") { + this.sendAsync("executeScript", {value: aRequest.value, args: aRequest.args}); + return; + } + + let curWindow = this.getCurrentWindow(); + let marionette = new Marionette(false, curWindow, "chrome", this.marionetteLog); + let _chromeSandbox = this.createExecuteSandbox(curWindow, marionette, aRequest.args); + if (!_chromeSandbox) + return; + + try { + _chromeSandbox.finish = function chromeSandbox_finish() { + return marionette.generate_results(); + }; + + let script; + if (directInject) { + script = aRequest.value; + } + else { + script = "let func = function() {" + + aRequest.value + + "};" + + "func.apply(null, __marionetteParams);"; + } + this.executeScriptInSandbox(_chromeSandbox, script, directInject, false); + } + catch (e) { + this.sendError(e.name + ': ' + e.message, 17, e.stack); + } + }, + + /** + * Set the timeout for asynchronous script execution + * + * @param object aRequest + * 'value' member is time in milliseconds to set timeout + */ + setScriptTimeout: function MDA_setScriptTimeout(aRequest) { + let timeout = parseInt(aRequest.value); + if(isNaN(timeout)){ + this.sendError("Not a Number", 500, null); + } + else { + this.scriptTimeout = timeout; + this.sendAsync("setScriptTimeout", {value: timeout}); + this.sendOk(); + } + }, + + /** + * execute pure JS script. Used to execute 'mochitest'-style Marionette tests. + * + * @param object aRequest + * 'value' member holds the script to execute + * 'args' member holds the arguments to the script + * 'timeout' member will be used as the script timeout if it is given + */ + executeJSScript: function MDA_executeJSScript(aRequest) { + //all pure JS scripts will need to call Marionette.finish() to complete the test. + if (this.context == "chrome") { + if (aRequest.timeout) { + this.executeWithCallback(aRequest, aRequest.timeout); + } + else { + this.execute(aRequest, true); + } + } + else { + this.sendAsync("executeJSScript", {value:aRequest.value, args:aRequest.args, timeout:aRequest.timeout}); + } + }, + + /** + * This function is used by executeAsync and executeJSScript to execute a script + * in a sandbox. + * + * For executeJSScript, it will return a message only when the finish() method is called. + * For executeAsync, it will return a response when marionetteScriptFinished/arguments[arguments.length-1] + * method is called, or if it times out. + * + * @param object aRequest + * 'value' member holds the script to execute + * 'args' member holds the arguments for the script + * @param boolean directInject + * if true, it will be run directly and not as a + * function body + */ + executeWithCallback: function MDA_executeWithCallback(aRequest, directInject) { + this.command_id = this.uuidGen.generateUUID().toString(); + + if (this.context == "content") { + this.sendAsync("executeAsyncScript", {value: aRequest.value, + args: aRequest.args, + id: this.command_id}); + return; + } + + let curWindow = this.getCurrentWindow(); + let original_onerror = curWindow.onerror; + let that = this; + let marionette = new Marionette(true, curWindow, "chrome", this.marionetteLog); + marionette.command_id = this.command_id; + + function chromeAsyncReturnFunc(value, status) { + if (value == undefined) + value = null; + if (that.command_id == marionette.command_id) { + if (that.timer != null) { + that.timer.cancel(); + that.timer = null; + } + + curWindow.onerror = original_onerror; + + if (status == 0 || status == undefined) { + that.sendToClient({from: that.actorID, value: that.elementManager.wrapValue(value), status: status}, + marionette.command_id); + } + else { + let error_msg = {message: value, status: status, stacktrace: null}; + that.sendToClient({from: that.actorID, error: error_msg}, + marionette.command_id); + } + } + } + + curWindow.onerror = function (errorMsg, url, lineNumber) { + chromeAsyncReturnFunc(errorMsg + " at: " + url + " line: " + lineNumber, 17); + return true; + }; + + function chromeAsyncFinish() { + chromeAsyncReturnFunc(marionette.generate_results(), 0); + } + + let _chromeSandbox = this.createExecuteSandbox(curWindow, marionette, aRequest.args); + if (!_chromeSandbox) + return; + + try { + + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + if (this.timer != null) { + this.timer.initWithCallback(function() { + chromeAsyncReturnFunc("timed out", 28); + }, that.scriptTimeout, Ci.nsITimer.TYPE_ONESHOT); + } + + _chromeSandbox.returnFunc = chromeAsyncReturnFunc; + _chromeSandbox.finish = chromeAsyncFinish; + + let script; + if (directInject) { + script = aRequest.value; + } + else { + script = '__marionetteParams.push(returnFunc);' + + 'let marionetteScriptFinished = returnFunc;' + + 'let __marionetteFunc = function() {' + aRequest.value + '};' + + '__marionetteFunc.apply(null, __marionetteParams);'; + } + + this.executeScriptInSandbox(_chromeSandbox, script, directInject, true); + } catch (e) { + this.sendError(e.name + ": " + e.message, 17, e.stack, marionette.command_id); + } + }, + + /** + * Navigates to given url + * + * @param object aRequest + * 'value' member holds the url to navigate to + */ + goUrl: function MDA_goUrl(aRequest) { + this.sendAsync("goUrl", aRequest); + }, + + /** + * Gets current url + */ + getUrl: function MDA_getUrl() { + if (this.context == "chrome") { + this.sendResponse(this.getCurrentWindow().location.href); + } + else { + this.sendAsync("getUrl", {}); + } + }, + + /** + * Go back in history + */ + goBack: function MDA_goBack() { + this.sendAsync("goBack", {}); + }, + + /** + * Go forward in history + */ + goForward: function MDA_goForward() { + this.sendAsync("goForward", {}); + }, + + /** + * Refresh the page + */ + refresh: function MDA_refresh() { + this.sendAsync("refresh", {}); + }, + + /** + * Get the current window's server-assigned ID + */ + getWindow: function MDA_getWindow() { + this.sendResponse(this.curBrowser); + }, + + /** + * Get the server-assigned IDs of all available windows + */ + getWindows: function MDA_getWindows() { + let res = []; + let winEn = this.getWinEnumerator(); + while(winEn.hasMoreElements()) { + let foundWin = winEn.getNext(); + let winId = foundWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils).outerWindowID; + winId = winId + ((appName == "B2G") ? '-b2g' : ''); + res.push(winId) + } + this.sendResponse(res); + }, + + /** + * Switch to a window based on name or server-assigned id. + * Searches based on name, then id. + * + * @param object aRequest + * 'value' member holds the id of the window to switch to + */ + switchToWindow: function MDA_switchToWindow(aRequest) { + let winEn = this.getWinEnumerator(); + while(winEn.hasMoreElements()) { + let foundWin = winEn.getNext(); + let winId = foundWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils).outerWindowID; + winId = winId + ((appName == "B2G") ? '-b2g' : ''); + if (aRequest.value == foundWin.name || aRequest.value == winId) { + if (this.browsers[winId] == undefined) { + //enable Marionette in that browser window + this.startBrowser(foundWin, false); + } + foundWin.focus(); + this.curBrowser = winId; + this.sendOk(); + return; + } + } + this.sendError("Unable to locate window " + aRequest.value, 23, null); + }, + + /** + * Switch to a given frame within the current window + * + * @param object aRequest + * 'value' holds the id of the frame to switch to + */ + switchToFrame: function MDA_switchToFrame(aRequest) { + this.sendAsync("switchToFrame", aRequest); + }, + + /** + * Set timeout for searching for elements + * + * @param object aRequest + * 'value' holds the search timeout in milliseconds + */ + setSearchTimeout: function MDA_setSearchTimeout(aRequest) { + if (this.context == "chrome") { + try { + this.elementManager.setSearchTimeout(aRequest.value); + this.sendOk(); + } + catch (e) { + this.sendError(e.message, e.num, e.stack); + } + } + else { + this.sendAsync("setSearchTimeout", {value: aRequest.value}); + } + }, + + /** + * Find an element using the indicated search strategy. + * + * @param object aRequest + * 'using' member indicates which search method to use + * 'value' member is the value the client is looking for + */ + findElement: function MDA_findElement(aRequest) { + if (this.context == "chrome") { + let id; + try { + let notify = this.sendResponse.bind(this); + id = this.elementManager.find(aRequest, this.getCurrentWindow().document, notify, false); + } + catch (e) { + this.sendError(e.message, e.num, e.stack); + return; + } + } + else { + this.sendAsync("findElementContent", {value: aRequest.value, using: aRequest.using, element: aRequest.element}); + } + }, + + /** + * Find elements using the indicated search strategy. + * + * @param object aRequest + * 'using' member indicates which search method to use + * 'value' member is the value the client is looking for + */ + findElements: function MDA_findElements(aRequest) { + if (this.context == "chrome") { + let id; + try { + let notify = this.sendResponse.bind(this); + id = this.elementManager.find(aRequest, this.getCurrentWindow().document, notify, true); + } + catch (e) { + this.sendError(e.message, e.num, e.stack); + return; + } + } + else { + this.sendAsync("findElementsContent", {value: aRequest.value, using: aRequest.using, element: aRequest.element}); + } + }, + + /** + * Send click event to element + * + * @param object aRequest + * 'element' member holds the reference id to + * the element that will be clicked + */ + clickElement: function MDA_clickElement(aRequest) { + this.sendAsync("clickElement", {element: aRequest.element}); + }, + + /** + * Deletes the session. + * + * If it is a desktop environment, it will close the session's tab and close all listeners + * + * If it is a B2G environment, it will make the main content listener sleep, and close + * all other listeners. The main content listener persists after disconnect (it's the homescreen), + * and can safely be reused. + */ + deleteSession: function MDA_deleteSession() { + if (this.browsers[this.curBrowser] != null) { + if (appName == "B2G") { + this.messageManager.sendAsyncMessage("Marionette:sleepSession" + this.browsers[this.curBrowser].mainContentId, {}); + this.browsers[this.curBrowser].knownFrames.splice(this.browsers[this.curBrowser].knownFrames.indexOf(this.browsers[this.curBrowser].mainContentId), 1); + } + else { + //don't set this pref for B2G since the framescript can be safely reused + prefs.setBoolPref("marionette.contentListener", false); + } + this.browsers[this.curBrowser].closeTab(); + //delete session in each frame in each browser + for (let win in this.browsers) { + for (let i in this.browsers[win].knownFrames) { + this.messageManager.sendAsyncMessage("Marionette:deleteSession" + this.browsers[win].knownFrames[i], {}); + } + } + let winEnum = this.getWinEnumerator(); + while (winEnum.hasMoreElements()) { + winEnum.getNext().messageManager.removeDelayedFrameScript("chrome://marionette/content/marionette-listener.js"); + } + } + this.sendOk(); + this.messageManager.removeMessageListener("Marionette:ok", this); + this.messageManager.removeMessageListener("Marionette:done", this); + this.messageManager.removeMessageListener("Marionette:error", this); + this.messageManager.removeMessageListener("Marionette:log", this); + this.messageManager.removeMessageListener("Marionette:testLog", this); + this.messageManager.removeMessageListener("Marionette:register", this); + this.messageManager.removeMessageListener("Marionette:goUrl", this); + this.curBrowser = null; + this.elementManager.reset(); + }, + + /** + * Receives all messages from content messageManager + */ + receiveMessage: function MDA_receiveMessage(message) { + switch (message.name) { + case "DOMContentLoaded": + this.sendOk(); + this.messageManager.removeMessageListener("DOMContentLoaded", this, true); + break; + case "Marionette:done": + this.sendResponse(message.json.value, message.json.command_id); + break; + case "Marionette:ok": + this.sendOk(message.json.command_id); + break; + case "Marionette:error": + this.sendError(message.json.message, message.json.status, message.json.stacktrace, message.json.command_id); + break; + case "Marionette:log": + //log server-side messages + logger.info(message.json.message); + break; + case "Marionette:testLog": + //log messages from tests + this.marionetteLog.addLogs(message.json.value); + break; + case "Marionette:register": + // This code processes the content listener's registration information + // and either accepts the listener, or ignores it + let nullPrevious= (this.browsers[this.curBrowser].curFrameId == null); + let curWin = this.getCurrentWindow(); + let frameObject = curWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils).getOuterWindowWithId(message.json.value); + let reg = this.browsers[this.curBrowser].register(message.json.value, message.json.href); + if (reg) { + this.elementManager.seenItems[reg] = frameObject; //add to seenItems + if (nullPrevious && (this.browsers[this.curBrowser].curFrameId != null)) { + this.sendAsync("newSession", {B2G: (appName == "B2G")}); + if (this.browsers[this.curBrowser].newSession) { + this.sendResponse(reg); + } + } + } + return reg; + case "Marionette:goUrl": + // if content determines that the goUrl call is directed at a top level window (not an iframe) + // it calls back into chrome to load the uri. + this.browsers[this.curBrowser].loadURI(message.json.value, this); + break; + } + }, + /** + * for non-e10s eventListening + */ + handleEvent: function MDA_handleEvent(evt) { + if (evt.type == "DOMContentLoaded") { + this.sendOk(); + this.browsers[this.curBrowser].browser.removeEventListener("DOMContentLoaded", this, false); + } + }, +}; + +MarionetteDriverActor.prototype.requestTypes = { + "newSession": MarionetteDriverActor.prototype.newSession, + "log": MarionetteDriverActor.prototype.log, + "getLogs": MarionetteDriverActor.prototype.getLogs, + "setContext": MarionetteDriverActor.prototype.setContext, + "executeScript": MarionetteDriverActor.prototype.execute, + "setScriptTimeout": MarionetteDriverActor.prototype.setScriptTimeout, + "executeAsyncScript": MarionetteDriverActor.prototype.executeWithCallback, + "executeJSScript": MarionetteDriverActor.prototype.executeJSScript, + "setSearchTimeout": MarionetteDriverActor.prototype.setSearchTimeout, + "findElement": MarionetteDriverActor.prototype.findElement, + "findElements": MarionetteDriverActor.prototype.findElements, + "clickElement": MarionetteDriverActor.prototype.clickElement, + "goUrl": MarionetteDriverActor.prototype.goUrl, + "getUrl": MarionetteDriverActor.prototype.getUrl, + "goBack": MarionetteDriverActor.prototype.goBack, + "goForward": MarionetteDriverActor.prototype.goForward, + "refresh": MarionetteDriverActor.prototype.refresh, + "getWindow": MarionetteDriverActor.prototype.getWindow, + "getWindows": MarionetteDriverActor.prototype.getWindows, + "switchToFrame": MarionetteDriverActor.prototype.switchToFrame, + "switchToWindow": MarionetteDriverActor.prototype.switchToWindow, + "deleteSession": MarionetteDriverActor.prototype.deleteSession +}; + +/** + * Creates a BrowserObj. BrowserObjs handle interactions with the + * browser, according to the current environment (desktop, b2g, etc.) + * + * @param nsIDOMWindow win + * The window whose browser needs to be accessed + */ + +function BrowserObj(win) { + this.DESKTOP = "desktop"; + this.B2G = "B2G"; + this.browser; + this.browser_mm; + this.tab = null; + this.knownFrames = []; + this.curFrameId = null; + this.startPage = "about:blank"; + this.mainContentId = null; // used in B2G to identify the homescreen content page + this.messageManager = Cc["@mozilla.org/globalmessagemanager;1"]. + getService(Ci.nsIChromeFrameMessageManager); + this.newSession = true; //used to set curFrameId upon new session + this.setBrowser(win); +} + +BrowserObj.prototype = { + /** + * Set the browser if the application is not B2G + * + * @param nsIDOMWindow win + * current window reference + */ + setBrowser: function BO_setBrowser(win) { + if (appName != "B2G") { + this.browser = win.gBrowser; + } + }, + /** + * Called when we start a session with this browser. + * + * In a desktop environment, if newTab is true, it will start + * a new 'about:blank' tab and change focus to this tab. + * + * This will also set the active messagemanager for this object + * + * @param boolean newTab + * If true, create new tab + */ + startSession: function BO_startSession(newTab) { + if (appName == "B2G") { + return; + } + if (newTab) { + this.addTab(this.startPage); + //if we have a new tab, make it the selected tab and give it focus + this.browser.selectedTab = this.tab; + let newTabBrowser = this.browser.getBrowserForTab(this.tab); + //focus the tab + newTabBrowser.ownerDocument.defaultView.focus(); + } + else { + //set this.tab to the currently focused tab + this.tab = this.browser.selectedTab; + this.browser_mm = this.browser.getBrowserForTab(this.tab).messageManager; + } + }, + + /** + * Closes current tab + */ + closeTab: function BO_closeTab() { + if (this.tab != null && (appName != "B2G")) { + this.browser.removeTab(this.tab); + this.tab = null; + } + }, + + /** + * Opens a tab with given uri + * + * @param string uri + * URI to open + */ + addTab: function BO_addTab(uri) { + this.tab = this.browser.addTab(uri, true); + }, + + /** + * Load a uri in the current tab + * + * @param string uri + * URI to load + * @param EventListener listener + * event listener fired on load + */ + loadURI: function BO_openURI(uri, listener) { + if (appName != "B2G") { + this.browser.addEventListener("DOMContentLoaded", listener, false); + this.browser.loadURI(uri); + } + else { + this.messageManager.addMessageListener("DOMContentLoaded", listener, true); + this.browser.selectedBrowser.loadURI(uri); + } + }, + + /** + * Loads content listeners if we don't already have them + * + * @param string script + * path of script to load + * @param nsIDOMWindow frame + * frame to load the script in + */ + loadFrameScript: function BO_loadFrameScript(script, frame) { + if (!prefs.getBoolPref("marionette.contentListener")) { + frame.window.messageManager.loadFrameScript(script, true); + prefs.setBoolPref("marionette.contentListener", true); + } + }, + + /** + * Registers a new frame, and sets its current frame id to this frame + * if it is not already assigned, and if a) we already have a session + * or b) we're starting a new session and it is the right start frame. + * + * @param string id + * frame id + * @param string href + * frame's href + */ + register: function BO_register(id, href) { + let uid = id + ((appName == "B2G") ? '-b2g' : ''); + if (this.curFrameId == null) { + if ((!this.newSession) || (this.newSession && ((appName == "B2G") || href.indexOf(this.startPage) > -1))) { + this.curFrameId = uid; + this.mainContentId = uid; + } + } + this.knownFrames.push(uid); //used to deletesessions + return uid; + }, +} diff --git a/testing/marionette/marionette-elements.js b/testing/marionette/marionette-elements.js new file mode 100644 index 00000000000..49447fb0e7c --- /dev/null +++ b/testing/marionette/marionette-elements.js @@ -0,0 +1,403 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * The ElementManager manages DOM references and interactions with elements. + * According to the WebDriver spec (http://code.google.com/p/selenium/wiki/JsonWireProtocol), the + * server sends the client an element reference, and maintains the map of reference to element. + * The client uses this reference when querying/interacting with the element, and the + * server uses maps this reference to the actual element when it executes the command. + */ + +let EXPORTED_SYMBOLS = ["ElementManager", "CLASS_NAME", "SELECTOR", "ID", "NAME", "LINK_TEXT", "PARTIAL_LINK_TEXT", "TAG", "XPATH"]; + +let uuidGen = Components.classes["@mozilla.org/uuid-generator;1"] + .getService(Components.interfaces.nsIUUIDGenerator); + +let CLASS_NAME = "class name"; +let SELECTOR = "css selector"; +let ID = "id"; +let NAME = "name"; +let LINK_TEXT = "link text"; +let PARTIAL_LINK_TEXT = "partial link text"; +let TAG = "tag name"; +let XPATH = "xpath"; + +function ElementException(msg, num, stack) { + this.message = msg; + this.num = num; + this.stack = stack; +} + +/* NOTE: Bug 736592 has been created to replace seenItems with a weakRef map */ +function ElementManager(notSupported) { + this.searchTimeout = 0; + this.seenItems = {}; + this.timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + this.elementStrategies = [CLASS_NAME, SELECTOR, ID, NAME, LINK_TEXT, PARTIAL_LINK_TEXT, TAG, XPATH]; + for (let i = 0; i < notSupported.length; i++) { + this.elementStrategies.splice(this.elementStrategies.indexOf(notSupported[i]), 1); + } +} + +ElementManager.prototype = { + /** + * Reset values + */ + reset: function EM_clear() { + this.searchTimeout = 0; + this.seenItems = {}; + }, + + /** + * Add element to list of seen elements + * + * @param nsIDOMElement element + * The element to add + * + * @return string + * Returns the server-assigned reference ID + */ + addToKnownElements: function EM_addToKnownElements(element) { + for (let i in this.seenItems) { + if (this.seenItems[i] == element) { + return i; + } + } + var id = uuidGen.generateUUID().toString(); + this.seenItems[id] = element; + return id; + }, + + /** + * Retrieve element from its unique ID + * + * @param String id + * The DOM reference ID + * @param nsIDOMWindow win + * The window that contains the element + * + * @returns nsIDOMElement + * Returns the element or throws Exception if not found + */ + getKnownElement: function EM_getKnownElement(id, win) { + let el = this.seenItems[id]; + if (!el) { + throw new ElementException("Element has not been seen before", 17, null); + } + el = el; + if (!(el.ownerDocument == win.document)) { + throw new ElementException("Stale element reference", 10, null); + } + return el; + }, + + /** + * Convert values to primitives that can be transported over the Marionette + * JSON protocol. + * + * @param object val + * object to be wrapped + * + * @return object + * Returns a JSON primitive or Object + */ + wrapValue: function EM_wrapValue(val) { + let result; + switch(typeof(val)) { + case "undefined": + result = null; + break; + case "string": + case "number": + case "boolean": + result = val; + break; + case "object": + if (Object.prototype.toString.call(val) == '[object Array]') { + result = []; + for (let i in val) { + result.push(this.wrapValue(val[i])); + } + } + else if (val == null) { + result = null; + } + // nodeType 1 == 'element' + else if (val.nodeType == 1) { + for(let i in this.seenItems) { + if (this.seenItems[i] == val) { + result = {'ELEMENT': i}; + } + } + result = {'ELEMENT': this.addToKnownElements(val)}; + } + else { + result = {}; + for (let prop in val) { + result[prop] = this.wrapValue(val[prop]); + } + } + break; + } + return result; + }, + + /** + * Convert any ELEMENT references in 'args' to the actual elements + * + * @param object args + * Arguments passed in by client + * @param nsIDOMWindow win + * The window that contains the elements + * + * @returns object + * Returns the objects passed in by the client, with the + * reference IDs replaced by the actual elements. + */ + convertWrappedArguments: function EM_convertWrappedArguments(args, win) { + let converted; + switch (typeof(args)) { + case 'number': + case 'string': + case 'boolean': + converted = args; + break; + case 'object': + if (args == null) { + converted = null; + } + else if (Object.prototype.toString.call(args) == '[object Array]') { + converted = []; + for (let i in args) { + converted.push(this.convertWrappedArguments(args[i], win)); + } + } + else if (typeof(args['ELEMENT'] === 'string') && + args.hasOwnProperty('ELEMENT')) { + converted = this.getKnownElement(args['ELEMENT'], win); + if (converted == null) + throw new ElementException("Unknown element: " + args['ELEMENT'], 500, null); + } + else { + converted = {}; + for (let prop in args) { + converted[prop] = this.convertWrappedArguments(args[prop], win); + } + } + break; + } + return converted; + }, + + /* + * Execute* helpers + */ + + /** + * Return an object with any namedArgs applied to it. Used + * to let clients use given names when refering to arguments + * in execute calls, instead of using the arguments list. + * + * @param object args + * list of arguments being passed in + * + * @return object + * If '__marionetteArgs' is in args, then + * it will return an object with these arguments + * as its members. + */ + applyNamedArgs: function EM_applyNamedArgs(args) { + namedArgs = {}; + args.forEach(function(arg) { + if (typeof(arg['__marionetteArgs']) === 'object') { + for (let prop in arg['__marionetteArgs']) { + namedArgs[prop] = arg['__marionetteArgs'][prop]; + } + } + }); + return namedArgs; + }, + + /** + * Find an element or elements starting at the document root + * using the given search strategy. Search + * will continue until the search timelimit has been reached. + * + * @param object values + * The 'using' member of values will tell us which search + * method to use. The 'value' member tells us the value we + * are looking for. + * If this object has a 'time' member, this number will be + * used to see if we have hit the search timelimit. + * @param nsIDOMElement rootNode + * The document root + * @param function notify + * The notification callback used when we are returning + * @param boolean all + * If true, all found elements will be returned. + * If false, only the first element will be returned. + * + * @return nsIDOMElement or list of nsIDOMElements + * Returns the element(s) by calling the notify function. + */ + find: function EM_find(values, rootNode, notify, all) { + let startTime = values.time ? values.time : new Date().getTime(); + if (this.elementStrategies.indexOf(values.using) < 0) { + throw new ElementException("No such strategy.", 17, null); + } + let found = all ? this.findElements(values.using, values.value, rootNode) : this.findElement(values.using, values.value, rootNode); + if (found) { + let type = Object.prototype.toString.call(found); + if ((type == '[object Array]') || (type == '[object HTMLCollection]')) { + let ids = [] + for (let i = 0 ; i < found.length ; i++) { + ids.push(this.addToKnownElements(found[i])); + } + notify(ids); + } + else { + let id = this.addToKnownElements(found); + notify(id); + } + return; + } else { + if (this.searchTimeout == 0 || new Date().getTime() - startTime > this.searchTimeout) { + throw new ElementException("Unable to locate element: " + values.value, 7, null); + } else { + values.time = startTime; + this.timer.initWithCallback(this.find.bind(this, values, rootNode, notify, all), 100, Components.interfaces.nsITimer.TYPE_ONE_SHOT); + } + } + }, + + /** + * Helper method to find. Finds one element using find's criteria + * + * @param string using + * String identifying which search method to use + * @param string value + * Value to look for + * @param nsIDOMElement rootNode + * Document root + * + * @return nsIDOMElement + * Returns found element or throws Exception if not found + */ + findElement: function EM_findElement(using, value, rootNode) { + let element; + switch (using) { + case ID: + element = rootNode.getElementById(value); + break; + case NAME: + element = rootNode.getElementsByName(value)[0]; + break; + case CLASS_NAME: + element = rootNode.getElementsByClassName(value)[0]; + break; + case TAG: + element = rootNode.getElementsByTagName(value)[0]; + break; + case XPATH: + element = rootNode.evaluate(value, rootNode, null, + Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null). + singleNodeValue; + break; + case LINK_TEXT: + case PARTIAL_LINK_TEXT: + let allLinks = rootNode.getElementsByTagName('A'); + for (let i = 0; i < allLinks.length && !element; i++) { + let text = allLinks[i].text; + if (PARTIAL_LINK_TEXT == using) { + if (text.indexOf(value) != -1) { + element = allLinks[i]; + } + } else if (text == value) { + element = allLinks[i]; + } + } + break; + case SELECTOR: + element = rootNode.querySelector(value); + break; + default: + throw new ElementException("No such strategy", 500, null); + } + return element; + }, + + /** + * Helper method to find. Finds all element using find's criteria + * + * @param string using + * String identifying which search method to use + * @param string value + * Value to look for + * @param nsIDOMElement rootNode + * Document root + * + * @return nsIDOMElement + * Returns found elements or throws Exception if not found + */ + findElements: function EM_findElements(using, value, rootNode) { + let elements = []; + switch (using) { + case ID: + value = './/*[@id="' + value + '"]'; + case XPATH: + values = rootNode.evaluate(value, rootNode, null, + Components.interfaces.nsIDOMXPathResult.ORDERED_NODE_ITERATOR_TYPE, null) + let element = values.iterateNext(); + while (element) { + elements.push(element); + element = values.iterateNext(); + } + break; + case NAME: + elements = rootNode.getElementsByName(value); + break; + case CLASS_NAME: + elements = rootNode.getElementsByClassName(value); + break; + case TAG: + elements = rootNode.getElementsByTagName(value); + break; + case LINK_TEXT: + case PARTIAL_LINK_TEXT: + let allLinks = rootNode.getElementsByTagName('A'); + for (let i = 0; i < allLinks.length; i++) { + let text = allLinks[i].text; + if (PARTIAL_LINK_TEXT == using) { + if (text.indexOf(value) != -1) { + elements.push(allLinks[i]); + } + } else if (text == value) { + elements.push(allLinks[i]); + } + } + break; + case SELECTOR: + elements = rootNode.querySelectorAll(value); + break; + default: + throw new ElementException("No such strategy", 500, null); + } + return elements; + }, + + /** + * Sets the timeout for searching for elements with find element + * + * @param number value + * Timeout value in milliseconds + */ + setSearchTimeout: function EM_setSearchTimeout(value) { + this.searchTimeout = parseInt(value); + if(isNaN(this.searchTimeout)){ + throw new ElementException("Not a Number", 500, null); + } + }, +} diff --git a/testing/marionette/marionette-listener.js b/testing/marionette/marionette-listener.js new file mode 100644 index 00000000000..5292e5101a0 --- /dev/null +++ b/testing/marionette/marionette-listener.js @@ -0,0 +1,534 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +let Cu = Components.utils; +let uuidGen = Components.classes["@mozilla.org/uuid-generator;1"] + .getService(Components.interfaces.nsIUUIDGenerator); + +let loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Components.interfaces.mozIJSSubScriptLoader); +loader.loadSubScript("chrome://marionette/content/marionette-simpletest.js"); +loader.loadSubScript("chrome://marionette/content/marionette-log-obj.js"); +Components.utils.import("chrome://marionette/content/marionette-elements.js"); +let marionetteLogObj = new MarionetteLogObj(); + +let isB2G = false; + +let marionetteTimeout = null; +let winUtil = content.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils); +let listenerId = null; //unique ID of this listener +let activeFrame = null; +let win = content; +let elementManager = new ElementManager([]); + +/** + * Called when listener is first started up. + * The listener sends its unique window ID and its current URI to the actor. + * If the actor returns an ID, we start the listeners. Otherwise, nothing happens. + */ +function registerSelf() { + let register = sendSyncMessage("Marionette:register", {value: winUtil.outerWindowID, href: content.location.href}); + + if (register[0]) { + listenerId = register[0]; + startListeners(); + } +} + +/** + * Start all message listeners + */ +function startListeners() { + addMessageListener("Marionette:newSession" + listenerId, newSession); + addMessageListener("Marionette:executeScript" + listenerId, executeScript); + addMessageListener("Marionette:setScriptTimeout" + listenerId, setScriptTimeout); + addMessageListener("Marionette:executeAsyncScript" + listenerId, executeAsyncScript); + addMessageListener("Marionette:executeJSScript" + listenerId, executeJSScript); + addMessageListener("Marionette:setSearchTimeout" + listenerId, setSearchTimeout); + addMessageListener("Marionette:goUrl" + listenerId, goUrl); + addMessageListener("Marionette:getUrl" + listenerId, getUrl); + addMessageListener("Marionette:goBack" + listenerId, goBack); + addMessageListener("Marionette:goForward" + listenerId, goForward); + addMessageListener("Marionette:refresh" + listenerId, refresh); + addMessageListener("Marionette:findElementContent" + listenerId, findElementContent); + addMessageListener("Marionette:findElementsContent" + listenerId, findElementsContent); + addMessageListener("Marionette:clickElement" + listenerId, clickElement); + addMessageListener("Marionette:switchToFrame" + listenerId, switchToFrame); + addMessageListener("Marionette:deleteSession" + listenerId, deleteSession); + addMessageListener("Marionette:sleepSession" + listenerId, sleepSession); +} + +/** + * Called when we start a new session. It registers the + * current environment, and resets all values + */ +function newSession(msg) { + isB2G = msg.json.B2G; + resetValues(); +} + +/** + * Puts the current session to sleep, so all listeners are removed except + * for the 'restart' listener. This is used to keep the content listener + * alive for reuse in B2G instead of reloading it each time. + */ +function sleepSession(msg) { + deleteSession(); + addMessageListener("Marionette:restart", restart); +} + +/** + * Restarts all our listeners after this listener was put to sleep + */ +function restart() { + removeMessageListener("Marionette:restart", restart); + registerSelf(); +} + +/** + * Removes all listeners + */ +function deleteSession(msg) { + removeMessageListener("Marionette:newSession" + listenerId, newSession); + removeMessageListener("Marionette:executeScript" + listenerId, executeScript); + removeMessageListener("Marionette:setScriptTimeout" + listenerId, setScriptTimeout); + removeMessageListener("Marionette:executeAsyncScript" + listenerId, executeAsyncScript); + removeMessageListener("Marionette:executeJSScript" + listenerId, executeJSScript); + removeMessageListener("Marionette:setSearchTimeout" + listenerId, setSearchTimeout); + removeMessageListener("Marionette:goUrl" + listenerId, goUrl); + removeMessageListener("Marionette:getUrl" + listenerId, getUrl); + removeMessageListener("Marionette:goBack" + listenerId, goBack); + removeMessageListener("Marionette:goForward" + listenerId, goForward); + removeMessageListener("Marionette:refresh" + listenerId, refresh); + removeMessageListener("Marionette:findElementContent" + listenerId, findElementContent); + removeMessageListener("Marionette:findElementsContent" + listenerId, findElementsContent); + removeMessageListener("Marionette:clickElement" + listenerId, clickElement); + removeMessageListener("Marionette:switchToFrame" + listenerId, switchToFrame); + removeMessageListener("Marionette:deleteSession" + listenerId, deleteSession); + removeMessageListener("Marionette:sleepSession" + listenerId, sleepSession); + this.elementManager.reset(); +} + +/* + * Helper methods + */ + +/** + * Generic method to send a message to the server + */ +function sendToServer(msg, value, command_id) { + if (command_id) { + value.command_id = command_id; + } + sendAsyncMessage(msg, value); +} + +/** + * Send response back to server + */ +function sendResponse(value, command_id) { + sendToServer("Marionette:done", value, command_id); +} + +/** + * Send ack back to server + */ +function sendOk(command_id) { + sendToServer("Marionette:ok", {}, command_id); +} + +/** + * Send log message to server + */ +function sendLog(msg) { + sendToServer("Marionette:log", { message: msg }); +} + +/** + * Send error message to server + */ +function sendError(message, status, trace, command_id) { + let error_msg = { message: message, status: status, stacktrace: trace }; + sendToServer("Marionette:error", error_msg, command_id); +} + +/** + * Clear test values after completion of test + */ +function resetValues() { + marionetteTimeout = null; +} + +/** + * send error when we detect an unload event during async scripts + */ +function errUnload() { + sendError("unload was called", 17, null); +} + + +/* + * Marionette Methods + */ + +/** + * Returns a content sandbox that can be used by the execute_foo functions. + */ +function createExecuteContentSandbox(aWindow, marionette, args) { + try { + args = elementManager.convertWrappedArguments(args, aWindow); + } + catch(e) { + sendError(e.message, e.num, e.stack); + return; + } + + let sandbox = new Cu.Sandbox(aWindow); + sandbox.window = aWindow; + sandbox.document = sandbox.window.document; + sandbox.navigator = sandbox.window.navigator; + sandbox.__namedArgs = elementManager.applyNamedArgs(args); + sandbox.__marionetteParams = args; + sandbox.__proto__ = sandbox.window; + + marionette.exports.forEach(function(fn) { + sandbox[fn] = marionette[fn].bind(marionette); + }); + + return sandbox; +} + +/** + * Execute the given script either as a function body (executeScript) + * or directly (for 'mochitest' like JS Marionette tests) + */ +function executeScript(msg, directInject) { + let script = msg.json.value; + let marionette = new Marionette(false, win, "content", marionetteLogObj); + + let sandbox = createExecuteContentSandbox(win, marionette, msg.json.args); + if (!sandbox) + return; + + sandbox.finish = function sandbox_finish() { + return marionette.generate_results(); + }; + + try { + if (directInject) { + let res = Cu.evalInSandbox(script, sandbox, "1.8"); + sendSyncMessage("Marionette:testLog", {value: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + if (res == undefined || res.passed == undefined) { + sendError("Marionette.finish() not called", 17, null); + } + else { + sendResponse({value: elementManager.wrapValue(res)}); + } + } + else { + let scriptSrc = "let __marionetteFunc = function(){" + script + "};" + + "__marionetteFunc.apply(null, __marionetteParams);"; + let res = Cu.evalInSandbox(scriptSrc, sandbox, "1.8"); + sendSyncMessage("Marionette:testLog", {value: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + sendResponse({value: elementManager.wrapValue(res)}); + } + } + catch (e) { + // 17 = JavascriptException + sendError(e.name + ': ' + e.message, 17, e.stack); + } +} + +/** + * Function to set the timeout of asynchronous scripts + */ +function setScriptTimeout(msg) { + marionetteTimeout = msg.json.value; +} + +/** + * Execute async script + */ +function executeAsyncScript(msg) { + executeWithCallback(msg); +} + +/** + * Execute pure JS test. Handles both async and sync cases. + */ +function executeJSScript(msg) { + if (msg.json.timeout) { + executeWithCallback(msg, msg.json.timeout); + } + else { + executeScript(msg, true); + } +} + +/** + * This function is used by executeAsync and executeJSScript to execute a script + * in a sandbox. + * + * For executeJSScript, it will return a message only when the finish() method is called. + * For executeAsync, it will return a response when marionetteScriptFinished/arguments[arguments.length-1] + * method is called, or if it times out. + */ +function executeWithCallback(msg, timeout) { + win.addEventListener("unload", errUnload, false); + let script = msg.json.value; + let command_id = msg.json.id; + + // Error code 28 is scriptTimeout, but spec says execute_async should return 21 (Timeout), + // see http://code.google.com/p/selenium/wiki/JsonWireProtocol#/session/:sessionId/execute_async. + // However Selenium code returns 28, see + // http://code.google.com/p/selenium/source/browse/trunk/javascript/firefox-driver/js/evaluate.js. + // We'll stay compatible with the Selenium code. + let timeoutId = win.setTimeout(function() { + contentAsyncReturnFunc('timed out', 28); + }, marionetteTimeout); + win.addEventListener('error', function win__onerror(evt) { + win.removeEventListener('error', win__onerror, true); + contentAsyncReturnFunc(evt, 17); + return true; + }, true); + + function contentAsyncReturnFunc(value, status) { + win.removeEventListener("unload", errUnload, false); + + /* clear all timeouts potentially generated by the script*/ + for(let i=0; i<=timeoutId; i++) { + win.clearTimeout(i); + } + + sendSyncMessage("Marionette:testLog", {value: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + if (status == 0){ + sendResponse({value: elementManager.wrapValue(value), status: status}, command_id); + } + else { + sendError(value, status, null, command_id); + } + }; + + let scriptSrc; + if (timeout) { + if (marionetteTimeout == null || marionetteTimeout == 0) { + sendError("Please set a timeout", 21, null); + } + scriptSrc = script; + } + else { + scriptSrc = "let marionetteScriptFinished = function(value) { return asyncComplete(value,0);};" + + "__marionetteParams.push(marionetteScriptFinished);" + + "let __marionetteFunc = function() { " + script + "};" + + "__marionetteFunc.apply(null, __marionetteParams); "; + } + + let marionette = new Marionette(true, win, "content", marionetteLogObj); + + let sandbox = createExecuteContentSandbox(win, marionette, msg.json.args); + if (!sandbox) + return; + + sandbox.asyncComplete = contentAsyncReturnFunc; + sandbox.finish = function sandbox_finish() { + contentAsyncReturnFunc(marionette.generate_results(), 0); + }; + + try { + Cu.evalInSandbox(scriptSrc, sandbox, "1.8"); + } catch (e) { + // 17 = JavascriptException + sendError(e.name + ': ' + e.message, 17, e.stack); + } +} + +/** + * Function to set the timeout period for element searching + */ +function setSearchTimeout(msg) { + try { + elementManager.setSearchTimeout(msg.json.value); + } + catch (e) { + sendError(e.message, e.num, e.stack); + return; + } + sendOk(); +} + +/** + * Navigate to URI. Handles the case where we navigate within an iframe. + * All other navigation is handled by the server (in chrome space). + */ +function goUrl(msg) { + if (activeFrame != null) { + win.document.location = msg.json.value; + //TODO: replace this with event firing when Bug 720714 is resolved + let checkTimer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer); + let checkLoad = function () { + if (win.document.readyState == "complete") { + sendOk(); + } + else { + checkTimer.initWithCallback(checkLoad, 100, Components.interfaces.nsITimer.TYPE_ONE_SHOT); + } + }; + checkLoad(); + } + else { + sendAsyncMessage("Marionette:goUrl", {value: msg.json.value}); + } +} + +/** + * Get the current URI + */ +function getUrl(msg) { + sendResponse({value: win.location.href}); +} + +/** + * Go back in history + */ +function goBack(msg) { + win.history.back(); + sendOk(); +} + +/** + * Go forward in history + */ +function goForward(msg) { + win.history.forward(); + sendOk(); +} + +/** + * Refresh the page + */ +function refresh(msg) { + win.location.reload(true); + let listen = function() { removeEventListener("DOMContentLoaded", arguments.callee, false); sendOk() } ; + addEventListener("DOMContentLoaded", listen, false); +} + +/** + * Find an element in the document using requested search strategy + */ +function findElementContent(msg) { + //Todo: extend to support findChildElement + let id; + try { + let notify = function(id) { sendResponse({value:id});}; + id = elementManager.find(msg.json, win.document, notify, false); + } + catch (e) { + sendError(e.message, e.num, e.stack); + return; + } +} + +/** + * Find elements in the document using requested search strategy + */ +function findElementsContent(msg) { + //Todo: extend to support findChildElement + let id; + try { + let notify = function(id) { sendResponse({value:id});}; + id = elementManager.find(msg.json, win.document, notify, true); + } + catch (e) { + sendError(e.message, e.num, e.stack); + return; + } +} + +/** + * Send click event to element + */ +function clickElement(msg) { + let el; + try { + el = elementManager.getKnownElement(msg.json.element, win); + } + catch (e) { + sendError(e.message, e.num, e.stack); + return; + } + el.click(); + sendOk(); +} + +/** + * Switch to frame given either the server-assigned element id, + * its index in window.frames, or the iframe's name or id. + */ +function switchToFrame(msg) { + let foundFrame = null; + if ((msg.json.value == null) && (msg.json.element == null)) { + win = content; + activeFrame = null; + content.focus(); + sendOk(); + return; + } + if (msg.json.element != undefined) { + if (elementManager.seenItems[msg.json.element] != undefined) { + let wantedFrame = elementManager.getKnownElement(msg.json.element, win);//HTMLIFrameElement + let numFrames = win.frames.length; + for (let i = 0; i < numFrames; i++) { + if (win.frames[i].frameElement == wantedFrame) { + win = win.frames[i]; + activeFrame = i; + win.focus(); + sendOk(); + return; + } + } + } + } + switch(typeof(msg.json.value)) { + case "string" : + let foundById = null; + let numFrames = win.frames.length; + for (let i = 0; i < numFrames; i++) { + //give precedence to name + let frame = win.frames[i]; + let frameElement = frame.frameElement; + if (frameElement.name == msg.json.value) { + foundFrame = i; + break; + } else if ((foundById == null) && (frameElement.id == msg.json.value)) { + foundById = i; + } + } + if ((foundFrame == null) && (foundById != null)) { + foundFrame = foundById; + } + break; + case "number": + if (win.frames[msg.json.value] != undefined) { + foundFrame = msg.json.value; + } + break; + } + //TODO: implement index + if (foundFrame != null) { + let frameWindow = win.frames[foundFrame]; + activeFrame = foundFrame; + win = frameWindow; + win.focus(); + sendOk(); + } else { + sendError("Unable to locate frame: " + msg.json.value, 8, null); + } +} + +//call register self when we get loaded +registerSelf(); diff --git a/testing/marionette/marionette-log-obj.js b/testing/marionette/marionette-log-obj.js new file mode 100644 index 00000000000..f19902d2a10 --- /dev/null +++ b/testing/marionette/marionette-log-obj.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function MarionetteLogObj() { + this.logs = []; +} +MarionetteLogObj.prototype = { + /** + * Log message. Accepts user defined log-level. + * @param msg String + * The message to be logged + * @param level String + * The logging level to be used + */ + log: function ML_log(msg, level) { + let lev = level ? level : "INFO"; + this.logs.push( [lev, msg, (new Date()).toString()]); + }, + + /** + * Add a list of logs to its list + * @param msgs Object + * Takes a list of strings + */ + addLogs: function ML_addLogs(msgs) { + for (let i = 0; i < msgs.length; i++) { + this.logs.push(msgs[i]); + } + }, + + /** + * Return all logged messages. + */ + getLogs: function ML_getLogs() { + return this.logs; + }, + + /** + * Clears the logs + */ + clearLogs: function ML_clearLogs() { + this.logs = []; + }, +} diff --git a/testing/marionette/marionette-simpletest.js b/testing/marionette/marionette-simpletest.js new file mode 100644 index 00000000000..51609940c95 --- /dev/null +++ b/testing/marionette/marionette-simpletest.js @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +/* + * The Marionette object, passed to the script context. + */ + +function Marionette(is_async, window, context, logObj) { + this.is_async = is_async; + this.window = window; + this.tests = []; + this.logObj = logObj; + this.context = context; + this.exports = ['ok', 'is', 'isnot', 'log', 'getLogs', 'generate_results', 'waitFor']; +} + +Marionette.prototype = { + ok: function Marionette__ok(condition, name, diag) { + let test = {'result': !!condition, 'name': name, 'diag': diag}; + this.logResult(test, "TEST-PASS", "TEST-UNEXPECTED-FAIL"); + this.tests.push(test); + }, + + is: function Marionette__is(a, b, name) { + let pass = (a == b); + let diag = pass ? this.repr(a) + " should equal " + this.repr(b) + : "got " + this.repr(a) + ", expected " + this.repr(b); + this.ok(pass, name, diag); + }, + + isnot: function Marionette__isnot (a, b, name) { + let pass = (a != b); + let diag = pass ? this.repr(a) + " should not equal " + this.repr(b) + : "didn't expect " + this.repr(a) + ", but got it"; + this.ok(pass, name, diag); + }, + + log: function Marionette__log(msg, level) { + if (this.logObj != null) { + this.logObj.log(msg, level); + } + }, + + getLogs: function Marionette__getLogs() { + if (this.logObj != null) { + this.logObj.getLogs(); + } + }, + + generate_results: function Marionette__generate_results() { + let passed = 0; + let failed = 0; + let failures = []; + for (let i in this.tests) { + if(this.tests[i].result) { + passed++; + } + else { + failed++; + failures.push({'name': this.tests[i].name, + 'diag': this.tests[i].diag}); + } + } + return {"passed": passed, "failed": failed, "failures": failures}; + }, + + logToFile: function Marionette__logToFile(file) { + //TODO + }, + + logResult: function Marionette__logResult(test, passString, failString) { + //TODO: dump to file + let resultString = test.result ? passString : failString; + let diagnostic = test.name + (test.diag ? " - " + test.diag : ""); + let msg = [resultString, diagnostic].join(" | "); + dump("MARIONETTE TEST RESULT:" + msg + "\n"); + }, + + repr: function Marionette__repr(o) { + if (typeof(o) == "undefined") { + return "undefined"; + } else if (o === null) { + return "null"; + } + try { + if (typeof(o.__repr__) == 'function') { + return o.__repr__(); + } else if (typeof(o.repr) == 'function' && o.repr != arguments.callee) { + return o.repr(); + } + } catch (e) { + } + try { + if (typeof(o.NAME) == 'string' && ( + o.toString == Function.prototype.toString || + o.toString == Object.prototype.toString + )) { + return o.NAME; + } + } catch (e) { + } + let ostring; + try { + ostring = (o + ""); + } catch (e) { + return "[" + typeof(o) + "]"; + } + if (typeof(o) == "function") { + o = ostring.replace(/^\s+/, ""); + let idx = o.indexOf("{"); + if (idx != -1) { + o = o.substr(0, idx) + "{...}"; + } + } + return ostring; + }, + + defaultWaitForTimeout: 10000, + waitFor: function test_waitFor(callback, test, timeout) { + if (test()) { + callback(); + return; + } + timeout = timeout || Date.now(); + if (Date.now() - timeout > this.defaultWaitForTimeout) { + throw 'waitFor timeout'; + } + this.window.setTimeout(this.waitFor.bind(this), 100, callback, test, timeout); + }, +}; + diff --git a/testing/marionette/tests/Makefile.in b/testing/marionette/tests/Makefile.in new file mode 100644 index 00000000000..b64af70b7a3 --- /dev/null +++ b/testing/marionette/tests/Makefile.in @@ -0,0 +1,17 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ +relativesrcdir = testing/marionette/tests + +include $(DEPTH)/config/autoconf.mk + +MODULE = test_marionette + +XPCSHELL_TESTS = unit + +include $(topsrcdir)/config/rules.mk diff --git a/testing/marionette/tests/unit/head_mar.js b/testing/marionette/tests/unit/head_mar.js new file mode 100644 index 00000000000..8420e46575e --- /dev/null +++ b/testing/marionette/tests/unit/head_mar.js @@ -0,0 +1,11 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; +const Cr = Components.results; + +Cu.import("resource:///modules/devtools/dbg-server.jsm"); +Cu.import("resource:///modules/devtools/dbg-client.jsm"); diff --git a/testing/marionette/tests/unit/test_marionette_err.js b/testing/marionette/tests/unit/test_marionette_err.js new file mode 100644 index 00000000000..67e53724e0b --- /dev/null +++ b/testing/marionette/tests/unit/test_marionette_err.js @@ -0,0 +1,53 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + //DebuggerServer.addActors("resource:///modules/marionette-actors.js"); + //DebuggerServer.init(); + + add_test(test_error); + + run_next_test(); +} +function test_error() +{ + //DebuggerServer.openListener(2828, true); + do_test_pending(); + received = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if (received) { + do_check_eq(aPacket.from, id); + if(aPacket.error == undefined) { + do_throw("Expected error, instead received 'done' packet!"); + transport.close(); + } + else { + transport.close(); + } + } + else { + received = true; + id = aPacket.id; + transport.send({to: id, + type: "nonExistent", + }); + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + }, + }; + transport.ready(); +} diff --git a/testing/marionette/tests/unit/test_marionette_exec.js b/testing/marionette/tests/unit/test_marionette_exec.js new file mode 100644 index 00000000000..ade850b2bf7 --- /dev/null +++ b/testing/marionette/tests/unit/test_marionette_exec.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + //DebuggerServer.addActors("resource:///modules/marionette-actors.js"); + //DebuggerServer.init(); + + add_test(test_execute); + run_next_test(); +} + +function test_execute() +{ + //DebuggerServer.openListener(2828, true); + do_test_pending(); + got_session = false; + received = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else { + if (received) { + do_check_eq(aPacket.from, id); + if(aPacket.value == "3") { + transport.send({to: id, + type: "executeScript", + value: "return 5+arguments[0];", + args: [1], + }); + } + if(aPacket.value == "6") { + transport.send({to: id, + type: "deleteSession" + }); + transport.close(); + } + if(aPacket.error != undefined) { + do_throw("Received error: " + aPacket.error); + transport.close(); + } + } + else { + received = true; + do_check_eq('session', aPacket.value); + transport.send({to: id, + type: "executeScript", + value: "alert('asdf'); return 2+1;", + }); + } + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + delete transport; + }, + }; + transport.ready(); +} diff --git a/testing/marionette/tests/unit/test_marionette_execAsync.js b/testing/marionette/tests/unit/test_marionette_execAsync.js new file mode 100644 index 00000000000..765f80e39a3 --- /dev/null +++ b/testing/marionette/tests/unit/test_marionette_execAsync.js @@ -0,0 +1,193 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + add_test(test_executeAsync); + add_test(test_executeAsyncTimeout); + add_test(test_executeAsyncUnload); //TODO: fix unload listener + run_next_test(); +} + +function test_executeAsync() +{ + do_test_pending(); + got_session = false; + received = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else { + if (received) { + do_check_eq(aPacket.from, id); + if(aPacket.ok == true) { + transport.send({to: id, + type: "executeAsyncScript", + value: "arguments[arguments.length - 1](5+1);", + }); + } + else if(aPacket.value == "6") { + transport.send({to: id, + type: "deleteSession", + }); + transport.close(); + } + else if(aPacket.error != undefined) { + do_throw("Received error: " + aPacket.error); + transport.close(); + } + } + else { + received = true; + do_check_eq('session', aPacket.value); + transport.send({to: id, + type: "setScriptTimeout", + value: "2000", + }); + } + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + delete transport; + run_next_test(); + }, + }; + transport.ready(); +} + +function test_executeAsyncTimeout() +{ + do_test_pending(); + got_session = false; + received = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else { + if (received) { + do_check_eq(aPacket.from, id); + if(aPacket.ok == true) { + transport.send({to: id, + type: "executeAsyncScript", + value: "window.setTimeout(arguments[arguments.length - 1], 5000, 6);", + }); + } + else if(aPacket.value == "6") { + do_throw("Should have timed out!"); + transport.close(); + } + else if(aPacket.error != undefined) { + do_check_eq(aPacket.error.message, "timed out"); + do_check_eq(aPacket.error.status, 28); + transport.close(); + } + } + else { + received = true; + do_check_eq('session', aPacket.value); + transport.send({to: id, + type: "setScriptTimeout", + value: "2000", + }); + } + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + delete transport; + run_next_test(); + }, + }; + transport.ready(); +} + +function test_executeAsyncUnload() +{ + do_test_pending(); + got_session = false; + received = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else { + if (received) { + do_check_eq(aPacket.from, id); + if(aPacket.ok == true) { + transport.send({to: id, + type: "executeAsyncScript", + value: "window.location.reload();", + }); + } + else if(aPacket.value == "6") { + do_throw("Should have thrown unload error!"); + transport.close(); + } + else if(aPacket.error != undefined) { + do_check_eq(aPacket.error.status, 17); + transport.close(); + } + } + else { + received = true; + do_check_eq('session', aPacket.value); + transport.send({to: id, + type: "setScriptTimeout", + value: "2000", + }); + } + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + delete transport; + run_next_test(); + }, + }; + transport.ready(); +} diff --git a/testing/marionette/tests/unit/test_marionette_execjs.js b/testing/marionette/tests/unit/test_marionette_execjs.js new file mode 100644 index 00000000000..19ff1ec9846 --- /dev/null +++ b/testing/marionette/tests/unit/test_marionette_execjs.js @@ -0,0 +1,365 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function run_test() +{ + add_test(test_execute); + add_test(test_execute_async); + add_test(test_execute_async_timeout); + add_test(test_execute_chrome); + add_test(test_execute_async_chrome); + add_test(test_execute_async_timeout_chrome); + run_next_test(); +} + +function test_execute() +{ + do_test_pending(); + got_session = false; + received = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else { + if (received) { + do_check_eq(aPacket.from, id); + do_check_eq(aPacket.value.passed, 1); + do_check_eq(aPacket.value.failed, 0); + transport.close(); + } + else { + received = true; + do_check_eq('mobile', aPacket.value); + transport.send({to: id, + type: "executeJSScript", + value: "Marionette.is(1,1, 'should return 1'); Marionette.finish();", + timeout: false + }); + } + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + delete transport; + }, + }; + transport.ready(); +} + +function test_execute_async() +{ + do_test_pending(); + got_session = false; + received = false; + received2 = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else { + if (received) { + if(received2) { + do_check_eq(aPacket.from, id); + do_check_eq(aPacket.value.passed, 1); + do_check_eq(aPacket.value.failed, 0); + transport.close(); + } + else { + received2 = true; + transport.send({to: id, + type: "executeJSScript", + value: "Marionette.is(1,1, 'should return 1'); Marionette.finish();", + timeout: true + }); + } + } + else { + received = true; + do_check_eq('mobile', aPacket.value); + transport.send({to: id, + type: "setScriptTimeout", + value: "2000", + }); + } + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + delete transport; + }, + }; + transport.ready(); +} + +function test_execute_async_timeout() +{ + do_test_pending(); + got_session = false; + received = false; + received2 = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else { + if (received) { + if(received2) { + do_check_eq(aPacket.from, id); + do_check_eq(aPacket.error.status, 28); + transport.close(); + } + else { + received2 = true; + transport.send({to: id, + type: "executeJSScript", + value: "Marionette.is(1,1, 'should return 1');", + timeout: true + }); + } + } + else { + received = true; + do_check_eq('mobile', aPacket.value); + transport.send({to: id, + type: "setScriptTimeout", + value: "2000", + }); + } + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + delete transport; + }, + }; + transport.ready(); +} + +function test_execute_chrome() +{ + do_test_pending(); + got_session = false; + got_context = false; + received = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else if (!got_context) { + got_context = true; + do_check_eq('mobile', aPacket.value); + transport.send({to: id, + type: "setContext", + value: "chrome", + }); + } + else if (!received) { + received = true; + transport.send({to: id, + type: "executeJSScript", + value: "Marionette.is(1,1, 'should return 1'); Marionette.finish();", + timeout: false + }); + } + else { + do_check_eq(aPacket.from, id); + do_check_eq(aPacket.value.passed, 1); + do_check_eq(aPacket.value.failed, 0); + transport.close(); + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + delete transport; + }, + }; + transport.ready(); +} + +function test_execute_async_chrome() +{ + do_test_pending(); + got_session = false; + got_context = false; + received = false; + received2 = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else if (!got_context) { + got_context = true; + do_check_eq('mobile', aPacket.value); + transport.send({to: id, + type: "setContext", + value: "chrome", + }); + } + else if (!received) { + received = true; + transport.send({to: id, + type: "setScriptTimeout", + value: "2000", + }); + } + else if (!received2) { + received2 = true; + transport.send({to: id, + type: "executeJSScript", + value: "Marionette.is(1,1, 'should return 1'); Marionette.finish();", + timeout: true + }); + } + else { + do_check_eq(aPacket.from, id); + do_check_eq(aPacket.value.passed, 1); + do_check_eq(aPacket.value.failed, 0); + transport.close(); + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + delete transport; + }, + }; + transport.ready(); +} + +function test_execute_async_timeout_chrome() +{ + do_test_pending(); + got_session = false; + got_context = false; + received = false; + received2 = false; + id = ""; + + let transport = debuggerSocketConnect("127.0.0.1", 2828); + transport.hooks = { + onPacket: function(aPacket) { + this.onPacket = function(aPacket) { + if(!got_session) { + got_session=true; + id = aPacket.id; + transport.send({to: id, + type: "newSession", + }); + } + else if (!got_context) { + got_context = true; + do_check_eq('mobile', aPacket.value); + transport.send({to: id, + type: "setContext", + value: "chrome", + }); + } + else if (!received) { + received = true; + transport.send({to: id, + type: "setScriptTimeout", + value: "2000", + }); + } + else if (!received2) { + received2 = true; + transport.send({to: id, + type: "executeJSScript", + value: "Marionette.is(1,1, 'should return 1');", + timeout: true + }); + } + else { + do_check_eq(aPacket.from, id); + do_check_eq(aPacket.error.status, 28); + transport.close(); + } + } + transport.send({to: "root", + type: "getMarionetteID", + }); + }, + onClosed: function(aStatus) { + do_check_eq(aStatus, 0); + do_test_finished(); + run_next_test(); + delete transport; + }, + }; + transport.ready(); +} diff --git a/testing/marionette/tests/unit/xpcshell.ini b/testing/marionette/tests/unit/xpcshell.ini new file mode 100644 index 00000000000..43eab6d5504 --- /dev/null +++ b/testing/marionette/tests/unit/xpcshell.ini @@ -0,0 +1,8 @@ +[DEFAULT] +head = head_mar.js +tail = + +[test_marionette_exec.js] +[test_marionette_execjs.js] +[test_marionette_execAsync.js] +[test_marionette_err.js] diff --git a/testing/xpcshell/xpcshell.ini b/testing/xpcshell/xpcshell.ini index 68fe6d89160..8c36df500e7 100644 --- a/testing/xpcshell/xpcshell.ini +++ b/testing/xpcshell/xpcshell.ini @@ -46,6 +46,7 @@ skip-if = os == "android" [include:toolkit/mozapps/update/test_svc/unit/xpcshell.ini] [include:toolkit/mozapps/update/test/unit/xpcshell.ini] [include:security/manager/ssl/tests/unit/xpcshell.ini] +[include:testing/marionette/tests/unit/xpcshell.ini] [include:testing/xpcshell/example/unit/xpcshell.ini] [include:xpcom/tests/unit/xpcshell.ini] [include:modules/libpref/test/unit/xpcshell.ini] diff --git a/toolkit/toolkit-tiers.mk b/toolkit/toolkit-tiers.mk index b29502d99eb..e58f675be60 100644 --- a/toolkit/toolkit-tiers.mk +++ b/toolkit/toolkit-tiers.mk @@ -273,6 +273,10 @@ ifdef MOZ_MAPINFO tier_platform_dirs += tools/codesighs endif +ifdef ENABLE_MARIONETTE +tier_platform_dirs += testing/marionette +endif + ifdef ENABLE_TESTS tier_platform_dirs += testing/mochitest tier_platform_dirs += testing/xpcshell