Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

add support for chainable matchers (commits condensed) #194

Closed
wants to merge 4 commits into from

4 participants

@maxbrunsfeld

I condensed the commits from this pull request down into 4, and gave lengthier commit messages.

maxbrunsfeld added some commits
@maxbrunsfeld maxbrunsfeld Add ability to update ExpectationResults after creating them.
This is to support chained matcher functions. A chain
of matcher functions needs to produce a single result.
So when a chained matcher executes, it needs to update
the message and pass/fail status of an existing expectation
result.

Three new methods have been added:
- ExpectationResult#update. 
- NestedResults#updateResult
- Spec#updateExpectationResult

The last of these functions will be called from inside
chained matcher functions, instead of #addExpectationResult.
59bdf0d
@maxbrunsfeld maxbrunsfeld Add utility method for creating subclasses.
When initializing and adding matchers, multiple
anonymous subclasses of jasmine.Matchers are created,
whose constructor functions just call 'super'. In order 
to support chainable matchers, more matchers classes
will have to be created, with the same behavior. 
This function reduces the repetition involved in doing this.
d8046d4
@maxbrunsfeld maxbrunsfeld allow Spec#addMatchers to create chained matchers classes
this adds three new ways off calling #addMatchers:

- with an additional string parameter that specifies
  which matcher the new matcher functions will be 
  chainable from.

- with an *array* of these matcher names. this allows
  a matcher to be made chainable from more than one
  matcher function.

