Skip to content
This repository

add support for chainable matchers (commits condensed) #194

Open
wants to merge 4 commits into from

3 participants

maxbrunsfeld Ulric Wilfred Rajan Agaskar
maxbrunsfeld

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

added some commits March 02, 2012
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 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 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 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?

Ulric Wilfred

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.

Rajan Agaskar
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 4 unique commits by 1 author.

Mar 02, 2012
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 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
Mar 03, 2012
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 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
This page is out of date. Refresh to see the latest.
47  spec/core/BaseSpec.js
@@ -24,4 +24,51 @@ describe("base.js", function() {
24 24
       expect(jasmine.getGlobal()).toBe(globalObject);
25 25
     });
26 26
   });
  27
+
  28
+  describe("jasmine.ExpectationResult", function() {
  29
+    var result;
  30
+
  31
+    beforeEach(function() {
  32
+      result = new jasmine.ExpectationResult({
  33
+        passed: true,
  34
+        message: "some message"
  35
+      });
  36
+    });
  37
+
  38
+    describe("#update", function() {
  39
+      it("updates the passing status", function() {
  40
+        result.update({ passed: false });
  41
+        expect(result.passed()).toBeFalsy();
  42
+      });
  43
+
  44
+      describe("when the result is passing", function() {
  45
+        it("sets the message to 'Passed.'", function() {
  46
+          result.update({
  47
+            passed: true,
  48
+            message: "some message"
  49
+          });
  50
+
  51
+          expect(result.message).toBe("Passed.");
  52
+        });
  53
+      });
  54
+
  55
+      describe("when the result is failing", function() {
  56
+        beforeEach(function() {
  57
+          result.update({
  58
+            passed: false,
  59
+            message: "some message"
  60
+          });
  61
+        });
  62
+
  63
+        it("updates the message", function() {
  64
+          expect(result.message).toBe("some message");
  65
+        });
  66
+
  67
+        it("creates a stack trace with the message", function() {
  68
+          expect(result.trace instanceof Error).toBeTruthy();
  69
+          expect(result.trace.message).toBe("some message");
  70
+        });
  71
+      });
  72
+    });
  73
+  });
27 74
 });
588  spec/core/ChainedMatchersSpec.js
... ...
@@ -0,0 +1,588 @@
  1
