diff options
Diffstat (limited to 'testing/marionette/listener.js')
-rw-r--r-- | testing/marionette/listener.js | 1812 |
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(); |