|
@@ -1,116 +1,34 @@
|
|
-/**
|
|
|
|
- * Custom error for failed assertions.
|
|
|
|
- * @constructor
|
|
|
|
- * @param {string} message Error message
|
|
|
|
- * @returns Error
|
|
|
|
- */
|
|
|
|
-function AssertionError(message) {
|
|
|
|
- var instance = new Error(message);
|
|
|
|
- instance.name = 'AssertionError';
|
|
|
|
- Object.setPrototypeOf(instance, Object.getPrototypeOf(this));
|
|
|
|
- return instance;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-/**
|
|
|
|
- * Throws an `AssertionError` if `value` is not truthy.
|
|
|
|
- * @param {*} value Value to be tested
|
|
|
|
- */
|
|
|
|
-function assert(value) {
|
|
|
|
- if (!value)
|
|
|
|
- throw new AssertionError("The assertion failed!");
|
|
|
|
-}
|
|
|
|
|
|
+let describe;
|
|
|
|
+let test;
|
|
|
|
+let expect;
|
|
|
|
|
|
-/**
|
|
|
|
- * Throws an `AssertionError` when called.
|
|
|
|
- * @throws {AssertionError}
|
|
|
|
- */
|
|
|
|
-function assertNotReached() {
|
|
|
|
- throw new AssertionError("assertNotReached() was reached!");
|
|
|
|
-}
|
|
|
|
|
|
+// Stores the results of each test and suite. Has a terrible
|
|
|
|
+// name to avoid name collision.
|
|
|
|
+let __TestResults__ = {};
|
|
|
|
|
|
-/**
|
|
|
|
- * Ensures the provided functions throws a specific error.
|
|
|
|
- * @param {Function} testFunction Function executing the throwing code
|
|
|
|
- * @param {object} [options]
|
|
|
|
- * @param {Error} [options.error] Expected error type
|
|
|
|
- * @param {string} [options.name] Expected error name
|
|
|
|
- * @param {string} [options.message] Expected error message
|
|
|
|
- */
|
|
|
|
-function assertThrowsError(testFunction, options) {
|
|
|
|
- try {
|
|
|
|
- testFunction();
|
|
|
|
- assertNotReached();
|
|
|
|
- } catch (e) {
|
|
|
|
- if (options.error !== undefined)
|
|
|
|
- assert(e instanceof options.error);
|
|
|
|
- if (options.name !== undefined)
|
|
|
|
- assert(e.name === options.name);
|
|
|
|
- if (options.message !== undefined)
|
|
|
|
- assert(e.message === options.message);
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-/**
|
|
|
|
- * Ensures the provided JavaScript source code results in a SyntaxError
|
|
|
|
- * @param {string} source The JavaScript source code to compile
|
|
|
|
- */
|
|
|
|
-function assertIsSyntaxError(source) {
|
|
|
|
- assertThrowsError(() => {
|
|
|
|
- new Function(source)();
|
|
|
|
- }, {
|
|
|
|
- error: SyntaxError,
|
|
|
|
- });
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-/**
|
|
|
|
- * Ensures the provided arrays contain exactly the same items.
|
|
|
|
- * @param {Array} a First array
|
|
|
|
- * @param {Array} b Second array
|
|
|
|
- */
|
|
|
|
-function assertArrayEquals(a, b) {
|
|
|
|
- if (a.length != b.length)
|
|
|
|
- throw new AssertionError("Array lengths do not match");
|
|
|
|
-
|
|
|
|
- for (var i = 0; i < a.length; i++) {
|
|
|
|
- if (a[i] !== b[i])
|
|
|
|
- throw new AssertionError("Elements do not match");
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
|
|
+// This array is used to communicate with the C++ program. It treats
|
|
|
|
+// each message in this array as a separate message. Has a terrible
|
|
|
|
+// name to avoid name collision.
|
|
|
|
+let __UserOutput__ = [];
|
|
|
|
|
|
-const assertVisitsAll = (testFunction, expectedOutput) => {
|
|
|
|
- const visited = [];
|
|
|
|
- testFunction(value => visited.push(value));
|
|
|
|
- assert(visited.length === expectedOutput.length);
|
|
|
|
- expectedOutput.forEach((value, i) => assert(visited[i] === value));
|
|
|
|
|
|
+// We also rebind console.log here to use the array above
|
|
|
|
+console.log = (...args) => {
|
|
|
|
+ __UserOutput__.push(args.join(" "));
|
|
};
|
|
};
|
|
|
|
|
|
-/**
|
|
|
|
- * Check whether the difference between two numbers is less than 0.000001.
|
|
|
|
- * @param {Number} a First number
|
|
|
|
- * @param {Number} b Second number
|
|
|
|
- */
|
|
|
|
-function isClose(a, b) {
|
|
|
|
- return Math.abs(a - b) < 0.000001;
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-/**
|
|
|
|
- * Quick and dirty deep equals method.
|
|
|
|
- * @param {*} a First value
|
|
|
|
- * @param {*} b Second value
|
|
|
|
- */
|
|
|
|
-function assertDeepEquals(a, b) {
|
|
|
|
- assert(deepEquals(a, b));
|
|
|
|
-}
|
|
|
|
|
|
+// Use an IIFE to avoid polluting the global namespace as much as possible
|
|
|
|
+(() => {
|
|
|
|
|
|
-function deepEquals(a, b) {
|
|
|
|
|
|
+// FIXME: This is a very naive deepEquals algorithm
|
|
|
|
+const deepEquals = (a, b) => {
|
|
if (Array.isArray(a))
|
|
if (Array.isArray(a))
|
|
return Array.isArray(b) && deepArrayEquals(a, b);
|
|
return Array.isArray(b) && deepArrayEquals(a, b);
|
|
if (typeof a === "object")
|
|
if (typeof a === "object")
|
|
return typeof b === "object" && deepObjectEquals(a, b);
|
|
return typeof b === "object" && deepObjectEquals(a, b);
|
|
- return a === b;
|
|
|
|
|
|
+ return Object.is(a, b);
|
|
}
|
|
}
|
|
|
|
|
|
-function deepArrayEquals(a, b) {
|
|
|
|
|
|
+const deepArrayEquals = (a, b) => {
|
|
if (a.length !== b.length)
|
|
if (a.length !== b.length)
|
|
return false;
|
|
return false;
|
|
for (let i = 0; i < a.length; ++i) {
|
|
for (let i = 0; i < a.length; ++i) {
|
|
@@ -120,7 +38,7 @@ function deepArrayEquals(a, b) {
|
|
return true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
-function deepObjectEquals(a, b) {
|
|
|
|
|
|
+const deepObjectEquals = (a, b) => {
|
|
if (a === null)
|
|
if (a === null)
|
|
return b === null;
|
|
return b === null;
|
|
for (let key of Reflect.ownKeys(a)) {
|
|
for (let key of Reflect.ownKeys(a)) {
|
|
@@ -129,3 +47,321 @@ function deepObjectEquals(a, b) {
|
|
}
|
|
}
|
|
return true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+class ExpectationError extends Error {
|
|
|
|
+ constructor(message, fileName, lineNumber) {
|
|
|
|
+ super(message, fileName, lineNumber);
|
|
|
|
+ this.name = "ExpectationError";
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+class Expector {
|
|
|
|
+ constructor(target, inverted) {
|
|
|
|
+ this.target = target;
|
|
|
|
+ this.inverted = !!inverted;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ get not() {
|
|
|
|
+ return new Expector(this.target, !this.inverted);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBe(value) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(Object.is(this.target, value));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toHaveLength(length) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(Object.is(this.target.length, length));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toHaveProperty(property, value) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ let object = this.target;
|
|
|
|
+
|
|
|
|
+ if (typeof property === "string" && property.includes(".")) {
|
|
|
|
+ let propertyArray = [];
|
|
|
|
+
|
|
|
|
+ while (true) {
|
|
|
|
+ let index = property.indexOf(".");
|
|
|
|
+ if (index === -1) {
|
|
|
|
+ propertyArray.push(property);
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ propertyArray.push(property.substring(0, index));
|
|
|
|
+ property = property.substring(index, property.length);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ property = propertyArray;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if (Array.isArray(property)) {
|
|
|
|
+ for (let key of property) {
|
|
|
|
+ if (object === undefined || object === null) {
|
|
|
|
+ if (this.inverted)
|
|
|
|
+ return;
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ }
|
|
|
|
+ object = object[key];
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ object = object[property];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.__expect(object !== undefined);
|
|
|
|
+ if (value !== undefined)
|
|
|
|
+ this.__expect(deepEquals(object, value));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeCloseTo(number, numDigits) {
|
|
|
|
+ if (numDigits === undefined)
|
|
|
|
+ numDigits = 2;
|
|
|
|
+
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(Math.abs(number - this.target) < (10 ** -numDigits / numDigits));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeDefined() {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target !== undefined);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeFalsey() {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(!this.target);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeGreaterThan(number) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target > number);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeGreaterThanOrEqual(number) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target >= number);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeLessThan(number) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target < number);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeLessThanOrEqual(number) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target <= number);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeInstanceOf(class_) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target instanceof class_);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeNull() {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target === null);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeTruthy() {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(!!this.target);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeUndefined() {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(this.target === undefined);
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toBeNaN() {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(isNaN(this.target));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toContain(item) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ // FIXME: Iterator check
|
|
|
|
+ for (let element of this.target) {
|
|
|
|
+ if (item === element)
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toContainEqual(item) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ // FIXME: Iterator check
|
|
|
|
+ for (let element of this.target) {
|
|
|
|
+ if (deepEquals(item, element))
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toEqual(value) {
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(deepEquals(this.target, value));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ toThrow(value) {
|
|
|
|
+ this.__expect(typeof this.target === "function");
|
|
|
|
+ this.__expect(typeof value === "string" || typeof value === "function" || value === undefined);
|
|
|
|
+
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ try {
|
|
|
|
+ this.target();
|
|
|
|
+ this.__expect(false);
|
|
|
|
+ } catch (e) {
|
|
|
|
+ if (typeof value === "string") {
|
|
|
|
+ this.__expect(e.message.includes(value));
|
|
|
|
+ } else if (typeof value === "function") {
|
|
|
|
+ this.__expect(e instanceof value);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pass(message) {
|
|
|
|
+ // FIXME: This does nothing. If we want to implement things
|
|
|
|
+ // like assertion count, this will have to do something
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // jest-extended
|
|
|
|
+ fail(message) {
|
|
|
|
+ // FIXME: message is currently ignored
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(false);
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // jest-extended
|
|
|
|
+ toThrowWithMessage(class_, message) {
|
|
|
|
+ this.__expect(typeof this.target === "function");
|
|
|
|
+ this.__expect(class_ !== undefined);
|
|
|
|
+ this.__expect(message !== undefined);
|
|
|
|
+
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ try {
|
|
|
|
+ this.target();
|
|
|
|
+ this.__expect(false);
|
|
|
|
+ } catch (e) {
|
|
|
|
+ this.__expect(e instanceof class_);
|
|
|
|
+ this.__expect(e.message.includes(message));
|
|
|
|
+ }
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Test for syntax errors; target must be a string
|
|
|
|
+ toEval() {
|
|
|
|
+ this.__expect(typeof this.target === "string");
|
|
|
|
+
|
|
|
|
+ if (!this.inverted) {
|
|
|
|
+ try {
|
|
|
|
+ new Function(this.target)();
|
|
|
|
+ } catch (e) {
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ try {
|
|
|
|
+ new Function(this.target)();
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ } catch (e) {
|
|
|
|
+ if (e.name !== "SyntaxError")
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Must compile regardless of inverted-ness
|
|
|
|
+ toEvalTo(value) {
|
|
|
|
+ this.__expect(typeof this.target === "string");
|
|
|
|
+
|
|
|
|
+ let result;
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ result = new Function(this.target)();
|
|
|
|
+ } catch (e) {
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ this.__doMatcher(() => {
|
|
|
|
+ this.__expect(deepEquals(value, result));
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ __doMatcher(matcher) {
|
|
|
|
+ if (!this.inverted) {
|
|
|
|
+ matcher();
|
|
|
|
+ } else {
|
|
|
|
+ let threw = false;
|
|
|
|
+ try {
|
|
|
|
+ matcher();
|
|
|
|
+ } catch (e) {
|
|
|
|
+ if (e.name === "ExpectationError")
|
|
|
|
+ threw = true;
|
|
|
|
+ }
|
|
|
|
+ if (!threw)
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ __expect(value) {
|
|
|
|
+ if (value !== true)
|
|
|
|
+ throw new ExpectationError();
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+expect = value => new Expector(value);
|
|
|
|
+
|
|
|
|
+// describe is able to lump test results inside of it by using this context
|
|
|
|
+// variable. Top level tests are assumed to be in the default context
|
|
|
|
+const defaultSuiteMessage = "__$$TOP_LEVEL$$__";
|
|
|
|
+let suiteMessage = defaultSuiteMessage;
|
|
|
|
+
|
|
|
|
+describe = (message, callback) => {
|
|
|
|
+ suiteMessage = message;
|
|
|
|
+ callback();
|
|
|
|
+ suiteMessage = defaultSuiteMessage;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+test = (message, callback) => {
|
|
|
|
+ if (!__TestResults__[suiteMessage])
|
|
|
|
+ __TestResults__[suiteMessage] = {};
|
|
|
|
+
|
|
|
|
+ const suite = __TestResults__[suiteMessage];
|
|
|
|
+
|
|
|
|
+ if (!suite[message])
|
|
|
|
+ suite[message] = {};
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ callback();
|
|
|
|
+ suite[message] = {
|
|
|
|
+ passed: true,
|
|
|
|
+ };
|
|
|
|
+ } catch (e) {
|
|
|
|
+ suite[message] = {
|
|
|
|
+ passed: false,
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+})();
|