summaryrefslogtreecommitdiffstats
path: root/testing/marionette/message.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/message.js')
-rw-r--r--testing/marionette/message.js331
1 files changed, 331 insertions, 0 deletions
diff --git a/testing/marionette/message.js b/testing/marionette/message.js
new file mode 100644
index 0000000000..9159112f27
--- /dev/null
+++ b/testing/marionette/message.js
@@ -0,0 +1,331 @@
+/* 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 EXPORTED_SYMBOLS = ["Command", "Message", "Response"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ assert: "chrome://marionette/content/assert.js",
+ error: "chrome://marionette/content/error.js",
+ truncate: "chrome://marionette/content/format.js",
+});
+
+/** Representation of the packets transproted over the wire. */
+class Message {
+ /**
+ * @param {number} messageID
+ * Message ID unique identifying this message.
+ */
+ constructor(messageID) {
+ this.id = assert.integer(messageID);
+ }
+
+ toString() {
+ let content = JSON.stringify(this.toPacket());
+ return truncate`${content}`;
+ }
+
+ /**
+ * Converts a data packet into a {@link Command} or {@link Response}.
+ *
+ * @param {Array.<number, number, ?, ?>} data
+ * A four element array where the elements, in sequence, signifies
+ * message type, message ID, method name or error, and parameters
+ * or result.
+ *
+ * @return {Message}
+ * Based on the message type, a {@link Command} or {@link Response}
+ * instance.
+ *
+ * @throws {TypeError}
+ * If the message type is not recognised.
+ */
+ static fromPacket(data) {
+ const [type] = data;
+
+ switch (type) {
+ case Command.Type:
+ return Command.fromPacket(data);
+
+ case Response.Type:
+ return Response.fromPacket(data);
+
+ default:
+ throw new TypeError(
+ "Unrecognised message type in packet: " + JSON.stringify(data)
+ );
+ }
+ }
+}
+
+/**
+ * Messages may originate from either the server or the client.
+ * Because the remote protocol is full duplex, both endpoints may be
+ * the origin of both commands and responses.
+ *
+ * @enum
+ * @see {@link Message}
+ */
+Message.Origin = {
+ /** Indicates that the message originates from the client. */
+ Client: 0,
+ /** Indicates that the message originates from the server. */
+ Server: 1,
+};
+
+/**
+ * A command is a request from the client to run a series of remote end
+ * steps and return a fitting response.
+ *
+ * The command can be synthesised from the message passed over the
+ * Marionette socket using the {@link fromPacket} function. The format of
+ * a message is:
+ *
+ * <pre>
+ * [<var>type</var>, <var>id</var>, <var>name</var>, <var>params</var>]
+ * </pre>
+ *
+ * where
+ *
+ * <dl>
+ * <dt><var>type</var> (integer)
+ * <dd>
+ * Must be zero (integer). Zero means that this message is
+ * a command.
+ *
+ * <dt><var>id</var> (integer)
+ * <dd>
+ * Integer used as a sequence number. The server replies with
+ * the same ID for the response.
+ *
+ * <dt><var>name</var> (string)
+ * <dd>
+ * String representing the command name with an associated set
+ * of remote end steps.
+ *
+ * <dt><var>params</var> (JSON Object or null)
+ * <dd>
+ * Object of command function arguments. The keys of this object
+ * must be strings, but the values can be arbitrary values.
+ * </dl>
+ *
+ * A command has an associated message <var>id</var> that prevents
+ * the dispatcher from sending responses in the wrong order.
+ *
+ * The command may also have optional error- and result handlers that
+ * are called when the client returns with a response. These are
+ * <code>function onerror({Object})</code>,
+ * <code>function onresult({Object})</code>, and
+ * <code>function onresult({Response})</code>:
+ *
+ * @param {number} messageID
+ * Message ID unique identifying this message.
+ * @param {string} name
+ * Command name.
+ * @param {Object.<string, ?>} params
+ * Command parameters.
+ */
+class Command extends Message {
+ constructor(messageID, name, params = {}) {
+ super(messageID);
+
+ this.name = assert.string(name);
+ this.parameters = assert.object(params);
+
+ this.onerror = null;
+ this.onresult = null;
+
+ this.origin = Message.Origin.Client;
+ this.sent = false;
+ }
+
+ /**
+ * Calls the error- or result handler associated with this command.
+ * This function can be replaced with a custom response handler.
+ *
+ * @param {Response} resp
+ * The response to pass on to the result or error to the
+ * <code>onerror</code> or <code>onresult</code> handlers to.
+ */
+ onresponse(resp) {
+ if (this.onerror && resp.error) {
+ this.onerror(resp.error);
+ } else if (this.onresult && resp.body) {
+ this.onresult(resp.body);
+ }
+ }
+
+ /**
+ * Encodes the command to a packet.
+ *
+ * @return {Array}
+ * Packet.
+ */
+ toPacket() {
+ return [Command.Type, this.id, this.name, this.parameters];
+ }
+
+ /**
+ * Converts a data packet into {@link Command}.
+ *
+ * @param {Array.<number, number, ?, ?>} data
+ * A four element array where the elements, in sequence, signifies
+ * message type, message ID, command name, and parameters.
+ *
+ * @return {Command}
+ * Representation of packet.
+ *
+ * @throws {TypeError}
+ * If the message type is not recognised.
+ */
+ static fromPacket(payload) {
+ let [type, msgID, name, params] = payload;
+ assert.that(n => n === Command.Type)(type);
+
+ // if parameters are given but null, treat them as undefined
+ if (params === null) {
+ params = undefined;
+ }
+
+ return new Command(msgID, name, params);
+ }
+}
+Command.Type = 0;
+
+/**
+ * @callback ResponseCallback
+ *
+ * @param {Response} resp
+ * Response to handle.
+ */
+
+/**
+ * Represents the response returned from the remote end after execution
+ * of its corresponding command.
+ *
+ * The response is a mutable object passed to each command for
+ * modification through the available setters. To send data in a response,
+ * you modify the body property on the response. The body property can
+ * also be replaced completely.
+ *
+ * The response is sent implicitly by
+ * {@link server.TCPConnection#execute when a command has finished
+ * executing, and any modifications made subsequent to that will have
+ * no effect.
+ *
+ * @param {number} messageID
+ * Message ID tied to the corresponding command request this is
+ * a response for.
+ * @param {ResponseHandler} respHandler
+ * Function callback called on sending the response.
+ */
+class Response extends Message {
+ constructor(messageID, respHandler = () => {}) {
+ super(messageID);
+
+ this.respHandler_ = assert.callable(respHandler);
+
+ this.error = null;
+ this.body = { value: null };
+
+ this.origin = Message.Origin.Server;
+ this.sent = false;
+ }
+
+ /**
+ * Sends response conditionally, given a predicate.
+ *
+ * @param {function(Response): boolean} predicate
+ * A predicate taking a Response object and returning a boolean.
+ */
+ sendConditionally(predicate) {
+ if (predicate(this)) {
+ this.send();
+ }
+ }
+
+ /**
+ * Sends response using the response handler provided on
+ * construction.
+ *
+ * @throws {RangeError}
+ * If the response has already been sent.
+ */
+ send() {
+ if (this.sent) {
+ throw new RangeError("Response has already been sent: " + this);
+ }
+ this.respHandler_(this);
+ this.sent = true;
+ }
+
+ /**
+ * Send error to client.
+ *
+ * Turns the response into an error response, clears any previously
+ * set body data, and sends it using the response handler provided
+ * on construction.
+ *
+ * @param {Error} err
+ * The Error instance to send.
+ *
+ * @throws {Error}
+ * If <var>err</var> is not a {@link WebDriverError}, the error
+ * is propagated, i.e. rethrown.
+ */
+ sendError(err) {
+ this.error = error.wrap(err).toJSON();
+ this.body = null;
+ this.send();
+
+ // propagate errors which are implementation problems
+ if (!error.isWebDriverError(err)) {
+ throw err;
+ }
+ }
+
+ /**
+ * Encodes the response to a packet.
+ *
+ * @return {Array}
+ * Packet.
+ */
+ toPacket() {
+ return [Response.Type, this.id, this.error, this.body];
+ }
+
+ /**
+ * Converts a data packet into {@link Response}.
+ *
+ * @param {Array.<number, number, ?, ?>} data
+ * A four element array where the elements, in sequence, signifies
+ * message type, message ID, error, and result.
+ *
+ * @return {Response}
+ * Representation of packet.
+ *
+ * @throws {TypeError}
+ * If the message type is not recognised.
+ */
+ static fromPacket(payload) {
+ let [type, msgID, err, body] = payload;
+ assert.that(n => n === Response.Type)(type);
+
+ let resp = new Response(msgID);
+ resp.error = assert.string(err);
+
+ resp.body = body;
+ return resp;
+ }
+}
+Response.Type = 1;
+
+this.Message = Message;
+this.Command = Command;
+this.Response = Response;