Comparing version 1.0.0-beta.3 to 1.0.0-rc.0
456
Assert.js
'use strict'; | ||
const inspect = require('./inspect').inspect; | ||
const isNode = typeof window === 'undefined'; | ||
const inspect = require(isNode ? 'util' : './inspect').inspect; | ||
const arraySlice = Array.prototype.slice; | ||
const toString = Object.prototype.toString; | ||
const toStringMap = {}; | ||
const toStringRe = /^\[object ([^\]]+)]$/; | ||
const useTypeOfRe = /booolean|number|string|undefined/; | ||
function toArray (value) { | ||
if (!value) { | ||
return []; | ||
} | ||
if (!Array.isArray(value)) { | ||
value = [value]; | ||
} | ||
return value; | ||
} | ||
/** | ||
@@ -32,12 +17,14 @@ * @class Assert | ||
constructor (value) { | ||
constructor (value, previous) { | ||
let me = this; | ||
me.modifiers = {}; | ||
me.state = me._getEntry('$', false); | ||
me.value = value; | ||
if (!me.state) { | ||
me._modifiers = {}; | ||
me._previous = previous || null; | ||
me._state = me._getEntry('$', false); | ||
if (!me._state) { | ||
me.constructor.setup(); | ||
me.state = me._getEntry('$'); | ||
me._state = me._getEntry('$'); | ||
} | ||
@@ -51,5 +38,10 @@ } | ||
if (expected) { | ||
let result = !me.def.fn.call(me, me.value, ...expected); | ||
try { | ||
let result = !me._def.fn.call(me, me.value, ...expected); | ||
me.failed = me.modifiers.not ? !result : result; | ||
me.failed = me._modifiers.not ? !result : result; | ||
} | ||
catch (e) { | ||
me.failed = e; | ||
} | ||
} | ||
@@ -59,3 +51,3 @@ } | ||
before (def) { | ||
this.def = def; | ||
this._def = def; | ||
} | ||
@@ -66,14 +58,14 @@ | ||
let A = me.constructor; | ||
let async = me.async; | ||
let previous = A._previous; | ||
let async = me._async; | ||
let previous = A._current; | ||
if (previous && !async) { | ||
me.async = previous.async.then(() => { | ||
me._async = previous._async.then(() => { | ||
me.begin(expected); | ||
return me.async; // this is reassigned below | ||
return me._async; // this is reassigned below | ||
}, | ||
e => { | ||
if (A._previous === me) { | ||
if (A._current === me) { | ||
// If this is the end of the line of assertions, clean things up | ||
A._previous = null; | ||
A._current = null; | ||
} | ||
@@ -85,12 +77,12 @@ | ||
// This assert was made after some other async asserts, so get in line... | ||
A._previous = me; | ||
A._current = me; | ||
} | ||
else if (async || A._isPromise(me.value) || A._isPromise(...expected)) { | ||
me.async = A.Promise.all(expected.concat(me.value)).then(values => { | ||
else if (async || Util.isPromise(me.value) || Util.isPromise(...expected)) { | ||
me._async = A.Promise.all(expected.concat(me.value)).then(values => { | ||
me.expected = values; | ||
me.value = values.pop(); | ||
if (A._previous === me) { | ||
if (A._current === me) { | ||
// If this is the end of the line of assertions, clean things up | ||
A._previous = null; | ||
A._current = null; | ||
} | ||
@@ -102,4 +94,4 @@ | ||
e => { | ||
if (A._previous === me) { | ||
A._previous = null; | ||
if (A._current === me) { | ||
A._current = null; | ||
} | ||
@@ -113,3 +105,3 @@ | ||
// This is the first async assert, so start the line with it... | ||
A._previous = me; | ||
A._current = me; | ||
} | ||
@@ -128,3 +120,3 @@ } | ||
let me = this; | ||
let def = me.def; | ||
let def = me._def; | ||
let fn = def.explain; | ||
@@ -139,3 +131,3 @@ let exp = me.expected; | ||
let mods = Object.keys(me.modifiers); | ||
let mods = Object.keys(me._modifiers); | ||
@@ -159,6 +151,6 @@ me.assertions = mods; | ||
let me = this; | ||
let async = me.async; | ||
let async = me._async; | ||
let A = me.constructor; | ||
let conjunction = { | ||
and: new A(me.value) | ||
and: new A(me.value, me) | ||
}; | ||
@@ -173,3 +165,3 @@ | ||
prop (name) { | ||
get (name) { | ||
let v = this.value; | ||
@@ -207,3 +199,3 @@ | ||
fn (actual, expected) { | ||
let te = A.typeOf(expected); | ||
let te = Util.typeOf(expected); | ||
let re = te === 'regexp'; | ||
@@ -215,3 +207,3 @@ | ||
let t = A.typeOf(actual); | ||
let t = Util.typeOf(actual); | ||
@@ -226,3 +218,3 @@ if (re) { | ||
if (typeof type === 'string') { | ||
this.expectation = type; | ||
this.expectation = type; // removes the quotes on "type" | ||
} | ||
@@ -246,3 +238,3 @@ } | ||
this.assertions.pop(); // remove "approximately" | ||
this.assertions.pop(); // remove "approx" | ||
this.expectation = `${A.print(expected)} ± ${A.print(epsilon)}`; | ||
@@ -271,3 +263,3 @@ } | ||
if (t === 'string' || A.isArrayLike(actual)) { | ||
if (t === 'string' || Util.isArrayLike(actual)) { | ||
ret = !actual.length; | ||
@@ -289,5 +281,9 @@ } | ||
'to.equal' (actual, expected) { | ||
return A.isEqual(actual, expected); | ||
return Util.isEqual(actual, expected); | ||
}, | ||
'falsy' (actual) { | ||
return !actual; | ||
}, | ||
'greaterThan,gt,above' (actual, expected) { | ||
@@ -307,4 +303,4 @@ return actual > expected; | ||
let ok = true; | ||
let only = this.modifiers.only; | ||
let own = this.modifiers.own; | ||
let only = this._modifiers.only; | ||
let own = this._modifiers.own; | ||
let keyMap = only && {}; | ||
@@ -370,6 +366,6 @@ | ||
fn (object, property, value) { | ||
let only = this.modifiers.only; | ||
let only = this._modifiers.only; | ||
//TODO deep properties "foo.bar.baz" | ||
if (this.modifiers.own) { | ||
if (this._modifiers.own) { | ||
if (!object.hasOwnProperty(property)) { | ||
@@ -412,3 +408,3 @@ return false; | ||
fn (actual, expected) { | ||
return A.isEqual(actual, expected, true); | ||
return Util.isEqual(actual, expected, true); | ||
}, | ||
@@ -444,18 +440,4 @@ explain () { | ||
'truthy,ok': { | ||
fn (actual) { | ||
return !!actual; | ||
}, | ||
explain () { | ||
let assertions = this.assertions; | ||
let not = this.modifiers.not; | ||
assertions.pop(); // remove "truthy" | ||
if (not) { | ||
assertions.splice(assertions.indexOf('not'), 1); | ||
} | ||
this.expectation = not ? 'falsy' : 'truthy'; | ||
} | ||
'truthy,ok' (actual) { | ||
return !!actual; | ||
}, | ||
@@ -471,3 +453,3 @@ | ||
else if (typeof min === 'string') { | ||
let m = this.constructor.rangeRe.exec(min); | ||
let m = Util.rangeRe.exec(min); | ||
lo = m[1]; | ||
@@ -493,3 +475,2 @@ min = parseFloat(m[2]); | ||
static normalize (registry) { | ||
let A = this; | ||
let ret = {}; | ||
@@ -508,28 +489,28 @@ | ||
def = { | ||
before: def // to: 'be' OR 'to.have': ['only', 'own'] | ||
after: def // to: 'be' OR 'to.have': ['only', 'own'] | ||
}; | ||
} | ||
let after = toArray(def.after); | ||
let before = toArray(def.before); | ||
let after = Util.toArray(def.after); | ||
let before = Util.toArray(def.before); | ||
if (A.tupleRe.test(name)) { | ||
if (Util.tupleRe.test(name)) { | ||
// 'afters.name,alias.befores' | ||
let tuples = name.split(A.tupleRe); | ||
let a = tuples.shift(); | ||
let tuples = name.split(Util.tupleRe); | ||
let b = tuples.shift(); | ||
name = tuples.shift(); | ||
let b = tuples[0]; | ||
let a = tuples[0]; | ||
a = a && a.split(','); | ||
b = b && b.split(','); | ||
a = a && a.split(','); | ||
before = b ? before.concat(b) : before; | ||
after = a ? after.concat(a) : after; | ||
after = a ? after.concat(a) : after; | ||
before = b ? before.concat(b) : before; | ||
} | ||
if (!after.length) { | ||
after = [def.fn ? (name === 'be' ? 'to' : 'be') : '$']; | ||
if (!before.length) { | ||
before = [def.fn ? (name === 'be' ? 'to' : 'be') : '$']; | ||
} | ||
if (after.indexOf('to') > -1 && after.indexOf('not') < 0) { | ||
after = ['not'].concat(after); | ||
if (before.indexOf('to') > -1 && before.indexOf('not') < 0) { | ||
before = ['not'].concat(before); | ||
} | ||
@@ -560,4 +541,11 @@ | ||
const P = A.prototype; | ||
const registry = A.normalize((typeof name === 'string') ? { [name] : def } : name); | ||
const t = typeof name; | ||
if (t === 'function') { | ||
name.call(A, A, Util); | ||
return; | ||
} | ||
const registry = A.normalize((t === 'string') ? { [name]: def } : name); | ||
for (name in registry) { | ||
@@ -571,5 +559,15 @@ def = registry[name]; | ||
let entry = A._getEntry(s, name); | ||
entry.add(def.before); | ||
entry.add(def.after); | ||
if (fn) { | ||
let was = entry.def; | ||
if (was) { | ||
def.fn._super = was.fn; | ||
if (def.explain) { | ||
def.explain._super = was.explain || null; | ||
} | ||
} | ||
entry.def = def; | ||
entry.fn = fn; | ||
@@ -582,4 +580,4 @@ A._addFn(P, s, fn); | ||
for (let a of def.after) { | ||
entry = A._getEntry(a); | ||
for (let b of def.before) { | ||
entry = A._getEntry(b); | ||
entry.add(s); | ||
@@ -598,2 +596,26 @@ } | ||
static print (obj, options) { | ||
let t = Util.typeOf(obj); | ||
if (t === 'error') { | ||
if (obj.message) { | ||
return `${obj.name}(${this.print(obj.message)})`; | ||
} | ||
return `${obj.name}`; | ||
} | ||
else if (t === 'arguments') { | ||
obj = arraySlice.call(obj); | ||
} | ||
else if (t === 'number') { | ||
if (isNaN(obj)) { | ||
return 'NaN'; | ||
} | ||
if (!isFinite(obj)) { | ||
return obj < 0 ? '-∞' : '∞'; | ||
} | ||
} | ||
return Util.inspect(obj, options); | ||
} | ||
static report (assertion) { | ||
@@ -623,3 +645,3 @@ let failure = assertion.failed; | ||
static wrapAssertion (def) { | ||
return function (...expected) { | ||
let wrap = function (...expected) { | ||
let me = this; | ||
@@ -634,2 +656,6 @@ | ||
}; | ||
wrap.def = def; | ||
return wrap; | ||
} | ||
@@ -641,3 +667,3 @@ | ||
/** | ||
* This method accepts a modifier (such as `'not'`) and updates the `modifiers` | ||
* This method accepts a modifier (such as `'not'`) and updates the `_modifiers` | ||
* map accordingly. This method will throw an `Error` if modifier cannot be used | ||
@@ -649,7 +675,7 @@ * in the current state. | ||
_applyModifier (modifier) { | ||
let state = this.state; | ||
let state = this._state; | ||
this.state = state = state.get(modifier); | ||
this._state = state = state.get(modifier); | ||
this.modifiers[state.name] = true; | ||
this._modifiers[state.name] = true; | ||
} | ||
@@ -663,3 +689,3 @@ | ||
if (Object.getOwnPropertyDescriptor(target, name)) { | ||
throw new Error(`Expectation modifier "${name}" already defined`); | ||
throw new Error(`Modifier "${name}" already defined`); | ||
} | ||
@@ -684,2 +710,4 @@ | ||
Object.defineProperty(target, modifier, { | ||
configurable: true, | ||
get () { | ||
@@ -746,8 +774,38 @@ let bound = function (...args) { | ||
} | ||
if (name !== '$' && !Util.validNameRe.test(name)) { | ||
throw new Error(`Cannot register invalid name "${name}"`); | ||
} | ||
let A = this; | ||
let registry = A.hasOwnProperty('registry') ? A.registry : (A.registry = {}); | ||
if (!A.hasOwnProperty('registry')) { | ||
A.registry = {}; | ||
A.forbidden = {}; | ||
let names = []; | ||
for (let C = A; ; C = Object.getPrototypeOf(C)) { | ||
names.push(...Object.getOwnPropertyNames(C.prototype)); | ||
if (C === Assert) { | ||
break; | ||
} | ||
} | ||
names.sort(); | ||
for (let key of names) { | ||
if (Util.validNameRe.test(key)) { | ||
A.forbidden[key] = true; // all keys (even inherited ones) | ||
} | ||
} | ||
} | ||
let registry = A.registry; | ||
let entry = registry[name]; | ||
if (!entry && autoCreate !== false) { | ||
if (A.forbidden[name]) { | ||
throw new Error(`Cannot redefine "${name}"`); | ||
} | ||
registry[name] = entry = new A.Modifier(A, name, canonicalName); | ||
@@ -758,5 +816,80 @@ } | ||
} | ||
} // Assert | ||
static isArrayLike (v) { | ||
if (!v || this._notArrayLikeRe.test(typeof v)) { | ||
Assert.Modifier = class { | ||
constructor (owner, name, canonicalName) { | ||
this.all = []; | ||
this.map = {}; | ||
this.name = name; | ||
this.owner = owner; | ||
this.canonicalName = canonicalName || name; | ||
this.alias = this.canonicalName !== name; | ||
} | ||
add (name) { | ||
if (!name) { | ||
return; | ||
} | ||
let map = this.map; | ||
let all = this.all; | ||
let names = (typeof name === 'string') ? [name] : name; | ||
for (let s of names) { | ||
if (!map[s]) { | ||
map[s] = true; | ||
all.push(s); | ||
} | ||
} | ||
} | ||
get (name) { | ||
if (!this.map[name]) { | ||
let msg = `Expectation cannot use "${name}" `; | ||
if (this.name === '$') { | ||
msg += 'immediately'; | ||
} else { | ||
msg += `after "${this.name}"`; | ||
} | ||
throw new Error(msg); | ||
} | ||
return this.owner.registry[name]; | ||
} | ||
}; | ||
Assert.Promise = (typeof Promise !== 'undefined') && Promise.resolve && Promise; | ||
// This prevents shape changes but also is used to generate the list of forbidden | ||
// names: | ||
Object.assign(Assert.prototype, { | ||
_async: null, | ||
_def: null, | ||
actual: null, | ||
assertions: null, | ||
expectation: null, | ||
expected: null, | ||
failed: null, | ||
value: null | ||
}); | ||
const Util = Assert.Util = { | ||
notArrayLikeRe: /^(?:function|string)$/, | ||
promiseTypesRe: /^(?:function|object)$/, | ||
rangeRe: /^\s*([\[(])\s*(\d+),\s*(\d+)\s*([\])])\s*$/, | ||
tupleRe: /[\.|\/]/, | ||
validNameRe: /^[a-z][a-z0-9_]*$/i, | ||
inspect: inspect, | ||
isArrayLike (v) { | ||
if (!v || Util.notArrayLikeRe.test(typeof v)) { | ||
return false; | ||
@@ -773,5 +906,5 @@ } | ||
return false; | ||
} | ||
}, | ||
static isEqual (o1, o2, strict) { | ||
isEqual (o1, o2, strict) { | ||
if (o1 === o2) { | ||
@@ -800,4 +933,4 @@ return true; | ||
let t1 = this.typeOf(o1); | ||
let t2 = this.typeOf(o2); | ||
let t1 = Util.typeOf(o1); | ||
let t2 = Util.typeOf(o2); | ||
@@ -856,3 +989,3 @@ if (t1 === t2) { | ||
k = keys1[i]; | ||
if (!this.isEqual(o1[k], o2[k], strict)) { | ||
if (!Util.isEqual(o1[k], o2[k], strict)) { | ||
return false; | ||
@@ -863,7 +996,7 @@ } | ||
return true; | ||
} | ||
}, | ||
static _isPromise (...obj) { | ||
isPromise (...obj) { | ||
for (let o of obj) { | ||
if (o && this.promiseTypesRe.test(typeof o) && typeof o.then === 'function') { | ||
if (o && Util.promiseTypesRe.test(typeof o) && typeof o.then === 'function') { | ||
return true; | ||
@@ -874,29 +1007,25 @@ } | ||
return false; | ||
} | ||
}, | ||
static print (obj, options) { | ||
let t = this.typeOf(obj); | ||
toArray (value) { | ||
if (value == null) { | ||
return []; | ||
} | ||
if (t === 'error') { | ||
if (obj.message) { | ||
return `${obj.name}(${this.print(obj.message)})`; | ||
if (!Array.isArray(value)) { | ||
if (Util.isArrayLike(value)) { | ||
let ret = []; | ||
for (let i = value.length; i--; ) { | ||
ret[i] = value[i]; | ||
} | ||
return ret; | ||
} | ||
return `${obj.name}`; | ||
value = [value]; | ||
} | ||
else if (t === 'arguments') { | ||
obj = arraySlice.call(obj); | ||
} | ||
else if (t === 'number') { | ||
if (isNaN(obj)) { | ||
return 'NaN'; | ||
} | ||
if (!isFinite(obj)) { | ||
return obj < 0 ? '-∞' : '∞'; | ||
} | ||
} | ||
return inspect(obj, options); | ||
} | ||
return value; | ||
}, | ||
static typeOf (v) { | ||
typeOf (v) { | ||
if (v === null) { | ||
@@ -906,11 +1035,12 @@ return 'null'; | ||
let cache = Util._typeOf.cache; | ||
let t = typeof v; | ||
if (!useTypeOfRe.test(t)) { | ||
if (!Util._typeOf.useTypeOfRe.test(t)) { | ||
let s = toString.call(v); | ||
if (!(t = toStringMap[s])) { | ||
let m = toStringRe.exec(s); | ||
if (!(t = cache[s])) { | ||
let m = Util._typeOf.toStringRe.exec(s); | ||
toStringMap[s] = t = m ? m[1].toLowerCase() : s; | ||
cache[s] = t = m ? m[1].toLowerCase() : s; | ||
} | ||
@@ -920,61 +1050,11 @@ } | ||
return t; | ||
} | ||
} // Assert | ||
}, | ||
Assert._notArrayLikeRe = /^(?:function|string)$/; | ||
Assert.promiseTypesRe = /^(?:function|object)$/; | ||
Assert.rangeRe = /^\s*([\[(])\s*(\d+),\s*(\d+)\s*([\])])\s*$/; | ||
Assert.tupleRe = /[\.|\/]/; | ||
Assert.Modifier = class { | ||
constructor (owner, name, canonicalName) { | ||
this.all = []; | ||
this.map = {}; | ||
this.name = name; | ||
this.owner = owner; | ||
this.canonicalName = canonicalName || name; | ||
this.alias = this.canonicalName !== name; | ||
_typeOf: { | ||
cache: {}, | ||
toStringRe: /^\[object ([^\]]+)]$/, | ||
useTypeOfRe: /booolean|number|string|undefined/ | ||
} | ||
add (name) { | ||
if (!name) { | ||
return; | ||
} | ||
let map = this.map; | ||
let all = this.all; | ||
let names = (typeof name === 'string') ? [name] : name; | ||
for (let s of names) { | ||
if (!map[s]) { | ||
map[s] = true; | ||
all.push(s); | ||
} | ||
} | ||
} | ||
get (name) { | ||
if (!this.map[name]) { | ||
let msg = `Expectation cannot use "${name}" `; | ||
if (this.name === '$') { | ||
msg += 'immediately'; | ||
} else { | ||
msg += `after "${this.name}"`; | ||
} | ||
throw new Error(msg); | ||
} | ||
return this.owner.registry[name]; | ||
} | ||
}; | ||
Assert.Promise = (typeof Promise !== 'undefined') && Promise.resolve && Promise; | ||
Object.assign(Assert.prototype, { | ||
async: null | ||
}); | ||
module.exports = Assert; |
@@ -9,3 +9,3 @@ 'use strict'; | ||
let entries = Assert.entries; | ||
let entries = Assert.registry; | ||
let keys = Object.keys(entries); | ||
@@ -12,0 +12,0 @@ |
@@ -45,3 +45,3 @@ # Assert | ||
Each piece of the dot-chain (`to`, `not` and `be`) are first processed as a property | ||
getter and are tracked in a `modifiers` object. | ||
getter and are tracked in a `_modifiers` object. | ||
@@ -55,11 +55,11 @@ Note: Due to this use of property getters means **assertly** cannot support IE8 even | ||
a.to; // sets "a.modifiers.to = true" and returns "a" | ||
a.not; // sets "a.modifiers.not = true" and returns "a" | ||
a.be(2); // sets "a.modifiers.be = true" and calls "a.be()" | ||
a.to; // sets "a._modifiers.to = true" and returns "a" | ||
a.not; // sets "a._modifiers.not = true" and returns "a" | ||
a.be(2); // sets "a._modifiers.be = true" and calls "a.be()" | ||
So in the end we have this: | ||
a.modifiers = { to: true, not: true, be: true }; | ||
a._modifiers = { to: true, not: true, be: true }; | ||
The assertion is also collected in the `modifiers` object because the property getter | ||
The assertion is also collected in the `_modifiers` object because the property getter | ||
cannot know if `a.be` will be invoked or use as a step in a deeper dot-chain: | ||
@@ -82,25 +82,15 @@ | ||
the `and` method (as well as a `then` method when using promises). The `and` method | ||
creates a new `Assert` instance and passes along the same value. | ||
creates a new `Assert` instance and passes along the same value and itself: | ||
## Reporting | ||
return { | ||
and: new Assert(this.value, this) | ||
}; | ||
Inside the assertion, the `Assert` instance is passed to a static method to report | ||
the result: | ||
The previous `Assert` instance is stored as `_previous` on the new instance. | ||
Assert.report(this); | ||
If the assertion fails, this method calls another static method: | ||
Assert.reportFailure(this); | ||
It is this method that contains the `throw` statement. | ||
Because `Assert` is an ES6 class these static methods can be overridden in a derived | ||
class. This is most useful when [integrating](./Integration.md) with other modules. | ||
## Custom Modifiers and Assertions | ||
All of the modifiers and assertions provided by `Assert` are dynamically added | ||
by the `setup()` method using the `register()` method. | ||
using the `register()` method. | ||
For more on these use cases, see [Extensibility](./Extensibility.md). |
# Extensibility | ||
The [Assert](./Assert.md) class is primarily intended to be created as a worker object | ||
by the public `expect` method. The `Assert` class is also designed, however, to allow | ||
for derivation and customization. | ||
by the public `expect` method. The `Assert` class can also be extended or customized. | ||
@@ -10,4 +9,4 @@ ## Registering Customizations | ||
All of the modifiers and assertions are incorporated by the static `register` method. | ||
This method accepts an object that describes the allowed modifiers and their sequence | ||
as well as the assertion functions. | ||
This method accepts an object (called the "registry") that describes the allowed | ||
modifiers and their sequence as well as the assertion functions. | ||
@@ -23,2 +22,3 @@ Consider this minimal form: | ||
// the rest of the arguments cme from the call to the assertion | ||
// "this" is the Assert instance | ||
@@ -55,5 +55,91 @@ return actual === expected; | ||
The `this` pointer when an assertion is called in the `Assert` instance. The most | ||
likely property to use is the `_modifiers` object which hold the pieces of the dot-chain | ||
used to arrive at the assertion (this includes the modifiers and the assertion method | ||
itself). | ||
While one can access `this._modifiers.not` in the assertion method, this is not | ||
recommended. This is because the truth result returned will be toggled based on this | ||
modifier and so is not needed in the truth test. | ||
When writing custom assertions, `Assert.Util` has some helpful [utility](./Utils.md) | ||
methods. | ||
#### Explaining Assertions | ||
The default mechanism for generating explanatory text for an assert is acceptable in | ||
most cases. This is because it reads almost identically to the assertion in the code | ||
itself. There are times, however, explanations can be improved with a little logic. | ||
Assert.register({ | ||
christmas: { | ||
fn (value) { | ||
return value.getMonth() === 11 && value.getDate() === 25; | ||
}, | ||
explain (value) { | ||
let y = value.getFullYear(); | ||
let x = +value; | ||
let t = +new Date(y, 11, 25) - x; | ||
let not = this._modifiers.not ? 'not ' : ''; | ||
if (t < 0) { | ||
t = +new Date(y+1, 11, 25) - x; | ||
} | ||
t = Math.round(t / (24 * 60 * 60 * 1000)); | ||
if (t) { | ||
return `Expected ${t} days before Christmas ${not}to be Christmas`; | ||
} | ||
return `Expected Christmas ${not}to be Christmas`; | ||
} | ||
} | ||
}); | ||
The parameters passed to `explain` are the same as those passed to the assertion `fn`, | ||
which are first the "actual" value (the one passed to `expect`) and then the values | ||
passed in the assertion call. | ||
For example: | ||
expect(x).to.be.foo(y, z); | ||
// ... | ||
Assert.register({ | ||
foo: { | ||
fn (x, y, z) { | ||
// truth test | ||
}, | ||
explain (x, y, z) { | ||
// explain the truth test | ||
} | ||
} | ||
}); | ||
The `this` pointer for an `explain` method is the `Assert` instance. Again, the most | ||
likely property to use is `_modifiers`. | ||
The `explain` method can (as above) return the full explanation. Alternatively, it | ||
can adjust the properties that are normally concatenated. The following properties | ||
are stored on the `Assert` instance for this purpose: | ||
- `actual` - A string with the printed (`Assert.print`) `value`. | ||
- `assertions` - A String[] of the modifiers followed by the assertion. | ||
- `expectation` - A string with the printed (`Assert.print`) `expected`. | ||
The default explanation consists of: | ||
Expected ${actual} ${assertions.join(' ')} ${expectation} | ||
For example: | ||
Expected 4 to be 2 | ||
### Modifiers | ||
Modifiers are added in the same way: | ||
Modifiers are added in basically the same way as assertions: | ||
@@ -76,22 +162,57 @@ Assert.register({ | ||
### Registry Keys | ||
As shown above, the keys of the registry object (the object passed to `register`) can | ||
contain some special syntactic forms. | ||
For a complete example: | ||
foo,bar,zip.thing,alt,other.blerp,derp | ||
\ / \ / \ / \ / | ||
\_______/ \_/ \_____/ \______/ | ||
before name aliases after | ||
The key is first split on `'.'` (or alternatively `'|'` or `'/'` for readability). | ||
For example: | ||
foo,bar,zip|thing,alt,other|blerp,derp | ||
// or | ||
foo,bar,zip/thing,alt,other/blerp,derp | ||
If there is only one element, that element is the name and its possible alternate | ||
aliases. | ||
If there are two or more elements in the split, the first element is the **before** | ||
collection and the second is the name and aliases. The set of **before** values are | ||
those words that come _before_ the word being defined. As with 'to.randomly' above, | ||
the 'to' is a **before** for the word 'randomly'. | ||
Only if there are three parts in the split does the **after** set come into play. | ||
This set is the names that can come _after_ this name. | ||
All sets of names can be comma-delimited. When this is applied to the primary name, | ||
the second and subsequent names are considered aliases. | ||
### Advanced Configuration | ||
The value of each property passed to `register` is often a simple value, but its full | ||
and normalized form is an object with the following properties: | ||
The value of each property in the registry object is often a simple value, but its | ||
full, normalized form is an object with the following properties: | ||
- `alias` - The array of alternate names (e.g. "a" and "an"). | ||
- `after` - The array of names that this name comes after. | ||
- `before` - The array of names that this name comes before. | ||
- `after` - The array of names that come _after_ this name. | ||
- `before` - The array of names that come _before_ this name. | ||
- `fn` - The assertion `Function` that will return `true` for success. | ||
- `explain` - The `Function` that will return a string explaining the assertion. | ||
Rewriting the above in fully normal form: | ||
Rewriting the 'to.randomly' in normal form: | ||
Assert.register({ | ||
randomly: { | ||
after: ['to'] | ||
before: ['to'] | ||
}, | ||
throw: { | ||
after: ['to', 'randomly'], | ||
before: ['to', 'randomly'], | ||
fn (fn, type) { | ||
@@ -106,2 +227,16 @@ if (this.modifiers.randomly) { | ||
Rewriting the complete example: | ||
foo,bar,zip|thing,alt,other|blerp,derp | ||
Would look like this: | ||
Assert.register({ | ||
thing: { | ||
alias: ['alt', 'other' ], | ||
before: ['foo', 'bar', 'zip'] | ||
after: ['blerp', 'derp'] | ||
} | ||
}); | ||
## Adjusting The Defaults | ||
@@ -125,2 +260,31 @@ | ||
### Replacing Assertions | ||
When an assertion is already registered, calling `register` again will replace it. | ||
The original assertion function and its `explain` method are preserved on the new | ||
functions. | ||
For example: | ||
Assert.setup(); // initialize with defaults | ||
// Take over nan assertion (which has an explain) | ||
Assert.register({ | ||
nan: { | ||
fn: function fn (actual, expected) { | ||
let r = fn._super.call(this, actual, expected); | ||
return r; | ||
}, | ||
explain: function fn (actual, expected) { | ||
let r = fn._super.call(this, actual, expected); | ||
return r; | ||
} | ||
} | ||
}); | ||
The `_super` property is stored on the function objects to form a chain. The odd | ||
syntax of declaring a named method is needed only when access to these replaced | ||
functions is desired. | ||
## Extending Assert | ||
@@ -147,1 +311,13 @@ | ||
common practice). | ||
As a derived class, it is also possible to implement the [Lifecycle](./Lifecycle.md) | ||
methods and customize these behaviors. | ||
### Add-ons | ||
You can use [add-ons](./Add-ons.md) with such classes in the same way as `Assert` | ||
itself: | ||
const addon = require('my-assertly-addon'); | ||
MyAssert.register(addon.init); |
@@ -7,89 +7,7 @@ # Integration | ||
Because [Assert](./Assert.md) is an ES6 class, it can be extended or even modified as | ||
necessary. Since testing is a much more controlled environment, it is not nearly as | ||
Because [Assert](./Assert.md) is an ES6 class, it can be extended cleanly. It can also | ||
be modified. Since testing is a much more controlled environment, it is not nearly as | ||
"hackish" to modify the `Assert` class and **assertly** tries to make this process as | ||
straightforward as possible. | ||
## Assert Life-Cycle Methods | ||
Each `Assert` instance goes through a simple life-cycle: | ||
- constructor | ||
- before | ||
- begin | ||
- assertion | ||
- explain | ||
- failure | ||
- report | ||
- finish | ||
The `constructor` is passed the "actual" value by the `expect` static method. After | ||
the dot-chain is processed and the `modifiers` are accumulated, eventually an | ||
assertion method is called. When that happens, the rest of the life-cycle is run. | ||
### before | ||
The `before` method is called when the assertion method is called. It is passed | ||
the "definition" object that defines the assertion. This is stored as `def` on the | ||
`Assert` instance. | ||
### begin | ||
This method is called passing the array of "expected" arguments (the arguments that | ||
were passed to the assertion method). | ||
To support promises, `begin` checks for a pending assertion and chains its evaluation | ||
to that assertion's completion. If the "actual" value (given to the constructor) or | ||
the "expected" array have promises, `begin` uses `Assert.Promise.all()` to resolve | ||
them and calls `begin` again with the resolved values. | ||
In the end, `begin` stores the array it is given as the `expected` property on the | ||
`Assert` instance. | ||
### assertion | ||
This method is called to invoke the actual assertion logic (`this.def.fn()`) and | ||
pass the actual and expected values. If the `not` modifier is present, the result | ||
is inverted. | ||
This method will set the `failed` property (possibly to `false`). | ||
This method does nothing if called before the `expected` instance property is set | ||
by `begin`. | ||
### explain | ||
This method is called to generate a string to explain an assertion. It is only | ||
called (by default) when an assertion fails. It is called by `report` before the | ||
instance is passed to the `Assert.report()` static method (more below). | ||
In the act of generating the explanation, several other properties are set: | ||
- `actual` - A string with the printed (`Assert.print`) `value`. | ||
- `assertions` - A String[] of the modifiers followed by the assertion. | ||
- `expectation` - A string with the printed (`Assert.print`) `expected`. | ||
These are primarily of interest when writing custom assertions. See | ||
[Extensibility](./Extensibility.md). | ||
### failure | ||
This method is called if the promise chain rejects and the assertion cannot be | ||
evaluated. This sets `failed` to the `Error` object given. | ||
### report | ||
This method is called to report the `Assert` results. Before passing on to the | ||
`Assert.report()` static method (see below), this method ensures that the `failed` | ||
property (if set to `true`) has been converted to a string explaining the failure. | ||
This method does nothing if called before the `failed` instance property is set | ||
by `assertion`. | ||
### finish | ||
This method is called at the end of the life-cycle. It returns an object that | ||
can be used as a conjunction (via `and`) or a promise (via `then`) if the assertion | ||
was asynchronous. | ||
## Static Methods | ||
@@ -106,2 +24,7 @@ | ||
### register | ||
This method is called to register modifiers and assertion methods. For more details | ||
see [Extensibility](./Extensibility.md). | ||
### report | ||
@@ -125,4 +48,4 @@ | ||
This method is called to wrap the assertion definition (the sole argument) to | ||
provide the proper life-cycles (as described above). The returned function is | ||
provide the proper [lifecycle](./Lifecycle.md). The returned function is | ||
what is returned by the property getter when the assertion is requested. For | ||
more details on an assertion definition see [Extensibility](./Extensibility.md). |
{ | ||
"name": "assertly", | ||
"version": "1.0.0-beta.3", | ||
"version": "1.0.0-rc.0", | ||
"description": "Assert class for BDD-style assertions", | ||
@@ -5,0 +5,0 @@ "main": "Assert.js", |
@@ -9,4 +9,5 @@ # assertly | ||
There are some additional goals, however, that led to this library: | ||
Assertly was created to address these shortcomings in **expect.js**: | ||
- [Add-ons](docs/Add-ons.md) | ||
- [Extensibility](docs/Extensibility.md) | ||
@@ -16,6 +17,30 @@ - [Integration](docs/Integration.md) | ||
## Why Not Chai? | ||
Chai pretty much has the above covered, but the somewhat common (and troubling) | ||
practice of using "dangling getters" is something I think should be avoided. While | ||
their use is not essential, it is, as mentioned, common practice. For example: | ||
expect(x).to.be.null; // dangling getter | ||
IDE's and linters **rightly** warn that an expression like the above "has no side-effects" | ||
or "does nothing". | ||
# API | ||
The Assertly API is based on BDD-style expectations. For example: | ||
expect(x).to.be(2); | ||
Where "x" is the "actual" value and 2 is the "expected" value. All things begin with | ||
the call to the `expect` method which returns an `Assert` instance. | ||
This instance has properties (like `to`) that modify the conditions of the expectation | ||
(called "modifiers") and methods (like `be`) that test these conditions (called | ||
"assertions"). | ||
## Modifiers | ||
Following are the modifiers provided by Assertly itself. | ||
### not | ||
@@ -38,3 +63,3 @@ | ||
Following are the assertion methods and their common aliases ("aka" = "also known as"). | ||
Following are the assertion methods and their aliases ("aka" = "also known as"). | ||
@@ -139,2 +164,18 @@ ### a (aka: "an") | ||
### falsy | ||
- to[.not].be.falsy | ||
The `falsy` assertion ensures that the `actual` value is "false-like". In other | ||
words, that it would fail an `if` test. | ||
For example: | ||
expect(x).to.be.falsy(); | ||
The above is equivalent to: | ||
expect(!x).to.be(true); | ||
### greaterThan (aka: "above", "gt") | ||
@@ -236,3 +277,3 @@ | ||
expect(b).to.have.only.own.key('a'); | ||
expect(b).to.have.only.own.key('b'); | ||
@@ -245,3 +286,2 @@ This assertion succeeds because, while "b" inherits the "a" property, "b" is the | ||
- to[.not].only.have.length | ||
- to[.not].have.length | ||
@@ -356,7 +396,7 @@ | ||
expect(1).to.equal('1'); // fails since 1 !== '1' | ||
expect(1).to.be.same('1'); // fails since 1 !== '1' | ||
expect(1).to.equal(1); // succeeds just like "to.be(1)" | ||
expect(1).to.be.same(1); // succeeds just like "to.be(1)" | ||
For object and arrays, `equal` compares corresponding properties and elements with the | ||
For object and arrays, `same` compares corresponding properties and elements with the | ||
same logic. In other words, the following passes for `equal` and fails for `same``: | ||
@@ -363,0 +403,0 @@ |
Sorry, the diff of this file is too big to display
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
312238
39
4241
483
4