+describe("Chained matchers", function() {
  2
+
  3
+  describe(".makeChainName(prefix, matcherName)", function() {
  4
+    describe("when there is no prefix", function() {
  5
+      it("returns the matcher name", function() {
  6
+        var chainName1 = jasmine.ChainedMatchers.makeChainName("", "toBeCool");
  7
+        expect(chainName1).toBe("toBeCool");
  8
+      });
  9
+    });
  10
+
  11
+    describe("when there is a prefix", function() {
  12
+      it("adds the matcherName to the prefix, separated by a space", function() {
  13
+        var chainName1 = jasmine.ChainedMatchers.makeChainName("toBeBetween", "and");
  14
+        var chainName2 = jasmine.ChainedMatchers.makeChainName("toHaveA between", "and");
  15
+        expect(chainName1).toBe("toBeBetween and");
  16
+        expect(chainName2).toBe("toHaveA between and");
  17
+      });
  18
+    });
  19
+  });
  20
+
  21
+  describe(".parseMatchers(matchersHash)", function() {
  22
+    var matchersHash, parsedMatchers;
  23
+
  24
+    beforeEach(function() {
  25
+      matchersHash = {
  26
+        "toHaveA":                                function() {},
  27
+        "toHaveA ofExactly":                      function() {},
  28
+        "toHaveA withA":                          function() {},
  29
+        "toHaveA withA ofExactly":                function() {},
  30
+        "toHaveA withA ofAtLeast":                function() {},
  31
+        "toHaveBeenCalled after":                 function() {},
  32
+        "toHaveBeenCalled before":                function() {},
  33
+        "toHaveBeenCalled atLeast times":         function() {},
  34
+        "toHaveBeenCalled atLeast secondsBefore": function() {},
  35
+        "toHaveBeenCalled atLeast secondsAfter":  function() {}
  36
+      };
  37
+
  38
+      parsedMatchers = jasmine.ChainedMatchers.parseMatchers(matchersHash);
  39
+    });
  40
+
  41
+    it("has two keys: 'topLevel' and 'chained', both of which are objects", function() {
  42
+      expect(parsedMatchers).toEqual({
  43
+        topLevel: jasmine.any(Object),
  44
+        chained:  jasmine.any(Object)
  45
+      });
  46
+    });
  47
+
  48
+    it("puts matchers with single-word keys into the 'topLevel' object", function() {
  49
+      expect(parsedMatchers.topLevel).toEqual({
  50
+        toHaveA: matchersHash.toHaveA,
  51
+        toHaveBeenCalled: matchersHash.toHaveBeenCalled,
  52
+      });
  53
+    });
  54
+
  55
+    it("groups the remaining methods by their prefix, in the 'chained' object", function() {
  56
+      expect(parsedMatchers.chained).toEqual({
  57
+        "toHaveA": {
  58
+          ofExactly: matchersHash["toHaveA ofExactly"],
  59
+          withA:     matchersHash["toHaveA withA"]
  60
+        },
  61
+
  62
+        "toHaveA withA": {
  63
+          ofExactly: matchersHash["toHaveA withA ofExactly"],
  64
+          ofAtLeast: matchersHash["toHaveA withA ofAtLeast"]
  65
+        },
  66
+
  67
+        "toHaveBeenCalled": {
  68
+          after:  matchersHash["toHaveBeenCalled after"],
  69
+          before: matchersHash["toHaveBeenCalled before"]
  70
+        },
  71
+
  72
+        "toHaveBeenCalled atLeast": {
  73
+          times:         matchersHash["toHaveBeenCalled atLeast times"],
  74
+          secondsAfter:  matchersHash["toHaveBeenCalled atLeast secondsAfter"],
  75
+          secondsBefore: matchersHash["toHaveBeenCalled atLeast secondsBefore"]
  76
+        }
  77
+      });
  78
+    });
  79
+
  80
+    it("handles key names containing '$' and '_'", function() {
  81
+      matchersHash = {
  82
+        "to_be_in$ane": function() {},
  83
+        "to_be_in$ane and_awe$ome": function() {},
  84
+        "to_be_in$ane and_a$tounding": function() {},
  85
+        "to_be_in$ane and_a$tounding beyond_compare": function() {},
  86
+      };
  87
+
  88
+      expect(jasmine.ChainedMatchers.parseMatchers(matchersHash)).toEqual({
  89
+        topLevel: {
  90
+          to_be_in$ane: matchersHash["to_be_in$ane"]
  91
+        },
  92
+
  93
+        chained: {
  94
+          "to_be_in$ane": {
  95
+            and_awe$ome:    matchersHash["to_be_in$ane and_awe$ome"],
  96
+            and_a$tounding: matchersHash["to_be_in$ane and_a$tounding"]
  97
+          },
  98
+
  99
+          "to_be_in$ane and_a$tounding": {
  100
+            beyond_compare: matchersHash["to_be_in$ane and_a$tounding beyond_compare"]
  101
+          }
  102
+        }
  103
+      });
  104
+    });
  105
+  });
  106
+
  107
+  describe("Spec#addMatchers", function() {
  108
+    var env, suite;
  109
+
  110
+    beforeEach(function() {
  111
+      env = new jasmine.Env();
  112
+      env.updateInterval = 0;
  113
+      suite = env.describe("suite", function() {});
  114
+      env.currentSuite = suite;
  115
+    });
  116
+
  117
+    describe("with a matchers object whose keys contain multiple matcher names", function() {
  118
+      beforeEach(function() {
  119
+        env.beforeEach(function() {
  120
+          this.addMatchers({
  121
+            'toHaveA': function(key) {
  122
+              this.valueToCompare = this.actual[key];
  123
+              return !!this.valueToCompare;
  124
+            },
  125
+
  126
+            'toHaveA ofExactly': function(value) {
  127
+              return this.valueToCompare === value;
  128
+            },
  129
+
  130
+            'toHaveA between': function(lowerBound) {
  131
+              return this.valueToCompare >= lowerBound;
  132
+            },
  133
+
  134
+            'toHaveA between and': function(upperBound) {
  135
+              return this.valueToCompare <= upperBound;
  136
+            }
  137
+          });
  138
+        });
  139
+      });
  140
+
  141
+      itCreatesMatcherMethodsCorrectly();
  142
+    });
  143
+
  144
+    describe("with a matcher name string and a matchers object", function() {
  145
+      beforeEach(function() {
  146
+        env.beforeEach(function() {
  147
+          this.addMatchers({
  148
+            toHaveA: function(key) {
  149
+              this.valueToCompare = this.actual[key];
  150
+              return !!this.valueToCompare;
  151
+            },
  152
+          });
  153
+
  154
+          this.addMatchers('toHaveA', {
  155
+            ofExactly: function(value) {
  156
+              return this.valueToCompare === value;
  157
+            },
  158
+
  159
+            between: function(lowerBound) {
  160
+              return this.valueToCompare >= lowerBound;
  161
+            }
  162
+          });
  163
+
  164
+          this.addMatchers('toHaveA between', {
  165
+            and: function(upperBound) {
  166
+              return this.valueToCompare <= upperBound;
  167
+            }
  168
+          })
  169
+        });
  170
+      });
  171
+
  172
+      itCreatesMatcherMethodsCorrectly();
  173
+    });
  174
+
  175
+    describe("with an array of matcher name strings and a matchers object", function() {
  176
+      beforeEach(function() {
  177
+        env.beforeEach(function() {
  178
+          this.addMatchers({
  179
+            toHaveA: function(key) {
  180
+              this.valueToCompare = this.actual[key];
  181
+              return !!this.valueToCompare;
  182
+            },
  183
+
  184
+            'toHaveA withA': function(key) {
  185
+              this.valueToCompare = this.valueToCompare[key];
  186
+              return !!this.valueToCompare;
  187
+            }
  188
+          });
  189
+
  190
+          this.addMatchers(["toHaveA", "toHaveA withA"], {
  191
+            ofExactly: function(value) {
  192
+              return this.valueToCompare === value;
  193
+            },
  194
+
  195
+            between: function(lowerBound) {
  196
+              return this.valueToCompare >= lowerBound;
  197
+            },
  198
+
  199
+            'between and': function(upperBound) {
  200
+              return this.valueToCompare <= upperBound;
  201
+            }
  202
+          });
  203
+        });
  204
+      });
  205
+
  206
+      itCreatesMatcherMethodsCorrectly();
  207
+
  208
+      it("adds the given matchers to ALL of the named matcher classes", function() {
  209
+        var passingResults = resultsOfSpec(function() {
  210
+          this.expect({ triangle: { height: 12 } }).toHaveA("triangle");
  211
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("square");
  212
+          this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height");
  213
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("width");
  214
+          this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").ofExactly(12);
  215
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").ofExactly(24);
  216
+          this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").between(10).and(20);
  217
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").between(1).and(10);
  218
+        });
  219
+        var passingItems = passingResults.getItems();
  220
+
  221
+        expect(passingItems.length).toBe(8);
  222
+        expect(passingResults.passedCount).toBe(8);
  223
+        expect(passingResults.failedCount).toBe(0);
  224
+
  225
+        expect(passingItems[0].passed()).toBeTruthy();
  226
+        expect(passingItems[1].passed()).toBeTruthy();
  227
+        expect(passingItems[2].passed()).toBeTruthy();
  228
+        expect(passingItems[3].passed()).toBeTruthy();
  229
+        expect(passingItems[4].passed()).toBeTruthy();
  230
+        expect(passingItems[5].passed()).toBeTruthy();
  231
+        expect(passingItems[6].passed()).toBeTruthy();
  232
+        expect(passingItems[7].passed()).toBeTruthy();
  233
+
  234
+        var failingResults = resultsOfSpec(function() {
  235
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle");
  236
+          this.expect({ triangle: { height: 12 } }).toHaveA("square");
  237
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height");
  238
+          this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("width");
  239
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").ofExactly(12);
  240
+          this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").ofExactly(24);
  241
+          this.expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").between(10).and(20);
  242
+          this.expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").between(1).and(10);
  243
+        });
  244
+        var failingItems = failingResults.getItems();
  245
+
  246
+        expect(failingItems.length).toBe(8);
  247
+        expect(failingResults.passedCount).toBe(0);
  248
+        expect(failingResults.failedCount).toBe(8);
  249
+
  250
+        expect(failingItems[0].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle'.");
  251
+        expect(failingItems[1].message).toBe("Expected { triangle : { height : 12 } } to have a 'square'.");
  252
+        expect(failingItems[2].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle' with a 'height'.");
  253
+        expect(failingItems[3].message).toBe("Expected { triangle : { height : 12 } } to have a 'triangle' with a 'width'.");
  254
+        expect(failingItems[4].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle' with a 'height' of exactly 12.");
  255
+        expect(failingItems[5].message).toBe("Expected { triangle : { height : 12 } } to have a 'triangle' with a 'height' of exactly 24.");
  256
+        expect(failingItems[6].message).toBe("Expected { triangle : { height : 12 } } not to have a 'triangle' with a 'height' between 10 and 20.");
  257
+        expect(failingItems[7].message).toBe("Expected { triangle : { height : 12 } } to have a 'triangle' with a 'height' between 1 and 10.");
  258
+      });
  259
+    });
  260
+
  261
+    describe("when some of the matchers define custom messages", function() {
  262
+      beforeEach(function() {
  263
+        env.beforeEach(function() {
  264
+          this.addMatchers({
  265
+            toHaveA: function(key) {
  266
+              this.valueToCompare = this.actual[key];
  267
+              return !!this.valueToCompare;
  268
+            },
  269
+
  270
+            toHaveASpecial: function(key) {
  271
+              this.message = function() { return ["message for toHaveASpecial", "message for not toHaveASpecial"]; };
  272
+              this.valueToCompare = this.actual[key];
  273
+              return !!this.valueToCompare;
  274
+            }
  275
+          });
  276
+
  277
+          this.addMatchers(["toHaveA", "toHaveASpecial"], {
  278
+            withA: function(key) {
  279
+              this.valueToCompare = this.valueToCompare[key];
  280
+              return !!this.valueToCompare;
  281
+            },
  282
+
  283
+            withASpecial: function(key) {
  284
+              this.message = function() { return ["message for withASpecial", "message for not withASpecial"]; };
  285
+              this.valueToCompare = this.valueToCompare[key];
  286
+              return !!this.valueToCompare;
  287
+            }
  288
+          });
  289
+
  290
+          this.addMatchers([ "toHaveA withA", "toHaveA withASpecial", "toHaveASpecial withA", "toHaveASpecial withASpecial" ], {
  291
+            thatIs: function(value) {
  292
+              return this.valueToCompare === value;
  293
+            },
  294
+
  295
+            thatIsEspecially: function(value) {
  296
+              this.message = function() { return ["message for thatIsEspecially", "message for not thatIsEspecially"]; };
  297
+              return this.valueToCompare === value;
  298
+            }
  299
+          });
  300
+        });
  301
+      });
  302
+
  303
+      describe("when the last matcher in a chain has a custom message", function() {
  304
+        it("uses the custom message", function() {
  305
+          var results = resultsOfSpec(function() {
  306
+            this.expect({ song: { melody: 'sad' } }).toHaveA("song").withASpecial('harmony');
  307
+            this.expect({ song: { melody: 'sad' } }).not.toHaveA("song").withASpecial('melody');
  308
+            this.expect({ song: { melody: 'sad' } }).toHaveA("song").withA('melody').thatIsEspecially('happy');
  309
+            this.expect({ song: { melody: 'sad' } }).not.toHaveA("song").withA('melody').thatIsEspecially('sad');
  310
+          });
  311
+          var items = results.getItems();
  312
+
  313
+          expect(items.length).toBe(4);
  314
+          expect(results.passedCount).toBe(0);
  315
+          expect(results.failedCount).toBe(4);
  316
+
  317
+          expect(items[0].message).toBe("message for withASpecial");
  318
+          expect(items[1].message).toBe("message for not withASpecial");
  319
+          expect(items[2].message).toBe("message for thatIsEspecially");
  320
+          expect(items[3].message).toBe("message for not thatIsEspecially");
  321
+        });
  322
+      });
  323
+
  324
+      describe("when the last matcher in the chain does not have a custom message", function() {
  325
+        it("uses a message based on the chain of matcher names", function() {
  326
+          var results = resultsOfSpec(function() {
  327
+            this.expect({ song: { melody: 'sad' } }).toHaveASpecial("song").withA('harmony');
  328
+            this.expect({ song: { melody: 'sad' } }).not.toHaveASpecial("song").withA('melody');
  329
+            this.expect({ song: { melody: 'sad' } }).toHaveASpecial("song").withASpecial('melody').thatIs("happy");
  330
+            this.expect({ song: { melody: 'sad' } }).not.toHaveASpecial("song").withASpecial('melody').thatIs("sad");
  331
+          });
  332
+          var items = results.getItems();
  333
+
  334
+          expect(items.length).toBe(4);
  335
+          expect(results.passedCount).toBe(0);
  336
+          expect(results.failedCount).toBe(4);
  337
+
  338
+          expect(items.length).toBe(4);
  339
+          expect(items[0].message).toBe("Expected { song : { melody : 'sad' } } to have a special 'song' with a 'harmony'.");
  340
+          expect(items[1].message).toBe("Expected { song : { melody : 'sad' } } not to have a special 'song' with a 'melody'.");
  341
+          expect(items[2].message).toBe("Expected { song : { melody : 'sad' } } to have a special 'song' with a special 'melody' that is 'happy'.");
  342
+          expect(items[3].message).toBe("Expected { song : { melody : 'sad' } } not to have a special 'song' with a special 'melody' that is 'sad'.");
  343
+        });
  344
+      });
  345
+    });
  346
+
  347
+    it("works in real life", function() {
  348
+      this.addMatchers({
  349
+        'toHaveA': function(key) {
  350
+          this.valueToCompare = this.actual[key];
  351
+          return !!this.valueToCompare;
  352
+        },
  353
+
  354
+        'toHaveA withA': function(key) {
  355
+          this.valueToCompare = this.valueToCompare[key];
  356
+          return !!this.valueToCompare;
  357
+        }
  358
+      });
  359
+
  360
+      this.addMatchers(["toHaveA", "toHaveA withA"], {
  361
+        'ofExactly': function(value) {
  362
+          return this.valueToCompare === value;
  363
+        },
  364
+
  365
+        'between': function(lowerBound) {
  366
+          return this.valueToCompare >= lowerBound;
  367
+        },
  368
+
  369
+        'between and': function(upperBound) {
  370
+          return this.valueToCompare <= upperBound;
  371
+        }
  372
+      });
  373
+
  374
+      expect({ height: 12 }).toHaveA("height");
  375
+      expect({ height: 12 }).not.toHaveA("width");
  376
+      expect({ height: 12 }).toHaveA("height").ofExactly(12);
  377
+      expect({ height: 12 }).not.toHaveA("height").ofExactly(20);
  378
+      expect({ height: 12 }).toHaveA("height").between(10).and(20);
  379
+      expect({ height: 12 }).not.toHaveA("height").between(1).and(10);
  380
+
  381
+      expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height");
  382
+      expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("width");
  383
+      expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").ofExactly(12);
  384
+      expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").ofExactly(24);
  385
+      expect({ triangle: { height: 12 } }).toHaveA("triangle").withA("height").between(10).and(20);
  386
+      expect({ triangle: { height: 12 } }).not.toHaveA("triangle").withA("height").between(1).and(10);
  387
+    });
  388
+
  389
+    function itCreatesMatcherMethodsCorrectly() {
  390
+      describe("the return value of a matcher function", function() {
  391
+        describe("when no further chained matchers have been added", function() {
  392
+          var unchainableMatcherValue1, unchainableMatcherValue2, unchainableMatcherValue3;
  393
+
  394
+          beforeEach(function() {
  395
+            env.it("spec", function() {
  396
+              unchainableMatcherValue1 = this.expect({ height: 12 }).toBeTruthy();
  397
+              unchainableMatcherValue2 = this.expect({ height: 12 }).toHaveA('height').ofExactly(10);
  398
+              unchainableMatcherValue3 = this.expect({ height: 12 }).toHaveA('height').between(10).and(20);
  399
+            });
  400
+
  401
+            suite.execute();
  402
+          });
  403
+
  404
+          it("is undefined", function() {
  405
+            expect(unchainableMatcherValue1).toBeUndefined();
  406
+            expect(unchainableMatcherValue2).toBeUndefined();
  407
+            expect(unchainableMatcherValue3).toBeUndefined();
  408
+          });
  409
+        });
  410
+
  411
+        describe("when further chained matchers have been added", function() {
  412
+          var expectValue, chainableMatcherValue1, chainableMatcherValue2;
  413
+
  414
+          beforeEach(function() {
  415
+            env.it("spec", function() {
  416
+              expectValue              = this.expect({ height: 12 });
  417
+              chainableMatcherValue1   = this.expect({ height: 12 }).toHaveA('height');
  418
+              chainableMatcherValue2   = this.expect({ height: 12 }).toHaveA('height').between(10);
  419
+            });
  420
+
  421
+            suite.execute();
  422
+          });
  423
+
  424
+          it("has methods for each of the chained matchers", function() {
  425
+            expect(typeof chainableMatcherValue1.ofExactly).toBe("function");
  426
+            expect(typeof chainableMatcherValue1.between).toBe("function");
  427
+            expect(typeof chainableMatcherValue2.and).toBe("function");
  428
+          });
  429
+
  430
+          it("does not have the same methods as other matchers", function() {
  431
+            expect(chainableMatcherValue2.ofExactly).toBeUndefined();
  432
+            expect(chainableMatcherValue2.between).toBeUndefined();
  433
+          });
  434
+
  435
+          it("does not have the normal top-level matcher methods", function() {
  436
+            expect(chainableMatcherValue1.toBe).toBeUndefined();
  437
+            expect(chainableMatcherValue2.toBe).toBeUndefined();
  438
+            expect(chainableMatcherValue1.toEqual).toBeUndefined();
  439
+            expect(chainableMatcherValue2.toEqual).toBeUndefined();
  440
+          });
  441
+
  442
+          it("has the same env and spec as the parent matcher object", function() {
  443
+            expect(chainableMatcherValue1.env).toBe(expectValue.env);
  444
+            expect(chainableMatcherValue2.env).toBe(expectValue.env);
  445
+            expect(chainableMatcherValue1.spec).toBe(expectValue.spec);
  446
+            expect(chainableMatcherValue2.spec).toBe(expectValue.spec);
  447
+          });
  448
+
  449
+          it("does NOT have a 'not' property (nots are only allowed at the beginning of a matcher chain)", function() {
  450
+            expect(chainableMatcherValue1['not']).toBeUndefined();
  451
+            expect(chainableMatcherValue2['not']).toBeUndefined();
  452
+          });
  453
+
  454
+          it("keeps any properties that are set on 'this' by earlier matcher functions", function() {
  455
+            expect(chainableMatcherValue1.valueToCompare).toBe(12);
  456
+            expect(chainableMatcherValue1.valueToCompare).toBe(12);
  457
+          });
  458
+        });
  459
+      });
  460
+
  461
+      describe("when matchers are chained without a 'not'", function() {
  462
+        var results, items;
  463
+
  464
+        describe("when any of the matchers in the chain do NOT match", function() {
  465
+          beforeEach(function() {
  466
+            results = resultsOfSpec(function() {
  467
+              this.expect({ height: 3 }).toHaveA("width").ofExactly(3);
  468
+              this.expect({ height: 3 }).toHaveA("height").ofExactly(5);
  469
+              this.expect({ height: 3 }).toHaveA("height").between(1).and(2);
  470
+            });
  471
+            items = results.getItems();
  472
+          });
  473
+
  474
+          it("adds one failure to the spec's results", function() {
  475
+            expect(items.length).toBe(3);
  476
+            expect(results.passedCount).toBe(0);
  477
+            expect(results.failedCount).toBe(3);
  478
+
  479
+            expect(items[0].passed()).toBeFalsy();
  480
+            expect(items[1].passed()).toBeFalsy();
  481
+            expect(items[2].passed()).toBeFalsy();
  482
+          });
  483
+
  484
+          it("builds a failure message from the complete chain of matchers", function() {
  485
+            expect(items[0].message).toBe("Expected { height : 3 } to have a 'width' of exactly 3.");
  486
+            expect(items[1].message).toBe("Expected { height : 3 } to have a 'height' of exactly 5.");
  487
+            expect(items[2].message).toBe("Expected { height : 3 } to have a 'height' between 1 and 2.");
  488
+          });
  489
+
  490
+          it("builds a trace with the right message", function() {
  491
+            expect(items[0].trace instanceof Error).toBeTruthy();
  492
+            expect(items[1].trace instanceof Error).toBeTruthy();
  493
+            expect(items[2].trace instanceof Error).toBeTruthy();
  494
+
  495
+            expect(items[0].trace.message).toBe(items[0].message);
  496
+            expect(items[1].trace.message).toBe(items[1].message);
  497
+            expect(items[2].trace.message).toBe(items[2].message);
  498
+          });
  499
+        });
  500
+
  501
+        describe("when all of the matchers match", function() {
  502
+          it("adds one success to the spec's results", function() {
  503
+            results = resultsOfSpec(function() {
  504
+              this.expect({ height: 3 }).toHaveA("height").ofExactly(3);
  505
+              this.expect({ height: 3 }).toHaveA("height").between(2).and(5);
  506
+            });
  507
+            items = results.getItems();
  508
+
  509
+            expect(items.length).toBe(2);
  510
+            expect(results.passedCount).toBe(2);
  511
+            expect(results.failedCount).toBe(0);
  512
+
  513
+            expect(items[0].passed()).toBeTruthy();
  514
+            expect(items[1].passed()).toBeTruthy();
  515
+            expect(items[0].message).toBe("Passed.");
  516
+            expect(items[1].message).toBe("Passed.");
  517
+          });
  518
+        });
  519
+      });
  520
+
  521
+      describe("when matchers are chained, starting with a 'not'", function() {
  522
+        var results, items;
  523
+
  524
+        describe("when any of the matchers in the chain do NOT match", function() {
  525
+          it("adds a single success to the spec's results", function() {
  526
+            results = resultsOfSpec(function() {
  527
+              this.expect({ height: 3 }).not.toHaveA("width").ofExactly(3);
  528
+              this.expect({ height: 3 }).not.toHaveA("height").ofExactly(5);
  529
+              this.expect({ height: 3 }).not.toHaveA("height").between(5).and(10);
  530
+              this.expect({ height: 3 }).not.toHaveA("height").between(10).and(20);
  531
+            });
  532
+            items = results.getItems();
  533
+
  534
+            expect(results.passedCount).toBe(4);
  535
+            expect(items.length).toBe(4);
  536
+
  537
+            expect(items[0].passed()).toBeTruthy();
  538
+            expect(items[1].passed()).toBeTruthy();
  539
+            expect(items[2].passed()).toBeTruthy();
  540
+            expect(items[3].passed()).toBeTruthy();
  541
+            expect(items[0].message).toBe("Passed.");
  542
+            expect(items[1].message).toBe("Passed.");
  543
+            expect(items[2].message).toBe("Passed.");
  544
+            expect(items[3].message).toBe("Passed.");
  545
+          });
  546
+        });
  547
+
  548
+        describe("when all of the matchers match", function() {
  549
+          beforeEach(function() {
  550
+            results = resultsOfSpec(function() {
  551
+              this.expect({ height: 3 }).not.toHaveA("height").ofExactly(3);
  552
+              this.expect({ height: 3 }).not.toHaveA("height").between(2).and(4);
  553
+            });
  554
+            items = results.getItems();
  555
+          });
  556
+
  557
+          it("adds a single failure to the spec's results", function() {
  558
+            expect(items.length).toBe(2);
  559
+            expect(results.passedCount).toBe(0);
  560
+            expect(results.failedCount).toBe(2);
  561
+
  562
+            expect(items[0].passed()).toBeFalsy();
  563
+            expect(items[1].passed()).toBeFalsy();
  564
+          });
  565
+
  566
+          it("builds a failure message from the complete chain of matchers", function() {
  567
+            expect(items[0].message).toBe("Expected { height : 3 } not to have a 'height' of exactly 3.");
  568
+            expect(items[1].message).toBe("Expected { height : 3 } not to have a 'height' between 2 and 4.");
  569
+          });
  570
+
  571
+          it("builds a trace with the right message", function() {
  572
+            expect(items[0].trace instanceof Error).toBeTruthy();
  573
+            expect(items[1].trace instanceof Error).toBeTruthy();
  574
+
  575
+            expect(items[0].trace.message).toBe(items[0].message);
  576
+            expect(items[1].trace.message).toBe(items[1].message);
  577
+          });
  578
+        });
  579
+      });
  580
+    }
  581
+
  582
+    function resultsOfSpec(specFunction) {
  583
+      var spec = env.it("spec", specFunction);
  584
+      suite.execute();
  585
+      return spec.results();
  586
+    }
  587
+  });
  588
+});
52  spec/core/NestedResultsSpec.js
@@ -51,4 +51,56 @@ describe('jasmine.NestedResults', function() {
51 51
     expect(branchResults.failedCount).toEqual(2);
52 52
   });
53 53
 
  54
+  describe("#updateResult", function() {
  55
+    var results, result1, result2;
  56
+
  57
+    beforeEach(function() {
  58
+      results = new jasmine.NestedResults();
  59
+      result1 = new jasmine.ExpectationResult({
  60
+        passed: true,
  61
+        message: "Passed."
  62
+      });
  63
+      result2 = new jasmine.ExpectationResult({
  64
+        passed: false,
  65
+        message: "fail."
  66
+      });
  67
+
  68
+      results.addResult(result1);
  69
+      results.addResult(result2);
  70
+    });
  71
+
  72
+    describe("when a result that was passing is updated to fail", function() {
  73
+      beforeEach(function() {
  74
+        results.updateResult(result1, { passed: false, message: "nope. failed." });
  75
+      });
  76
+
  77
+      it("increments the failed count and decrements the passed count", function() {
  78
+        expect(results.totalCount).toEqual(2);
  79
+        expect(results.passedCount).toEqual(0);
  80
+        expect(results.failedCount).toEqual(2);
  81
+      });
  82
+
  83
+      it("updates the message and passing status of the result", function() {
  84
+        expect(result1.passed()).toBeFalsy();
  85
+        expect(result1.message).toBe("nope. failed.");
  86
+      });
  87
+    });
  88
+
  89
+    describe("when a result that was failing is updated to pass", function() {
  90
+      beforeEach(function() {
  91
+        results.updateResult(result2, { passed: true });
  92
+      });
  93
+
  94
+      it("increments the failed count and decrements the passed count", function() {
  95
+        expect(results.totalCount).toEqual(2);
  96
+        expect(results.passedCount).toEqual(2);
  97
+        expect(results.failedCount).toEqual(0);
  98
+      });
  99
+
  100
+      it("updates the message and passing status of the result", function() {
  101
+        expect(result2.passed()).toBeTruthy();
  102
+        expect(result2.message).toBe("Passed.");
  103
+      });
  104
+    });
  105
+  });
54 106
 });
47  spec/core/UtilSpec.js
@@ -36,4 +36,51 @@ describe("jasmine.util", function() {
36 36
       expect(jasmine.isArray_(null)).toBe(false);
37 37
     });
38 38
   });
  39
