diff options
author | Matthew Olsson <matthewcolsson@gmail.com> | 2020-07-03 14:36:58 -0700 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2020-07-06 23:40:35 +0200 |
commit | b9cf7a833f46fb6a3add0802b6b9f65f9e70ea04 (patch) | |
tree | ee80547ab9a1907290808710bf4884e0eeda731d /Libraries/LibJS/Tests | |
parent | c43afe71b7dbe6ca0d76bf75631f85e463489d7c (diff) | |
download | serenity-b9cf7a833f46fb6a3add0802b6b9f65f9e70ea04.zip |
LibJS/test-js: Create test-js program, prepare for test suite refactor
This moves most of the work from run-tests.sh to test-js.cpp. This way,
we have a lot more control over how the test suite runs, as well as how
it outputs. This should result in some cool functionality!
This commit also refactors test-common.js to mimic the jest library.
This should allow tests to be much more expressive :)
Diffstat (limited to 'Libraries/LibJS/Tests')
-rw-r--r-- | Libraries/LibJS/Tests/test-common.js | 440 |
1 files changed, 338 insertions, 102 deletions
diff --git a/Libraries/LibJS/Tests/test-common.js b/Libraries/LibJS/Tests/test-common.js index 4c7e23ac8b..d109988bca 100644 --- a/Libraries/LibJS/Tests/test-common.js +++ b/Libraries/LibJS/Tests/test-common.js @@ -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)) return Array.isArray(b) && deepArrayEquals(a, b); if (typeof a === "object") 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) return false; for (let i = 0; i < a.length; ++i) { @@ -120,7 +38,7 @@ function deepArrayEquals(a, b) { return true; } -function deepObjectEquals(a, b) { +const deepObjectEquals = (a, b) => { if (a === null) return b === null; for (let key of Reflect.ownKeys(a)) { @@ -129,3 +47,321 @@ function deepObjectEquals(a, b) { } 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, + }; + } +} + +})(); |