gecko/testing/marionette/driver.js
Andreas Tolfsen 6a44259ba9 Bug 1153832: New dispatch style framework in Marionette listener
Takes advantage of the new dispatching technique introduced in bug
1107706 on the content side.

The patch introduces the framework to write simpler command handlers
in content space, but does not convert all commands in listener.js to
use this.  This can be done gradually, as both techniques are still
compatible.

r=dburns
2015-04-15 12:18:00 +01:00

3348 lines
103 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";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
const loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader);
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
this.DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
XPCOMUtils.defineLazyServiceGetter(
this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager");
Cu.import("chrome://marionette/content/actions.js");
Cu.import("chrome://marionette/content/elements.js");
Cu.import("chrome://marionette/content/emulator.js");
Cu.import("chrome://marionette/content/error.js");
Cu.import("chrome://marionette/content/modal.js");
Cu.import("chrome://marionette/content/simpletest.js");
loader.loadSubScript("chrome://marionette/content/common.js");
// preserve this import order:
let utils = {};
loader.loadSubScript("chrome://marionette/content/EventUtils.js", utils);
loader.loadSubScript("chrome://marionette/content/ChromeUtils.js", utils);
loader.loadSubScript("chrome://marionette/content/atoms.js", utils);
loader.loadSubScript("chrome://marionette/content/sendkeys.js", utils);
loader.loadSubScript("chrome://marionette/content/frame-manager.js");
this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
const FRAME_SCRIPT = "chrome://marionette/content/listener.js";
const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer";
const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
const CONTENT_LISTENER_PREF = "marionette.contentListener";
const logger = Log.repository.getLogger("Marionette");
const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
.getService(Ci.nsIMessageBroadcaster);
let specialpowers = {};
// This is used to prevent newSession from returning before the telephony
// API's are ready; see bug 792647. This assumes that marionette-server.js
// will be loaded before the 'system-message-listener-ready' message
// is fired. If this stops being true, this approach will have to change.
let systemMessageListenerReady = false;
Services.obs.addObserver(function() {
systemMessageListenerReady = true;
}, "system-message-listener-ready", false);
// This is used on desktop to prevent newSession from returning before a page
// load initiated by the Firefox command line has completed.
let delayedBrowserStarted = false;
Services.obs.addObserver(function () {
delayedBrowserStarted = true;
}, BROWSER_STARTUP_FINISHED, false);
this.Context = {
CHROME: "chrome",
CONTENT: "content",
};
this.Context.fromString = function(s) {
s = s.toUpperCase();
if (s in this) {
return this[s];
}
return null;
};
/**
* Creates a transparent interface between the chrome- and content
* processes.
*
* Calls to this object will be proxied via the message manager to the active
* browsing context (content) and responses will be provided back as
* promises.
*
* The argument sequence is serialised and passed as an array, unless it
* consists of a single object type that isn't null, in which case it's
* passed literally. The latter specialisation is temporary to achieve
* backwards compatibility with listener.js.
*
* @param {function(): nsIMessageManager} mmFn
* Function returning the current message manager.
* @param {function(string, Object, number)} sendAsyncFn
* Callback for sending async messages to the current listener.
* @param {function(): BrowserObj} curBrowserFn
* Function that returns the current browser.
*/
let ListenerProxy = function(mmFn, sendAsyncFn, curBrowserFn) {
this.curCmdId = null;
this.sendAsync = sendAsyncFn;
this.mmFn_ = mmFn;
this.curBrowserFn_ = curBrowserFn;
};
Object.defineProperty(ListenerProxy.prototype, "mm", {
get: function() { return this.mmFn_(); }
});
Object.defineProperty(ListenerProxy.prototype, "curBrowser", {
get: function() { return this.curBrowserFn_(); }
});
ListenerProxy.prototype.__noSuchMethod__ = function*(name, args) {
const ok = "Marionette:ok";
const val = "Marionette:done";
const err = "Marionette:error";
let proxy = new Promise((resolve, reject) => {
let removeListeners = (name, fn) => {
let rmFn = msg => {
if (this.isOutOfSync(msg.json.command_id)) {
logger.warn("Skipping out-of-sync response from listener: " +
msg.name + msg.json.toSource());
return;
}
listeners.remove();
modal.removeHandler(handleDialog);
fn(msg);
this.curCmdId = null;
};
listeners.push([name, rmFn]);
return rmFn;
};
let listeners = [];
listeners.add = () => {
this.mm.addMessageListener(ok, removeListeners(ok, okListener));
this.mm.addMessageListener(val, removeListeners(val, valListener));
this.mm.addMessageListener(err, removeListeners(err, errListener));
};
listeners.remove = () =>
listeners.map(l => this.mm.removeMessageListener(l[0], l[1]));
let okListener = () => resolve();
let valListener = msg => resolve(msg.json.value);
let errListener = msg => reject(msg.objects.error);
let handleDialog = function(subject, topic) {
listeners.remove();
modal.removeHandler(handleDialog);
this.sendAsync("cancelRequest");
resolve();
}.bind(this);
// start content process listeners, and install observers for global-
// and tab modal dialogues
listeners.add();
modal.addHandler(handleDialog);
// convert to array if passed arguments
let msg;
if (args.length == 1 && typeof args[0] == "object" && args[0] !== null) {
msg = args[0];
} else {
msg = Array.prototype.slice.call(args);
}
this.sendAsync(name, msg, this.curCmdId);
});
return proxy;
};
ListenerProxy.prototype.isOutOfSync = function(id) {
return this.curCmdId !== id;
};
/**
* Implements (parts of) the W3C WebDriver protocol. GeckoDriver lives
* in the chrome context and mediates content calls to the listener via
* ListenerProxy.
*
* Throughout this prototype, functions with the argument {@code cmd}'s
* documentation refers to the contents of the {@code cmd.parameters}
* object.
*
* @param {string} appName
* Description of the product, for example "B2G" or "Firefox".
* @param {string} device
* Device this driver should assume.
* @param {Emulator=} emulator
* Reference to the emulator connection, if running on an emulator.
*/
this.GeckoDriver = function(appName, device, emulator) {
this.appName = appName;
this.emulator = emulator;
this.sessionId = null;
// holds list of BrowserObjs
this.browsers = {};
// points to current browser
this.curBrowser = null;
this.context = Context.CONTENT;
this.scriptTimeout = null;
this.searchTimeout = null;
this.pageTimeout = null;
this.timer = null;
this.inactivityTimer = null;
// called by simpletest methods
this.heartbeatCallback = function() {};
this.marionetteLog = new MarionetteLogObj();
// topmost chrome frame
this.mainFrame = null;
// chrome iframe that currently has focus
this.curFrame = null;
this.mainContentFrameId = null;
this.importedScripts = FileUtils.getFile("TmpD", ["marionetteChromeScripts"]);
this.importedScriptHashes = {};
this.importedScriptHashes[Context.CONTENT] = [];
this.importedScriptHashes[Context.CHROME] = [];
this.currentFrameElement = null;
this.testName = null;
this.mozBrowserClose = null;
this.enabled_security_pref = false;
this.sandbox = null;
// frame ID of the current remote frame, used for mozbrowserclose events
this.oopFrameId = null;
this.observing = null;
this._browserIds = new WeakMap();
this.actions = new ActionChain(utils);
this.sessionCapabilities = {
// Mandated capabilities
"browserName": this.appName,
"browserVersion": Services.appinfo.version,
"platformName": Services.appinfo.OS.toUpperCase(),
"platformVersion": Services.appinfo.platformVersion,
// Supported features
"handlesAlerts": false,
"nativeEvents": false,
"raisesAccessibilityExceptions": false,
"rotatable": this.appName == "B2G",
"secureSsl": false,
"takesElementScreenshot": true,
"takesScreenshot": true,
// Selenium 2 compat
"platform": Services.appinfo.OS.toUpperCase(),
// Proprietary extensions
"XULappId" : Services.appinfo.ID,
"appBuildId" : Services.appinfo.appBuildID,
"device": device,
"version": Services.appinfo.version
};
this.mm = globalMessageManager;
this.listener = new ListenerProxy(
() => this.mm,
this.sendAsync.bind(this),
() => this.curBrowser);
this.dialog = null;
let handleDialog = (subject, topic) => {
let winr;
if (topic == modal.COMMON_DIALOG_LOADED) {
winr = Cu.getWeakReference(subject);
}
this.dialog = new modal.Dialog(() => this.curBrowser, winr);
};
modal.addHandler(handleDialog);
};
GeckoDriver.prototype.QueryInterface = XPCOMUtils.generateQI([
Ci.nsIMessageListener,
Ci.nsIObserver,
Ci.nsISupportsWeakReference
]);
/**
* Switches to the global ChromeMessageBroadcaster, potentially replacing
* a frame-specific ChromeMessageSender. Has no effect if the global
* ChromeMessageBroadcaster is already in use. If this replaces a
* frame-specific ChromeMessageSender, it removes the message listeners
* from that sender, and then puts the corresponding frame script "to
* sleep", which removes most of the message listeners from it as well.
*/
GeckoDriver.prototype.switchToGlobalMessageManager = function() {
if (this.curBrowser && this.curBrowser.frameManager.currentRemoteFrame !== null) {
this.curBrowser.frameManager.removeMessageManagerListeners(this.mm);
this.sendAsync("sleepSession");
this.curBrowser.frameManager.currentRemoteFrame = null;
}
this.mm = globalMessageManager;
};
/**
* Helper method to send async messages to the content listener.
* Correct usage is to pass in the name of a function in listener.js,
* a message object consisting of JSON serialisable primitives,
* and the current command's ID.
*
* @param {string} name
* Suffix of the targetted message listener ({@code Marionette:<suffix>}).
* @param {Object=} msg
* JSON serialisable object to send to the listener.
* @param {number=} cmdId
* Command ID to ensure synchronisity.
*/
GeckoDriver.prototype.sendAsync = function(name, msg, cmdId) {
let curRemoteFrame = this.curBrowser.frameManager.currentRemoteFrame;
name = `Marionette:${name}`;
if (cmdId) {
msg.command_id = cmdId;
}
if (curRemoteFrame === null) {
this.curBrowser.executeWhenReady(() => {
this.mm.broadcastAsyncMessage(name + this.curBrowser.curFrameId, msg);
});
} else {
let remoteFrameId = curRemoteFrame.targetFrameId;
try {
this.mm.sendAsyncMessage(name + remoteFrameId, msg);
} catch (e) {
switch(e.result) {
case Components.results.NS_ERROR_FAILURE:
throw new FrameSendFailureError(curRemoteFrame);
case Components.results.NS_ERROR_NOT_INITIALIZED:
throw new FrameSendNotInitializedError(curRemoteFrame);
default:
throw new WebDriverError(e.toString());
}
}
}
};
/**
* Gets the current active window.
*
* @return {nsIDOMWindow}
*/
GeckoDriver.prototype.getCurrentWindow = function() {
let typ = null;
if (this.curFrame == null) {
if (this.curBrowser == null) {
if (this.context == Context.CONTENT) {
typ = 'navigator:browser';
}
return Services.wm.getMostRecentWindow(typ);
} else {
return this.curBrowser.window;
}
} else {
return this.curFrame;
}
};
/**
* Gets the the window enumerator.
*
* @return {nsISimpleEnumerator}
*/
GeckoDriver.prototype.getWinEnumerator = function() {
let typ = null;
if (this.appName != "B2G" && this.context == Context.CONTENT) {
typ = "navigator:browser";
}
return Services.wm.getEnumerator(typ);
};
GeckoDriver.prototype.addFrameCloseListener = function(action) {
let win = this.getCurrentWindow();
this.mozBrowserClose = e => {
if (e.target.id == this.oopFrameId) {
win.removeEventListener("mozbrowserclose", this.mozBrowserClose, true);
this.switchToGlobalMessageManager();
throw new FrameSendFailureError(
`The frame closed during the ${action}, recovering to allow further communications`);
}
};
win.addEventListener("mozbrowserclose", this.mozBrowserClose, true);
};
/**
* Create a new BrowserObj for window and add to known browsers.
*
* @param {nsIDOMWindow} win
* Window for which we will create a BrowserObj.
*
* @return {string}
* Returns the unique server-assigned ID of the window.
*/
GeckoDriver.prototype.addBrowser = function(win) {
let browser = new BrowserObj(win, this);
let winId = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
winId = winId + ((this.appName == "B2G") ? "-b2g" : "");
this.browsers[winId] = browser;
this.curBrowser = this.browsers[winId];
if (typeof this.curBrowser.elementManager.seenItems[winId] == "undefined") {
// add this to seenItems so we can guarantee
// the user will get winId as this window's id
this.curBrowser.elementManager.seenItems[winId] = Cu.getWeakReference(win);
}
};
/**
* Registers a new browser, win, with Marionette.
*
* If we have not seen the browser content window before, the listener
* frame script will be loaded into it. If isNewSession is true, we will
* switch focus to the start frame when it registers.
*
* @param {nsIDOMWindow} win
* Window whose browser we need to access.
* @param {boolean=false} isNewSession
* True if this is the first time we're talking to this browser.
*/
GeckoDriver.prototype.startBrowser = function(win, isNewSession=false) {
this.mainFrame = win;
this.curFrame = null;
this.addBrowser(win);
this.curBrowser.isNewSession = isNewSession;
this.curBrowser.startSession(isNewSession, win, this.whenBrowserStarted.bind(this));
};
/**
* Callback invoked after a new session has been started in a browser.
* Loads the Marionette frame script into the browser if needed.
*
* @param {nsIDOMWindow} win
* Window whose browser we need to access.
* @param {boolean} isNewSession
* True if this is the first time we're talking to this browser.
*/
GeckoDriver.prototype.whenBrowserStarted = function(win, isNewSession) {
utils.window = win;
try {
let mm = win.window.messageManager;
if (!isNewSession) {
// Loading the frame script corresponds to a situation we need to
// return to the server. If the messageManager is a message broadcaster
// with no children, we don't have a hope of coming back from this call,
// so send the ack here. Otherwise, make a note of how many child scripts
// will be loaded so we known when it's safe to return.
if (mm.childCount != 0) {
this.curBrowser.frameRegsPending = mm.childCount;
}
}
if (!Services.prefs.getBoolPref("marionette.contentListener") || !isNewSession) {
mm.loadFrameScript(FRAME_SCRIPT, true, true);
Services.prefs.setBoolPref("marionette.contentListener", true);
}
} catch (e) {
// there may not always be a content process
logger.error(
`Could not load listener into content for page ${win.location.href}: ${e}`);
}
};
/**
* Recursively get all labeled text.
*
* @param {nsIDOMElement} el
* The parent element.
* @param {Array.<string>} lines
* Array that holds the text lines.
*/
GeckoDriver.prototype.getVisibleText = function(el, lines) {
try {
if (utils.isElementDisplayed(el)) {
if (el.value) {
lines.push(el.value);
}
for (let child in el.childNodes) {
this.getVisibleText(el.childNodes[child], lines);
}
}
} catch (e) {
if (el.nodeName == "#text") {
lines.push(el.textContent);
}
}
};
/**
* Given a file name, this will delete the file from the temp directory
* if it exists.
*
* @param {string} filename
*/
GeckoDriver.prototype.deleteFile = function(filename) {
let file = FileUtils.getFile("TmpD", [filename.toString()]);
if (file.exists()) {
file.remove(true);
}
};
/**
* Handles registration of new content listener browsers. Depending on
* their type they are either accepted or ignored.
*/
GeckoDriver.prototype.registerBrowser = function(id, be) {
let nullPrevious = this.curBrowser.curFrameId == null;
let listenerWindow = Services.wm.getOuterWindowWithId(id);
// go in here if we're already in a remote frame
if (this.curBrowser.frameManager.currentRemoteFrame !== null &&
(!listenerWindow || this.mm == this.curBrowser.frameManager
.currentRemoteFrame.messageManager.get())) {
// The outerWindowID from an OOP frame will not be meaningful to
// the parent process here, since each process maintains its own
// independent window list. So, it will either be null (!listenerWindow)
// if we're already in a remote frame, or it will point to some
// random window, which will hopefully cause an href mismatch.
// Currently this only happens in B2G for OOP frames registered in
// Marionette:switchToFrame, so we'll acknowledge the switchToFrame
// message here.
//
// TODO: Should have a better way of determining that this message
// is from a remote frame.
this.curBrowser.frameManager.currentRemoteFrame.targetFrameId =
this.generateFrameId(id);
}
let reg = {};
// this will be sent to tell the content process if it is the main content
let mainContent = this.curBrowser.mainContentId == null;
if (be.getAttribute("type") != "content") {
// curBrowser holds all the registered frames in knownFrames
let uid = this.generateFrameId(id);
reg.id = uid;
reg.remotenessChange = this.curBrowser.register(uid, be);
}
// set to true if we updated mainContentId
mainContent = mainContent == true &&
this.curBrowser.mainContentId != null;
if (mainContent) {
this.mainContentFrameId = this.curBrowser.curFrameId;
}
this.curBrowser.elementManager.seenItems[reg.id] =
Cu.getWeakReference(listenerWindow);
if (nullPrevious && (this.curBrowser.curFrameId != null)) {
this.sendAsync("newSession",
{
B2G: (this.appName == "B2G"),
raisesAccessibilityExceptions:
this.sessionCapabilities.raisesAccessibilityExceptions
},
this.newSessionCommandId);
if (this.curBrowser.isNewSession) {
this.newSessionCommandId = null;
}
}
return [reg, mainContent];
};
GeckoDriver.prototype.registerPromise = function() {
const li = "Marionette:register";
return new Promise((resolve) => {
this.mm.addMessageListener(li, function cb(msg) {
let wid = msg.json.value;
let be = msg.target;
let rv = this.registerBrowser(wid, be);
if (this.curBrowser.frameRegsPending > 0) {
this.curBrowser.frameRegsPending--;
}
if (this.curBrowser.frameRegsPending == 0) {
this.mm.removeMessageListener(li, cb);
resolve();
}
// this is a sync message and listeners expect the ID back
return rv;
}.bind(this));
});
};
GeckoDriver.prototype.listeningPromise = function() {
const li = "Marionette:listenersAttached";
return new Promise((resolve) => {
this.mm.addMessageListener(li, function() {
this.mm.removeMessageListener(li, this);
resolve();
}.bind(this));
});
};
/** Create a new session. */
GeckoDriver.prototype.newSession = function(cmd, resp) {
this.sessionId = cmd.parameters.sessionId ||
cmd.parameters.session_id ||
uuidGen.generateUUID().toString();
this.newSessionCommandId = cmd.id;
this.setSessionCapabilities(cmd.parameters.capabilities);
this.scriptTimeout = 10000;
// SpecialPowers requires insecure automation-only features that we
// put behind a pref
let sec = false;
try {
sec = Services.prefs.getBoolPref(SECURITY_PREF);
} catch (e) {}
if (!sec) {
this.enabled_security_pref = true;
Services.prefs.setBoolPref(SECURITY_PREF, true);
}
if (!specialpowers.hasOwnProperty("specialPowersObserver")) {
loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.js",
specialpowers);
specialpowers.specialPowersObserver = new specialpowers.SpecialPowersObserver();
specialpowers.specialPowersObserver.init();
specialpowers.specialPowersObserver._loadFrameScript();
}
let registerBrowsers = this.registerPromise();
let browserListening = this.listeningPromise();
let waitForWindow = function() {
let win = this.getCurrentWindow();
if (!win) {
// if the window isn't even created, just poll wait for it
let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
checkTimer.initWithCallback(waitForWindow.bind(this), 100,
Ci.nsITimer.TYPE_ONE_SHOT);
} else if (win.document.readyState != "complete") {
// otherwise, wait for it to be fully loaded before proceeding
let listener = ev => {
// ensure that we proceed, on the top level document load event
// (not an iframe one...)
if (ev.target != win.document) {
return;
}
win.removeEventListener("load", listener);
waitForWindow.call(this);
};
win.addEventListener("load", listener, true);
} else {
let clickToStart;
try {
clickToStart = Services.prefs.getBoolPref(CLICK_TO_START_PREF);
} catch (e) {}
if (clickToStart && (this.appName != "B2G")) {
let pService = Cc["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Ci.nsIPromptService);
pService.alert(win, "", "Click to start execution of marionette tests");
}
this.startBrowser(win, true);
}
};
let runSessionStart = function() {
if (!Services.prefs.getBoolPref(CONTENT_LISTENER_PREF)) {
waitForWindow.call(this);
} else if (this.appName != "Firefox" && this.curBrowser === null) {
// if there is a content listener, then we just wake it up
this.addBrowser(this.getCurrentWindow());
this.curBrowser.startSession(this.whenBrowserStarted.bind(this));
this.mm.broadcastAsyncMessage("Marionette:restart", {});
} else {
throw new WebDriverError("Session already running");
}
this.switchToGlobalMessageManager();
};
if (!delayedBrowserStarted && this.appName != "B2G") {
let self = this;
Services.obs.addObserver(function onStart() {
Services.obs.removeObserver(onStart, BROWSER_STARTUP_FINISHED);
runSessionStart.call(self);
}, BROWSER_STARTUP_FINISHED, false);
} else {
runSessionStart.call(this);
}
yield registerBrowsers;
yield browserListening;
resp.sessionId = this.sessionId;
resp.value = this.sessionCapabilities;
};
/**
* Send the current session's capabilities to the client.
*
* Capabilities informs the client of which WebDriver features are
* supported by Firefox and Marionette. They are immutable for the
* length of the session.
*
* The return value is an immutable map of string keys
* ("capabilities") to values, which may be of types boolean,
* numerical or string.
*/
GeckoDriver.prototype.getSessionCapabilities = function(cmd, resp) {
resp.value = this.sessionCapabilities;
};
/**
* Update the sessionCapabilities object with the keys that have been
* passed in when a new session is created.
*
* This part of the WebDriver spec is currently in flux, see
* http://lists.w3.org/Archives/Public/public-browser-tools-testing/2014OctDec/0000.html
*
* This is not a public API, only available when a new session is
* created.
*
* @param {Object} newCaps
* Key/value dictionary to overwrite session's current capabilities.
*/
GeckoDriver.prototype.setSessionCapabilities = function(newCaps) {
const copy = (from, to={}) => {
let errors = {};
for (let key in from) {
if (key === "desiredCapabilities") {
// Keeping desired capabilities separate for now so that we can keep
// backwards compatibility
to = copy(from[key], to);
} else if (key === "requiredCapabilities") {
for (let caps in from[key]) {
if (from[key][caps] !== this.sessionCapabilities[caps]) {
errors[caps] = from[key][caps] + " does not equal " +
this.sessionCapabilities[caps];
}
}
}
to[key] = from[key];
}
if (Object.keys(errors).length == 0) {
return to;
}
throw new SessionNotCreatedError(
`Not all requiredCapabilities could be met: ${JSON.stringify(errors)}`);
};
// clone, overwrite, and set
let caps = copy(this.sessionCapabilities);
caps = copy(newCaps, caps);
this.sessionCapabilities = caps;
};
/**
* Log message. Accepts user defined log-level.
*
* @param {string} value
* Log message.
* @param {string} level
* Arbitrary log level.
*/
GeckoDriver.prototype.log = function(cmd, resp) {
this.marionetteLog.log(cmd.parameters.value, cmd.parameters.level);
};
/** Return all logged messages. */
GeckoDriver.prototype.getLogs = function(cmd, resp) {
resp.value = this.marionetteLog.getLogs();
};
/**
* Sets the context of the subsequent commands to be either "chrome" or
* "content".
*
* @param {string} value
* Name of the context to be switched to. Must be one of "chrome" or
* "content".
*/
GeckoDriver.prototype.setContext = function(cmd, resp) {
let val = cmd.parameters.value;
let ctx = Context.fromString(val);
if (ctx === null) {
throw new WebDriverError(`Invalid context: ${val}`);
}
this.context = ctx;
};
/** Gets the context of the server, either "chrome" or "content". */
GeckoDriver.prototype.getContext = function(cmd, resp) {
resp.value = this.context.toString();
};
/**
* Returns a chrome sandbox that can be used by the execute and
* executeWithCallback functions.
*
* @param {nsIDOMWindow} win
* Window in which we will execute code.
* @param {Marionette} mn
* Marionette test instance.
* @param {Object} args
* Arguments given by client.
* @param {boolean} sp
* True to enable special powers in the sandbox, false not to.
*
* @return {nsIXPCComponents_utils_Sandbox}
* Returns the sandbox.
*/
GeckoDriver.prototype.createExecuteSandbox = function(win, mn, sp) {
let sb = new Cu.Sandbox(win,
{sandboxPrototype: win, wantXrays: false, sandboxName: ""});
sb.global = sb;
sb.testUtils = utils;
mn.exports.forEach(function(fn) {
if (typeof mn[fn] === 'function') {
sb[fn] = mn[fn].bind(mn);
} else {
sb[fn] = mn[fn];
}
});
sb.isSystemMessageListenerReady = () => systemMessageListenerReady;
if (sp) {
let pow = [
"chrome://specialpowers/content/specialpowersAPI.js",
"chrome://specialpowers/content/SpecialPowersObserverAPI.js",
"chrome://specialpowers/content/ChromePowers.js",
];
pow.map(s => loader.loadSubScript(s, sb));
}
return sb;
};
/**
* Apply arguments sent from the client to the current (possibly reused)
* execution sandbox.
*/
GeckoDriver.prototype.applyArgumentsToSandbox = function(win, sb, args) {
sb.__marionetteParams = this.curBrowser.elementManager.convertWrappedArguments(args, win);
sb.__namedArgs = this.curBrowser.elementManager.applyNamedArgs(args);
};
/**
* Executes a script in the given sandbox.
*
* @param {Response} resp
* Response object given to the command calling this routine.
* @param {nsIXPCComponents_utils_Sandbox} sandbox
* Sandbox in which the script will run.
* @param {string} script
* Script to run.
* @param {boolean} directInject
* If true, then the script will be run as is, and not as a function
* body (as you would do using the WebDriver spec).
* @param {boolean} async
* True if the script is asynchronous.
* @param {number} timeout
* When to interrupt script in milliseconds.
*/
GeckoDriver.prototype.executeScriptInSandbox = function(
resp,
sandbox,
script,
directInject,
async,
timeout) {
if (directInject && async && (timeout == null || timeout == 0)) {
throw new TimeoutError("Please set a timeout");
}
if (this.importedScripts.exists()) {
let stream = Cc["@mozilla.org/network/file-input-stream;1"]
.createInstance(Ci.nsIFileInputStream);
stream.init(this.importedScripts, -1, 0, 0);
let data = NetUtil.readInputStreamToString(stream, stream.available());
stream.close();
script = data + script;
}
let res = Cu.evalInSandbox(script, sandbox, "1.8", "dummy file", 0);
if (directInject && !async &&
(res == undefined || res.passed == undefined)) {
throw new WebDriverError("finish() not called");
}
if (!async) {
// It's fine to pass on and modify resp here because
// executeScriptInSandbox is the last function to be called
// in execute and executeWithCallback respectively.
resp.value = this.curBrowser.elementManager.wrapValue(res);
}
};
/**
* Execute the given script either as a function body or directly (for
* mochitest-like JS Marionette tests).
*
* If directInject is ture, it will run directly and not as a function
* body.
*/
GeckoDriver.prototype.execute = function(cmd, resp, directInject) {
let {inactivityTimeout,
scriptTimeout,
script,
newSandbox,
args,
specialPowers,
filename,
line} = cmd.parameters;
if (!scriptTimeout) {
scriptTimeout = this.scriptTimeout;
}
if (typeof newSandbox == "undefined") {
newSandbox = true;
}
if (this.context == Context.CONTENT) {
resp.value = yield this.listener.executeScript({
script: script,
args: args,
newSandbox: newSandbox,
timeout: scriptTimeout,
specialPowers: specialPowers,
filename: filename,
line: line
});
return;
}
// handle the inactivity timeout
let that = this;
if (inactivityTimeout) {
let setTimer = function() {
that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
if (that.inactivityTimer != null) {
that.inactivityTimer.initWithCallback(function() {
throw new ScriptTimeoutError("timed out due to inactivity");
}, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
}
};
setTimer();
this.heartbeatCallback = function() {
that.inactivityTimer.cancel();
setTimer();
};
}
let win = this.getCurrentWindow();
if (!this.sandbox || newSandbox) {
let marionette = new Marionette(
this,
win,
"chrome",
this.marionetteLog,
scriptTimeout,
this.heartbeatCallback,
this.testName);
this.sandbox = this.createExecuteSandbox(
win,
marionette,
specialPowers);
if (!this.sandbox) {
return;
}
}
this.applyArgumentsToSandbox(win, this.sandbox, args);
try {
this.sandbox.finish = () => {
if (this.inactivityTimer != null) {
this.inactivityTimer.cancel();
}
return this.sandbox.generate_results();
};
if (!directInject) {
script = `let func = function() { ${script} }; func.apply(null, __marionetteParams);`;
}
this.executeScriptInSandbox(
resp,
this.sandbox,
script,
directInject,
false /* async */,
scriptTimeout);
} catch (e) {
throw new JavaScriptError(e, "execute_script", filename, line, script);
}
};
/**
* Set the timeout for asynchronous script execution.
*
* @param {number} ms
* Time in milliseconds.
*/
GeckoDriver.prototype.setScriptTimeout = function(cmd, resp) {
let ms = parseInt(cmd.parameters.ms);
if (isNaN(ms)) {
throw new WebDriverError("Not a Number");
}
this.scriptTimeout = ms;
};
/**
* Execute pure JavaScript. Used to execute mochitest-like Marionette
* tests.
*/
GeckoDriver.prototype.executeJSScript = function(cmd, resp) {
// TODO(ato): cmd.newSandbox doesn't ever exist?
// All pure JS scripts will need to call
// Marionette.finish() to complete the test
if (typeof cmd.newSandbox == "undefined") {
// If client does not send a value in newSandbox,
// then they expect the same behaviour as WebDriver.
cmd.newSandbox = true;
}
switch (this.context) {
case Context.CHROME:
if (cmd.parameters.async) {
yield this.executeWithCallback(cmd, resp, cmd.parameters.async);
} else {
this.execute(cmd, resp, true /* async */);
}
break;
case Context.CONTENT:
resp.value = yield this.listener.executeJSScript({
script: cmd.parameters.script,
args: cmd.parameters.args,
newSandbox: cmd.parameters.newSandbox,
async: cmd.parameters.async,
timeout: cmd.parameters.scriptTimeout ?
cmd.parameters.scriptTimeout : this.scriptTimeout,
inactivityTimeout: cmd.parameters.inactivityTimeout,
specialPowers: cmd.parameters.specialPowers,
filename: cmd.parameters.filename,
line: cmd.parameters.line,
});
break;
}
};
/**
* This function is used by executeAsync and executeJSScript to execute
* a script in a sandbox.
*
* For executeJSScript, it will return a message only when the finish()
* method is called.
*
* For executeAsync, it will return a response when
* {@code marionetteScriptFinished} (equivalent to
* {@code arguments[arguments.length-1]}) function is called,
* or if it times out.
*
* If directInject is true, it will be run directly and not as a
* function body.
*/
GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) {
let {script,
args,
newSandbox,
inactivityTimeout,
scriptTimeout,
specialPowers,
filename,
line} = cmd.parameters;
if (!scriptTimeout) {
scriptTimeout = this.scriptTimeout;
}
if (typeof newSandbox == "undefined") {
newSandbox = true;
}
if (this.context == Context.CONTENT) {
resp.value = yield this.listener.executeAsyncScript({
script: script,
args: args,
id: cmd.id,
newSandbox: newSandbox,
timeout: scriptTimeout,
inactivityTimeout: inactivityTimeout,
specialPowers: specialPowers,
filename: filename,
line: line
});
return;
}
// handle the inactivity timeout
let that = this;
if (inactivityTimeout) {
this.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
if (this.inactivityTimer != null) {
this.inactivityTimer.initWithCallback(function() {
chromeAsyncReturnFunc(new ScriptTimeoutError("timed out due to inactivity"));
}, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
}
this.heartbeatCallback = function resetInactivityTimer() {
that.inactivityTimer.cancel();
that.inactivityTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
if (that.inactivityTimer != null) {
that.inactivityTimer.initWithCallback(function() {
chromeAsyncReturnFunc(new ScriptTimeoutError("timed out due to inactivity"));
}, inactivityTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
}
};
}
let win = this.getCurrentWindow();
let origOnError = win.onerror;
that.timeout = scriptTimeout;
let res = yield new Promise(function(resolve, reject) {
let chromeAsyncReturnFunc = function(val) {
if (that.emulator.cbs.length > 0) {
that.emulator.cbs = [];
throw new WebDriverError("Emulator callback still pending when finish() called");
}
if (cmd.id == that.sandbox.command_id) {
if (that.timer != null) {
that.timer.cancel();
that.timer = null;
}
win.onerror = origOnError;
if (error.isError(val)) {
reject(val);
} else {
resolve(val);
}
}
if (that.inactivityTimer != null) {
that.inactivityTimer.cancel();
}
};
let chromeAsyncFinish = function() {
let res = that.sandbox.generate_results();
chromeAsyncReturnFunc(res);
};
let chromeAsyncError = function(e, func, file, line, script) {
let err = new JavaScriptError(e, func, file, line, script);
chromeAsyncReturnFunc(err);
};
if (!this.sandbox || newSandbox) {
let marionette = new Marionette(
this,
win,
"chrome",
this.marionetteLog,
scriptTimeout,
this.heartbeatCallback,
this.testName);
this.sandbox = this.createExecuteSandbox(win, marionette, specialPowers);
if (!this.sandbox) {
return;
}
}
this.sandbox.command_id = cmd.id;
this.sandbox.runEmulatorCmd = (cmd, cb) => {
let ecb = new EmulatorCallback();
ecb.onresult = cb;
ecb.onerror = chromeAsyncError;
this.emulator.pushCallback(ecb);
this.emulator.send({emulator_cmd: cmd, id: ecb.id});
};
this.sandbox.runEmulatorShell = (args, cb) => {
let ecb = new EmulatorCallback();
ecb.onresult = cb;
ecb.onerror = chromeAsyncError;
this.emulator.pushCallback(ecb);
this.emulator.send({emulator_shell: args, id: ecb.id});
};
this.applyArgumentsToSandbox(win, this.sandbox, args);
// NB: win.onerror is not hooked by default due to the inability to
// differentiate content exceptions from chrome exceptions. See bug
// 1128760 for more details. A debug_script flag can be set to
// reenable onerror hooking to help debug test scripts.
if (cmd.parameters.debug_script) {
win.onerror = function(msg, url, line) {
let err = new JavaScriptError(`${msg} at: ${url} line: ${line}`);
chromeAsyncReturnFunc(err);
return true;
};
}
try {
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
if (this.timer != null) {
this.timer.initWithCallback(function() {
chromeAsyncReturnFunc(new ScriptTimeoutError("timed out"));
}, that.timeout, Ci.nsITimer.TYPE_ONE_SHOT);
}
this.sandbox.returnFunc = chromeAsyncReturnFunc;
this.sandbox.finish = chromeAsyncFinish;
if (!directInject) {
script = "__marionetteParams.push(returnFunc);" +
"let marionetteScriptFinished = returnFunc;" +
"let __marionetteFunc = function() {" + script + "};" +
"__marionetteFunc.apply(null, __marionetteParams);";
}
this.executeScriptInSandbox(
resp,
this.sandbox,
script,
directInject,
true /* async */,
scriptTimeout);
} catch (e) {
chromeAsyncError(e, "execute_async_script", filename, line, script);
}
}.bind(this));
resp.value = that.curBrowser.elementManager.wrapValue(res);
};
/**
* Navigate to to given URL.
*
* This will follow redirects issued by the server. When the method
* returns is based on the page load strategy that the user has
* selected.
*
* Documents that contain a META tag with the "http-equiv" attribute
* set to "refresh" will return if the timeout is greater than 1
* second and the other criteria for determining whether a page is
* loaded are met. When the refresh period is 1 second or less and
* the page load strategy is "normal" or "conservative", it will
* wait for the page to complete loading before returning.
*
* If any modal dialog box, such as those opened on
* window.onbeforeunload or window.alert, is opened at any point in
* the page load, it will return immediately.
*
* If a 401 response is seen by the browser, it will return
* immediately. That is, if BASIC, DIGEST, NTLM or similar
* authentication is required, the page load is assumed to be
* complete. This does not include FORM-based authentication.
*
* @param {string} url
* URL to navigate to.
*/
GeckoDriver.prototype.get = function(cmd, resp) {
let url = cmd.parameters.url;
switch (this.context) {
case Context.CONTENT:
// If a remoteness update interrupts our page load, this will never return
// We need to re-issue this request to correctly poll for readyState and
// send errors.
this.curBrowser.pendingCommands.push(() => {
cmd.parameters.command_id = this.listener.curCmdId;
this.mm.broadcastAsyncMessage(
"Marionette:pollForReadyState" + this.curBrowser.curFrameId,
cmd.parameters);
});
yield this.listener.get({url: url, pageTimeout: this.pageTimeout});
break;
case Context.CHROME:
// At least on desktop, navigating in chrome scope does not
// correspond to something a user can do, and leaves marionette
// and the browser in an unusable state. Return a generic error insted.
// TODO: Error codes need to be refined as a part of bug 1100545 and
// bug 945729.
if (this.appName == "Firefox") {
throw new UnknownError("Cannot navigate in chrome context");
}
this.getCurrentWindow().location.href = url;
yield this.pageLoadPromise();
break;
}
};
GeckoDriver.prototype.pageLoadPromise = function() {
let win = this.getCurrentWindow();
let timeout = this.pageTimeout;
let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let start = new Date().getTime();
let end = null;
return new Promise((resolve) => {
let checkLoad = function() {
end = new Date().getTime();
let elapse = end - start;
if (timeout == null || elapse <= timeout) {
if (win.document.readyState == "complete") {
resolve();
} else {
checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
}
} else {
throw new UnknownError("Error loading page");
}
};
checkTimer.initWithCallback(checkLoad, 100, Ci.nsITimer.TYPE_ONE_SHOT);
});
};
/**
* Get a string representing the current URL.
*
* On Desktop this returns a string representation of the URL of the
* current top level browsing context. This is equivalent to
* document.location.href.
*
* When in the context of the chrome, this returns the canonical URL
* of the current resource.
*/
GeckoDriver.prototype.getCurrentUrl = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
resp.value = this.getCurrentWindow().location.href;
break;
case Context.CONTENT:
let isB2G = this.appName == "B2G";
resp.value = yield this.listener.getCurrentUrl({isB2G: isB2G});
break;
}
};
/** Gets the current title of the window. */
GeckoDriver.prototype.getTitle = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
resp.value = win.document.documentElement.getAttribute("title");
break;
case Context.CONTENT:
resp.value = yield this.listener.getTitle();
break;
}
};
/** Gets the current type of the window. */
GeckoDriver.prototype.getWindowType = function(cmd, resp) {
let win = this.getCurrentWindow();
resp.value = win.document.documentElement.getAttribute("windowtype");
};
/** Gets the page source of the content document. */
GeckoDriver.prototype.getPageSource = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let s = new win.XMLSerializer();
resp.value = s.serializeToString(win.document);
break;
case Context.CONTENT:
resp.value = yield this.listener.getPageSource();
break;
}
};
/** Go back in history. */
GeckoDriver.prototype.goBack = function(cmd, resp) {
yield this.listener.goBack();
};
/** Go forward in history. */
GeckoDriver.prototype.goForward = function(cmd, resp) {
yield this.listener.goForward();
};
/** Refresh the page. */
GeckoDriver.prototype.refresh = function(cmd, resp) {
yield this.listener.refresh();
};
/**
* Get the current window's handle. On desktop this typically corresponds
* to the currently selected tab.
*
* Return an opaque server-assigned identifier to this window that
* uniquely identifies it within this Marionette instance. This can
* be used to switch to this window at a later point.
*
* @return {string}
* Unique window handle.
*/
GeckoDriver.prototype.getWindowHandle = function(cmd, resp) {
// curFrameId always holds the current tab.
if (this.curBrowser.curFrameId && this.appName != "B2G") {
resp.value = this.curBrowser.curFrameId;
return;
}
for (let i in this.browsers) {
if (this.curBrowser == this.browsers[i]) {
resp.value = i;
return;
}
}
};
/**
* Forces an update for the given browser's id.
*/
GeckoDriver.prototype.updateIdForBrowser = function (browser, newId) {
this._browserIds.set(browser.permanentKey, newId);
};
/**
* Retrieves a listener id for the given xul browser element. In case
* the browser is not known, an attempt is made to retrieve the id from
* a CPOW, and null is returned if this fails.
*/
GeckoDriver.prototype.getIdForBrowser = function getIdForBrowser(browser) {
if (browser === null) {
return null;
}
let permKey = browser.permanentKey;
if (this._browserIds.has(permKey)) {
return this._browserIds.get(permKey);
}
let winId = browser.outerWindowID;
if (winId) {
winId += "";
this._browserIds.set(permKey, winId);
return winId;
}
return null;
},
/**
* Get a list of top-level browsing contexts. On desktop this typically
* corresponds to the set of open tabs.
*
* Each window handle is assigned by the server and is guaranteed unique,
* however the return array does not have a specified ordering.
*
* @return {Array.<string>}
* Unique window handles.
*/
GeckoDriver.prototype.getWindowHandles = function(cmd, resp) {
let rv = [];
let winEn = this.getWinEnumerator();
while (winEn.hasMoreElements()) {
let win = winEn.getNext();
if (win.gBrowser && this.appName != "B2G") {
let tabbrowser = win.gBrowser;
for (let i = 0; i < tabbrowser.browsers.length; ++i) {
let winId = this.getIdForBrowser(tabbrowser.getBrowserAtIndex(i));
if (winId !== null) {
rv.push(winId);
}
}
} else {
// XUL Windows, at least, do not have gBrowser
let winId = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
winId += (this.appName == "B2G") ? "-b2g" : "";
rv.push(winId);
}
}
resp.value = rv;
};
/**
* Get the current window's handle. This corresponds to a window that
* may itself contain tabs.
*
* Return an opaque server-assigned identifier to this window that
* uniquely identifies it within this Marionette instance. This can
* be used to switch to this window at a later point.
*
* @return {string}
* Unique window handle.
*/
GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) {
for (let i in this.browsers) {
if (this.curBrowser == this.browsers[i]) {
resp.value = i;
return;
}
}
};
/**
* Returns identifiers for each open chrome window for tests interested in
* managing a set of chrome windows and tabs separately.
*
* @return {Array.<string>}
* Unique window handles.
*/
GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) {
let rv = [];
let winEn = this.getWinEnumerator();
while (winEn.hasMoreElements()) {
let foundWin = winEn.getNext();
let winId = foundWin.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
winId = winId + ((this.appName == "B2G") ? "-b2g" : "");
rv.push(winId);
}
resp.value = rv;
};
/**
* Get the current window position.
*
* @return {Object.<string, number>}
* Object with x and y coordinates.
*/
GeckoDriver.prototype.getWindowPosition = function(cmd, resp) {
let win = this.getCurrentWindow();
resp.value = {x: win.screenX, y: win.screenY};
};
/**
* Set the window position of the browser on the OS Window Manager
*
* @param {number} x
* X coordinate of the top/left of the window that it will be
* moved to.
* @param {number} y
* Y coordinate of the top/left of the window that it will be
* moved to.
*/
GeckoDriver.prototype.setWindowPosition = function(cmd, resp) {
if (this.appName != "Firefox") {
throw new WebDriverError("Unable to set the window position on mobile");
}
let x = parseInt(cmd.parameters.x);
let y = parseInt(cmd.parameters.y);
if (isNaN(x) || isNaN(y)) {
throw new UnknownError("x and y arguments should be integers");
}
let win = this.getCurrentWindow();
win.moveTo(x, y);
};
/**
* Switch current top-level browsing context by name or server-assigned ID.
* Searches for windows by name, then ID. Content windows take precedence.
*
* @param {string} name
* Target name or ID of the window to switch to.
*/
GeckoDriver.prototype.switchToWindow = function(cmd, resp) {
let switchTo = cmd.parameters.name;
let isB2G = this.appName == "B2G";
let found;
let getOuterWindowId = function(win) {
let rv = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
rv += isB2G ? "-b2g" : "";
return rv;
};
let byNameOrId = function(name, outerId, contentWindowId) {
return switchTo == name ||
switchTo == contentWindowId ||
switchTo == outerId;
};
let winEn = this.getWinEnumerator();
while (winEn.hasMoreElements()) {
let win = winEn.getNext();
let outerId = getOuterWindowId(win);
if (win.gBrowser && !isB2G) {
let tabbrowser = win.gBrowser;
for (let i = 0; i < tabbrowser.browsers.length; ++i) {
let browser = tabbrowser.getBrowserAtIndex(i);
let contentWindowId = this.getIdForBrowser(browser);
if (byNameOrId(win.name, contentWindowId, outerId)) {
found = {
win: win,
outerId: outerId,
tabIndex: i,
contentId: contentWindowId
};
break;
}
}
} else {
if (byNameOrId(win.name, outerId)) {
found = {win: win, outerId: outerId};
break;
}
}
}
if (found) {
// As in content, switching to a new window invalidates a sandbox
// for reuse.
this.sandbox = null;
// Initialise Marionette if browser has not been seen before,
// otherwise switch to known browser and activate the tab if it's a
// content browser.
if (!(found.outerId in this.browsers)) {
let registerBrowsers, browserListening;
if (found.contentId) {
registerBrowsers = this.registerPromise();
browserListening = this.listeningPromise();
}
this.startBrowser(found.win, false /* isNewSession */);
if (registerBrowsers && browserListening) {
yield registerBrowsers;
yield browserListening;
}
} else {
utils.window = found.win;
this.curBrowser = this.browsers[found.outerId];
if (found.contentId) {
this.curBrowser.switchToTab(found.tabIndex);
}
}
} else {
throw new NoSuchWindowError(`Unable to locate window: ${switchTo}`);
}
};
GeckoDriver.prototype.getActiveFrame = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
// no frame means top-level
resp.value = null;
if (this.curFrame) {
resp.value = this.curBrowser.elementManager
.addToKnownElements(this.curFrame.frameElement);
}
break;
case Context.CONTENT:
resp.value = this.currentFrameElement;
break;
}
};
/**
* Switch to a given frame within the current window.
*
* @param {Object} element
* A web element reference to the element to switch to.
* @param {(string|number)} id
* If element is not defined, then this holds either the id, name,
* or index of the frame to switch to.
*/
GeckoDriver.prototype.switchToFrame = function(cmd, resp) {
let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let curWindow = this.getCurrentWindow();
let checkLoad = function() {
let errorRegex = /about:.+(error)|(blocked)\?/;
let curWindow = this.getCurrentWindow();
if (curWindow.document.readyState == "complete") {
return;
} else if (curWindow.document.readyState == "interactive" &&
errorRegex.exec(curWindow.document.baseURI)) {
throw new UnknownError("Error loading page");
}
checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
};
if (this.context == Context.CHROME) {
let foundFrame = null;
if ((cmd.parameters.id == null) && (cmd.parameters.element == null)) {
this.curFrame = null;
if (cmd.parameters.focus) {
this.mainFrame.focus();
}
checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
return;
}
if (cmd.parameters.element != undefined) {
if (this.curBrowser.elementManager.seenItems[cmd.parameters.element]) {
// HTMLIFrameElement
let wantedFrame = this.curBrowser.elementManager
.getKnownElement(cmd.parameters.element, curWindow);
// Deal with an embedded xul:browser case
if (wantedFrame.tagName == "xul:browser" || wantedFrame.tagName == "browser") {
curWindow = wantedFrame.contentWindow;
this.curFrame = curWindow;
if (cmd.parameters.focus) {
this.curFrame.focus();
}
checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
return;
}
// else, assume iframe
let frames = curWindow.document.getElementsByTagName("iframe");
let numFrames = frames.length;
for (let i = 0; i < numFrames; i++) {
if (XPCNativeWrapper(frames[i]) == XPCNativeWrapper(wantedFrame)) {
curWindow = frames[i].contentWindow;
this.curFrame = curWindow;
if (cmd.parameters.focus) {
this.curFrame.focus();
}
checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
return;
}
}
}
}
switch(typeof(cmd.parameters.id)) {
case "string" :
let foundById = null;
let frames = curWindow.document.getElementsByTagName("iframe");
let numFrames = frames.length;
for (let i = 0; i < numFrames; i++) {
//give precedence to name
let frame = frames[i];
if (frame.getAttribute("name") == cmd.parameters.id) {
foundFrame = i;
curWindow = frame.contentWindow;
break;
} else if ((foundById == null) && (frame.id == cmd.parameters.id)) {
foundById = i;
}
}
if ((foundFrame == null) && (foundById != null)) {
foundFrame = foundById;
curWindow = frames[foundById].contentWindow;
}
break;
case "number":
if (curWindow.frames[cmd.parameters.id] != undefined) {
foundFrame = cmd.parameters.id;
curWindow = curWindow.frames[foundFrame].frameElement.contentWindow;
}
break;
}
if (foundFrame != null) {
this.curFrame = curWindow;
if (cmd.parameters.focus) {
this.curFrame.focus();
}
checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
} else {
throw new NoSuchFrameError(
`Unable to locate frame: ${cmd.parameters.id}`);
}
}
else {
if ((!cmd.parameters.id) && (!cmd.parameters.element) &&
(this.curBrowser.frameManager.currentRemoteFrame !== null)) {
// We're currently using a ChromeMessageSender for a remote frame, so this
// request indicates we need to switch back to the top-level (parent) frame.
// We'll first switch to the parent's (global) ChromeMessageBroadcaster, so
// we send the message to the right listener.
this.switchToGlobalMessageManager();
}
cmd.command_id = cmd.id;
let res = yield this.listener.switchToFrame(cmd.parameters);
if (res) {
let {win: winId, frame: frameId} = res;
this.mm = this.curBrowser.frameManager.getFrameMM(winId, frameId);
let registerBrowsers = this.registerPromise();
let browserListening = this.listeningPromise();
this.oopFrameId =
this.curBrowser.frameManager.switchToFrame(winId, frameId);
yield registerBrowsers;
yield browserListening;
}
}
};
/**
* Set timeout for searching for elements.
*
* @param {number} ms
* Search timeout in milliseconds.
*/
GeckoDriver.prototype.setSearchTimeout = function(cmd, resp) {
let ms = parseInt(cmd.parameters.ms);
if (isNaN(ms)) {
throw new WebDriverError("Not a Number");
}
this.searchTimeout = ms;
};
/**
* Set timeout for page loading, searching, and scripts.
*
* @param {string} type
* Type of timeout.
* @param {number} ms
* Timeout in milliseconds.
*/
GeckoDriver.prototype.timeouts = function(cmd, resp) {
let typ = cmd.parameters.type;
let ms = parseInt(cmd.parameters.ms);
if (isNaN(ms)) {
throw new WebDriverError("Not a Number");
}
switch (typ) {
case "implicit":
this.setSearchTimeout(cmd, resp);
break;
case "script":
this.setScriptTimeout(cmd, resp);
break;
default:
this.pageTimeout = ms;
break;
}
};
/** Single tap. */
GeckoDriver.prototype.singleTap = function(cmd, resp) {
let {id, x, y} = cmd.parameters;
switch (this.context) {
case Context.CHROME:
throw new WebDriverError("Command 'singleTap' is not available in chrome context");
case Context.CONTENT:
this.addFrameCloseListener("tap");
yield this.listener.singleTap({id: id, corx: x, cory: y});
break;
}
};
/**
* An action chain.
*
* @param {Object} value
* A nested array where the inner array represents each event,
* and the outer array represents a collection of events.
*
* @return {number}
* Last touch ID.
*/
GeckoDriver.prototype.actionChain = function(cmd, resp) {
let {chain, nextId} = cmd.parameters;
switch (this.context) {
case Context.CHROME:
if (this.appName != "Firefox") {
// be conservative until this has a use case and is established
// to work as expected on b2g/fennec
throw new WebDriverError(
"Command 'actionChain' is not available in chrome context");
}
let cbs = {};
cbs.onSuccess = val => resp.value = val;
cbs.onError = err => { throw err };
let win = this.getCurrentWindow();
let elm = this.curBrowser.elementManager;
this.actions.dispatchActions(chain, nextId, win, elm, cbs);
break;
case Context.CONTENT:
this.addFrameCloseListener("action chain");
resp.value = yield this.listener.actionChain({chain: chain, nextId: nextId});
break;
}
};
/**
* A multi-action chain.
*
* @param {Object} value
* A nested array where the inner array represents eache vent,
* the middle array represents a collection of events for each
* finger, and the outer array represents all fingers.
*/
GeckoDriver.prototype.multiAction = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
throw new WebDriverError("Command 'multiAction' is not available in chrome context");
case Context.CONTENT:
this.addFrameCloseListener("multi action chain");
yield this.listener.multiAction(
{value: cmd.parameters.value, maxlen: cmd.parameters.max_len});
break;
}
};
/**
* Find an element using the indicated search strategy.
*
* @param {string} using
* Indicates which search method to use.
* @param {string} value
* Value the client is looking for.
*/
GeckoDriver.prototype.findElement = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
resp.value = yield new Promise((resolve, reject) => {
let win = this.getCurrentWindow();
this.curBrowser.elementManager.find(
win,
cmd.parameters,
this.searchTimeout,
false /* all */,
resolve,
reject);
}).then(null, e => { throw e; });
break;
case Context.CONTENT:
resp.value = yield this.listener.findElementContent({
value: cmd.parameters.value,
using: cmd.parameters.using,
element: cmd.parameters.element,
searchTimeout: this.searchTimeout});
break;
}
};
/**
* Find element using the indicated search strategy starting from a
* known element. Used for WebDriver Compatibility only.
*
* @param {string} using
* Indicates which search method to use.
* @param {string} value
* Value the client is looking for.
* @param {string} id
* Value of the element to start from.
*/
GeckoDriver.prototype.findChildElement = function(cmd, resp) {
resp.value = yield this.listener.findElementContent({
value: cmd.parameters.value,
using: cmd.parameters.using,
element: cmd.parameters.id,
searchTimeout: this.searchTimeout});
};
/**
* Find elements using the indicated search strategy.
*
* @param {string} using
* Indicates which search method to use.
* @param {string} value
* Value the client is looking for.
*/
GeckoDriver.prototype.findElements = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
resp.value = yield new Promise((resolve, reject) => {
let win = this.getCurrentWindow();
this.curBrowser.elementManager.find(
win,
cmd.parameters,
this.searchTimeout,
true /* all */,
resolve,
reject);
}).then(null, e => { throw new NoSuchElementError(e.message); });
break;
case Context.CONTENT:
resp.value = yield this.listener.findElementsContent({
value: cmd.parameters.value,
using: cmd.parameters.using,
element: cmd.parameters.element,
searchTimeout: this.searchTimeout});
break;
}
};
/**
* Find elements using the indicated search strategy starting from a
* known element. Used for WebDriver Compatibility only.
*
* @param {string} using
* Indicates which search method to use.
* @param {string} value
* Value the client is looking for.
* @param {string} id
* Value of the element to start from.
*/
GeckoDriver.prototype.findChildElements = function(cmd, resp) {
resp.value = yield this.listener.findElementsContent({
value: cmd.parameters.value,
using: cmd.parameters.using,
element: cmd.parameters.id,
searchTimeout: this.searchTimeout});
};
/** Return the active element on the page. */
GeckoDriver.prototype.getActiveElement = function(cmd, resp) {
resp.value = yield this.listener.getActiveElement();
};
/**
* Send click event to element.
*
* @param {string} id
* Reference ID to the element that will be clicked.
*/
GeckoDriver.prototype.clickElement = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
// click atom fails, fall back to click() action
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
el.click();
break;
case Context.CONTENT:
// We need to protect against the click causing an OOP frame to close.
// This fires the mozbrowserclose event when it closes so we need to
// listen for it and then just send an error back. The person making the
// call should be aware something isnt right and handle accordingly
this.addFrameCloseListener("click");
yield this.listener.clickElement(id);
break;
}
};
/**
* Get a given attribute of an element.
*
* @param {string} id
* Reference ID to the element that will be inspected.
* @param {string} name
* Name of the attribute to retrieve.
*/
GeckoDriver.prototype.getElementAttribute = function(cmd, resp) {
let {id, name} = cmd.parameters;
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
resp.value = utils.getElementAttribute(el, name);
break;
case Context.CONTENT:
resp.value = yield this.listener.getElementAttribute(id, name);
break;
}
};
/**
* Get the text of an element, if any. Includes the text of all child
* elements.
*
* @param {string} id
* Reference ID to the element that will be inspected.
*/
GeckoDriver.prototype.getElementText = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
// for chrome, we look at text nodes, and any node with a "label" field
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
let lines = [];
this.getVisibleText(el, lines);
resp.value = lines.join("\n");
break;
case Context.CONTENT:
resp.value = yield this.listener.getElementText(id);
break;
}
};
/**
* Get the tag name of the element.
*
* @param {string} id
* Reference ID to the element that will be inspected.
*/
GeckoDriver.prototype.getElementTagName = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
resp.value = el.tagName.toLowerCase();
break;
case Context.CONTENT:
resp.value = yield this.listener.getElementTagName(id);
break;
}
};
/**
* Check if element is displayed.
*
* @param {string} id
* Reference ID to the element that will be inspected.
*/
GeckoDriver.prototype.isElementDisplayed = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
resp.value = utils.isElementDisplayed(el);
break;
case Context.CONTENT:
resp.value = yield this.listener.isElementDisplayed({id: id});
break;
}
};
/**
* Return the property of the computed style of an element.
*
* @param {string} id
* Reference ID to the element that will be checked.
* @param {string} propertyName
* CSS rule that is being requested.
*/
GeckoDriver.prototype.getElementValueOfCssProperty = function(cmd, resp) {
let {id, propertyName: prop} = cmd.parameters;
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
let sty = win.document.defaultView.getComputedStyle(el, null);
resp.value = sty.getPropertyValue(prop);
break;
case Context.CONTENT:
resp.value = yield this.listener.getElementValueOfCssProperty(
{id: id, propertyName: prop});
break;
}
};
/**
* Submit a form on a content page by either using form or element in
* a form.
*
* @param {string} id
* Reference to the elemen that will be checked.
*/
GeckoDriver.prototype.submitElement = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
throw new WebDriverError(
"Command 'submitElement' is not available in chrome context");
case Context.CONTENT:
yield this.listener.submitElement({id: cmd.parameters.id});
break;
}
};
/**
* Check if element is enabled.
*
* @param {string} id
* Reference ID to the element that will be checked.
*/
GeckoDriver.prototype.isElementEnabled = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
// Selenium atom doesn't quite work here
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
resp.value = !(!!el.disabled);
break;
case Context.CONTENT:
resp.value = yield this.listener.isElementEnabled(id);
break;
}
},
/**
* Check if element is selected.
*
* @param {string} id
* Reference ID to the element that will be checked.
*/
GeckoDriver.prototype.isElementSelected = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
// Selenium atom doesn't quite work here
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
if (typeof el.checked != "undefined") {
resp.value = !!el.checked;
} else if (typeof el.selected != "undefined") {
resp.value = !!el.selected;
} else {
resp.value = true;
}
break;
case Context.CONTENT:
resp.value = yield this.listener.isElementSelected({id: id});
break;
}
};
GeckoDriver.prototype.getElementSize = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
let rect = el.getBoundingClientRect();
resp.value = {width: rect.width, height: rect.height};
break;
case Context.CONTENT:
resp.value = yield this.listener.getElementSize(id);
break;
}
};
GeckoDriver.prototype.getElementRect = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
let rect = el.getBoundingClientRect();
resp.value = {
x: rect.x + win.pageXOffset,
y: rect.y + win.pageYOffset,
width: rect.width,
height: rect.height
};
break;
case Context.CONTENT:
resp.value = yield this.listener.getElementRect(id);
break;
}
};
/**
* Send key presses to element after focusing on it.
*
* @param {string} id
* Reference ID to the element that will be checked.
* @param {string} value
* Value to send to the element.
*/
GeckoDriver.prototype.sendKeysToElement = function(cmd, resp) {
let {id, value} = cmd.parameters;
if (!value) {
throw new IllegalArgumentError(`Expected character sequence: ${value}`);
}
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
utils.sendKeysToElement(
win,
el,
value,
() => {},
e => { throw e; },
cmd.id,
true /* ignore visibility check */);
break;
case Context.CONTENT:
let err;
let listener = function(msg) {
this.mm.removeMessageListener("Marionette:setElementValue", listener);
let val = msg.data.value;
let el = msg.objects.element;
let win = this.getCurrentWindow();
if (el.type == "file") {
Cu.importGlobalProperties(["File"]);
let fs = Array.prototype.slice.call(el.files);
let file;
try {
file = new File(val);
} catch (e) {
err = new IllegalArgumentError(`File not found: ${val}`);
}
fs.push(file);
el.mozSetFileArray(fs);
} else {
el.value = val;
}
}.bind(this);
this.mm.addMessageListener("Marionette:setElementValue", listener);
yield this.listener.sendKeysToElement({id: id, value: value});
this.mm.removeMessageListener("Marionette:setElementValue", listener);
if (err) {
throw err;
}
break;
}
};
/** Sets the test name. The test name is used for logging purposes. */
GeckoDriver.prototype.setTestName = function(cmd, resp) {
let val = cmd.parameters.value;
this.testName = val;
yield this.listener.setTestName({value: val});
};
/**
* Clear the text of an element.
*
* @param {string} id
* Reference ID to the element that will be cleared.
*/
GeckoDriver.prototype.clearElement = function(cmd, resp) {
let id = cmd.parameters.id;
switch (this.context) {
case Context.CHROME:
// the selenium atom doesn't work here
let win = this.getCurrentWindow();
let el = this.curBrowser.elementManager.getKnownElement(id, win);
if (el.nodeName == "textbox") {
el.value = "";
} else if (el.nodeName == "checkbox") {
el.checked = false;
}
break;
case Context.CONTENT:
yield this.listener.clearElement({id: id});
break;
}
};
/**
* Get an element's location on the page.
*
* The returned point will contain the x and y coordinates of the
* top left-hand corner of the given element. The point (0,0)
* refers to the upper-left corner of the document.
*
* @return {Object.<string, number>}
* A point containing X and Y coordinates as properties.
*/
GeckoDriver.prototype.getElementLocation = function(cmd, resp) {
resp.value = yield this.listener.getElementLocation(
{id: cmd.parameters.id});
};
/** Add a cookie to the document. */
GeckoDriver.prototype.addCookie = function(cmd, resp) {
yield this.listener.addCookie({cookie: cmd.parameters.cookie});
};
/**
* Get all the cookies for the current domain.
*
* This is the equivalent of calling {@code document.cookie} and parsing
* the result.
*/
GeckoDriver.prototype.getCookies = function(cmd, resp) {
resp.value = yield this.listener.getCookies();
};
/** Delete all cookies that are visible to a document. */
GeckoDriver.prototype.deleteAllCookies = function(cmd, resp) {
yield this.listener.deleteAllCookies();
};
/** Delete a cookie by name. */
GeckoDriver.prototype.deleteCookie = function(cmd, resp) {
yield this.listener.deleteCookie({name: cmd.parameters.name});
};
/**
* Close the current window, ending the session if it's the last
* window currently open.
*
* On B2G this method is a noop and will return immediately.
*/
GeckoDriver.prototype.close = function(cmd, resp) {
// can't close windows on B2G
if (this.appName == "B2G") {
return;
}
let nwins = 0;
let winEn = this.getWinEnumerator();
while (winEn.hasMoreElements()) {
let win = winEn.getNext();
// count both windows and tabs
if (win.gBrowser) {
nwins += win.gBrowser.browsers.length;
} else {
nwins++;
}
}
// if there is only 1 window left, delete the session
if (nwins == 1) {
this.sessionTearDown();
return;
}
try {
if (this.mm != globalMessageManager) {
this.mm.removeDelayedFrameScript(FRAME_SCRIPT);
}
if (this.curBrowser.tab) {
this.curBrowser.closeTab();
} else {
this.getCurrentWindow().close();
}
} catch (e) {
throw new UnknownError(`Could not close window: ${e.message}`);
}
};
/**
* Close the currently selected chrome window, ending the session if it's the last
* window currently open.
*
* On B2G this method is a noop and will return immediately.
*/
GeckoDriver.prototype.closeChromeWindow = function(cmd, resp) {
// can't close windows on B2G
if (this.appName == "B2G") {
return;
}
// Get the total number of windows
let nwins = 0;
let winEn = this.getWinEnumerator();
while (winEn.hasMoreElements()) {
nwins++;
winEn.getNext();
}
// if there is only 1 window left, delete the session
if (nwins == 1) {
this.sessionTearDown();
return;
}
try {
this.mm.removeDelayedFrameScript(FRAME_SCRIPT);
this.getCurrentWindow().close();
} catch (e) {
throw new UnknownError(`Could not close window: ${e.message}`);
}
};
/**
* Deletes the session.
*
* If it is a desktop environment, it will close all listeners.
*
* If it is a B2G environment, it will make the main content listener
* sleep, and close all other listeners. The main content listener
* persists after disconnect (it's the homescreen), and can safely
* be reused.
*/
GeckoDriver.prototype.sessionTearDown = function(cmd, resp) {
if (this.curBrowser != null) {
if (this.appName == "B2G") {
globalMessageManager.broadcastAsyncMessage(
"Marionette:sleepSession" + this.curBrowser.mainContentId, {});
this.curBrowser.knownFrames.splice(
this.curBrowser.knownFrames.indexOf(this.curBrowser.mainContentId), 1);
} else {
// don't set this pref for B2G since the framescript can be safely reused
Services.prefs.setBoolPref("marionette.contentListener", false);
}
// delete session in each frame in each browser
for (let win in this.browsers) {
let browser = this.browsers[win];
for (let i in browser.knownFrames) {
globalMessageManager.broadcastAsyncMessage(
"Marionette:deleteSession" + browser.knownFrames[i], {});
}
}
let winEn = this.getWinEnumerator();
while (winEn.hasMoreElements()) {
winEn.getNext().messageManager.removeDelayedFrameScript(FRAME_SCRIPT);
}
this.curBrowser.frameManager.removeSpecialPowers();
this.curBrowser.frameManager.removeMessageManagerListeners(
globalMessageManager);
}
this.switchToGlobalMessageManager();
// reset frame to the top-most frame
this.curFrame = null;
if (this.mainFrame) {
this.mainFrame.focus();
}
this.sessionId = null;
this.deleteFile("marionetteChromeScripts");
this.deleteFile("marionetteContentScripts");
if (this.observing !== null) {
for (let topic in this.observing) {
Services.obs.removeObserver(this.observing[topic], topic);
}
this.observing = null;
}
};
/**
* Processes the "deleteSession" request from the client by tearing down
* the session and responding "ok".
*/
GeckoDriver.prototype.deleteSession = function(cmd, resp) {
this.sessionTearDown();
};
/** Returns the current status of the Application Cache. */
GeckoDriver.prototype.getAppCacheStatus = function(cmd, resp) {
resp.value = yield this.listener.getAppCacheStatus();
};
GeckoDriver.prototype.importScript = function(cmd, resp) {
let script = cmd.parameters.script;
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let result = {};
let data = converter.convertToByteArray(cmd.parameters.script, result);
let ch = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
ch.init(ch.MD5);
ch.update(data, data.length);
let hash = ch.finish(true);
// return if we've already imported this script
if (this.importedScriptHashes[this.context].indexOf(hash) > -1) {
return;
}
this.importedScriptHashes[this.context].push(hash);
switch (this.context) {
case Context.CHROME:
let file;
if (this.importedScripts.exists()) {
file = FileUtils.openFileOutputStream(this.importedScripts,
FileUtils.MODE_APPEND | FileUtils.MODE_WRONLY);
} else {
// the permission bits here don't actually get set (bug 804563)
this.importedScripts.createUnique(
Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
file = FileUtils.openFileOutputStream(this.importedScripts,
FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE);
this.importedScripts.permissions = parseInt("0666", 8);
}
file.write(script, script.length);
file.close();
break;
case Context.CONTENT:
yield this.listener.importScript({script: script});
break;
}
};
GeckoDriver.prototype.clearImportedScripts = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
this.deleteFile("marionetteChromeScripts");
break;
case Context.CONTENT:
this.deleteFile("marionetteContentScripts");
break;
}
};
/**
* Takes a screenshot of a web element, current frame, or viewport.
*
* The screen capture is returned as a lossless PNG image encoded as
* a base 64 string.
*
* If called in the content context, the <code>id</code> argument is not null
* and refers to a present and visible web element's ID, the capture area
* will be limited to the bounding box of that element. Otherwise, the
* capture area will be the bounding box of the current frame.
*
* If called in the chrome context, the screenshot will always represent the
* entire viewport.
*
* @param {string} id
* Reference to a web element.
* @param {string} highlights
* List of web elements to highlight.
*
* @return {string}
* PNG image encoded as base64 encoded string.
*/
GeckoDriver.prototype.takeScreenshot = function(cmd, resp) {
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
let doc;
if (this.appName == "B2G") {
doc = win.document.body;
} else {
doc = win.document.getElementsByTagName("window")[0];
}
let docRect = doc.getBoundingClientRect();
let width = docRect.width;
let height = docRect.height;
// Convert width and height from CSS pixels (potentially fractional)
// to device pixels (integer).
let scale = win.devicePixelRatio;
canvas.setAttribute("width", Math.round(width * scale));
canvas.setAttribute("height", Math.round(height * scale));
let context = canvas.getContext("2d");
let flags;
if (this.appName == "B2G") {
flags =
context.DRAWWINDOW_DRAW_CARET |
context.DRAWWINDOW_DRAW_VIEW |
context.DRAWWINDOW_USE_WIDGET_LAYERS;
} else {
// Bug 1075168: CanvasRenderingContext2D image is distorted
// when using certain flags in chrome context.
flags =
context.DRAWWINDOW_DRAW_VIEW |
context.DRAWWINDOW_USE_WIDGET_LAYERS;
}
context.scale(scale, scale);
context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags);
let dataUrl = canvas.toDataURL("image/png", "");
let data = dataUrl.substring(dataUrl.indexOf(",") + 1);
resp.value = data;
break;
case Context.CONTENT:
resp.value = yield this.listener.takeScreenshot({
id: cmd.parameters.id,
highlights: cmd.parameters.highlights,
full: cmd.parameters.full});
break;
}
};
/**
* Get the current browser orientation.
*
* Will return one of the valid primary orientation values
* portrait-primary, landscape-primary, portrait-secondary, or
* landscape-secondary.
*/
GeckoDriver.prototype.getScreenOrientation = function(cmd, resp) {
resp.value = this.getCurrentWindow().screen.mozOrientation;
};
/**
* Set the current browser orientation.
*
* The supplied orientation should be given as one of the valid
* orientation values. If the orientation is unknown, an error will
* be raised.
*
* Valid orientations are "portrait" and "landscape", which fall
* back to "portrait-primary" and "landscape-primary" respectively,
* and "portrait-secondary" as well as "landscape-secondary".
*/
GeckoDriver.prototype.setScreenOrientation = function(cmd, resp) {
const ors = [
"portrait", "landscape",
"portrait-primary", "landscape-primary",
"portrait-secondary", "landscape-secondary"
];
let or = String(cmd.parameters.orientation);
let mozOr = or.toLowerCase();
if (ors.indexOf(mozOr) < 0) {
throw new WebDriverError(`Unknown screen orientation: ${or}`);
}
let win = this.getCurrentWindow();
if (!win.screen.mozLockOrientation(mozOr)) {
throw new WebDriverError(`Unable to set screen orientation: ${or}`);
}
};
/**
* Get the size of the browser window currently in focus.
*
* Will return the current browser window size in pixels. Refers to
* window outerWidth and outerHeight values, which include scroll bars,
* title bars, etc.
*/
GeckoDriver.prototype.getWindowSize = function(cmd, resp) {
let win = this.getCurrentWindow();
resp.value = {width: win.outerWidth, height: win.outerHeight};
};
/**
* Set the size of the browser window currently in focus.
*
* Not supported on B2G. The supplied width and height values refer to
* the window outerWidth and outerHeight values, which include scroll
* bars, title bars, etc.
*
* An error will be returned if the requested window size would result
* in the window being in the maximized state.
*/
GeckoDriver.prototype.setWindowSize = function(cmd, resp) {
if (this.appName !== "Firefox") {
throw new UnsupportedOperationError("Not supported on mobile");
}
let width = parseInt(cmd.parameters.width);
let height = parseInt(cmd.parameters.height);
let win = this.getCurrentWindow();
if (width >= win.screen.availWidth && height >= win.screen.availHeight) {
throw new UnsupportedOperationError("Invalid requested size, cannot maximize");
}
win.resizeTo(width, height);
};
/**
* Maximizes the user agent window as if the user pressed the maximise
* button.
*
* Not Supported on B2G or Fennec.
*/
GeckoDriver.prototype.maximizeWindow = function(cmd, resp) {
if (this.appName != "Firefox") {
throw new UnsupportedOperationError("Not supported for mobile");
}
let win = this.getCurrentWindow();
win.moveTo(0,0);
win.resizeTo(win.screen.availWidth, win.screen.availHeight);
};
/**
* Dismisses a currently displayed tab modal, or returns no such alert if
* no modal is displayed.
*/
GeckoDriver.prototype.dismissDialog = function(cmd, resp) {
if (!this.dialog) {
throw new NoAlertOpenError(
"No tab modal was open when attempting to dismiss the dialog");
}
let {button0, button1} = this.dialog.ui;
(button1 ? button1 : button0).click();
this.dialog = null;
};
/**
* Accepts a currently displayed tab modal, or returns no such alert if
* no modal is displayed.
*/
GeckoDriver.prototype.acceptDialog = function(cmd, resp) {
if (!this.dialog) {
throw new NoAlertOpenError(
"No tab modal was open when attempting to accept the dialog");
}
let {button0} = this.dialog.ui;
button0.click();
this.dialog = null;
};
/**
* Returns the message shown in a currently displayed modal, or returns a no such
* alert error if no modal is currently displayed.
*/
GeckoDriver.prototype.getTextFromDialog = function(cmd, resp) {
if (!this.dialog) {
throw new NoAlertOpenError(
"No tab modal was open when attempting to get the dialog text");
}
let {infoBody} = this.dialog.ui;
resp.value = infoBody.textContent;
};
/**
* Sends keys to the input field of a currently displayed modal, or
* returns a no such alert error if no modal is currently displayed. If
* a tab modal is currently displayed but has no means for text input,
* an element not visible error is returned.
*/
GeckoDriver.prototype.sendKeysToDialog = function(cmd, resp) {
if (!this.dialog) {
throw new NoAlertOpenError(
"No tab modal was open when attempting to send keys to a dialog");
}
// see toolkit/components/prompts/content/commonDialog.js
let {loginContainer, loginTextbox} = this.dialog.ui;
if (loginContainer.hidden) {
throw new ElementNotVisibleError("This prompt does not accept text input");
}
let win = this.dialog.window ? this.dialog.window : this.getCurrentWindow();
utils.sendKeysToElement(
win,
loginTextbox,
cmd.parameters.value,
() => {},
e => { throw e; },
this.command_id,
true /* ignore visibility check */);
};
/**
* Helper function to convert an outerWindowID into a UID that Marionette
* tracks.
*/
GeckoDriver.prototype.generateFrameId = function(id) {
let uid = id + (this.appName == "B2G" ? "-b2g" : "");
return uid;
};
/** Receives all messages from content messageManager. */
GeckoDriver.prototype.receiveMessage = function(message) {
// we need to just check if we need to remove the mozbrowserclose listener
if (this.mozBrowserClose !== null) {
let win = this.getCurrentWindow();
win.removeEventListener("mozbrowserclose", this.mozBrowserClose, true);
this.mozBrowserClose = null;
}
switch (message.name) {
case "Marionette:log":
// log server-side messages
logger.info(message.json.message);
break;
case "Marionette:shareData":
// log messages from tests
if (message.json.log) {
this.marionetteLog.addLogs(message.json.log);
}
break;
case "Marionette:runEmulatorCmd":
case "Marionette:runEmulatorShell":
this.emulator.send(message.json);
break;
case "Marionette:switchToModalOrigin":
this.curBrowser.frameManager.switchToModalOrigin(message);
this.mm = this.curBrowser.frameManager
.currentRemoteFrame.messageManager.get();
break;
case "Marionette:switchedToFrame":
if (message.json.restorePrevious) {
this.currentFrameElement = this.previousFrameElement;
} else {
// we don't arbitrarily save previousFrameElement, since
// we allow frame switching after modals appear, which would
// override this value and we'd lose our reference
if (message.json.storePrevious) {
this.previousFrameElement = this.currentFrameElement;
}
this.currentFrameElement = message.json.frameValue;
}
break;
case "Marionette:getVisibleCookies":
let [currentPath, host] = message.json.value;
let isForCurrentPath = path => currentPath.indexOf(path) != -1;
let results = [];
let en = cookieManager.enumerator;
while (en.hasMoreElements()) {
let cookie = en.getNext().QueryInterface(Ci.nsICookie);
// take the hostname and progressively shorten
let hostname = host;
do {
if ((cookie.host == "." + hostname || cookie.host == hostname) &&
isForCurrentPath(cookie.path)) {
results.push({
"name": cookie.name,
"value": cookie.value,
"path": cookie.path,
"host": cookie.host,
"secure": cookie.isSecure,
"expiry": cookie.expires
});
break;
}
hostname = hostname.replace(/^.*?\./, "");
} while (hostname.indexOf(".") != -1);
}
return results;
case "Marionette:addCookie":
let cookieToAdd = message.json.value;
Services.cookies.add(
cookieToAdd.domain,
cookieToAdd.path,
cookieToAdd.name,
cookieToAdd.value,
cookieToAdd.secure,
false,
false,
cookieToAdd.expiry);
return true;
case "Marionette:deleteCookie":
let cookieToDelete = message.json.value;
cookieManager.remove(
cookieToDelete.host,
cookieToDelete.name,
cookieToDelete.path,
false);
return true;
case "Marionette:emitTouchEvent":
globalMessageManager.broadcastAsyncMessage(
"MarionetteMainListener:emitTouchEvent", message.json);
break;
case "Marionette:register":
let wid = message.json.value;
let be = message.target;
let rv = this.registerBrowser(wid, be);
return rv;
case "Marionette:listenersAttached":
if (message.json.listenerId === this.curBrowser.curFrameId) {
// If remoteness gets updated we need to call newSession. In the case
// of desktop this just sets up a small amount of state that doesn't
// change over the course of a session.
let newSessionValues = {
B2G: (this.appName == "B2G"),
raisesAccessibilityExceptions:
this.sessionCapabilities.raisesAccessibilityExceptions
};
this.sendAsync("newSession", newSessionValues);
this.curBrowser.flushPendingCommands();
}
break;
}
};
GeckoDriver.prototype.responseCompleted = function () {
if (this.curBrowser !== null) {
this.curBrowser.pendingCommands = [];
}
};
GeckoDriver.prototype.commands = {
"getMarionetteID": GeckoDriver.prototype.getMarionetteID,
"sayHello": GeckoDriver.prototype.sayHello,
"newSession": GeckoDriver.prototype.newSession,
"getSessionCapabilities": GeckoDriver.prototype.getSessionCapabilities,
"log": GeckoDriver.prototype.log,
"getLogs": GeckoDriver.prototype.getLogs,
"setContext": GeckoDriver.prototype.setContext,
"getContext": GeckoDriver.prototype.getContext,
"executeScript": GeckoDriver.prototype.execute,
"setScriptTimeout": GeckoDriver.prototype.setScriptTimeout,
"timeouts": GeckoDriver.prototype.timeouts,
"singleTap": GeckoDriver.prototype.singleTap,
"actionChain": GeckoDriver.prototype.actionChain,
"multiAction": GeckoDriver.prototype.multiAction,
"executeAsyncScript": GeckoDriver.prototype.executeWithCallback,
"executeJSScript": GeckoDriver.prototype.executeJSScript,
"setSearchTimeout": GeckoDriver.prototype.setSearchTimeout,
"findElement": GeckoDriver.prototype.findElement,
"findChildElement": GeckoDriver.prototype.findChildElements, // Needed for WebDriver compat
"findElements": GeckoDriver.prototype.findElements,
"findChildElements":GeckoDriver.prototype.findChildElements, // Needed for WebDriver compat
"clickElement": GeckoDriver.prototype.clickElement,
"getElementAttribute": GeckoDriver.prototype.getElementAttribute,
"getElementText": GeckoDriver.prototype.getElementText,
"getElementTagName": GeckoDriver.prototype.getElementTagName,
"isElementDisplayed": GeckoDriver.prototype.isElementDisplayed,
"getElementValueOfCssProperty": GeckoDriver.prototype.getElementValueOfCssProperty,
"submitElement": GeckoDriver.prototype.submitElement,
"getElementSize": GeckoDriver.prototype.getElementSize, //deprecated
"getElementRect": GeckoDriver.prototype.getElementRect,
"isElementEnabled": GeckoDriver.prototype.isElementEnabled,
"isElementSelected": GeckoDriver.prototype.isElementSelected,
"sendKeysToElement": GeckoDriver.prototype.sendKeysToElement,
"getElementLocation": GeckoDriver.prototype.getElementLocation, // deprecated
"getElementPosition": GeckoDriver.prototype.getElementLocation, // deprecated
"clearElement": GeckoDriver.prototype.clearElement,
"getTitle": GeckoDriver.prototype.getTitle,
"getWindowType": GeckoDriver.prototype.getWindowType,
"getPageSource": GeckoDriver.prototype.getPageSource,
"get": GeckoDriver.prototype.get,
"goUrl": GeckoDriver.prototype.get, // deprecated
"getCurrentUrl": GeckoDriver.prototype.getCurrentUrl,
"getUrl": GeckoDriver.prototype.getCurrentUrl, // deprecated
"goBack": GeckoDriver.prototype.goBack,
"goForward": GeckoDriver.prototype.goForward,
"refresh": GeckoDriver.prototype.refresh,
"getWindowHandle": GeckoDriver.prototype.getWindowHandle,
"getCurrentWindowHandle": GeckoDriver.prototype.getWindowHandle, // Selenium 2 compat
"getChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle,
"getCurrentChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle,
"getWindow": GeckoDriver.prototype.getWindowHandle, // deprecated
"getWindowHandles": GeckoDriver.prototype.getWindowHandles,
"getChromeWindowHandles": GeckoDriver.prototype.getChromeWindowHandles,
"getCurrentWindowHandles": GeckoDriver.prototype.getWindowHandles, // Selenium 2 compat
"getWindows": GeckoDriver.prototype.getWindowHandles, // deprecated
"getWindowPosition": GeckoDriver.prototype.getWindowPosition,
"setWindowPosition": GeckoDriver.prototype.setWindowPosition,
"getActiveFrame": GeckoDriver.prototype.getActiveFrame,
"switchToFrame": GeckoDriver.prototype.switchToFrame,
"switchToWindow": GeckoDriver.prototype.switchToWindow,
"deleteSession": GeckoDriver.prototype.deleteSession,
"importScript": GeckoDriver.prototype.importScript,
"clearImportedScripts": GeckoDriver.prototype.clearImportedScripts,
"getAppCacheStatus": GeckoDriver.prototype.getAppCacheStatus,
"close": GeckoDriver.prototype.close,
"closeWindow": GeckoDriver.prototype.close, // deprecated
"closeChromeWindow": GeckoDriver.prototype.closeChromeWindow,
"setTestName": GeckoDriver.prototype.setTestName,
"takeScreenshot": GeckoDriver.prototype.takeScreenshot,
"screenShot": GeckoDriver.prototype.takeScreenshot, // deprecated
"screenshot": GeckoDriver.prototype.takeScreenshot, // Selenium 2 compat
"addCookie": GeckoDriver.prototype.addCookie,
"getCookies": GeckoDriver.prototype.getCookies,
"getAllCookies": GeckoDriver.prototype.getCookies, // deprecated
"deleteAllCookies": GeckoDriver.prototype.deleteAllCookies,
"deleteCookie": GeckoDriver.prototype.deleteCookie,
"getActiveElement": GeckoDriver.prototype.getActiveElement,
"getScreenOrientation": GeckoDriver.prototype.getScreenOrientation,
"setScreenOrientation": GeckoDriver.prototype.setScreenOrientation,
"getWindowSize": GeckoDriver.prototype.getWindowSize,
"setWindowSize": GeckoDriver.prototype.setWindowSize,
"maximizeWindow": GeckoDriver.prototype.maximizeWindow,
"dismissDialog": GeckoDriver.prototype.dismissDialog,
"acceptDialog": GeckoDriver.prototype.acceptDialog,
"getTextFromDialog": GeckoDriver.prototype.getTextFromDialog,
"sendKeysToDialog": GeckoDriver.prototype.sendKeysToDialog
};
/**
* Creates a BrowserObj. BrowserObjs handle interactions with the
* browser, according to the current environment (desktop, b2g, etc.).
*
* @param {nsIDOMWindow} win
* The window whose browser needs to be accessed.
* @param {GeckoDriver} driver
* Reference to the driver the browser is attached to.
*/
let BrowserObj = function(win, driver) {
this.browser = undefined;
this.window = win;
this.driver = driver;
this.knownFrames = [];
this.startPage = "about:blank";
// used in B2G to identify the homescreen content page
this.mainContentId = null;
// used to set curFrameId upon new session
this.newSession = true;
this.elementManager = new ElementManager([NAME, LINK_TEXT, PARTIAL_LINK_TEXT]);
this.setBrowser(win);
// A reference to the tab corresponding to the current window handle, if any.
this.tab = null;
this.pendingCommands = [];
// we should have one FM per BO so that we can handle modals in each Browser
this.frameManager = new FrameManager(driver);
this.frameRegsPending = 0;
// register all message listeners
this.frameManager.addMessageManagerListeners(driver.mm);
this.getIdForBrowser = driver.getIdForBrowser.bind(driver);
this.updateIdForBrowser = driver.updateIdForBrowser.bind(driver);
this._curFrameId = null;
this._browserWasRemote = null;
this._hasRemotenessChange = false;
};
Object.defineProperty(BrowserObj.prototype, "browserForTab", {
get() {
return this.browser.getBrowserForTab(this.tab);
}
});
/**
* The current frame ID is managed per browser element on desktop in
* case the ID needs to be refreshed. The currently selected window is
* identified within BrowserObject by a tab.
*/
Object.defineProperty(BrowserObj.prototype, "curFrameId", {
get() {
let rv = null;
if (this.driver.appName != "Firefox") {
rv = this._curFrameId;
} else if (this.tab) {
rv = this.getIdForBrowser(this.browserForTab);
}
return rv;
},
set(id) {
if (this.driver.appName != "Firefox") {
this._curFrameId = id;
}
}
});
/**
* Retrieves the current tabmodal UI object. According to the browser
* associated with the currently selected tab.
*/
BrowserObj.prototype.getTabModalUI = function() {
let br = this.browserForTab;
if (!br.hasAttribute("tabmodalPromptShowing")) {
return null;
}
// The modal is a direct sibling of the browser element.
// See tabbrowser.xml's getTabModalPromptBox.
let modals = br.parentNode.getElementsByTagNameNS(
XUL_NS, "tabmodalprompt");
return modals[0].ui;
};
/**
* Set the browser if the application is not B2G.
*
* @param {nsIDOMWindow} win
* Current window reference.
*/
BrowserObj.prototype.setBrowser = function(win) {
switch (this.driver.appName) {
case "Firefox":
this.browser = win.gBrowser;
break;
case "Fennec":
this.browser = win.BrowserApp;
break;
case "B2G":
// eideticker (bug 965297) and mochitest (bug 965304)
// compatibility. They only check for the presence of this
// property and should not be in caps if not on a B2G device.
this.driver.sessionCapabilities.b2g = true;
break;
}
};
/** Called when we start a session with this browser. */
BrowserObj.prototype.startSession = function(newSession, win, callback) {
callback(win, newSession);
};
/** Closes current tab. */
BrowserObj.prototype.closeTab = function() {
if (this.browser &&
this.browser.removeTab &&
this.tab != null && (this.driver.appName != "B2G")) {
this.browser.removeTab(this.tab);
}
};
/**
* Opens a tab with given URI.
*
* @param {string} uri
* URI to open.
*/
BrowserObj.prototype.addTab = function(uri) {
return this.browser.addTab(uri, true);
};
/**
* Re-sets this BrowserObject's current tab and updates remoteness tracking.
*/
BrowserObj.prototype.switchToTab = function(ind) {
if (this.browser) {
this.browser.selectTabAtIndex(ind);
this.tab = this.browser.selectedTab;
}
this._browserWasRemote = this.browserForTab.isRemoteBrowser;
this._hasRemotenessChange = false;
};
/**
* Registers a new frame, and sets its current frame id to this frame
* if it is not already assigned, and if a) we already have a session
* or b) we're starting a new session and it is the right start frame.
*
* @param {string} uid
* Frame uid for use by Marionette.
* @param the XUL <browser> that was the target of the originating message.
*/
BrowserObj.prototype.register = function(uid, target) {
let remotenessChange = this.hasRemotenessChange();
if (this.curFrameId === null || remotenessChange) {
if (this.browser) {
// If we're setting up a new session on Firefox, we only process the
// registration for this frame if it belongs to the current tab.
if (!this.tab) {
this.switchToTab(this.browser.selectedIndex);
}
if (target == this.browserForTab) {
this.updateIdForBrowser(this.browserForTab, uid);
this.mainContentId = uid;
}
} else {
this._curFrameId = uid;
this.mainContentId = uid;
}
}
// used to delete sessions
this.knownFrames.push(uid);
return remotenessChange;
};
/**
* When navigating between pages results in changing a browser's
* process, we need to take measures not to lose contact with a listener
* script. This function does the necessary bookkeeping.
*/
BrowserObj.prototype.hasRemotenessChange = function() {
// None of these checks are relevant on b2g or if we don't have a tab yet,
// and may not apply on Fennec.
if (this.driver.appName != "Firefox" || this.tab === null) {
return false;
}
if (this._hasRemotenessChange) {
return true;
}
let currentIsRemote = this.browserForTab.isRemoteBrowser;
this._hasRemotenessChange = this._browserWasRemote !== currentIsRemote;
this._browserWasRemote = currentIsRemote;
return this._hasRemotenessChange;
};
/**
* Flushes any pending commands queued when a remoteness change is being
* processed and mark this remotenessUpdate as complete.
*/
BrowserObj.prototype.flushPendingCommands = function() {
if (!this._hasRemotenessChange) {
return;
}
this._hasRemotenessChange = false;
this.pendingCommands.forEach(cb => cb());
this.pendingCommands = [];
};
/**
* This function intercepts commands interacting with content and queues
* or executes them as needed.
*
* No commands interacting with content are safe to process until
* the new listener script is loaded and registers itself.
* This occurs when a command whose effect is asynchronous (such
* as goBack) results in a remoteness change and new commands
* are subsequently posted to the server.
*/
BrowserObj.prototype.executeWhenReady = function(cb) {
if (this.hasRemotenessChange()) {
this.pendingCommands.push(cb);
} else {
cb();
}
};