From 1fe3c441c1b99dd44d1ae7a19ccc8abfb85c4f15 Mon Sep 17 00:00:00 2001 From: Andreas Tolfsen Date: Thu, 21 May 2015 11:26:58 +0100 Subject: [PATCH] Bug 1153822: Adjust Marionette responses to match WebDriver protocol Introduce protocol version levels in the Marionette server. On establishing a connection to a local end, the remote will return a `marionetteProtocol` field indicating which level it speaks. The protocol level can be used by local ends to either fall into compatibility mode or warn the user that the local end is incompatible with the remote. The protocol is currently also more expressive than it needs to be and this expressiveness has previously resulted in subtle inconsistencies in the fields returned. This patch reduces the amount of superfluous fields, reducing the amount of data sent. Aligning the protocol closer to the WebDriver specification's expectations will also reduce the amount of post-processing required in the httpd. Previous to this patch, this is a value response: {"from":"0","value":null,"status":0,"sessionId":"{6b6d68d2-4ac9-4308-9f07-d2e72519c407}"} And this for ok responses: {"from":"0","ok":true} And this for errors: {"from":"0","status":21,"sessionId":"{6b6d68d2-4ac9-4308-9f07-d2e72519c407}","error":{"message":"Error loading page, timed out (onDOMContentLoaded)","stacktrace":null,"status":21}} This patch drops the `from` and `sessionId` fields, and the `status` field from non-error responses. It also drops the `ok` field in non-value responses and flattens the error response to a simple dictionary with the `error` (previously `status`), `message`, and `stacktrace` properties, which are now all required. r=jgriffin --HG-- extra : commitid : FbEkv70rxl9 extra : rebase_source : 3116110a0d197289cc95eba8748be0a33566c5a5 --- .hgignore | 1 + testing/marionette/actions.js | 2 +- testing/marionette/command.js | 144 ++++++++++++++-------------- testing/marionette/dispatcher.js | 94 ++++-------------- testing/marionette/driver.js | 157 ++++++++++++++++--------------- testing/marionette/error.js | 28 +----- testing/marionette/listener.js | 25 +++-- 7 files changed, 187 insertions(+), 264 deletions(-) diff --git a/.hgignore b/.hgignore index 6eed30b621b9..7357cb7bb3de 100644 --- a/.hgignore +++ b/.hgignore @@ -6,6 +6,7 @@ (?i)(^|/)TAGS$ (^|/)ID$ (^|/)\.DS_Store$ +.*\.egg-info # Vim swap files. ^\.sw.$ diff --git a/testing/marionette/actions.js b/testing/marionette/actions.js index 0d5343155071..3ce7c44f76fe 100644 --- a/testing/marionette/actions.js +++ b/testing/marionette/actions.js @@ -156,7 +156,7 @@ ActionChain.prototype.resetValues = function() { */ ActionChain.prototype.actions = function(chain, touchId, i, keyModifiers) { if (i == chain.length) { - this.onSuccess({value: touchId}); + this.onSuccess({value: touchId || null}); this.resetValues(); return; } diff --git a/testing/marionette/command.js b/testing/marionette/command.js index 52bc32d3c479..905197a31ad7 100644 --- a/testing/marionette/command.js +++ b/testing/marionette/command.js @@ -14,94 +14,89 @@ Cu.import("chrome://marionette/content/error.js"); this.EXPORTED_SYMBOLS = ["CommandProcessor", "Response"]; const logger = Log.repository.getLogger("Marionette"); +const validator = { + exclusionary: { + "capabilities": ["error", "value"], + "error": ["value", "sessionId", "capabilities"], + "sessionId": ["error", "value"], + "value": ["error", "sessionId", "capabilities"], + }, + + set: function(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} property on the body when + * {@code value}, {@code sessionId}, or {@code capabilities} have been + * set previously will cause an error. + */ +this.ResponseBody = () => new Proxy({}, validator); + /** * 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. The response is sent - * implicitly by CommandProcessor when a command is finished executing, - * and any modifications made subsequent to this will have no effect. + * 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 CommandProcessor when a command + * has finished executing, and any modifications made subsequent to that + * will have no effect. * * @param {number} cmdId * UUID tied to the corresponding command request this is * a response for. - * @param {function(number)} okHandler - * Callback function called on successful responses with no body. * @param {function(Object, number)} respHandler - * Callback function called on successful responses with body. - * @param {Object=} msg - * A message to populate the response, containing the properties - * "sessionId", "status", and "value". - * @param {function(Map)=} sanitizer - * Run before sending message. + * Callback function called on responses. */ -this.Response = function(cmdId, okHandler, respHandler, msg, sanitizer) { - const removeEmpty = function(map) { - let rv = {}; - for (let [key, value] of map) { - if (typeof value == "undefined") { - value = null; - } - rv[key] = value; - } - return rv; - }; - +this.Response = function(cmdId, respHandler) { this.id = cmdId; - this.ok = true; - this.okHandler = okHandler; this.respHandler = respHandler; - this.sanitizer = sanitizer || removeEmpty; - - this.data = new Map([ - ["sessionId", msg.sessionId ? msg.sessionId : null], - ["status", msg.status ? msg.status : "success"], - ["value", msg.value ? msg.value : undefined], - ]); -}; - -Response.prototype = { - get name() { return this.data.get("name"); }, - set name(n) { this.data.set("name", n); }, - get sessionId() { return this.data.get("sessionId"); }, - set sessionId(id) { this.data.set("sessionId", id); }, - get status() { return this.data.get("status"); }, - set status(ns) { this.data.set("status", ns); }, - get value() { return this.data.get("value"); }, - set value(val) { - this.data.set("value", val); - this.ok = false; - } + this.sent = false; + this.body = ResponseBody(); }; Response.prototype.send = function() { if (this.sent) { - logger.warn("Skipped sending response to command ID " + - this.id + " because response has already been sent"); - return; - } - - if (this.ok) { - this.okHandler(this.id); - } else { - let rawData = this.sanitizer(this.data); - this.respHandler(rawData, this.id); + throw new RangeError("Response has already been sent: " + this.toString()); } + this.respHandler(this.body, this.id); + this.sent = true; }; -/** - * @param {(Error|Object)} err - * The error to send, either an instance of the Error prototype, - * or an object with the properties "message", "status", and "stack". - */ Response.prototype.sendError = function(err) { - this.status = "status" in err ? err.status : new UnknownError().status; - this.value = error.toJSON(err); + let wd = error.isWebDriverError(err); + let we = wd ? err : new WebDriverError(err.message); + + this.body.error = we.status; + this.body.message = we.message || null; + this.body.stacktrace = we.stack || null; + this.send(); // propagate errors that are implementation problems - if (!error.isWebDriverError(err)) { + if (!wd) { throw err; } }; @@ -131,17 +126,14 @@ this.CommandProcessor = function(driver) { * * @param {Object} payload * Message as received from client. - * @param {function(number)} okHandler - * Callback function called on successful responses with no body. * @param {function(Object, number)} respHandler - * Callback function called on successful responses with body. + * Callback function called on responses. * @param {number} cmdId * The unique identifier for the command to execute. */ -CommandProcessor.prototype.execute = function(payload, okHandler, respHandler, cmdId) { +CommandProcessor.prototype.execute = function(payload, respHandler, cmdId) { let cmd = payload; - let resp = new Response( - cmdId, okHandler, respHandler, {sessionId: this.driver.sessionId}); + let resp = new Response(cmdId, respHandler); let sendResponse = resp.send.bind(resp); let sendError = resp.sendError.bind(resp); @@ -156,7 +148,15 @@ CommandProcessor.prototype.execute = function(payload, okHandler, respHandler, c throw new UnknownCommandError(cmd.name); } - yield fn.bind(this.driver)(cmd, resp); + let rv = yield fn.bind(this.driver)(cmd, resp); + + if (typeof rv != "undefined") { + if (typeof rv != "object") { + resp.body = {value: rv}; + } else { + resp.body = rv; + } + } }.bind(this)); req.then(sendResponse, sendError).catch(error.report); diff --git a/testing/marionette/dispatcher.js b/testing/marionette/dispatcher.js index 1c5885214ceb..511f214e9038 100644 --- a/testing/marionette/dispatcher.js +++ b/testing/marionette/dispatcher.js @@ -17,6 +17,8 @@ Cu.import("chrome://marionette/content/driver.js"); this.EXPORTED_SYMBOLS = ["Dispatcher"]; +const PROTOCOL_VERSION = 2; + const logger = Log.repository.getLogger("Marionette"); const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); @@ -38,12 +40,6 @@ this.Dispatcher = function(connId, transport, driverFactory, stopSignal) { this.id = connId; this.conn = transport; - // Marionette uses a protocol based on the debugger server, which - // requires passing back actor ID's with responses. Unlike the debugger - // server, we don't actually have multiple actors, so just use a dummy - // value of "0". - this.actorId = "0"; - // callback for when connection is closed this.onclose = null; @@ -64,20 +60,16 @@ this.Dispatcher = function(connId, transport, driverFactory, stopSignal) { * over those defined in this.driver.commands. */ Dispatcher.prototype.onPacket = function(packet) { - // Avoid using toSource and template strings (or touching the payload at all - // if not necessary) for the sake of memory use. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1150170 if (logger.level <= Log.Level.Debug) { - logger.debug(this.id + " -> (" + JSON.stringify(packet) + ")"); + logger.debug(this.id + " -> " + JSON.stringify(packet)); } if (this.requests && this.requests[packet.name]) { this.requests[packet.name].bind(this)(packet); } else { let id = this.beginNewCommand(); - let ok = this.sendOk.bind(this); let send = this.send.bind(this); - this.commandProcessor.execute(packet, ok, send, id); + this.commandProcessor.execute(packet, send, id); } }; @@ -94,11 +86,6 @@ Dispatcher.prototype.onClosed = function(status) { // Dispatcher specific command handlers: -Dispatcher.prototype.getMarionetteID = function() { - let id = this.beginNewCommand(); - this.sendResponse({from: "root", id: this.actorId}, id); -}; - Dispatcher.prototype.emulatorCmdResult = function(msg) { switch (this.driver.context) { case Context.CONTENT: @@ -122,10 +109,7 @@ Dispatcher.prototype.quitApplication = function(msg) { let id = this.beginNewCommand(); if (this.driver.appName != "Firefox") { - this.sendError({ - "message": "In app initiated quit only supported on Firefox", - "status": "webdriver error", - }, id); + this.sendError(new WebDriverError("In app initiated quit only supported in Firefox")); return; } @@ -145,64 +129,22 @@ Dispatcher.prototype.quitApplication = function(msg) { Dispatcher.prototype.sayHello = function() { let id = this.beginNewCommand(); - let yo = {from: "root", applicationType: "gecko", traits: []}; - this.sendResponse(yo, id); + let whatHo = { + applicationType: "gecko", + marionetteProtocol: PROTOCOL_VERSION, + }; + this.send(whatHo, id); }; Dispatcher.prototype.sendOk = function(cmdId) { - this.sendResponse({from: this.actorId, ok: true}, cmdId); + this.send({}, cmdId); }; Dispatcher.prototype.sendError = function(err, cmdId) { - let packet = { - from: this.actorId, - status: err.status, - sessionId: this.driver.sessionId, - error: err - }; - this.sendResponse(packet, cmdId); + let resp = new Response(cmdId, this.send.bind(this)); + resp.sendError(err); }; -/** - * Marshals and sends message to either client or emulator based on the - * provided {@code cmdId}. - * - * This routine produces a Marionette protocol packet, which is different - * to a WebDriver protocol response in that it contains an extra key - * {@code from} for the debugger transport actor ID. It also replaces the - * key {@code value} with {@code error} when {@code msg.status} isn't - * {@code 0}. - * - * @param {Object} msg - * Object with the properties {@code value}, {@code status}, and - * {@code sessionId}. - * @param {UUID} cmdId - * The unique identifier for the command the message is a response to. - */ -Dispatcher.prototype.send = function(msg, cmdId) { - let packet = { - from: this.actorId, - value: msg.value, - status: msg.status, - sessionId: msg.sessionId, - }; - - if (typeof packet.value == "undefined") { - packet.value = null; - } - - // the Marionette protocol sends errors using the "error" - // key instead of, as Selenium, "value" - if (!error.isSuccess(msg.status)) { - packet.error = packet.value; - delete packet.value; - } - - this.sendResponse(packet, cmdId); -}; - -// Low-level methods: - /** * Delegates message to client or emulator based on the provided * {@code cmdId}. The message is sent over the debugger transport socket. @@ -220,7 +162,7 @@ Dispatcher.prototype.send = function(msg, cmdId) { * The unique identifier for this payload. {@code -1} signifies * that it's an emulator callback. */ -Dispatcher.prototype.sendResponse = function(payload, cmdId) { +Dispatcher.prototype.send = function(payload, cmdId) { if (emulator.isCallback(cmdId)) { this.sendToEmulator(payload); } else { @@ -229,6 +171,8 @@ Dispatcher.prototype.sendResponse = function(payload, cmdId) { } }; +// Low-level methods: + /** * Send message to emulator over the debugger transport socket. * Notably this skips out-of-sync command checks. @@ -265,11 +209,8 @@ Dispatcher.prototype.sendToClient = function(payload, cmdId) { * and logs it. */ Dispatcher.prototype.sendRaw = function(dest, payload) { - // Avoid using toSource and template strings (or touching the payload at all - // if not necessary) for the sake of memory use. - // See https://bugzilla.mozilla.org/show_bug.cgi?id=1150170 if (logger.level <= Log.Level.Debug) { - logger.debug(this.id + " " + dest + " <- (" + JSON.stringify(payload) + ")"); + logger.debug(this.id + " " + dest + " <- " + JSON.stringify(payload)); } this.conn.send(payload); }; @@ -292,7 +233,6 @@ Dispatcher.prototype.isOutOfSync = function(cmdId) { }; Dispatcher.prototype.requests = { - getMarionetteID: Dispatcher.prototype.getMarionetteID, emulatorCmdResult: Dispatcher.prototype.emulatorCmdResult, quitApplication: Dispatcher.prototype.quitApplication }; diff --git a/testing/marionette/driver.js b/testing/marionette/driver.js index 2e713165151a..48d48a9bc03f 100644 --- a/testing/marionette/driver.js +++ b/testing/marionette/driver.js @@ -573,8 +573,8 @@ GeckoDriver.prototype.newSession = function(cmd, resp) { yield registerBrowsers; yield browserListening; - resp.sessionId = this.sessionId; - resp.value = this.sessionCapabilities; + resp.body.sessionId = this.sessionId; + resp.body.capabilities = this.sessionCapabilities; }; /** @@ -589,7 +589,7 @@ GeckoDriver.prototype.newSession = function(cmd, resp) { * numerical or string. */ GeckoDriver.prototype.getSessionCapabilities = function(cmd, resp) { - resp.value = this.sessionCapabilities; + resp.body.capabilities = this.sessionCapabilities; }; /** @@ -721,7 +721,7 @@ GeckoDriver.prototype.log = function(cmd, resp) { /** Return all logged messages. */ GeckoDriver.prototype.getLogs = function(cmd, resp) { - resp.value = this.marionetteLog.getLogs(); + resp.body = this.marionetteLog.getLogs(); }; /** @@ -743,7 +743,7 @@ GeckoDriver.prototype.setContext = function(cmd, resp) { /** Gets the context of the server, either "chrome" or "content". */ GeckoDriver.prototype.getContext = function(cmd, resp) { - resp.value = this.context.toString(); + resp.body.value = this.context.toString(); }; /** @@ -844,7 +844,7 @@ GeckoDriver.prototype.executeScriptInSandbox = function( // It's fine to pass on and modify resp here because // executeScriptInSandbox is the last function to be called // in execute and executeWithCallback respectively. - resp.value = this.curBrowser.elementManager.wrapValue(res); + resp.body.value = this.curBrowser.elementManager.wrapValue(res); } }; @@ -873,7 +873,7 @@ GeckoDriver.prototype.execute = function(cmd, resp, directInject) { } if (this.context == Context.CONTENT) { - resp.value = yield this.listener.executeScript({ + resp.body.value = yield this.listener.executeScript({ script: script, args: args, newSandbox: newSandbox, @@ -986,7 +986,7 @@ GeckoDriver.prototype.executeJSScript = function(cmd, resp) { break; case Context.CONTENT: - resp.value = yield this.listener.executeJSScript({ + resp.body.value = yield this.listener.executeJSScript({ script: cmd.parameters.script, args: cmd.parameters.args, newSandbox: cmd.parameters.newSandbox, @@ -1025,7 +1025,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { scriptTimeout, filename, line} = cmd.parameters; - let sandboxName = cmd.parameters.sandbox || 'default'; + let sandboxName = cmd.parameters.sandbox || "default"; if (!scriptTimeout) { scriptTimeout = this.scriptTimeout; @@ -1035,7 +1035,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { } if (this.context == Context.CONTENT) { - resp.value = yield this.listener.executeAsyncScript({ + resp.body.value = yield this.listener.executeAsyncScript({ script: script, args: args, id: cmd.id, @@ -1184,7 +1184,7 @@ GeckoDriver.prototype.executeWithCallback = function(cmd, resp, directInject) { } }.bind(this)); - resp.value = that.curBrowser.elementManager.wrapValue(res); + resp.body.value = that.curBrowser.elementManager.wrapValue(res) || null; }; /** @@ -1282,15 +1282,15 @@ GeckoDriver.prototype.pageLoadPromise = function() { * When in the context of the chrome, this returns the canonical URL * of the current resource. */ -GeckoDriver.prototype.getCurrentUrl = function(cmd, resp) { +GeckoDriver.prototype.getCurrentUrl = function(cmd) { switch (this.context) { case Context.CHROME: - resp.value = this.getCurrentWindow().location.href; + return this.getCurrentWindow().location.href; break; case Context.CONTENT: let isB2G = this.appName == "B2G"; - resp.value = yield this.listener.getCurrentUrl(isB2G); + return this.listener.getCurrentUrl(isB2G); break; } }; @@ -1300,11 +1300,11 @@ GeckoDriver.prototype.getTitle = function(cmd, resp) { switch (this.context) { case Context.CHROME: let win = this.getCurrentWindow(); - resp.value = win.document.documentElement.getAttribute("title"); + resp.body.value = win.document.documentElement.getAttribute("title"); break; case Context.CONTENT: - resp.value = yield this.listener.getTitle(); + resp.body.value = yield this.listener.getTitle(); break; } }; @@ -1312,7 +1312,7 @@ GeckoDriver.prototype.getTitle = function(cmd, resp) { /** Gets the current type of the window. */ GeckoDriver.prototype.getWindowType = function(cmd, resp) { let win = this.getCurrentWindow(); - resp.value = win.document.documentElement.getAttribute("windowtype"); + resp.body.value = win.document.documentElement.getAttribute("windowtype"); }; /** Gets the page source of the content document. */ @@ -1321,11 +1321,11 @@ GeckoDriver.prototype.getPageSource = function(cmd, resp) { case Context.CHROME: let win = this.getCurrentWindow(); let s = new win.XMLSerializer(); - resp.value = s.serializeToString(win.document); + resp.body.value = s.serializeToString(win.document); break; case Context.CONTENT: - resp.value = yield this.listener.getPageSource(); + resp.body.value = yield this.listener.getPageSource(); break; } }; @@ -1359,13 +1359,13 @@ GeckoDriver.prototype.refresh = function(cmd, resp) { GeckoDriver.prototype.getWindowHandle = function(cmd, resp) { // curFrameId always holds the current tab. if (this.curBrowser.curFrameId && this.appName != "B2G") { - resp.value = this.curBrowser.curFrameId; + resp.body.value = this.curBrowser.curFrameId; return; } for (let i in this.browsers) { if (this.curBrowser == this.browsers[i]) { - resp.value = i; + resp.body.value = i; return; } } @@ -1412,7 +1412,7 @@ GeckoDriver.prototype.getIdForBrowser = function getIdForBrowser(browser) { * Unique window handles. */ GeckoDriver.prototype.getWindowHandles = function(cmd, resp) { - let rv = []; + let hs = []; let winEn = this.getWinEnumerator(); while (winEn.hasMoreElements()) { let win = winEn.getNext(); @@ -1421,7 +1421,7 @@ GeckoDriver.prototype.getWindowHandles = function(cmd, resp) { for (let i = 0; i < tabbrowser.browsers.length; ++i) { let winId = this.getIdForBrowser(tabbrowser.getBrowserAtIndex(i)); if (winId !== null) { - rv.push(winId); + hs.push(winId); } } } else { @@ -1430,10 +1430,10 @@ GeckoDriver.prototype.getWindowHandles = function(cmd, resp) { .getInterface(Ci.nsIDOMWindowUtils) .outerWindowID; winId += (this.appName == "B2G") ? "-b2g" : ""; - rv.push(winId); + hs.push(winId); } } - resp.value = rv; + resp.body = hs; }; /** @@ -1450,7 +1450,7 @@ GeckoDriver.prototype.getWindowHandles = function(cmd, resp) { GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) { for (let i in this.browsers) { if (this.curBrowser == this.browsers[i]) { - resp.value = i; + resp.body.value = i; return; } } @@ -1464,7 +1464,7 @@ GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) { * Unique window handles. */ GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) { - let rv = []; + let hs = []; let winEn = this.getWinEnumerator(); while (winEn.hasMoreElements()) { let foundWin = winEn.getNext(); @@ -1472,9 +1472,9 @@ GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) { .getInterface(Ci.nsIDOMWindowUtils) .outerWindowID; winId = winId + ((this.appName == "B2G") ? "-b2g" : ""); - rv.push(winId); + hs.push(winId); } - resp.value = rv; + resp.body = hs; }; /** @@ -1485,7 +1485,8 @@ GeckoDriver.prototype.getChromeWindowHandles = function(cmd, resp) { */ GeckoDriver.prototype.getWindowPosition = function(cmd, resp) { let win = this.getCurrentWindow(); - resp.value = {x: win.screenX, y: win.screenY}; + resp.body.x = win.screenX; + resp.body.y = win.screenY; }; /** @@ -1600,15 +1601,15 @@ GeckoDriver.prototype.getActiveFrame = function(cmd, resp) { switch (this.context) { case Context.CHROME: // no frame means top-level - resp.value = null; + resp.body.value = null; if (this.curFrame) { - resp.value = this.curBrowser.elementManager + resp.body.value = this.curBrowser.elementManager .addToKnownElements(this.curFrame.frameElement); } break; case Context.CONTENT: - resp.value = this.currentFrameElement; + resp.body.value = this.currentFrameElement; break; } }; @@ -1833,7 +1834,7 @@ GeckoDriver.prototype.actionChain = function(cmd, resp) { } let cbs = {}; - cbs.onSuccess = val => resp.value = val; + cbs.onSuccess = val => resp.body.value = val; cbs.onError = err => { throw err; }; let win = this.getCurrentWindow(); @@ -1843,7 +1844,7 @@ GeckoDriver.prototype.actionChain = function(cmd, resp) { case Context.CONTENT: this.addFrameCloseListener("action chain"); - resp.value = yield this.listener.actionChain({chain: chain, nextId: nextId}); + resp.body.value = yield this.listener.actionChain({chain: chain, nextId: nextId}); break; } }; @@ -1880,7 +1881,7 @@ GeckoDriver.prototype.multiAction = function(cmd, resp) { GeckoDriver.prototype.findElement = function(cmd, resp) { switch (this.context) { case Context.CHROME: - resp.value = yield new Promise((resolve, reject) => { + resp.body.value = yield new Promise((resolve, reject) => { let win = this.getCurrentWindow(); this.curBrowser.elementManager.find( { frame: win }, @@ -1893,7 +1894,7 @@ GeckoDriver.prototype.findElement = function(cmd, resp) { break; case Context.CONTENT: - resp.value = yield this.listener.findElementContent({ + resp.body.value = yield this.listener.findElementContent({ value: cmd.parameters.value, using: cmd.parameters.using, element: cmd.parameters.element, @@ -1914,7 +1915,7 @@ GeckoDriver.prototype.findElement = function(cmd, resp) { * Value of the element to start from. */ GeckoDriver.prototype.findChildElement = function(cmd, resp) { - resp.value = yield this.listener.findElementContent({ + resp.body.value = yield this.listener.findElementContent({ value: cmd.parameters.value, using: cmd.parameters.using, element: cmd.parameters.id, @@ -1932,7 +1933,7 @@ GeckoDriver.prototype.findChildElement = function(cmd, resp) { GeckoDriver.prototype.findElements = function(cmd, resp) { switch (this.context) { case Context.CHROME: - resp.value = yield new Promise((resolve, reject) => { + resp.body = yield new Promise((resolve, reject) => { let win = this.getCurrentWindow(); this.curBrowser.elementManager.find( { frame: win }, @@ -1945,11 +1946,11 @@ GeckoDriver.prototype.findElements = function(cmd, resp) { break; case Context.CONTENT: - resp.value = yield this.listener.findElementsContent({ - value: cmd.parameters.value, - using: cmd.parameters.using, - element: cmd.parameters.element, - searchTimeout: this.searchTimeout}); + resp.body = yield this.listener.findElementsContent({ + value: cmd.parameters.value, + using: cmd.parameters.using, + element: cmd.parameters.element, + searchTimeout: this.searchTimeout}); break; } }; @@ -1966,7 +1967,7 @@ GeckoDriver.prototype.findElements = function(cmd, resp) { * Value of the element to start from. */ GeckoDriver.prototype.findChildElements = function(cmd, resp) { - resp.value = yield this.listener.findElementsContent({ + resp.body.value = yield this.listener.findElementsContent({ value: cmd.parameters.value, using: cmd.parameters.using, element: cmd.parameters.id, @@ -1975,7 +1976,7 @@ GeckoDriver.prototype.findChildElements = function(cmd, resp) { /** Return the active element on the page. */ GeckoDriver.prototype.getActiveElement = function(cmd, resp) { - resp.value = yield this.listener.getActiveElement(); + resp.body.value = yield this.listener.getActiveElement(); }; /** @@ -2020,12 +2021,12 @@ GeckoDriver.prototype.getElementAttribute = function(cmd, resp) { switch (this.context) { case Context.CHROME: let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); - resp.value = utils.getElementAttribute(el, name); + let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win}); + resp.body.value = utils.getElementAttribute(el, name); break; case Context.CONTENT: - resp.value = yield this.listener.getElementAttribute(id, name); + resp.body.value = yield this.listener.getElementAttribute(id, name); break; } }; @@ -2047,11 +2048,11 @@ GeckoDriver.prototype.getElementText = function(cmd, resp) { let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); let lines = []; this.getVisibleText(el, lines); - resp.value = lines.join("\n"); + resp.body.value = lines.join("\n"); break; case Context.CONTENT: - resp.value = yield this.listener.getElementText(id); + resp.body.value = yield this.listener.getElementText(id); break; } }; @@ -2068,12 +2069,12 @@ GeckoDriver.prototype.getElementTagName = function(cmd, resp) { switch (this.context) { case Context.CHROME: let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); - resp.value = el.tagName.toLowerCase(); + let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win}); + resp.body.value = el.tagName.toLowerCase(); break; case Context.CONTENT: - resp.value = yield this.listener.getElementTagName(id); + resp.body.value = yield this.listener.getElementTagName(id); break; } }; @@ -2090,12 +2091,12 @@ GeckoDriver.prototype.isElementDisplayed = function(cmd, resp) { switch (this.context) { case Context.CHROME: let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); - resp.value = utils.isElementDisplayed(el); + let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win}); + resp.body.value = utils.isElementDisplayed(el); break; case Context.CONTENT: - resp.value = yield this.listener.isElementDisplayed(id); + resp.body.value = yield this.listener.isElementDisplayed(id); break; } }; @@ -2116,11 +2117,11 @@ GeckoDriver.prototype.getElementValueOfCssProperty = function(cmd, resp) { let win = this.getCurrentWindow(); let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); let sty = win.document.defaultView.getComputedStyle(el, null); - resp.value = sty.getPropertyValue(prop); + resp.body.value = sty.getPropertyValue(prop); break; case Context.CONTENT: - resp.value = yield this.listener.getElementValueOfCssProperty(id, prop); + resp.body.value = yield this.listener.getElementValueOfCssProperty(id, prop); break; } }; @@ -2138,12 +2139,12 @@ GeckoDriver.prototype.isElementEnabled = function(cmd, resp) { case Context.CHROME: // Selenium atom doesn't quite work here let win = this.getCurrentWindow(); - let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); - resp.value = !(!!el.disabled); + let el = this.curBrowser.elementManager.getKnownElement(id, {frame: win}); + resp.body.value = !(!!el.disabled); break; case Context.CONTENT: - resp.value = yield this.listener.isElementEnabled(id); + resp.body.value = yield this.listener.isElementEnabled(id); break; } }, @@ -2163,16 +2164,16 @@ GeckoDriver.prototype.isElementSelected = function(cmd, resp) { let win = this.getCurrentWindow(); let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); if (typeof el.checked != "undefined") { - resp.value = !!el.checked; + resp.body.value = !!el.checked; } else if (typeof el.selected != "undefined") { - resp.value = !!el.selected; + resp.body.value = !!el.selected; } else { - resp.value = true; + resp.body.value = true; } break; case Context.CONTENT: - resp.value = yield this.listener.isElementSelected(id); + resp.body.value = yield this.listener.isElementSelected(id); break; } }; @@ -2185,11 +2186,12 @@ GeckoDriver.prototype.getElementSize = function(cmd, resp) { let win = this.getCurrentWindow(); let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); let rect = el.getBoundingClientRect(); - resp.value = {width: rect.width, height: rect.height}; + resp.body.width = rect.width; + resp.body.height = rect.height; break; case Context.CONTENT: - resp.value = yield this.listener.getElementSize(id); + resp.body = yield this.listener.getElementSize(id); break; } }; @@ -2202,7 +2204,7 @@ GeckoDriver.prototype.getElementRect = function(cmd, resp) { let win = this.getCurrentWindow(); let el = this.curBrowser.elementManager.getKnownElement(id, { frame: win }); let rect = el.getBoundingClientRect(); - resp.value = { + resp.body = { x: rect.x + win.pageXOffset, y: rect.y + win.pageYOffset, width: rect.width, @@ -2211,7 +2213,7 @@ GeckoDriver.prototype.getElementRect = function(cmd, resp) { break; case Context.CONTENT: - resp.value = yield this.listener.getElementRect(id); + resp.body = yield this.listener.getElementRect(id); break; } }; @@ -2351,7 +2353,7 @@ GeckoDriver.prototype.addCookie = function(cmd, resp) { * the result. */ GeckoDriver.prototype.getCookies = function(cmd, resp) { - resp.value = yield this.listener.getCookies(); + resp.body = yield this.listener.getCookies(); }; /** Delete all cookies that are visible to a document. */ @@ -2515,7 +2517,7 @@ GeckoDriver.prototype.deleteSession = function(cmd, resp) { /** Returns the current status of the Application Cache. */ GeckoDriver.prototype.getAppCacheStatus = function(cmd, resp) { - resp.value = yield this.listener.getAppCacheStatus(); + resp.body.value = yield this.listener.getAppCacheStatus(); }; GeckoDriver.prototype.importScript = function(cmd, resp) { @@ -2633,11 +2635,11 @@ GeckoDriver.prototype.takeScreenshot = function(cmd, resp) { context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags); let dataUrl = canvas.toDataURL("image/png", ""); let data = dataUrl.substring(dataUrl.indexOf(",") + 1); - resp.value = data; + resp.body.value = data; break; case Context.CONTENT: - resp.value = yield this.listener.takeScreenshot({ + resp.body.value = yield this.listener.takeScreenshot({ id: cmd.parameters.id, highlights: cmd.parameters.highlights, full: cmd.parameters.full}); @@ -2653,7 +2655,7 @@ GeckoDriver.prototype.takeScreenshot = function(cmd, resp) { * landscape-secondary. */ GeckoDriver.prototype.getScreenOrientation = function(cmd, resp) { - resp.value = this.getCurrentWindow().screen.mozOrientation; + resp.body.value = this.getCurrentWindow().screen.mozOrientation; }; /** @@ -2695,7 +2697,8 @@ GeckoDriver.prototype.setScreenOrientation = function(cmd, resp) { */ GeckoDriver.prototype.getWindowSize = function(cmd, resp) { let win = this.getCurrentWindow(); - resp.value = {width: win.outerWidth, height: win.outerHeight}; + resp.body.width = win.outerWidth; + resp.body.height = win.outerHeight; }; /** @@ -2781,7 +2784,7 @@ GeckoDriver.prototype.getTextFromDialog = function(cmd, resp) { } let {infoBody} = this.dialog.ui; - resp.value = infoBody.textContent; + resp.body.value = infoBody.textContent; }; /** diff --git a/testing/marionette/error.js b/testing/marionette/error.js index 9a96b988545d..f49e39600960 100644 --- a/testing/marionette/error.js +++ b/testing/marionette/error.js @@ -53,32 +53,6 @@ const XPCOM_EXCEPTIONS = []; this.error = {}; -/** - * Marshals an error object into a WebDriver protocol error. The given - * error can be a prototypal Error or an object, as long as it has the - * properties message, stack, and status. - * - * If err is a native JavaScript error, the returned object's message - * property will be changed to include the error's name. - * - * @param {Object} err - * Object with the properties message, stack, and status. - * - * @return {Object} - * Object with the properties message, stacktrace, and status. - */ -error.toJSON = function(err) { - let msg = err.message; - if (!error.isWebDriverError(err) && "name" in error) { - msg = `${err.name}: ${msg}`; - } - return { - message: msg, - stacktrace: err.stack || null, - status: err.status - }; -}; - /** * Determines if the given status is successful. */ @@ -110,7 +84,7 @@ error.isError = function(val) { */ error.isWebDriverError = function(obj) { return error.isError(obj) && - ("name" in obj && errors.indexOf(obj.name) > 0); + ("name" in obj && errors.indexOf(obj.name) >= 0); }; /** diff --git a/testing/marionette/listener.js b/testing/marionette/listener.js index 5f0830e8c92c..fb895ebfc0dd 100644 --- a/testing/marionette/listener.js +++ b/testing/marionette/listener.js @@ -213,6 +213,7 @@ let clearElementFn = dispatch(clearElement); let isElementDisplayedFn = dispatch(isElementDisplayed); let getElementValueOfCssPropertyFn = dispatch(getElementValueOfCssProperty); let switchToShadowRootFn = dispatch(switchToShadowRoot); +let getCookiesFn = dispatch(getCookies); /** * Start all message listeners @@ -261,7 +262,7 @@ function startListeners() { addMessageListenerId("Marionette:setTestName", setTestName); addMessageListenerId("Marionette:takeScreenshot", takeScreenshot); addMessageListenerId("Marionette:addCookie", addCookie); - addMessageListenerId("Marionette:getCookies", getCookies); + addMessageListenerId("Marionette:getCookies", getCookiesFn); addMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies); addMessageListenerId("Marionette:deleteCookie", deleteCookie); } @@ -367,7 +368,7 @@ function deleteSession(msg) { removeMessageListenerId("Marionette:setTestName", setTestName); removeMessageListenerId("Marionette:takeScreenshot", takeScreenshot); removeMessageListenerId("Marionette:addCookie", addCookie); - removeMessageListenerId("Marionette:getCookies", getCookies); + removeMessageListenerId("Marionette:getCookies", getCookiesFn); removeMessageListenerId("Marionette:deleteAllCookies", deleteAllCookies); removeMessageListenerId("Marionette:deleteCookie", deleteCookie); if (isB2G) { @@ -1878,17 +1879,20 @@ function addCookie(msg) { /** * Get all cookies for the current domain. */ -function getCookies(msg) { - var toReturn = []; - var cookies = getVisibleCookies(curContainer.frame.location); +function getCookies() { + let rv = []; + let cookies = getVisibleCookies(curContainer.frame.location); + for (let cookie of cookies) { - var expires = cookie.expires; - if (expires == 0) { // Session cookie, don't return an expiry. + let expires = cookie.expires; + // session cookie, don't return an expiry + if (expires == 0) { expires = null; - } else if (expires == 1) { // Date before epoch time, cap to epoch. + // date before epoch time, cap to epoch + } else if (expires == 1) { expires = 0; } - toReturn.push({ + rv.push({ 'name': cookie.name, 'value': cookie.value, 'path': cookie.path, @@ -1898,7 +1902,8 @@ function getCookies(msg) { 'expiry': expires }); } - sendResponse({value: toReturn}, msg.json.command_id); + + return rv; } /**