error.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const {interfaces: Ci, utils: Cu} = Components;
  6. const ERRORS = new Set([
  7. "ElementClickInterceptedError",
  8. "ElementNotAccessibleError",
  9. "ElementNotInteractableError",
  10. "InsecureCertificateError",
  11. "InvalidArgumentError",
  12. "InvalidElementStateError",
  13. "InvalidSelectorError",
  14. "InvalidSessionIDError",
  15. "JavaScriptError",
  16. "MoveTargetOutOfBoundsError",
  17. "NoAlertOpenError",
  18. "NoSuchElementError",
  19. "NoSuchFrameError",
  20. "NoSuchWindowError",
  21. "ScriptTimeoutError",
  22. "SessionNotCreatedError",
  23. "StaleElementReferenceError",
  24. "TimeoutError",
  25. "UnableToSetCookieError",
  26. "UnknownCommandError",
  27. "UnknownError",
  28. "UnsupportedOperationError",
  29. "WebDriverError",
  30. ]);
  31. const BUILTIN_ERRORS = new Set([
  32. "Error",
  33. "EvalError",
  34. "InternalError",
  35. "RangeError",
  36. "ReferenceError",
  37. "SyntaxError",
  38. "TypeError",
  39. "URIError",
  40. ]);
  41. this.EXPORTED_SYMBOLS = ["error"].concat(Array.from(ERRORS));
  42. this.error = {};
  43. /**
  44. * Checks if obj is an instance of the Error prototype in a safe manner.
  45. * Prefer using this over using instanceof since the Error prototype
  46. * isn't unique across browsers, and XPCOM nsIException's are special
  47. * snowflakes.
  48. *
  49. * @param {*} val
  50. * Any value that should be undergo the test for errorness.
  51. * @return {boolean}
  52. * True if error, false otherwise.
  53. */
  54. error.isError = function (val) {
  55. if (val === null || typeof val != "object") {
  56. return false;
  57. } else if (val instanceof Ci.nsIException) {
  58. return true;
  59. } else {
  60. // DOMRectList errors on string comparison
  61. try {
  62. let proto = Object.getPrototypeOf(val);
  63. return BUILTIN_ERRORS.has(proto.toString());
  64. } catch (e) {
  65. return false;
  66. }
  67. }
  68. };
  69. /**
  70. * Checks if obj is an object in the WebDriverError prototypal chain.
  71. */
  72. error.isWebDriverError = function (obj) {
  73. return error.isError(obj) &&
  74. ("name" in obj && ERRORS.has(obj.name));
  75. };
  76. /**
  77. * Wraps any error as a WebDriverError. If the given error is already in
  78. * the WebDriverError prototype chain, this function returns it
  79. * unmodified.
  80. */
  81. error.wrap = function (err) {
  82. if (error.isWebDriverError(err)) {
  83. return err;
  84. }
  85. return new WebDriverError(err);
  86. };
  87. /**
  88. * Unhandled error reporter. Dumps the error and its stacktrace to console,
  89. * and reports error to the Browser Console.
  90. */
  91. error.report = function (err) {
  92. let msg = "Marionette threw an error: " + error.stringify(err);
  93. dump(msg + "\n");
  94. if (Cu.reportError) {
  95. Cu.reportError(msg);
  96. }
  97. };
  98. /**
  99. * Prettifies an instance of Error and its stacktrace to a string.
  100. */
  101. error.stringify = function (err) {
  102. try {
  103. let s = err.toString();
  104. if ("stack" in err) {
  105. s += "\n" + err.stack;
  106. }
  107. return s;
  108. } catch (e) {
  109. return "<unprintable error>";
  110. }
  111. };
  112. /**
  113. * Pretty-print values passed to template strings.
  114. *
  115. * Usage:
  116. *
  117. * let bool = {value: true};
  118. * error.pprint`Expected boolean, got ${bool}`;
  119. * => 'Expected boolean, got [object Object] {"value": true}'
  120. *
  121. * let htmlElement = document.querySelector("input#foo");
  122. * error.pprint`Expected element ${htmlElement}`;
  123. * => 'Expected element <input id="foo" class="bar baz">'
  124. */
  125. error.pprint = function (ss, ...values) {
  126. function prettyObject (obj) {
  127. let proto = Object.prototype.toString.call(obj);
  128. let s = "";
  129. try {
  130. s = JSON.stringify(obj);
  131. } catch (e if e instanceof TypeError) {
  132. s = `<${e.message}>`;
  133. }
  134. return proto + " " + s;
  135. }
  136. function prettyElement (el) {
  137. let ident = [];
  138. if (el.id) {
  139. ident.push(`id="${el.id}"`);
  140. }
  141. if (el.classList.length > 0) {
  142. ident.push(`class="${el.className}"`);
  143. }
  144. let idents = "";
  145. if (ident.length > 0) {
  146. idents = " " + ident.join(" ");
  147. }
  148. return `<${el.localName}${idents}>`;
  149. }
  150. let res = [];
  151. for (let i = 0; i < ss.length; i++) {
  152. res.push(ss[i]);
  153. if (i < values.length) {
  154. let val = values[i];
  155. let typ = Object.prototype.toString.call(val);
  156. let s;
  157. try {
  158. if (val && val.nodeType === 1) {
  159. s = prettyElement(val);
  160. } else {
  161. s = prettyObject(val);
  162. }
  163. } catch (e) {
  164. s = typeof val;
  165. }
  166. res.push(s);
  167. }
  168. }
  169. return res.join("");
  170. };
  171. /**
  172. * WebDriverError is the prototypal parent of all WebDriver errors.
  173. * It should not be used directly, as it does not correspond to a real
  174. * error in the specification.
  175. */
  176. class WebDriverError extends Error {
  177. /**
  178. * @param {(string|Error)=} x
  179. * Optional string describing error situation or Error instance
  180. * to propagate.
  181. */
  182. constructor (x) {
  183. super(x);
  184. this.name = this.constructor.name;
  185. this.status = "webdriver error";
  186. // Error's ctor does not preserve x' stack
  187. if (error.isError(x)) {
  188. this.stack = x.stack;
  189. }
  190. }
  191. toJSON () {
  192. return {
  193. error: this.status,
  194. message: this.message || "",
  195. stacktrace: this.stack || "",
  196. }
  197. }
  198. static fromJSON (json) {
  199. if (typeof json.error == "undefined") {
  200. let s = JSON.stringify(json);
  201. throw new TypeError("Undeserialisable error type: " + s);
  202. }
  203. if (!STATUSES.has(json.error)) {
  204. throw new TypeError("Not of WebDriverError descent: " + json.error);
  205. }
  206. let cls = STATUSES.get(json.error);
  207. let err = new cls();
  208. if ("message" in json) {
  209. err.message = json.message;
  210. }
  211. if ("stacktrace" in json) {
  212. err.stack = json.stacktrace;
  213. }
  214. return err;
  215. }
  216. }
  217. class ElementNotAccessibleError extends WebDriverError {
  218. constructor (message) {
  219. super(message);
  220. this.status = "element not accessible";
  221. }
  222. }
  223. /**
  224. * An element click could not be completed because the element receiving
  225. * the events is obscuring the element that was requested clicked.
  226. *
  227. * @param {Element=} obscuredEl
  228. * Element obscuring the element receiving the click. Providing this
  229. * is not required, but will produce a nicer error message.
  230. * @param {Map.<string, number>} coords
  231. * Original click location. Providing this is not required, but
  232. * will produce a nicer error message.
  233. */
  234. class ElementClickInterceptedError extends WebDriverError {
  235. constructor (obscuredEl = undefined, coords = undefined) {
  236. let msg = "";
  237. if (obscuredEl && coords) {
  238. const doc = obscuredEl.ownerDocument;
  239. const overlayingEl = doc.elementFromPoint(coords.x, coords.y);
  240. switch (obscuredEl.style.pointerEvents) {
  241. case "none":
  242. msg = error.pprint`Element ${obscuredEl} is not clickable ` +
  243. `at point (${coords.x},${coords.y}) ` +
  244. `because it does not have pointer events enabled, ` +
  245. error.pprint`and element ${overlayingEl} ` +
  246. `would receive the click instead`;
  247. break;
  248. default:
  249. msg = error.pprint`Element ${obscuredEl} is not clickable ` +
  250. `at point (${coords.x},${coords.y}) ` +
  251. error.pprint`because another element ${overlayingEl} ` +
  252. `obscures it`;
  253. break;
  254. }
  255. }
  256. super(msg);
  257. this.status = "element click intercepted";
  258. }
  259. }
  260. class ElementNotInteractableError extends WebDriverError {
  261. constructor (message) {
  262. super(message);
  263. this.status = "element not interactable";
  264. }
  265. }
  266. class InsecureCertificateError extends WebDriverError {
  267. constructor (message) {
  268. super(message);
  269. this.status = "insecure certificate";
  270. }
  271. }
  272. class InvalidArgumentError extends WebDriverError {
  273. constructor (message) {
  274. super(message);
  275. this.status = "invalid argument";
  276. }
  277. }
  278. class InvalidElementStateError extends WebDriverError {
  279. constructor (message) {
  280. super(message);
  281. this.status = "invalid element state";
  282. }
  283. }
  284. class InvalidSelectorError extends WebDriverError {
  285. constructor (message) {
  286. super(message);
  287. this.status = "invalid selector";
  288. }
  289. }
  290. class InvalidSessionIDError extends WebDriverError {
  291. constructor (message) {
  292. super(message);
  293. this.status = "invalid session id";
  294. }
  295. }
  296. /**
  297. * Creates a richly annotated error for an error situation that occurred
  298. * whilst evaluating injected scripts.
  299. */
  300. class JavaScriptError extends WebDriverError {
  301. /**
  302. * @param {(string|Error)} x
  303. * An Error object instance or a string describing the error
  304. * situation.
  305. * @param {string=} fnName
  306. * Name of the function to use in the stack trace message.
  307. * @param {string=} file
  308. * Filename of the test file on the client.
  309. * @param {number=} line
  310. * Line number of |file|.
  311. * @param {string=} script
  312. * Script being executed, in text form.
  313. */
  314. constructor (
  315. x,
  316. fnName = undefined,
  317. file = undefined,
  318. line = undefined,
  319. script = undefined) {
  320. let msg = String(x);
  321. let trace = "";
  322. if (fnName) {
  323. trace += fnName;
  324. if (file) {
  325. trace += ` @${file}`;
  326. if (line) {
  327. trace += `, line ${line}`;
  328. }
  329. }
  330. }
  331. if (error.isError(x)) {
  332. let jsStack = x.stack.split("\n");
  333. let match = jsStack[0].match(/:(\d+):\d+$/);
  334. let jsLine = match ? parseInt(match[1]) : 0;
  335. if (script) {
  336. let src = script.split("\n")[jsLine];
  337. trace += "\n" +
  338. `inline javascript, line ${jsLine}\n` +
  339. `src: "${src}"`;
  340. }
  341. trace += "\nStack:\n" + x.stack;
  342. }
  343. super(msg);
  344. this.status = "javascript error";
  345. this.stack = trace;
  346. }
  347. }
  348. class MoveTargetOutOfBoundsError extends WebDriverError {
  349. constructor (message) {
  350. super(message);
  351. this.status = "move target out of bounds";
  352. }
  353. }
  354. class NoAlertOpenError extends WebDriverError {
  355. constructor (message) {
  356. super(message);
  357. this.status = "no such alert";
  358. }
  359. }
  360. class NoSuchElementError extends WebDriverError {
  361. constructor (message) {
  362. super(message);
  363. this.status = "no such element";
  364. }
  365. }
  366. class NoSuchFrameError extends WebDriverError {
  367. constructor (message) {
  368. super(message);
  369. this.status = "no such frame";
  370. }
  371. }
  372. class NoSuchWindowError extends WebDriverError {
  373. constructor (message) {
  374. super(message);
  375. this.status = "no such window";
  376. }
  377. }
  378. class ScriptTimeoutError extends WebDriverError {
  379. constructor (message) {
  380. super(message);
  381. this.status = "script timeout";
  382. }
  383. }
  384. class SessionNotCreatedError extends WebDriverError {
  385. constructor (message) {
  386. super(message);
  387. this.status = "session not created";
  388. }
  389. }
  390. class StaleElementReferenceError extends WebDriverError {
  391. constructor (message) {
  392. super(message);
  393. this.status = "stale element reference";
  394. }
  395. }
  396. class TimeoutError extends WebDriverError {
  397. constructor (message) {
  398. super(message);
  399. this.status = "timeout";
  400. }
  401. }
  402. class UnableToSetCookieError extends WebDriverError {
  403. constructor (message) {
  404. super(message);
  405. this.status = "unable to set cookie";
  406. }
  407. }
  408. class UnknownCommandError extends WebDriverError {
  409. constructor (message) {
  410. super(message);
  411. this.status = "unknown command";
  412. }
  413. }
  414. class UnknownError extends WebDriverError {
  415. constructor (message) {
  416. super(message);
  417. this.status = "unknown error";
  418. }
  419. }
  420. class UnsupportedOperationError extends WebDriverError {
  421. constructor (message) {
  422. super(message);
  423. this.status = "unsupported operation";
  424. }
  425. }
  426. const STATUSES = new Map([
  427. ["element not accessible", ElementNotAccessibleError],
  428. ["element not interactable", ElementNotInteractableError],
  429. ["element click intercepted", ElementClickInterceptedError],
  430. ["insecure certificate", InsecureCertificateError],
  431. ["invalid argument", InvalidArgumentError],
  432. ["invalid element state", InvalidElementStateError],
  433. ["invalid selector", InvalidSelectorError],
  434. ["invalid session id", InvalidSessionIDError],
  435. ["javascript error", JavaScriptError],
  436. ["move target out of bounds", MoveTargetOutOfBoundsError],
  437. ["no alert open", NoAlertOpenError],
  438. ["no such element", NoSuchElementError],
  439. ["no such frame", NoSuchFrameError],
  440. ["no such window", NoSuchWindowError],
  441. ["script timeout", ScriptTimeoutError],
  442. ["session not created", SessionNotCreatedError],
  443. ["stale element reference", StaleElementReferenceError],
  444. ["timeout", TimeoutError],
  445. ["unable to set cookie", UnableToSetCookieError],
  446. ["unknown command", UnknownCommandError],
  447. ["unknown error", UnknownError],
  448. ["unsupported operation", UnsupportedOperationError],
  449. ["webdriver error", WebDriverError],
  450. ]);