diff options
Diffstat (limited to 'testing/marionette/message.js')
-rw-r--r-- | testing/marionette/message.js | 331 |
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; |