dirty-chai
Version:
Extends Chai with lint-friendly terminating assertions.
187 lines (160 loc) • 5.67 kB
JavaScript
export default function dirtyChai(chai, util) {
var DEFERRED = '__deferred__';
var flag = util.flag,
Assertion = chai.Assertion;
// Defer some chain operation
function defer(ctx, deferFunc) {
// See if we have any deferred asserts
var deferred = flag(ctx, DEFERRED) || [];
deferred.push(deferFunc);
flag(ctx, DEFERRED, deferred);
}
// Grab and assert on any deferred operations
function execDeferred(ctx) {
var deferreds = flag(ctx, DEFERRED) || [],
root = ctx,
deferred;
// Clear the deferred asserts
flag(ctx, DEFERRED, undefined);
while((deferred = deferreds.shift())) {
var result = deferred.call(root);
if(result !== undefined) {
root = result;
}
}
return root;
}
function applyMessageToLastDeferred(ctx, msg) {
var deferreds = flag(ctx, DEFERRED);
if(deferreds && deferreds.length > 0) {
deferreds.splice(-1, 0, function() {
flag(this, 'message', msg);
});
}
}
function convertAssertionPropertyToChainMethod(name, getter) {
if(getter) {
Assertion.addChainableMethod(name,
function newMethod(msg) {
if(msg) { applyMessageToLastDeferred(this, msg); }
// Execute any deferred asserts when the method is executed
return execDeferred(this);
},
function newProperty() {
// Flag deferred assert here
defer(this, getter);
return this;
});
}
}
/**
* Checks to see if a getter calls the `this.assert` function
*
* This is not super-reliable since we don't know the required
* preconditions for the getter. A best option would be for chai
* to differentiate between asserting properties and ones that only chain.
*/
function callsAssert(getter) {
var stub = {
assertCalled: false,
assert: function() {
this.assertCalled = true;
}
};
try {
getter.call(stub);
} catch(e) {
// This most likely happened because we don't meet the getter's preconditions
// Error on the side of conversion
stub.assertCalled = true;
}
return stub.assertCalled;
}
// Get a list of all the assertion object's properties
var properties = Object.getOwnPropertyNames(Assertion.prototype)
.map(function(name) { var descriptor = Object.getOwnPropertyDescriptor(Assertion.prototype, name); descriptor.name = name; return descriptor; });
// For all pure function assertions, exec deferreds before the original function body.
properties
.filter(function(property) { return property.name !== 'assert' && property.name !== 'constructor' && typeof property.value === 'function'; })
.forEach(function(property) {
Assertion.overwriteMethod(property.name, function(_super) {
return function() {
var result = execDeferred(this);
return _super.apply(result, arguments);
};
});
});
// For chainable methods, defer the getter, exec deferreds before the assertion function
properties
.filter(function(property) { return Assertion.prototype.__methods.hasOwnProperty(property.name); })
.forEach(function(property) {
Assertion.overwriteChainableMethod(property.name, function(_super) {
return function() {
// Method call of the chainable method
var result = execDeferred(this);
return _super.apply(result, arguments);
};
}, function(_super) {
return function() {
// Getter of chainable method
defer(this, _super);
return this;
};
});
});
var getters = properties.filter(function(property) {
return property.name !== '_obj' &&
typeof property.get === 'function' &&
!Assertion.prototype.__methods.hasOwnProperty(property.name);
});
// For all pure properties, defer the getter
getters
.filter(function(property) { return !callsAssert(property.get); })
.forEach(function(property) {
Assertion.overwriteProperty(property.name, function(_super) {
return function() {
defer(this, _super);
return this;
};
});
});
// For all assertion properties, convert it to a chainable
getters
.filter(function(property) { return callsAssert(property.get); })
.forEach(function(property) {
convertAssertionPropertyToChainMethod(property.name, property.get);
});
Assertion.addMethod('ensure', function() { return execDeferred(this); });
// Hook new property creations
chai.util.events.addEventListener("addProperty", function({ name, fn }) {
convertAssertionPropertyToChainMethod(name, fn);
})
// Hook new method assertions
chai.util.events.addEventListener("addMethod", function({ name }) {
Assertion.overwriteMethod(name, function(_super) {
return function() {
var result = execDeferred(this);
return _super.apply(result, arguments);
};
});
})
// Hook new chainable methods
chai.util.events.addEventListener("addChainableMethod", function({ name }) {
// When overwriting an existing property, don't patch it
if(!Assertion.prototype.hasOwnProperty(name)) {
Assertion.overwriteChainableMethod(name, function(_super) {
return function() {
// Method call of the chainable method
var result = execDeferred(this);
return _super.apply(result, arguments);
};
}, function(_super) {
return function() {
// Getter of chainable method
defer(this, _super);
return this;
};
});
}
})
}