Mypal/testing/marionette/listener.js

1817 lines
60 KiB
JavaScript

/* 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.<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 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.<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();