summaryrefslogtreecommitdiffstats
path: root/testing/marionette/driver.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/driver.js')
-rw-r--r--testing/marionette/driver.js2907
1 files changed, 2907 insertions, 0 deletions
diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js
new file mode 100644
index 000000000..7ef72d3bb
--- /dev/null
+++ b/testing/marionette/driver.js
@@ -0,0 +1,2907 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+var loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader);
+
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(
+ this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
+
+Cu.import("chrome://marionette/content/accessibility.js");
+Cu.import("chrome://marionette/content/addon.js");
+Cu.import("chrome://marionette/content/assert.js");
+Cu.import("chrome://marionette/content/atom.js");
+Cu.import("chrome://marionette/content/browser.js");
+Cu.import("chrome://marionette/content/capture.js");
+Cu.import("chrome://marionette/content/cert.js");
+Cu.import("chrome://marionette/content/element.js");
+Cu.import("chrome://marionette/content/error.js");
+Cu.import("chrome://marionette/content/evaluate.js");
+Cu.import("chrome://marionette/content/event.js");
+Cu.import("chrome://marionette/content/interaction.js");
+Cu.import("chrome://marionette/content/l10n.js");
+Cu.import("chrome://marionette/content/legacyaction.js");
+Cu.import("chrome://marionette/content/logging.js");
+Cu.import("chrome://marionette/content/modal.js");
+Cu.import("chrome://marionette/content/proxy.js");
+Cu.import("chrome://marionette/content/session.js");
+Cu.import("chrome://marionette/content/simpletest.js");
+
+this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
+
+var FRAME_SCRIPT = "chrome://marionette/content/listener.js";
+const BROWSER_STARTUP_FINISHED = "browser-delayed-startup-finished";
+const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
+const CONTENT_LISTENER_PREF = "marionette.contentListener";
+
+const SUPPORTED_STRATEGIES = new Set([
+ element.Strategy.ClassName,
+ element.Strategy.Selector,
+ element.Strategy.ID,
+ element.Strategy.TagName,
+ element.Strategy.XPath,
+ element.Strategy.Anon,
+ element.Strategy.AnonAttribute,
+]);
+
+const logger = Log.repository.getLogger("Marionette");
+const globalMessageManager = Cc["@mozilla.org/globalmessagemanager;1"]
+ .getService(Ci.nsIMessageBroadcaster);
+
+// 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.
+var 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.
+var 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;
+};
+
+/**
+ * Implements (parts of) the W3C WebDriver protocol. GeckoDriver lives
+ * in chrome space and mediates calls to the message listener of the current
+ * browsing context's content frame message 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 {MarionetteServer} server
+ * The instance of Marionette server.
+ */
+this.GeckoDriver = function (appName, server) {
+ this.appName = appName;
+ this._server = server;
+
+ this.sessionId = null;
+ this.wins = new browser.Windows();
+ this.browsers = {};
+ // points to current browser
+ this.curBrowser = null;
+ // topmost chrome frame
+ this.mainFrame = null;
+ // chrome iframe that currently has focus
+ this.curFrame = null;
+ this.mainContentFrameId = null;
+ this.mozBrowserClose = null;
+ this.currentFrameElement = null;
+ // frame ID of the current remote frame, used for mozbrowserclose events
+ this.oopFrameId = null;
+ this.observing = null;
+ this._browserIds = new WeakMap();
+
+ // The curent context decides if commands should affect chrome- or
+ // content space.
+ this.context = Context.CONTENT;
+
+ this.importedScripts = new evaluate.ScriptStorageService(
+ [Context.CHROME, Context.CONTENT]);
+ this.sandboxes = new Sandboxes(() => this.getCurrentWindow());
+ this.legacyactions = new legacyaction.Chain();
+
+ this.timer = null;
+ this.inactivityTimer = null;
+
+ this.marionetteLog = new logging.ContentLogger();
+ this.testName = null;
+
+ this.capabilities = new session.Capabilities();
+
+ this.mm = globalMessageManager;
+ this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this));
+
+ // always keep weak reference to current dialogue
+ 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);
+};
+
+Object.defineProperty(GeckoDriver.prototype, "a11yChecks", {
+ get: function () {
+ return this.capabilities.get("moz:accessibilityChecks");
+ }
+});
+
+Object.defineProperty(GeckoDriver.prototype, "proxy", {
+ get: function () {
+ return this.capabilities.get("proxy");
+ }
+});
+
+Object.defineProperty(GeckoDriver.prototype, "secureTLS", {
+ get: function () {
+ return !this.capabilities.get("acceptInsecureCerts");
+ }
+});
+
+Object.defineProperty(GeckoDriver.prototype, "timeouts", {
+ get: function () {
+ return this.capabilities.get("timeouts");
+ },
+
+ set: function (newTimeouts) {
+ this.capabilities.set("timeouts", newTimeouts);
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "windowHandles", {
+ get: function () {
+ let hs = [];
+ let winEn = Services.wm.getEnumerator(null);
+
+ while (winEn.hasMoreElements()) {
+ let win = winEn.getNext();
+ let tabBrowser = browser.getTabBrowser(win);
+
+ if (tabBrowser) {
+ tabBrowser.tabs.forEach(tab => {
+ let winId = this.getIdForBrowser(browser.getBrowserForTab(tab));
+ if (winId !== null) {
+ hs.push(winId);
+ }
+ });
+ } else {
+ // For other chrome windows beside the browser window, only add the window itself.
+ hs.push(getOuterWindowId(win));
+ }
+ }
+
+ return hs;
+ },
+});
+
+Object.defineProperty(GeckoDriver.prototype, "chromeWindowHandles", {
+ get : function () {
+ let hs = [];
+ let winEn = Services.wm.getEnumerator(null);
+
+ while (winEn.hasMoreElements()) {
+ hs.push(getOuterWindowId(winEn.getNext()));
+ }
+
+ return hs;
+ },
+});
+
+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 serialisable object, and optionally the current command's ID
+ * when not using the modern dispatching technique.
+ *
+ * @param {string} name
+ * Suffix of the targetted message listener
+ * ({@code Marionette:<suffix>}).
+ * @param {Object=} msg
+ * Optional JSON serialisable object to send to the listener.
+ * @param {number=} commandID
+ * Optional command ID to ensure synchronisity.
+ */
+GeckoDriver.prototype.sendAsync = function (name, data, commandID) {
+ name = "Marionette:" + name;
+ let payload = copy(data);
+
+ // TODO(ato): When proxy.AsyncMessageChannel
+ // is used for all chrome <-> content communication
+ // this can be removed.
+ if (commandID) {
+ payload.command_id = commandID;
+ }
+
+ if (!this.curBrowser.frameManager.currentRemoteFrame) {
+ this.broadcastDelayedAsyncMessage_(name, payload);
+ } else {
+ this.sendTargettedAsyncMessage_(name, payload);
+ }
+};
+
+GeckoDriver.prototype.broadcastDelayedAsyncMessage_ = function (name, payload) {
+ this.curBrowser.executeWhenReady(() => {
+ if (this.curBrowser.curFrameId) {
+ const target = name + this.curBrowser.curFrameId;
+ this.mm.broadcastAsyncMessage(target, payload);
+ } else {
+ throw new NoSuchWindowError(
+ "No such content frame; perhaps the listener was not registered?");
+ }
+ });
+};
+
+GeckoDriver.prototype.sendTargettedAsyncMessage_ = function (name, payload) {
+ const curRemoteFrame = this.curBrowser.frameManager.currentRemoteFrame;
+ const target = name + curRemoteFrame.targetFrameId;
+
+ try {
+ this.mm.sendAsyncMessage(target, payload);
+ } catch (e) {
+ switch (e.result) {
+ case Cr.NS_ERROR_FAILURE:
+ case Cr.NS_ERROR_NOT_INITIALIZED:
+ throw new NoSuchWindowError();
+
+ default:
+ throw new WebDriverError(e);
+ }
+ }
+};
+
+/**
+ * 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;
+ }
+};
+
+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 NoSuchWindowError("The window closed during action: " + action);
+ }
+ };
+ win.addEventListener("mozbrowserclose", this.mozBrowserClose, true);
+};
+
+/**
+ * Create a new browsing context for window and add to known browsers.
+ *
+ * @param {nsIDOMWindow} win
+ * Window for which we will create a browsing context.
+ *
+ * @return {string}
+ * Returns the unique server-assigned ID of the window.
+ */
+GeckoDriver.prototype.addBrowser = function (win) {
+ let bc = new browser.Context(win, this);
+ let winId = getOuterWindowId(win);
+
+ this.browsers[winId] = bc;
+ this.curBrowser = this.browsers[winId];
+ if (!this.wins.has(winId)) {
+ // add this to seenItems so we can guarantee
+ // the user will get winId as this window's id
+ this.wins.set(winId, 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) {
+ let mm = win.window.messageManager;
+ if (mm) {
+ 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.
+ // Child managers may not have child scripts yet (e.g. socialapi), only
+ // count child managers that have children, but only count the top level
+ // children as they are the ones that we expect a response from.
+ if (mm.childCount !== 0) {
+ this.curBrowser.frameRegsPending = 0;
+ for (let i = 0; i < mm.childCount; i++) {
+ if (mm.getChildAt(i).childCount !== 0) {
+ this.curBrowser.frameRegsPending += 1;
+ }
+ }
+ }
+ }
+
+ if (!Preferences.get(CONTENT_LISTENER_PREF) || !isNewSession) {
+ // load listener into the remote frame
+ // and any applicable new frames
+ // opened after this call
+ mm.loadFrameScript(FRAME_SCRIPT, true);
+ Preferences.set(CONTENT_LISTENER_PREF, true);
+ }
+ } else {
+ logger.error(
+ `Could not load listener into content for page ${win.location.href}`);
+ }
+};
+
+/**
+ * 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 (atom.isElementDisplayed(el, this.getCurrentWindow())) {
+ 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);
+ }
+ }
+};
+
+/**
+ * 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 && this.curBrowser.mainContentId !== null;
+ if (mainContent) {
+ this.mainContentFrameId = this.curBrowser.curFrameId;
+ }
+
+ this.wins.set(reg.id, listenerWindow);
+ if (nullPrevious && (this.curBrowser.curFrameId !== null)) {
+ this.sendAsync(
+ "newSession",
+ this.capabilities.toJSON(),
+ this.newSessionCommandId);
+ if (this.curBrowser.isNewSession) {
+ this.newSessionCommandId = null;
+ }
+ }
+
+ return [reg, mainContent, this.capabilities.toJSON()];
+};
+
+GeckoDriver.prototype.registerPromise = function () {
+ const li = "Marionette:register";
+
+ return new Promise(resolve => {
+ let 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;
+ };
+ this.mm.addMessageListener(li, cb);
+ });
+};
+
+GeckoDriver.prototype.listeningPromise = function () {
+ const li = "Marionette:listenersAttached";
+ return new Promise(resolve => {
+ let cb = () => {
+ this.mm.removeMessageListener(li, cb);
+ resolve();
+ };
+ this.mm.addMessageListener(li, cb);
+ });
+};
+
+/** Create a new session. */
+GeckoDriver.prototype.newSession = function* (cmd, resp) {
+ if (this.sessionId) {
+ throw new SessionNotCreatedError("Maximum number of active sessions");
+ }
+
+ this.sessionId = cmd.parameters.sessionId ||
+ cmd.parameters.session_id ||
+ element.generateUUID();
+ this.newSessionCommandId = cmd.id;
+
+ try {
+ this.capabilities = session.Capabilities.fromJSON(
+ cmd.parameters.capabilities, {merge: true});
+ logger.config("Matched capabilities: " +
+ JSON.stringify(this.capabilities));
+ } catch (e) {
+ throw new SessionNotCreatedError(e);
+ }
+
+ if (!this.secureTLS) {
+ logger.warn("TLS certificate errors will be ignored for this session");
+ let acceptAllCerts = new cert.InsecureSweepingOverride();
+ cert.installOverride(acceptAllCerts);
+ }
+
+ if (this.proxy.init()) {
+ logger.info("Proxy settings initialised: " + JSON.stringify(this.proxy));
+ }
+
+ // If we are testing accessibility with marionette, start a11y service in
+ // chrome first. This will ensure that we do not have any content-only
+ // services hanging around.
+ if (this.a11yChecks && accessibility.service) {
+ logger.info("Preemptively starting accessibility service in Chrome");
+ }
+
+ 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 = Preferences.get(CLICK_TO_START_PREF);
+ 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 (!Preferences.get(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;
+
+ if (this.curBrowser.tab) {
+ browser.getBrowserForTab(this.curBrowser.tab).focus();
+ }
+
+ return {
+ sessionId: this.sessionId,
+ capabilities: this.capabilities,
+ };
+};
+
+/**
+ * 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.body.capabilities = this.capabilities;
+};
+
+/**
+ * Log message. Accepts user defined log-level.
+ *
+ * @param {string} value
+ * Log message.
+ * @param {string} level
+ * Arbitrary log level.
+ */
+GeckoDriver.prototype.log = function (cmd, resp) {
+ // if level is null, we want to use ContentLogger#send's default
+ this.marionetteLog.log(
+ cmd.parameters.value,
+ cmd.parameters.level || undefined);
+};
+
+/** Return all logged messages. */
+GeckoDriver.prototype.getLogs = function (cmd, resp) {
+ resp.body = this.marionetteLog.get();
+};
+
+/**
+ * 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.body.value = this.context.toString();
+};
+
+/**
+ * Executes a JavaScript function in the context of the current browsing
+ * context, if in content space, or in chrome space otherwise, and returns
+ * the return value of the function.
+ *
+ * It is important to note that if the {@code sandboxName} parameter
+ * is left undefined, the script will be evaluated in a mutable sandbox,
+ * causing any change it makes on the global state of the document to have
+ * lasting side-effects.
+ *
+ * @param {string} script
+ * Script to evaluate as a function body.
+ * @param {Array.<(string|boolean|number|object|WebElement)>} args
+ * Arguments exposed to the script in {@code arguments}. The array
+ * items must be serialisable to the WebDriver protocol.
+ * @param {number} scriptTimeout
+ * Duration in milliseconds of when to interrupt and abort the
+ * script evaluation.
+ * @param {string=} sandbox
+ * Name of the sandbox to evaluate the script in. The sandbox is
+ * cached for later re-use on the same Window object if
+ * {@code newSandbox} is false. If he parameter is undefined,
+ * the script is evaluated in a mutable sandbox. If the parameter
+ * is "system", it will be evaluted in a sandbox with elevated system
+ * privileges, equivalent to chrome space.
+ * @param {boolean=} newSandbox
+ * Forces the script to be evaluated in a fresh sandbox. Note that if
+ * it is undefined, the script will normally be evaluted in a fresh
+ * sandbox.
+ * @param {string=} filename
+ * Filename of the client's program where this script is evaluated.
+ * @param {number=} line
+ * Line in the client's program where this script is evaluated.
+ * @param {boolean=} debug_script
+ * Attach an {@code onerror} event handler on the Window object.
+ * It does not differentiate content errors from chrome errors.
+ * @param {boolean=} directInject
+ * Evaluate the script without wrapping it in a function.
+ *
+ * @return {(string|boolean|number|object|WebElement)}
+ * Return value from the script, or null which signifies either the
+ * JavaScript notion of null or undefined.
+ *
+ * @throws ScriptTimeoutError
+ * If the script was interrupted due to reaching the {@code
+ * scriptTimeout} or default timeout.
+ * @throws JavaScriptError
+ * If an Error was thrown whilst evaluating the script.
+ */
+GeckoDriver.prototype.executeScript = function*(cmd, resp) {
+ let {script, args, scriptTimeout} = cmd.parameters;
+ scriptTimeout = scriptTimeout || this.timeouts.script;
+
+ let opts = {
+ sandboxName: cmd.parameters.sandbox,
+ newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") ||
+ cmd.parameters.newSandbox,
+ filename: cmd.parameters.filename,
+ line: cmd.parameters.line,
+ debug: cmd.parameters.debug_script,
+ };
+
+ resp.body.value = yield this.execute_(script, args, scriptTimeout, opts);
+};
+
+/**
+ * Executes a JavaScript function in the context of the current browsing
+ * context, if in content space, or in chrome space otherwise, and returns
+ * the object passed to the callback.
+ *
+ * The callback is always the last argument to the {@code arguments}
+ * list passed to the function scope of the script. It can be retrieved
+ * as such:
+ *
+ * let callback = arguments[arguments.length - 1];
+ * callback("foo");
+ * // "foo" is returned
+ *
+ * It is important to note that if the {@code sandboxName} parameter
+ * is left undefined, the script will be evaluated in a mutable sandbox,
+ * causing any change it makes on the global state of the document to have
+ * lasting side-effects.
+ *
+ * @param {string} script
+ * Script to evaluate as a function body.
+ * @param {Array.<(string|boolean|number|object|WebElement)>} args
+ * Arguments exposed to the script in {@code arguments}. The array
+ * items must be serialisable to the WebDriver protocol.
+ * @param {number} scriptTimeout
+ * Duration in milliseconds of when to interrupt and abort the
+ * script evaluation.
+ * @param {string=} sandbox
+ * Name of the sandbox to evaluate the script in. The sandbox is
+ * cached for later re-use on the same Window object if
+ * {@code newSandbox} is false. If the parameter is undefined,
+ * the script is evaluated in a mutable sandbox. If the parameter
+ * is "system", it will be evaluted in a sandbox with elevated system
+ * privileges, equivalent to chrome space.
+ * @param {boolean=} newSandbox
+ * Forces the script to be evaluated in a fresh sandbox. Note that if
+ * it is undefined, the script will normally be evaluted in a fresh
+ * sandbox.
+ * @param {string=} filename
+ * Filename of the client's program where this script is evaluated.
+ * @param {number=} line
+ * Line in the client's program where this script is evaluated.
+ * @param {boolean=} debug_script
+ * Attach an {@code onerror} event handler on the Window object.
+ * It does not differentiate content errors from chrome errors.
+ * @param {boolean=} directInject
+ * Evaluate the script without wrapping it in a function.
+ *
+ * @return {(string|boolean|number|object|WebElement)}
+ * Return value from the script, or null which signifies either the
+ * JavaScript notion of null or undefined.
+ *
+ * @throws ScriptTimeoutError
+ * If the script was interrupted due to reaching the {@code
+ * scriptTimeout} or default timeout.
+ * @throws JavaScriptError
+ * If an Error was thrown whilst evaluating the script.
+ */
+GeckoDriver.prototype.executeAsyncScript = function* (cmd, resp) {
+ let {script, args, scriptTimeout} = cmd.parameters;
+ scriptTimeout = scriptTimeout || this.timeouts.script;
+
+ let opts = {
+ sandboxName: cmd.parameters.sandbox,
+ newSandbox: !!(typeof cmd.parameters.newSandbox == "undefined") ||
+ cmd.parameters.newSandbox,
+ filename: cmd.parameters.filename,
+ line: cmd.parameters.line,
+ debug: cmd.parameters.debug_script,
+ async: true,
+ };
+
+ resp.body.value = yield this.execute_(script, args, scriptTimeout, opts);
+};
+
+GeckoDriver.prototype.execute_ = function (script, args, timeout, opts = {}) {
+ switch (this.context) {
+ case Context.CONTENT:
+ // evaluate in content with lasting side-effects
+ if (!opts.sandboxName) {
+ return this.listener.execute(script, args, timeout, opts);
+
+ // evaluate in content with sandbox
+ } else {
+ return this.listener.executeInSandbox(script, args, timeout, opts);
+ }
+
+ case Context.CHROME:
+ let sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox);
+ if (opts.sandboxName) {
+ sb = sandbox.augment(sb, new logging.Adapter(this.marionetteLog));
+ sb = sandbox.augment(sb, {global: sb});
+ }
+
+ opts.timeout = timeout;
+ script = this.importedScripts.for(Context.CHROME).concat(script);
+ let wargs = element.fromJson(args, this.curBrowser.seenEls, sb.window);
+ let evaluatePromise = evaluate.sandbox(sb, script, wargs, opts);
+ return evaluatePromise.then(res => element.toJson(res, this.curBrowser.seenEls));
+ }
+};
+
+/**
+ * Execute pure JavaScript. Used to execute simpletest harness tests,
+ * which are like mochitests only injected using Marionette.
+ *
+ * Scripts are expected to call the {@code finish} global when done.
+ */
+GeckoDriver.prototype.executeJSScript = function* (cmd, resp) {
+ let {script, args, scriptTimeout} = cmd.parameters;
+ scriptTimeout = scriptTimeout || this.timeouts.script;
+
+ let opts = {
+ filename: cmd.parameters.filename,
+ line: cmd.parameters.line,
+ async: cmd.parameters.async,
+ };
+
+ switch (this.context) {
+ case Context.CHROME:
+ let win = this.getCurrentWindow();
+ let wargs = element.fromJson(args, this.curBrowser.seenEls, win);
+ let harness = new simpletest.Harness(
+ win,
+ Context.CHROME,
+ this.marionetteLog,
+ scriptTimeout,
+ function() {},
+ this.testName);
+
+ let sb = sandbox.createSimpleTest(win, harness);
+ // TODO(ato): Not sure this is needed:
+ sb = sandbox.augment(sb, new logging.Adapter(this.marionetteLog));
+
+ let res = yield evaluate.sandbox(sb, script, wargs, opts);
+ resp.body.value = element.toJson(res, this.curBrowser.seenEls);
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.executeSimpleTest(script, args, scriptTimeout, opts);
+ break;
+ }
+};
+
+/**
+ * Navigate to given URL.
+ *
+ * Navigates the current browsing context to the given URL and waits for
+ * the document to load or the session's page timeout duration to elapse
+ * before returning.
+ *
+ * The command will return with a failure if there is an error loading
+ * the document or the URL is blocked. This can occur if it fails to
+ * reach host, the URL is malformed, or if there is a certificate issue
+ * to name some examples.
+ *
+ * The document is considered successfully loaded when the
+ * DOMContentLoaded event on the frame element associated with the
+ * current window triggers and document.readyState is "complete".
+ *
+ * In chrome context it will change the current window's location to
+ * the supplied URL and wait until document.readyState equals "complete"
+ * or the page timeout duration has elapsed.
+ *
+ * @param {string} url
+ * URL to navigate to.
+ */
+GeckoDriver.prototype.get = function*(cmd, resp) {
+ assert.content(this.context);
+
+ let url = cmd.parameters.url;
+
+ let get = this.listener.get({url: url, pageTimeout: this.timeouts.pageLoad});
+
+ // 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(() => {
+ let parameters = {
+ // TODO(ato): Bug 1242595
+ command_id: this.listener.activeMessageId,
+ pageTimeout: this.timeouts.pageLoad,
+ startTime: new Date().getTime(),
+ };
+ this.mm.broadcastAsyncMessage(
+ "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+ parameters);
+ });
+
+ yield get;
+ browser.getBrowserForTab(this.curBrowser.tab).focus();
+};
+
+/**
+ * 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) {
+ switch (this.context) {
+ case Context.CHROME:
+ return this.getCurrentWindow().location.href;
+
+ case Context.CONTENT:
+ let isB2G = this.appName == "B2G";
+ return this.listener.getCurrentUrl(isB2G);
+ }
+};
+
+/** Gets the current title of the window. */
+GeckoDriver.prototype.getTitle = function* (cmd, resp) {
+ switch (this.context) {
+ case Context.CHROME:
+ let win = this.getCurrentWindow();
+ resp.body.value = win.document.documentElement.getAttribute("title");
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.getTitle();
+ break;
+ }
+};
+
+/** Gets the current type of the window. */
+GeckoDriver.prototype.getWindowType = function (cmd, resp) {
+ let win = this.getCurrentWindow();
+ resp.body.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.body.value = s.serializeToString(win.document);
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.getPageSource();
+ break;
+ }
+};
+
+/**
+ * Cause the browser to traverse one step backward in the joint history
+ * of the current browsing context.
+ */
+GeckoDriver.prototype.goBack = function* (cmd, resp) {
+ assert.content(this.context);
+
+ if (!this.curBrowser.tab) {
+ // Navigation does not work for non-browser windows
+ return;
+ }
+
+ let contentBrowser = browser.getBrowserForTab(this.curBrowser.tab)
+ if (!contentBrowser.webNavigation.canGoBack) {
+ return;
+ }
+
+ let currentURL = yield this.listener.getCurrentUrl();
+ let goBack = this.listener.goBack({pageTimeout: this.timeouts.pageLoad});
+
+ // 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(() => {
+ let parameters = {
+ // TODO(ato): Bug 1242595
+ command_id: this.listener.activeMessageId,
+ lastSeenURL: currentURL,
+ pageTimeout: this.timeouts.pageLoad,
+ startTime: new Date().getTime(),
+ };
+ this.mm.broadcastAsyncMessage(
+ // TODO: combine with
+ // "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+ "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+ parameters);
+ });
+
+ yield goBack;
+};
+
+/**
+ * Cause the browser to traverse one step forward in the joint history
+ * of the current browsing context.
+ */
+GeckoDriver.prototype.goForward = function* (cmd, resp) {
+ assert.content(this.context);
+
+ if (!this.curBrowser.tab) {
+ // Navigation does not work for non-browser windows
+ return;
+ }
+
+ let contentBrowser = browser.getBrowserForTab(this.curBrowser.tab)
+ if (!contentBrowser.webNavigation.canGoForward) {
+ return;
+ }
+
+ let currentURL = yield this.listener.getCurrentUrl();
+ let goForward = this.listener.goForward({pageTimeout: this.timeouts.pageLoad});
+
+ // 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(() => {
+ let parameters = {
+ // TODO(ato): Bug 1242595
+ command_id: this.listener.activeMessageId,
+ lastSeenURL: currentURL,
+ pageTimeout: this.timeouts.pageLoad,
+ startTime: new Date().getTime(),
+ };
+ this.mm.broadcastAsyncMessage(
+ // TODO: combine with
+ // "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+ "Marionette:pollForReadyState" + this.curBrowser.curFrameId,
+ parameters);
+ });
+
+ yield goForward;
+};
+
+/** Refresh the page. */
+GeckoDriver.prototype.refresh = function*(cmd, resp) {
+ assert.content(this.context);
+
+ yield this.listener.refresh();
+};
+
+/**
+ * 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 (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 = winId.toString();
+ this._browserIds.set(permKey, winId);
+ return winId;
+ }
+ return null;
+},
+
+/**
+ * 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) {
+ resp.body.value = this.curBrowser.curFrameId;
+ return;
+ }
+
+ for (let i in this.browsers) {
+ if (this.curBrowser == this.browsers[i]) {
+ resp.body.value = i;
+ return;
+ }
+ }
+};
+
+/**
+ * Get a list of top-level browsing contexts. On desktop this typically
+ * corresponds to the set of open tabs for browser windows, or the window itself
+ * for non-browser chrome windows.
+ *
+ * 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) {
+ return this.windowHandles;
+}
+
+/**
+ * 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.body.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) {
+ return this.chromeWindowHandles;
+}
+
+/**
+ * Get the current window position.
+ *
+ * @return {Object.<string, number>}
+ * Object with |x| and |y| coordinates.
+ */
+GeckoDriver.prototype.getWindowPosition = function (cmd, resp) {
+ return this.curBrowser.position;
+};
+
+/**
+ * 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.
+ *
+ * @return {Object.<string, number>}
+ * Object with |x| and |y| coordinates.
+ */
+GeckoDriver.prototype.setWindowPosition = function (cmd, resp) {
+ assert.firefox()
+
+ let {x, y} = cmd.parameters;
+ assert.positiveInteger(x);
+ assert.positiveInteger(y);
+
+ let win = this.getCurrentWindow();
+ win.moveTo(x, y);
+
+ return this.curBrowser.position;
+};
+
+/**
+ * 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.
+ * @param {boolean=} focus
+ * A boolean value which determines whether to focus
+ * the window. Defaults to true.
+ */
+GeckoDriver.prototype.switchToWindow = function* (cmd, resp) {
+ let switchTo = cmd.parameters.name;
+ let focus = (cmd.parameters.focus !== undefined) ? cmd.parameters.focus : true;
+ let found;
+
+ let byNameOrId = function (name, windowId) {
+ return switchTo === name || switchTo === windowId;
+ };
+
+ let winEn = Services.wm.getEnumerator(null);
+ while (winEn.hasMoreElements()) {
+ let win = winEn.getNext();
+ let outerId = getOuterWindowId(win);
+ let tabBrowser = browser.getTabBrowser(win);
+
+ if (byNameOrId(win.name, outerId)) {
+ // In case the wanted window is a chrome window, we are done.
+ found = {win: win, outerId: outerId, hasTabBrowser: !!tabBrowser};
+ break;
+
+ } else if (tabBrowser) {
+ // Otherwise check if the chrome window has a tab browser, and that it
+ // contains a tab with the wanted window handle.
+ for (let i = 0; i < tabBrowser.tabs.length; ++i) {
+ let contentBrowser = browser.getBrowserForTab(tabBrowser.tabs[i]);
+ let contentWindowId = this.getIdForBrowser(contentBrowser);
+
+ if (byNameOrId(win.name, contentWindowId)) {
+ found = {
+ win: win,
+ outerId: outerId,
+ hasTabBrowser: true,
+ tabIndex: i,
+ };
+ break;
+ }
+ }
+ }
+ }
+
+ if (found) {
+ if (!(found.outerId in this.browsers)) {
+ // Initialise Marionette if the current chrome window has not been seen
+ // before. Also register the initial tab, if one exists.
+ let registerBrowsers, browserListening;
+
+ if (found.hasTabBrowser) {
+ registerBrowsers = this.registerPromise();
+ browserListening = this.listeningPromise();
+ }
+
+ this.startBrowser(found.win, false /* isNewSession */);
+
+ if (registerBrowsers && browserListening) {
+ yield registerBrowsers;
+ yield browserListening;
+ }
+
+ } else {
+ // Otherwise switch to the known chrome window, and activate the tab
+ // if it's a content browser.
+ this.curBrowser = this.browsers[found.outerId];
+
+ if ("tabIndex" in found) {
+ this.curBrowser.switchToTab(found.tabIndex, found.win, focus);
+ }
+ }
+ } 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.body.value = null;
+ if (this.curFrame) {
+ let elRef = this.curBrowser.seenEls
+ .add(this.curFrame.frameElement);
+ let el = element.makeWebElement(elRef);
+ resp.body.value = el;
+ }
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = null;
+ if (this.currentFrameElement !== null) {
+ let el = element.makeWebElement(this.currentFrameElement);
+ resp.body.value = el;
+ }
+ break;
+ }
+};
+
+GeckoDriver.prototype.switchToParentFrame = function*(cmd, resp) {
+ let res = yield this.listener.switchToParentFrame();
+};
+
+/**
+ * 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 {id, element, focus} = cmd.parameters;
+
+ const otherErrorsExpr = /about:.+(error)|(blocked)\?/;
+ const checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+ let curWindow = this.getCurrentWindow();
+
+ let checkLoad = function() {
+ let win = this.getCurrentWindow();
+ if (win.document.readyState == "complete") {
+ return;
+ } else if (win.document.readyState == "interactive") {
+ let baseURI = win.document.baseURI;
+ if (baseURI.startsWith("about:certerror")) {
+ throw new InsecureCertificateError();
+ } else if (otherErrorsExpr.exec(win.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;
+
+ // just focus
+ if (typeof id == "undefined" && typeof element == "undefined") {
+ this.curFrame = null;
+ if (focus) {
+ this.mainFrame.focus();
+ }
+ checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+
+ // by element
+ if (this.curBrowser.seenEls.has(element)) {
+ // HTMLIFrameElement
+ let wantedFrame = this.curBrowser.seenEls.get(element, {frame: curWindow});
+ // Deal with an embedded xul:browser case
+ if (wantedFrame.tagName == "xul:browser" || wantedFrame.tagName == "browser") {
+ curWindow = wantedFrame.contentWindow;
+ this.curFrame = curWindow;
+ if (focus) {
+ this.curFrame.focus();
+ }
+ checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+
+ // Check if the frame is XBL anonymous
+ let parent = curWindow.document.getBindingParent(wantedFrame);
+ // Shadow nodes also show up in getAnonymousNodes, we should ignore them.
+ if (parent && !(parent.shadowRoot && parent.shadowRoot.contains(wantedFrame))) {
+ let anonNodes = [...curWindow.document.getAnonymousNodes(parent) || []];
+ if (anonNodes.length > 0) {
+ let el = wantedFrame;
+ while (el) {
+ if (anonNodes.indexOf(el) > -1) {
+ curWindow = wantedFrame.contentWindow;
+ this.curFrame = curWindow;
+ if (focus) {
+ this.curFrame.focus();
+ }
+ checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+ el = el.parentNode;
+ }
+ }
+ }
+
+ // else, assume iframe
+ let frames = curWindow.document.getElementsByTagName("iframe");
+ let numFrames = frames.length;
+ for (let i = 0; i < numFrames; i++) {
+ if (new XPCNativeWrapper(frames[i]) == new XPCNativeWrapper(wantedFrame)) {
+ curWindow = frames[i].contentWindow;
+ this.curFrame = curWindow;
+ if (focus) {
+ this.curFrame.focus();
+ }
+ checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+ }
+ }
+
+ switch (typeof 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") == id) {
+ foundFrame = i;
+ curWindow = frame.contentWindow;
+ break;
+ } else if (foundById === null && frame.id == id) {
+ foundById = i;
+ }
+ }
+ if (foundFrame === null && foundById !== null) {
+ foundFrame = foundById;
+ curWindow = frames[foundById].contentWindow;
+ }
+ break;
+ case "number":
+ if (typeof curWindow.frames[id] != "undefined") {
+ foundFrame = id;
+ curWindow = curWindow.frames[foundFrame].frameElement.contentWindow;
+ }
+ break;
+ }
+
+ if (foundFrame !== null) {
+ this.curFrame = curWindow;
+ if (focus) {
+ this.curFrame.focus();
+ }
+ checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ } else {
+ throw new NoSuchFrameError(`Unable to locate frame: ${id}`);
+ }
+
+ } else if (this.context == Context.CONTENT) {
+ if (!id && !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;
+ }
+ }
+};
+
+GeckoDriver.prototype.getTimeouts = function (cmd, resp) {
+ return this.timeouts;
+};
+
+/**
+ * Set timeout for page loading, searching, and scripts.
+ *
+ * @param {Object.<string, number>}
+ * Dictionary of timeout types and their new value, where all timeout
+ * types are optional.
+ *
+ * @throws {InvalidArgumentError}
+ * If timeout type key is unknown, or the value provided with it is
+ * not an integer.
+ */
+GeckoDriver.prototype.setTimeouts = function (cmd, resp) {
+ // backwards compatibility with old API
+ // that accepted a dictionary {type: <string>, ms: <number>}
+ let json = {};
+ if (typeof cmd.parameters == "object" &&
+ "type" in cmd.parameters &&
+ "ms" in cmd.parameters) {
+ logger.warn("Using deprecated data structure for setting timeouts");
+ json = {[cmd.parameters.type]: parseInt(cmd.parameters.ms)};
+ } else {
+ json = cmd.parameters;
+ }
+
+ // merge with existing timeouts
+ let merged = Object.assign(this.timeouts.toJSON(), json);
+ this.timeouts = session.Timeouts.fromJSON(merged);
+};
+
+/** Single tap. */
+GeckoDriver.prototype.singleTap = function*(cmd, resp) {
+ let {id, x, y} = cmd.parameters;
+
+ switch (this.context) {
+ case Context.CHROME:
+ throw new UnsupportedOperationError(
+ "Command 'singleTap' is not yet available in chrome context");
+
+ case Context.CONTENT:
+ this.addFrameCloseListener("tap");
+ yield this.listener.singleTap(id, x, y);
+ break;
+ }
+};
+
+/**
+ * Perform a series of grouped actions at the specified points in time.
+ *
+ * @param {Array.<?>} actions
+ * Array of objects that each represent an action sequence.
+ *
+ * @throws {UnsupportedOperationError}
+ * If the command is made in chrome context.
+ */
+GeckoDriver.prototype.performActions = function(cmd, resp) {
+ switch (this.context) {
+ case Context.CHROME:
+ throw new UnsupportedOperationError(
+ "Command 'performActions' is not yet available in chrome context");
+
+ case Context.CONTENT:
+ return this.listener.performActions({"actions": cmd.parameters.actions});
+ }
+};
+
+/**
+ * Release all the keys and pointer buttons that are currently depressed.
+ */
+GeckoDriver.prototype.releaseActions = function(cmd, resp) {
+ switch (this.context) {
+ case Context.CHROME:
+ throw new UnsupportedOperationError(
+ "Command 'releaseActions' is not yet available in chrome context");
+
+ case Context.CONTENT:
+ return this.listener.releaseActions();
+ }
+};
+
+/**
+ * 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:
+ // be conservative until this has a use case and is established
+ // to work as expected in Fennec
+ assert.firefox()
+
+ let win = this.getCurrentWindow();
+ resp.body.value = yield this.legacyactions.dispatchActions(
+ chain, nextId, {frame: win}, this.curBrowser.seenEls);
+ break;
+
+ case Context.CONTENT:
+ this.addFrameCloseListener("action chain");
+ resp.body.value = yield this.listener.actionChain(chain, 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 UnsupportedOperationError(
+ "Command 'multiAction' is not yet available in chrome context");
+
+ case Context.CONTENT:
+ this.addFrameCloseListener("multi action chain");
+ yield this.listener.multiAction(cmd.parameters.value, cmd.parameters.max_length);
+ 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) {
+ let strategy = cmd.parameters.using;
+ let expr = cmd.parameters.value;
+ let opts = {
+ startNode: cmd.parameters.element,
+ timeout: this.timeouts.implicit,
+ all: false,
+ };
+
+ switch (this.context) {
+ case Context.CHROME:
+ if (!SUPPORTED_STRATEGIES.has(strategy)) {
+ throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
+ }
+
+ let container = {frame: this.getCurrentWindow()};
+ if (opts.startNode) {
+ opts.startNode = this.curBrowser.seenEls.get(opts.startNode, container);
+ }
+ let el = yield element.find(container, strategy, expr, opts);
+ let elRef = this.curBrowser.seenEls.add(el);
+ let webEl = element.makeWebElement(elRef);
+
+ resp.body.value = webEl;
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.findElementContent(
+ strategy,
+ expr,
+ opts);
+ break;
+ }
+};
+
+/**
+ * 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) {
+ let strategy = cmd.parameters.using;
+ let expr = cmd.parameters.value;
+ let opts = {
+ startNode: cmd.parameters.element,
+ timeout: this.timeouts.implicit,
+ all: true,
+ };
+
+ switch (this.context) {
+ case Context.CHROME:
+ if (!SUPPORTED_STRATEGIES.has(strategy)) {
+ throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
+ }
+
+ let container = {frame: this.getCurrentWindow()};
+ if (opts.startNode) {
+ opts.startNode = this.curBrowser.seenEls.get(opts.startNode, container);
+ }
+ let els = yield element.find(container, strategy, expr, opts);
+
+ let elRefs = this.curBrowser.seenEls.addAll(els);
+ let webEls = elRefs.map(element.makeWebElement);
+ resp.body = webEls;
+ break;
+
+ case Context.CONTENT:
+ resp.body = yield this.listener.findElementsContent(
+ cmd.parameters.using,
+ cmd.parameters.value,
+ opts);
+ break;
+ }
+};
+
+/** Return the active element on the page. */
+GeckoDriver.prototype.getActiveElement = function*(cmd, resp) {
+ switch (this.context) {
+ case Context.CHROME:
+ throw new UnsupportedOperationError(
+ "Command 'getActiveElement' is not yet available in chrome context");
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.getActiveElement();
+ break;
+ }
+};
+
+/**
+ * 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:
+ let win = this.getCurrentWindow();
+ let el = this.curBrowser.seenEls.get(id, {frame: win});
+ yield interaction.clickElement(el, this.a11yChecks);
+ 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
+ * Web element reference ID to the element that will be inspected.
+ * @param {string} name
+ * Name of the attribute which value to retrieve.
+ *
+ * @return {string}
+ * Value of the attribute.
+ */
+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.seenEls.get(id, {frame: win});
+
+ resp.body.value = el.getAttribute(name);
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.getElementAttribute(id, name);
+ break;
+ }
+};
+
+/**
+ * Returns the value of a property associated with given element.
+ *
+ * @param {string} id
+ * Web element reference ID to the element that will be inspected.
+ * @param {string} name
+ * Name of the property which value to retrieve.
+ *
+ * @return {string}
+ * Value of the property.
+ */
+GeckoDriver.prototype.getElementProperty = function*(cmd, resp) {
+ let {id, name} = cmd.parameters;
+
+ switch (this.context) {
+ case Context.CHROME:
+ let win = this.getCurrentWindow();
+ let el = this.curBrowser.seenEls.get(id, {frame: win});
+ resp.body.value = el[name];
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.getElementProperty(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.seenEls.get(id, {frame: win});
+ let lines = [];
+ this.getVisibleText(el, lines);
+ resp.body.value = lines.join("\n");
+ break;
+
+ case Context.CONTENT:
+ resp.body.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.seenEls.get(id, {frame: win});
+ resp.body.value = el.tagName.toLowerCase();
+ break;
+
+ case Context.CONTENT:
+ resp.body.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.seenEls.get(id, {frame: win});
+ resp.body.value = yield interaction.isElementDisplayed(
+ el, this.a11yChecks);
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.isElementDisplayed(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.seenEls.get(id, {frame: win});
+ let sty = win.document.defaultView.getComputedStyle(el, null);
+ resp.body.value = sty.getPropertyValue(prop);
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.getElementValueOfCssProperty(id, prop);
+ 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.seenEls.get(id, {frame: win});
+ resp.body.value = yield interaction.isElementEnabled(
+ el, this.a11yChecks);
+ break;
+
+ case Context.CONTENT:
+ resp.body.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.seenEls.get(id, {frame: win});
+ resp.body.value = yield interaction.isElementSelected(
+ el, this.a11yChecks);
+ break;
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.isElementSelected(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.seenEls.get(id, {frame: win});
+ let rect = el.getBoundingClientRect();
+ resp.body = {
+ x: rect.x + win.pageXOffset,
+ y: rect.y + win.pageYOffset,
+ width: rect.width,
+ height: rect.height
+ };
+ break;
+
+ case Context.CONTENT:
+ resp.body = 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;
+ assert.defined(value, `Expected character sequence: ${value}`);
+
+ switch (this.context) {
+ case Context.CHROME:
+ let win = this.getCurrentWindow();
+ let el = this.curBrowser.seenEls.get(id, {frame: win});
+ yield interaction.sendKeysToElement(
+ el, value, true, this.a11yChecks);
+ break;
+
+ case Context.CONTENT:
+ yield this.listener.sendKeysToElement(id, value);
+ 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.seenEls.get(id, {frame: win});
+ if (el.nodeName == "textbox") {
+ el.value = "";
+ } else if (el.nodeName == "checkbox") {
+ el.checked = false;
+ }
+ break;
+
+ case Context.CONTENT:
+ yield this.listener.clearElement(id);
+ break;
+ }
+};
+
+/**
+ * Switch to shadow root of the given host element.
+ *
+ * @param {string} id element id.
+ */
+GeckoDriver.prototype.switchToShadowRoot = function*(cmd, resp) {
+ assert.content(this.context)
+
+ let id;
+ if (cmd.parameters) { id = cmd.parameters.id; }
+ yield this.listener.switchToShadowRoot(id);
+};
+
+/** Add a cookie to the document. */
+GeckoDriver.prototype.addCookie = function*(cmd, resp) {
+ assert.content(this.context)
+
+ let cb = msg => {
+ this.mm.removeMessageListener("Marionette:addCookie", cb);
+ let cookie = msg.json;
+ Services.cookies.add(
+ cookie.domain,
+ cookie.path,
+ cookie.name,
+ cookie.value,
+ cookie.secure,
+ cookie.httpOnly,
+ cookie.session,
+ cookie.expiry,
+ {}); // originAttributes
+ return true;
+ };
+
+ this.mm.addMessageListener("Marionette:addCookie", cb);
+ yield this.listener.addCookie(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) {
+ assert.content(this.context)
+
+ resp.body = yield this.listener.getCookies();
+};
+
+/** Delete all cookies that are visible to a document. */
+GeckoDriver.prototype.deleteAllCookies = function*(cmd, resp) {
+ assert.content(this.context)
+
+ let cb = msg => {
+ let cookie = msg.json;
+ cookieManager.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ false,
+ cookie.originAttributes);
+ return true;
+ };
+
+ this.mm.addMessageListener("Marionette:deleteCookie", cb);
+ yield this.listener.deleteAllCookies();
+ this.mm.removeMessageListener("Marionette:deleteCookie", cb);
+};
+
+/** Delete a cookie by name. */
+GeckoDriver.prototype.deleteCookie = function*(cmd, resp) {
+ assert.content(this.context)
+
+ let cb = msg => {
+ this.mm.removeMessageListener("Marionette:deleteCookie", cb);
+ let cookie = msg.json;
+ cookieManager.remove(
+ cookie.host,
+ cookie.name,
+ cookie.path,
+ false,
+ cookie.originAttributes);
+ return true;
+ };
+
+ this.mm.addMessageListener("Marionette:deleteCookie", cb);
+ yield this.listener.deleteCookie(cmd.parameters.name);
+};
+
+/**
+ * Close the currently selected tab/window.
+ *
+ * With multiple open tabs present the currently selected tab will be closed.
+ * Otherwise the window itself will be closed. If it is the last window
+ * currently open, the window will not be closed to prevent a shutdown of the
+ * application. Instead the returned list of window handles is empty.
+ *
+ * @return {Array.<string>}
+ * Unique window handles of remaining windows.
+ */
+GeckoDriver.prototype.close = function (cmd, resp) {
+ let nwins = 0;
+ let winEn = Services.wm.getEnumerator(null);
+
+ while (winEn.hasMoreElements()) {
+ let win = winEn.getNext();
+
+ // For browser windows count the tabs. Otherwise take the window itself.
+ let tabbrowser = browser.getTabBrowser(win);
+ if (tabbrowser) {
+ nwins += tabbrowser.tabs.length;
+ } else {
+ nwins++;
+ }
+ }
+
+ // If there is only 1 window left, do not close it. Instead return a faked
+ // empty array of window handles. This will instruct geckodriver to terminate
+ // the application.
+ if (nwins == 1) {
+ return [];
+ }
+
+ if (this.mm != globalMessageManager) {
+ this.mm.removeDelayedFrameScript(FRAME_SCRIPT);
+ }
+
+ return this.curBrowser.closeTab().then(() => this.windowHandles);
+};
+
+/**
+ * Close the currently selected chrome window.
+ *
+ * If it is the last window currently open, the chrome window will not be
+ * closed to prevent a shutdown of the application. Instead the returned
+ * list of chrome window handles is empty.
+ *
+ * @return {Array.<string>}
+ * Unique chrome window handles of remaining chrome windows.
+ */
+GeckoDriver.prototype.closeChromeWindow = function (cmd, resp) {
+ assert.firefox();
+
+ // Get the total number of windows
+ let nwins = 0;
+ let winEn = Services.wm.getEnumerator(null);
+
+ while (winEn.hasMoreElements()) {
+ nwins++;
+ winEn.getNext();
+ }
+
+ // If there is only 1 window left, do not close it. Instead return a faked
+ // empty array of window handles. This will instruct geckodriver to terminate
+ // the application.
+ if (nwins == 1) {
+ return [];
+ }
+
+ // reset frame to the top-most frame
+ this.curFrame = null;
+
+ if (this.mm != globalMessageManager) {
+ this.mm.removeDelayedFrameScript(FRAME_SCRIPT);
+ }
+
+ return this.curBrowser.closeWindow().then(() => this.chromeWindowHandles);
+};
+
+/** Delete Marionette session. */
+GeckoDriver.prototype.deleteSession = function (cmd, resp) {
+ if (this.curBrowser !== null) {
+ // frame scripts can be safely reused
+ Preferences.set(CONTENT_LISTENER_PREF, 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 = Services.wm.getEnumerator(null);
+ while (winEn.hasMoreElements()) {
+ let win = winEn.getNext();
+ if (win.messageManager) {
+ win.messageManager.removeDelayedFrameScript(FRAME_SCRIPT);
+ } else {
+ logger.error(
+ `Could not remove listener from page ${win.location.href}`);
+ }
+ }
+
+ this.curBrowser.frameManager.removeMessageManagerListeners(
+ globalMessageManager);
+ }
+
+ this.switchToGlobalMessageManager();
+
+ // reset frame to the top-most frame
+ this.curFrame = null;
+ if (this.mainFrame) {
+ try {
+ this.mainFrame.focus();
+ } catch (e) {
+ this.mainFrame = null;
+ }
+ }
+
+ if (this.observing !== null) {
+ for (let topic in this.observing) {
+ Services.obs.removeObserver(this.observing[topic], topic);
+ }
+ this.observing = null;
+ }
+
+ this.sandboxes.clear();
+ cert.uninstallOverride();
+
+ this.sessionId = null;
+ this.capabilities = new session.Capabilities();
+};
+
+/** Returns the current status of the Application Cache. */
+GeckoDriver.prototype.getAppCacheStatus = function* (cmd, resp) {
+ switch (this.context) {
+ case Context.CHROME:
+ throw new UnsupportedOperationError(
+ "Command 'getAppCacheStatus' is not yet available in chrome context");
+
+ case Context.CONTENT:
+ resp.body.value = yield this.listener.getAppCacheStatus();
+ break;
+ }
+};
+
+/**
+ * Import script to the JS evaluation runtime.
+ *
+ * Imported scripts are exposed in the contexts of all subsequent
+ * calls to {@code executeScript}, {@code executeAsyncScript}, and
+ * {@code executeJSScript} by prepending them to the evaluated script.
+ *
+ * Scripts can be cleared with the {@code clearImportedScripts} command.
+ *
+ * @param {string} script
+ * Script to include. If the script is byte-by-byte equal to an
+ * existing imported script, it is not imported.
+ */
+GeckoDriver.prototype.importScript = function*(cmd, resp) {
+ let script = cmd.parameters.script;
+ this.importedScripts.for(this.context).add(script);
+};
+
+/**
+ * Clear all scripts that are imported into the JS evaluation runtime.
+ *
+ * Scripts can be imported using the {@code importScript} command.
+ */
+GeckoDriver.prototype.clearImportedScripts = function*(cmd, resp) {
+ this.importedScripts.for(this.context).clear();
+};
+
+/**
+ * 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
+ * Optional web element reference to take a screenshot of.
+ * If undefined, a screenshot will be taken of the document element.
+ * @param {Array.<string>=} highlights
+ * List of web elements to highlight.
+ * @param {boolean} full
+ * True to take a screenshot of the entire document element. Is not
+ * considered if {@code id} is not defined. Defaults to true.
+ * @param {boolean=} hash
+ * True if the user requests a hash of the image data.
+ * @param {boolean=} scroll
+ * Scroll to element if |id| is provided. If undefined, it will
+ * scroll to the element.
+ *
+ * @return {string}
+ * If {@code hash} is false, PNG image encoded as base64 encoded string. If
+ * 'hash' is True, hex digest of the SHA-256 hash of the base64 encoded
+ * string.
+ */
+GeckoDriver.prototype.takeScreenshot = function (cmd, resp) {
+ let {id, highlights, full, hash, scroll} = cmd.parameters;
+ highlights = highlights || [];
+ let format = hash ? capture.Format.Hash : capture.Format.Base64;
+
+ switch (this.context) {
+ case Context.CHROME:
+ let container = {frame: this.getCurrentWindow().document.defaultView};
+ if (!container.frame) {
+ throw new NoSuchWindowError("Unable to locate window");
+ }
+
+ let highlightEls = highlights.map(
+ ref => this.curBrowser.seenEls.get(ref, container));
+
+ // viewport
+ let canvas;
+ if (!id && !full) {
+ canvas = capture.viewport(container.frame, highlightEls);
+
+ // element or full document element
+ } else {
+ let node;
+ if (id) {
+ node = this.curBrowser.seenEls.get(id, container);
+ } else {
+ node = container.frame.document.documentElement;
+ }
+
+ canvas = capture.element(node, highlightEls);
+ }
+
+ switch (format) {
+ case capture.Format.Hash:
+ return capture.toHash(canvas);
+
+ case capture.Format.Base64:
+ return capture.toBase64(canvas);
+ }
+ break;
+
+ case Context.CONTENT:
+ return this.listener.takeScreenshot(format, cmd.parameters);
+ }
+};
+
+/**
+ * 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) {
+ assert.fennec();
+
+ resp.body.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) {
+ assert.fennec();
+
+ const ors = [
+ "portrait", "landscape",
+ "portrait-primary", "landscape-primary",
+ "portrait-secondary", "landscape-secondary",
+ ];
+
+ let or = String(cmd.parameters.orientation);
+ assert.string(or);
+ let mozOr = or.toLowerCase();
+ if (!ors.includes(mozOr)) {
+ throw new InvalidArgumentError(`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.body.width = win.outerWidth;
+ resp.body.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.
+ */
+GeckoDriver.prototype.setWindowSize = function (cmd, resp) {
+ assert.firefox()
+
+ let {width, height} = cmd.parameters;
+ let win = this.getCurrentWindow();
+ win.resizeTo(width, height);
+ this.getWindowSize(cmd, resp);
+};
+
+/**
+ * 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) {
+ assert.firefox()
+
+ let win = this.getCurrentWindow();
+ win.maximize()
+};
+
+/**
+ * Dismisses a currently displayed tab modal, or returns no such alert if
+ * no modal is displayed.
+ */
+GeckoDriver.prototype.dismissDialog = function (cmd, resp) {
+ this._checkIfAlertIsPresent();
+
+ 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) {
+ this._checkIfAlertIsPresent();
+
+ 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) {
+ this._checkIfAlertIsPresent();
+
+ let {infoBody} = this.dialog.ui;
+ resp.body.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) {
+ this._checkIfAlertIsPresent();
+
+ // see toolkit/components/prompts/content/commonDialog.js
+ let {loginContainer, loginTextbox} = this.dialog.ui;
+ if (loginContainer.hidden) {
+ throw new ElementNotInteractableError(
+ "This prompt does not accept text input");
+ }
+
+ let win = this.dialog.window ? this.dialog.window : this.getCurrentWindow();
+ event.sendKeysToElement(
+ cmd.parameters.value,
+ loginTextbox,
+ {ignoreVisibility: true},
+ win);
+};
+
+GeckoDriver.prototype._checkIfAlertIsPresent = function() {
+ if (!this.dialog || !this.dialog.ui) {
+ throw new NoAlertOpenError(
+ "No tab modal was open when attempting to get the dialog text");
+ }
+};
+
+/**
+ * Enables or disables accepting new socket connections.
+ *
+ * By calling this method with `false` the server will not accept any further
+ * connections, but existing connections will not be forcible closed. Use `true`
+ * to re-enable accepting connections.
+ *
+ * Please note that when closing the connection via the client you can end-up in
+ * a non-recoverable state if it hasn't been enabled before.
+ *
+ * This method is used for custom in application shutdowns via marionette.quit()
+ * or marionette.restart(), like File -> Quit.
+ *
+ * @param {boolean} state
+ * True if the server should accept new socket connections.
+ */
+GeckoDriver.prototype.acceptConnections = function (cmd, resp) {
+ assert.boolean(cmd.parameters.value);
+ this._server.acceptConnections = cmd.parameters.value;
+}
+
+/**
+ * Quits Firefox with the provided flags and tears down the current
+ * session.
+ */
+GeckoDriver.prototype.quitApplication = function (cmd, resp) {
+ assert.firefox("Bug 1298921 - In app initiated quit not yet available beside Firefox")
+
+ let flags = Ci.nsIAppStartup.eAttemptQuit;
+ for (let k of cmd.parameters.flags || []) {
+ flags |= Ci.nsIAppStartup[k];
+ }
+
+ this._server.acceptConnections = false;
+ resp.send();
+
+ this.deleteSession();
+ Services.startup.quit(flags);
+};
+
+GeckoDriver.prototype.installAddon = function (cmd, resp) {
+ assert.firefox()
+
+ let path = cmd.parameters.path;
+ let temp = cmd.parameters.temporary || false;
+ if (typeof path == "undefined" || typeof path != "string" ||
+ typeof temp != "boolean") {
+ throw InvalidArgumentError();
+ }
+
+ return addon.install(path, temp);
+};
+
+GeckoDriver.prototype.uninstallAddon = function (cmd, resp) {
+ assert.firefox()
+
+ let id = cmd.parameters.id;
+ if (typeof id == "undefined" || typeof id != "string") {
+ throw new InvalidArgumentError();
+ }
+
+ return addon.uninstall(id);
+};
+
+/**
+ * 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) {
+ switch (message.name) {
+ case "Marionette:ok":
+ case "Marionette:done":
+ case "Marionette:error":
+ // 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;
+ }
+ break;
+
+ 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.addAll(message.json.log);
+ }
+ 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;
+ let isForCurrentPath = path => currentPath.indexOf(path) != -1;
+ let results = [];
+
+ let en = cookieManager.getCookiesFromHost(host, {});
+ while (en.hasMoreElements()) {
+ let cookie = en.getNext().QueryInterface(Ci.nsICookie2);
+ // 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,
+ "httpOnly": cookie.isHttpOnly,
+ "originAttributes": cookie.originAttributes
+ });
+ break;
+ }
+ hostname = hostname.replace(/^.*?\./, "");
+ } while (hostname.indexOf(".") != -1);
+ }
+ return results;
+
+ 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.
+ this.sendAsync("newSession", this.capabilities);
+ this.curBrowser.flushPendingCommands();
+ }
+ break;
+ }
+};
+
+GeckoDriver.prototype.responseCompleted = function () {
+ if (this.curBrowser !== null) {
+ this.curBrowser.pendingCommands = [];
+ }
+};
+
+/**
+ * Retrieve the localized string for the specified entity id.
+ *
+ * Example:
+ * localizeEntity(["chrome://global/locale/about.dtd"], "about.version")
+ *
+ * @param {Array.<string>} urls
+ * Array of .dtd URLs.
+ * @param {string} id
+ * The ID of the entity to retrieve the localized string for.
+ *
+ * @return {string}
+ * The localized string for the requested entity.
+ */
+GeckoDriver.prototype.localizeEntity = function (cmd, resp) {
+ let {urls, id} = cmd.parameters;
+
+ if (!Array.isArray(urls)) {
+ throw new InvalidArgumentError("Value of `urls` should be of type 'Array'");
+ }
+ if (typeof id != "string") {
+ throw new InvalidArgumentError("Value of `id` should be of type 'string'");
+ }
+
+ resp.body.value = l10n.localizeEntity(urls, id);
+}
+
+/**
+ * Retrieve the localized string for the specified property id.
+ *
+ * Example:
+ * localizeProperty(["chrome://global/locale/findbar.properties"], "FastFind")
+ *
+ * @param {Array.<string>} urls
+ * Array of .properties URLs.
+ * @param {string} id
+ * The ID of the property to retrieve the localized string for.
+ *
+ * @return {string}
+ * The localized string for the requested property.
+ */
+GeckoDriver.prototype.localizeProperty = function (cmd, resp) {
+ let {urls, id} = cmd.parameters;
+
+ if (!Array.isArray(urls)) {
+ throw new InvalidArgumentError("Value of `urls` should be of type 'Array'");
+ }
+ if (typeof id != "string") {
+ throw new InvalidArgumentError("Value of `id` should be of type 'string'");
+ }
+
+ resp.body.value = l10n.localizeProperty(urls, id);
+}
+
+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.executeScript,
+ "getTimeouts": GeckoDriver.prototype.getTimeouts,
+ "timeouts": GeckoDriver.prototype.setTimeouts, // deprecated until Firefox 55
+ "setTimeouts": GeckoDriver.prototype.setTimeouts,
+ "singleTap": GeckoDriver.prototype.singleTap,
+ "performActions": GeckoDriver.prototype.performActions,
+ "releaseActions": GeckoDriver.prototype.releaseActions,
+ "actionChain": GeckoDriver.prototype.actionChain, // deprecated
+ "multiAction": GeckoDriver.prototype.multiAction, // deprecated
+ "executeAsyncScript": GeckoDriver.prototype.executeAsyncScript,
+ "executeJSScript": GeckoDriver.prototype.executeJSScript,
+ "findElement": GeckoDriver.prototype.findElement,
+ "findElements": GeckoDriver.prototype.findElements,
+ "clickElement": GeckoDriver.prototype.clickElement,
+ "getElementAttribute": GeckoDriver.prototype.getElementAttribute,
+ "getElementProperty": GeckoDriver.prototype.getElementProperty,
+ "getElementText": GeckoDriver.prototype.getElementText,
+ "getElementTagName": GeckoDriver.prototype.getElementTagName,
+ "isElementDisplayed": GeckoDriver.prototype.isElementDisplayed,
+ "getElementValueOfCssProperty": GeckoDriver.prototype.getElementValueOfCssProperty,
+ "getElementRect": GeckoDriver.prototype.getElementRect,
+ "isElementEnabled": GeckoDriver.prototype.isElementEnabled,
+ "isElementSelected": GeckoDriver.prototype.isElementSelected,
+ "sendKeysToElement": GeckoDriver.prototype.sendKeysToElement,
+ "clearElement": GeckoDriver.prototype.clearElement,
+ "getTitle": GeckoDriver.prototype.getTitle,
+ "getWindowType": GeckoDriver.prototype.getWindowType,
+ "getPageSource": GeckoDriver.prototype.getPageSource,
+ "get": GeckoDriver.prototype.get,
+ "getCurrentUrl": GeckoDriver.prototype.getCurrentUrl,
+ "goBack": GeckoDriver.prototype.goBack,
+ "goForward": GeckoDriver.prototype.goForward,
+ "refresh": GeckoDriver.prototype.refresh,
+ "getWindowHandle": GeckoDriver.prototype.getWindowHandle,
+ "getChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle,
+ "getCurrentChromeWindowHandle": GeckoDriver.prototype.getChromeWindowHandle,
+ "getWindowHandles": GeckoDriver.prototype.getWindowHandles,
+ "getChromeWindowHandles": GeckoDriver.prototype.getChromeWindowHandles,
+ "getWindowPosition": GeckoDriver.prototype.getWindowPosition,
+ "setWindowPosition": GeckoDriver.prototype.setWindowPosition,
+ "getActiveFrame": GeckoDriver.prototype.getActiveFrame,
+ "switchToFrame": GeckoDriver.prototype.switchToFrame,
+ "switchToParentFrame": GeckoDriver.prototype.switchToParentFrame,
+ "switchToWindow": GeckoDriver.prototype.switchToWindow,
+ "switchToShadowRoot": GeckoDriver.prototype.switchToShadowRoot,
+ "deleteSession": GeckoDriver.prototype.deleteSession,
+ "importScript": GeckoDriver.prototype.importScript,
+ "clearImportedScripts": GeckoDriver.prototype.clearImportedScripts,
+ "getAppCacheStatus": GeckoDriver.prototype.getAppCacheStatus,
+ "close": GeckoDriver.prototype.close,
+ "closeChromeWindow": GeckoDriver.prototype.closeChromeWindow,
+ "setTestName": GeckoDriver.prototype.setTestName,
+ "takeScreenshot": GeckoDriver.prototype.takeScreenshot,
+ "addCookie": GeckoDriver.prototype.addCookie,
+ "getCookies": GeckoDriver.prototype.getCookies,
+ "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,
+ "acceptConnections": GeckoDriver.prototype.acceptConnections,
+ "quitApplication": GeckoDriver.prototype.quitApplication,
+
+ "localization:l10n:localizeEntity": GeckoDriver.prototype.localizeEntity,
+ "localization:l10n:localizeProperty": GeckoDriver.prototype.localizeProperty,
+
+ "addon:install": GeckoDriver.prototype.installAddon,
+ "addon:uninstall": GeckoDriver.prototype.uninstallAddon,
+};
+
+function copy (obj) {
+ if (Array.isArray(obj)) {
+ return obj.slice();
+ } else if (typeof obj == "object") {
+ return Object.assign({}, obj);
+ }
+ return obj;
+}
+
+/**
+ * Get the outer window ID for the specified window.
+ *
+ * @param {nsIDOMWindow} win
+ * Window whose browser we need to access.
+ *
+ * @return {string}
+ * Returns the unique window ID.
+ */
+function getOuterWindowId(win) {
+ let id = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+
+ return id.toString();
+}