Assert.sys.mjs (21984B)
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 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 // Originally from narwhal.js (http://narwhaljs.org) 6 // Copyright (c) 2009 Thomas Robinson <280north.com> 7 // MIT license: http://opensource.org/licenses/MIT 8 9 import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs"; 10 11 /** 12 * This module is based on the 13 * `CommonJS spec <https://wiki.commonjs.org/wiki/Unit_Testing/1.0>`_ 14 * 15 * When you see a jsdoc comment that contains a number, it's a reference to a 16 * specific section of the CommonJS spec. 17 * 18 * 1. The assert module provides functions that throw AssertionError's when 19 * particular conditions are not met. 20 * 21 * To use the module you may instantiate it first. 22 * 23 * @param {reporterFunc} reporterFunc 24 * Allows consumers to override reporting for this instance. 25 * @param {boolean} isDefault 26 * Used by test suites to set ``reporterFunc`` as the default 27 * used by the global instance, which is called for example 28 * by other test-only modules. This is false when the 29 * reporter is set by content scripts, because they may still 30 * run in the parent process. 31 * 32 * @class 33 */ 34 export function Assert(reporterFunc, isDefault) { 35 if (reporterFunc) { 36 this.setReporter(reporterFunc); 37 } 38 if (isDefault) { 39 Assert.setReporter(reporterFunc); 40 } 41 } 42 43 // This allows using the Assert object as an additional global instance. 44 Object.setPrototypeOf(Assert, Assert.prototype); 45 46 function instanceOf(object, type) { 47 return Object.prototype.toString.call(object) == "[object " + type + "]"; 48 } 49 50 function replacer(key, value) { 51 if (value === undefined) { 52 return "" + value; 53 } 54 if (typeof value === "number" && (isNaN(value) || !isFinite(value))) { 55 return value.toString(); 56 } 57 if (typeof value === "function" || instanceOf(value, "RegExp")) { 58 return value.toString(); 59 } 60 if ( 61 typeof value === "object" && 62 value !== null && 63 "QueryInterface" in value 64 ) { 65 return value.toString(); 66 } 67 return value; 68 } 69 70 const kTruncateLength = 128; 71 72 function truncate(text, newLength = kTruncateLength) { 73 if (typeof text == "string") { 74 return text.length < newLength ? text : text.slice(0, newLength); 75 } 76 return text; 77 } 78 79 function getMessage(error, prefix = "") { 80 let actual, expected; 81 // Wrap calls to JSON.stringify in try...catch blocks, as they may throw. If 82 // so, fall back to toString(). 83 try { 84 actual = JSON.stringify(error.actual, replacer); 85 } catch (ex) { 86 actual = Object.prototype.toString.call(error.actual); 87 } 88 try { 89 expected = JSON.stringify(error.expected, replacer); 90 } catch (ex) { 91 expected = Object.prototype.toString.call(error.expected); 92 } 93 let message = prefix; 94 if (error.operator) { 95 let truncateLength = error.truncate ? kTruncateLength : Infinity; 96 message += 97 (prefix ? " - " : "") + 98 truncate(actual, truncateLength) + 99 " " + 100 error.operator + 101 " " + 102 truncate(expected, truncateLength); 103 } 104 return message; 105 } 106 107 /** 108 * 2. The AssertionError is defined in assert. 109 * 110 * At present only the four keys mentioned below are used and 111 * understood by the spec. Implementations or sub modules can pass 112 * other keys to the AssertionError's constructor - they will be 113 * ignored. 114 * 115 * @example 116 * 117 * new assert.AssertionError({ 118 * message: message, 119 * actual: actual, 120 * expected: expected, 121 * operator: operator, 122 * truncate: truncate, 123 * stack: stack, // Optional, defaults to the current stack. 124 * }); 125 */ 126 Assert.AssertionError = function (options) { 127 this.name = "AssertionError"; 128 this.actual = options.actual; 129 this.expected = options.expected; 130 this.operator = options.operator; 131 this.message = getMessage(this, options.message, options.truncate); 132 // The part of the stack that comes from this module is not interesting. 133 let stack = options.stack || Components.stack; 134 do { 135 stack = stack.asyncCaller || stack.caller; 136 } while ( 137 stack && 138 stack.filename && 139 stack.filename.includes("Assert.sys.mjs") 140 ); 141 this.stack = stack; 142 }; 143 144 // assert.AssertionError instanceof Error 145 Assert.AssertionError.prototype = Object.create(Error.prototype, { 146 constructor: { 147 value: Assert.AssertionError, 148 enumerable: false, 149 writable: true, 150 configurable: true, 151 }, 152 }); 153 154 Assert.prototype._reporter = null; 155 156 /** 157 * This callback type is used for custom assertion report handling. 158 * 159 * @callback reporterFunc 160 * @param {AssertionError|null} err 161 * An error object when the assertion failed, or null when it passed. 162 * @param {string} message 163 * Message describing the assertion. 164 * @param {Stack} stack 165 * Stack trace of the assertion function. 166 */ 167 168 /** 169 * Set a custom assertion report handler function. 170 * 171 * @example 172 * 173 * Assert.setReporter(function customReporter(err, message, stack) { 174 * if (err) { 175 * do_report_result(false, err.message, err.stack); 176 * } else { 177 * do_report_result(true, message, stack); 178 * } 179 * }); 180 * 181 * @param {reporterFunc} reporterFunc 182 * Report handler function. 183 */ 184 Assert.prototype.setReporter = function (reporterFunc) { 185 this._reporter = reporterFunc; 186 }; 187 188 /** 189 * 3. All of the following functions must throw an AssertionError when a 190 * corresponding condition is not met, with a message that may be undefined if 191 * not provided. All assertion methods provide both the actual and expected 192 * values to the assertion error for display purposes. 193 * 194 * This report method only throws errors on assertion failures, as per spec, 195 * but consumers of this module (think: xpcshell-test, mochitest) may want to 196 * override this default implementation. 197 * 198 * @example 199 * 200 * // The following will report an assertion failure. 201 * this.report(1 != 2, 1, 2, "testing JS number math!", "=="); 202 * 203 * @param {boolean} failed 204 * Indicates if the assertion failed or not. 205 * @param {*} actual 206 * The result of evaluating the assertion. 207 * @param {*} [expected] 208 * Expected result from the test author. 209 * @param {string} [message] 210 * Short explanation of the expected result. 211 * @param {string} [operator] 212 * Operation qualifier used by the assertion method (ex: '=='). 213 * @param {boolean} [truncate=true] 214 * Whether or not ``actual`` and ``expected`` should be truncated when printing. 215 * @param {nsIStackFrame} [stack] 216 * The stack trace including the caller of the assertion method, 217 * if this cannot be inferred automatically (e.g. due to async callbacks). 218 */ 219 Assert.prototype.report = function ( 220 failed, 221 actual, 222 expected, 223 message, 224 operator, 225 truncate = true, 226 stack = null // Defaults to Components.stack in AssertionError. 227 ) { 228 // Although not ideal, we allow a "null" message due to the way some of the extension tests 229 // work. 230 if (message !== undefined && message !== null && typeof message != "string") { 231 this.ok( 232 false, 233 `Expected a string or undefined for the error message to Assert.*, got ${typeof message}` 234 ); 235 } 236 let err = new Assert.AssertionError({ 237 message, 238 actual, 239 expected, 240 operator, 241 truncate, 242 stack, 243 }); 244 if (!this._reporter) { 245 // If no custom reporter is set, throw the error. 246 if (failed) { 247 throw err; 248 } 249 } else { 250 this._reporter(failed ? err : null, err.message, err.stack); 251 } 252 }; 253 254 /** 255 * 4. Pure assertion tests whether a value is truthy, as determined by !!guard. 256 * ``assert.ok(guard, message_opt);`` 257 * This statement is equivalent to ``assert.equal(true, !!guard, message_opt);``. 258 * To test strictly for the value true, use ``assert.strictEqual(true, guard, 259 * message_opt);``. 260 * 261 * @param {*} value 262 * Test subject to be evaluated as truthy. 263 * @param {string} [message] 264 * Short explanation of the expected result. 265 */ 266 Assert.prototype.ok = function (value, message) { 267 if (arguments.length > 2) { 268 this.report( 269 true, 270 false, 271 true, 272 "Too many arguments passed to `Assert.ok()`", 273 "==" 274 ); 275 } else { 276 this.report(!value, value, true, message, "=="); 277 } 278 }; 279 280 /** 281 * 5. The equality assertion tests shallow, coercive equality with ==. 282 * ``assert.equal(actual, expected, message_opt);`` 283 * 284 * @param {*} actual 285 * Test subject to be evaluated as equivalent to ``expected``. 286 * @param {*} expected 287 * Test reference to evaluate against ``actual``. 288 * @param {string} [message] 289 * Short explanation of the expected result. 290 */ 291 Assert.prototype.equal = function equal(actual, expected, message) { 292 this.report(actual != expected, actual, expected, message, "=="); 293 }; 294 295 /** 296 * 6. The non-equality assertion tests for whether two objects are not equal 297 * with ``!=`` 298 * 299 * @example 300 * assert.notEqual(actual, expected, message_opt); 301 * 302 * @param {*} actual 303 * Test subject to be evaluated as NOT equivalent to ``expected``. 304 * @param {*} expected 305 * Test reference to evaluate against ``actual``. 306 * @param {string} [message] 307 * Short explanation of the expected result. 308 */ 309 Assert.prototype.notEqual = function notEqual(actual, expected, message) { 310 this.report(actual == expected, actual, expected, message, "!="); 311 }; 312 313 /** 314 * 7. The equivalence assertion tests a deep equality relation. 315 * assert.deepEqual(actual, expected, message_opt); 316 * 317 * We check using the most exact approximation of equality between two objects 318 * to keep the chance of false positives to a minimum. 319 * `JSON.stringify` is not designed to be used for this purpose; objects may 320 * have ambiguous `toJSON()` implementations that would influence the test. 321 * 322 * @param {*} actual 323 * Test subject to be evaluated as equivalent to ``expected``, including nested properties. 324 * @param {*} expected 325 * Test reference to evaluate against ``actual``. 326 * @param {string} [message] 327 * Short explanation of the expected result. 328 */ 329 Assert.prototype.deepEqual = function deepEqual(actual, expected, message) { 330 this.report( 331 !ObjectUtils.deepEqual(actual, expected), 332 actual, 333 expected, 334 message, 335 "deepEqual", 336 false 337 ); 338 }; 339 340 /** 341 * 8. The non-equivalence assertion tests for any deep inequality. 342 * assert.notDeepEqual(actual, expected, message_opt); 343 * 344 * @param {*} actual 345 * Test subject to be evaluated as NOT equivalent to ``expected``, including nested 346 * properties. 347 * @param {*} expected 348 * Test reference to evaluate against ``actual``. 349 * @param {string} [message] 350 * Short explanation of the expected result. 351 */ 352 Assert.prototype.notDeepEqual = function notDeepEqual( 353 actual, 354 expected, 355 message 356 ) { 357 this.report( 358 ObjectUtils.deepEqual(actual, expected), 359 actual, 360 expected, 361 message, 362 "notDeepEqual", 363 false 364 ); 365 }; 366 367 /** 368 * 9. The strict equality assertion tests strict equality, as determined by ===. 369 * ``assert.strictEqual(actual, expected, message_opt);`` 370 * 371 * @param {*} actual 372 * Test subject to be evaluated as strictly equivalent to ``expected``. 373 * @param {*} expected 374 * Test reference to evaluate against ``actual``. 375 * @param {string} [message] 376 * Short explanation of the expected result. 377 */ 378 Assert.prototype.strictEqual = function strictEqual(actual, expected, message) { 379 this.report(actual !== expected, actual, expected, message, "==="); 380 }; 381 382 /** 383 * 10. The strict non-equality assertion tests for strict inequality, as 384 * determined by !==. ``assert.notStrictEqual(actual, expected, message_opt);`` 385 * 386 * @param {*} actual 387 * Test subject to be evaluated as NOT strictly equivalent to ``expected``. 388 * @param {*} expected 389 * Test reference to evaluate against ``actual``. 390 * @param {string} [message] 391 * Short explanation of the expected result. 392 */ 393 Assert.prototype.notStrictEqual = function notStrictEqual( 394 actual, 395 expected, 396 message 397 ) { 398 this.report(actual === expected, actual, expected, message, "!=="); 399 }; 400 401 function checkExpectedArgument(instance, funcName, expected) { 402 if (!expected) { 403 instance.ok( 404 false, 405 `Error: The 'expected' argument was not supplied to Assert.${funcName}()` 406 ); 407 } 408 409 if ( 410 !instanceOf(expected, "RegExp") && 411 typeof expected !== "function" && 412 typeof expected !== "object" 413 ) { 414 instance.ok( 415 false, 416 `Error: The 'expected' argument to Assert.${funcName}() must be a RegExp, function or an object` 417 ); 418 } 419 } 420 421 function expectedException(actual, expected) { 422 if (!actual || !expected) { 423 return false; 424 } 425 426 if (instanceOf(expected, "RegExp")) { 427 return expected.test(actual); 428 // We need to guard against the right hand parameter of "instanceof" lacking 429 // the "prototype" property, which is true of arrow functions in particular. 430 } else if ( 431 !(typeof expected === "function" && !expected.prototype) && 432 actual instanceof expected 433 ) { 434 return true; 435 } else if (expected.call({}, actual) === true) { 436 return true; 437 } 438 439 return false; 440 } 441 442 /** 443 * 11. Expected to throw an error: 444 * assert.throws(block, Error_opt, message_opt); 445 * 446 * Example: 447 * ```js 448 * // The following will verify that an error of type TypeError was thrown: 449 * Assert.throws(() => testBody(), TypeError); 450 * // The following will verify that an error was thrown with an error message matching "hello": 451 * Assert.throws(() => testBody(), /hello/); 452 * ``` 453 * 454 * @param {Function} block 455 * Function to evaluate and catch eventual thrown errors. 456 * @param {RegExp|Function} expected 457 * This parameter can be either a RegExp or a function. The function is 458 * either the error type's constructor, or it's a method that returns 459 * a boolean that describes the test outcome. 460 * @param {string} [message] 461 * Short explanation of the expected result. 462 */ 463 Assert.prototype.throws = function (block, expected, message) { 464 checkExpectedArgument(this, "throws", expected); 465 466 // `true` if we realize that we have added an 467 // error to `ChromeUtils.recentJSDevError` and 468 // that we probably need to clean it up. 469 let cleanupRecentJSDevError = false; 470 if ("recentJSDevError" in ChromeUtils) { 471 // Check that we're in a build of Firefox that supports 472 // the `recentJSDevError` mechanism (i.e. Nightly build). 473 if (ChromeUtils.recentJSDevError === undefined) { 474 // There was no previous error, so if we throw 475 // an error here, we may need to clean it up. 476 cleanupRecentJSDevError = true; 477 } 478 } 479 480 let actual; 481 482 try { 483 block(); 484 } catch (e) { 485 actual = e; 486 } 487 488 message = 489 (expected.name ? " (" + expected.name + ")." : ".") + 490 (message ? " " + message : "."); 491 492 if (!actual) { 493 this.report(true, actual, expected, "Missing expected exception" + message); 494 } 495 496 if (actual && !expectedException(actual, expected)) { 497 throw actual; 498 } 499 500 this.report(false, expected, expected, message); 501 502 // Make sure that we don't cause failures for JS Dev Errors that 503 // were expected, typically for tests that attempt to check 504 // that we react properly to TypeError, ReferenceError, SyntaxError. 505 if (cleanupRecentJSDevError) { 506 let recentJSDevError = ChromeUtils.recentJSDevError; 507 if (recentJSDevError) { 508 if (expectedException(recentJSDevError)) { 509 ChromeUtils.clearRecentJSDevError(); 510 } 511 } 512 } 513 }; 514 515 /** 516 * A promise that is expected to reject: 517 * assert.rejects(promise, expected, message); 518 * 519 * @param {Promise} promise 520 * A promise that is expected to reject. 521 * @param {?} [expected] 522 * Test reference to evaluate against the rejection result. 523 * @param {string} [message] 524 * Short explanation of the expected result. 525 */ 526 Assert.prototype.rejects = function (promise, expected, message) { 527 checkExpectedArgument(this, "rejects", expected); 528 const operator = undefined; // Should we use "rejects" here? 529 const stack = Components.stack; 530 return new Promise((resolve, reject) => { 531 return promise 532 .then( 533 () => { 534 this.report( 535 true, 536 null, 537 expected, 538 "Missing expected exception " + message, 539 operator, 540 true, 541 stack 542 ); 543 // this.report() above should raise an AssertionError. If _reporter 544 // has been overridden and doesn't throw an error, just resolve. 545 // Otherwise we'll have a never-resolving promise that got stuck. 546 resolve(); 547 }, 548 err => { 549 if (!expectedException(err, expected)) { 550 // TODO bug 1480075: Should report error instead of rejecting. 551 reject(err); 552 return; 553 } 554 this.report(false, err, expected, message, operator, truncate, stack); 555 resolve(); 556 } 557 ) 558 .catch(reject); 559 }); 560 }; 561 562 function compareNumbers(expression, lhs, rhs, message, operator) { 563 let lhsIsNumber = typeof lhs == "number" && !Number.isNaN(lhs); 564 let rhsIsNumber = typeof rhs == "number" && !Number.isNaN(rhs); 565 566 if (lhsIsNumber && rhsIsNumber) { 567 this.report(expression, lhs, rhs, message, operator); 568 return; 569 } 570 let lhsIsDate = 571 typeof lhs == "object" && lhs.constructor.name == "Date" && !isNaN(lhs); 572 let rhsIsDate = 573 typeof rhs == "object" && rhs.constructor.name == "Date" && !isNaN(rhs); 574 if (lhsIsDate && rhsIsDate) { 575 this.report(expression, lhs, rhs, message, operator); 576 return; 577 } 578 579 let errorMessage; 580 if (!lhsIsNumber && !rhsIsNumber && !lhsIsDate && !rhsIsDate) { 581 errorMessage = `Neither '${lhs}' nor '${rhs}' are numbers or dates.`; 582 } else if ((lhsIsNumber && rhsIsDate) || (lhsIsDate && rhsIsNumber)) { 583 errorMessage = `'${lhsIsNumber ? lhs : rhs}' is a number and '${ 584 rhsIsDate ? rhs : lhs 585 }' is a date.`; 586 } else { 587 errorMessage = `'${ 588 lhsIsNumber || lhsIsDate ? rhs : lhs 589 }' is not a number or date.`; 590 } 591 this.report(true, lhs, rhs, errorMessage); 592 } 593 594 /** 595 * The lhs must be greater than the rhs. 596 * assert.greater(lhs, rhs, message_opt); 597 * 598 * @param {number} lhs 599 * The left-hand side value. 600 * @param {number} rhs 601 * The right-hand side value. 602 * @param {string} [message] 603 * Short explanation of the comparison result. 604 */ 605 Assert.prototype.greater = function greater(lhs, rhs, message) { 606 compareNumbers.call(this, lhs <= rhs, lhs, rhs, message, ">"); 607 }; 608 609 /** 610 * The lhs must be greater than or equal to the rhs. 611 * assert.greaterOrEqual(lhs, rhs, message_opt); 612 * 613 * @param {number} [lhs] 614 * The left-hand side value. 615 * @param {number} [rhs] 616 * The right-hand side value. 617 * @param {string} [message] 618 * Short explanation of the comparison result. 619 */ 620 Assert.prototype.greaterOrEqual = function greaterOrEqual(lhs, rhs, message) { 621 compareNumbers.call(this, lhs < rhs, lhs, rhs, message, ">="); 622 }; 623 624 /** 625 * The lhs must be less than the rhs. 626 * assert.less(lhs, rhs, message_opt); 627 * 628 * @param {number} [lhs] 629 * The left-hand side value. 630 * @param {number} [rhs] 631 * The right-hand side value. 632 * @param {string} [message] 633 * Short explanation of the comparison result. 634 */ 635 Assert.prototype.less = function less(lhs, rhs, message) { 636 compareNumbers.call(this, lhs >= rhs, lhs, rhs, message, "<"); 637 }; 638 639 /** 640 * The lhs must be less than or equal to the rhs. 641 * assert.lessOrEqual(lhs, rhs, message_opt); 642 * 643 * @param {number} [lhs] 644 * The left-hand side value. 645 * @param {number} [rhs] 646 * The right-hand side value. 647 * @param {string} [message] 648 * Short explanation of the comparison result. 649 */ 650 Assert.prototype.lessOrEqual = function lessOrEqual(lhs, rhs, message) { 651 compareNumbers.call(this, lhs > rhs, lhs, rhs, message, "<="); 652 }; 653 654 /** 655 * The lhs must be a string that matches the rhs regular expression. 656 * rhs can be specified either as a string or a RegExp object. If specified as a 657 * string it will be interpreted as a regular expression so take care to escape 658 * special characters such as "?" or "(" if you need the actual characters. 659 * 660 * @param {string} lhs 661 * The string to be tested. 662 * @param {string | RegExp} rhs 663 * The regular expression that the string will be tested with. 664 * Note that if passed as a string, this will be interpreted. 665 * as a regular expression. 666 * @param {string} [message] 667 * Short explanation of the comparison result. 668 */ 669 Assert.prototype.stringMatches = function stringMatches(lhs, rhs, message) { 670 if (typeof rhs != "string" && !instanceOf(rhs, "RegExp")) { 671 this.report( 672 true, 673 lhs, 674 String(rhs), 675 `Expected a string or a RegExp for rhs, but "${rhs}" isn't a string or a RegExp object.` 676 ); 677 return; 678 } 679 680 if (typeof lhs != "string") { 681 this.report( 682 true, 683 lhs, 684 String(rhs), 685 `Expected a string for lhs, but "${lhs}" isn't a string.` 686 ); 687 return; 688 } 689 690 if (typeof rhs == "string") { 691 try { 692 rhs = new RegExp(rhs); 693 } catch { 694 this.report( 695 true, 696 lhs, 697 rhs, 698 `Expected a valid regular expression for rhs, but "${rhs}" isn't one.` 699 ); 700 return; 701 } 702 } 703 704 const isCorrect = rhs.test(lhs); 705 this.report(!isCorrect, lhs, rhs.toString(), message, "matches"); 706 }; 707 708 /** 709 * The lhs must be a string that contains the rhs string. 710 * 711 * @param {string} lhs 712 * The string to be tested (haystack). 713 * @param {string} rhs 714 * The string to be found (needle). 715 * @param {string} [message] 716 * Short explanation of the expected result. 717 */ 718 Assert.prototype.stringContains = function stringContains(lhs, rhs, message) { 719 if (typeof lhs != "string" || typeof rhs != "string") { 720 this.report( 721 true, 722 lhs, 723 rhs, 724 `Expected a string for both lhs and rhs, but either "${lhs}" or "${rhs}" is not a string.` 725 ); 726 } 727 728 const isCorrect = lhs.includes(rhs); 729 this.report(!isCorrect, lhs, rhs, message, "includes"); 730 };