/* 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/Preferences.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.} * 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 if ((el.type == "date" || el.type == "time") && Preferences.get("dom.forms.datetime")) { yield interaction.setFormControlValue(el, val); } 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.=} 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.} 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();