gecko-dev/testing/marionette/message.js

377 строки
9.6 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
ChromeUtils.import("chrome://marionette/content/assert.js");
ChromeUtils.import("chrome://marionette/content/error.js");
const {truncate} = ChromeUtils.import("chrome://marionette/content/format.js", {});
this.EXPORTED_SYMBOLS = [
"Command",
"Message",
"Response",
];
/** 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;
const validator = {
exclusionary: {
"capabilities": ["error", "value"],
"error": ["value", "sessionId", "capabilities"],
"sessionId": ["error", "value"],
"value": ["error", "sessionId", "capabilities"],
},
set(obj, prop, val) {
let tests = this.exclusionary[prop];
if (tests) {
for (let t of tests) {
if (obj.hasOwnProperty(t)) {
throw new TypeError(`${t} set, cannot set ${prop}`);
}
}
}
obj[prop] = val;
return true;
},
};
/**
* The response body is exposed as an argument to commands.
* Commands can set fields on the body through defining properties.
*
* Setting properties invokes a validator that performs tests for
* mutually exclusionary fields on the input against the existing data
* in the body.
*
* For example setting the <code>error</code> property on
* the body when <code>value</code>, <code>sessionId</code>, or
* <code>capabilities</code> have been set previously will cause
* an error.
*/
const ResponseBody = () => new Proxy({}, validator);
/**
* @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 = ResponseBody();
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;