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