gecko/testing/marionette/proxy.js

159 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("chrome://marionette/content/modal.js");
this.EXPORTED_SYMBOLS = ["proxy"];
const MARIONETTE_OK = "Marionette:ok";
const MARIONETTE_DONE = "Marionette:done";
const MARIONETTE_ERROR = "Marionette:error";
const logger = Log.repository.getLogger("Marionette");
const uuidgen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
this.proxy = {};
/**
* 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(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
* Function returning the current message manager.
* @param {function(string, Object, number)} sendAsyncFn
* Callback for sending async messages to the current listener.
*/
proxy.toListener = function(mmFn, sendAsyncFn) {
let sender = new ContentSender(mmFn, sendAsyncFn);
let handler = {
get: (obj, prop) => {
if (obj.hasOwnProperty(prop)) {
return obj[prop];
}
return (...args) => obj.send(prop, args);
}
};
return new Proxy(sender, handler);
};
/**
* The ContentSender allows one to make synchronous calls to the
* message listener of the content frame of the current browsing context.
*
* Presumptions about the responses from content space are made so we
* can provide a nicer API on top of the message listener primitives that
* make calls from chrome- to content space seem synchronous by leveraging
* promises.
*
* The promise is guaranteed not to resolve until the execution of the
* command in content space is complete.
*
* @param {function(): (nsIMessageSender|nsIMessageBroadcaster)} mmFn
* Function returning the current message manager.
* @param {function(string, Object, number)} sendAsyncFn
* Callback for sending async messages to the current listener.
*/
var ContentSender = function(mmFn, sendAsyncFn) {
this.curId = null;
this.sendAsync = sendAsyncFn;
this.mmFn_ = mmFn;
};
Object.defineProperty(ContentSender.prototype, "mm", {
get: function() { return this.mmFn_(); }
});
/**
* Call registered function in the frame script environment of the
* current browsing context's content frame.
*
* @param {string} name
* Function to call in the listener, e.g. for "Marionette:foo8",
* use "foo".
* @param {Array} args
* Argument list to pass the function. If args has a single entry
* that is an object, we assume it's an old style dispatch, and
* the object will passed literally.
*
* @return {Promise}
* A promise that resolves to the result of the command.
*/
ContentSender.prototype.send = function(name, args) {
this.curId = uuidgen.generateUUID().toString();
let proxy = new Promise((resolve, reject) => {
let removeListeners = (n, fn) => {
let rmFn = msg => {
if (this.curId !== msg.json.command_id) {
logger.warn("Skipping out-of-sync response from listener: " +
`Expected response to \`${name}' with ID ${this.curId}, ` +
"but got: " + msg.name + msg.json.toSource());
return;
}
listeners.remove();
modal.removeHandler(handleDialog);
fn(msg);
this.curId = null;
};
listeners.push([n, rmFn]);
return rmFn;
};
let listeners = [];
listeners.add = () => {
this.mm.addMessageListener(MARIONETTE_OK, removeListeners(MARIONETTE_OK, okListener));
this.mm.addMessageListener(MARIONETTE_DONE, removeListeners(MARIONETTE_DONE, valListener));
this.mm.addMessageListener(MARIONETTE_ERROR, removeListeners(MARIONETTE_ERROR, 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);
// new style dispatches are arrays of arguments, old style dispatches
// are key-value objects
let msg = args;
if (args.length == 1 && typeof args[0] == "object") {
msg = args[0];
}
this.sendAsync(name, msg, this.curId);
});
return proxy;
};
proxy.ContentSender = ContentSender;