summaryrefslogtreecommitdiffstats
path: root/testing/marionette/listener.js
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/marionette/listener.js
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/marionette/listener.js')
-rw-r--r--testing/marionette/listener.js1812
1 files changed, 1812 insertions, 0 deletions
diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js
new file mode 100644
index 000000000..b64eb378d
--- /dev/null
+++ b/testing/marionette/listener.js
@@ -0,0 +1,1812 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+var uuidGen = Cc["@mozilla.org/uuid-generator;1"]
+ .getService(Ci.nsIUUIDGenerator);
+
+var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader);
+
+Cu.import("chrome://marionette/content/accessibility.js");
+Cu.import("chrome://marionette/content/action.js");
+Cu.import("chrome://marionette/content/atom.js");
+Cu.import("chrome://marionette/content/capture.js");
+Cu.import("chrome://marionette/content/cookies.js");
+Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/evaluate.js");
+Cu.import("chrome://marionette/content/event.js");
+Cu.import("chrome://marionette/content/interaction.js");
+Cu.import("chrome://marionette/content/legacyaction.js");
+Cu.import("chrome://marionette/content/logging.js");
+Cu.import("chrome://marionette/content/navigate.js");
+Cu.import("chrome://marionette/content/proxy.js");
+Cu.import("chrome://marionette/content/session.js");
+Cu.import("chrome://marionette/content/simpletest.js");
+
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+Cu.importGlobalProperties(["URL"]);
+
+var contentLog = new logging.ContentLogger();
+
+var isB2G = false;
+
+var marionetteTestName;
+var winUtil = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+var listenerId = null; // unique ID of this listener
+var curContainer = { frame: content, shadowRoot: null };
+var isRemoteBrowser = () => curContainer.frame.contentWindow !== null;
+var previousContainer = null;
+
+var seenEls = new element.Store();
+var SUPPORTED_STRATEGIES = new Set([
+ element.Strategy.ClassName,
+ element.Strategy.Selector,
+ element.Strategy.ID,
+ element.Strategy.Name,
+ element.Strategy.LinkText,
+ element.Strategy.PartialLinkText,
+ element.Strategy.TagName,
+ element.Strategy.XPath,
+]);
+
+var capabilities;
+
+var legacyactions = new legacyaction.Chain(checkForInterrupted);
+
+// the unload handler
+var onunload;
+
+// Flag to indicate whether an async script is currently running or not.
+var asyncTestRunning = false;
+var asyncTestCommandId;
+var asyncTestTimeoutId;
+
+var inactivityTimeoutId = null;
+
+var originalOnError;
+//timer for doc changes
+var checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+//timer for readystate
+var readyStateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+// timer for navigation commands.
+var navTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+var onDOMContentLoaded;
+// Send move events about this often
+var EVENT_INTERVAL = 30; // milliseconds
+// last touch for each fingerId
+var multiLast = {};
+var asyncChrome = proxy.toChromeAsync({
+ addMessageListener: addMessageListenerId.bind(this),
+ removeMessageListener: removeMessageListenerId.bind(this),
+ sendAsyncMessage: sendAsyncMessage.bind(this),
+});
+var syncChrome = proxy.toChrome(sendSyncMessage.bind(this));
+var cookies = new Cookies(() => curContainer.frame.document, syncChrome);
+var importedScripts = new evaluate.ScriptStorageServiceClient(syncChrome);
+
+Cu.import("resource://gre/modules/Log.jsm");
+var logger = Log.repository.getLogger("Marionette");
+logger.debug("loaded listener.js");
+
+var 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) {
+ previousContainer = curContainer;
+ }
+ curContainer = {frame: content, shadowRoot: null};
+};
+
+// sandbox storage and name of the current sandbox
+var sandboxes = new Sandboxes(() => curContainer.frame);
+var sandboxName = "default";
+
+/**
+ * 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};
+ // 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]) {
+ let {id, remotenessChange} = register[0][0];
+ capabilities = session.Capabilities.fromJSON(register[0][2]);
+ listenerId = id;
+ if (typeof id != "undefined") {
+ // check if we're the main process
+ if (register[0][1]) {
+ addMessageListener("MarionetteMainListener:emitTouchEvent", emitTouchEventForIFrame);
+ }
+ startListeners();
+ let rv = {};
+ if (remotenessChange) {
+ rv.listenerId = id;
+ }
+ sendAsyncMessage("Marionette:listenersAttached", rv);
+ }
+ }
+}
+
+function emitTouchEventForIFrame(message) {
+ message = message.json;
+ let identifier = legacyactions.nextTouchId;
+
+ let domWindowUtils = curContainer.frame.
+ QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+ getInterface(Components.interfaces.nsIDOMWindowUtils);
+ var ratio = domWindowUtils.screenPixelsPerCSSPixel;
+
+ var typeForUtils;
+ switch (message.type) {
+ case 'touchstart':
+ typeForUtils = domWindowUtils.TOUCH_CONTACT;
+ break;
+ case 'touchend':
+ typeForUtils = domWindowUtils.TOUCH_REMOVE;
+ break;
+ case 'touchcancel':
+ typeForUtils = domWindowUtils.TOUCH_CANCEL;
+ break;
+ case 'touchmove':
+ typeForUtils = domWindowUtils.TOUCH_CONTACT;
+ break;
+ }
+ domWindowUtils.sendNativeTouchPoint(identifier, typeForUtils,
+ Math.round(message.screenX * ratio), Math.round(message.screenY * ratio),
+ message.force, 90);
+}
+
+// Eventually we will not have a closure for every single command, but
+// use a generic dispatch for all listener commands.
+//
+// Perhaps one could even conceive having a separate instance of
+// CommandProcessor for the listener, because the code is mostly the same.
+function dispatch(fn) {
+ if (typeof fn != "function") {
+ throw new TypeError("Provided dispatch handler is not a function");
+ }
+
+ return function (msg) {
+ let id = msg.json.command_id;
+
+ let req = Task.spawn(function*() {
+ if (typeof msg.json == "undefined" || msg.json instanceof Array) {
+ return yield fn.apply(null, msg.json);
+ } else {
+ return yield fn(msg.json);
+ }
+ });
+
+ let okOrValueResponse = rv => {
+ if (typeof rv == "undefined") {
+ sendOk(id);
+ } else {
+ sendResponse(rv, id);
+ }
+ };
+
+ req.then(okOrValueResponse, err => sendError(err, id))
+ .catch(error.report);
+ };
+}
+
+/**
+ * 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);
+}
+
+var getTitleFn = dispatch(getTitle);
+var getPageSourceFn = dispatch(getPageSource);
+var getActiveElementFn = dispatch(getActiveElement);
+var clickElementFn = dispatch(clickElement);
+var getElementAttributeFn = dispatch(getElementAttribute);
+var getElementPropertyFn = dispatch(getElementProperty);
+var getElementTextFn = dispatch(getElementText);
+var getElementTagNameFn = dispatch(getElementTagName);
+var getElementRectFn = dispatch(getElementRect);
+var isElementEnabledFn = dispatch(isElementEnabled);
+var getCurrentUrlFn = dispatch(getCurrentUrl);
+var findElementContentFn = dispatch(findElementContent);
+var findElementsContentFn = dispatch(findElementsContent);
+var isElementSelectedFn = dispatch(isElementSelected);
+var clearElementFn = dispatch(clearElement);
+var isElementDisplayedFn = dispatch(isElementDisplayed);
+var getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty);
+var switchToShadowRootFn = dispatch(switchToShadowRoot);
+var getCookiesFn = dispatch(getCookies);
+var singleTapFn = dispatch(singleTap);
+var takeScreenshotFn = dispatch(takeScreenshot);
+var performActionsFn = dispatch(performActions);
+var releaseActionsFn = dispatch(releaseActions);
+var actionChainFn = dispatch(actionChain);
+var multiActionFn = dispatch(multiAction);
+var addCookieFn = dispatch(addCookie);
+var deleteCookieFn = dispatch(deleteCookie);
+var deleteAllCookiesFn = dispatch(deleteAllCookies);
+var executeFn = dispatch(execute);
+var executeInSandboxFn = dispatch(executeInSandbox);
+var executeSimpleTestFn = dispatch(executeSimpleTest);
+var sendKeysToElementFn = dispatch(sendKeysToElement);
+
+/**
+ * Start all message listeners
+ */
+function startListeners() {
+ addMessageListenerId("Marionette:newSession", newSession);
+ addMessageListenerId("Marionette:execute", executeFn);
+ addMessageListenerId("Marionette:executeInSandbox", executeInSandboxFn);
+ addMessageListenerId("Marionette:executeSimpleTest", executeSimpleTestFn);
+ addMessageListenerId("Marionette:singleTap", singleTapFn);
+ addMessageListenerId("Marionette:performActions", performActionsFn);
+ addMessageListenerId("Marionette:releaseActions", releaseActionsFn);
+ addMessageListenerId("Marionette:actionChain", actionChainFn);
+ addMessageListenerId("Marionette:multiAction", multiActionFn);
+ addMessageListenerId("Marionette:get", get);
+ addMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
+ addMessageListenerId("Marionette:cancelRequest", cancelRequest);
+ addMessageListenerId("Marionette:getCurrentUrl", getCurrentUrlFn);
+ addMessageListenerId("Marionette:getTitle", getTitleFn);
+ addMessageListenerId("Marionette:getPageSource", getPageSourceFn);
+ addMessageListenerId("Marionette:goBack", goBack);
+ addMessageListenerId("Marionette:goForward", goForward);
+ addMessageListenerId("Marionette:refresh", refresh);
+ addMessageListenerId("Marionette:findElementContent", findElementContentFn);
+ addMessageListenerId("Marionette:findElementsContent", findElementsContentFn);
+ addMessageListenerId("Marionette:getActiveElement", getActiveElementFn);
+ addMessageListenerId("Marionette:clickElement", clickElementFn);
+ addMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn);
+ addMessageListenerId("Marionette:getElementProperty", getElementPropertyFn);
+ addMessageListenerId("Marionette:getElementText", getElementTextFn);
+ addMessageListenerId("Marionette:getElementTagName", getElementTagNameFn);
+ addMessageListenerId("Marionette:isElementDisplayed", isElementDisplayedFn);
+ addMessageListenerId("Marionette:getElementValueOfCssProperty", getElementValueOfCssPropertyFn);
+ addMessageListenerId("Marionette:getElementRect", getElementRectFn);
+ addMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn);
+ addMessageListenerId("Marionette:isElementSelected", isElementSelectedFn);
+ addMessageListenerId("Marionette:sendKeysToElement", sendKeysToElementFn);
+ addMessageListenerId("Marionette:clearElement", clearElementFn);
+ addMessageListenerId("Marionette:switchToFrame", switchToFrame);
+ addMessageListenerId("Marionette:switchToParentFrame", switchToParentFrame);
+ addMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
+ addMessageListenerId("Marionette:deleteSession", deleteSession);
+ addMessageListenerId("Marionette:sleepSession", sleepSession);
+ addMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
+ addMessageListenerId("Marionette:setTestName", setTestName);
+ addMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
+ addMessageListenerId("Marionette:addCookie", addCookieFn);
+ addMessageListenerId("Marionette:getCookies", getCookiesFn);
+ addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
+ addMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
+}
+
+/**
+ * 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) {
+ capabilities = session.Capabilities.fromJSON(msg.json);
+ isB2G = capabilities.get("platformName") === "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.
+ legacyactions.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:execute", executeFn);
+ removeMessageListenerId("Marionette:executeInSandbox", executeInSandboxFn);
+ removeMessageListenerId("Marionette:executeSimpleTest", executeSimpleTestFn);
+ removeMessageListenerId("Marionette:singleTap", singleTapFn);
+ removeMessageListenerId("Marionette:performActions", performActionsFn);
+ removeMessageListenerId("Marionette:releaseActions", releaseActionsFn);
+ removeMessageListenerId("Marionette:actionChain", actionChainFn);
+ removeMessageListenerId("Marionette:multiAction", multiActionFn);
+ removeMessageListenerId("Marionette:get", get);
+ removeMessageListenerId("Marionette:pollForReadyState", pollForReadyState);
+ removeMessageListenerId("Marionette:cancelRequest", cancelRequest);
+ removeMessageListenerId("Marionette:getTitle", getTitleFn);
+ removeMessageListenerId("Marionette:getPageSource", getPageSourceFn);
+ removeMessageListenerId("Marionette:getCurrentUrl", getCurrentUrlFn);
+ removeMessageListenerId("Marionette:goBack", goBack);
+ removeMessageListenerId("Marionette:goForward", goForward);
+ removeMessageListenerId("Marionette:refresh", refresh);
+ removeMessageListenerId("Marionette:findElementContent", findElementContentFn);
+ removeMessageListenerId("Marionette:findElementsContent", findElementsContentFn);
+ removeMessageListenerId("Marionette:getActiveElement", getActiveElementFn);
+ removeMessageListenerId("Marionette:clickElement", clickElementFn);
+ removeMessageListenerId("Marionette:getElementAttribute", getElementAttributeFn);
+ removeMessageListenerId("Marionette:getElementProperty", getElementPropertyFn);
+ removeMessageListenerId("Marionette:getElementText", getElementTextFn);
+ removeMessageListenerId("Marionette:getElementTagName", getElementTagNameFn);
+ removeMessageListenerId("Marionette:isElementDisplayed", isElementDisplayedFn);
+ removeMessageListenerId("Marionette:getElementValueOfCssProperty", getElementValueOfCssPropertyFn);
+ removeMessageListenerId("Marionette:getElementRect", getElementRectFn);
+ removeMessageListenerId("Marionette:isElementEnabled", isElementEnabledFn);
+ removeMessageListenerId("Marionette:isElementSelected", isElementSelectedFn);
+ removeMessageListenerId("Marionette:sendKeysToElement", sendKeysToElementFn);
+ removeMessageListenerId("Marionette:clearElement", clearElementFn);
+ removeMessageListenerId("Marionette:switchToFrame", switchToFrame);
+ removeMessageListenerId("Marionette:switchToParentFrame", switchToParentFrame);
+ removeMessageListenerId("Marionette:switchToShadowRoot", switchToShadowRootFn);
+ removeMessageListenerId("Marionette:deleteSession", deleteSession);
+ removeMessageListenerId("Marionette:sleepSession", sleepSession);
+ removeMessageListenerId("Marionette:getAppCacheStatus", getAppCacheStatus);
+ removeMessageListenerId("Marionette:setTestName", setTestName);
+ removeMessageListenerId("Marionette:takeScreenshot", takeScreenshotFn);
+ removeMessageListenerId("Marionette:addCookie", addCookieFn);
+ removeMessageListenerId("Marionette:getCookies", getCookiesFn);
+ removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookiesFn);
+ removeMessageListenerId("Marionette:deleteCookie", deleteCookieFn);
+ if (isB2G) {
+ content.removeEventListener("mozbrowsershowmodalprompt", modalHandler, false);
+ }
+ seenEls.clear();
+ // reset container frame to the top-most frame
+ curContainer = { frame: content, shadowRoot: null };
+ curContainer.frame.focus();
+ legacyactions.touchIds = {};
+ if (action.inputStateMap !== undefined) {
+ action.inputStateMap.clear();
+ }
+ if (action.inputsToCancel !== undefined) {
+ action.inputsToCancel.length = 0;
+ }
+}
+
+/**
+ * Send asynchronous reply to chrome.
+ *
+ * @param {UUID} uuid
+ * Unique identifier of the request.
+ * @param {AsyncContentSender.ResponseType} type
+ * Type of response.
+ * @param {?=} data
+ * JSON serialisable object to accompany the message. Defaults to
+ * an empty dictionary.
+ */
+function sendToServer(uuid, data = undefined) {
+ let channel = new proxy.AsyncMessageChannel(
+ () => this,
+ sendAsyncMessage.bind(this));
+ channel.reply(uuid, data);
+}
+
+/**
+ * Send asynchronous reply with value to chrome.
+ *
+ * @param {?} obj
+ * JSON serialisable object of arbitrary type and complexity.
+ * @param {UUID} uuid
+ * Unique identifier of the request.
+ */
+function sendResponse(obj, id) {
+ sendToServer(id, obj);
+}
+
+/**
+ * Send asynchronous reply to chrome.
+ *
+ * @param {UUID} uuid
+ * Unique identifier of the request.
+ */
+function sendOk(uuid) {
+ sendToServer(uuid);
+}
+
+/**
+ * Send asynchronous error reply to chrome.
+ *
+ * @param {Error} err
+ * Error to notify chrome of.
+ * @param {UUID} uuid
+ * Unique identifier of the request.
+ */
+function sendError(err, uuid) {
+ sendToServer(uuid, err);
+}
+
+/**
+ * Send log message to server
+ */
+function sendLog(msg) {
+ sendToServer("Marionette:log", {message: msg});
+}
+
+/**
+ * Clear test values after completion of test
+ */
+function resetValues() {
+ sandboxes.clear();
+ curContainer = {frame: content, shadowRoot: null};
+ legacyactions.mouseEventsOnly = false;
+ action.inputStateMap = new Map();
+ action.inputsToCancel = [];
+}
+
+/**
+ * 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 (previousContainer) {
+ 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;
+}
+
+function checkForInterrupted() {
+ if (wasInterrupted()) {
+ if (previousContainer) {
+ // if previousContainer is set, then we're in a single process environment
+ curContainer = legacyactions.container = previousContainer;
+ previousContainer = 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* execute(script, args, timeout, opts) {
+ opts.timeout = timeout;
+ script = importedScripts.for("content").concat(script);
+
+ let sb = sandbox.createMutable(curContainer.frame);
+ let wargs = element.fromJson(
+ args, seenEls, curContainer.frame, curContainer.shadowRoot);
+ let res = yield evaluate.sandbox(sb, script, wargs, opts);
+
+ return element.toJson(res, seenEls);
+}
+
+function* executeInSandbox(script, args, timeout, opts) {
+ opts.timeout = timeout;
+ script = importedScripts.for("content").concat(script);
+
+ let sb = sandboxes.get(opts.sandboxName, opts.newSandbox);
+ if (opts.sandboxName) {
+ sb = sandbox.augment(sb, {global: sb});
+ sb = sandbox.augment(sb, new logging.Adapter(contentLog));
+ }
+
+ let wargs = element.fromJson(
+ args, seenEls, curContainer.frame, curContainer.shadowRoot);
+ let evaluatePromise = evaluate.sandbox(sb, script, wargs, opts);
+
+ let res = yield evaluatePromise;
+ sendSyncMessage(
+ "Marionette:shareData",
+ {log: element.toJson(contentLog.get(), seenEls)});
+ return element.toJson(res, seenEls);
+}
+
+function* executeSimpleTest(script, args, timeout, opts) {
+ opts.timeout = timeout;
+ let win = curContainer.frame;
+ script = importedScripts.for("content").concat(script);
+
+ let harness = new simpletest.Harness(
+ win,
+ "content",
+ contentLog,
+ timeout,
+ marionetteTestName);
+ let sb = sandbox.createSimpleTest(curContainer.frame, harness);
+ // TODO(ato): Not sure this is needed:
+ sb = sandbox.augment(sb, new logging.Adapter(contentLog));
+
+ let wargs = element.fromJson(
+ args, seenEls, curContainer.frame, curContainer.shadowRoot);
+ let evaluatePromise = evaluate.sandbox(sb, script, wargs, opts);
+
+ let res = yield evaluatePromise;
+ sendSyncMessage(
+ "Marionette:shareData",
+ {log: element.toJson(contentLog.get(), seenEls)});
+ return element.toJson(res, seenEls);
+}
+
+/**
+ * Sets the test name, used in logging messages.
+ */
+function setTestName(msg) {
+ marionetteTestName = msg.json.value;
+ sendOk(msg.json.command_id);
+}
+
+/**
+ * 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 = curContainer.frame.document.defaultView.
+ QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+ getInterface(Components.interfaces.nsIWebNavigation).
+ QueryInterface(Components.interfaces.nsIDocShell);
+ if (docShell.asyncPanZoomEnabled && legacyactions.scrolling) {
+ // if we're in APZ and we're scrolling, we must use sendNativeTouchPoint 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,
+ screenX: touch.screenX, screenY: touch.screenY,
+ 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
+ contentLog.log(loggingInfo, "TRACE");
+ sendSyncMessage(
+ "Marionette:shareData",
+ {log: element.toJson(contentLog.get(), seenEls)});
+ contentLog.clear();
+ */
+ let domWindowUtils = curContainer.frame.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);
+ }
+}
+
+/**
+ * Function that perform a single tap
+ */
+function singleTap(id, corx, cory) {
+ let el = seenEls.get(id, curContainer);
+ // after this block, the element will be scrolled into view
+ let visible = element.isVisible(el, corx, cory);
+ if (!visible) {
+ throw new ElementNotInteractableError("Element is not currently visible and may not be manipulated");
+ }
+
+ let a11y = accessibility.get(capabilities.get("moz:accessibilityChecks"));
+ return a11y.getAccessible(el, true).then(acc => {
+ a11y.assertVisible(acc, el, visible);
+ a11y.assertActionable(acc, el);
+ if (!curContainer.frame.document.createTouch) {
+ legacyactions.mouseEventsOnly = true;
+ }
+ let c = element.coordinates(el, corx, cory);
+ if (!legacyactions.mouseEventsOnly) {
+ let touchId = legacyactions.nextTouchId++;
+ let touch = createATouch(el, c.x, c.y, touchId);
+ emitTouchEvent('touchstart', touch);
+ emitTouchEvent('touchend', touch);
+ }
+ legacyactions.mouseTap(el.ownerDocument, c.x, c.y);
+ });
+}
+
+/**
+ * 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, clientY, pageX, pageY, screenX, screenY] =
+ legacyactions.getCoordinateInfo(el, corx, cory);
+ let atouch = doc.createTouch(win, el, touchId, pageX, pageY, screenX, screenY, clientX, clientY);
+ return atouch;
+}
+
+/**
+ * Perform a series of grouped actions at the specified points in time.
+ *
+ * @param {obj} msg
+ * Object with an |actions| attribute that is an Array of objects
+ * each of which represents an action sequence.
+ */
+function* performActions(msg) {
+ let chain = action.Chain.fromJson(msg.actions);
+ yield action.dispatch(chain, seenEls, curContainer);
+}
+
+/**
+ * The Release Actions command is used to release all the keys and pointer
+ * buttons that are currently depressed. This causes events to be fired as if
+ * the state was released by an explicit series of actions. It also clears all
+ * the internal state of the virtual devices.
+ */
+function* releaseActions() {
+ yield action.dispatchTickActions(action.inputsToCancel.reverse(), 0, seenEls, curContainer);
+ action.inputsToCancel.length = 0;
+ action.inputStateMap.clear();
+}
+
+/**
+ * Start action chain on one finger.
+ */
+function actionChain(chain, touchId) {
+ let touchProvider = {};
+ touchProvider.createATouch = createATouch;
+ touchProvider.emitTouchEvent = emitTouchEvent;
+
+ return legacyactions.dispatchActions(
+ chain,
+ touchId,
+ curContainer,
+ seenEls,
+ touchProvider);
+}
+
+/**
+ * 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, batchIndex=0) {
+ // check if all the sets have been fired
+ if (batchIndex >= batches.length) {
+ multiLast = {};
+ 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;
+
+ // loop through the batch
+ batchIndex++;
+ for (let i = 0; i < batch.length; i++) {
+ pack = batch[i];
+ touchId = pack[0];
+ command = pack[1];
+
+ switch (command) {
+ case "press":
+ el = seenEls.get(pack[2], curContainer);
+ c = element.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 = seenEls.get(pack[2], curContainer);
+ c = element.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 (typeof pack[2] != "undefined") {
+ waitTime = pack[2] * 1000;
+ if (waitTime > maxTime) {
+ maxTime = waitTime;
+ }
+ }
+ break;
+ }
+ }
+
+ if (maxTime != 0) {
+ checkTimer.initWithCallback(function() {
+ setDispatch(batches, touches, batchIndex);
+ }, maxTime, Ci.nsITimer.TYPE_ONE_SHOT);
+ } else {
+ setDispatch(batches, touches, batchIndex);
+ }
+}
+
+/**
+ * Start multi-action.
+ *
+ * @param {Number} maxLen
+ * Longest action chain for one finger.
+ */
+function multiAction(args, maxLen) {
+ // unwrap the original nested array
+ let commandArray = element.fromJson(
+ args, seenEls, curContainer.frame, curContainer.shadowRoot);
+ let concurrentEvent = [];
+ let temp;
+ for (let i = 0; i < maxLen; i++) {
+ let row = [];
+ for (let j = 0; j < commandArray.length; j++) {
+ if (typeof 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);
+}
+
+/**
+ * This implements the latter part of a get request (for the case we need to resume one
+ * when a remoteness update happens in the middle of a navigate request). This is most of
+ * of the work of a navigate request, but doesn't assume DOMContentLoaded is yet to fire.
+ *
+ * @param {function=} cleanupCallback
+ * Callback to execute when registered event handlers or observer notifications
+ * have to be cleaned-up.
+ * @param {number} command_id
+ * ID of the currently handled message between the driver and listener.
+ * @param {string=} lastSeenURL
+ * Last URL as seen before the navigation request got triggered.
+ * @param {number} pageTimeout
+ * Timeout in seconds the method has to wait for the page being finished loading.
+ * @param {number} startTime
+ * Unix timestap when the navitation request got triggred.
+ */
+function pollForReadyState(msg) {
+ let {cleanupCallback, command_id, lastSeenURL, pageTimeout, startTime} = msg.json;
+
+ if (typeof startTime == "undefined") {
+ startTime = new Date().getTime();
+ }
+
+ if (typeof cleanupCallback == "undefined") {
+ cleanupCallback = () => {};
+ }
+
+ let endTime = startTime + pageTimeout;
+
+ let checkLoad = function() {
+ navTimer.cancel();
+
+ let doc = curContainer.frame.document;
+
+ if (pageTimeout === null || new Date().getTime() <= endTime) {
+ // Under some conditions (eg. for error pages) the pagehide event is fired
+ // even with a readyState complete for the formerly loaded page.
+ // To prevent race conditition for goBack and goForward we have to wait
+ // until the last seen page has been fully unloaded.
+ // TODO: Bug 1333458 has to improve this.
+ if (!doc.location || lastSeenURL && doc.location.href === lastSeenURL) {
+ navTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ // document fully loaded
+ } else if (doc.readyState === "complete") {
+ cleanupCallback();
+ sendOk(command_id);
+
+ // document with an insecure cert
+ } else if (doc.readyState === "interactive" &&
+ doc.baseURI.startsWith("about:certerror")) {
+ cleanupCallback();
+ sendError(new InsecureCertificateError(), command_id);
+
+ // we have reached an error url without requesting it
+ } else if (doc.readyState === "interactive" &&
+ /about:.+(error)\?/.exec(doc.baseURI)) {
+ cleanupCallback();
+ sendError(new UnknownError("Reached error page: " + doc.baseURI), command_id);
+
+ // return early for about: urls
+ } else if (doc.readyState === "interactive" && doc.baseURI.startsWith("about:")) {
+ cleanupCallback();
+ sendOk(command_id);
+
+ // document not fully loaded
+ } else {
+ navTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+
+ } else {
+ cleanupCallback();
+ sendError(new TimeoutError("Error loading page, timed out (checkLoad)"), command_id);
+ }
+ };
+
+ checkLoad();
+}
+
+/**
+ * Navigate to the given URL. The operation will be performed on the
+ * current browsing context, which means it handles the case where we
+ * navigate within an iframe. All other navigation is handled by the
+ * driver (in chrome space).
+ */
+function get(msg) {
+ let {pageTimeout, url, command_id} = msg.json;
+
+ let startTime = new Date().getTime();
+
+ // We need to move to the top frame before navigating
+ sendSyncMessage("Marionette:switchedToFrame", {frameValue: null});
+ curContainer.frame = content;
+
+ let docShell = curContainer.frame
+ .document
+ .defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebProgress);
+ let sawLoad = false;
+
+ let requestedURL;
+ let loadEventExpected = false;
+ try {
+ requestedURL = new URL(url).toString();
+ let curURL = curContainer.frame.location;
+ loadEventExpected = navigate.isLoadEventExpected(curURL, requestedURL);
+ } catch (e) {
+ sendError(new InvalidArgumentError("Malformed URL: " + e.message), command_id);
+ return;
+ }
+
+ // It's possible that a site we're being sent to will end up redirecting
+ // us before we end up on a page that fires DOMContentLoaded. We can ensure
+ // This loadListener ensures that we don't send a success signal back to
+ // the caller until we've seen the load of the requested URL attempted
+ // on this frame.
+ let loadListener = {
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]),
+
+ onStateChange(webProgress, request, state, status) {
+ if (!(request instanceof Ci.nsIChannel)) {
+ return;
+ }
+
+ const isDocument = state & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
+ const loadedURL = request.URI.spec;
+
+ // We have to look at the originalURL because of about: pages,
+ // the loadedURL is what the about: page resolves to, and is
+ // not the one that was requested.
+ const originalURL = request.originalURI.spec;
+ const isRequestedURL = loadedURL == requestedURL ||
+ originalURL == requestedURL;
+
+ if (!isDocument || !isRequestedURL) {
+ return;
+ }
+
+ // We started loading the requested document. This document
+ // might not be the one that ends up firing DOMContentLoaded
+ // (if it, for example, redirects), but because we've started
+ // loading this URL, we know that any future DOMContentLoaded's
+ // are fair game to tell the Marionette client about.
+ if (state & Ci.nsIWebProgressListener.STATE_START) {
+ sawLoad = true;
+ }
+
+ // This indicates network stop or last request stop outside of
+ // loading the document. We hit this when DOMContentLoaded is
+ // not triggered, which is the case for image documents.
+ else if (state & Ci.nsIWebProgressListener.STATE_STOP &&
+ content.document instanceof content.ImageDocument) {
+ pollForReadyState({json: {
+ command_id: command_id,
+ pageTimeout: pageTimeout,
+ startTime: startTime,
+ cleanupCallback: () => {
+ webProgress.removeProgressListener(loadListener);
+ removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+ }
+ }});
+ }
+ },
+
+ onLocationChange() {},
+ onProgressChange() {},
+ onStatusChange() {},
+ onSecurityChange() {},
+ };
+
+ webProgress.addProgressListener(
+ loadListener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+
+ // 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).
+ onDOMContentLoaded = function onDOMContentLoaded(event) {
+ let frameEl = event.originalTarget.defaultView.frameElement;
+ let correctFrame = !frameEl || frameEl == curContainer.frame.frameElement;
+
+ // If the page we're at fired DOMContentLoaded and appears
+ // to be the one we asked to load, then we definitely
+ // saw the load occur. We need this because for error
+ // pages, like about:neterror for unsupported protocols,
+ // we don't end up opening a channel that our
+ // WebProgressListener can monitor.
+ if (curContainer.frame.location == requestedURL) {
+ sawLoad = true;
+ }
+
+ // We also need to make sure that if the requested URL is not about:blank
+ // the DOMContentLoaded we saw isn't for the initial about:blank of a newly
+ // created docShell.
+ let loadedRequestedURI = (requestedURL == "about:blank") ||
+ docShell.hasLoadedNonBlankURI;
+
+ if (correctFrame && sawLoad && loadedRequestedURI) {
+ pollForReadyState({json: {
+ command_id: command_id,
+ pageTimeout: pageTimeout,
+ startTime: startTime,
+ cleanupCallback: () => {
+ webProgress.removeProgressListener(loadListener);
+ removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+ }
+ }});
+ }
+ };
+
+ if (typeof pageTimeout != "undefined") {
+ let onTimeout = function() {
+ if (loadEventExpected) {
+ removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+ }
+ webProgress.removeProgressListener(loadListener);
+ sendError(new TimeoutError("Error loading page, timed out (onDOMContentLoaded)"), command_id);
+ }
+ navTimer.initWithCallback(onTimeout, pageTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+
+ if (loadEventExpected) {
+ addEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+ }
+ curContainer.frame.location = requestedURL;
+ if (!loadEventExpected) {
+ sendOk(command_id);
+ }
+}
+
+/**
+ * Cancel the polling and remove the event listener associated with a
+ * current navigation request in case we're interupted by an onbeforeunload
+ * handler and navigation doesn't complete.
+ */
+function cancelRequest() {
+ navTimer.cancel();
+ if (onDOMContentLoaded) {
+ removeEventListener("DOMContentLoaded", onDOMContentLoaded, false);
+ }
+}
+
+/**
+ * Get URL of the top-level browsing context.
+ */
+function getCurrentUrl() {
+ return content.location.href;
+}
+
+/**
+ * Get the title of the current browsing context.
+ */
+function getTitle() {
+ return curContainer.frame.top.document.title;
+}
+
+/**
+ * Get source of the current browsing context's DOM.
+ */
+function getPageSource() {
+ return curContainer.frame.document.documentElement.outerHTML;
+}
+
+/**
+ * Wait for the current page to be unloaded after a navigation got triggered.
+ *
+ * @param {function} trigger
+ * Callback to execute which triggers a page navigation.
+ * @param {function} doneCallback
+ * Callback to execute when the current page has been unloaded.
+ *
+ * It receives a dictionary with the following items as argument:
+ * loading - Flag if a page load will follow.
+ * lastSeenURL - Last seen URL before the navigation request.
+ * startTime - Time when the navigation request has been triggered.
+ */
+function waitForPageUnloaded(trigger, doneCallback) {
+ let currentURL = curContainer.frame.location.href;
+ let start = new Date().getTime();
+
+ function handleEvent(event) {
+ // In case of a remoteness change it can happen that we are no longer able
+ // to access the document's location. In those cases ignore the event,
+ // but keep the code waiting, and assume in the driver that waiting for the
+ // page load is necessary. Bug 1333458 should improve things.
+ if (typeof event.originalTarget.location == "undefined") {
+ return;
+ }
+
+ switch (event.type) {
+ case "hashchange":
+ removeEventListener("hashchange", handleEvent);
+ removeEventListener("pagehide", handleEvent);
+ removeEventListener("unload", handleEvent);
+
+ doneCallback({loading: false, lastSeenURL: currentURL});
+ break;
+
+ case "pagehide":
+ case "unload":
+ if (event.originalTarget === curContainer.frame.document) {
+ removeEventListener("hashchange", handleEvent);
+ removeEventListener("pagehide", handleEvent);
+ removeEventListener("unload", handleEvent);
+
+ doneCallback({loading: true, lastSeenURL: currentURL, startTime: start});
+ }
+ break;
+ }
+ }
+
+ addEventListener("hashchange", handleEvent, false);
+ addEventListener("pagehide", handleEvent, false);
+ addEventListener("unload", handleEvent, false);
+
+ trigger();
+}
+
+/**
+ * Cause the browser to traverse one step backward in the joint history
+ * of the current browsing context.
+ *
+ * @param {number} command_id
+ * ID of the currently handled message between the driver and listener.
+ * @param {number} pageTimeout
+ * Timeout in milliseconds the method has to wait for the page being finished loading.
+ */
+function goBack(msg) {
+ let {command_id, pageTimeout} = msg.json;
+
+ waitForPageUnloaded(() => {
+ curContainer.frame.history.back();
+ }, pageLoadStatus => {
+ if (pageLoadStatus.loading) {
+ pollForReadyState({json: {
+ command_id: command_id,
+ lastSeenURL: pageLoadStatus.lastSeenURL,
+ pageTimeout: pageTimeout,
+ startTime: pageLoadStatus.startTime,
+ }});
+ } else {
+ sendOk(command_id);
+ }
+ });
+}
+
+/**
+ * Cause the browser to traverse one step forward in the joint history
+ * of the current browsing context.
+ *
+ * @param {number} command_id
+ * ID of the currently handled message between the driver and listener.
+ * @param {number} pageTimeout
+ * Timeout in milliseconds the method has to wait for the page being finished loading.
+ */
+function goForward(msg) {
+ let {command_id, pageTimeout} = msg.json;
+
+ waitForPageUnloaded(() => {
+ curContainer.frame.history.forward();
+ }, pageLoadStatus => {
+ if (pageLoadStatus.loading) {
+ pollForReadyState({json: {
+ command_id: command_id,
+ lastSeenURL: pageLoadStatus.lastSeenURL,
+ pageTimeout: pageTimeout,
+ startTime: pageLoadStatus.startTime,
+ }});
+ } else {
+ sendOk(command_id);
+ }
+ });
+}
+
+/**
+ * Refresh the page
+ */
+function refresh(msg) {
+ let command_id = msg.json.command_id;
+ curContainer.frame.location.reload(true);
+ let listen = function() {
+ removeEventListener("DOMContentLoaded", listen, false);
+ sendOk(command_id);
+ };
+ addEventListener("DOMContentLoaded", listen, false);
+}
+
+/**
+ * Find an element in the current browsing context's document using the
+ * given search strategy.
+ */
+function* findElementContent(strategy, selector, opts = {}) {
+ if (!SUPPORTED_STRATEGIES.has(strategy)) {
+ throw new InvalidSelectorError("Strategy not supported: " + strategy);
+ }
+
+ opts.all = false;
+ if (opts.startNode) {
+ opts.startNode = seenEls.get(opts.startNode, curContainer);
+ }
+
+ let el = yield element.find(curContainer, strategy, selector, opts);
+ let elRef = seenEls.add(el);
+ let webEl = element.makeWebElement(elRef);
+ return webEl;
+}
+
+/**
+ * Find elements in the current browsing context's document using the
+ * given search strategy.
+ */
+function* findElementsContent(strategy, selector, opts = {}) {
+ if (!SUPPORTED_STRATEGIES.has(strategy)) {
+ throw new InvalidSelectorError("Strategy not supported: " + strategy);
+ }
+
+ opts.all = true;
+ if (opts.startNode) {
+ opts.startNode = seenEls.get(opts.startNode, curContainer);
+ }
+
+ let els = yield element.find(curContainer, strategy, selector, opts);
+ let elRefs = seenEls.addAll(els);
+ let webEls = elRefs.map(element.makeWebElement);
+ return webEls;
+}
+
+/** Find and return the active element on the page. */
+function getActiveElement() {
+ let el = curContainer.frame.document.activeElement;
+ return element.toJson(el, seenEls);
+}
+
+/**
+ * Send click event to element.
+ *
+ * @param {WebElement} id
+ * Reference to the web element to click.
+ */
+function clickElement(id) {
+ let el = seenEls.get(id, curContainer);
+ return interaction.clickElement(
+ el,
+ capabilities.get("moz:accessibilityChecks"),
+ capabilities.get("specificationLevel") >= 1);
+}
+
+function getElementAttribute(id, name) {
+ let el = seenEls.get(id, curContainer);
+ if (element.isBooleanAttribute(el, name)) {
+ if (el.hasAttribute(name)) {
+ return "true";
+ } else {
+ return null;
+ }
+ } else {
+ return el.getAttribute(name);
+ }
+}
+
+function getElementProperty(id, name) {
+ let el = seenEls.get(id, curContainer);
+ return typeof el[name] != "undefined" ? el[name] : null;
+}
+
+/**
+ * Get the text of this element. This includes text from child elements.
+ *
+ * @param {WebElement} id
+ * Reference to web element.
+ *
+ * @return {string}
+ * Text of element.
+ */
+function getElementText(id) {
+ let el = seenEls.get(id, curContainer);
+ return atom.getElementText(el, curContainer.frame);
+}
+
+/**
+ * Get the tag name of an element.
+ *
+ * @param {WebElement} id
+ * Reference to web element.
+ *
+ * @return {string}
+ * Tag name of element.
+ */
+function getElementTagName(id) {
+ let el = seenEls.get(id, curContainer);
+ return el.tagName.toLowerCase();
+}
+
+/**
+ * Determine the element displayedness of the given web element.
+ *
+ * Also performs additional accessibility checks if enabled by session
+ * capability.
+ */
+function isElementDisplayed(id) {
+ let el = seenEls.get(id, curContainer);
+ return interaction.isElementDisplayed(
+ el, capabilities.get("moz:accessibilityChecks"));
+}
+
+/**
+ * Retrieves the computed value of the given CSS property of the given
+ * web element.
+ *
+ * @param {String} id
+ * Web element reference.
+ * @param {String} prop
+ * The CSS property to get.
+ *
+ * @return {String}
+ * Effective value of the requested CSS property.
+ */
+function getElementValueOfCssProperty(id, prop) {
+ let el = seenEls.get(id, curContainer);
+ let st = curContainer.frame.document.defaultView.getComputedStyle(el, null);
+ return st.getPropertyValue(prop);
+}
+
+/**
+ * Get the position and dimensions of the element.
+ *
+ * @param {WebElement} id
+ * Reference to web element.
+ *
+ * @return {Object.<string, number>}
+ * The x, y, width, and height properties of the element.
+ */
+function getElementRect(id) {
+ let el = seenEls.get(id, curContainer);
+ let clientRect = el.getBoundingClientRect();
+ return {
+ x: clientRect.x + curContainer.frame.pageXOffset,
+ y: clientRect.y + curContainer.frame.pageYOffset,
+ width: clientRect.width,
+ height: clientRect.height
+ };
+}
+
+/**
+ * Check if element is enabled.
+ *
+ * @param {WebElement} id
+ * Reference to web element.
+ *
+ * @return {boolean}
+ * True if enabled, false otherwise.
+ */
+function isElementEnabled(id) {
+ let el = seenEls.get(id, curContainer);
+ return interaction.isElementEnabled(
+ el, capabilities.get("moz:accessibilityChecks"));
+}
+
+/**
+ * Determines if the referenced element is selected or not.
+ *
+ * This operation only makes sense on input elements of the Checkbox-
+ * and Radio Button states, or option elements.
+ */
+function isElementSelected(id) {
+ let el = seenEls.get(id, curContainer);
+ return interaction.isElementSelected(
+ el, capabilities.get("moz:accessibilityChecks"));
+}
+
+function* sendKeysToElement(id, val) {
+ let el = seenEls.get(id, curContainer);
+ if (el.type == "file") {
+ let path = val.join("");
+ yield interaction.uploadFile(el, path);
+ } else {
+ yield interaction.sendKeysToElement(
+ el, val, false, capabilities.get("moz:accessibilityChecks"));
+ }
+}
+
+/**
+ * Clear the text of an element.
+ */
+function clearElement(id) {
+ try {
+ let el = seenEls.get(id, curContainer);
+ if (el.type == "file") {
+ el.value = null;
+ } else {
+ atom.clearElement(el, curContainer.frame);
+ }
+ } catch (e) {
+ // Bug 964738: Newer atoms contain status codes which makes wrapping
+ // this in an error prototype that has a status property unnecessary
+ if (e.name == "InvalidElementStateError") {
+ throw new InvalidElementStateError(e.message);
+ } else {
+ throw e;
+ }
+ }
+}
+
+/**
+ * Switch the current context to the specified host's Shadow DOM.
+ * @param {WebElement} id
+ * Reference to web element.
+ */
+function switchToShadowRoot(id) {
+ if (!id) {
+ // If no host element is passed, attempt to find a parent shadow root or, if
+ // none found, unset the current shadow root
+ if (curContainer.shadowRoot) {
+ let parent;
+ try {
+ parent = curContainer.shadowRoot.host;
+ } catch (e) {
+ // There is a chance that host element is dead and we are trying to
+ // access a dead object.
+ curContainer.shadowRoot = null;
+ return;
+ }
+ while (parent && !(parent instanceof curContainer.frame.ShadowRoot)) {
+ parent = parent.parentNode;
+ }
+ curContainer.shadowRoot = parent;
+ }
+ return;
+ }
+
+ let foundShadowRoot;
+ let hostEl = seenEls.get(id, curContainer);
+ foundShadowRoot = hostEl.shadowRoot;
+ if (!foundShadowRoot) {
+ throw new NoSuchElementError('Unable to locate shadow root: ' + id);
+ }
+ curContainer.shadowRoot = foundShadowRoot;
+}
+
+/**
+ * Switch to the parent frame of the current Frame. If the frame is the top most
+ * is the current frame then no action will happen.
+ */
+ function switchToParentFrame(msg) {
+ let command_id = msg.json.command_id;
+ curContainer.frame = curContainer.frame.parent;
+ let parentElement = seenEls.add(curContainer.frame);
+
+ sendSyncMessage(
+ "Marionette:switchedToFrame", {frameValue: parentElement});
+
+ sendOk(msg.json.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 (curContainer.frame.document.readyState == "complete") {
+ sendOk(command_id);
+ return;
+ } else if (curContainer.frame.document.readyState == "interactive" &&
+ errorRegex.exec(curContainer.frame.document.baseURI)) {
+ sendError(new UnknownError("Error loading page"), command_id);
+ return;
+ }
+ checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+ let foundFrame = null;
+ let frames = [];
+ let parWindow = null;
+ // Check of the curContainer.frame reference is dead
+ try {
+ frames = curContainer.frame.frames;
+ //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 = curContainer.frame.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.id === undefined) && (msg.json.element == null)) {
+ // returning to root frame
+ sendSyncMessage("Marionette:switchedToFrame", { frameValue: null });
+
+ curContainer.frame = content;
+ if(msg.json.focus == true) {
+ curContainer.frame.focus();
+ }
+
+ checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+
+ let id = msg.json.element;
+ if (seenEls.has(id)) {
+ let wantedFrame;
+ try {
+ wantedFrame = seenEls.get(id, curContainer);
+ } catch (e) {
+ sendError(e, command_id);
+ }
+
+ if (frames.length > 0) {
+ for (let i = 0; i < frames.length; i++) {
+ // use XPCNativeWrapper to compare elements; see bug 834266
+ if (XPCNativeWrapper(frames[i].frameElement) == XPCNativeWrapper(wantedFrame)) {
+ curContainer.frame = frames[i].frameElement;
+ foundFrame = i;
+ }
+ }
+ }
+
+ if (foundFrame === null) {
+ // Either the frame has been removed or we have a OOP frame
+ // so lets just get all the iframes and do a quick loop before
+ // throwing in the towel
+ let iframes = curContainer.frame.document.getElementsByTagName("iframe");
+ for (var i = 0; i < iframes.length; i++) {
+ if (XPCNativeWrapper(iframes[i]) == XPCNativeWrapper(wantedFrame)) {
+ curContainer.frame = iframes[i];
+ foundFrame = i;
+ }
+ }
+ }
+ }
+
+ if (foundFrame === null) {
+ if (typeof(msg.json.id) === 'number') {
+ try {
+ foundFrame = frames[msg.json.id].frameElement;
+ if (foundFrame !== null) {
+ curContainer.frame = foundFrame;
+ foundFrame = seenEls.add(curContainer.frame);
+ }
+ else {
+ // If foundFrame is null at this point then we have the top level browsing
+ // context so should treat it accordingly.
+ sendSyncMessage("Marionette:switchedToFrame", { frameValue: null});
+ curContainer.frame = content;
+ if(msg.json.focus == true) {
+ curContainer.frame.focus();
+ }
+
+ checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+ } catch (e) {
+ // Since window.frames does not return OOP frames it will throw
+ // and we land up here. Let's not give up and check if there are
+ // iframes and switch to the indexed frame there
+ let iframes = curContainer.frame.document.getElementsByTagName("iframe");
+ if (msg.json.id >= 0 && msg.json.id < iframes.length) {
+ curContainer.frame = iframes[msg.json.id];
+ foundFrame = msg.json.id;
+ }
+ }
+ }
+ }
+
+ if (foundFrame === null) {
+ sendError(new NoSuchFrameError("Unable to locate frame: " + (msg.json.id || msg.json.element)), command_id);
+ return true;
+ }
+
+ // send a synchronous message to let the server update the currently active
+ // frame element (for getActiveFrame)
+ let frameValue = element.toJson(
+ curContainer.frame.wrappedJSObject, seenEls)[element.Key];
+ sendSyncMessage("Marionette:switchedToFrame", {frameValue: frameValue});
+
+ let rv = null;
+ if (curContainer.frame.contentWindow === null) {
+ // The frame we want to switch to is a remote/OOP frame;
+ // notify our parent to handle the switch
+ curContainer.frame = content;
+ rv = {win: parWindow, frame: foundFrame};
+ } else {
+ curContainer.frame = curContainer.frame.contentWindow;
+ if (msg.json.focus) {
+ curContainer.frame.focus();
+ }
+ checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ }
+
+ sendResponse(rv, command_id);
+}
+
+function addCookie(cookie) {
+ cookies.add(cookie.name, cookie.value, cookie);
+}
+
+/**
+ * Get all cookies for the current domain.
+ */
+function getCookies() {
+ let rv = [];
+
+ for (let cookie of cookies) {
+ let expires = cookie.expires;
+ // session cookie, don't return an expiry
+ if (expires == 0) {
+ expires = null;
+ // date before epoch time, cap to epoch
+ } else if (expires == 1) {
+ expires = 0;
+ }
+ rv.push({
+ 'name': cookie.name,
+ 'value': cookie.value,
+ 'path': cookie.path,
+ 'domain': cookie.host,
+ 'secure': cookie.isSecure,
+ 'httpOnly': cookie.httpOnly,
+ 'expiry': expires
+ });
+ }
+
+ return rv;
+}
+
+/**
+ * Delete a cookie by name.
+ */
+function deleteCookie(name) {
+ cookies.delete(name);
+}
+
+/**
+ * Delete all the visibile cookies on a page.
+ */
+function deleteAllCookies() {
+ for (let cookie of cookies) {
+ cookies.delete(cookie);
+ }
+}
+
+function getAppCacheStatus(msg) {
+ sendResponse(
+ curContainer.frame.applicationCache.status, msg.json.command_id);
+}
+
+/**
+ * Perform a screen capture in content context.
+ *
+ * Accepted values for |opts|:
+ *
+ * @param {UUID=} id
+ * Optional web element reference of an element to take a screenshot
+ * of.
+ * @param {boolean=} full
+ * True to take a screenshot of the entire document element. Is not
+ * considered if {@code id} is not defined. Defaults to true.
+ * @param {Array.<UUID>=} highlights
+ * Draw a border around the elements found by their web element
+ * references.
+ * @param {boolean=} scroll
+ * When |id| is given, scroll it into view before taking the
+ * screenshot. Defaults to true.
+ *
+ * @param {capture.Format} format
+ * Format to return the screenshot in.
+ * @param {Object.<string, ?>} opts
+ * Options.
+ *
+ * @return {string}
+ * Base64 encoded string or a SHA-256 hash of the screenshot.
+ */
+function takeScreenshot(format, opts = {}) {
+ let id = opts.id;
+ let full = !!opts.full;
+ let highlights = opts.highlights || [];
+ let scroll = !!opts.scroll;
+
+ let highlightEls = highlights.map(ref => seenEls.get(ref, curContainer));
+
+ let canvas;
+
+ // viewport
+ if (!id && !full) {
+ canvas = capture.viewport(curContainer.frame, highlightEls);
+
+ // element or full document element
+ } else {
+ let el;
+ if (id) {
+ el = seenEls.get(id, curContainer);
+ if (scroll) {
+ element.scrollIntoView(el);
+ }
+ } else {
+ el = curContainer.frame.document.documentElement;
+ }
+
+ canvas = capture.element(el, highlightEls);
+ }
+
+ switch (format) {
+ case capture.Format.Base64:
+ return capture.toBase64(canvas);
+
+ case capture.Format.Hash:
+ return capture.toHash(canvas);
+
+ default:
+ throw new TypeError("Unknown screenshot format: " + format);
+ }
+}
+
+// Call register self when we get loaded
+registerSelf();