diff options
Diffstat (limited to 'testing/marionette/error.js')
-rw-r--r-- | testing/marionette/error.js | 538 |
1 files changed, 538 insertions, 0 deletions
diff --git a/testing/marionette/error.js b/testing/marionette/error.js new file mode 100644 index 0000000000..d8c8263934 --- /dev/null +++ b/testing/marionette/error.js @@ -0,0 +1,538 @@ +/* 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 = ["error"]; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + pprint: "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", +]); + +/** @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. + */ + isError(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. + */ + isWebDriverError(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. + */ + wrap(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. + */ + report(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. + */ + stringify(err) { + try { + let s = err.toString(); + if ("stack" in err) { + s += "\n" + err.stack; + } + return s; + } catch (e) { + return "<unprintable error>"; + } + }, + + /** Create a stacktrace to the current line in the program. */ + stack() { + 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.<string, string>} + * 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.<string, string>} 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.<string, number>} 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()) { + error[cls.name] = cls; +} |