diff --git a/GOALS_2.0.md b/GOALS_2.0.md index 68eaa5d7..2033bc10 100644 --- a/GOALS_2.0.md +++ b/GOALS_2.0.md @@ -18,37 +18,42 @@ * New objects can have constructors on `jasmine` * Top level functions can live on `jasmine` * Top level (i.e., any `jasmine` property) should only be referenced inside the `Env` constructor - * Spies - * isA functions: - * isArray_ - used in matchers and spies - * isString_ - * isDOMNode_ - * isA_ + * should better allow any object to get jasmine code (Node-friendly) +* review everything in base.js +* Spies + * break these out into their own tests/file +* Remove isA functions: + * isArray_ - used in matchers and spies + * isString_ + * isDOMNode_ + * isA_ * unimplementedMethod_, used by PrettyPrinter - * jasmine.util should be util closure inside of env or something - * argsToArray is used for Spies and matching - * inherit is for how matchers are added/mixed in, reporters, and pretty printers - * formatException is used only inside Env/spec - * htmlEscape is for messages in matchers - should this be HTML at all? Is that * Matchers improvements - * move AddMatchers to Env & global (away from spec) - * make matchers unit-testable - * write doc on how to make a matcher +* jasmine.util should be util closure inside of env or something + * argsToArray is used for Spies and matching (and can be replaced) + * inherit is only for PrettyPrinter now + * formatException is used only inside Env/spec + * htmlEscape is for messages in matchers - should this be HTML at all? +* Matchers improvements + * unit testable DONE + * better equality (from Underscore) DONE + * refactor equals function so that it just loops & recurses over a list of fns (custom and built-in) - 2.1? + * addCustomMatchers doesn't explode stack +* Pretty printing + * move away from pretty printer and to a JSON.stringify implementation? + * jasmineToString vs. custom toString ? ### Easy +* unify params to ctors: options vs. attrs. +* This will be a lot of the TODOs, but clean up & simplify Env.js (is this a 2.1 task?) + ## Other Topics -* Build - can we, should we redo the build and release process AGAIN in order to make it less arcane - * Want to add JSHint to build - * Use a standard JS/Node based concat system instead of custom Ruby? * Docs - * JsDoc is a pain to host and RubyMine is pretty good at navigating. I say we kill it officially * Docco has gone over well. Should we annotate all the sources and then have Pages be more complex, having tutorials and annotated source like Backbone? Are we small enough? - -# Build - -* lib -> generated -* + * Need examples for: + * How to build a Custom Matcher + * How to add a custom equality tester diff --git a/spec/core/AnySpec.js b/spec/core/AnySpec.js new file mode 100644 index 00000000..97b1c247 --- /dev/null +++ b/spec/core/AnySpec.js @@ -0,0 +1,39 @@ +describe("Any", function() { + it("matches a string", function() { + var any = new j$.Any(String); + + expect(any.jasmineMatches("foo")).toBe(true); + }); + + it("matches a number", function() { + var any = new j$.Any(Number); + + expect(any.jasmineMatches(1)).toBe(true); + }); + + it("matches a function", function() { + var any = new j$.Any(Function); + + expect(any.jasmineMatches(function(){})).toBe(true); + }); + + it("matches an Object", function() { + var any = new j$.Any(Object); + + expect(any.jasmineMatches({})).toBe(true); + }); + + it("matches another constructed object", function() { + var Thing = function() {}, + any = new j$.Any(Thing); + + expect(any.jasmineMatches(new Thing())).toBe(true); + }); + + it("jasmineToString's itself", function() { + var any = new j$.Any(Number); + + expect(any.jasmineToString()).toMatch('>", function() { - var TestClass; - beforeEach(function() { - TestClass = { - normalFunction: function() { - }, - spyFunction: jasmine.createSpy("My spy") - }; - }); - - function shouldThrowAnExceptionWhenInvokedOnANonSpy(methodName) { - return function() { - expect( - function() { - match(TestClass.normalFunction)[methodName](); - }).toThrow('Expected a spy, but got Function.'); - - expect( - function() { - match(jasmine.undefined)[methodName](); - }).toThrow('Expected a spy, but got undefined.'); - - expect( - function() { - match({some:'object'})[methodName](); - }).toThrow('Expected a spy, but got { some : \'object\' }.'); - - expect( - function() { - match("")[methodName](); - }).toThrow('Expected a spy, but got \'\'.'); - }; - } - - describe("toHaveBeenCalled", function() { - it("should pass if the spy was called", function() { - expect(match(TestClass.spyFunction).toHaveBeenCalled()).toFail(); - - TestClass.spyFunction(); - expect(match(TestClass.spyFunction).toHaveBeenCalled()).toPass(); - }); - - it("should throw an exception when invoked with any arguments", function() { - expect( - function() { - match(TestClass.normalFunction).toHaveBeenCalled("unwanted argument"); - }).toThrow('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); - }); - - it('should throw an exception when invoked on a non-spy', shouldThrowAnExceptionWhenInvokedOnANonSpy('toHaveBeenCalled')); - }); - - describe("wasCalled", function() { - it("should alias toHaveBeenCalled", function() { - spyOn(TestClass, 'normalFunction'); - - TestClass.normalFunction(); - - expect(TestClass.normalFunction).wasCalled(); - }); - }); - - - describe("wasNotCalled", function() { - it("should pass iff the spy was not called", function() { - expect(match(TestClass.spyFunction).wasNotCalled()).toPass(); - - TestClass.spyFunction(); - expect(match(TestClass.spyFunction).wasNotCalled()).toFail(); - }); - - it("should throw an exception when invoked with any arguments", function() { - expect( - function() { - match(TestClass.normalFunction).wasNotCalled("unwanted argument"); - }).toThrow('wasNotCalled does not take arguments'); - }); - - it('should throw an exception when invoked on a non-spy', shouldThrowAnExceptionWhenInvokedOnANonSpy('wasNotCalled')); - }); - - describe("toHaveBeenCalledWith", function() { - it('toHaveBeenCalledWith should return true if it was called with the expected args', function() { - TestClass.spyFunction('a', 'b', 'c'); - expect(match(TestClass.spyFunction).toHaveBeenCalledWith('a', 'b', 'c')).toPass(); - }); - - it('should return false if it was not called with the expected args', function() { - TestClass.spyFunction('a', 'b', 'c'); - var expected = match(TestClass.spyFunction); - expect(expected.toHaveBeenCalledWith('c', 'b', 'a')).toFail(); - var result = lastResult(); - expect(result.passed).toBe(false); - expect(result.expected).toEqual(['c', 'b', 'a']); - expect(result.actual.mostRecentCall.args).toEqual(['a', 'b', 'c']); - expect(result.message).toContain(jasmine.pp(result.expected)); - expect(result.message).toContain(jasmine.pp(result.actual.mostRecentCall.args)); - }); - - it('should return false if it was not called', function() { - var expected = match(TestClass.spyFunction); - expect(expected.toHaveBeenCalledWith('c', 'b', 'a')).toFail(); - var result = lastResult(); - expect(result.passed).toBe(false); - expect(result.expected).toEqual(['c', 'b', 'a']); - expect(result.actual.argsForCall).toEqual([]); - expect(result.message).toContain(jasmine.pp(result.expected)); - }); - - it('should allow matches across multiple calls', function() { - TestClass.spyFunction('a', 'b', 'c'); - TestClass.spyFunction('d', 'e', 'f'); - var expected = match(TestClass.spyFunction); - expect(expected.toHaveBeenCalledWith('a', 'b', 'c')).toPass(); - expect(expected.toHaveBeenCalledWith('d', 'e', 'f')).toPass(); - expect(expected.toHaveBeenCalledWith('x', 'y', 'z')).toFail(); - }); - - it("should return a decent message", function() { - TestClass.spyFunction('a', 'b', 'c'); - TestClass.spyFunction('d', 'e', 'f'); - var expected = match(TestClass.spyFunction); - expect(expected.toHaveBeenCalledWith('a', 'b')).toFail(); - expect(lastResult().message).toEqual("Expected spy My spy to have been called with [ 'a', 'b' ] but actual calls were [ 'a', 'b', 'c' ], [ 'd', 'e', 'f' ]"); - }); - - it("should return a decent message when it hasn't been called", function() { - var expected = match(TestClass.spyFunction); - expect(expected.toHaveBeenCalledWith('a', 'b')).toFail(); - expect(lastResult().message).toEqual("Expected spy My spy to have been called with [ 'a', 'b' ] but it was never called."); - }); - - it("should return a decent message when inverted", function() { - TestClass.spyFunction('a', 'b', 'c'); - TestClass.spyFunction('d', 'e', 'f'); - var expected = match(TestClass.spyFunction); - expect(expected.not.toHaveBeenCalledWith('a', 'b', 'c')).toFail(); - expect(lastResult().message).toEqual("Expected spy My spy not to have been called with [ 'a', 'b', 'c' ] but it was."); - }); - - it('should throw an exception when invoked on a non-spy', shouldThrowAnExceptionWhenInvokedOnANonSpy('toHaveBeenCalledWith')); - - describe("to build an ExpectationResult", function () { - beforeEach(function() { - var currentSuite; - var spec; - currentSuite = env.describe('default current suite', function() { - spec = env.it(); - }, spec); - TestClass = { someFunction: function(a, b) { - } }; - env.spyOn(TestClass, 'someFunction'); - }); - - it("should should handle the case of a spy", function() { - TestClass.someFunction('a', 'c'); - var matcher = match(TestClass.someFunction); - matcher.toHaveBeenCalledWith('a', 'b'); - - var result = lastResult(); - expect(result.matcherName).toEqual("toHaveBeenCalledWith"); - expect(result.passed).toBe(false); - expect(result.message).toContain(jasmine.pp(['a', 'b'])); - expect(result.message).toContain(jasmine.pp(['a', 'c'])); - expect(result.actual).toEqual(TestClass.someFunction); - expect(result.expected).toEqual(['a','b']); - }); - }); - }); - - describe("wasCalledWith", function() { - it("should alias toHaveBeenCalledWith", function() { - spyOn(TestClass, 'normalFunction'); - - TestClass.normalFunction(123); - - expect(TestClass.normalFunction).wasCalledWith(123); - }); - }); - - describe("wasNotCalledWith", function() { - it('should return true if the spy was NOT called with the expected args', function() { - TestClass.spyFunction('a', 'b', 'c'); - expect(match(TestClass.spyFunction).wasNotCalledWith('c', 'b', 'a')).toPass(); - }); - - it('should return false if it WAS called with the expected args', function() { - TestClass.spyFunction('a', 'b', 'c'); - var expected = match(TestClass.spyFunction); - expect(expected.wasNotCalledWith('a', 'b', 'c')).toFail(); - var result = lastResult(); - expect(result.passed).toBe(false); - expect(result.expected).toEqual(['a', 'b', 'c']); - expect(result.actual.mostRecentCall.args).toEqual(['a', 'b', 'c']); - expect(result.message).toContain(jasmine.pp(result.expected)); - }); - - it('should return true if it was not called', function() { - var expected = match(TestClass.spyFunction); - expect(expected.wasNotCalledWith('c', 'b', 'a')).toPass(); - }); - - it('should allow matches across multiple calls', function() { - var expected = match(TestClass.spyFunction); - TestClass.spyFunction('a', 'b', 'c'); - TestClass.spyFunction('d', 'e', 'f'); - expect(expected.wasNotCalledWith('a', 'b', 'c')).toFail(); - expect(expected.wasNotCalledWith('d', 'e', 'f')).toFail(); - expect(expected.wasNotCalledWith('x', 'y', 'z')).toPass(); - }); - - it('should throw an exception when invoked on a non-spy', shouldThrowAnExceptionWhenInvokedOnANonSpy('wasNotCalledWith')); - }); - }); - - describe("ObjectContaining", function () { - describe("with an empty object", function () { - var containing; - beforeEach(function () { - containing = new j$.Matchers.ObjectContaining({}); - }); - it("matches everything", function () { - expect(containing.jasmineMatches("foo", [], [])).toBe(true); - }); - - it("says it didn't expect to contain anything", function () { - expect(containing.jasmineToString()).toEqual(""); - }); - }); - - describe("with an object with items in it", function () { - var containing, mismatchKeys, mismatchValues; - beforeEach(function () { - mismatchKeys = []; - mismatchValues = []; - containing = new j$.Matchers.ObjectContaining({foo: "fooVal", bar: "barVal"}); - }); - - it("doesn't match an empty object", function () { - expect(containing.jasmineMatches({}, mismatchKeys, mismatchValues)).toBe(false); - }); - - it("doesn't match an object with none of the specified options", function () { - expect(containing.jasmineMatches({baz:"stuff"}, mismatchKeys, mismatchValues)).toBe(false); - }); - - it("adds a message for each missing key", function () { - containing.jasmineMatches({foo: "fooVal"}, mismatchKeys, mismatchValues); - expect(mismatchKeys.length).toEqual(1); - }); - - it("doesn't match an object when the values are different", function () { - expect(containing.jasmineMatches({foo:"notFoo", bar:"notBar"}, mismatchKeys, mismatchValues)).toBe(false); - }); - - it("adds a message when values don't match", function () { - containing.jasmineMatches({foo: "fooVal", bar: "notBar"}, mismatchKeys, mismatchValues); - expect(mismatchValues.length).toEqual(1); - }); - - it("doesn't match an object with only one of the values matching", function () { - expect(containing.jasmineMatches({foo:"notFoo", bar:"barVal"}, mismatchKeys, mismatchValues)).toBe(false); - }); - - it("matches when all the values are the same", function () { - expect(containing.jasmineMatches({foo: "fooVal", bar: "barVal"}, mismatchKeys, mismatchValues)).toBe(true); - }); - - it("matches when there are additional values", function () { - expect(containing.jasmineMatches({foo: "fooVal", bar: "barVal", baz: "bazVal"}, mismatchKeys, mismatchValues)).toBe(true); - }); - - it("doesn't modify missingKeys or missingValues when match is successful", function () { - containing.jasmineMatches({foo: "fooVal", bar: "barVal"}, mismatchKeys, mismatchValues); - expect(mismatchKeys.length).toEqual(0); - expect(mismatchValues.length).toEqual(0); - }); - - it("says what it expects to contain", function () { - expect(containing.jasmineToString()).toEqual(""); - }); - }); - - describe("in real life", function () { - var method; - beforeEach(function () { - method = jasmine.createSpy("method"); - method({a:"b", c:"d"}); - }); - it("works correctly for positive matches", function () { - expect(method).toHaveBeenCalledWith(jasmine.objectContaining({a:"b"})); - }); - - it("works correctly for negative matches", function () { - expect(method).not.toHaveBeenCalledWith(jasmine.objectContaining({z:"x"})); - }); - }); - }); - - describe("Matchers.Any", function () { - var any; - describe(".jasmineToString", function () { - describe("with Object", function () { - it("says it's looking for an object", function () { - any = jasmine.any(Object); - expect(any.jasmineToString().replace(/\n/g, "")).toMatch(//); - }); - }); - - describe("with Function", function () { - it("says it's looking for a function", function () { - any = jasmine.any(Function); - expect(any.jasmineToString().replace(/\n/g, "")).toMatch(//); - }); - }); - - describe("with String", function () { - it("says it's looking for a string", function () { - any = jasmine.any(String); - expect(any.jasmineToString().replace(/\n/g, "")).toMatch(//); - }); - }); - - describe("with Number", function () { - it("says it's looking for a number", function () { - any = jasmine.any(Number); - expect(any.jasmineToString().replace(/\n/g, "")).toMatch(//); - }); - }); - - describe("with some other defined 'class'", function () { - it("says it's looking for an object", function () { - function MyClass () {} - any = jasmine.any(MyClass); - expect(any.jasmineToString().replace("\n", "")).toMatch(//); - }); - }); - }); - - describe(".jasmineMatches", function () { - describe("with Object", function () { - beforeEach(function () { - any = jasmine.any(Object); - }); - - it("matches an empty object", function () { - expect(any.jasmineMatches({})).toEqual(true); - }); - - it("matches a newed up object", function () { - expect(any.jasmineMatches(new Object())).toEqual(true); - }); - - it("doesn't match a string", function () { - expect(any.jasmineMatches("")).toEqual(false); - }); - - it("doesn't match a number", function () { - expect(any.jasmineMatches(123)).toEqual(false); - }); - - it("doesn't match a function", function () { - expect(any.jasmineMatches(function () {})).toEqual(false); - }); - }); - - describe("with Function", function () { - beforeEach(function () { - any = jasmine.any(Function); - }); - - it("doesn't match an object", function () { - expect(any.jasmineMatches({})).toEqual(false); - }); - - it("doesn't match a string", function () { - expect(any.jasmineMatches("")).toEqual(false); - }); - - it("doesn't match a number", function () { - expect(any.jasmineMatches(123)).toEqual(false); - }); - - it("matches a function", function () { - expect(any.jasmineMatches(function () {})).toEqual(true); - }); - }); - - describe("with Number", function () { - beforeEach(function () { - any = jasmine.any(Number); - }); - - it("doesn't match an object", function () { - expect(any.jasmineMatches({})).toEqual(false); - }); - - it("doesn't match a string", function () { - expect(any.jasmineMatches("")).toEqual(false); - }); - - it("matches a number", function () { - expect(any.jasmineMatches(123)).toEqual(true); - }); - - it("doesn't match a function", function () { - expect(any.jasmineMatches(function () {})).toEqual(false); - }); - }); - - describe("with String", function () { - beforeEach(function () { - any = jasmine.any(String); - }); - - it("doesn't match an object", function () { - expect(any.jasmineMatches({})).toEqual(false); - }); - - it("matches a string", function () { - expect(any.jasmineMatches("")).toEqual(true); - }); - - it("doesn't match a number", function () { - expect(any.jasmineMatches(123)).toEqual(false); - }); - - it("doesn't match a function", function () { - expect(any.jasmineMatches(function () {})).toEqual(false); - }); - }); - - describe("with some defined 'class'", function () { - function MyClass () {} - beforeEach(function () { - any = jasmine.any(MyClass); - }); - - it("doesn't match an object", function () { - expect(any.jasmineMatches({})).toEqual(false); - }); - - it("doesn't match a string", function () { - expect(any.jasmineMatches("")).toEqual(false); - }); - - it("doesn't match a number", function () { - expect(any.jasmineMatches(123)).toEqual(false); - }); - - it("doesn't match a function", function () { - expect(any.jasmineMatches(function () {})).toEqual(false); - }); - - it("matches an instance of the defined class", function () { - expect(any.jasmineMatches(new MyClass())).toEqual(true); - }); - }); - }); - }); - - describe("all matchers", function() { - it("should return null, for future-proofing, since we might eventually allow matcher chaining", function() { - expect(match(true).toBe(true)).toBeUndefined(); - }); - }); -}); diff --git a/spec/core/ObjectContainingSpec.js b/spec/core/ObjectContainingSpec.js new file mode 100644 index 00000000..88e6f3e5 --- /dev/null +++ b/spec/core/ObjectContainingSpec.js @@ -0,0 +1,38 @@ +describe("ObjectContaining", function() { + + it("matches any actual to an empty object", function() { + var containing = new j$.ObjectContaining({}); + + expect(containing.jasmineMatches("foo")).toBe(true); + }); + + it("does not match an empty object actual", function() { + var containing = new j$.ObjectContaining("foo"); + + expect(containing.jasmineMatches({})).toBe(false); + }); + + it("matches when the key/value pair is present in the actual", function() { + var containing = new jasmine.Matchers.ObjectContaining({foo: "fooVal"}); + + expect(containing.jasmineMatches({foo: "fooVal", bar: "barVal"})).toBe(true); + }); + + it("does not match when the key/value pair is not present in the actual", function() { + var containing = new jasmine.Matchers.ObjectContaining({foo: "fooVal"}); + + expect(containing.jasmineMatches({bar: "barVal", quux: "quuxVal"})).toBe(false); + }); + + it("does not match when the key is present but the value is different in the actual", function() { + var containing = new jasmine.Matchers.ObjectContaining({foo: "other"}); + + expect(containing.jasmineMatches({foo: "fooVal", bar: "barVal"})).toBe(false); + }); + + it("jasmineToString's itself", function() { + var containing = new j$.ObjectContaining({}); + + expect(containing.jasmineToString()).toMatch(" expected", function() { + var matcher = j$.matchers.toBeGreaterThan(), + result; + + result = matcher.compare(2, 1); + expect(result.pass).toBe(true); + }); + + it("fails when actual <= expected", function() { + var matcher = j$.matchers.toBeGreaterThan(); + + result = matcher.compare(1, 1); + expect(result.pass).toBe(false); + + result = matcher.compare(1, 2); + expect(result.pass).toBe(false); + }); + }); + + describe("toBeLessThan", function() { + it("passes when actual < expected", function() { + var matcher = j$.matchers.toBeLessThan(), + result; + + result = matcher.compare(1, 2); + expect(result.pass).toBe(true); + }); + + it("fails when actual <= expected", function() { + var matcher = j$.matchers.toBeLessThan(), + result; + + result = matcher.compare(1, 1); + expect(result.pass).toBe(false); + + result = matcher.compare(2, 1); + expect(result.pass).toBe(false); + }); + }); + + describe("toBeNaN", function() { + it("passes for NaN with a custom .not fail", function() { + var matcher = j$.matchers.toBeNaN(), + result; + + result = matcher.compare(Number.NaN); + expect(result.pass).toBe(true); + expect(result.message).toEqual("Expected actual not to be NaN."); + }); + + it("fails for anything not a NaN", function() { + var matcher = j$.matchers.toBeNaN(); + + result = matcher.compare(1); + expect(result.pass).toBe(false); + + result = matcher.compare(null); + expect(result.pass).toBe(false); + + result = matcher.compare(void 0); + expect(result.pass).toBe(false); + + result = matcher.compare(''); + expect(result.pass).toBe(false); + + result = matcher.compare(Number.POSITIVE_INFINITY); + expect(result.pass).toBe(false); + }); + + it("has a custom message on failure", function() { + var matcher = j$.matchers.toBeNaN(), + result = matcher.compare(0); + + expect(result.message).toEqual("Expected 0 to be NaN."); + }); + }); + + describe("toBeNull", function() { + it("passes for null", function() { + var matcher = j$.matchers.toBeNull(), + result; + + result = matcher.compare(null); + expect(result.pass).toBe(true); + }); + + it("fails for non-null", function() { + var matcher = j$.matchers.toBeNull(), + result; + + result = matcher.compare('foo'); + expect(result.pass).toBe(false); + }); + }); + + describe("toBeTruthy", function() { + it("passes for 'truthy' values", function() { + var matcher = j$.matchers.toBeTruthy(), + result; + + result = matcher.compare(true); + expect(result.pass).toBe(true); + + result = matcher.compare(1); + expect(result.pass).toBe(true); + + result = matcher.compare("foo"); + expect(result.pass).toBe(true); + + result = matcher.compare({}); + expect(result.pass).toBe(true); + }); + + it("fails for 'falsy' values", function() { + var matcher = j$.matchers.toBeTruthy(), + result; + + result = matcher.compare(false); + expect(result.pass).toBe(false); + + result = matcher.compare(0); + expect(result.pass).toBe(false); + + result = matcher.compare(''); + expect(result.pass).toBe(false); + + result = matcher.compare(null); + expect(result.pass).toBe(false); + + result = matcher.compare(void 0); + expect(result.pass).toBe(false); + }); + }); + + describe("toBeUndefined", function() { + it("passes for undefined values", function() { + var matcher = j$.matchers.toBeUndefined(), + result; + + result = matcher.compare(void 0); + expect(result.pass).toBe(true); + + }); + + it("fails when matching defined values", function() { + var matcher = j$.matchers.toBeUndefined(); + + result = matcher.compare('foo'); + expect(result.pass).toBe(false); + }) + }); + + describe("toContain", function() { + it("delegates to j$.matchersUtil.contains", function() { + var util = { + contains: j$.createSpy('delegated-contains').andReturn(true) + }, + matcher = j$.matchers.toContain(util); + + result = matcher.compare("ABC", "B"); + expect(util.contains).toHaveBeenCalledWith("ABC", "B", []); + expect(result.pass).toBe(true); + }); + + it("delegates to j$.matchersUtil.contains, passing in equality testers if present", function() { + var util = { + contains: j$.createSpy('delegated-contains').andReturn(true) + }, + customEqualityTesters = ['a', 'b'], + matcher = j$.matchers.toContain(util, customEqualityTesters); + + result = matcher.compare("ABC", "B"); + expect(util.contains).toHaveBeenCalledWith("ABC", "B", ['a', 'b']); + expect(result.pass).toBe(true); + }); + }); + + describe("toEqual", function() { + it("delegates to equals function", function() { + var util = { + equals: j$.createSpy('delegated-equals').andReturn(true) + }, + matcher = j$.matchers.toEqual(util), + result; + + result = matcher.compare(1, 1); + + expect(util.equals).toHaveBeenCalledWith(1, 1, []); + expect(result.pass).toBe(true); + }); + + it("delegates custom equality testers, if present", function() { + var util = { + equals: j$.createSpy('delegated-equals').andReturn(true) + }, + customEqualityTesters = ['a', 'b'], + matcher = j$.matchers.toEqual(util, customEqualityTesters), + result; + + result = matcher.compare(1, 1); + + expect(util.equals).toHaveBeenCalledWith(1, 1, ['a', 'b']); + expect(result.pass).toBe(true); + }); + }); + + describe("toHaveBeenCalled", function() { + it("passes when the actual was called, with a custom .not fail message", function() { + var matcher = j$.matchers.toHaveBeenCalled(), + calledSpy = j$.createSpy('called-spy'), + result; + + calledSpy(); + + result = matcher.compare(calledSpy); + expect(result.pass).toBe(true); + expect(result.message).toEqual("Expected spy called-spy not to have been called."); + }); + + it("fails when the actual was not called", function() { + var matcher = j$.matchers.toHaveBeenCalled(), + uncalledSpy = j$.createSpy('uncalled spy'); + + result = matcher.compare(uncalledSpy); + expect(result.pass).toBe(false); + }); + + it("throws an exception when the actual is not a spy", function() { + var matcher = j$.matchers.toHaveBeenCalled(), + fn = function() {}; + + expect(function() { matcher.compare(fn) }).toThrow(new Error("Expected a spy, but got Function.")); + }); + + it("throws an exception when invoked with any arguments", function() { + var matcher = j$.matchers.toHaveBeenCalled(), + spy = j$.createSpy('sample spy'); + + expect(function() { matcher.compare(spy, 'foo') }).toThrow(new Error("toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith")); + }); + + it("has a custom message on failure", function() { + var matcher = j$.matchers.toHaveBeenCalled(), + spy = j$.createSpy('sample-spy'), + result; + + result = matcher.compare(spy); + + expect(result.message).toEqual("Expected spy sample-spy to have been called."); + }); + }); + + describe("toHaveBeenCalledWith", function() { + it("passes when the actual was called with matching parameters", function() { + var util = { + contains: j$.createSpy('delegated-contains').andReturn(true) + }, + matcher = j$.matchers.toHaveBeenCalledWith(util) + calledSpy = j$.createSpy('called-spy'), + result; + + calledSpy('a', 'b'); + result = matcher.compare(calledSpy, 'a', 'b'); + + expect(result.pass).toBe(true); + }); + + it("fails when the actual was not called", function() { + var util = { + contains: j$.createSpy('delegated-contains').andReturn(false) + }, + matcher = j$.matchers.toHaveBeenCalledWith(util), + uncalledSpy = j$.createSpy('uncalled spy'), + result; + + result = matcher.compare(uncalledSpy); + expect(result.pass).toBe(false); + }); + + it("fails when the actual was called with different parameters", function() { + var util = { + contains: j$.createSpy('delegated-contains').andReturn(false) + }, + matcher = j$.matchers.toHaveBeenCalledWith(util), + calledSpy = j$.createSpy('called spy'), + result; + + calledSpy('a'); + result = matcher.compare(calledSpy, 'a', 'b'); + + expect(result.pass).toBe(false); + }); + + it("throws an exception when the actual is not a spy", function() { + var matcher = j$.matchers.toHaveBeenCalledWith(), + fn = function() {}; + + expect(function() { matcher.compare(fn) }).toThrow(new Error("Expected a spy, but got Function.")); + }); + + it("has a custom message on failure", function() { + var matcher = j$.matchers.toHaveBeenCalledWith(), + spy = j$.createSpy('sample-spy'), + messages = matcher.message(spy); + + expect(messages.affirmative).toEqual("Expected spy sample-spy to have been called.") + expect(messages.negative).toEqual("Expected spy sample-spy not to have been called.") + }); + }); + + describe("toMatch", function() { + it("passes when RegExps are equivalent", function() { + var matcher = j$.matchers.toMatch(), + result; + + result = matcher.compare(/foo/, /foo/); + expect(result.pass).toBe(true); + }); + + it("fails when RegExps are not equivalent", function() { + var matcher = j$.matchers.toMatch(), + result; + + result = matcher.compare(/bar/, /foo/); + expect(result.pass).toBe(false); + }); + + it("passes when the actual matches the expected string as a pattern", function() { + var matcher = j$.matchers.toMatch(), + result; + + result = matcher.compare('foosball', 'foo'); + expect(result.pass).toBe(true); + }); + + it("fails when the actual matches the expected string as a pattern", function() { + var matcher = j$.matchers.toMatch(), + result; + + result = matcher.compare('bar', 'foo'); + expect(result.pass).toBe(false); + }); + }); + + describe("toThrow", function() { + it("throw an error when the acutal is not a function ", function() { + var matcher = j$.matchers.toThrow(); + + expect(function() { + matcher.compare({}); + }).toThrow(new Error("Actual is not a Function")); + }); + + it("throws an error when the expected can't be turned into an exception", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "foo"; + }, + result; + + expect(function() { + matcher.compare(fn, 1); + }).toThrow(new Error("Expected cannot be treated as an exception.")); + }); + + it("passes if the actual throws any exception", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "foo"; + }, + result; + + result = matcher.compare(fn); + + expect(result.pass).toBe(true); + expect(result.message).toEqual("Expected function not to throw an exception."); + }); + + it("fails if the actual does not throw an exception", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + return 0; + }, + result; + + result = matcher.compare(fn); + + expect(result.pass).toBe(false); + expect(result.message).toEqual("Expected function to throw an exception."); + }); + + it("passes if the actual throws an exception with the expected message", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "foo"; + }, + result; + + result = matcher.compare(fn, "foo"); + + expect(result.pass).toBe(true); + expect(result.message).toEqual("Expected function not to throw an exception \"foo\"."); + }); + + it("fails if the actual throws an exception with a different message", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "foo"; + }, + result; + + result = matcher.compare(fn, "bar"); + + expect(result.pass).toBe(false); + expect(result.message).toEqual("Expected function to throw an exception \"bar\"."); + }); + + it("passes if the actual throws an exception and matches the message of the expected exception", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "foo"; + }, + result; + + result = matcher.compare(fn, new Error("foo")); + + expect(result.pass).toBe(true); + expect(result.message).toEqual("Expected function not to throw an exception \"foo\"."); + }); + + it("fails if the actual throws an exception and it does not match the message of the expected exception with a custom message", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "foo"; + }, + result; + + result = matcher.compare(fn, new Error("bar")); + + expect(result.pass).toBe(false); + expect(result.message).toEqual("Expected function to throw an exception \"bar\"."); + }); + + it("passes if the actual throws an exception and the message matches the expected regular expression", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "a long message"; + }, + result; + + result = matcher.compare(fn, /long/); + + expect(result.pass).toBe(true); + expect(result.message).toEqual("Expected function not to throw an exception matching /long/."); + }); + + it("fails if the actual throws an exception and the message does not match the expected regular expression", function() { + var matcher = j$.matchers.toThrow(), + fn = function() { + throw "a long message"; + }, + result; + + result = matcher.compare(fn, /short/); + + expect(result.pass).toBe(false); + expect(result.message).toEqual("Expected function to throw an exception matching /short/."); + }); + }); +}); \ No newline at end of file diff --git a/spec/core/matchersUtilSpec.js b/spec/core/matchersUtilSpec.js new file mode 100644 index 00000000..b0f0c847 --- /dev/null +++ b/spec/core/matchersUtilSpec.js @@ -0,0 +1,199 @@ +describe("matchersUtil", function() { + describe("equals", function() { + it("passes for literals that are threequal", function() { + expect(j$.matchersUtil.equals(null, null)).toBe(true); + expect(j$.matchersUtil.equals(void 0, void 0)).toBe(true); + }); + + it("fails for things that are not equivalent", function() { + expect(j$.matchersUtil.equals({a: "foo"}, 1)).toBe(false); + }); + + it("passes for Strings that are equivalent", function() { + expect(j$.matchersUtil.equals("foo", "foo")).toBe(true); + }); + + it("fails for Strings that are not equivalent", function() { + expect(j$.matchersUtil.equals("foo", "bar")).toBe(false); + }); + + it("passes for Numbers that are equivalent", function() { + expect(j$.matchersUtil.equals(123, 123)).toBe(true); + }); + + it("fails for Numbers that are not equivalent", function() { + expect(j$.matchersUtil.equals(123, 456)).toBe(false); + }); + + it("passes for Dates that are equivalent", function() { + expect(j$.matchersUtil.equals(new Date("Jan 1, 1970"), new Date("Jan 1, 1970"))).toBe(true); + }); + + it("fails for Dates that are not equivalent", function() { + expect(j$.matchersUtil.equals(new Date("Jan 1, 1970"), new Date("Feb 3, 1991"))).toBe(false); + }); + + it("passes for Booleans that are equivalent", function() { + expect(j$.matchersUtil.equals(true, true)).toBe(true); + }); + + it("fails for Booleans that are not equivalent", function() { + expect(j$.matchersUtil.equals(true, false)).toBe(false); + }); + + it("passes for RegExps that are equivalent", function() { + expect(j$.matchersUtil.equals(/foo/, /foo/)).toBe(true); + }); + + it("fails for RegExps that are not equivalent", function() { + expect(j$.matchersUtil.equals(/foo/, /bar/)).toBe(false); + expect(j$.matchersUtil.equals(new RegExp("foo", "i"), new RegExp("foo"))).toBe(false); + }); + + it("passes for Arrays that are equivalent", function() { + expect(j$.matchersUtil.equals([1, 2], [1, 2])).toBe(true); + }); + + it("fails for Arrays that are not equivalent", function() { + expect(j$.matchersUtil.equals([1, 2], [1, 2, 3])).toBe(false); + }); + + it("passes for Objects that are equivalent (simple case)", function() { + expect(j$.matchersUtil.equals({a: "foo"}, {a: "foo"})).toBe(true); + }); + + it("fails for Objects that are not equivalent (simple case)", function() { + expect(j$.matchersUtil.equals({a: "foo"}, {a: "bar"})).toBe(false); + }); + + it("passes for Objects that are equivalent (deep case)", function() { + expect(j$.matchersUtil.equals({a: "foo", b: { c: "bar"}}, {a: "foo", b: { c: "bar"}})).toBe(true); + }); + + it("fails for Objects that are not equivalent (deep case)", function() { + expect(j$.matchersUtil.equals({a: "foo", b: { c: "baz"}}, {a: "foo", b: { c: "bar"}})).toBe(false); + }); + + it("passes for Objects that are equivalent (with cycles)", function() { + var actual = { a: "foo" }, + expected = { a: "foo" }; + + actual.b = actual; + expected.b = actual; + + expect(j$.matchersUtil.equals(actual, expected)).toBe(true); + }); + + it("fails for Objects that are not equivalent (with cycles)", function() { + var actual = { a: "foo" }, + expected = { a: "bar" }; + + actual.b = actual; + expected.b = actual; + + expect(j$.matchersUtil.equals(actual, expected)).toBe(false); + }); + + it("fails when comparing an empty object to an empty array (issue #114)", function() { + var emptyObject = {}, + emptyArray = []; + + expect(j$.matchersUtil.equals(emptyObject, emptyArray)).toBe(false); + expect(j$.matchersUtil.equals(emptyArray, emptyObject)).toBe(false); + }); + + it("passes when Any is used", function() { + var number = 3, + anyNumber = new j$.Any(Number); + + expect(j$.matchersUtil.equals(number, anyNumber)).toBe(true); + expect(j$.matchersUtil.equals(anyNumber, number)).toBe(true); + }); + + it("fails when Any is compared to something unexepcted", function() { + var number = 3, + anyString = new j$.Any(String); + + expect(j$.matchersUtil.equals(number, anyString)).toBe(false); + expect(j$.matchersUtil.equals(anyString, number)).toBe(false); + }); + + it("passes when ObjectContaining is used", function() { + var obj = { + foo: 3, + bar: 7 + }; + + expect(j$.matchersUtil.equals(obj, new j$.ObjectContaining({foo: 3}))).toBe(true); + }); + + it("passes when a custom equality matcher returns true", function() { + var tester = function(a, b) { return true; }; + + expect(j$.matchersUtil.equals(1, 2, [tester])).toBe(true); + }); + + it("fails for equivalents when a custom equality matcher returns false", function() { + var tester = function(a, b) { return false; }; + + expect(j$.matchersUtil.equals(1, 2, [tester])).toBe(false); + }); + }); + + describe("contains", function() { + it("passes when expected is a substring of actual", function() { + expect(j$.matchersUtil.contains("ABC", "B")).toBe(true); + }); + + it("fails when expected is a not substring of actual", function() { + expect(j$.matchersUtil.contains("ABC", "X")).toBe(false); + }); + + it("passes when expected is an element in an actual array", function() { + expect(j$.matchersUtil.contains(['foo', 'bar'], 'foo')).toBe(true); + }); + + it("fails when expected is not an element in an actual array", function() { + expect(j$.matchersUtil.contains(['foo', 'bar'], 'baz')).toBe(false); + }); + + it("passes with mixed-element arrays", function() { + expect(j$.matchersUtil.contains(["foo", {some: "bar"}], "foo")).toBe(true); + expect(j$.matchersUtil.contains(["foo", {some: "bar"}], {some: "bar"})).toBe(true); + }); + + it("uses custom equality testers if passed in and actual is an Array", function() { + var customTester = function(a,b) {return true;}; + + expect(j$.matchersUtil.contains([1,2], 2, [customTester])).toBe(true); + }); + }); + + describe("buildMessage", function() { + + it("builds an English sentence for a failure case", function() { + var actual = "foo", + name = "toBar", + message = j$.matchersUtil.buildFailureMessage(name, false, actual); + + expect(message).toEqual("Expected 'foo' to bar."); + }); + + it("builds an English sentence for a 'not' failure case", function() { + var actual = "foo", + name = "toBar", + isNot = true, + message = message = j$.matchersUtil.buildFailureMessage(name, isNot, actual); + + expect(message).toEqual("Expected 'foo' not to bar."); + }); + + it("builds an English sentence for an arbitrary array of expected arguments", function(){ + var actual = "foo", + name = "toBar", + message = j$.matchersUtil.buildFailureMessage(name, false, actual, "quux", "corge"); + + expect(message).toEqual("Expected 'foo' to bar 'quux', 'corge'."); + }); + }); +}); \ No newline at end of file diff --git a/src/core/Any.js b/src/core/Any.js new file mode 100644 index 00000000..d2739015 --- /dev/null +++ b/src/core/Any.js @@ -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 ''; + }; + + return Any; +}; \ No newline at end of file diff --git a/src/core/Env.js b/src/core/Env.js index 22200fb9..c3777bc7 100644 --- a/src/core/Env.js +++ b/src/core/Env.js @@ -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; }; diff --git a/src/core/Expectation.js b/src/core/Expectation.js new file mode 100644 index 00000000..e8205eb9 --- /dev/null +++ b/src/core/Expectation.js @@ -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; +}; diff --git a/src/core/Matchers.js b/src/core/Matchers.js deleted file mode 100644 index c9643735..00000000 --- a/src/core/Matchers.js +++ /dev/null @@ -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 ''; - }; - - 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 ""; - }; - - return Matchers; - -}; diff --git a/src/core/ObjectContaining.js b/src/core/ObjectContaining.js new file mode 100644 index 00000000..3695b9f0 --- /dev/null +++ b/src/core/ObjectContaining.js @@ -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 ""; + }; + + return ObjectContaining; +}; \ No newline at end of file diff --git a/src/core/matchers.js b/src/core/matchers.js new file mode 100644 index 00000000..206d3ab2 --- /dev/null +++ b/src/core/matchers.js @@ -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; +}; diff --git a/src/core/matchersUtil.js b/src/core/matchersUtil.js new file mode 100644 index 00000000..12b545df --- /dev/null +++ b/src/core/matchersUtil.js @@ -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'; + } + } +}; \ No newline at end of file diff --git a/src/core/requireCore.js b/src/core/requireCore.js index f9f2c8f9..4b216f66 100644 --- a/src/core/requireCore.js +++ b/src/core/requireCore.js @@ -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();