Permalink
Browse files

extraction of Backbone matchers for Chai plugin and NPM

  • Loading branch information...
1 parent c35e4b5 commit 8aed2b6307026ab24ead311aae914302088cbd62 @matthijsgroen committed Oct 13, 2012
Showing with 411 additions and 2 deletions.
  1. +22 −0 LICENSE
  2. +47 −2 README.md
  3. +101 −0 chai-backbone.js
  4. +140 −0 chai-backbone.js.coffee
  5. +22 −0 package.json
  6. +79 −0 test/chai-backbone_spec.js.coffee
View
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Matthijs Groen
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
49 README.md
@@ -1,4 +1,49 @@
chai-backbone
-=============
+============
+
+chai-backbone is an extension to the [chai](http://chaijs.com/) assertion library that
+provides a set of backbone specific assertions.
+
+Usage
+-----
+
+Include `chai-backbone.js` in your test file, after `chai.js` (version 1.0.0-rc1 or later):
+
+ <script src="chai-backbone.js"></script>
+
+Use the assertions with chai's `expect` or `should` assertions.
+
+Assertions
+----------
+
+### `trigger`
+
+ model.should.trigger("change", with: [model]).when -> model.set attribute: "value"
+
+this can also be chained further:
+
+ model.should.trigger("change").and.trigger("change:attribute").when -> model.set attribute: "value"
+ model.should.trigger("change").and.not.trigger("reset").when -> model.set attribute: "value"
+
+### `route.to`
+
+Tests if a route is delegated to the correct router and if the arguments
+are extracted in the expected manner.
+
+ "page/3".should.route.to myRouter, "openPage", arguments: ["3"]
+ "page/3".should.route.to myRouter, "openPage", considering: [conflictingRouter]
+
+### `call`
+
+This assertion is ideal for testing view callbacks it will rebind view
+events to test DOM events
+
+ view.should.call('startAuthentication').when ->
+ view.$('a.login').trigger 'click'
+
+## License
+
+Copyright (c) 2012 Matthijs Groen
+
+MIT License (see the LICENSE file)
-Chai Assertion Matchers for Backbone.js
View
101 chai-backbone.js
@@ -0,0 +1,101 @@
+(function() {
+
+ (function(chaiBackbone) {
+ if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
+ return module.exports = chaiBackbone;
+ } else if (typeof define === "function" && define.amd) {
+ return define(function() {
+ return chaiBackbone;
+ });
+ } else {
+ return chai.use(chaiBackbone);
+ }
+ })(function(chai, utils) {
+ var flag, inspect, routeTo;
+ inspect = utils.inspect;
+ flag = utils.flag;
+ chai.Assertion.addMethod('trigger', function(trigger, options) {
+ var definedActions;
+ if (options == null) options = {};
+ definedActions = flag(this, 'whenActions') || [];
+ definedActions.push({
+ negate: flag(this, 'negate'),
+ before: function(context) {
+ this.callback = sinon.spy();
+ return flag(context, 'object').on(trigger, this.callback);
+ },
+ after: function(context) {
+ var negate, _ref;
+ negate = flag(context, 'negate');
+ flag(context, 'negate', this.negate);
+ context.assert(this.callback.calledOnce, "expected to trigger " + trigger, "expected not to trigger " + trigger);
+ if (options["with"] != null) {
+ context.assert((_ref = this.callback).calledWith.apply(_ref, options["with"]), "expected trigger to be called with " + (inspect(options["with"])) + ", but was called with " + (inspect(this.callback.args[0])) + ".", "expected trigger not to be called with " + (inspect(options["with"])) + ", but was");
+ }
+ return flag(context, 'negate', negate);
+ }
+ });
+ return flag(this, 'whenActions', definedActions);
+ });
+ chai.Assertion.addProperty('route', function() {
+ return flag(this, 'routing', true);
+ });
+ routeTo = function(router, methodName, options) {
+ var consideredRouter, current_history, route, spy, _i, _len, _ref;
+ if (options == null) options = {};
+ current_history = Backbone.history;
+ Backbone.history = new Backbone.History;
+ spy = sinon.spy(router, methodName);
+ this.assert(router._bindRoutes != null, 'provided router is not a Backbone.Router');
+ router._bindRoutes();
+ if (options.considering != null) {
+ _ref = options.considering;
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ consideredRouter = _ref[_i];
+ consideredRouter._bindRoutes();
+ }
+ }
+ Backbone.history.options = {
+ root: '/'
+ };
+ route = flag(this, 'object');
+ Backbone.history.loadUrl(route);
+ Backbone.history = current_history;
+ router[methodName].restore();
+ this.assert(spy.calledOnce, "expected `" + route + "` to route to " + methodName, "expected `" + route + "` not to route to " + methodName);
+ if (options.arguments != null) {
+ return this.assert(spy.calledWith.apply(spy, options.arguments), "expected `" + methodName + "` to be called with " + (inspect(options.arguments)) + ", but was called with " + (inspect(spy.args[0])) + " instead", "expected `" + methodName + "` not to be called with " + (inspect(options.arguments)) + ", but was");
+ }
+ };
+ chai.Assertion.overwriteProperty('to', function(_super) {
+ return function() {
+ if (flag(this, 'routing')) {
+ return routeTo;
+ } else {
+ return _super.apply(this, arguments);
+ }
+ };
+ });
+ return chai.Assertion.addMethod('call', function(methodName) {
+ var definedActions, object;
+ object = flag(this, 'object');
+ definedActions = flag(this, 'whenActions') || [];
+ definedActions.push({
+ negate: flag(this, 'negate'),
+ before: function(context) {
+ this.originalMethod = object[methodName];
+ this.spy = sinon.spy();
+ object[methodName] = this.spy;
+ return typeof object.delegateEvents === "function" ? object.delegateEvents() : void 0;
+ },
+ after: function(context) {
+ object[methodName] = this.originalMethod;
+ if (typeof object.delegateEvents === "function") object.delegateEvents();
+ return context.assert(this.spy.callCount > 0, this.spy.printf("expected %n to have been called at least once"), this.spy.printf("expected %n to not have been called"));
+ }
+ });
+ return flag(this, 'whenActions', definedActions);
+ });
+ });
+
+}).call(this);
View
140 chai-backbone.js.coffee
@@ -0,0 +1,140 @@
+#= require underscore
+#= require ./chai-changes
+
+((chaiBackbone) ->
+ # Module systems magic dance.
+ if (typeof require == "function" && typeof exports == "object" && typeof module == "object")
+ # NodeJS
+ module.exports = chaiBackbone
+ else if (typeof define == "function" && define.amd)
+ # AMD
+ define -> chaiBackbone
+ else
+ # Other environment (usually <script> tag): plug in to global chai instance directly.
+ chai.use chaiBackbone
+)((chai, utils) ->
+ inspect = utils.inspect
+ flag = utils.flag
+
+ # Verifies if the subject fires a trigger 'when' events happen
+ #
+ # Examples:
+ # model.should.trigger("change", with: [model]).when -> model.set attribute: "value"
+ # model.should.not.trigger("change:thing").when -> model.set attribute: "value"
+ # model.should.trigger("change").and.not.trigger("change:thing").when -> model.set attribute: "value"
+ #
+ # @param trigger the trigger expected to be fired
+ chai.Assertion.addMethod 'trigger', (trigger, options = {}) ->
+ definedActions = flag(this, 'whenActions') || []
+
+ # Add a around filter to the when actions
+ definedActions.push
+ negate: flag(this, 'negate')
+
+ # set up the callback to trigger
+ before: (context) ->
+ @callback = sinon.spy()
+ flag(context, 'object').on trigger, @callback
+
+ # verify if our callback is triggered
+ after: (context) ->
+ negate = flag(context, 'negate')
+ flag(context, 'negate', @negate)
+ context.assert @callback.calledOnce,
+ "expected to trigger #{trigger}",
+ "expected not to trigger #{trigger}"
+
+ if options.with?
+ context.assert @callback.calledWith(options.with...),
+ "expected trigger to be called with #{inspect options.with}, but was called with #{inspect @callback.args[0]}.",
+ "expected trigger not to be called with #{inspect options.with}, but was"
+ flag(context, 'negate', negate)
+ flag(this, 'whenActions', definedActions)
+
+ # Verify if a url fragment is routed to a certain method on the router
+ # Options:
+ # - you can consider multiple routers to test routing priorities
+ # - you can indicate expected arguments to test url extractions
+ #
+ # Examples:
+ #
+ # class MyRouter extends Backbone.Router
+ # routes:
+ # "home/:page/:other": "homeAction"
+ #
+ # myRouter = new MyRouter
+ #
+ # "home/stuff/thing".should.route_to(myRouter, "homeAction")
+ # "home/stuff/thing".should.route_to(myRouter, "homeAction", arguments: ["stuff", "thing"])
+ # "home/stuff/thing".should.route_to(myRouter, "homeAction", consider: [otherRouterWithPossiblyConflictingRoute])
+ #
+ chai.Assertion.addProperty 'route', ->
+ flag(this, 'routing', true)
+
+ routeTo = (router, methodName, options = {}) ->
+ # move possible active Backbone history out of the way temporary
+ current_history = Backbone.history
+
+ # reset history to clear active routes
+ Backbone.history = new Backbone.History
+
+ spy = sinon.spy router, methodName # spy on our expected method call
+ @assert router._bindRoutes?, 'provided router is not a Backbone.Router'
+
+ router._bindRoutes() # inject router routes into our history
+ if options.considering? # if multiple routers are provided load their routes aswell
+ consideredRouter._bindRoutes() for consideredRouter in options.considering
+
+ # manually set the root option to prevent calling Backbone.history.start() which is global
+ Backbone.history.options =
+ root: '/'
+
+ route = flag(this, 'object')
+ # fire our route to test
+ Backbone.history.loadUrl route
+
+ # set back our history. The spy should have our collected info now
+ Backbone.history = current_history
+ # restore the router method
+ router[methodName].restore()
+
+ # now assert if everything went according to spec
+ @assert spy.calledOnce,
+ "expected `#{route}` to route to #{methodName}",
+ "expected `#{route}` not to route to #{methodName}"
+
+ # verify arguments if they were provided
+ if options.arguments?
+ @assert spy.calledWith(options.arguments...),
+ "expected `#{methodName}` to be called with #{inspect options.arguments}, but was called with #{inspect spy.args[0]} instead",
+ "expected `#{methodName}` not to be called with #{inspect options.arguments}, but was"
+
+ chai.Assertion.overwriteProperty 'to', (_super) ->
+ ->
+ if flag(this, 'routing')
+ routeTo
+ else
+ _super.apply(this, arguments)
+
+ chai.Assertion.addMethod 'call', (methodName) ->
+ object = flag(this, 'object')
+ definedActions = flag(this, 'whenActions') || []
+ definedActions.push
+ negate: flag(this, 'negate')
+ before: (context) ->
+ @originalMethod = object[methodName]
+ @spy = sinon.spy()
+ object[methodName] = @spy
+ object.delegateEvents?()
+ after: (context) ->
+ object[methodName] = @originalMethod
+ object.delegateEvents?()
+
+ context.assert @spy.callCount > 0,
+ @spy.printf("expected %n to have been called at least once"),
+ @spy.printf("expected %n to not have been called")
+
+ flag(this, 'whenActions', definedActions)
+
+)
+
View
22 package.json
@@ -0,0 +1,22 @@
+{
+ "author": "Matthijs Groen <matthijs.groen@gmail.com>",
+ "name": "chai-backbone",
+ "description": "Backbone assertions for the Chai assertion library",
+ "keywords": [ "test", "assertion", "assert", "testing", "backbone" ],
+ "version": "0.8.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/matthijsgroen/chai-backbone"
+ },
+ "bugs": {
+ "url": "https://github.com/matthijsgroen/chai-backbone/issues"
+ },
+ "main": "./chai-backbone",
+ "devDependencies": {
+ "chai": ">= 1.0.0",
+ "mocha": ">= 1.0.0"
+ },
+ "dependencies": {
+ "chai-changes": ">= 0.8.0"
+ }
+}
View
79 test/chai-backbone_spec.js.coffee
@@ -0,0 +1,79 @@
+#= require ./../chai-backbone
+
+describe 'Chai-Backbone', ->
+
+ describe 'trigger / when', ->
+
+ it 'asserts if a trigger is fired', ->
+ m = new Backbone.Model
+ m.should.trigger('change').when ->
+ m.set fire: 'trigger'
+
+ it 'asserts if a trigger is not fired', ->
+ m = new Backbone.Model
+ m.should.not.trigger('change:not_fire').when ->
+ m.set fire: 'trigger'
+
+ it 'knows the negate state in the chain', ->
+ m = new Backbone.Model
+ m.should.trigger('change').and.not.trigger('change:not_fire').when ->
+ m.set fire: 'trigger'
+
+ describe 'assert backbone routes', ->
+ routerClass = null
+ router = null
+
+ before ->
+ routerClass = class extends Backbone.Router
+ routes:
+ 'route1/sub': 'subRoute'
+ 'route2/:par1': 'routeWithArg'
+ subRoute: ->
+ routeWithArg: (arg) ->
+
+ beforeEach ->
+ router = new routerClass
+
+ it 'checks if a method is trigger by route', ->
+ "route1/sub".should.route.to router, 'subRoute'
+ expect(->
+ "route1/ere".should.route.to router, 'subRoute'
+ ).to.throw 'expected `route1/ere` to route to subRoute'
+
+ it 'verifies argument parsing', ->
+ "route2/argVal".should.route.to router, 'routeWithArg', arguments: ['argVal']
+ expect(->
+ "route2/ere".should.route.to router, 'routeWithArg', arguments: ['argVal']
+ ).to.throw 'expected `routeWithArg` to be called with [ \'argVal\' ], but was called with [ \'ere\' ] instead'
+
+ it 'leaves the `to` keyword working properly', ->
+ expect('1').to.be.equal '1'
+
+ describe 'call', ->
+
+ it 'asserts if a method on provided object is called', ->
+ obj =
+ method: ->
+
+ obj.should.call('method').when ->
+ obj.method()
+
+ it 'raises AssertionError if method was not called', ->
+ obj =
+ method: ->
+ expect(->
+ obj.should.call('method').when ->
+ "noop"
+ ).to.throw /been called/
+
+ if Backbone? and jQuery?
+ it 'can check event calls of Backbone.Views', ->
+ viewClass = class extends Backbone.View
+ events:
+ 'click': 'eventCall'
+ eventCall: ->
+
+ viewInstance = new viewClass
+ viewInstance.should.call('eventCall').when ->
+ viewInstance.$el.trigger('click')
+

0 comments on commit 8aed2b6

Please sign in to comment.