Mypal/testing/marionette/error.js

501 lines
12 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";
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);
switch (obscuredEl.style.pointerEvents) {
case "none":
msg = error.pprint`Element ${obscuredEl} is not clickable ` +
`at point (${coords.x},${coords.y}) ` +
`because it does not have pointer events enabled, ` +
error.pprint`and element ${overlayingEl} ` +
`would receive the click instead`;
break;
default:
msg = error.pprint`Element ${obscuredEl} is not clickable ` +
`at point (${coords.x},${coords.y}) ` +
error.pprint`because another element ${overlayingEl} ` +
`obscures it`;
break;
}
}
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],
]);