+
  40
+  describe("inherit(childClass, parentClass)", function() {
  41
+    var ParentClass, ChildClass, childInstance;
  42
+
  43
+    beforeEach(function() {
  44
+      ParentClass = function() {};
  45
+      ChildClass  = function() {};
  46
+      jasmine.util.inherit(ChildClass, ParentClass);
  47
+      childInstance = new ChildClass();
  48
+    });
  49
+
  50
+    it("sets the given child class's prototype to be an instance of the parent class", function() {
  51
+      expect(ChildClass.prototype instanceof ParentClass).toBeTruthy();
  52
+      expect(childInstance instanceof ChildClass).toBeTruthy();
  53
+    });
  54
+
  55
+    it("sets the 'constructor' property correctly on the prototype", function() {
  56
+      expect(childInstance.constructor).toBe(ChildClass);
  57
+    });
  58
+  });
  59
+
  60
+  describe("subclass(parentClass)", function() {
  61
+    var ParentClass, ChildClass, childInstance;
  62
+
  63
+    beforeEach(function() {
  64
+      ParentClass = function(param) { this.parentParam = param };
  65
+      ChildClass = jasmine.util.subclass(ParentClass);
  66
+      childInstance = new ChildClass("foo");
  67
+    });
  68
+
  69
+    it("returns a constructor function", function() {
  70
+      expect(typeof ChildClass).toBe("function");
  71
+    });
  72
+
  73
+    it("sets the constructor's prototype correctly", function() {
  74
+      expect(childInstance instanceof ChildClass).toBeTruthy();
  75
+      expect(childInstance instanceof ParentClass).toBeTruthy();
  76
+    });
  77
+
  78
+    it("sets the 'constructor' property correctly", function() {
  79
+      expect(childInstance.constructor).toBe(ChildClass);
  80
+    });
  81
+
  82
+    it("sets up the new constructor function to call the parent constructor", function() {
  83
+      expect(childInstance.parentParam).toBe("foo");
  84
+    });
  85
+  });
39 86
 });
