From chai\'s api you\'ve got code like this:
.exist
Asserts that the target is neither null nor undefined.
var foo = \'hi\'
, bar = null
, baz;
expect
chai exposes an use
method to access the chai
export and it's utils
.
This method can be used by third parties when creating plugins, but it's also used internally to load it's interface.
The implementation of this method is simple:
exports.use = function (fn) {
if (!~used.indexOf(fn)) {
fn(this, util);
used.push(fn);
}
return this;
};
Internally it uses this to load (among other) the primary Assertion prototype
and the core assertion functionality:
var assertion = require('./chai/assertion'); // primary Assertion prototype
exports.use(assertion); // load it
var core = require('./chai/core/assertions'); // core assertion functionality
exports.use(core); // load it
One of the methods that are exposed by the Assertion prototype
is the addProperty
method which allows you to add properties to said prototype
.
Internally chai
uses this method to add the core assertion functionality to the Assertion prototype
. For instance, all language chains and assertion helpers (exist
, empty
, etc) are added this way.
Language chains:
[ 'to', 'be', 'been'
, 'is', 'and', 'has', 'have'
, 'with', 'that', 'which', 'at'
, 'of', 'same' ].forEach(function (chain) {
Assertion.addProperty(chain, function () {
return this;
});
});
All this functionality becomes available when a specific interface gets loaded internally, for instance expect
. When this interface is loaded, a new Assertion prototype
will be instantiated whenever expect
gets executed, which will contain all functionality:
// load expect interface
var expect = require('./chai/interface/expect'); // expect interface
exports.use(expect); // load it
// expect interface
module.exports = function (chai, util) {
chai.expect = function (val, message) {
return new chai.Assertion(val, message); // return new Assertion Object with all functionality
};
};
As you can see the expect
method accepts a val
argument (and an optional message
argument). When this method is called (for instance expect(foo)
) a new Assertion prototype
will be instantiated and returned, exposing all core functionality (allowing you to do expect(foo).to.exist
).
The Assertion Constructor
uses the flag
util
to set a flag value on the Object that maps to the passed in val
argument.
function Assertion (obj, msg, stack) {
flag(this, 'ssfi', stack || arguments.callee);
flag(this, 'object', obj); // the 'object' flag maps to the passed in val
flag(this, 'message', msg);
}
All exist
then does, is get this value through the flag
util
and evaluates if it not equals to null
, using the assert
method defined on the Assertion prototype
.
Assertion.addProperty('exist', function () {
this.assert(
null != flag(this, 'object')
, 'expected #{this} to exist'
, 'expected #{this} to not exist'
);
});
When you call expect(foo)
, a new Assertion object is instantiated.
to, have, with, and similar properties do nothing but to return that Assertion instance. They are only for readability.
However, in your example, exists, is actually something that runs an assertion.
Its a property. They way properties are added to Assertion is that they are defined as getter functions as you can see here.
expect(foo).to.exist
could be broken down to this:
const assertion = new Assertion;
assertion.exists;
assertion.exists
is added to the assertion object with a getter.
That means when you execute assertion.exists, to evaluate the value of assertion.exists, a function that was earlier provided to addProperty
is executed.
You can read more about getter functions.