Comparing version 1.0.0-beta.2 to 1.0.0-beta.3
320
Assert.js
'use strict'; | ||
const BE = ['be']; | ||
const ROOT = ['$']; | ||
const TO = ['to']; | ||
const inspect = require('./inspect').inspect; | ||
@@ -13,2 +11,14 @@ const arraySlice = Array.prototype.slice; | ||
function toArray (value) { | ||
if (!value) { | ||
return []; | ||
} | ||
if (!Array.isArray(value)) { | ||
value = [value]; | ||
} | ||
return value; | ||
} | ||
/** | ||
@@ -175,133 +185,9 @@ * @class Assert | ||
/** | ||
* Adds assertion methods. | ||
* | ||
* Assert.register({ | ||
* to: ['be', 'have', 'include', 'only', 'not'], | ||
* | ||
* lt: { | ||
* test (actual, expected) { | ||
* return actual < expected; | ||
* }, | ||
* | ||
* explain (actual, expected) { | ||
* let not = this.modifiers.not ? 'not ' : ''; | ||
* return `Expected ${this.actual} to ${not}be less-than ${this.expected}`; | ||
* } | ||
* } | ||
* }); | ||
* | ||
* Assert.expect(42).to.be.lt(427); | ||
* // ^^ ^^^ | ||
* // actual expected | ||
*/ | ||
static register (name, def) { | ||
static getDefaults () { | ||
const A = this; | ||
const P = A.prototype; | ||
if (typeof name !== 'string') { | ||
let names = Object.keys(name); | ||
return this.normalize({ | ||
not: 'to', | ||
to: 'not', | ||
names.forEach(key => A.register(key, name[key])); | ||
names = Object.keys(A.entries); | ||
for (let s of names) { | ||
if (s !== '$' && !Object.getOwnPropertyDescriptor(P, s)) { | ||
A._addBit(P, s); | ||
} | ||
} | ||
return; | ||
} | ||
let after, before, entry; | ||
let fn = def; | ||
let t = typeof def; | ||
if (A.tupleRe.test(name)) { | ||
// 'afters.name,alias.befores' | ||
let tuples = name.split(A.tupleRe); | ||
let a = tuples.shift(); | ||
name = tuples.shift(); | ||
let b = tuples[0]; | ||
before = b && b.split(','); | ||
after = a && a.split(','); | ||
} | ||
if (t === 'function') { | ||
def = { fn: fn }; | ||
} else { | ||
fn = def.fn; | ||
} | ||
let names = name.split(','); | ||
name = names[0]; | ||
after = after || (fn ? (name === 'be' ? TO : BE) : ROOT); | ||
if (after.indexOf('to') > -1 && after.indexOf('not') < 0) { | ||
after = ['not'].concat(after); | ||
} | ||
if (t === 'string' || Array.isArray(def)) { | ||
for (let s of names) { | ||
A._addBit(P, s, name); | ||
//A._getEntry('$').add(s); | ||
for (let a of after) { | ||
entry = A._getEntry(a); | ||
entry.add(s); | ||
} | ||
entry = A._getEntry(s, name); | ||
entry.add(def); | ||
} | ||
return; | ||
} | ||
let wrap = fn && A.wrapAssertion(def); | ||
for (let s of names) { | ||
entry = A._getEntry(s, name); | ||
entry.add(before); | ||
if (fn) { | ||
entry.fn = wrap; | ||
A._addFn(P, s, wrap); | ||
} | ||
else { | ||
A._addBit(P, s, name); | ||
} | ||
for (let a of after) { | ||
entry = A._getEntry(a); | ||
entry.add(s); | ||
} | ||
} | ||
} // register | ||
static report (assertion) { | ||
let failure = assertion.failed; | ||
if (this.log) { | ||
this.log.push(assertion); | ||
} | ||
if (failure) { | ||
this.reportFailure(failure, assertion); | ||
} | ||
} | ||
static reportFailure (msg) { | ||
//console.error(msg); | ||
throw new Error(msg); | ||
} | ||
static setup () { | ||
const A = this; | ||
A.register({ | ||
not: ['to'], | ||
to: ['not'], | ||
'to.only': ['have', 'own'], | ||
@@ -520,3 +406,3 @@ 'to.have': ['own', 'only'], | ||
'to.throw' (fn, type) { | ||
let ok = false; | ||
let msg, ok = false; | ||
@@ -528,5 +414,4 @@ try { | ||
ok = true; | ||
msg = e.message; | ||
let msg = e.message; | ||
if (typeof type === 'string') { | ||
@@ -587,4 +472,128 @@ ok = (msg.indexOf(type) > -1); | ||
}); | ||
} // setup | ||
} // getDefaults | ||
static normalize (registry) { | ||
let A = this; | ||
let ret = {}; | ||
for (let name of Object.keys(registry)) { | ||
let def = registry[name]; | ||
let t = typeof def; | ||
if (t === 'function') { | ||
def = { | ||
fn: def | ||
}; | ||
} | ||
else if (t === 'string' || Array.isArray(def)) { | ||
def = { | ||
before: def // to: 'be' OR 'to.have': ['only', 'own'] | ||
}; | ||
} | ||
let after = toArray(def.after); | ||
let before = toArray(def.before); | ||
if (A.tupleRe.test(name)) { | ||
// 'afters.name,alias.befores' | ||
let tuples = name.split(A.tupleRe); | ||
let a = tuples.shift(); | ||
name = tuples.shift(); | ||
let b = tuples[0]; | ||
b = b && b.split(','); | ||
a = a && a.split(','); | ||
before = b ? before.concat(b) : before; | ||
after = a ? after.concat(a) : after; | ||
} | ||
if (!after.length) { | ||
after = [def.fn ? (name === 'be' ? 'to' : 'be') : '$']; | ||
} | ||
if (after.indexOf('to') > -1 && after.indexOf('not') < 0) { | ||
after = ['not'].concat(after); | ||
} | ||
let names = name.split(','); | ||
name = names.shift(); | ||
if (def.alias) { | ||
names = names.concat(def.alias); // handle String or String[] | ||
} | ||
ret[name] = { | ||
alias: names.length ? names : [], | ||
after: after, | ||
before: before, | ||
explain: def.explain || null, | ||
fn: def.fn || null, | ||
name: name | ||
}; | ||
} | ||
return ret; | ||
} | ||
static register (name, def) { | ||
const A = this; | ||
const P = A.prototype; | ||
const registry = A.normalize((typeof name === 'string') ? { [name] : def } : name); | ||
for (name in registry) { | ||
def = registry[name]; | ||
let fn = def.fn && A.wrapAssertion(def); | ||
let names = [name].concat(def.alias); | ||
for (let s of names) { | ||
let entry = A._getEntry(s, name); | ||
entry.add(def.before); | ||
if (fn) { | ||
entry.fn = fn; | ||
A._addFn(P, s, fn); | ||
} | ||
else { | ||
A._addBit(P, s, name); | ||
} | ||
for (let a of def.after) { | ||
entry = A._getEntry(a); | ||
entry.add(s); | ||
} | ||
} | ||
} | ||
let names = Object.keys(A.registry); | ||
for (let s of names) { | ||
if (s !== '$' && !Object.getOwnPropertyDescriptor(P, s)) { | ||
A._addBit(P, s); | ||
} | ||
} | ||
} | ||
static report (assertion) { | ||
let failure = assertion.failed; | ||
if (this.log) { | ||
this.log.push(assertion); | ||
} | ||
if (failure) { | ||
this.reportFailure(failure, assertion); | ||
} | ||
} | ||
static reportFailure (msg) { | ||
//console.error(msg); | ||
throw new Error(msg); | ||
} | ||
static setup () { | ||
let registry = this.getDefaults(); | ||
this.register(registry); | ||
} | ||
static wrapAssertion (def) { | ||
@@ -646,6 +655,2 @@ return function (...expected) { | ||
if (Object.getOwnPropertyDescriptor(target, modifier)) { | ||
throw new Error(`Assertion method "${modifier}" already defined`); | ||
} | ||
Object.defineProperty(target, modifier, { | ||
@@ -715,7 +720,7 @@ get () { | ||
let A = this; | ||
let entries = A.hasOwnProperty('entries') ? A.entries : (A.entries = {}); | ||
let entry = entries[name]; | ||
let registry = A.hasOwnProperty('registry') ? A.registry : (A.registry = {}); | ||
let entry = registry[name]; | ||
if (!entry && autoCreate !== false) { | ||
entries[name] = entry = new A.Modifier(A, name, canonicalName); | ||
registry[name] = entry = new A.Modifier(A, name, canonicalName); | ||
} | ||
@@ -838,15 +843,6 @@ | ||
static print (obj) { | ||
static print (obj, options) { | ||
let t = this.typeOf(obj); | ||
if (t === 'arguments') { | ||
obj = arraySlice.call(obj); | ||
} | ||
else if (t === 'function') { | ||
return obj.$className || obj.name || 'anonymous-function'; | ||
} | ||
else if (t === 'date') { | ||
return obj.toISOString(); | ||
} | ||
else if (t === 'error') { | ||
if (t === 'error') { | ||
if (obj.message) { | ||
@@ -857,4 +853,4 @@ return `${obj.name}(${this.print(obj.message)})`; | ||
} | ||
else if (t === 'regexp') { | ||
return String(obj); | ||
else if (t === 'arguments') { | ||
obj = arraySlice.call(obj); | ||
} | ||
@@ -868,11 +864,5 @@ else if (t === 'number') { | ||
} | ||
if (!obj && 1 / obj < 0) { | ||
// 0 and -0 are different things... sadly 0 === -0 but we can | ||
// tell them apart by dropping them in a denominator since 0 | ||
// produces Infinity and -0 produces -Infinity | ||
return '-0'; | ||
} | ||
} | ||
return JSON.stringify(obj); | ||
return inspect(obj, options); | ||
} | ||
@@ -901,4 +891,4 @@ | ||
Assert._notArrayLikeRe = /function|string/; | ||
Assert.promiseTypesRe = /function|object/; | ||
Assert._notArrayLikeRe = /^(?:function|string)$/; | ||
Assert.promiseTypesRe = /^(?:function|object)$/; | ||
Assert.rangeRe = /^\s*([\[(])\s*(\d+),\s*(\d+)\s*([\])])\s*$/; | ||
@@ -948,3 +938,3 @@ Assert.tupleRe = /[\.|\/]/; | ||
return this.owner.entries[name]; | ||
return this.owner.registry[name]; | ||
} | ||
@@ -951,0 +941,0 @@ }; |
# 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. | ||
## 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. | ||
Consider this minimal form: | ||
Assert.register({ | ||
to: 'not', // to.not is allowed | ||
not: 'to'. // not.to is allowed | ||
be (actual, expected) { | ||
// "actual" (arguments[0]) is the value passed to expect() | ||
// the rest of the arguments cme from the call to the assertion | ||
return actual === expected; | ||
} | ||
}); | ||
The above modifiers (`to` and `not`) and assertion (`be`) have some special handling | ||
in the registration process. | ||
- Modifiers default to being allowed to start the dot-chain. | ||
- Assertions default to following after `be`. | ||
- The `be` assertion defaults to following after `to`. | ||
- Modifiers and asserts that follow `to` can also follow `not`. | ||
### Assertions | ||
With the basics in place, the following adds a new assertion (`throw`): | ||
Assert.register({ | ||
'to.throw' (fn, type) { | ||
//... | ||
} | ||
}); | ||
The `throw` assertion follows the `to` modifier (it would have defaulted to `be`). The | ||
above enables these assertions: | ||
expect(fn).to.throw(type); | ||
expect(fn).not.to.throw(type); | ||
expect(fn).to.not.throw(type); | ||
### Modifiers | ||
Modifiers are added in the same way: | ||
Assert.register({ | ||
'to.randomly': true, | ||
'to,randomly.throw' (fn, type) { | ||
if (this.modifiers.randomly) { | ||
//... hmmm | ||
} | ||
//... | ||
} | ||
}); | ||
The above adds the (not very helpful) `randomly` modifier. The first key `to.randomly` | ||
allows `randomly` to follow the `to` modifier (by default it would belong at the start | ||
of the dot-chain). The second key defines the `throw` assertion and it can now be | ||
used after `to` or after `randomly`. | ||
### 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: | ||
- `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. | ||
- `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: | ||
Assert.register({ | ||
randomly: { | ||
after: ['to'] | ||
}, | ||
throw: { | ||
after: ['to', 'randomly'], | ||
fn (fn, type) { | ||
if (this.modifiers.randomly) { | ||
//... hmmm | ||
} | ||
//... | ||
} | ||
} | ||
}); | ||
## Adjusting The Defaults | ||
The above examples are overly simplistic since they replace the standard modifiers | ||
and assertions by only registering customizations. | ||
A more realistic approach: | ||
let registry = Assert.getDefaults(); | ||
// registry looks like the above object | ||
Assert.register(registry); | ||
This sequence is performed automatically when the first `Assert` instance is created, | ||
but only if no registrations have been made. An easy way to adjust the dot-chains and | ||
make additions or removals is to edit the object returned by `getDefaults`. The object | ||
returned by `getDefaults` is always fully normalized to simplify this task. | ||
## Extending Assert | ||
The `Assert` class can also be extended to make customizations. | ||
class MyAssert extends Assert { | ||
static expect (value) { | ||
return new MyAssert(value); | ||
} | ||
static getDefaults () { | ||
let registry = super.getDefaults(); | ||
// hack away | ||
return registry; | ||
} | ||
} | ||
This is similar to adjusting `Assert` itself, but obviously a bit safer if one is | ||
planning to combine test suites with those developed by others (though not a very | ||
common practice). |
@@ -101,2 +101,6 @@ # Integration | ||
The `print` method converts the object passed to it into a readable string. This | ||
method is used internally to print the actual and expected parameters in the | ||
`explain` method. | ||
### report | ||
@@ -103,0 +107,0 @@ |
@@ -63,2 +63,17 @@ # Promises | ||
To see this in action, consider this adjusted version: | ||
it('should work asynchronously', function () { | ||
expect(ajax('something.txt')).to.be('Some text').then(() => { | ||
console.log('A'); | ||
}); | ||
return expect(2).to.be(2).then(() => { | ||
console.log('B'); | ||
}); | ||
}); | ||
The log statements will appear in "A" then "B" order as a side-effect of the internal | ||
FIFO ordering of asynchronous assertions. | ||
## Custom Promise Implementations | ||
@@ -65,0 +80,0 @@ |
{ | ||
"name": "assertly", | ||
"version": "1.0.0-beta.2", | ||
"version": "1.0.0-beta.3", | ||
"description": "Assert class for BDD-style assertions", | ||
@@ -9,4 +9,4 @@ "main": "Assert.js", | ||
"debugx": "devtool node_modules/mocha/bin/_mocha -qc --break -- --compilers js:babel-core/register --require babel-polyfill ./test/specs/**/*.js", | ||
"debug-print": "devtool print.js", | ||
"print": "node print.js", | ||
"docs": "node docs.js", | ||
"docs-debug": "devtool docs.js", | ||
"test": "mocha ./test/specs/**/*.js", | ||
@@ -13,0 +13,0 @@ "testx": "mocha --compilers js:babel-core/register --require babel-polyfill ./test/specs/**/*.js" |
@@ -37,6 +37,3 @@ # assertly | ||
Following are the assertion methods and their common aliases ("aka" = "also known as"). | ||
The remainder of this document can be regenerated using: | ||
npm run print >> README.md | ||
### a (aka: "an") | ||
@@ -43,0 +40,0 @@ |
Sorry, the diff of this file is too big to display
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
289156
35
3952
443
3