44  src/core/ChainedMatchers.js
... ...
@@ -0,0 +1,44 @@
  1
+/**
  2
+ * @constructor
  3
+ * @param {Object} params
  4
+ */
  5
+jasmine.ChainedMatchers = function(params) {
  6
+  var precedingMatcher = params.precedingMatcher;
  7
+  for (var key in precedingMatcher) {
  8
+    if (key === "not") continue;
  9
+    if (key === "message") continue;
  10
+    if (precedingMatcher.hasOwnProperty(key)) {
  11
+      this[key] = precedingMatcher[key]
  12
+    }
  13
+  }
  14
+  this.precedingResult  = params.precedingResult;
  15
+  this.precedingMessage = params.precedingMessage;
  16
+};
  17
+
  18
+jasmine.ChainedMatchers.makeChainName = function(/* chain names, matcher names */) {
  19
+  var i, name, names = [];
  20
+  for (i = 0; i < arguments.length; i++) {
  21
+    name = arguments[i];
  22
+    if (name) names.push(name);
  23
+  }
  24
+  return names.join(" ");
  25
+};
  26
+
  27
+jasmine.ChainedMatchers.parseMatchers = function(matcherDefinitions) {
  28
+  var chained = {}, topLevel = {},
  29
+      names, prefix, matcherName, method;
  30
+  for (var key in matcherDefinitions) {
  31
+    method = matcherDefinitions[key];
  32
+    names  = key.match(/[\w$]+/g);
  33
+    prefix = names.slice(0, names.length - 1).join(" ");
  34
+    matcherName = names.slice(names.length - 1)[0];
  35
+    if (prefix.length > 0) {
  36
+      chained[prefix] || (chained[prefix] = {});
  37
+      chained[prefix][matcherName] = method;
  38
+    } else {
  39
+      topLevel[matcherName] = method;
  40
+    }
  41
+  }
  42
+  return { topLevel: topLevel, chained: chained };
  43
+};
  44
