diff -r 000000000000 -r 6474c204b198 testing/marionette/marionette-listener.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/testing/marionette/marionette-listener.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,2196 @@ +/* -*- 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 {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +let uuidGen = Cc["@mozilla.org/uuid-generator;1"] + .getService(Ci.nsIUUIDGenerator); + +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-common.js"); +Cu.import("chrome://marionette/content/marionette-elements.js"); +Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/NetUtil.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +let utils = {}; +utils.window = content; +// Load Event/ChromeUtils for use with JS scripts: +loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils); +loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils); +loader.loadSubScript("chrome://marionette/content/atoms.js", utils); +loader.loadSubScript("chrome://marionette/content/marionette-sendkeys.js", utils); + +loader.loadSubScript("chrome://specialpowers/content/specialpowersAPI.js"); +loader.loadSubScript("chrome://specialpowers/content/specialpowers.js"); + +let marionetteLogObj = new MarionetteLogObj(); + +let isB2G = false; + +let marionetteTestName; +let winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); +let listenerId = null; //unique ID of this listener +let curFrame = content; +let previousFrame = null; +let elementManager = new ElementManager([]); +let importedScripts = null; +let inputSource = null; + +// The sandbox we execute test scripts in. Gets lazily created in +// createExecuteContentSandbox(). +let sandbox; + +// the unload handler +let onunload; + +// Flag to indicate whether an async script is currently running or not. +let asyncTestRunning = false; +let asyncTestCommandId; +let asyncTestTimeoutId; + +let inactivityTimeoutId = null; +let heartbeatCallback = function () {}; // Called by the simpletest methods. + +let originalOnError; +//timer for doc changes +let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); +//timer for readystate +let readyStateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); +// Send move events about this often +let EVENT_INTERVAL = 30; // milliseconds +// For assigning unique ids to all touches +let nextTouchId = 1000; +//Keep track of active Touches +let touchIds = {}; +// last touch for each fingerId +let multiLast = {}; +let lastCoordinates = null; +let isTap = false; +let scrolling = false; +// whether to send mouse event +let mouseEventsOnly = false; + +Cu.import("resource://gre/modules/Log.jsm"); +let logger = Log.repository.getLogger("Marionette"); +logger.info("loaded marionette-listener.js"); +let modalHandler = function() { + // This gets called on the system app only since it receives the mozbrowserprompt event + sendSyncMessage("Marionette:switchedToFrame", { frameValue: null, storePrevious: true }); + let isLocal = sendSyncMessage("MarionetteFrame:handleModal", {})[0].value; + if (isLocal) { + previousFrame = curFrame; + } + curFrame = content; + sandbox = null; +}; + +/** + * 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 msg = {value: winUtil.outerWindowID, href: content.location.href}; + // register will have the ID and a boolean describing if this is the main process or not + let register = sendSyncMessage("Marionette:register", msg); + + if (register[0]) { + listenerId = register[0][0].id; + // check if we're the main process + if (register[0][1] == true) { + addMessageListener("MarionetteMainListener:emitTouchEvent", emitTouchEventForIFrame); + } + importedScripts = FileUtils.getDir('TmpD', [], false); + importedScripts.append('marionetteContentScripts'); + startListeners(); + } +} + +function emitTouchEventForIFrame(message) { + let message = message.json; + let frames = curFrame.document.getElementsByTagName("iframe"); + let iframe = frames[message.index]; + let identifier = touchId = nextTouchId++; + let tabParent = iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.tabParent; + tabParent.injectTouchEvent(message.type, [identifier], + [message.clientX], [message.clientY], + [message.radiusX], [message.radiusY], + [message.rotationAngle], [message.force], + 1, 0); +} + +/** + * Add a message listener that's tied to our listenerId. + */ +function addMessageListenerId(messageName, handler) { + addMessageListener(messageName + listenerId, handler); +} + +/** + * Remove a message listener that's tied to our listenerId. + */ +function removeMessageListenerId(messageName, handler) { + removeMessageListener(messageName + listenerId, handler); +} + +/** + * Start all message listeners + */ +function startListeners() { + addMessageListenerId("Marionette:newSession", newSession); + addMessageListenerId("Marionette:executeScript", executeScript); + addMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript); + addMessageListenerId("Marionette:executeJSScript", executeJSScript); + addMessageListenerId("Marionette:singleTap", singleTap); + addMessageListenerId("Marionette:actionChain", actionChain); + addMessageListenerId("Marionette:multiAction", multiAction); + addMessageListenerId("Marionette:get", get); + addMessageListenerId("Marionette:getCurrentUrl", getCurrentUrl); + addMessageListenerId("Marionette:getTitle", getTitle); + addMessageListenerId("Marionette:getPageSource", getPageSource); + addMessageListenerId("Marionette:goBack", goBack); + addMessageListenerId("Marionette:goForward", goForward); + addMessageListenerId("Marionette:refresh", refresh); + addMessageListenerId("Marionette:findElementContent", findElementContent); + addMessageListenerId("Marionette:findElementsContent", findElementsContent); + addMessageListenerId("Marionette:getActiveElement", getActiveElement); + addMessageListenerId("Marionette:clickElement", clickElement); + addMessageListenerId("Marionette:getElementAttribute", getElementAttribute); + addMessageListenerId("Marionette:getElementText", getElementText); + addMessageListenerId("Marionette:getElementTagName", getElementTagName); + addMessageListenerId("Marionette:isElementDisplayed", isElementDisplayed); + addMessageListenerId("Marionette:getElementValueOfCssProperty", getElementValueOfCssProperty); + addMessageListenerId("Marionette:submitElement", submitElement); + addMessageListenerId("Marionette:getElementSize", getElementSize); + addMessageListenerId("Marionette:isElementEnabled", isElementEnabled); + addMessageListenerId("Marionette:isElementSelected", isElementSelected); + addMessageListenerId("Marionette:sendKeysToElement", sendKeysToElement); + addMessageListenerId("Marionette:getElementLocation", getElementLocation); + addMessageListenerId("Marionette:clearElement", clearElement); + addMessageListenerId("Marionette:switchToFrame", switchToFrame); + addMessageListenerId("Marionette:deleteSession", deleteSession); + addMessageListenerId("Marionette:sleepSession", sleepSession); + addMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult); + addMessageListenerId("Marionette:importScript", importScript); + addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus); + addMessageListenerId("Marionette:setTestName", setTestName); + addMessageListenerId("Marionette:takeScreenshot", takeScreenshot); + addMessageListenerId("Marionette:addCookie", addCookie); + addMessageListenerId("Marionette:getCookies", getCookies); + addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies); + addMessageListenerId("Marionette:deleteCookie", deleteCookie); +} + +/** + * Used during newSession and restart, called to set up the modal dialog listener in b2g + */ +function waitForReady() { + if (content.document.readyState == 'complete') { + readyStateTimer.cancel(); + content.addEventListener("mozbrowsershowmodalprompt", modalHandler, false); + content.addEventListener("unload", waitForReady, false); + } + else { + readyStateTimer.initWithCallback(waitForReady, 100, Ci.nsITimer.TYPE_ONE_SHOT); + } +} + +/** + * Called when we start a new session. It registers the + * current environment, and resets all values + */ +function newSession(msg) { + isB2G = msg.json.B2G; + resetValues(); + if (isB2G) { + readyStateTimer.initWithCallback(waitForReady, 100, Ci.nsITimer.TYPE_ONE_SHOT); + // We have to set correct mouse event source to MOZ_SOURCE_TOUCH + // to offer a way for event listeners to differentiate + // events being the result of a physical mouse action. + // This is especially important for the touch event shim, + // in order to prevent creating touch event for these fake mouse events. + inputSource = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH; + } +} + +/** + * 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(msg) { + removeMessageListener("Marionette:restart", restart); + if (isB2G) { + readyStateTimer.initWithCallback(waitForReady, 100, Ci.nsITimer.TYPE_ONE_SHOT); + } + registerSelf(); +} + +/** + * Removes all listeners + */ +function deleteSession(msg) { + removeMessageListenerId("Marionette:newSession", newSession); + removeMessageListenerId("Marionette:executeScript", executeScript); + removeMessageListenerId("Marionette:executeAsyncScript", executeAsyncScript); + removeMessageListenerId("Marionette:executeJSScript", executeJSScript); + removeMessageListenerId("Marionette:singleTap", singleTap); + removeMessageListenerId("Marionette:actionChain", actionChain); + removeMessageListenerId("Marionette:multiAction", multiAction); + removeMessageListenerId("Marionette:get", get); + removeMessageListenerId("Marionette:getTitle", getTitle); + removeMessageListenerId("Marionette:getPageSource", getPageSource); + removeMessageListenerId("Marionette:getCurrentUrl", getCurrentUrl); + removeMessageListenerId("Marionette:goBack", goBack); + removeMessageListenerId("Marionette:goForward", goForward); + removeMessageListenerId("Marionette:refresh", refresh); + removeMessageListenerId("Marionette:findElementContent", findElementContent); + removeMessageListenerId("Marionette:findElementsContent", findElementsContent); + removeMessageListenerId("Marionette:getActiveElement", getActiveElement); + removeMessageListenerId("Marionette:clickElement", clickElement); + removeMessageListenerId("Marionette:getElementAttribute", getElementAttribute); + removeMessageListenerId("Marionette:getElementTagName", getElementTagName); + removeMessageListenerId("Marionette:isElementDisplayed", isElementDisplayed); + removeMessageListenerId("Marionette:getElementValueOfCssProperty", getElementValueOfCssProperty); + removeMessageListenerId("Marionette:submitElement", submitElement); + removeMessageListenerId("Marionette:getElementSize", getElementSize); + removeMessageListenerId("Marionette:isElementEnabled", isElementEnabled); + removeMessageListenerId("Marionette:isElementSelected", isElementSelected); + removeMessageListenerId("Marionette:sendKeysToElement", sendKeysToElement); + removeMessageListenerId("Marionette:getElementLocation", getElementLocation); + removeMessageListenerId("Marionette:clearElement", clearElement); + removeMessageListenerId("Marionette:switchToFrame", switchToFrame); + removeMessageListenerId("Marionette:deleteSession", deleteSession); + removeMessageListenerId("Marionette:sleepSession", sleepSession); + removeMessageListenerId("Marionette:emulatorCmdResult", emulatorCmdResult); + removeMessageListenerId("Marionette:importScript", importScript); + removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus); + removeMessageListenerId("Marionette:setTestName", setTestName); + removeMessageListenerId("Marionette:takeScreenshot", takeScreenshot); + removeMessageListenerId("Marionette:addCookie", addCookie); + removeMessageListenerId("Marionette:getCookies", getCookies); + removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies); + removeMessageListenerId("Marionette:deleteCookie", deleteCookie); + if (isB2G) { + content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false); + } + elementManager.reset(); + // reset frame to the top-most frame + curFrame = content; + curFrame.focus(); + touchIds = {}; +} + +/* + * 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() { + sandbox = null; + curFrame = content; + mouseEventsOnly = false; +} + +/** + * Dump a logline to stdout. Prepends logline with a timestamp. + */ +function dumpLog(logline) { + dump(Date.now() + " Marionette: " + logline); +} + +/** + * Check if our context was interrupted + */ +function wasInterrupted() { + if (previousFrame) { + let element = content.document.elementFromPoint((content.innerWidth/2), (content.innerHeight/2)); + if (element.id.indexOf("modal-dialog") == -1) { + return true; + } + else { + return false; + } + } + return sendSyncMessage("MarionetteFrame:getInterruptedState", {})[0].value; +} + +/* + * Marionette Methods + */ + +/** + * Returns a content sandbox that can be used by the execute_foo functions. + */ +function createExecuteContentSandbox(aWindow, timeout) { + let sandbox = new Cu.Sandbox(aWindow, {sandboxPrototype: aWindow}); + sandbox.global = sandbox; + sandbox.window = aWindow; + sandbox.document = sandbox.window.document; + sandbox.navigator = sandbox.window.navigator; + sandbox.testUtils = utils; + sandbox.asyncTestCommandId = asyncTestCommandId; + + let marionette = new Marionette(this, aWindow, "content", + marionetteLogObj, timeout, + heartbeatCallback, + marionetteTestName); + sandbox.marionette = marionette; + marionette.exports.forEach(function(fn) { + try { + sandbox[fn] = marionette[fn].bind(marionette); + } + catch(e) { + sandbox[fn] = marionette[fn]; + } + }); + + XPCOMUtils.defineLazyGetter(sandbox, 'SpecialPowers', function() { + return new SpecialPowers(aWindow); + }); + + sandbox.asyncComplete = function sandbox_asyncComplete(value, status, stack, commandId) { + if (commandId == asyncTestCommandId) { + curFrame.removeEventListener("unload", onunload, false); + curFrame.clearTimeout(asyncTestTimeoutId); + + if (inactivityTimeoutId != null) { + curFrame.clearTimeout(inactivityTimeoutId); + } + + + sendSyncMessage("Marionette:shareData", + {log: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + + if (status == 0){ + if (Object.keys(_emu_cbs).length) { + _emu_cbs = {}; + sendError("Emulator callback still pending when finish() called", + 500, null, commandId); + } + else { + sendResponse({value: elementManager.wrapValue(value), status: status}, + commandId); + } + } + else { + sendError(value, status, stack, commandId); + } + + asyncTestRunning = false; + asyncTestTimeoutId = undefined; + asyncTestCommandId = undefined; + inactivityTimeoutId = null; + } + }; + sandbox.finish = function sandbox_finish() { + if (asyncTestRunning) { + sandbox.asyncComplete(marionette.generate_results(), 0, null, sandbox.asyncTestCommandId); + } else { + return marionette.generate_results(); + } + }; + sandbox.marionetteScriptFinished = function sandbox_marionetteScriptFinished(value) { + return sandbox.asyncComplete(value, 0, null, sandbox.asyncTestCommandId); + }; + + return sandbox; +} + +/** + * Execute the given script either as a function body (executeScript) + * or directly (for 'mochitest' like JS Marionette tests) + */ +function executeScript(msg, directInject) { + // Set up inactivity timeout. + if (msg.json.inactivityTimeout) { + let setTimer = function() { + inactivityTimeoutId = curFrame.setTimeout(function() { + sendError('timed out due to inactivity', 28, null, asyncTestCommandId); + }, msg.json.inactivityTimeout); + }; + + setTimer(); + heartbeatCallback = function resetInactivityTimeout() { + curFrame.clearTimeout(inactivityTimeoutId); + setTimer(); + }; + } + + asyncTestCommandId = msg.json.command_id; + let script = msg.json.script; + + if (msg.json.newSandbox || !sandbox) { + sandbox = createExecuteContentSandbox(curFrame, + msg.json.timeout); + if (!sandbox) { + sendError("Could not create sandbox!", 500, null, asyncTestCommandId); + return; + } + } + else { + sandbox.asyncTestCommandId = asyncTestCommandId; + } + + try { + if (directInject) { + if (importedScripts.exists()) { + let stream = Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + stream.init(importedScripts, -1, 0, 0); + let data = NetUtil.readInputStreamToString(stream, stream.available()); + script = data + script; + } + let res = Cu.evalInSandbox(script, sandbox, "1.8", "dummy file" ,0); + sendSyncMessage("Marionette:shareData", + {log: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + + if (res == undefined || res.passed == undefined) { + sendError("Marionette.finish() not called", 17, null, asyncTestCommandId); + } + else { + sendResponse({value: elementManager.wrapValue(res)}, asyncTestCommandId); + } + } + else { + try { + sandbox.__marionetteParams = elementManager.convertWrappedArguments( + msg.json.args, curFrame); + } + catch(e) { + sendError(e.message, e.code, e.stack, asyncTestCommandId); + return; + } + + script = "let __marionetteFunc = function(){" + script + "};" + + "__marionetteFunc.apply(null, __marionetteParams);"; + if (importedScripts.exists()) { + let stream = Components.classes["@mozilla.org/network/file-input-stream;1"]. + createInstance(Components.interfaces.nsIFileInputStream); + stream.init(importedScripts, -1, 0, 0); + let data = NetUtil.readInputStreamToString(stream, stream.available()); + script = data + script; + } + let res = Cu.evalInSandbox(script, sandbox, "1.8", "dummy file", 0); + sendSyncMessage("Marionette:shareData", + {log: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + sendResponse({value: elementManager.wrapValue(res)}, asyncTestCommandId); + } + } + catch (e) { + // 17 = JavascriptException + let error = createStackMessage(e, + "execute_script", + msg.json.filename, + msg.json.line, + script); + sendError(error[0], 17, error[1], asyncTestCommandId); + } +} + +/** + * Sets the test name, used in logging messages. + */ +function setTestName(msg) { + marionetteTestName = msg.json.value; + sendOk(msg.json.command_id); +} + +/** + * Execute async script + */ +function executeAsyncScript(msg) { + executeWithCallback(msg); +} + +/** + * Execute pure JS test. Handles both async and sync cases. + */ +function executeJSScript(msg) { + if (msg.json.async) { + executeWithCallback(msg, msg.json.async); + } + 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, useFinish) { + // Set up inactivity timeout. + if (msg.json.inactivityTimeout) { + let setTimer = function() { + inactivityTimeoutId = curFrame.setTimeout(function() { + sandbox.asyncComplete('timed out due to inactivity', 28, null, asyncTestCommandId); + }, msg.json.inactivityTimeout); + }; + + setTimer(); + heartbeatCallback = function resetInactivityTimeout() { + curFrame.clearTimeout(inactivityTimeoutId); + setTimer(); + }; + } + + let script = msg.json.script; + asyncTestCommandId = msg.json.command_id; + + onunload = function() { + sendError("unload was called", 17, null, asyncTestCommandId); + }; + curFrame.addEventListener("unload", onunload, false); + + if (msg.json.newSandbox || !sandbox) { + sandbox = createExecuteContentSandbox(curFrame, + msg.json.timeout); + if (!sandbox) { + sendError("Could not create sandbox!", 17, null, asyncTestCommandId); + return; + } + } + else { + sandbox.asyncTestCommandId = asyncTestCommandId; + } + sandbox.tag = script; + + // 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. + asyncTestTimeoutId = curFrame.setTimeout(function() { + sandbox.asyncComplete('timed out', 28, null, asyncTestCommandId); + }, msg.json.timeout); + + originalOnError = curFrame.onerror; + curFrame.onerror = function errHandler(errMsg, url, line) { + sandbox.asyncComplete(errMsg, 17, "@" + url + ", line " + line, asyncTestCommandId); + curFrame.onerror = originalOnError; + }; + + let scriptSrc; + if (useFinish) { + if (msg.json.timeout == null || msg.json.timeout == 0) { + sendError("Please set a timeout", 21, null, asyncTestCommandId); + } + scriptSrc = script; + } + else { + try { + sandbox.__marionetteParams = elementManager.convertWrappedArguments( + msg.json.args, curFrame); + } + catch(e) { + sendError(e.message, e.code, e.stack, asyncTestCommandId); + return; + } + + scriptSrc = "__marionetteParams.push(marionetteScriptFinished);" + + "let __marionetteFunc = function() { " + script + "};" + + "__marionetteFunc.apply(null, __marionetteParams); "; + } + + try { + asyncTestRunning = true; + if (importedScripts.exists()) { + let stream = Cc["@mozilla.org/network/file-input-stream;1"]. + createInstance(Ci.nsIFileInputStream); + stream.init(importedScripts, -1, 0, 0); + let data = NetUtil.readInputStreamToString(stream, stream.available()); + scriptSrc = data + scriptSrc; + } + Cu.evalInSandbox(scriptSrc, sandbox, "1.8", "dummy file", 0); + } catch (e) { + // 17 = JavascriptException + let error = createStackMessage(e, + "execute_async_script", + msg.json.filename, + msg.json.line, + scriptSrc); + sandbox.asyncComplete(error[0], 17, error[1], asyncTestCommandId); + } +} + +/** + * This function creates a touch event given a touch type and a touch + */ +function emitTouchEvent(type, touch) { + if (!wasInterrupted()) { + let loggingInfo = "emitting Touch event of type " + type + " to element with id: " + touch.target.id + " and tag name: " + touch.target.tagName + " at coordinates (" + touch.clientX + ", " + touch.clientY + ") relative to the viewport"; + dumpLog(loggingInfo); + var docShell = curFrame.document.defaultView. + QueryInterface(Components.interfaces.nsIInterfaceRequestor). + getInterface(Components.interfaces.nsIWebNavigation). + QueryInterface(Components.interfaces.nsIDocShell); + if (docShell.asyncPanZoomEnabled && scrolling) { + // if we're in APZ and we're scrolling, we must use injectTouchEvent to dispatch our touchmove events + let index = sendSyncMessage("MarionetteFrame:getCurrentFrameId"); + // only call emitTouchEventForIFrame if we're inside an iframe. + if (index != null) { + sendSyncMessage("Marionette:emitTouchEvent", {index: index, type: type, id: touch.identifier, + clientX: touch.clientX, clientY: touch.clientY, + radiusX: touch.radiusX, radiusY: touch.radiusY, + rotation: touch.rotationAngle, force: touch.force}); + return; + } + } + // we get here if we're not in asyncPacZoomEnabled land, or if we're the main process + /* + Disabled per bug 888303 + marionetteLogObj.log(loggingInfo, "TRACE"); + sendSyncMessage("Marionette:shareData", + {log: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + */ + let domWindowUtils = curFrame.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils); + domWindowUtils.sendTouchEvent(type, [touch.identifier], [touch.clientX], [touch.clientY], [touch.radiusX], [touch.radiusY], [touch.rotationAngle], [touch.force], 1, 0); + } +} + +/** + * This function emit mouse event + * @param: doc is the current document + * type is the type of event to dispatch + * clickCount is the number of clicks, button notes the mouse button + * elClientX and elClientY are the coordinates of the mouse relative to the viewport + */ +function emitMouseEvent(doc, type, elClientX, elClientY, clickCount, button) { + if (!wasInterrupted()) { + let loggingInfo = "emitting Mouse event of type " + type + " at coordinates (" + elClientX + ", " + elClientY + ") relative to the viewport"; + dumpLog(loggingInfo); + /* + Disabled per bug 888303 + marionetteLogObj.log(loggingInfo, "TRACE"); + sendSyncMessage("Marionette:shareData", + {log: elementManager.wrapValue(marionetteLogObj.getLogs())}); + marionetteLogObj.clearLogs(); + */ + let win = doc.defaultView; + let domUtils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindowUtils); + domUtils.sendMouseEvent(type, elClientX, elClientY, button || 0, clickCount || 1, 0, false, 0, inputSource); + } +} + +/** + * Helper function that perform a mouse tap + */ +function mousetap(doc, x, y) { + emitMouseEvent(doc, 'mousemove', x, y); + emitMouseEvent(doc, 'mousedown', x, y); + emitMouseEvent(doc, 'mouseup', x, y); +} + + +/** + * This function generates a pair of coordinates relative to the viewport given a + * target element and coordinates relative to that element's top-left corner. + * @param 'x', and 'y' are the relative to the target. + * If they are not specified, then the center of the target is used. + */ +function coordinates(target, x, y) { + let box = target.getBoundingClientRect(); + if (x == null) { + x = box.width / 2; + } + if (y == null) { + y = box.height / 2; + } + let coords = {}; + coords.x = box.left + x; + coords.y = box.top + y; + return coords; +} + +/** + * This function returns if the element is in viewport + */ +function elementInViewport(el) { + let rect = el.getBoundingClientRect(); + let viewPort = {top: curFrame.pageYOffset, + left: curFrame.pageXOffset, + bottom: (curFrame.pageYOffset + curFrame.innerHeight), + right:(curFrame.pageXOffset + curFrame.innerWidth)}; + return (viewPort.left <= rect.right + curFrame.pageXOffset && + rect.left + curFrame.pageXOffset <= viewPort.right && + viewPort.top <= rect.bottom + curFrame.pageYOffset && + rect.top + curFrame.pageYOffset <= viewPort.bottom); +} + +/** + * This function throws the visibility of the element error + */ +function checkVisible(el) { + //check if the element is visible + let visible = utils.isElementDisplayed(el); + if (!visible) { + return false; + } + if (el.tagName.toLowerCase() === 'body') { + return true; + } + if (!elementInViewport(el)) { + //check if scroll function exist. If so, call it. + if (el.scrollIntoView) { + el.scrollIntoView(false); + if (!elementInViewport(el)) { + return false; + } + } + else { + return false; + } + } + return true; +} + +//x and y are coordinates relative to the viewport +function generateEvents(type, x, y, touchId, target) { + lastCoordinates = [x, y]; + let doc = curFrame.document; + switch (type) { + case 'tap': + if (mouseEventsOnly) { + mousetap(target.ownerDocument, x, y); + } + else { + let touchId = nextTouchId++; + let touch = createATouch(target, x, y, touchId); + emitTouchEvent('touchstart', touch); + emitTouchEvent('touchend', touch); + mousetap(target.ownerDocument, x, y); + } + lastCoordinates = null; + break; + case 'press': + isTap = true; + if (mouseEventsOnly) { + emitMouseEvent(doc, 'mousemove', x, y); + emitMouseEvent(doc, 'mousedown', x, y); + } + else { + let touchId = nextTouchId++; + let touch = createATouch(target, x, y, touchId); + emitTouchEvent('touchstart', touch); + touchIds[touchId] = touch; + return touchId; + } + break; + case 'release': + if (mouseEventsOnly) { + emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1]); + } + else { + let touch = touchIds[touchId]; + touch = createATouch(touch.target, lastCoordinates[0], lastCoordinates[1], touchId); + emitTouchEvent('touchend', touch); + if (isTap) { + mousetap(touch.target.ownerDocument, touch.clientX, touch.clientY); + } + delete touchIds[touchId]; + } + isTap = false; + lastCoordinates = null; + break; + case 'cancel': + isTap = false; + if (mouseEventsOnly) { + emitMouseEvent(doc, 'mouseup', lastCoordinates[0], lastCoordinates[1]); + } + else { + emitTouchEvent('touchcancel', touchIds[touchId]); + delete touchIds[touchId]; + } + lastCoordinates = null; + break; + case 'move': + isTap = false; + if (mouseEventsOnly) { + emitMouseEvent(doc, 'mousemove', x, y); + } + else { + touch = createATouch(touchIds[touchId].target, x, y, touchId); + touchIds[touchId] = touch; + emitTouchEvent('touchmove', touch); + } + break; + case 'contextmenu': + isTap = false; + let event = curFrame.document.createEvent('HTMLEvents'); + event.initEvent('contextmenu', true, true); + if (mouseEventsOnly) { + target = doc.elementFromPoint(lastCoordinates[0], lastCoordinates[1]); + } + else { + target = touchIds[touchId].target; + } + target.dispatchEvent(event); + break; + default: + throw {message:"Unknown event type: " + type, code: 500, stack:null}; + } + if (wasInterrupted()) { + if (previousFrame) { + //if previousFrame is set, then we're in a single process environment + curFrame = previousFrame; + previousFrame = null; + sandbox = null; + } + else { + //else we're in OOP environment, so we'll switch to the original OOP frame + sendSyncMessage("Marionette:switchToModalOrigin"); + } + sendSyncMessage("Marionette:switchedToFrame", { restorePrevious: true }); + } +} + +/** + * Function that perform a single tap + */ +function singleTap(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + // after this block, the element will be scrolled into view + if (!checkVisible(el)) { + sendError("Element is not currently visible and may not be manipulated", 11, null, command_id); + return; + } + if (!curFrame.document.createTouch) { + mouseEventsOnly = true; + } + let c = coordinates(el, msg.json.corx, msg.json.cory); + generateEvents('tap', c.x, c.y, null, el); + sendOk(msg.json.command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, msg.json.command_id); + } +} + +/** + * Function to create a touch based on the element + * corx and cory are relative to the viewport, id is the touchId + */ +function createATouch(el, corx, cory, touchId) { + let doc = el.ownerDocument; + let win = doc.defaultView; + let clientX = corx; + let clientY = cory; + let pageX = clientX + win.pageXOffset, + pageY = clientY + win.pageYOffset; + let screenX = clientX + win.mozInnerScreenX, + screenY = clientY + win.mozInnerScreenY; + let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY); + return atouch; +} + +/** + * Function to emit touch events for each finger. e.g. finger=[['press', id], ['wait', 5], ['release']] + * touchId represents the finger id, i keeps track of the current action of the chain + */ +function actions(chain, touchId, command_id, i) { + if (typeof i === "undefined") { + i = 0; + } + if (i == chain.length) { + sendResponse({value: touchId}, command_id); + return; + } + let pack = chain[i]; + let command = pack[0]; + let el; + let c; + i++; + if (command != 'press') { + //if mouseEventsOnly, then touchIds isn't used + if (!(touchId in touchIds) && !mouseEventsOnly) { + sendError("Element has not been pressed", 500, null, command_id); + return; + } + } + switch(command) { + case 'press': + if (lastCoordinates) { + generateEvents('cancel', lastCoordinates[0], lastCoordinates[1], touchId); + sendError("Invalid Command: press cannot follow an active touch event", 500, null, command_id); + return; + } + // look ahead to check if we're scrolling. Needed for APZ touch dispatching. + if ((i != chain.length) && (chain[i][0].indexOf('move') !== -1)) { + scrolling = true; + } + el = elementManager.getKnownElement(pack[1], curFrame); + c = coordinates(el, pack[2], pack[3]); + touchId = generateEvents('press', c.x, c.y, null, el); + actions(chain, touchId, command_id, i); + break; + case 'release': + generateEvents('release', lastCoordinates[0], lastCoordinates[1], touchId); + actions(chain, null, command_id, i); + scrolling = false; + break; + case 'move': + el = elementManager.getKnownElement(pack[1], curFrame); + c = coordinates(el); + generateEvents('move', c.x, c.y, touchId); + actions(chain, touchId, command_id, i); + break; + case 'moveByOffset': + generateEvents('move', lastCoordinates[0] + pack[1], lastCoordinates[1] + pack[2], touchId); + actions(chain, touchId, command_id, i); + break; + case 'wait': + if (pack[1] != null ) { + let time = pack[1]*1000; + // standard waiting time to fire contextmenu + let standard = 750; + try { + standard = Services.prefs.getIntPref("ui.click_hold_context_menus.delay"); + } + catch (e){} + if (time >= standard && isTap) { + chain.splice(i, 0, ['longPress'], ['wait', (time-standard)/1000]); + time = standard; + } + checkTimer.initWithCallback(function(){actions(chain, touchId, command_id, i);}, time, Ci.nsITimer.TYPE_ONE_SHOT); + } + else { + actions(chain, touchId, command_id, i); + } + break; + case 'cancel': + generateEvents('cancel', lastCoordinates[0], lastCoordinates[1], touchId); + actions(chain, touchId, command_id, i); + scrolling = false; + break; + case 'longPress': + generateEvents('contextmenu', lastCoordinates[0], lastCoordinates[1], touchId); + actions(chain, touchId, command_id, i); + break; + } +} + +/** + * Function to start action chain on one finger + */ +function actionChain(msg) { + let command_id = msg.json.command_id; + let args = msg.json.chain; + let touchId = msg.json.nextId; + try { + let commandArray = elementManager.convertWrappedArguments(args, curFrame); + // loop the action array [ ['press', id], ['move', id], ['release', id] ] + if (touchId == null) { + touchId = nextTouchId++; + } + if (!curFrame.document.createTouch) { + mouseEventsOnly = true; + } + actions(commandArray, touchId, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, msg.json.command_id); + } +} + +/** + * Function to emit touch events which allow multi touch on the screen + * @param type represents the type of event, touch represents the current touch,touches are all pending touches + */ +function emitMultiEvents(type, touch, touches) { + let target = touch.target; + let doc = target.ownerDocument; + let win = doc.defaultView; + // touches that are in the same document + let documentTouches = doc.createTouchList(touches.filter(function(t) { + return ((t.target.ownerDocument === doc) && (type != 'touchcancel')); + })); + // touches on the same target + let targetTouches = doc.createTouchList(touches.filter(function(t) { + return ((t.target === target) && ((type != 'touchcancel') || (type != 'touchend'))); + })); + // Create changed touches + let changedTouches = doc.createTouchList(touch); + // Create the event object + let event = doc.createEvent('TouchEvent'); + event.initTouchEvent(type, + true, + true, + win, + 0, + false, false, false, false, + documentTouches, + targetTouches, + changedTouches); + target.dispatchEvent(event); +} + +/** + * Function to dispatch one set of actions + * @param touches represents all pending touches, batchIndex represents the batch we are dispatching right now + */ +function setDispatch(batches, touches, command_id, batchIndex) { + if (typeof batchIndex === "undefined") { + batchIndex = 0; + } + // check if all the sets have been fired + if (batchIndex >= batches.length) { + multiLast = {}; + sendOk(command_id); + return; + } + // a set of actions need to be done + let batch = batches[batchIndex]; + // each action for some finger + let pack; + // the touch id for the finger (pack) + let touchId; + // command for the finger + let command; + // touch that will be created for the finger + let el; + let corx; + let cory; + let touch; + let lastTouch; + let touchIndex; + let waitTime = 0; + let maxTime = 0; + let c; + batchIndex++; + // loop through the batch + for (let i = 0; i < batch.length; i++) { + pack = batch[i]; + touchId = pack[0]; + command = pack[1]; + switch (command) { + case 'press': + el = elementManager.getKnownElement(pack[2], curFrame); + c = coordinates(el, pack[3], pack[4]); + touch = createATouch(el, c.x, c.y, touchId); + multiLast[touchId] = touch; + touches.push(touch); + emitMultiEvents('touchstart', touch, touches); + break; + case 'release': + touch = multiLast[touchId]; + // the index of the previous touch for the finger may change in the touches array + touchIndex = touches.indexOf(touch); + touches.splice(touchIndex, 1); + emitMultiEvents('touchend', touch, touches); + break; + case 'move': + el = elementManager.getKnownElement(pack[2], curFrame); + c = coordinates(el); + touch = createATouch(multiLast[touchId].target, c.x, c.y, touchId); + touchIndex = touches.indexOf(lastTouch); + touches[touchIndex] = touch; + multiLast[touchId] = touch; + emitMultiEvents('touchmove', touch, touches); + break; + case 'moveByOffset': + el = multiLast[touchId].target; + lastTouch = multiLast[touchId]; + touchIndex = touches.indexOf(lastTouch); + let doc = el.ownerDocument; + let win = doc.defaultView; + // since x and y are relative to the last touch, therefore, it's relative to the position of the last touch + let clientX = lastTouch.clientX + pack[2], + clientY = lastTouch.clientY + pack[3]; + let pageX = clientX + win.pageXOffset, + pageY = clientY + win.pageYOffset; + let screenX = clientX + win.mozInnerScreenX, + screenY = clientY + win.mozInnerScreenY; + touch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY); + touches[touchIndex] = touch; + multiLast[touchId] = touch; + emitMultiEvents('touchmove', touch, touches); + break; + case 'wait': + if (pack[2] != undefined ) { + waitTime = pack[2]*1000; + if (waitTime > maxTime) { + maxTime = waitTime; + } + } + break; + }//end of switch block + }//end of for loop + if (maxTime != 0) { + checkTimer.initWithCallback(function(){setDispatch(batches, touches, command_id, batchIndex);}, maxTime, Ci.nsITimer.TYPE_ONE_SHOT); + } + else { + setDispatch(batches, touches, command_id, batchIndex); + } +} + +/** + * Function to start multi-action + */ +function multiAction(msg) { + let command_id = msg.json.command_id; + let args = msg.json.value; + // maxlen is the longest action chain for one finger + let maxlen = msg.json.maxlen; + try { + // unwrap the original nested array + let commandArray = elementManager.convertWrappedArguments(args, curFrame); + let concurrentEvent = []; + let temp; + for (let i = 0; i < maxlen; i++) { + let row = []; + for (let j = 0; j < commandArray.length; j++) { + if (commandArray[j][i] != undefined) { + // add finger id to the front of each action, i.e. [finger_id, action, element] + temp = commandArray[j][i]; + temp.unshift(j); + row.push(temp); + } + } + concurrentEvent.push(row); + } + // now concurrent event is made of sets where each set contain a list of actions that need to be fired. + // note: each action belongs to a different finger + // pendingTouches keeps track of current touches that's on the screen + let pendingTouches = []; + setDispatch(concurrentEvent, pendingTouches, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, msg.json.command_id); + } +} + +/** + * Navigate to the given URL. The operation will be performed on the + * current browser context, and handles the case where we navigate + * within an iframe. All other navigation is handled by the server + * (in chrome space). + */ +function get(msg) { + let command_id = msg.json.command_id; + + let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let start = new Date().getTime(); + let end = null; + function checkLoad() { + checkTimer.cancel(); + end = new Date().getTime(); + let errorRegex = /about:.+(error)|(blocked)\?/; + let elapse = end - start; + if (msg.json.pageTimeout == null || elapse <= msg.json.pageTimeout) { + if (curFrame.document.readyState == "complete") { + removeEventListener("DOMContentLoaded", onDOMContentLoaded, false); + sendOk(command_id); + } + else if (curFrame.document.readyState == "interactive" && + errorRegex.exec(curFrame.document.baseURI)) { + removeEventListener("DOMContentLoaded", onDOMContentLoaded, false); + sendError("Error loading page", 13, null, command_id); + } + else { + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); + } + } + else { + removeEventListener("DOMContentLoaded", onDOMContentLoaded, false); + sendError("Error loading page, timed out (checkLoad)", 21, null, + command_id); + } + } + // Prevent DOMContentLoaded events from frames from invoking this + // code, unless the event is coming from the frame associated with + // the current window (i.e. someone has used switch_to_frame). + let onDOMContentLoaded = function onDOMContentLoaded(event) { + if (!event.originalTarget.defaultView.frameElement || + event.originalTarget.defaultView.frameElement == curFrame.frameElement) { + checkLoad(); + } + }; + + function timerFunc() { + removeEventListener("DOMContentLoaded", onDOMContentLoaded, false); + sendError("Error loading page, timed out (onDOMContentLoaded)", 21, + null, command_id); + } + if (msg.json.pageTimeout != null) { + checkTimer.initWithCallback(timerFunc, msg.json.pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT); + } + addEventListener("DOMContentLoaded", onDOMContentLoaded, false); + curFrame.location = msg.json.url; +} + +/** + * Get URL of the top level browsing context. + */ +function getCurrentUrl(msg) { + sendResponse({value: curFrame.location.href}, msg.json.command_id); +} + +/** + * Get the current Title of the window + */ +function getTitle(msg) { + sendResponse({value: curFrame.top.document.title}, msg.json.command_id); +} + +/** + * Get the current page source + */ +function getPageSource(msg) { + var XMLSerializer = curFrame.XMLSerializer; + var pageSource = new XMLSerializer().serializeToString(curFrame.document); + sendResponse({value: pageSource}, msg.json.command_id); +} + +/** + * Go back in history + */ +function goBack(msg) { + curFrame.history.back(); + sendOk(msg.json.command_id); +} + +/** + * Go forward in history + */ +function goForward(msg) { + curFrame.history.forward(); + sendOk(msg.json.command_id); +} + +/** + * Refresh the page + */ +function refresh(msg) { + let command_id = msg.json.command_id; + curFrame.location.reload(true); + let listen = function() { + removeEventListener("DOMContentLoaded", arguments.callee, false); + sendOk(command_id); + }; + addEventListener("DOMContentLoaded", listen, false); +} + +/** + * Find an element in the document using requested search strategy + */ +function findElementContent(msg) { + let command_id = msg.json.command_id; + try { + let on_success = function(id, cmd_id) { sendResponse({value:id}, cmd_id); }; + let on_error = sendError; + elementManager.find(curFrame, msg.json, msg.json.searchTimeout, + on_success, on_error, false, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Find elements in the document using requested search strategy + */ +function findElementsContent(msg) { + let command_id = msg.json.command_id; + try { + let on_success = function(id, cmd_id) { sendResponse({value:id}, cmd_id); }; + let on_error = sendError; + elementManager.find(curFrame, msg.json, msg.json.searchTimeout, + on_success, on_error, true, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Find and return the active element on the page + */ +function getActiveElement(msg) { + let command_id = msg.json.command_id; + var element = curFrame.document.activeElement; + var id = elementManager.addToKnownElements(element); + sendResponse({value: id}, command_id); +} + +/** + * Send click event to element + */ +function clickElement(msg) { + let command_id = msg.json.command_id; + let el; + try { + el = elementManager.getKnownElement(msg.json.id, curFrame); + if (checkVisible(el)) { + if (utils.isElementEnabled(el)) { + utils.synthesizeMouseAtCenter(el, {}, el.ownerDocument.defaultView) + } + else { + sendError("Element is not Enabled", 12, null, command_id) + } + } + else { + sendError("Element is not visible", 11, null, command_id) + } + sendOk(command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Get a given attribute of an element + */ +function getElementAttribute(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + sendResponse({value: utils.getElementAttribute(el, msg.json.name)}, + command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Get the text of this element. This includes text from child elements. + */ +function getElementText(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + sendResponse({value: utils.getElementText(el)}, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Get the tag name of an element. + */ +function getElementTagName(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + sendResponse({value: el.tagName.toLowerCase()}, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Check if element is displayed + */ +function isElementDisplayed(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + sendResponse({value: utils.isElementDisplayed(el)}, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Return the property of the computed style of an element + * + * @param object aRequest + * 'element' member holds the reference id to + * the element that will be checked + * 'propertyName' is the CSS rule that is being requested + */ +function getElementValueOfCssProperty(msg){ + let command_id = msg.json.command_id; + let propertyName = msg.json.propertyName; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + sendResponse({value: curFrame.document.defaultView.getComputedStyle(el, null).getPropertyValue(propertyName)}, + command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Submit a form on a content page by either using form or element in a form + * @param object msg + * 'json' JSON object containing 'id' member of the element + */ +function submitElement (msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + while (el.parentNode != null && el.tagName.toLowerCase() != 'form') { + el = el.parentNode; + } + if (el.tagName && el.tagName.toLowerCase() == 'form') { + el.submit(); + sendOk(command_id); + } + else { + sendError("Element is not a form element or in a form", 7, null, command_id); + } + + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Get the size of the element and return it + */ +function getElementSize(msg){ + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + let clientRect = el.getBoundingClientRect(); + sendResponse({value: {width: clientRect.width, height: clientRect.height}}, + command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Check if element is enabled + */ +function isElementEnabled(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + sendResponse({value: utils.isElementEnabled(el)}, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Check if element is selected + */ +function isElementSelected(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + sendResponse({value: utils.isElementSelected(el)}, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Send keys to element + */ +function sendKeysToElement(msg) { + let command_id = msg.json.command_id; + + let el = elementManager.getKnownElement(msg.json.id, curFrame); + if (checkVisible(el)) { + if (el.mozIsTextField && el.mozIsTextField(false)) { + var currentTextLength = el.value ? el.value.length : 0; + el.selectionStart = currentTextLength; + el.selectionEnd = currentTextLength; + } + el.focus(); + var value = msg.json.value.join(""); + let hasShift = null; + let hasCtrl = null; + let hasAlt = null; + let hasMeta = null; + for (var i = 0; i < value.length; i++) { + let upper = value.charAt(i).toUpperCase(); + var keyCode = null; + var c = value.charAt(i); + switch (c) { + case '\uE001': + keyCode = "VK_CANCEL"; + break; + case '\uE002': + keyCode = "VK_HELP"; + break; + case '\uE003': + keyCode = "VK_BACK_SPACE"; + break; + case '\uE004': + keyCode = "VK_TAB"; + break; + case '\uE005': + keyCode = "VK_CLEAR"; + break; + case '\uE006': + case '\uE007': + keyCode = "VK_RETURN"; + break; + case '\uE008': + keyCode = "VK_SHIFT"; + hasShift = !hasShift; + break; + case '\uE009': + keyCode = "VK_CONTROL"; + controlKey = !controlKey; + break; + case '\uE00A': + keyCode = "VK_ALT"; + altKey = !altKey; + break; + case '\uE03D': + keyCode = "VK_META"; + metaKey = !metaKey; + break; + case '\uE00B': + keyCode = "VK_PAUSE"; + break; + case '\uE00C': + keyCode = "VK_ESCAPE"; + break; + case '\uE00D': + keyCode = "VK_Space"; // printable + break; + case '\uE00E': + keyCode = "VK_PAGE_UP"; + break; + case '\uE00F': + keyCode = "VK_PAGE_DOWN"; + break; + case '\uE010': + keyCode = "VK_END"; + break; + case '\uE011': + keyCode = "VK_HOME"; + break; + case '\uE012': + keyCode = "VK_LEFT"; + break; + case '\uE013': + keyCode = "VK_UP"; + break; + case '\uE014': + keyCode = "VK_RIGHT"; + break; + case '\uE015': + keyCode = "VK_DOWN"; + break; + case '\uE016': + keyCode = "VK_INSERT"; + break; + case '\uE017': + keyCode = "VK_DELETE"; + break; + case '\uE018': + keyCode = "VK_SEMICOLON"; + break; + case '\uE019': + keyCode = "VK_EQUALS"; + break; + case '\uE01A': + keyCode = "VK_NUMPAD0"; + break; + case '\uE01B': + keyCode = "VK_NUMPAD1"; + break; + case '\uE01C': + keyCode = "VK_NUMPAD2"; + break; + case '\uE01D': + keyCode = "VK_NUMPAD3"; + break; + case '\uE01E': + keyCode = "VK_NUMPAD4"; + break; + case '\uE01F': + keyCode = "VK_NUMPAD5"; + break; + case '\uE020': + keyCode = "VK_NUMPAD6"; + break; + case '\uE021': + keyCode = "VK_NUMPAD7"; + break; + case '\uE022': + keyCode = "VK_NUMPAD8"; + break; + case '\uE023': + keyCode = "VK_NUMPAD9"; + break; + case '\uE024': + keyCode = "VK_MULTIPLY"; + break; + case '\uE025': + keyCode = "VK_ADD"; + break; + case '\uE026': + keyCode = "VK_SEPARATOR"; + break; + case '\uE027': + keyCode = "VK_SUBTRACT"; + break; + case '\uE028': + keyCode = "VK_DECIMAL"; + break; + case '\uE029': + keyCode = "VK_DIVIDE"; + break; + case '\uE031': + keyCode = "VK_F1"; + break; + case '\uE032': + keyCode = "VK_F2"; + break; + case '\uE033': + keyCode = "VK_F3"; + break; + case '\uE034': + keyCode = "VK_F4"; + break; + case '\uE035': + keyCode = "VK_F5"; + break; + case '\uE036': + keyCode = "VK_F6"; + break; + case '\uE037': + keyCode = "VK_F7"; + break; + case '\uE038': + keyCode = "VK_F8"; + break; + case '\uE039': + keyCode = "VK_F9"; + break; + case '\uE03A': + keyCode = "VK_F10"; + break; + case '\uE03B': + keyCode = "VK_F11"; + break; + case '\uE03C': + keyCode = "VK_F12"; + break; + } + hasShift = value.charAt(i) == upper; + utils.synthesizeKey(keyCode || value[i], + { shiftKey: hasShift, ctrlKey: hasCtrl, altKey: hasAlt, metaKey: hasMeta }, + curFrame); + }; + sendOk(command_id); + } + else { + sendError("Element is not visible", 11, null, command_id) + } +} + +/** + * Get the element's top left-hand corner point. + */ +function getElementLocation(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + let rect = el.getBoundingClientRect(); + + let location = {}; + location.x = rect.left; + location.y = rect.top; + + sendResponse({value: location}, command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * Clear the text of an element + */ +function clearElement(msg) { + let command_id = msg.json.command_id; + try { + let el = elementManager.getKnownElement(msg.json.id, curFrame); + utils.clearElement(el); + sendOk(command_id); + } + catch (e) { + sendError(e.message, e.code, e.stack, command_id); + } +} + +/** + * 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 command_id = msg.json.command_id; + function checkLoad() { + let errorRegex = /about:.+(error)|(blocked)\?/; + if (curFrame.document.readyState == "complete") { + sendOk(command_id); + return; + } + else if (curFrame.document.readyState == "interactive" && errorRegex.exec(curFrame.document.baseURI)) { + sendError("Error loading page", 13, null, command_id); + return; + } + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); + } + let foundFrame = null; + let frames = []; //curFrame.document.getElementsByTagName("iframe"); + let parWindow = null; //curFrame.QueryInterface(Ci.nsIInterfaceRequestor) + // Check of the curFrame reference is dead + try { + frames = curFrame.document.getElementsByTagName("iframe"); + //Until Bug 761935 lands, we won't have multiple nested OOP iframes. We will only have one. + //parWindow will refer to the iframe above the nested OOP frame. + parWindow = curFrame.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils).outerWindowID; + } catch (e) { + // We probably have a dead compartment so accessing it is going to make Firefox + // very upset. Let's now try redirect everything to the top frame even if the + // user has given us a frame since search doesnt look up. + msg.json.id = null; + msg.json.element = null; + } + if ((msg.json.id == null) && (msg.json.element == null)) { + // returning to root frame + sendSyncMessage("Marionette:switchedToFrame", { frameValue: null }); + + curFrame = content; + if(msg.json.focus == true) { + curFrame.focus(); + } + sandbox = null; + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); + return; + } + if (msg.json.element != undefined) { + if (elementManager.seenItems[msg.json.element] != undefined) { + let wantedFrame; + try { + wantedFrame = elementManager.getKnownElement(msg.json.element, curFrame); //HTMLIFrameElement + } + catch(e) { + sendError(e.message, e.code, e.stack, command_id); + } + for (let i = 0; i < frames.length; i++) { + // use XPCNativeWrapper to compare elements; see bug 834266 + if (XPCNativeWrapper(frames[i]) == XPCNativeWrapper(wantedFrame)) { + curFrame = frames[i]; + foundFrame = i; + } + } + } + } + if (foundFrame == null) { + switch(typeof(msg.json.id)) { + case "string" : + let foundById = null; + for (let i = 0; i < frames.length; i++) { + //give precedence to name + let frame = frames[i]; + let name = utils.getElementAttribute(frame, 'name'); + let id = utils.getElementAttribute(frame, 'id'); + if (name == msg.json.id) { + foundFrame = i; + break; + } else if ((foundById == null) && (id == msg.json.id)) { + foundById = i; + } + } + if ((foundFrame == null) && (foundById != null)) { + foundFrame = foundById; + curFrame = frames[foundFrame]; + } + break; + case "number": + if (frames[msg.json.id] != undefined) { + foundFrame = msg.json.id; + curFrame = frames[foundFrame]; + } + break; + } + } + if (foundFrame == null) { + sendError("Unable to locate frame: " + msg.json.id, 8, null, command_id); + return; + } + + sandbox = null; + + // send a synchronous message to let the server update the currently active + // frame element (for getActiveFrame) + let frameValue = elementManager.wrapValue(curFrame.wrappedJSObject)['ELEMENT']; + sendSyncMessage("Marionette:switchedToFrame", { frameValue: frameValue }); + + if (curFrame.contentWindow == null) { + // The frame we want to switch to is a remote (out-of-process) frame; + // notify our parent to handle the switch. + curFrame = content; + sendToServer('Marionette:switchToFrame', {frame: foundFrame, + win: parWindow, + command_id: command_id}); + } + else { + curFrame = curFrame.contentWindow; + if(msg.json.focus == true) { + curFrame.focus(); + } + checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT); + } +} + /** + * Add a cookie to the document + */ +function addCookie(msg) { + cookie = msg.json.cookie; + + if (!cookie.expiry) { + var date = new Date(); + var thePresent = new Date(Date.now()); + date.setYear(thePresent.getFullYear() + 20); + cookie.expiry = date.getTime() / 1000; // Stored in seconds. + } + + if (!cookie.domain) { + var location = curFrame.document.location; + cookie.domain = location.hostname; + } + else { + var currLocation = curFrame.location; + var currDomain = currLocation.host; + if (currDomain.indexOf(cookie.domain) == -1) { + sendError("You may only set cookies for the current domain", 24, null, msg.json.command_id); + } + } + + // The cookie's domain may include a port. Which is bad. Remove it + // We'll catch ip6 addresses by mistake. Since no-one uses those + // this will be okay for now. See Bug 814416 + if (cookie.domain.match(/:\d+$/)) { + cookie.domain = cookie.domain.replace(/:\d+$/, ''); + } + + var document = curFrame.document; + if (!document || !document.contentType.match(/html/i)) { + sendError('You may only set cookies on html documents', 25, null, msg.json.command_id); + } + var cookieManager = Cc['@mozilla.org/cookiemanager;1']. + getService(Ci.nsICookieManager2); + cookieManager.add(cookie.domain, cookie.path, cookie.name, cookie.value, + cookie.secure, false, false, cookie.expiry); + sendOk(msg.json.command_id); +} + +/** + * Get all cookies for the current domain. + */ +function getCookies(msg) { + var toReturn = []; + var cookies = getVisibleCookies(curFrame.location); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i]; + var expires = cookie.expires; + if (expires == 0) { // Session cookie, don't return an expiry. + expires = null; + } else if (expires == 1) { // Date before epoch time, cap to epoch. + expires = 0; + } + toReturn.push({ + 'name': cookie.name, + 'value': cookie.value, + 'path': cookie.path, + 'domain': cookie.host, + 'secure': cookie.isSecure, + 'expiry': expires + }); + } + + sendResponse({value: toReturn}, msg.json.command_id); +} + +/** + * Delete a cookie by name + */ +function deleteCookie(msg) { + var toDelete = msg.json.name; + var cookieManager = Cc['@mozilla.org/cookiemanager;1']. + getService(Ci.nsICookieManager); + + var cookies = getVisibleCookies(curFrame.location); + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i]; + if (cookie.name == toDelete) { + cookieManager.remove(cookie.host, cookie.name, cookie.path, false); + } + } + + sendOk(msg.json.command_id); +} + +/** + * Delete all the visibile cookies on a page + */ +function deleteAllCookies(msg) { + let cookieManager = Cc['@mozilla.org/cookiemanager;1']. + getService(Ci.nsICookieManager); + let cookies = getVisibleCookies(curFrame.location); + for (let i = 0; i < cookies.length; i++) { + let cookie = cookies[i]; + cookieManager.remove(cookie.host, cookie.name, cookie.path, false); + } + sendOk(msg.json.command_id); +} + +/** + * Get all the visible cookies from a location + */ +function getVisibleCookies(location) { + let results = []; + let currentPath = location.pathname; + if (!currentPath) currentPath = '/'; + let isForCurrentPath = function(aPath) { + return currentPath.indexOf(aPath) != -1; + } + + let cookieManager = Cc['@mozilla.org/cookiemanager;1']. + getService(Ci.nsICookieManager); + let enumerator = cookieManager.enumerator; + while (enumerator.hasMoreElements()) { + let cookie = enumerator.getNext().QueryInterface(Ci['nsICookie']); + + // Take the hostname and progressively shorten + let hostname = location.hostname; + do { + if ((cookie.host == '.' + hostname || cookie.host == hostname) + && isForCurrentPath(cookie.path)) { + results.push(cookie); + break; + } + hostname = hostname.replace(/^.*?\./, ''); + } while (hostname.indexOf('.') != -1); + } + + return results; +} + +function getAppCacheStatus(msg) { + sendResponse({ value: curFrame.applicationCache.status }, + msg.json.command_id); +} + +// emulator callbacks +let _emu_cb_id = 0; +let _emu_cbs = {}; + +function runEmulatorCmd(cmd, callback) { + if (callback) { + _emu_cbs[_emu_cb_id] = callback; + } + sendAsyncMessage("Marionette:runEmulatorCmd", {emulator_cmd: cmd, id: _emu_cb_id}); + _emu_cb_id += 1; +} + +function runEmulatorShell(args, callback) { + if (callback) { + _emu_cbs[_emu_cb_id] = callback; + } + sendAsyncMessage("Marionette:runEmulatorShell", {emulator_shell: args, id: _emu_cb_id}); + _emu_cb_id += 1; +} + +function emulatorCmdResult(msg) { + let message = msg.json; + if (!sandbox) { + return; + } + let cb = _emu_cbs[message.id]; + delete _emu_cbs[message.id]; + if (!cb) { + return; + } + try { + cb(message.result); + } + catch(e) { + sendError(e.message, e.code, e.stack, -1); + return; + } +} + +function importScript(msg) { + let command_id = msg.json.command_id; + let file; + if (importedScripts.exists()) { + file = FileUtils.openFileOutputStream(importedScripts, + FileUtils.MODE_APPEND | FileUtils.MODE_WRONLY); + } + else { + //Note: The permission bits here don't actually get set (bug 804563) + importedScripts.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, + parseInt("0666", 8)); + file = FileUtils.openFileOutputStream(importedScripts, + FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE); + importedScripts.permissions = parseInt("0666", 8); //actually set permissions + } + file.write(msg.json.script, msg.json.script.length); + file.close(); + sendOk(command_id); +} + +/** + * Takes a screen capture of the given web element if id + * property exists in the message's JSON object, or if null captures + * the bounding box of the current frame. + * + * If given an array of web element references in + * msg.json.highlights, a red box will be painted around + * them to highlight their position. + */ +function takeScreenshot(msg) { + let node = null; + if (msg.json.id) { + try { + node = elementManager.getKnownElement(msg.json.id, curFrame) + } + catch (e) { + sendResponse(e.message, e.code, e.stack, msg.json.command_id); + return; + } + } + else { + node = curFrame; + } + let highlights = msg.json.highlights; + + var document = curFrame.document; + var rect, win, width, height, left, top; + // node can be either a window or an arbitrary DOM node + if (node == curFrame) { + // node is a window + win = node; + width = document.body.scrollWidth; + height = document.body.scrollHeight; + top = 0; + left = 0; + } + else { + // node is an arbitrary DOM node + win = node.ownerDocument.defaultView; + rect = node.getBoundingClientRect(); + width = rect.width; + height = rect.height; + top = rect.top; + left = rect.left; + } + + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", + "canvas"); + canvas.width = width; + canvas.height = height; + var ctx = canvas.getContext("2d"); + // Draws the DOM contents of the window to the canvas + ctx.drawWindow(win, left, top, width, height, "rgb(255,255,255)"); + + // This section is for drawing a red rectangle around each element + // passed in via the highlights array + if (highlights) { + ctx.lineWidth = "2"; + ctx.strokeStyle = "red"; + ctx.save(); + + for (var i = 0; i < highlights.length; ++i) { + var elem = elementManager.getKnownElement(highlights[i], curFrame); + rect = elem.getBoundingClientRect(); + + var offsetY = -top; + var offsetX = -left; + + // Draw the rectangle + ctx.strokeRect(rect.left + offsetX, + rect.top + offsetY, + rect.width, + rect.height); + } + } + + // Return the Base64 encoded string back to the client so that it + // can save the file to disk if it is required + var dataUrl = canvas.toDataURL("image/png", ""); + var data = dataUrl.substring(dataUrl.indexOf(",") + 1); + sendResponse({value: data}, msg.json.command_id); +} + +// Call register self when we get loaded +registerSelf();