- with a matchers hash whose keys contain multiple
  space-separated matcher names, e.g.
  "toHaveBeenCalled before": function() {...

A Spec object now has a hash of chained matchers
classes. Calling #addMatchers in the above ways
will create and add methods to these classes. 
When a chained matchers class exists for a given 
matcher function, that function will return an 
instance of that matchers class.
b4bb2eb
@maxbrunsfeld maxbrunsfeld Make chained matchers produce correct expectation results
The ExpectationResult created by the first matcher in
a chain is now passed to all of the successive matchers.
Each matcher updates the result's pass/fail status,
message and stack trace.

Normally, the chain of matchers will produce a passing
result iff *all* of the matchers in the chain match
successfully (i.e. their definition functions return
true).

With a `not` at the beginning of the matcher chain,
the chain will produce a passing result iff *any*
of the matchers in the chain fail to match.
8126fdc
@maxbrunsfeld

Do you guys have any feedback on this one? I wanted to build on it by adding these chainable matchers for spies:

  expect(spy).toHaveBeenCalled().on(object).with(args).before(otherSpy);
  expect(spy).toHaveBeenCalled().with(args).atLeast(3).times();
  expect(spy).toHaveBeenCalled().exactly(5).times().after(otherSpy);
  expect(spy).toHaveBeenCalled().exactly().once();

They'd be usable in any combination with each other, and with people's custom spy matchers. Would you guys merge such a thing?

@shamansir

I am not from Pivotal, and I think this PR is nice, but a rare user should need it. So if you'll just keep it here and we'll just know that it exists, will be very fine.

For my cases I've created toHaveBeenCalledOnce and toHaveBeenCalledThisAmountOfTimes, they are in separate file and take no more than 10 lines, and it's enough for me.

@ragaskar
Owner

This is something I'd like to see eventually make it in; can't be auto-merged any longer, but we'll keep it open to make sure we're tracking it.

@infews
Owner

We pondered this for 2.0 and decided to defer.

Saved this for posterity in this Tracker Story.

@infews infews closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 3, 2012
  1. @maxbrunsfeld

    Add ability to update ExpectationResults after creating them.

    maxbrunsfeld authored
    This is to support chained matcher functions. A chain
    of matcher functions needs to produce a single result.
    So when a chained matcher executes, it needs to update
    the message and pass/fail status of an existing expectation
    result.
    
    Three new methods have been added:
    - ExpectationResult#update. 
    - NestedResults#updateResult
    - Spec#updateExpectationResult
    
    The last of these functions will be called from inside
    chained matcher functions, instead of #addExpectationResult.
  2. @maxbrunsfeld

    Add utility method for creating subclasses.

    maxbrunsfeld authored
    When initializing and adding matchers, multiple
    anonymous subclasses of jasmine.Matchers are created,
    whose constructor functions just call 'super'. In order 
    to support chainable matchers, more matchers classes
    will have to be created, with the same behavior. 
    This function reduces the repetition involved in doing this.
  3. @maxbrunsfeld

    allow Spec#addMatchers to create chained matchers classes

    maxbrunsfeld authored
    this adds three new ways off calling #addMatchers:
    
    - with an additional string parameter that specifies
      which matcher the new matcher functions will be 
      chainable from.
    
    - with an *array* of these matcher names. this allows
      a matcher to be made chainable from more than one
      matcher function.
    
    - with a matchers hash whose keys contain multiple
      space-separated matcher names, e.g.
      "toHaveBeenCalled before": function() {...
    
    A Spec object now has a hash of chained matchers
    classes. Calling #addMatchers in the above ways
    will create and add methods to these classes. 
    When a chained matchers class exists for a given 
    matcher function, that function will return an 
    instance of that matchers class.
  4. @maxbrunsfeld

    Make chained matchers produce correct expectation results

    maxbrunsfeld authored
    The ExpectationResult created by the first matcher in
    a chain is now passed to all of the successive matchers.
    Each matcher updates the result's pass/fail status,
    message and stack trace.
    
    Normally, the chain of matchers will produce a passing
    result iff *all* of the matchers in the chain match
    successfully (i.e. their definition functions return
    true).
    
    With a `not` at the beginning of the matcher chain,
    the chain will produce a passing result iff *any*
    of the matchers in the chain fail to match.
This page is out of date. Refresh to see the latest.
View
47 spec/core/BaseSpec.js
@@ -24,4 +24,51 @@ describe("base.js", function() {
expect(jasmine.getGlobal()).toBe(globalObject);
});
});
+
+ describe("jasmine.ExpectationResult", function() {
+ var result;
+
+ beforeEach(function() {
+ result = new jasmine.ExpectationResult({
+ passed: true,
+ message: "some message"
+ });
+ });
+
+ describe("#update", function() {
+ it("updates the passing status", function() {
+ result.update({ passed: false });
+ expect(result.passed()).toBeFalsy();
+ });
+
+ describe("when the result is passing", function() {
+ it("sets the message to 'Passed.'", function() {
+ result.update({
+ passed: true,
+ message: "some message"
+ });
+
+ expect(result.message).toBe("Passed.");
+ });
+ });
+
+ describe("when the result is failing", function() {
+ beforeEach(function() {
+ result.update({
+ passed: false,
+ message: "some message"
+ });
+ });
+
+ it("updates the message", function() {
+ expect(result.message).toBe("some message");
+ });
+
+ it("creates a stack trace with the message", function() {
+ expect(result.trace instanceof Error).toBeTruthy();
+ expect(result.trace.message).toBe("some message");
+ });
+ });
+ });
+ });
});
View
588 spec/core/ChainedMatchersSpec.js
@@ -0,0 +1,588 @@
+describe("Chained matchers", function() {
+
+ describe(".makeChainName(prefix, matcherName)", function() {
+ describe("when there is no prefix", function() {
+ it("returns the matcher name", function() {
+ var chainName1 = jasmine.ChainedMatchers.makeChainName("", "toBeCool");
+ expect(chainName1).toBe("toBeCool");
+ });
+ });
+
+ describe("when there is a prefix", function() {
+ it("adds the matcherName to the prefix, separated by a space", function() {
+ var chainName1 = jasmine.ChainedMatchers.makeChainName("toBeBetween", "and");
+ var chainName2 = jasmine.ChainedMatchers.makeChainName("toHaveA between", "and");
+ expect(chainName1).toBe("toBeBetween and");
+ expect(chainName2).toBe("toHaveA between and");
+ });
+ });
+ });
+
+ describe(".parseMatchers(matchersHash)", function() {
+ var matchersHash, parsedMatchers;
+
+ beforeEach(function() {
+ matchersHash = {
+ "toHaveA": function() {},
+ "toHaveA ofExactly": function() {},
+ "toHaveA withA": function() {},
+ "toHaveA withA ofExactly": function() {},
+ "toHaveA withA ofAtLeast": function() {},
+ "toHaveBeenCalled after": function() {},
+ "toHaveBeenCalled before": function() {},
+ "toHaveBeenCalled atLeast times": function() {},
+ "toHaveBeenCalled atLeast secondsBefore": function() {},
+ "toHaveBeenCalled atLeast secondsAfter": function() {}
+ };
+
+ parsedMatchers = jasmine.ChainedMatchers.parseMatchers(matchersHash);
+ });
+
+ it("has two keys: 'topLevel' and 'chained', both of which are objects", function() {
+ expect(parsedMatchers).toEqual({
+ topLevel: jasmine.any(Object),
+ chained: jasmine.any(Object)
+ });
+ });
+
+ it("puts matchers with single-word keys into the 'topLevel' object", function() {
+ expect(parsedMatchers.topLevel).toEqual({
+ toHaveA: matchersHash.toHaveA,
+ toHaveBeenCalled: matchersHash.toHaveBeenCalled,
+ });
+ });
+
+ it("groups the remaining methods by their prefix, in the 'chained' object", function() {
+ expect(parsedMatchers.chained).toEqual({
+ "toHaveA": {
+ ofExactly: matchersHash["toHaveA ofExactly"],
+ withA: matchersHash["toHaveA withA"]
+ },
+
+ "toHaveA withA": {
+ ofExactly: matchersHash["toHaveA withA ofExactly"],
+ ofAtLeast: matchersHash["toHaveA withA ofAtLeast"]
+ },
+
+ "toHaveBeenCalled": {
+ after: matchersHash["toHaveBeenCalled after"],
+ before: matchersHash["toHaveBeenCalled before"]
+ },
+
+ "toHaveBeenCalled atLeast": {
+ times: matchersHash["toHaveBeenCalled atLeast times"],
+ secondsAfter: matchersHash["toHaveBeenCalled atLeast secondsAfter"],
+ secondsBefore: matchersHash["toHaveBeenCalled atLeast secondsBefore"]
+ }
+ });
+ });
+
+ it("handles key names containing '$' and '_'", function() {
+ matchersHash = {
+ "to_be_in$ane": function() {},
+ "to_be_in$ane and_awe$ome": function() {},
+ "to_be_in$ane and_a$tounding": function() {},
+ "to_be_in$ane and_a$tounding beyond_compare": function() {},
+ };
+
+ expect(jasmine.ChainedMatchers.parseMatchers(matchersHash)).toEqual({
+ topLevel: {
+ to_be_in$ane: matchersHash["to_be_in$ane"]
+ },
+
+ chained: {
+ "to_be_in$ane": {
+ and_awe$ome: matchersHash["to_be_in$ane and_awe$ome"],
+ and_a$tounding: matchersHash["to_be_in$ane and_a$tounding"]
+ },
+
+ "to_be_in$ane and_a$tounding": {
+ beyond_compare: matchersHash["to_be_in$ane and_a$tounding beyond_compare"]
+ }
+ }
+ });
+ });
+ });
+
+ describe("Spec#addMatchers", function() {
+ var env, suite;
+
+ beforeEach(function() {
+ env = new jasmine.Env();
+ env.updateInterval = 0;
+ suite = env.describe("suite", function() {});
+ env.currentSuite = suite;
+ });
+
+ describe("with a matchers object whose keys contain multiple matcher names", function() {
+ beforeEach(function() {
+ env.beforeEach(function() {
+ this.addMatchers({
+ 'toHaveA': function(key) {
+ this.valueToCompare = this.actual[key];
+ return !!this.valueToCompare;
+ },
+
+ 'toHaveA ofExactly': function(value) {
+ return this.valueToCompare === value;
+ },
+
+ 'toHaveA between': function(lowerBound) {
+ return this.valueToCompare >= lowerBound;
+ },
+
+ 'toHaveA between and': function(upperBound) {
+ return this.valueToCompare <= upperBound;
+ }
+ });
+ });
+ });
+
+ itCreatesMatcherMethodsCorrectly();
+ });
+
+ describe("with a matcher name string and a matchers object", function() {
+ beforeEach(function() {
+ env.beforeEach(function() {
+ this.addMatchers({
+ toHaveA: function(key) {
+ this.valueToCompare = this.actual[key];
+ return !!this.valueToCompare;
+ },
+ });
+
+ this.addMatchers('toHaveA', {
+ ofExactly: function(value) {
+ return this.valueToCompare === value;
+ },
+
+ between: function(lowerBound) {
+ return this.valueToCompare >= lowerBound;
+ }
+ });
+
+ this.addMatchers('toHaveA between', {
+ and: function(upperBound) {
+ return this.valueToCompare <= upperBound;
+ }
+ })
+ });
+ });
+
+ itCreatesMatcherMethodsCorrectly();
+ });
+
+ describe("with an array of matcher name strings and a matchers object", function() {
+ beforeEach(function() {
+ env.beforeEach(function() {
+ this.addMatchers({
+ toHaveA: function(key) {
+ this.valueToCompare = this.actual[key];
+ return !!this.valueToCompare;
+ },
+
+ 'toHaveA withA': function(key) {
+ this.valueToCompare = this.valueToCompare[key];
+ return !!this.valueToCompare;
+ }
+ });
+
+ this.addMatchers(["toHaveA", "toHaveA withA"], {
+ ofExactly: function(value) {
+ return this.valueToCompare === value;
+ },
+
+ between: function(lowerBound) {
+ return this.valueToCompare >= lowerBound;
+ },
+
+ 'between and': function(upperBound) {
+ return this.valueToCompare <= upperBound;
+ }
+ });
+ });
+ });
+
+ itCreatesMatcherMethodsCorrectly();
+
+ it("adds the given matchers to ALL of the named matcher classes", function() {
+ var passingResults = resultsOfSpec(function() {
+ this.expect({ triangle: { height: 12 } }).toHaveA("triangle");
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("square");
+ this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height");
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("width");
+ this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").ofExactly(12);
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").ofExactly(24);
+ this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").between(10).and(20);
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").between(1).and(10);
+ });
+ var passingItems = passingResults.getItems();
+
+ expect(passingItems.length).toBe(8);
+ expect(passingResults.passedCount).toBe(8);
+ expect(passingResults.failedCount).toBe(0);
+
+ expect(passingItems[0].passed()).toBeTruthy();
+ expect(passingItems[1].passed()).toBeTruthy();
+ expect(passingItems[2].passed()).toBeTruthy();
+ expect(passingItems[3].passed()).toBeTruthy();
+ expect(passingItems[4].passed()).toBeTruthy();
+ expect(passingItems[5].passed()).toBeTruthy();
+ expect(passingItems[6].passed()).toBeTruthy();
+ expect(passingItems[7].passed()).toBeTruthy();
+
+ var failingResults = resultsOfSpec(function() {
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle");
+ this.expect({ triangle: { height: 12 } }).toHaveA("square");
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height");
+ this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("width");
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").ofExactly(12);
+ this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").ofExactly(24);
+ this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").between(10).and(20);
+ this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").between(1).and(10);
+ });
+ var failingItems = failingResults.getItems();
+
+ expect(failingItems.length).toBe(8);
+ expect(failingResults.passedCount).toBe(0);
+ expect(failingResults.failedCount).toBe(8);
+
+ expect(failingItems[0].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle'.");
+ expect(failingItems[1].message).toBe("Expected { triangle : { height : 12 } } to have a 'square'.");
+ expect(failingItems[2].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle' with a 'height'.");
+ expect(failingItems[3].message).toBe("Expected { triangle : { height : 12 } } to have a 'triangle' with a 'width'.");
+ expect(failingItems[4].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle' with a 'height' of exactly 12.");
+ expect(failingItems[5].message).toBe("Expected { triangle : { height : 12 } } to have a 'triangle' with a 'height' of exactly 24.");
+ expect(failingItems[6].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle' with a 'height' between 10 and 20.");
+ expect(failingItems[7].message).toBe("Expected { triangle : { height : 12 } } to have a 'triangle' with a 'height' between 1 and 10.");
+ });
+ });
+
+ describe("when some of the matchers define custom messages", function() {
+ beforeEach(function() {
+ env.beforeEach(function() {
+ this.addMatchers({
+ toHaveA: function(key) {
+ this.valueToCompare = this.actual[key];
+ return !!this.valueToCompare;
+ },
+
+ toHaveASpecial: function(key) {
+ this.message = function() { return ["message for toHaveASpecial", "message for not toHaveASpecial"]; };
+ this.valueToCompare = this.actual[key];
+ return !!this.valueToCompare;
+ }
+ });
+
+ this.addMatchers(["toHaveA", "toHaveASpecial"], {
+ withA: function(key) {
+ this.valueToCompare = this.valueToCompare[key];
+ return !!this.valueToCompare;
+ },
+
+ withASpecial: function(key) {
+ this.message = function() { return ["message for withASpecial", "message for not withASpecial"]; };
+ this.valueToCompare = this.valueToCompare[key];
+ return !!this.valueToCompare;
+ }
+ });
+
+ this.addMatchers([ "toHaveA withA", "toHaveA withASpecial", "toHaveASpecial withA", "toHaveASpecial withASpecial" ], {
+ thatIs: function(value) {
+ return this.valueToCompare === value;
+ },
+
+ thatIsEspecially: function(value) {
+ this.message = function() { return ["message for thatIsEspecially", "message for not thatIsEspecially"]; };
+ return this.valueToCompare === value;
+ }
+ });
+ });
+ });
+
+ describe("when the last matcher in a chain has a custom message", function() {
+ it("uses the custom message", function() {
+ var results = resultsOfSpec(function() {
+ this.expect({ song: { melody: 'sad' } }).toHaveA("song").withASpecial('harmony');
+ this.expect({ song: { melody: 'sad' } }).not.toHaveA("song").withASpecial('melody');
+ this.expect({ song: { melody: 'sad' } }).toHaveA("song").withA('melody').thatIsEspecially('happy');
+ this.expect({ song: { melody: 'sad' } }).not.toHaveA("song").withA('melody').thatIsEspecially('sad');
+ });
+ var items = results.getItems();
+
+ expect(items.length).toBe(4);
+ expect(results.passedCount).toBe(0);
+ expect(results.failedCount).toBe(4);
+
+ expect(items[0].message).toBe("message for withASpecial");
+ expect(items[1].message).toBe("message for not withASpecial");
+ expect(items[2].message).toBe("message for thatIsEspecially");
+ expect(items[3].message).toBe("message for not thatIsEspecially");
+ });
+ });
+
+ describe("when the last matcher in the chain does not have a custom message", function() {
+ it("uses a message based on the chain of matcher names", function() {
+ var results = resultsOfSpec(function() {
+ this.expect({ song: { melody: 'sad' } }).toHaveASpecial("song").withA('harmony');
+ this.expect({ song: { melody: 'sad' } }).not.toHaveASpecial("song").withA('melody');
+ this.expect({ song: { melody: 'sad' } }).toHaveASpecial("song").withASpecial('melody').thatIs("happy");
+ this.expect({ song: { melody: 'sad' } }).not.toHaveASpecial("song").withASpecial('melody').thatIs("sad");
+ });
+ var items = results.getItems();
+
+ expect(items.length).toBe(4);
+ expect(results.passedCount).toBe(0);
+ expect(results.failedCount).toBe(4);
+
+ expect(items.length).toBe(4);
+ expect(items[0].message).toBe("Expected { song : { melody : 'sad' } } to have a special 'song' with a 'harmony'.");
+ expect(items[1].message).toBe("Expected { song : { melody : 'sad' } } not to have a special 'song' with a 'melody'.");
+ expect(items[2].message).toBe("Expected { song : { melody : 'sad' } } to have a special 'song' with a special 'melody' that is 'happy'.");
+ expect(items[3].message).toBe("Expected { song : { melody : 'sad' } } not to have a special 'song' with a special 'melody' that is 'sad'.");
+ });
+ });
+ });
+
+ it("works in real life", function() {
+ this.addMatchers({
+ 'toHaveA': function(key) {
+ this.valueToCompare = this.actual[key];
+ return !!this.valueToCompare;
+ },
+
+ 'toHaveA withA': function(key) {
+ this.valueToCompare = this.valueToCompare[key];
+ return !!this.valueToCompare;
+ }
+ });
+
+ this.addMatchers(["toHaveA", "toHaveA withA"], {
+ 'ofExactly': function(value) {
+ return this.valueToCompare === value;
+ },
+
+ 'between': function(lowerBound) {
+ return this.valueToCompare >= lowerBound;
+ },
+
+ 'between and': function(upperBound) {
+ return this.valueToCompare <= upperBound;
+ }
+ });
+
+ expect({ height: 12 }).toHaveA("height");
+ expect({ height: 12 }).not.toHaveA("width");
+ expect({ height: 12 }).toHaveA("height").ofExactly(12);
+ expect({ height: 12 }).not.toHaveA("height").ofExactly(20);
+ expect({ height: 12 }).toHaveA("height").between(10).and(20);
+ expect({ height: 12 }).not.toHaveA("height").between(1).and(10);
+
+ expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height");
+ expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("width");
+ expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").ofExactly(12);
+ expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").ofExactly(24);
+ expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").between(10).and(20);
+ expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").between(1).and(10);
+ });
+
+ function itCreatesMatcherMethodsCorrectly() {
+ describe("the return value of a matcher function", function() {
+ describe("when no further chained matchers have been added", function() {
+ var unchainableMatcherValue1, unchainableMatcherValue2, unchainableMatcherValue3;
+
+ beforeEach(function() {
+ env.it("spec", function() {
+ unchainableMatcherValue1 = this.expect({ height: 12 }).toBeTruthy();
+ unchainableMatcherValue2 = this.expect({ height: 12 }).toHaveA('height').ofExactly(10);
+ unchainableMatcherValue3 = this.expect({ height: 12 }).toHaveA('height').between(10).and(20);
+ });
+
+ suite.execute();
+ });
+
+ it("is undefined", function() {
+ expect(unchainableMatcherValue1).toBeUndefined();
+ expect(unchainableMatcherValue2).toBeUndefined();
+ expect(unchainableMatcherValue3).toBeUndefined();
+ });
+ });
+
+ describe("when further chained matchers have been added", function() {
+ var expectValue, chainableMatcherValue1, chainableMatcherValue2;
+
+ beforeEach(function() {
+ env.it("spec", function() {
+ expectValue = this.expect({ height: 12 });
+ chainableMatcherValue1 = this.expect({ height: 12 }).toHaveA('height');
+ chainableMatcherValue2 = this.expect({ height: 12 }).toHaveA('height').between(10);
+ });
+
+ suite.execute();
+ });
+
+ it("has methods for each of the chained matchers", function() {
+ expect(typeof chainableMatcherValue1.ofExactly).toBe("function");
+ expect(typeof chainableMatcherValue1.between).toBe("function");
+ expect(typeof chainableMatcherValue2.and).toBe("function");
+ });
+
+ it("does not have the same methods as other matchers", function() {
+ expect(chainableMatcherValue2.ofExactly).toBeUndefined();
+ expect(chainableMatcherValue2.between).toBeUndefined();
+ });
+
+ it("does not have the normal top-level matcher methods", function() {
+ expect(chainableMatcherValue1.toBe).toBeUndefined();
+ expect(chainableMatcherValue2.toBe).toBeUndefined();
+ expect(chainableMatcherValue1.toEqual).toBeUndefined();
+ expect(chainableMatcherValue2.toEqual).toBeUndefined();
+ });
+
+ it("has the same env and spec as the parent matcher object", function() {
+ expect(chainableMatcherValue1.env).toBe(expectValue.env);
+ expect(chainableMatcherValue2.env).toBe(expectValue.env);
+ expect(chainableMatcherValue1.spec).toBe(expectValue.spec);
+ expect(chainableMatcherValue2.spec).toBe(expectValue.spec);
+ });
+
+ it("does NOT have a 'not' property (nots are only allowed at the beginning of a matcher chain)", function() {
+ expect(chainableMatcherValue1['not']).toBeUndefined();
+ expect(chainableMatcherValue2['not']).toBeUndefined();
+ });
+
+ it("keeps any properties that are set on 'this' by earlier matcher functions", function() {
+ expect(chainableMatcherValue1.valueToCompare).toBe(12);
+ expect(chainableMatcherValue1.valueToCompare).toBe(12);
+ });
+ });
+ });
+
+ describe("when matchers are chained without a 'not'", function() {
+ var results, items;
+
+ describe("when any of the matchers in the chain do NOT match", function() {
+ beforeEach(function() {
+ results = resultsOfSpec(function() {
+ this.expect({ height: 3 }).toHaveA("width").ofExactly(3);
+ this.expect({ height: 3 }).toHaveA("height").ofExactly(5);
+ this.expect({ height: 3 }).toHaveA("height").between(1).and(2);
+ });
+ items = results.getItems();
+ });
+
+ it("adds one failure to the spec's results", function() {
+ expect(items.length).toBe(3);
+ expect(results.passedCount).toBe(0);
+ expect(results.failedCount).toBe(3);
+
+ expect(items[0].passed()).toBeFalsy();
+ expect(items[1].passed()).toBeFalsy();
+ expect(items[2].passed()).toBeFalsy();
+ });
+
+ it("builds a failure message from the complete chain of matchers", function() {
+ expect(items[0].message).toBe("Expected { height : 3 } to have a 'width' of exactly 3.");
+ expect(items[1].message).toBe("Expected { height : 3 } to have a 'height' of exactly 5.");
+ expect(items[2].message).toBe("Expected { height : 3 } to have a 'height' between 1 and 2.");
+ });
+
+ it("builds a trace with the right message", function() {
+ expect(items[0].trace instanceof Error).toBeTruthy();
+ expect(items[1].trace instanceof Error).toBeTruthy();
+ expect(items[2].trace instanceof Error).toBeTruthy();
+
+ expect(items[0].trace.message).toBe(items[0].message);
+ expect(items[1].trace.message).toBe(items[1].message);
+ expect(items[2].trace.message).toBe(items[2].message);
+ });
+ });
+
+ describe("when all of the matchers match", function() {
+ it("adds one success to the spec's results", function() {
+ results = resultsOfSpec(function() {
+ this.expect({ height: 3 }).toHaveA("height").ofExactly(3);
+ this.expect({ height: 3 }).toHaveA("height").between(2).and(5);
+ });
+ items = results.getItems();
+
+ expect(items.length).toBe(2);
+ expect(results.passedCount).toBe(2);
+ expect(results.failedCount).toBe(0);
+
+ expect(items[0].passed()).toBeTruthy();
+ expect(items[1].passed()).toBeTruthy();
+ expect(items[0].message).toBe("Passed.");
+ expect(items[1].message).toBe("Passed.");
+ });
+ });
+ });
+
+ describe("when matchers are chained, starting with a 'not'", function() {
+ var results, items;
+
+ describe("when any of the matchers in the chain do NOT match", function() {
+ it("adds a single success to the spec's results", function() {
+ results = resultsOfSpec(function() {
+ this.expect({ height: 3 }).not.toHaveA("width").ofExactly(3);
+ this.expect({ height: 3 }).not.toHaveA("height").ofExactly(5);
+ this.expect({ height: 3 }).not.toHaveA("height").between(5).and(10);
+ this.expect({ height: 3 }).not.toHaveA("height").between(10).and(20);
+ });
+ items = results.getItems();
+
+ expect(results.passedCount).toBe(4);
+ expect(items.length).toBe(4);
+
+ expect(items[0].passed()).toBeTruthy();
+ expect(items[1].passed()).toBeTruthy();
+ expect(items[2].passed()).toBeTruthy();
+ expect(items[3].passed()).toBeTruthy();
+ expect(items[0].message).toBe("Passed.");
+ expect(items[1].message).toBe("Passed.");
+ expect(items[2].message).toBe("Passed.");
+ expect(items[3].message).toBe("Passed.");
+ });
+ });
+
+ describe("when all of the matchers match", function() {
+ beforeEach(function() {
+ results = resultsOfSpec(function() {
+ this.expect({ height: 3 }).not.toHaveA("height").ofExactly(3);
+ this.expect({ height: 3 }).not.toHaveA("height").between(2).and(4);
+ });
+ items = results.getItems();
+ });
+
+ it("adds a single failure to the spec's results", function() {
+ expect(items.length).toBe(2);
+ expect(results.passedCount).toBe(0);
+ expect(results.failedCount).toBe(2);
+
+ expect(items[0].passed()).toBeFalsy();
+ expect(items[1].passed()).toBeFalsy();
+ });
+
+ it("builds a failure message from the complete chain of matchers", function() {
+ expect(items[0].message).toBe("Expected { height : 3 } not to have a 'height' of exactly 3.");
+ expect(items[1].message).toBe("Expected { height : 3 } not to have a 'height' between 2 and 4.");
+ });
+
+ it("builds a trace with the right message", function() {
+ expect(items[0].trace instanceof Error).toBeTruthy();
+ expect(items[1].trace instanceof Error).toBeTruthy();
+
+ expect(items[0].trace.message).toBe(items[0].message);
+ expect(items[1].trace.message).toBe(items[1].message);
+ });
+ });
+ });
+ }
+
+ function resultsOfSpec(specFunction) {
+ var spec = env.it("spec", specFunction);
+ suite.execute();
+ return spec.results();
+ }
+ });
+});
View
52 spec/core/NestedResultsSpec.js
@@ -51,4 +51,56 @@ describe('jasmine.NestedResults', function() {
expect(branchResults.failedCount).toEqual(2);
});
+ describe("#updateResult", function() {
+ var results, result1, result2;
+
+ beforeEach(function() {
+ results = new jasmine.NestedResults();
+ result1 = new jasmine.ExpectationResult({
+ passed: true,
+ message: "Passed."
+ });
+ result2 = new jasmine.ExpectationResult({
+ passed: false,
+ message: "fail."
+ });
+
+ results.addResult(result1);
+ results.addResult(result2);
+ });
+
+ describe("when a result that was passing is updated to fail", function() {
+ beforeEach(function() {
+ results.updateResult(result1, { passed: false, message: "nope. failed." });
+ });
+
+ it("increments the failed count and decrements the passed count", function() {
+ expect(results.totalCount).toEqual(2);
+ expect(results.passedCount).toEqual(0);
+ expect(results.failedCount).toEqual(2);
+ });
+
+ it("updates the message and passing status of the result", function() {
+ expect(result1.passed()).toBeFalsy();
+ expect(result1.message).toBe("nope. failed.");
+ });
+ });
+
+ describe("when a result that was failing is updated to pass", function() {
+ beforeEach(function() {
+ results.updateResult(result2, { passed: true });
+ });
+
+ it("increments the failed count and decrements the passed count", function() {
+ expect(results.totalCount).toEqual(2);
+ expect(results.passedCount).toEqual(2);
+ expect(results.failedCount).toEqual(0);
+ });
+
+ it("updates the message and passing status of the result", function() {
+ expect(result2.passed()).toBeTruthy();
+ expect(result2.message).toBe("Passed.");
+ });
+ });
+ });
});
View
47 spec/core/UtilSpec.js
@@ -36,4 +36,51 @@ describe("jasmine.util", function() {
expect(jasmine.isArray_(null)).toBe(false);
});
});
+
+ describe("inherit(childClass, parentClass)", function() {
+ var ParentClass, ChildClass, childInstance;
+
+ beforeEach(function() {
+ ParentClass = function() {};
+ ChildClass = function() {};
+ jasmine.util.inherit(ChildClass, ParentClass);
+ childInstance = new ChildClass();
+ });
+
+ it("sets the given child class's prototype to be an instance of the parent class", function() {
+ expect(ChildClass.prototype instanceof ParentClass).toBeTruthy();
+ expect(childInstance instanceof ChildClass).toBeTruthy();
+ });
+
+ it("sets the 'constructor' property correctly on the prototype", function() {
+ expect(childInstance.constructor).toBe(ChildClass);
+ });
+ });
+
+ describe("subclass(parentClass)", function() {
+ var ParentClass, ChildClass, childInstance;
+
+ beforeEach(function() {
+ ParentClass = function(param) { this.parentParam = param };
+ ChildClass = jasmine.util.subclass(ParentClass);
+ childInstance = new ChildClass("foo");
+ });
+
+ it("returns a constructor function", function() {
+ expect(typeof ChildClass).toBe("function");
+ });
+
+ it("sets the constructor's prototype correctly", function() {
+ expect(childInstance instanceof ChildClass).toBeTruthy();
+ expect(childInstance instanceof ParentClass).toBeTruthy();
+ });
+
+ it("sets the 'constructor' property correctly", function() {
+ expect(childInstance.constructor).toBe(ChildClass);
+ });
+
+ it("sets up the new constructor function to call the parent constructor", function() {
+ expect(childInstance.parentParam).toBe("foo");
+ });
+ });
});
View
44 src/core/ChainedMatchers.js
@@ -0,0 +1,44 @@
+/**
+ * @constructor
+ * @param {Object} params
+ */
+jasmine.ChainedMatchers = function(params) {
+ var precedingMatcher = params.precedingMatcher;
+ for (var key in precedingMatcher) {
+ if (key === "not") continue;
+ if (key === "message") continue;
+ if (precedingMatcher.hasOwnProperty(key)) {
+ this[key] = precedingMatcher[key]
+ }
+ }
+ this.precedingResult = params.precedingResult;
+ this.precedingMessage = params.precedingMessage;
+};
+
+jasmine.ChainedMatchers.makeChainName = function(/* chain names, matcher names */) {
+ var i, name, names = [];
+ for (i = 0; i < arguments.length; i++) {
+ name = arguments[i];
+ if (name) names.push(name);
+ }
+ return names.join(" ");
+};
+
+jasmine.ChainedMatchers.parseMatchers = function(matcherDefinitions) {
+ var chained = {}, topLevel = {},
+ names, prefix, matcherName, method;
+ for (var key in matcherDefinitions) {
+ method = matcherDefinitions[key];
+ names = key.match(/[\w$]+/g);
+ prefix = names.slice(0, names.length - 1).join(" ");
+ matcherName = names.slice(names.length - 1)[0];
+ if (prefix.length > 0) {
+ chained[prefix] || (chained[prefix] = {});
+ chained[prefix][matcherName] = method;
+ } else {
+ topLevel[matcherName] = method;
+ }
+ }
+ return { topLevel: topLevel, chained: chained };
+};
+
View
7 src/core/Env.js
@@ -21,12 +21,7 @@ jasmine.Env = function() {
this.nextSuiteId_ = 0;
this.equalityTesters_ = [];
- // wrap matchers
- this.matchersClass = function() {
- jasmine.Matchers.apply(this, arguments);
- };
- jasmine.util.inherit(this.matchersClass, jasmine.Matchers);
-
+ this.matchersClass = jasmine.util.subclass(jasmine.Matchers);
jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);
};
View
107 src/core/Matchers.js
@@ -30,48 +30,85 @@ jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
}
};
-jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
- return function() {
- var matcherArgs = jasmine.util.argsToArray(arguments);
- var result = matcherFunction.apply(this, arguments);
+;(function() {
+ jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
+ return function() {
+ var matcherArgs = jasmine.util.argsToArray(arguments);
+ var result = matcherFunction.apply(this, arguments);
+ var passed = isPassing.call(this, result);
+ var defaultMessage = makeDefaultMessage.call(this, matcherName, matcherArgs);
+ var customMessage = makeCustomMessage.call(this, passed);
+ if (this.reportWasCalled_) return passed;
+
+ var expectationResult;
+ if (this.precedingResult) {
+ expectationResult = this.precedingResult;
+ this.spec.updateMatcherResult(expectationResult, {
+ passed: passed,
+ message: customMessage || defaultMessage
+ });
+ } else {
+ expectationResult = new jasmine.ExpectationResult({
+ passed: passed,
+ message: customMessage || defaultMessage,
+ matcherName: matcherName,
+ expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
+ actual: this.actual
+ });
+ this.spec.addMatcherResult(expectationResult);
+ }
- if (this.isNot) {
- result = !result;
- }
+ var nextChainName = jasmine.ChainedMatchers.makeChainName(this.constructor.chainName, matcherName);
+ var chainedMatchersClass = this.spec.chainedMatchersClasses[nextChainName];
+ if (chainedMatchersClass) {
+ return new chainedMatchersClass({
+ precedingMatcher: this,
+ precedingResult: expectationResult,
+ precedingMessage: defaultMessage
+ });
+ } else {
+ return jasmine.undefined;
+ }
+ };
+ };
- if (this.reportWasCalled_) return result;
+ function makeCustomMessage(passed) {
+ if (this.message && !passed) {
+ var customMessage = this.message.apply(this, arguments);
+ if (jasmine.isArray_(customMessage)) customMessage = customMessage[this.isNot ? 1 : 0];
+ return customMessage;
+ }
+ }
+ function makeDefaultMessage(matcherName, matcherArgs) {
var message;
- if (!result) {
- if (this.message) {
- message = this.message.apply(this, arguments);
- if (jasmine.isArray_(message)) {
- message = message[this.isNot ? 1 : 0];
- }
+ if (this.precedingMessage) {
+ message = this.precedingMessage.replace(/\.$/, " ");
+ } else {
+ message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ");
+ }
+ message += matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+ for (var i = 0; i < matcherArgs.length; i++) {
+ if (i > 0) message += ",";
+ message += " " + jasmine.pp(matcherArgs[i]);
+ }
+ message += ".";
+ return message;
+ }
+
+ function isPassing(result) {
+ var thisPassed = this.isNot ? !result : result;
+ if (this.precedingResult) {
+ if (this.isNot) {
+ return this.precedingResult.passed() || thisPassed;
} else {
- var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
- message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
- if (matcherArgs.length > 0) {
- for (var i = 0; i < matcherArgs.length; i++) {
- if (i > 0) message += ",";
- message += " " + jasmine.pp(matcherArgs[i]);
- }
- }
- message += ".";
+ return this.precedingResult.passed() && thisPassed;
}
+ } else {
+ return thisPassed;
}
- var expectationResult = new jasmine.ExpectationResult({
- matcherName: matcherName,
- passed: result,
- expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
- actual: this.actual,
- message: message
- });
- this.spec.addMatcherResult(expectationResult);
- return jasmine.undefined;
- };
-};
-
+ }
+})();
View
18 src/core/NestedResults.js
@@ -73,6 +73,24 @@ jasmine.NestedResults.prototype.addResult = function(result) {
};
/**
+ * Updates a result, tracking counts (passed & failed)
+ * @param {jasmine.ExpectationResult} result
+ * @param {Object} params
+ */
+jasmine.NestedResults.prototype.updateResult = function(result, params) {
+ var wasPassing = result.passed_,
+ isPassing = params.passed;
+ if (wasPassing && !isPassing) {
+ this.passedCount--;
+ this.failedCount++;
+ } else if (isPassing && !wasPassing) {
+ this.passedCount++;
+ this.failedCount--;
+ }
+ result.update(params);
+};
+
+/**
* @returns {Boolean} True if <b>everything</b> below passed
*/
jasmine.NestedResults.prototype.passed = function() {
View
54 src/core/Spec.js
@@ -26,6 +26,7 @@ jasmine.Spec = function(env, suite, description) {
spec.results_ = new jasmine.NestedResults();
spec.results_.description = description;
spec.matchersClass = null;
+ spec.chainedMatchersClasses = {};
};
jasmine.Spec.prototype.getFullName = function() {
@@ -67,6 +68,10 @@ jasmine.Spec.prototype.addMatcherResult = function(result) {
this.results_.addResult(result);
};
+jasmine.Spec.prototype.updateMatcherResult = function(result, params) {
+ this.results_.updateResult(result, params);
+};
+
jasmine.Spec.prototype.expect = function(actual) {
var positive = new (this.getMatchersClass_())(this.env, actual, this);
positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);
@@ -130,14 +135,49 @@ jasmine.Spec.prototype.getMatchersClass_ = function() {
return this.matchersClass || this.env.matchersClass;
};
-jasmine.Spec.prototype.addMatchers = function(matchersPrototype) {
- var parent = this.getMatchersClass_();
- var newMatchersClass = function() {
- parent.apply(this, arguments);
- };
- jasmine.util.inherit(newMatchersClass, parent);
+jasmine.Spec.prototype.makeMatchersClass_ = function(chainName) {
+ if (chainName) {
+ if (!this.chainedMatchersClasses[chainName]) {
+ this.chainedMatchersClasses[chainName] = jasmine.util.subclass(jasmine.ChainedMatchers);
+ this.chainedMatchersClasses[chainName].chainName = chainName;
+ }
+ return this.chainedMatchersClasses[chainName];
+ } else {
+ if (!this.matchersClass) {
+ this.matchersClass = jasmine.util.subclass(this.env.matchersClass);
+ }
+ return this.matchersClass;
+ }
+};
+
+jasmine.Spec.prototype.addMatchers = function(arg1, arg2) {
+ var matchers, chainPrefixes;
+ if (arg2) {
+ chainPrefixes = jasmine.isArray_(arg1) ? arg1 : [arg1];
+ matchers = arg2;
+ } else {
+ chainPrefixes = [""];
+ matchers = arg1;
+ }
+
+ var parsedMatchers = jasmine.ChainedMatchers.parseMatchers(matchers),
+ topLevelMatchers = parsedMatchers.topLevel,
+ chainedMatchers = parsedMatchers.chained;
+
+ var chainPrefix, fullChainName;
+ for (var i = 0; i < chainPrefixes.length; i++) {
+ chainPrefix = chainPrefixes[i];
+ this.addMatchers_(chainPrefix, topLevelMatchers);
+ for (var chainName in chainedMatchers) {
+ fullChainName = jasmine.ChainedMatchers.makeChainName(chainPrefix, chainName);
+ this.addMatchers_(fullChainName, chainedMatchers[chainName]);
+ }
+ }
+};
+
+jasmine.Spec.prototype.addMatchers_ = function(chainName, matchersPrototype) {
+ var newMatchersClass = this.makeMatchersClass_(chainName);
jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
- this.matchersClass = newMatchersClass;
};
jasmine.Spec.prototype.finishCallback = function() {
View
19 src/core/base.js
@@ -94,13 +94,9 @@ jasmine.MessageResult.prototype.toString = function() {
jasmine.ExpectationResult = function(params) {
this.type = 'expect';
this.matcherName = params.matcherName;
- this.passed_ = params.passed;
this.expected = params.expected;
this.actual = params.actual;
- this.message = this.passed_ ? 'Passed.' : params.message;
-
- var trace = (params.trace || new Error(this.message));
- this.trace = this.passed_ ? '' : trace;
+ this.update(params);
};
jasmine.ExpectationResult.prototype.toString = function () {
@@ -111,6 +107,19 @@ jasmine.ExpectationResult.prototype.passed = function () {
return this.passed_;
};
+jasmine.ExpectationResult.prototype.update = function(params) {
+ this.passed_ = params.passed;
+
+ if (this.passed_) {
+ this.message = "Passed.";
+ this.trace = "";
+ } else {
+ this.message = params.message;
+ this.trace = params.trace || new Error(this.message);
+ }
+};
+
+
/**
* Getter for the Jasmine environment. Ensures one gets created
*/
View
7 src/core/util.js
@@ -18,6 +18,13 @@ jasmine.util.inherit = function(childClass, parentClass) {
};
subclass.prototype = parentClass.prototype;
childClass.prototype = new subclass();
+ childClass.prototype.constructor = childClass;
+};
+
+jasmine.util.subclass = function(parentClass) {
+ var childClass = function() { return parentClass.apply(this, arguments) };
+ jasmine.util.inherit(childClass, parentClass);
+ return childClass;
};
jasmine.util.formatException = function(e) {
Something went wrong with that request. Please try again.