+
7  src/core/Env.js
@@ -21,12 +21,7 @@ jasmine.Env = function() {
21 21
   this.nextSuiteId_ = 0;
22 22
   this.equalityTesters_ = [];
23 23
 
24  
-  // wrap matchers
25  
-  this.matchersClass = function() {
26  
-    jasmine.Matchers.apply(this, arguments);
27  
-  };
28  
-  jasmine.util.inherit(this.matchersClass, jasmine.Matchers);
29  
-
  24
+  this.matchersClass = jasmine.util.subclass(jasmine.Matchers);
30 25
   jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);
31 26
 };
32 27
 
107  src/core/Matchers.js
@@ -30,48 +30,85 @@ jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {
30 30
   }
31 31
 };
32 32
 
33  
-jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
34  
-  return function() {
35  
-    var matcherArgs = jasmine.util.argsToArray(arguments);
36  
-    var result = matcherFunction.apply(this, arguments);
  33
+;(function() {
  34
+  jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {
  35
+    return function() {
  36
+      var matcherArgs = jasmine.util.argsToArray(arguments);
  37
+      var result = matcherFunction.apply(this, arguments);
  38
+      var passed = isPassing.call(this, result);
  39
+      var defaultMessage = makeDefaultMessage.call(this, matcherName, matcherArgs);
  40
+      var customMessage = makeCustomMessage.call(this, passed);
  41
+      if (this.reportWasCalled_) return passed;
  42
+
  43
+      var expectationResult;
  44
+      if (this.precedingResult) {
  45
+        expectationResult = this.precedingResult;
  46
+        this.spec.updateMatcherResult(expectationResult, {
  47
+          passed: passed,
  48
+          message: customMessage || defaultMessage
  49
+        });
  50
+      } else {
  51
+        expectationResult = new jasmine.ExpectationResult({
  52
+          passed: passed,
  53
+          message: customMessage || defaultMessage,
  54
+          matcherName: matcherName,
  55
+          expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
  56
+          actual: this.actual
  57
+        });
  58
+        this.spec.addMatcherResult(expectationResult);
  59
+      }
37 60
 
38  
-    if (this.isNot) {
39  
-      result = !result;
40  
-    }
  61
+      var nextChainName = jasmine.ChainedMatchers.makeChainName(this.constructor.chainName, matcherName);
  62
+      var chainedMatchersClass = this.spec.chainedMatchersClasses[nextChainName];
  63
+      if (chainedMatchersClass) {
  64
+        return new chainedMatchersClass({
  65
+          precedingMatcher: this,
  66
+          precedingResult:  expectationResult,
  67
+          precedingMessage: defaultMessage
  68
+        });
  69
+      } else {
  70
+        return jasmine.undefined;
  71
+      }
  72
+    };
  73
+  };
41 74
 
42  
-    if (this.reportWasCalled_) return result;
  75
+  function makeCustomMessage(passed) {
  76
+    if (this.message && !passed) {
  77
+      var customMessage = this.message.apply(this, arguments);
  78
+      if (jasmine.isArray_(customMessage)) customMessage = customMessage[this.isNot ? 1 : 0];
  79
+      return customMessage;
  80
+    }
  81
+  }
43 82
 
  83
+  function makeDefaultMessage(matcherName, matcherArgs) {
44 84
     var message;
45  
-    if (!result) {
46  
-      if (this.message) {
47  
-        message = this.message.apply(this, arguments);
48  
-        if (jasmine.isArray_(message)) {
49  
-          message = message[this.isNot ? 1 : 0];
50  
-        }
  85
+    if (this.precedingMessage) {
  86
+      message = this.precedingMessage.replace(/\.$/, " ");
  87
+    } else {
  88
+      message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ");
  89
+    }
  90
+    message += matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
  91
+    for (var i = 0; i < matcherArgs.length; i++) {
  92
+      if (i > 0) message += ",";
  93
+      message += " " + jasmine.pp(matcherArgs[i]);
  94
+    }
  95
+    message += ".";
  96
+    return message;
  97
+  }
  98
+
  99
+  function isPassing(result) {
  100
+    var thisPassed = this.isNot ? !result : result;
  101
+    if (this.precedingResult) {
  102
+      if (this.isNot) {
  103
+        return this.precedingResult.passed() || thisPassed;
51 104
       } else {
52  
-        var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
53  
-        message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate;
54  
-        if (matcherArgs.length > 0) {
55  
-          for (var i = 0; i < matcherArgs.length; i++) {
56  
-            if (i > 0) message += ",";
57  
-            message += " " + jasmine.pp(matcherArgs[i]);
58  
-          }
59  
-        }
60  
-        message += ".";
  105
+        return this.precedingResult.passed() && thisPassed;
61 106
       }
  107
+    } else {
  108
+      return thisPassed;
62 109
     }
63  
-    var expectationResult = new jasmine.ExpectationResult({
64  
-      matcherName: matcherName,
65  
-      passed: result,
66  
-      expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],
67  
-      actual: this.actual,
68  
-      message: message
69  
-    });
70  
-    this.spec.addMatcherResult(expectationResult);
71  
-    return jasmine.undefined;
72  
-  };
73  
-};
74  
-
  110
+  }
  111
+})();
75 112
 
