diff options
Diffstat (limited to 'testing/marionette/error.js')
-rw-r--r-- | testing/marionette/error.js | 487 |
1 files changed, 487 insertions, 0 deletions
diff --git a/testing/marionette/error.js b/testing/marionette/error.js new file mode 100644 index 000000000..97cc3fb25 --- /dev/null +++ b/testing/marionette/error.js @@ -0,0 +1,487 @@ +/* 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 {interfaces: Ci, utils: Cu} = Components; + +const ERRORS = new Set([ + "ElementClickInterceptedError", + "ElementNotAccessibleError", + "ElementNotInteractableError", + "InsecureCertificateError", + "InvalidArgumentError", + "InvalidElementStateError", + "InvalidSelectorError", + "InvalidSessionIDError", + "JavaScriptError", + "MoveTargetOutOfBoundsError", + "NoAlertOpenError", + "NoSuchElementError", + "NoSuchFrameError", + "NoSuchWindowError", + "ScriptTimeoutError", + "SessionNotCreatedError", + "StaleElementReferenceError", + "TimeoutError", + "UnableToSetCookieError", + "UnknownCommandError", + "UnknownError", + "UnsupportedOperationError", + "WebDriverError", +]); + +const BUILTIN_ERRORS = new Set([ + "Error", + "EvalError", + "InternalError", + "RangeError", + "ReferenceError", + "SyntaxError", + "TypeError", + "URIError", +]); + +this.EXPORTED_SYMBOLS = ["error"].concat(Array.from(ERRORS)); + +this.error = {}; + +/** + * Checks if obj is an instance of the Error prototype in a safe manner. + * Prefer using this over using instanceof since the Error prototype + * isn't unique across browsers, and XPCOM nsIException's are special + * snowflakes. + * + * @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; + } else { + // 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 WebDriverError prototypal chain. + */ +error.isWebDriverError = function (obj) { + return error.isError(obj) && + ("name" in obj && ERRORS.has(obj.name)); +}; + +/** + * Wraps any error as a WebDriverError. If the given error is already in + * the WebDriverError prototype chain, this function returns it + * unmodified. + */ +error.wrap = function (err) { + if (error.isWebDriverError(err)) { + return err; + } + return new WebDriverError(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 "<unprintable error>"; + } +}; + +/** + * Pretty-print values passed to template strings. + * + * Usage: + * + * let bool = {value: true}; + * error.pprint`Expected boolean, got ${bool}`; + * => 'Expected boolean, got [object Object] {"value": true}' + * + * let htmlElement = document.querySelector("input#foo"); + * error.pprint`Expected element ${htmlElement}`; + * => 'Expected element <input id="foo" class="bar baz">' + */ +error.pprint = function (ss, ...values) { + function prettyObject (obj) { + let proto = Object.prototype.toString.call(obj); + let s = ""; + try { + s = JSON.stringify(obj); + } catch (e if e instanceof TypeError) { + s = `<${e.message}>`; + } + return proto + " " + s; + } + + function prettyElement (el) { + let ident = []; + if (el.id) { + ident.push(`id="${el.id}"`); + } + if (el.classList.length > 0) { + ident.push(`class="${el.className}"`); + } + + let idents = ""; + if (ident.length > 0) { + idents = " " + ident.join(" "); + } + + return `<${el.localName}${idents}>`; + } + + let res = []; + for (let i = 0; i < ss.length; i++) { + res.push(ss[i]); + if (i < values.length) { + let val = values[i]; + let typ = Object.prototype.toString.call(val); + let s; + try { + if (val && val.nodeType === 1) { + s = prettyElement(val); + } else { + s = prettyObject(val); + } + } catch (e) { + s = typeof val; + } + res.push(s); + } + } + return res.join(""); +}; + +/** + * 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; + } + } + + toJSON () { + return { + error: this.status, + message: this.message || "", + stacktrace: this.stack || "", + } + } + + 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; + } +} + +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); + msg = error.pprint`Element ${obscuredEl} is not clickable ` + + `at point (${coords.x},${coords.y}) ` + + error.pprint`because another element ${overlayingEl} ` + + `obscures it`; + } + + super(msg); + this.status = "element click intercepted"; + } +} + +class ElementNotInteractableError extends WebDriverError { + constructor (message) { + super(message); + this.status = "element not interactable"; + } +} + +class InsecureCertificateError extends WebDriverError { + constructor (message) { + super(message); + this.status = "insecure certificate"; + } +} + +class InvalidArgumentError extends WebDriverError { + constructor (message) { + super(message); + this.status = "invalid argument"; + } +} + +class InvalidElementStateError extends WebDriverError { + constructor (message) { + super(message); + this.status = "invalid element state"; + } +} + +class InvalidSelectorError extends WebDriverError { + constructor (message) { + super(message); + this.status = "invalid selector"; + } +} + +class InvalidSessionIDError extends WebDriverError { + constructor (message) { + super(message); + this.status = "invalid session id"; + } +} + +/** + * Creates a richly annotated error for an error situation that occurred + * whilst evaluating injected scripts. + */ +class JavaScriptError extends WebDriverError { + /** + * @param {(string|Error)} x + * An Error object instance or a string describing the error + * situation. + * @param {string=} fnName + * Name of the function to use in the stack trace message. + * @param {string=} file + * Filename of the test file on the client. + * @param {number=} line + * Line number of |file|. + * @param {string=} script + * Script being executed, in text form. + */ + constructor ( + x, + fnName = undefined, + file = undefined, + line = undefined, + script = undefined) { + let msg = String(x); + let trace = ""; + + if (fnName) { + trace += fnName; + if (file) { + trace += ` @${file}`; + if (line) { + trace += `, line ${line}`; + } + } + } + + if (error.isError(x)) { + let jsStack = x.stack.split("\n"); + let match = jsStack[0].match(/:(\d+):\d+$/); + let jsLine = match ? parseInt(match[1]) : 0; + if (script) { + let src = script.split("\n")[jsLine]; + trace += "\n" + + `inline javascript, line ${jsLine}\n` + + `src: "${src}"`; + } + trace += "\nStack:\n" + x.stack; + } + + super(msg); + this.status = "javascript error"; + this.stack = trace; + } +} + +class MoveTargetOutOfBoundsError extends WebDriverError { + constructor (message) { + super(message); + this.status = "move target out of bounds"; + } +} + +class NoAlertOpenError extends WebDriverError { + constructor (message) { + super(message); + this.status = "no such alert"; + } +} + +class NoSuchElementError extends WebDriverError { + constructor (message) { + super(message); + this.status = "no such element"; + } +} + +class NoSuchFrameError extends WebDriverError { + constructor (message) { + super(message); + this.status = "no such frame"; + } +} + +class NoSuchWindowError extends WebDriverError { + constructor (message) { + super(message); + this.status = "no such window"; + } +} + +class ScriptTimeoutError extends WebDriverError { + constructor (message) { + super(message); + this.status = "script timeout"; + } +} + +class SessionNotCreatedError extends WebDriverError { + constructor (message) { + super(message); + this.status = "session not created"; + } +} + +class StaleElementReferenceError extends WebDriverError { + constructor (message) { + super(message); + this.status = "stale element reference"; + } +} + +class TimeoutError extends WebDriverError { + constructor (message) { + super(message); + this.status = "timeout"; + } +} + +class UnableToSetCookieError extends WebDriverError { + constructor (message) { + super(message); + this.status = "unable to set cookie"; + } +} + +class UnknownCommandError extends WebDriverError { + constructor (message) { + super(message); + this.status = "unknown command"; + } +} + +class UnknownError extends WebDriverError { + constructor (message) { + super(message); + this.status = "unknown error"; + } +} + +class UnsupportedOperationError extends WebDriverError { + constructor (message) { + super(message); + this.status = "unsupported operation"; + } +} + +const STATUSES = new Map([ + ["element not accessible", ElementNotAccessibleError], + ["element not interactable", ElementNotInteractableError], + ["element click intercepted", ElementClickInterceptedError], + ["insecure certificate", InsecureCertificateError], + ["invalid argument", InvalidArgumentError], + ["invalid element state", InvalidElementStateError], + ["invalid selector", InvalidSelectorError], + ["invalid session id", InvalidSessionIDError], + ["javascript error", JavaScriptError], + ["move target out of bounds", MoveTargetOutOfBoundsError], + ["no alert open", NoAlertOpenError], + ["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], + ["unknown command", UnknownCommandError], + ["unknown error", UnknownError], + ["unsupported operation", UnsupportedOperationError], + ["webdriver error", WebDriverError], +]); |