Squashed matchers refactor - matchers now unit-testable apart from Expectation and Spec.

This commit is contained in:
Davis W. Frank
2013-05-28 16:54:01 -07:00
parent aca43bd3a3
commit 5700ace2c9
16 changed files with 1949 additions and 1719 deletions

32
src/core/Any.js Normal file
View File

@@ -0,0 +1,32 @@
getJasmineRequireObj().Any = function() {
function Any(expectedObject) {
this.expectedObject = expectedObject;
}
Any.prototype.jasmineMatches = function(other) {
if (this.expectedObject == String) {
return typeof other == 'string' || other instanceof String;
}
if (this.expectedObject == Number) {
return typeof other == 'number' || other instanceof Number;
}
if (this.expectedObject == Function) {
return typeof other == 'function' || other instanceof Function;
}
if (this.expectedObject == Object) {
return typeof other == 'object';
}
return other instanceof this.expectedObject;
};
Any.prototype.jasmineToString = function() {
return '<jasmine.any(' + this.expectedClass + ')>';
};
return Any;
};

View File

@@ -29,18 +29,24 @@ getJasmineRequireObj().Env = function(j$) {
this.nextSuiteId_ = 0;
this.equalityTesters_ = [];
// wrap matchers
this.matchersClass = function() {
j$.Matchers.apply(this, arguments);
var customEqualityTesters = [];
this.addCustomEqualityTester = function(tester) {
customEqualityTesters.push(tester);
};
j$.util.inherit(this.matchersClass, j$.Matchers);
j$.Matchers.wrapInto_(j$.Matchers.prototype, this.matchersClass);
j$.Expectation.addCoreMatchers(j$.matchers);
var expectationFactory = function(actual, spec) {
var expect = new (self.matchersClass)(self, actual, spec);
expect.not = new (self.matchersClass)(self, actual, spec, true);
return expect;
return j$.Expectation.Factory({
util: j$.matchersUtil,
customEqualityTesters: customEqualityTesters,
actual: actual,
addExpectationResult: addExpectationResult
});
function addExpectationResult(passed, result) {
return spec.addExpectationResult(passed, result);
}
};
var specStarted = function(spec) {
@@ -68,7 +74,7 @@ getJasmineRequireObj().Env = function(j$) {
};
};
var specConstructor = j$.Spec;
var specConstructor = j$.Spec; // TODO: inline this
var getSpecName = function(spec, currentSuite) {
return currentSuite.getFullName() + ' ' + spec.description + '.';
@@ -147,6 +153,8 @@ getJasmineRequireObj().Env = function(j$) {
function specResultCallback(result) {
self.removeAllSpies();
j$.Expectation.resetMatchers();
customEqualityTesters.length = 0;
self.clock.uninstall();
self.currentSpec = null;
self.reporter.specDone(result);
@@ -191,15 +199,8 @@ getJasmineRequireObj().Env = function(j$) {
};
}
//TODO: shim Spec addMatchers behavior into Env. Should be rewritten to remove globals, etc.
Env.prototype.addMatchers = function(matchersPrototype) {
var parent = this.matchersClass;
var newMatchersClass = function() {
parent.apply(this, arguments);
};
j$.util.inherit(newMatchersClass, parent);
j$.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
this.matchersClass = newMatchersClass;
Env.prototype.addMatchers = function(matchersToAdd) {
j$.Expectation.addMatchers(matchersToAdd);
};
Env.prototype.version = function() {
@@ -333,138 +334,5 @@ getJasmineRequireObj().Env = function(j$) {
return this.topSuite;
};
Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) {
if (a.source != b.source)
mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/");
if (a.ignoreCase != b.ignoreCase)
mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier");
if (a.global != b.global)
mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier");
if (a.multiline != b.multiline)
mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier");
if (a.sticky != b.sticky)
mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier");
return (mismatchValues.length === 0);
};
Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {
if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {
return true;
}
a.__Jasmine_been_here_before__ = b;
b.__Jasmine_been_here_before__ = a;
var hasKey = function(obj, keyName) {
return obj !== null && !j$.util.isUndefined(obj[keyName]);
};
for (var property in b) {
if (!hasKey(a, property) && hasKey(b, property)) {
mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
}
}
for (property in a) {
if (!hasKey(b, property) && hasKey(a, property)) {
mismatchKeys.push("expected missing key '" + property + "', but present in actual.");
}
}
for (property in b) {
if (property == '__Jasmine_been_here_before__') continue;
if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) {
mismatchValues.push("'" + property + "' was '" + (b[property] ? j$.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? j$.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual.");
}
}
if (j$.isArray_(a) && j$.isArray_(b) && a.length != b.length) {
mismatchValues.push("arrays were not the same length");
}
delete a.__Jasmine_been_here_before__;
delete b.__Jasmine_been_here_before__;
return (mismatchKeys.length === 0 && mismatchValues.length === 0);
};
Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {
mismatchKeys = mismatchKeys || [];
mismatchValues = mismatchValues || [];
for (var i = 0; i < this.equalityTesters_.length; i++) {
var equalityTester = this.equalityTesters_[i];
var result = equalityTester(a, b, this, mismatchKeys, mismatchValues);
if (!j$.util.isUndefined(result)) {
return result;
}
}
if (a === b) return true;
if (j$.util.isUndefined(a) || a === null || j$.util.isUndefined(b) || b === null) {
return (j$.util.isUndefined(a) && j$.util.isUndefined(b));
}
if (j$.isDomNode(a) && j$.isDomNode(b)) {
return a === b;
}
if (a instanceof Date && b instanceof Date) {
return a.getTime() == b.getTime();
}
if (a.jasmineMatches) {
return a.jasmineMatches(b);
}
if (b.jasmineMatches) {
return b.jasmineMatches(a);
}
if (a instanceof j$.Matchers.ObjectContaining) {
return a.matches(b);
}
if (b instanceof j$.Matchers.ObjectContaining) {
return b.matches(a);
}
if (j$.isString_(a) && j$.isString_(b)) {
return (a == b);
}
if (j$.isNumber_(a) && j$.isNumber_(b)) {
return (a == b);
}
if (a instanceof RegExp && b instanceof RegExp) {
return this.compareRegExps_(a, b, mismatchKeys, mismatchValues);
}
if (typeof a === "object" && typeof b === "object") {
return this.compareObjects_(a, b, mismatchKeys, mismatchValues);
}
//Straight check
return (a === b);
};
Env.prototype.contains_ = function(haystack, needle) {
if (j$.isArray_(haystack)) {
for (var i = 0; i < haystack.length; i++) {
if (this.equals_(haystack[i], needle)) return true;
}
return false;
}
return haystack.indexOf(needle) >= 0;
};
Env.prototype.addEqualityTester = function(equalityTester) {
this.equalityTesters_.push(equalityTester);
};
return Env;
};

94
src/core/Expectation.js Normal file
View File

@@ -0,0 +1,94 @@
getJasmineRequireObj().Expectation = function() {
var matchers = {};
function Expectation(options) {
this.util = options.util || { buildFailureMessage: function() {} };
this.customEqualityTesters = options.customEqualityTesters || [];
this.actual = options.actual;
this.addExpectationResult = options.addExpectationResult || function(){};
this.isNot = options.isNot;
for (var matcherName in matchers) {
this[matcherName] = matchers[matcherName];
}
}
Expectation.prototype.wrapCompare = function(name, matcherFactory) {
return function() {
var args = Array.prototype.slice.call(arguments, 0),
expected = args.slice(0),
message = "";
args.unshift(this.actual);
var result = matcherFactory(this.util, this.customEqualityTesters).compare.apply(null, args);
if (this.isNot) {
result.pass = !result.pass;
}
if (!result.pass) {
if (!result.message) {
args.unshift(this.isNot);
args.unshift(name);
message = this.util.buildFailureMessage.apply(null, args);
} else {
message = result.message;
}
}
if (expected.length == 1) {
expected = expected[0];
}
// TODO: how many of these params are needed?
this.addExpectationResult(
result.pass,
{
matcherName: name,
passed: result.pass,
message: message,
actual: this.actual,
expected: expected // TODO: this may need to be arrayified/sliced
}
);
};
};
Expectation.addCoreMatchers = function(matchers) {
var prototype = Expectation.prototype;
for (var matcherName in matchers) {
var matcher = matchers[matcherName];
prototype[matcherName] = prototype.wrapCompare(matcherName, matcher);
}
};
Expectation.addMatchers = function(matchersToAdd) {
for (var name in matchersToAdd) {
var matcher = matchersToAdd[name];
matchers[name] = Expectation.prototype.wrapCompare(name, matcher);
}
};
Expectation.resetMatchers = function() {
for (var name in matchers) {
delete matchers[name];
}
};
Expectation.Factory = function(options) {
options = options || {};
var expect = new Expectation(options);
// TODO: this would be nice as its own Object - NegativeExpectation
// TODO: copy instead of mutate options
options.isNot = true;
expect.not = new Expectation(options);
return expect;
};
return Expectation;
};

View File

@@ -1,303 +0,0 @@
getJasmineRequireObj().Matchers = function(j$) {
function Matchers(env, actual, spec, opt_isNot) {
//TODO: true dependency: equals, contains
this.env = env;
this.actual = actual;
this.spec = spec;
this.isNot = opt_isNot || false;
}
Matchers.wrapInto_ = function(prototype, matchersClass) {
for (var methodName in prototype) {
var orig = prototype[methodName];
matchersClass.prototype[methodName] = Matchers.matcherFn_(methodName, orig);
}
};
Matchers.matcherFn_ = function(matcherName, matcherFunction) {
return function() {
var matcherArgs = j$.util.argsToArray(arguments);
var result = matcherFunction.apply(this, arguments);
if (this.isNot) {
result = !result;
}
var message;
if (!result) {
if (this.message) {
message = this.message.apply(this, arguments);
if (j$.isArray_(message)) {
message = message[this.isNot ? 1 : 0];
}
} else {
var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
message = "Expected " + j$.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
if (matcherArgs.length > 0) {
for (var i = 0; i < matcherArgs.length; i++) {
if (i > 0) message += ",";
message += " " + j$.pp(matcherArgs[i]);
}
}
message += ".";
}
}
this.spec.addExpectationResult(result, {
matcherName: matcherName,
passed: result,
expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
actual: this.actual,
message: message
});
return void 0;
};
};
Matchers.prototype.toBe = function(expected) {
return this.actual === expected;
};
Matchers.prototype.toNotBe = function(expected) {
return this.actual !== expected;
};
Matchers.prototype.toEqual = function(expected) {
return this.env.equals_(this.actual, expected);
};
Matchers.prototype.toNotEqual = function(expected) {
return !this.env.equals_(this.actual, expected);
};
Matchers.prototype.toMatch = function(expected) {
return new RegExp(expected).test(this.actual);
};
Matchers.prototype.toNotMatch = function(expected) {
return !(new RegExp(expected).test(this.actual));
};
Matchers.prototype.toBeDefined = function() {
return !j$.util.isUndefined(this.actual);
};
Matchers.prototype.toBeUndefined = function() {
return j$.util.isUndefined(this.actual);
};
Matchers.prototype.toBeNull = function() {
return (this.actual === null);
};
Matchers.prototype.toBeNaN = function() {
this.message = function() {
return [ "Expected " + j$.pp(this.actual) + " to be NaN." ];
};
return (this.actual !== this.actual);
};
Matchers.prototype.toBeTruthy = function() {
return !!this.actual;
};
Matchers.prototype.toBeFalsy = function() {
return !this.actual;
};
Matchers.prototype.toHaveBeenCalled = function() {
if (arguments.length > 0) {
throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
}
if (!j$.isSpy(this.actual)) {
throw new Error('Expected a spy, but got ' + j$.pp(this.actual) + '.');
}
this.message = function() {
return [
"Expected spy " + this.actual.identity + " to have been called.",
"Expected spy " + this.actual.identity + " not to have been called."
];
};
return this.actual.wasCalled;
};
// TODO: kill this for 2.0
Matchers.prototype.wasCalled = Matchers.prototype.toHaveBeenCalled;
Matchers.prototype.wasNotCalled = function() {
if (arguments.length > 0) {
throw new Error('wasNotCalled does not take arguments');
}
if (!j$.isSpy(this.actual)) {
throw new Error('Expected a spy, but got ' + j$.pp(this.actual) + '.');
}
this.message = function() {
return [
"Expected spy " + this.actual.identity + " to not have been called.",
"Expected spy " + this.actual.identity + " to have been called."
];
};
return !this.actual.wasCalled;
};
Matchers.prototype.toHaveBeenCalledWith = function() {
var expectedArgs = j$.util.argsToArray(arguments);
if (!j$.isSpy(this.actual)) {
throw new Error('Expected a spy, but got ' + j$.pp(this.actual) + '.');
}
this.message = function() {
var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + j$.pp(expectedArgs) + " but it was.";
var positiveMessage = "";
if (this.actual.callCount === 0) {
positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + j$.pp(expectedArgs) + " but it was never called.";
} else {
positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + j$.pp(expectedArgs) + " but actual calls were " + j$.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '');
}
return [positiveMessage, invertedMessage];
};
return this.env.contains_(this.actual.argsForCall, expectedArgs);
};
// TODO: kill for 2.0
Matchers.prototype.wasCalledWith = Matchers.prototype.toHaveBeenCalledWith;
// TODO: kill for 2.0
Matchers.prototype.wasNotCalledWith = function() {
var expectedArgs = j$.util.argsToArray(arguments);
if (!j$.isSpy(this.actual)) {
throw new Error('Expected a spy, but got ' + j$.pp(this.actual) + '.');
}
this.message = function() {
return [
"Expected spy not to have been called with " + j$.pp(expectedArgs) + " but it was",
"Expected spy to have been called with " + j$.pp(expectedArgs) + " but it was"
];
};
return !this.env.contains_(this.actual.argsForCall, expectedArgs);
};
Matchers.prototype.toContain = function(expected) {
return this.env.contains_(this.actual, expected);
};
Matchers.prototype.toNotContain = function(expected) {
return !this.env.contains_(this.actual, expected);
};
Matchers.prototype.toBeLessThan = function(expected) {
return this.actual < expected;
};
Matchers.prototype.toBeGreaterThan = function(expected) {
return this.actual > expected;
};
Matchers.prototype.toBeCloseTo = function(expected, precision) {
if (precision !== 0) {
precision = precision || 2;
}
return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2);
};
Matchers.prototype.toThrow = function(expected) {
var result = false;
var exception, exceptionMessage;
if (typeof this.actual != 'function') {
throw new Error('Actual is not a function');
}
try {
this.actual();
} catch (e) {
exception = e;
}
if (exception) {
exceptionMessage = exception.message || exception;
result = (j$.util.isUndefined(expected) || this.env.equals_(exceptionMessage, expected.message || expected) || (j$.isA_("RegExp", expected) && expected.test(exceptionMessage)));
}
var not = this.isNot ? "not " : "";
var regexMatch = j$.isA_("RegExp", expected) ? " an exception matching" : "";
this.message = function() {
if (exception) {
return ["Expected function " + not + "to throw" + regexMatch, expected ? expected.message || expected : "an exception", ", but it threw", exceptionMessage].join(' ');
} else {
return "Expected function to throw an exception.";
}
};
return result;
};
Matchers.Any = function(expectedClass) {
this.expectedClass = expectedClass;
};
Matchers.Any.prototype.jasmineMatches = function(other) {
if (this.expectedClass == String) {
return typeof other == 'string' || other instanceof String;
}
if (this.expectedClass == Number) {
return typeof other == 'number' || other instanceof Number;
}
if (this.expectedClass == Function) {
return typeof other == 'function' || other instanceof Function;
}
if (this.expectedClass == Object) {
return typeof other == 'object';
}
return other instanceof this.expectedClass;
};
Matchers.Any.prototype.jasmineToString = function() {
return '<jasmine.any(' + this.expectedClass + ')>';
};
Matchers.ObjectContaining = function(sample) {
this.sample = sample;
};
Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {
mismatchKeys = mismatchKeys || [];
mismatchValues = mismatchValues || [];
var env = j$.getEnv();
var hasKey = function(obj, keyName) {
return obj !== null && !j$.util.isUndefined(obj[keyName]);
};
for (var property in this.sample) {
if (!hasKey(other, property) && hasKey(this.sample, property)) {
mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
}
else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) {
mismatchValues.push("'" + property + "' was '" + (other[property] ? j$.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? j$.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual.");
}
}
return (mismatchKeys.length === 0 && mismatchValues.length === 0);
};
Matchers.ObjectContaining.prototype.jasmineToString = function() {
return "<jasmine.objectContaining(" + j$.pp(this.sample) + ")>";
};
return Matchers;
};

View File

@@ -0,0 +1,32 @@
getJasmineRequireObj().ObjectContaining = function(j$) {
function ObjectContaining(sample) {
this.sample = sample;
}
ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {
mismatchKeys = mismatchKeys || [];
mismatchValues = mismatchValues || [];
var hasKey = function(obj, keyName) {
return obj !== null && !j$.util.isUndefined(obj[keyName]);
};
for (var property in this.sample) {
if (!hasKey(other, property) && hasKey(this.sample, property)) {
mismatchKeys.push("expected has key '" + property + "', but missing from actual.");
}
else if (!j$.matchersUtil.equals(this.sample[property], other[property], mismatchKeys, mismatchValues)) {
mismatchValues.push("'" + property + "' was '" + (other[property] ? j$.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? j$.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual.");
}
}
return (mismatchKeys.length === 0 && mismatchValues.length === 0);
};
ObjectContaining.prototype.jasmineToString = function() {
return "<jasmine.objectContaining(" + j$.pp(this.sample) + ")>";
};
return ObjectContaining;
};

268
src/core/matchers.js Normal file
View File

@@ -0,0 +1,268 @@
getJasmineRequireObj().matchers = function() {
matchers = {};
matchers.toBe = function() {
return {
compare: function(actual, expected) {
return {
pass: actual === expected
};
}
};
};
matchers.toBeCloseTo = function() {
return {
compare: function(actual, expected, precision) {
if (precision !== 0) {
precision = precision || 2;
}
return {
pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2)
};
}
};
};
matchers.toBeDefined = function() {
return {
compare: function(actual) {
return {
pass: (void 0 !== actual)
};
}
};
};
matchers.toBeFalsy = function() {
return {
compare: function(actual) {
return {
pass: !!!actual
};
}
};
};
matchers.toBeGreaterThan = function() {
return {
compare: function(actual, expected) {
return {
pass: actual > expected
};
}
};
};
matchers.toBeLessThan = function() {
return {
compare: function(actual, expected) {
return {
pass: actual < expected
};
}
};
};
matchers.toBeNaN = function() {
return {
compare: function(actual) {
var result = {
pass: (actual !== actual)
};
if (result.pass) {
result.message = "Expected actual not to be NaN.";
} else {
result.message = "Expected " + j$.pp(actual) + " to be NaN.";
}
return result;
}
};
};
matchers.toBeNull = function() {
return {
compare: function(actual) {
return {
pass: actual === null
};
}
};
};
matchers.toBeTruthy = function() {
return {
compare: function(actual) {
return {
pass: !!actual
};
}
};
};
matchers.toBeUndefined = function() {
return {
compare: function(actual) {
return {
pass: void 0 === actual
};
}
};
};
matchers.toEqual = function(util, customEqualityTesters) {
customEqualityTesters = customEqualityTesters || [];
return {
compare: function(actual, expected) {
var result = {
pass: false
};
result.pass = util.equals(actual, expected, customEqualityTesters);
return result;
}
};
};
matchers.toHaveBeenCalled = function() {
return {
compare: function(actual) {
var result = {};
if (!j$.isSpy(actual)) {
throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.');
}
if (arguments.length > 1) {
throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');
}
result.pass = actual.wasCalled;
result.message = result.pass ?
"Expected spy " + actual.identity + " not to have been called." :
"Expected spy " + actual.identity + " to have been called.";
return result;
}
};
};
matchers.toHaveBeenCalledWith = function(util) {
return {
compare: function() {
var args = Array.prototype.slice.call(arguments, 0),
actual = args[0],
expectedArgs = args.slice(1);
if (!j$.isSpy(actual)) {
throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.');
}
return {
pass: util.contains(actual.argsForCall, expectedArgs)
};
},
message: function(actual) {
return {
affirmative: "Expected spy " + actual.identity + " to have been called.",
negative: "Expected spy " + actual.identity + " not to have been called."
};
}
};
};
matchers.toMatch = function() {
return {
compare: function(actual, expected) {
var regexp = new RegExp(expected);
return {
pass: regexp.test(actual)
};
}
};
};
matchers.toThrow = function() {
return {
compare: function(actual, expected) {
var result = { pass: false },
exception;
if (typeof actual != "function") {
throw new Error("Actual is not a Function");
}
if (expectedCannotBeTreatedAsException()) {
throw new Error("Expected cannot be treated as an exception.");
}
try {
actual();
} catch (e) {
exception = new Error(e);
}
if (!exception) {
result.message = "Expected function to throw an exception.";
return result;
}
if (void 0 == expected) {
result.pass = true;
result.message = "Expected function not to throw an exception.";
} else if (exception.message == expected) {
result.pass = true;
result.message = "Expected function not to throw an exception \"" + expected + "\".";
} else if (exception.message == expected.message) {
result.pass = true;
result.message = "Expected function not to throw an exception \"" + expected.message + "\".";
} else if (expected instanceof RegExp) {
if (expected.test(exception.message)) {
result.pass = true;
result.message = "Expected function not to throw an exception matching " + expected + ".";
} else {
result.pass = false;
result.message = "Expected function to throw an exception matching " + expected + ".";
}
} else {
result.pass = false;
result.message = "Expected function to throw an exception \"" + (expected.message || expected) + "\".";
}
return result;
function expectedCannotBeTreatedAsException() {
return !(
(void 0 == expected) ||
(expected instanceof Error) ||
(typeof expected == "string") ||
(expected instanceof RegExp)
);
}
}
};
};
matchers.toContain = function(util, customEqualityTesters) {
customEqualityTesters = customEqualityTesters || [];
return {
compare: function(actual, expected) {
return {
pass: util.contains(actual, expected, customEqualityTesters)
};
}
};
};
return matchers;
};

177
src/core/matchersUtil.js Normal file
View File

@@ -0,0 +1,177 @@
getJasmineRequireObj().matchersUtil = function(j$) {
// TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?
return {
equals: function(a, b, customTesters) {
customTesters = customTesters || [];
return eq(a, b, [], [], customTesters);
},
contains: function(haystack, needle, customTesters) {
customTesters = customTesters || [];
if (Object.prototype.toString.apply(haystack) === "[object Array]") {
for (var i = 0; i < haystack.length; i++) {
if (eq(haystack[i], needle, [], [], customTesters)) {
return true;
}
}
return false;
}
return haystack.indexOf(needle) >= 0;
},
buildFailureMessage: function() {
var args = Array.prototype.slice.call(arguments, 0),
matcherName = args[0],
isNot = args[1],
actual = args[2],
expected = args.slice(3),
englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
var message = "Expected " +
j$.pp(actual) +
(isNot ? " not " : " ") +
englishyPredicate;
if (expected.length > 0) {
for (var i = 0; i < expected.length; i++) {
if (i > 0) message += ",";
message += " " + j$.pp(expected[i]);
}
}
return message + ".";
}
};
// Equality function lovingly adapted from isEqual in
// [Underscore](http://underscorejs.org)
function eq(a, b, aStack, bStack, customTesters) {
var result = true;
for (var i = 0; i < customTesters.length; i++) {
result = customTesters[i](a, b);
if (result) {
return true;
}
}
if (a instanceof j$.Any) {
result = a.jasmineMatches(b);
if (result) {
return true;
}
}
if (b instanceof j$.Any) {
result = b.jasmineMatches(a);
if (result) {
return true;
}
}
if (b instanceof j$.ObjectContaining) {
result = b.jasmineMatches(a);
if (result) {
return true;
}
}
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a == 1 / b;
// A strict comparison is necessary because `null == undefined`.
if (a === null || b === null) return a === b;
var className = Object.prototype.toString.call(a);
if (className != Object.prototype.toString.call(b)) return false;
switch (className) {
// Strings, numbers, dates, and booleans are compared by value.
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return a == String(b);
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a == +b;
// RegExps are compared by their source patterns and flags.
case '[object RegExp]':
return a.source == b.source &&
a.global == b.global &&
a.multiline == b.multiline &&
a.ignoreCase == b.ignoreCase;
}
if (typeof a != 'object' || typeof b != 'object') return false;
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
var length = aStack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (aStack[length] == a) return bStack[length] == b;
}
// Add the first object to the stack of traversed objects.
aStack.push(a);
bStack.push(b);
var size = 0;
// Recursively compare objects and arrays.
if (className == '[object Array]') {
// Compare array lengths to determine if a deep comparison is necessary.
size = a.length;
result = size == b.length;
if (result) {
// Deep compare the contents, ignoring non-numeric properties.
while (size--) {
if (!(result = eq(a[size], b[size], aStack, bStack, customTesters))) break;
}
}
} else {
// Objects with different constructors are not equivalent, but `Object`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) &&
isFunction(bCtor) && (bCtor instanceof bCtor))) {
return false;
}
// Deep compare objects.
for (var key in a) {
if (has(a, key)) {
// Count the expected number of properties.
size++;
// Deep compare each member.
if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters))) break;
}
}
// Ensure that both objects contain the same number of properties.
if (result) {
for (key in b) {
if (has(b, key) && !(size--)) break;
}
result = !size;
}
}
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
return result;
function has(obj, key) {
return obj.hasOwnProperty(key);
}
function isFunction(obj) {
return typeof obj === 'function';
}
}
};

View File

@@ -12,13 +12,17 @@ getJasmineRequireObj().core = function(jRequire) {
jRequire.base(j$);
j$.util = jRequire.util();
j$.Any = jRequire.Any();
j$.Clock = jRequire.Clock();
j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler();
j$.Env = jRequire.Env(j$);
j$.ExceptionFormatter = jRequire.ExceptionFormatter();
j$.Expectation = jRequire.Expectation();
j$.buildExpectationResult = jRequire.buildExpectationResult();
j$.JsApiReporter = jRequire.JsApiReporter();
j$.Matchers = jRequire.Matchers(j$);
j$.matchers = jRequire.matchers(j$);
j$.matchersUtil = jRequire.matchersUtil(j$);
j$.ObjectContaining = jRequire.ObjectContaining(j$);
j$.StringPrettyPrinter = jRequire.StringPrettyPrinter(j$);
j$.QueueRunner = jRequire.QueueRunner();
j$.ReportDispatcher = jRequire.ReportDispatcher();