76 113
 
77 114
 
18  src/core/NestedResults.js
@@ -73,6 +73,24 @@ jasmine.NestedResults.prototype.addResult = function(result) {
73 73
 };
74 74
 
75 75
 /**
  76
+ * Updates a result, tracking counts (passed & failed)
  77
+ * @param {jasmine.ExpectationResult} result
  78
+ * @param {Object} params
  79
+ */
  80
+jasmine.NestedResults.prototype.updateResult = function(result, params) {
  81
+  var wasPassing = result.passed_,
  82
+      isPassing = params.passed;
  83
+  if (wasPassing && !isPassing) {
  84
+    this.passedCount--;
  85
+    this.failedCount++;
  86
+  } else if (isPassing && !wasPassing) {
  87
+    this.passedCount++;
  88
+    this.failedCount--;
  89
+  }
  90
+  result.update(params);
  91
+};
  92
+
  93
+/**
76 94
  * @returns {Boolean} True if <b>everything</b> below passed
77 95
  */
78 96
 jasmine.NestedResults.prototype.passed = function() {
54  src/core/Spec.js
@@ -26,6 +26,7 @@ jasmine.Spec = function(env, suite, description) {
26 26
   spec.results_ = new jasmine.NestedResults();
27 27
   spec.results_.description = description;
28 28
   spec.matchersClass = null;
  29
+  spec.chainedMatchersClasses = {};
29 30
 };
30 31
 
31 32
 jasmine.Spec.prototype.getFullName = function() {
@@ -67,6 +68,10 @@ jasmine.Spec.prototype.addMatcherResult = function(result) {
67 68
   this.results_.addResult(result);
68 69
 };
69 70
 
  71
+jasmine.Spec.prototype.updateMatcherResult = function(result, params) {
  72
+  this.results_.updateResult(result, params);
  73
+};
  74
+
70 75
 jasmine.Spec.prototype.expect = function(actual) {
71 76
   var positive = new (this.getMatchersClass_())(this.env, actual, this);
72 77
   positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);
@@ -130,14 +135,49 @@ jasmine.Spec.prototype.getMatchersClass_ = function() {
130 135
   return this.matchersClass || this.env.matchersClass;
131 136
 };
132 137
 
133  
-jasmine.Spec.prototype.addMatchers = function(matchersPrototype) {
134  
-  var parent = this.getMatchersClass_();
135  
-  var newMatchersClass = function() {
136  
-    parent.apply(this, arguments);
137  
-  };
138  
-  jasmine.util.inherit(newMatchersClass, parent);
  138
+jasmine.Spec.prototype.makeMatchersClass_ = function(chainName) {
  139
+  if (chainName) {
  140
+    if (!this.chainedMatchersClasses[chainName]) {
  141
+      this.chainedMatchersClasses[chainName] = jasmine.util.subclass(jasmine.ChainedMatchers);
  142
+      this.chainedMatchersClasses[chainName].chainName = chainName;
  143
+    }
  144
+    return this.chainedMatchersClasses[chainName];
  145
+  } else {
  146
+    if (!this.matchersClass) {
  147
+      this.matchersClass = jasmine.util.subclass(this.env.matchersClass);
  148
+    }
  149
+    return this.matchersClass;
  150
+  }
  151
+};
  152
+
  153
+jasmine.Spec.prototype.addMatchers = function(arg1, arg2) {
  154
+  var matchers, chainPrefixes;
  155
+  if (arg2) {
  156
+    chainPrefixes = jasmine.isArray_(arg1) ? arg1 : [arg1];
  157
+    matchers = arg2;
  158
+  } else {
  159
+    chainPrefixes = [""];
  160
+    matchers = arg1;
  161
+  }
  162
+
  163
+  var parsedMatchers   = jasmine.ChainedMatchers.parseMatchers(matchers),
  164
+      topLevelMatchers = parsedMatchers.topLevel,
  165
+      chainedMatchers  = parsedMatchers.chained;
  166
+
  167
+  var chainPrefix, fullChainName;
  168
+  for (var i = 0; i < chainPrefixes.length; i++) {
  169
+    chainPrefix = chainPrefixes[i];
  170
+    this.addMatchers_(chainPrefix, topLevelMatchers);
  171
+    for (var chainName in chainedMatchers) {
  172
+      fullChainName = jasmine.ChainedMatchers.makeChainName(chainPrefix, chainName);
  173
+      this.addMatchers_(fullChainName, chainedMatchers[chainName]);
  174
+    }
  175
+  }
  176
+};
  177
+
  178
+jasmine.Spec.prototype.addMatchers_ = function(chainName, matchersPrototype) {
  179
+  var newMatchersClass = this.makeMatchersClass_(chainName);
139 180
   jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);
140  
-  this.matchersClass = newMatchersClass;
141 181
 };
142 182
 
143 183
 jasmine.Spec.prototype.finishCallback = function() {
19  src/core/base.js
@@ -94,13 +94,9 @@ jasmine.MessageResult.prototype.toString = function() {
94 94
 jasmine.ExpectationResult = function(params) {
95 95
   this.type = 'expect';
96 96
   this.matcherName = params.matcherName;
97  
-  this.passed_ = params.passed;
98 97
   this.expected = params.expected;
99 98
   this.actual = params.actual;
100  
-  this.message = this.passed_ ? 'Passed.' : params.message;
101  
-
102  
-  var trace = (params.trace || new Error(this.message));
103  
-  this.trace = this.passed_ ? '' : trace;
  99
+  this.update(params);
104 100
 };
105 101
 
106 102
 jasmine.ExpectationResult.prototype.toString = function () {
@@ -111,6 +107,19 @@ jasmine.ExpectationResult.prototype.passed = function () {
111 107
   return this.passed_;
112 108
 };
113 109
 
  110
+jasmine.ExpectationResult.prototype.update = function(params) {
  111
+  this.passed_ = params.passed;
  112
+
  113
+  if (this.passed_) {
  114
+    this.message = "Passed.";
  115
+    this.trace = "";
  116
+  } else {
  117
+    this.message = params.message;
  118
+    this.trace = params.trace || new Error(this.message);
  119
+  }
  120
+};
  121
+
  122
+
114 123
 /**
115 124
  * Getter for the Jasmine environment. Ensures one gets created
116 125
  */
7  src/core/util.js
@@ -18,6 +18,13 @@ jasmine.util.inherit = function(childClass, parentClass) {
18 18
   };
19 19
   subclass.prototype = parentClass.prototype;
20 20
   childClass.prototype = new subclass();
  21
+  childClass.prototype.constructor = childClass;
  22
+};