/* 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 {pprint} = ChromeUtils.import("chrome://marionette/content/format.js"); const ERRORS = new Set([ "ElementClickInterceptedError", "ElementNotAccessibleError", "ElementNotInteractableError", "InsecureCertificateError", "InvalidArgumentError", "InvalidCookieDomainError", "InvalidElementStateError", "InvalidSelectorError", "InvalidSessionIDError", "JavaScriptError", "MoveTargetOutOfBoundsError", "NoSuchAlertError", "NoSuchElementError", "NoSuchFrameError", "NoSuchWindowError", "ScriptTimeoutError", "SessionNotCreatedError", "StaleElementReferenceError", "TimeoutError", "UnableToSetCookieError", "UnexpectedAlertOpenError", "UnknownCommandError", "UnknownError", "UnsupportedOperationError", "WebDriverError", ]); const BUILTIN_ERRORS = new Set([ "Error", "EvalError", "InternalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", ]); this.EXPORTED_SYMBOLS = [ "error", "stack", ].concat(Array.from(ERRORS)); /** @namespace */ this.error = {}; /** * Check if ``val`` is an instance of the ``Error`` prototype. * * Because error objects may originate from different globals, comparing * the prototype of the left hand side with the prototype property from * the right hand side, which is what ``instanceof`` does, will not work. * If the LHS and RHS come from different globals, this check will always * fail because the two objects will not have the same identity. * * Therefore it is not safe to use ``instanceof`` in any multi-global * situation, e.g. in content across multiple ``Window`` objects or anywhere * in chrome scope. * * This function also contains a special check if ``val`` is an XPCOM * ``nsIException`` because they are special snowflakes and may indeed * cause Firefox to crash if used with ``instanceof``. * * @param {*} val * Any value that should be undergo the test for errorness. * @return {boolean} * True if error, false otherwise. */ error.isError = function(val) { if (val === null || typeof val != "object") { return false; } else if (val instanceof Ci.nsIException) { return true; } // DOMRectList errors on string comparison try { let proto = Object.getPrototypeOf(val); return BUILTIN_ERRORS.has(proto.toString()); } catch (e) { return false; } }; /** * Checks if ``obj`` is an object in the :js:class:`WebDriverError` * prototypal chain. * * @param {*} obj * Arbitrary object to test. * * @return {boolean} * True if ``obj`` is of the WebDriverError prototype chain, * false otherwise. */ error.isWebDriverError = function(obj) { return error.isError(obj) && ("name" in obj && ERRORS.has(obj.name)); }; /** * Ensures error instance is a :js:class:`WebDriverError`. * * If the given error is already in the WebDriverError prototype * chain, ``err`` is returned unmodified. If it is not, it is wrapped * in :js:class:`UnknownError`. * * @param {Error} err * Error to conditionally turn into a WebDriverError. * * @return {WebDriverError} * If ``err`` is a WebDriverError, it is returned unmodified. * Otherwise an UnknownError type is returned. */ error.wrap = function(err) { if (error.isWebDriverError(err)) { return err; } return new UnknownError(err); }; /** * Unhandled error reporter. Dumps the error and its stacktrace to console, * and reports error to the Browser Console. */ error.report = function(err) { let msg = "Marionette threw an error: " + error.stringify(err); dump(msg + "\n"); if (Cu.reportError) { Cu.reportError(msg); } }; /** * Prettifies an instance of Error and its stacktrace to a string. */ error.stringify = function(err) { try { let s = err.toString(); if ("stack" in err) { s += "\n" + err.stack; } return s; } catch (e) { return ""; } }; /** Create a stacktrace to the current line in the program. */ this.stack = function() { let trace = new Error().stack; let sa = trace.split("\n"); sa = sa.slice(1); let rv = "stacktrace:\n" + sa.join("\n"); return rv.trimEnd(); }; /** * WebDriverError is the prototypal parent of all WebDriver errors. * It should not be used directly, as it does not correspond to a real * error in the specification. */ class WebDriverError extends Error { /** * @param {(string|Error)=} x * Optional string describing error situation or Error instance * to propagate. */ constructor(x) { super(x); this.name = this.constructor.name; this.status = "webdriver error"; // Error's ctor does not preserve x' stack if (error.isError(x)) { this.stack = x.stack; } } /** * @return {Object.} * JSON serialisation of error prototype. */ toJSON() { return { error: this.status, message: this.message || "", stacktrace: this.stack || "", }; } /** * Unmarshals a JSON error representation to the appropriate Marionette * error type. * * @param {Object.} json * Error object. * * @return {Error} * Error prototype. */ static fromJSON(json) { if (typeof json.error == "undefined") { let s = JSON.stringify(json); throw new TypeError("Undeserialisable error type: " + s); } if (!STATUSES.has(json.error)) { throw new TypeError("Not of WebDriverError descent: " + json.error); } let cls = STATUSES.get(json.error); let err = new cls(); if ("message" in json) { err.message = json.message; } if ("stacktrace" in json) { err.stack = json.stacktrace; } return err; } } /** The Gecko a11y API indicates that the element is not accessible. */ class ElementNotAccessibleError extends WebDriverError { constructor(message) { super(message); this.status = "element not accessible"; } } /** * An element click could not be completed because the element receiving * the events is obscuring the element that was requested clicked. * * @param {Element=} obscuredEl * Element obscuring the element receiving the click. Providing this * is not required, but will produce a nicer error message. * @param {Map.} coords * Original click location. Providing this is not required, but * will produce a nicer error message. */ class ElementClickInterceptedError extends WebDriverError { constructor(obscuredEl = undefined, coords = undefined) { let msg = ""; if (obscuredEl && coords) { const doc = obscuredEl.ownerDocument; const overlayingEl = doc.elementFromPoint(coords.x, coords.y); switch (obscuredEl.style.pointerEvents) { case "none": msg = pprint`Element ${obscuredEl} is not clickable ` + `at point (${coords.x},${coords.y}) ` + `because it does not have pointer events enabled, ` + pprint`and element ${overlayingEl} ` + `would receive the click instead`; break; default: msg = pprint`Element ${obscuredEl} is not clickable ` + `at point (${coords.x},${coords.y}) ` + pprint`because another element ${overlayingEl} ` + `obscures it`; break; } } super(msg); this.status = "element click intercepted"; } } /** * A command could not be completed because the element is not pointer- * or keyboard interactable. */ class ElementNotInteractableError extends WebDriverError { constructor(message) { super(message); this.status = "element not interactable"; } } /** * Navigation caused the user agent to hit a certificate warning, which * is usually the result of an expired or invalid TLS certificate. */ class InsecureCertificateError extends WebDriverError { constructor(message) { super(message); this.status = "insecure certificate"; } } /** The arguments passed to a command are either invalid or malformed. */ class InvalidArgumentError extends WebDriverError { constructor(message) { super(message); this.status = "invalid argument"; } } /** * An illegal attempt was made to set a cookie under a different * domain than the current page. */ class InvalidCookieDomainError extends WebDriverError { constructor(message) { super(message); this.status = "invalid cookie domain"; } } /** * A command could not be completed because the element is in an * invalid state, e.g. attempting to clear an element that isn't both * editable and resettable. */ class InvalidElementStateError extends WebDriverError { constructor(message) { super(message); this.status = "invalid element state"; } } /** Argument was an invalid selector. */ class InvalidSelectorError extends WebDriverError { constructor(message) { super(message); this.status = "invalid selector"; } } /** * Occurs if the given session ID is not in the list of active sessions, * meaning the session either does not exist or that it's not active. */ class InvalidSessionIDError extends WebDriverError { constructor(message) { super(message); this.status = "invalid session id"; } } /** An error occurred whilst executing JavaScript supplied by the user. */ class JavaScriptError extends WebDriverError { constructor(x) { super(x); this.status = "javascript error"; } } /** * The target for mouse interaction is not in the browser's viewport * and cannot be brought into that viewport. */ class MoveTargetOutOfBoundsError extends WebDriverError { constructor(message) { super(message); this.status = "move target out of bounds"; } } /** * An attempt was made to operate on a modal dialog when one was * not open. */ class NoSuchAlertError extends WebDriverError { constructor(message) { super(message); this.status = "no such alert"; } } /** * An element could not be located on the page using the given * search parameters. */ class NoSuchElementError extends WebDriverError { constructor(message) { super(message); this.status = "no such element"; } } /** * A command to switch to a frame could not be satisfied because * the frame could not be found. */ class NoSuchFrameError extends WebDriverError { constructor(message) { super(message); this.status = "no such frame"; } } /** * A command to switch to a window could not be satisfied because * the window could not be found. */ class NoSuchWindowError extends WebDriverError { constructor(message) { super(message); this.status = "no such window"; } } /** A script did not complete before its timeout expired. */ class ScriptTimeoutError extends WebDriverError { constructor(message) { super(message); this.status = "script timeout"; } } /** A new session could not be created. */ class SessionNotCreatedError extends WebDriverError { constructor(message) { super(message); this.status = "session not created"; } } /** * A command failed because the referenced element is no longer * attached to the DOM. */ class StaleElementReferenceError extends WebDriverError { constructor(message) { super(message); this.status = "stale element reference"; } } /** An operation did not complete before its timeout expired. */ class TimeoutError extends WebDriverError { constructor(message) { super(message); this.status = "timeout"; } } /** A command to set a cookie's value could not be satisfied. */ class UnableToSetCookieError extends WebDriverError { constructor(message) { super(message); this.status = "unable to set cookie"; } } /** A modal dialog was open, blocking this operation. */ class UnexpectedAlertOpenError extends WebDriverError { constructor(message) { super(message); this.status = "unexpected alert open"; } } /** * A command could not be executed because the remote end is not * aware of it. */ class UnknownCommandError extends WebDriverError { constructor(message) { super(message); this.status = "unknown command"; } } /** * An unknown error occurred in the remote end while processing * the command. */ class UnknownError extends WebDriverError { constructor(message) { super(message); this.status = "unknown error"; } } /** * Indicates that a command that should have executed properly * cannot be supported for some reason. */ class UnsupportedOperationError extends WebDriverError { constructor(message) { super(message); this.status = "unsupported operation"; } } const STATUSES = new Map([ ["element click intercepted", ElementClickInterceptedError], ["element not accessible", ElementNotAccessibleError], ["element not interactable", ElementNotInteractableError], ["insecure certificate", InsecureCertificateError], ["invalid argument", InvalidArgumentError], ["invalid cookie domain", InvalidCookieDomainError], ["invalid element state", InvalidElementStateError], ["invalid selector", InvalidSelectorError], ["invalid session id", InvalidSessionIDError], ["javascript error", JavaScriptError], ["move target out of bounds", MoveTargetOutOfBoundsError], ["no such alert", NoSuchAlertError], ["no such element", NoSuchElementError], ["no such frame", NoSuchFrameError], ["no such window", NoSuchWindowError], ["script timeout", ScriptTimeoutError], ["session not created", SessionNotCreatedError], ["stale element reference", StaleElementReferenceError], ["timeout", TimeoutError], ["unable to set cookie", UnableToSetCookieError], ["unexpected alert open", UnexpectedAlertOpenError], ["unknown command", UnknownCommandError], ["unknown error", UnknownError], ["unsupported operation", UnsupportedOperationError], ["webdriver error", WebDriverError], ]); // Errors must be expored on the local this scope so that the // EXPORTED_SYMBOLS and the Cu.import("foo", {}) machinery sees them. // We could assign each error definition directly to |this|, but // because they are Error prototypes this would mess up their names. for (let cls of STATUSES.values()) { this[cls.name] = cls; }