diff --git a/client/css/style.css b/client/css/style.css index d2df23d..652eb90 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -10,9 +10,6 @@ footer ul { list-style: none; padding-left: 0; } -body, html { - height: 100%; -} body { font-family: 'Open Sans', sans-serif; margin-top: 50px; @@ -185,3 +182,54 @@ div .txt-help { background: #EFEFEF; padding: 30px; } + +/* Instructors */ +.instructor .main, .instructors .main, +.course .main, .courses .main { + background: #555; + max-width: 700px; + margin: auto; + color: #fff; + padding-bottom: 1em; + border-radius: 10px; +} +.instructor h1, .instructors h1, +.course h1, .courses h1 { + text-align: center; + margin: 5px 0 1em; +} +.instructor p, .instructors p, +.course p, .course p { + font-size: 1.2em; +} +.instructor #single, .instructors #single, +.course #single, .courses #single { + padding-bottom: 2em; +} +.instructor .content, .instructors .content, +.course .content, .courses .content { + padding: 2em 4em 1em; +} +#result, #added { + padding: 1em 0 0; +} +.success { + color: #8DC63F; +} +button.btn { + margin: 0 5px 10px 0; +} +input, select { + margin: 0 0 1em 0; + color: #000; +} +option { + padding: 8px 10px; +} +#collections ul { + padding: 1em 0 0; +} +#collections li { + list-style: none; + padding: 0 0 1em; +} diff --git a/client/spa/js/course/course.controller.js b/client/spa/js/course/course.controller.js new file mode 100644 index 0000000..3ca9b8b --- /dev/null +++ b/client/spa/js/course/course.controller.js @@ -0,0 +1,71 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +var $ = require('../vendor/index').$; +var Model = require('./course.model'); +var View = require('./course.view'); + +module.exports = Backbone.Controller.extend({ + routes: { + 'courses/:id': 'showCourse', + 'courses/new': 'addCourse' + }, + initialize: function(){ + this.options.container = this.options.container || 'body'; + this.model = new Model(); + this.view = new View({model: this.model}); + }, + showCourse: function(courseId, cb){ + this.fetchModel(courseId, function(err){ + var view; + + this.view.remove(); + this.view = new View({model: this.model}); + + if (err){ + view = this.renderError(); + } else { + this.view.template = this.view.showTemplate; + view = this.renderView(); + } + if (cb){ + cb(err, view); + } + + }.bind(this)); + }, + addCourse: function() { + this.model = new Model(); + this.model.isNew(); + + this.view.remove(); + + this.view.template = this.view.editTemplate; + this.renderView(); + }, + fetchModel: function(courseId, cb){ + this.model.set({id: courseId}); + this.model.fetch({ + success: function(model, response, options){ + //console.log(model); + cb(null, model); + }, + error: function(model, response, options){ + //console.error(response); + cb(response, model); + } + }); + }, + renderToContainer: function(view){ + return $(this.options.container).html(view); + }, + renderView: function(){ + this.renderToContainer(this.view.render().$el); + this.view.delegateEvents(); // delegate for add in collections + return this.view; + }, + renderError: function(){ + return this.renderToContainer( + '

There was a problem rendering this course

'); + } +}); diff --git a/client/spa/js/course/course.html b/client/spa/js/course/course.html new file mode 100644 index 0000000..b9caeba --- /dev/null +++ b/client/spa/js/course/course.html @@ -0,0 +1,10 @@ +
+
+

<%- title %>

+

Course Type: <%- courseType %>

+

Description:

<%- description %>

+ + +
+
+
diff --git a/client/spa/js/course/course.model.js b/client/spa/js/course/course.model.js new file mode 100644 index 0000000..99e13d9 --- /dev/null +++ b/client/spa/js/course/course.model.js @@ -0,0 +1,29 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +module.exports = Backbone.Model.extend({ + defaults: { + title: '', + courseType: 'video', + description: '' + }, + urlRoot: '/api/courses', + initialize: function(){ + this.on('change', function(){ + + }); + }, + validate: function(attrs){ + var errors = []; + if (!attrs.title){ + errors.push('title cannot be empty'); + } + if (!attrs.courseType){ + errors.push('courseType cannot be empty'); + } + if (!attrs.description){ + errors.push('description cannot be empty'); + } + return errors.length > 0 ? errors: false; + } +}); diff --git a/client/spa/js/course/course.view.js b/client/spa/js/course/course.view.js new file mode 100644 index 0000000..3a727b3 --- /dev/null +++ b/client/spa/js/course/course.view.js @@ -0,0 +1,94 @@ +'use strict'; +var Backbone = require('../vendor/index').Backbone; +var _ = require('../vendor/index')._; +var $ = require('../vendor/index').$; +var fs = require('fs'); //will be replaced by brfs in the browser +// readFileSync will be evaluated statically so errors can't be caught +var template = fs.readFileSync(__dirname + '/course.html', 'utf8'); +var editTemplate = fs.readFileSync(__dirname + '/editCourse.html', 'utf8'); + +module.exports = Backbone.View.extend({ + className: 'course', + template: _.template(template), + showTemplate: _.template(template), + editTemplate: _.template(editTemplate), + events: { + 'click .delete': 'destroy', + 'click .modify': 'modify', + 'click .save': 'save', + 'click .cancel': 'cancel' + }, + initialize: function(){ + this.listenTo(this.model, 'destroy', this.remove); + this.listenTo(this.model, 'change', this.render); + }, + render: function(){ + var context = this.model.toJSON(); + this.$el.html(this.template(context)); + + // if it's adding new model, change button to Add + if (this.model.get('id') === undefined) { + this.$('.save').html('Add'); + } + + return this; + }, + destroy: function(){ + this.model.destroy(); + + $('body').append($('
').addClass('course') + .append($('
') + .addClass('container main') + .append($('
') + .attr('id', 'result') + .addClass('success content') + .html('Successfully deleted course')))); + + this.remove(); + }, + modify: function(e){ + var context = this.model.toJSON(); + this.$el.html(this.editTemplate(context)); + + return this; + }, + save: function(e) { + // if there's no changes, do not do anything + e.preventDefault(); + + var formData = { + title: this.$('#title').val().trim(), + courseType: this.$('#courseType').val(), + description: this.$('#description').val().trim() + }; + + var check = { + success: function() { + $('#result').addClass('success') + .html('Successfully updated course') + .fadeIn().delay(4000).fadeOut(); + + var addNew = $('.save').html(); + + if (addNew === 'Add') { + $('#added').addClass('success') + .html('Successfully added new course') + .fadeIn().delay(4000).fadeOut(); + } + }, + error: function(model, errors) { + _.each(errors, function (err) { + $('#result').addClass('error'); + + $('input').find('.help-inline').text(err.message); + }, this); + } + }; + + this.model.save(formData, check); + }, + cancel: function(e) { + e.preventDefault(); // prevent event bubbling + this.render(); + } +}); diff --git a/client/spa/js/course/courses.collection.js b/client/spa/js/course/courses.collection.js new file mode 100644 index 0000000..783b75c --- /dev/null +++ b/client/spa/js/course/courses.collection.js @@ -0,0 +1,51 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; + +module.exports = Backbone.Collection.extend({ + url: '/api/courses/', + + initialize: function(){ + this.on('sortById', this.sortById); + this.on('sortByTitle', this.sortByTitle); + this.on('sortByCourseType', this.sortByCourseType); + this.on('addNew', this.addNew); + this.on('filterByCourseType', this.filterByCourseType); + this.trigger('sortById'); + }, + + sortById: function(){ + this.comparator = function(model){ + return model.get('id'); + }; + this.sort(); + }, + + sortByTitle: function(){ + this.comparator = function(model){ + return model.get('title'); + }; + this.sort(); + }, + + sortByCourseType: function(){ + this.comparator = function(model){ + return model.get('courseType'); + }; + this.sort(); + }, + + addNew: function() { + this.create = function(model) { + model.get('title'); + model.get('courseType'); + model.get('description'); + }; + }, + + filterByCourseType: function() { + var filtered = this.where({courseType: 'instructor led'}); + + return new Backbone.Collection(filtered); + } +}); diff --git a/client/spa/js/course/courses.controller.js b/client/spa/js/course/courses.controller.js new file mode 100644 index 0000000..53b9900 --- /dev/null +++ b/client/spa/js/course/courses.controller.js @@ -0,0 +1,66 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +var $ = require('../vendor/index').$; +var Model = require('./course.model'); +var Collection = require('./courses.collection'); +var View = require('./courses.view'); + +module.exports = Backbone.Controller.extend({ + routes: { + 'courses': 'showCourses' + }, + initialize: function(){ + this.options.container = this.options.container || 'body'; + + // listen to event from instructor controller + this.on('display:courses', function(data) { + // make sure this view exist + this.getView(); + this.view.trigger('display:courses', data); + }); + }, + getCollection: function(){ + if (!this.collection){ + Collection = Collection.extend({model: Model}); + this.collection = new Collection(); + } + return this.collection; + }, + getView: function(){ + if (!this.view){ + var V = View.extend({collection: this.collection}); + this.view = new V(); + this.view.on('addNew', function() { + // trigger the router for addNew + this.navigate('courses/new', { trigger: true }); + }.bind(this)); + } + return this.view; + }, + showCourses: function(){ + var self = this; + + this.getCollection().fetch({ + success: function(collection, response, options){ + self.getView(); + self.renderView(); + }, + error: function(collection, response, options){ + self.renderError(); + } + }); + }, + renderToContainer: function(html){ + return $(this.options.container).html(html); + }, + renderView: function(){ + this.renderToContainer(this.view.render().$el); + this.view.delegateEvents(); + return this.view; + }, + renderError: function(){ + return this.renderToContainer( + '

There was a problem rendering courses

'); + } +}); diff --git a/client/spa/js/course/courses.html b/client/spa/js/course/courses.html new file mode 100644 index 0000000..dd4d98a --- /dev/null +++ b/client/spa/js/course/courses.html @@ -0,0 +1,18 @@ +
+
+

Courses

+ + + + + +
    + <% _.each( models, function( model ){ %> +
  • + <%- model.attributes.title %> +
    Type: <%- model.attributes.courseType %> +
  • + <% }); %> +
+
+
diff --git a/client/spa/js/course/courses.view.js b/client/spa/js/course/courses.view.js new file mode 100644 index 0000000..47fabf7 --- /dev/null +++ b/client/spa/js/course/courses.view.js @@ -0,0 +1,82 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +var _ = require('../vendor/index')._; +var fs = require('fs'); //will be replaced by brfs in the browser +// readFileSync will be evaluated statically so errors can't be caught +var template = fs.readFileSync(__dirname + '/courses.html', 'utf8'); + +module.exports = Backbone.View.extend({ + className: 'courses', + template: _.template(template), + events:{ + 'click .sortById': 'sortById', + 'click .sortByTitle': 'sortByTitle', + 'click .sortByCourseType': 'sortByCourseType', + 'click .addNew': 'addNew', + 'click .filterByCourseType': 'filterByCourseType' + }, + + initialize: function() { + this.listenTo(this.collection, 'add', function(){ + this.render(); + }); + this.listenTo(this.collection, 'reset', function(){ + this.render(); + }); + this.listenTo(this.collection, 'sort', function(){ + this.render(); + }); + /* listen to the controller's event on this view + * where displayCourses is an object and + * this.on is for events happening to object on itself */ + this.on('display:courses', function(data) { + console.log('inside listen'); + this.displayCourses(data); + }); + }, + + render: function(collection) { + var context = collection || this.collection; + this.$el.html(this.template(context)); + return this; + }, + + addNew: function() { + this.trigger('addNew'); + this.render(); + }, + + sortById: function(){ + this.collection.trigger('sortById'); + this.render(); + }, + + sortByTitle: function(){ + this.collection.trigger('sortByTitle'); + this.render(); + }, + + sortByCourseType: function(){ + this.collection.trigger('sortByCourseType'); + this.render(); + }, + + /* Filter courses by the instructor id */ + displayCourses: function(data) { + console.log('displayCourses'); + var subset = this.collection.where({instructorId: data.instructorId}); + var collection = new Backbone.Collection(subset); + + // pass this subset to the container from the instructor + this(data.container).html(this.template(collection)); + }, + + filterByCourseType: function() { + var subset = this.collection.trigger('filterByCourseType'); + var collection = new Backbone.Collection(subset); + this.render(collection); + + return this; + } +}); diff --git a/client/spa/js/course/editCourse.html b/client/spa/js/course/editCourse.html new file mode 100644 index 0000000..87455dd --- /dev/null +++ b/client/spa/js/course/editCourse.html @@ -0,0 +1,18 @@ +
+
+
+
+ + + +
+ + +
+
+
+
diff --git a/client/spa/js/course/spec/course.controller.spec.js b/client/spa/js/course/spec/course.controller.spec.js new file mode 100644 index 0000000..f45f829 --- /dev/null +++ b/client/spa/js/course/spec/course.controller.spec.js @@ -0,0 +1,96 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Controller = require('../course.controller'); +var $ = require('jquery'); +var matchers = require('jasmine-jquery-matchers'); +// Test suite +console.log('Test course.controller'); +describe('Course controller', function(){ + var controller; + + beforeEach(function(){ + controller = new Controller(); + }); + + it('can be created', function(){ + expect(controller).toBeDefined(); + }); + + describe('when it is created', function(){ + + it('has the expected routes', function(){ + expect(controller.routes).toEqual(jasmine.objectContaining({ + 'courses/:id': 'showCourse' + })); + }); + + it('without a container option, uses body as the container', function(){ + expect(controller.options.container).toEqual('body'); + }); + + it('with a container option, uses specified container', function(){ + var ctrl = new Controller({container: '.newcontainer'}); + expect(ctrl.options.container).toEqual('.newcontainer'); + }); + + }); + + describe('when calling showCourse', function(){ + + beforeEach(function(){ + jasmine.addMatchers(matchers); + }); + + var success = function(callbacks){ + controller.model.set({'title': 'valid title', + 'courseType': 'valid courseType', 'description':'valid description'}); + callbacks.success(controller.model); + }; + + var err = function(callbacks){ + callbacks.error('error', controller.model); + }; + + it('with a valid course id, fetches the model', function(){ + spyOn(controller.model, 'fetch').and.callFake(success); + var cb = function(err, view){ + expect(err).toBeNull(); + expect(controller.model.get('title')).toEqual('valid title'); + expect(controller.model.get('courseType')).toEqual('valid courseType'); + expect(controller.model.get('description')). + toEqual('valid description'); + }; + + controller.showCourse(1, cb); + + }); + + it('with a valid course id, renders the view', function(){ + spyOn(controller.model, 'fetch').and.callFake(success); + spyOn(controller.view, 'render').and.callFake(function(){ + controller.view.$el = 'fake render'; + return controller.view; + }); + var cb = function(err, view){ + expect($('body')).toHaveText(''); + expect(view.cid).toEqual(controller.view.cid); + }; + controller.showCourse(1, cb); + }); + + it('with an invalid course id, renders an error message', function(){ + spyOn(controller.model, 'fetch').and.callFake(err); + var cb = function(err, view){ + expect(err).toBeTruthy(); + expect($('body')).toHaveText( + 'There was a problem rendering this course'); + }; + controller.showCourse('whatid', cb); + }); + }); +}); diff --git a/client/spa/js/course/spec/course.model.spec.js b/client/spa/js/course/spec/course.model.spec.js new file mode 100644 index 0000000..378a7cd --- /dev/null +++ b/client/spa/js/course/spec/course.model.spec.js @@ -0,0 +1,127 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Model = require('../course.model'); + +// Test suite +console.log('test course.model'); +describe('course model ', function(){ + var model; + + describe('when creating a new model ', function(){ + beforeEach(function(){ + model = new Model(); + }); + + it('has the expected routes', function(){ + expect(model.urlRoot).toEqual('/api/courses'); + }); + }); + + describe('when updating the model for course with errorSpy ', function(){ + var errorSpy; + + beforeEach(function(){ + errorSpy = jasmine.createSpy('Invalid'); + model = new Model({ + id: 1, + title: 'Full Stack Dev I', + courseType: 'video', + description: 'Learn how to do single page apps' + }); + model.on('invalid', errorSpy); + }); + + it('does not save when title is empty ', function(){ + model.set('title', null); + model.save(); + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy.calls.mostRecent().args[0]).toBe(model); + expect(errorSpy.calls.mostRecent().args[1][0]).toEqual( + 'title cannot be empty'); + }); + + it('does not save when courseType is empty ', function(){ + model.set('courseType', null); + model.save(); + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy.calls.mostRecent().args[0]).toBe(model); + expect(errorSpy.calls.mostRecent().args[1][0]).toEqual( + 'courseType cannot be empty'); + }); + + it('does not save when description is empty ', function(){ + model.set('description', null); + model.save(); + expect(errorSpy).toHaveBeenCalled(); + expect(errorSpy.calls.mostRecent().args[0]).toBe(model); + expect(errorSpy.calls.mostRecent().args[1][0]).toEqual( + 'description cannot be empty'); + }); + }); + + describe('when changing the state of the model without errorSpy', function(){ + + beforeEach(function(){ + + model = new Model({ + id: 1, + title: 'Front End Dev', + courseType: 'instructor led', + description: 'Learn front end dev' + }); + + }); + + it('does not save when title is empty ', function(){ + model.set('title', null); + model.save(); + expect(model.validationError).toEqual(['title cannot be empty']); + }); + + it('does not save when title and courseType are empty ', function(){ + model.set({title:null, courseType:null}); + model.save(); + expect(model.validationError).toEqual(['title cannot be empty', + 'courseType cannot be empty']); + }); + + it('does not save when title and description are empty ', function(){ + model.set({title:null, description:null}); + model.save(); + expect(model.validationError).toEqual(['title cannot be empty', + 'description cannot be empty']); + }); + + it('does not save when description is empty ', function(){ + model.set('description', null); + model.save(); + expect(model.validationError).toEqual(['description cannot be empty']); + }); + + it('does not save when description and courseType are empty ', function(){ + model.set({description:null, courseType:null}); + model.save(); + expect(model.validationError).toEqual(['courseType cannot be empty', + 'description cannot be empty']); + }); + + it('does not save when courseType is empty ', function(){ + model.set('courseType', null); + model.save(); + expect(model.validationError).toEqual(['courseType cannot be empty']); + }); + + it('does not save when all fields are empty ', function(){ + model.set({title:null, description:null, courseType:null}); + model.save(); + expect(model.validationError).toEqual(['title cannot be empty', + 'courseType cannot be empty', + 'description cannot be empty']); + }); + }); +}); diff --git a/client/spa/js/course/spec/course.view.spec.js b/client/spa/js/course/spec/course.view.spec.js new file mode 100644 index 0000000..c39d4d6 --- /dev/null +++ b/client/spa/js/course/spec/course.view.spec.js @@ -0,0 +1,129 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var View = require('../course.view.js'); +var matchers = require('jasmine-jquery-matchers'); +var Backbone = require('../../vendor/index').Backbone; +// Test suite +console.log('test course.view'); +describe('Course view ', function(){ + + var model; + var view; + var Model; + + beforeEach(function(){ + + // Add some convenience tests for working with the DOM + jasmine.addMatchers(matchers); + Model = Backbone.Model.extend({}); + + spyOn(Model.prototype, 'save'); + + // Needs to have the fields required by the template + model = new Model({ + title: 'Full Stack Dev I', + courseType: 'video', + description: 'Learn how to do single page apps' + }); + + view = new View({ + model: model + }); + }); + + describe('when the view is instantiated ', function(){ + + it('creates the correct element', function(){ + + // Element has to be uppercase + expect(view.el.nodeName).toEqual('DIV'); + + }); + + it('sets the correct class', function(){ + expect(view.$el).toHaveClass('course'); + }); + }); + + describe('when the view is rendered ', function(){ + it('returns the view object ', function(){ + expect(view.render()).toEqual(view); + }); + + it('produces the correct HTML ', function(){ + view.render(); + expect(view.$('h1').html()).toEqual('Full Stack Dev I'); + }); + }); + + describe('when the user clicks on the Edit button ', function(){ + beforeEach(function(){ + // do all spyOn before rendering + spyOn(view, 'save').and.callThrough(); + spyOn(view, 'cancel').and.callThrough(); + // call delegate after spyOn + view.delegateEvents(); + view.render(); + view.$('.modify').trigger('click'); + }); + + describe('when the user enters new course information ', function(){ + + describe('when user clicks on the cancel button', function(){ + + beforeEach(function(){ + view.$('.cancel').trigger('click'); + }); + + it('cancels the user input', function(){ + expect(view.cancel).toHaveBeenCalled(); + }); + }); + + describe('when user clicks on the save button', function(){ + beforeEach(function(){ + view.$('#title').val('changed title'); + view.$('#courseType').val('changed courseType'); + view.$('#description').val('changed description'); + + view.$('.save').trigger('click'); + }); + + it('updates the model', function(){ + expect(view.save).toHaveBeenCalled(); + expect(Model.prototype.save).toHaveBeenCalled(); + }); + }); + + }); + + }); // end edit/update test + + describe('when the user clicks on the Delete button ', function(){ + + beforeEach(function(){ + + // Must call through otherwise the actual view function won't be called + spyOn(view, 'destroy').and.callThrough(); + + // Must delegateEvents for the spy on a DOM event to work + view.delegateEvents(); + spyOn(model, 'destroy'); + }); + + it('deletes the model', function(){ + // Must render for the event to be fired + view.render(); + view.$('.delete').trigger('click'); + expect(view.destroy).toHaveBeenCalled(); + expect(model.destroy).toHaveBeenCalled(); + }); // end delete model test + + }); // end delete + +}); // end entire suite diff --git a/client/spa/js/course/spec/courses.collection.spec.js b/client/spa/js/course/spec/courses.collection.spec.js new file mode 100644 index 0000000..dd33366 --- /dev/null +++ b/client/spa/js/course/spec/courses.collection.spec.js @@ -0,0 +1,127 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Collection = require('../courses.collection'); +// Test suite +console.log('Test courses.collection'); +describe('Courses collection ', function(){ + var collection; + var modelA; + var modelB; + var modelC; + + beforeEach(function(){ + + // Set up test data + modelA = {id: 3, title: 'Full Stack Dev I', courseType: 'video'}; + modelB = {id: 1, title: 'Front End Dev', courseType: 'instructor led'}; + modelC = {id: 2, title: 'Graphics', courseType: 'instructor led'}; + }); + + describe('when models are added to the collection ', function(){ + beforeEach(function(){ + collection = new Collection(); + collection.add([ + modelA, + modelC, + modelB + ], + {silent: false} // Set to true to suppress add event + ); + }); + + it('orders the models by the course id', function(){ + collection.trigger('sortById'); + expect(collection.at(2).get('id')).toEqual(modelA.id); + expect(collection.at(0).get('id')).toEqual(modelB.id); + expect(collection.at(1).get('id')).toEqual(modelC.id); + }); + }); + + describe('when models are filtered in the collection ', function(){ + beforeEach(function(){ + collection = new Collection(); + collection.add([ + modelA, + modelC, + modelB + ], + {silent: false} // Set to true to suppress add event + ); + }); + + it('filters by courseType', function(){ + var typeCollection = collection.where({courseType: 'instructor led'}); + var newcollection = new Collection(typeCollection); + + //console.log('newcollection length = ' + newcollection.length); + + expect(newcollection.at(0).get('id')).toEqual(modelB.id); + expect(newcollection.at(1).get('id')).toEqual(modelC.id); + expect(newcollection.length).toEqual(2); + }); + + it('filters by courseType2', function(){ + var instructorLed = collection.trigger('filterByCourseType'); + + //var newInstructors = new Collection(instructorLed); + + //console.log('instructorLed length = ' + instructorLed.length); + + expect(collection.at(0).get('id')).toEqual(modelB.id); + expect(collection.at(1).get('id')).toEqual(modelC.id); + //expect(collection.length).toEqual(2); + }); + }); + + describe('when the collection interacts with the server', function(){ + it('fetches from the correct url', function(){ + collection = new Collection(); + expect(collection.url).toEqual('/api/courses/'); + }); + }); + + describe('when a sort event is triggered', function(){ + beforeEach(function(){ + collection = new Collection(); + collection.add([ + modelC, + modelB, + modelA + ], + {silent: false} // Set to true to suppress add event + ); + }); + + it('sorts by id', function(){ + collection.trigger('sortById'); + expect(collection.at(2).get('id')).toEqual(modelA.id); + expect(collection.at(0).get('id')).toEqual(modelB.id); + expect(collection.at(1).get('id')).toEqual(modelC.id); + }); + + it('sorts by title', function(){ + collection.trigger('sortByTitle'); + expect(collection.at(1).get('title')).toEqual(modelA.title); + expect(collection.at(0).get('title')).toEqual(modelB.title); + expect(collection.at(2).get('title')).toEqual(modelC.title); + }); + + it('sorts by courseType', function(){ + collection.trigger('sortByCourseType'); + expect(collection.at(2).get('courseType')).toEqual(modelA.courseType); + expect(collection.at(1).get('courseType')).toEqual(modelB.courseType); + expect(collection.at(0).get('courseType')).toEqual(modelC.courseType); + }); + + it('filters by courseType', function(){ + collection.trigger('filterByCourseType'); + expect(collection.at(1).get('courseType')).toEqual(modelB.courseType); + expect(collection.at(0).get('courseType')).toEqual(modelC.courseType); + }); + }); +}); diff --git a/client/spa/js/course/spec/courses.controller.spec.js b/client/spa/js/course/spec/courses.controller.spec.js new file mode 100644 index 0000000..b2ae015 --- /dev/null +++ b/client/spa/js/course/spec/courses.controller.spec.js @@ -0,0 +1,96 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Controller = require('../course.controller'); +var $ = require('jquery'); +var matchers = require('jasmine-jquery-matchers'); +// Test suite +console.log('Test course.controller'); +describe('Course controller', function(){ + var controller; + + beforeEach(function(){ + controller = new Controller(); + }); + + it('can be created', function(){ + expect(controller).toBeDefined(); + }); + + describe('when it is created', function(){ + + it('has the expected routes', function(){ + expect(controller.routes).toEqual(jasmine.objectContaining({ + 'courses/:id': 'showCourse' + })); + }); + + it('without a container option, uses body as the container', function(){ + expect(controller.options.container).toEqual('body'); + }); + + it('with a container option, uses specified container', function(){ + var ctrl = new Controller({container: '.newcontainer'}); + expect(ctrl.options.container).toEqual('.newcontainer'); + }); + + }); + + describe('when calling showCourse', function(){ + + beforeEach(function(){ + jasmine.addMatchers(matchers); + }); + + var success = function(callbacks){ + controller.model.set({'title': 'valid title', + 'courseType': 'valid courseType', 'description':'valid description'}); + callbacks.success(controller.model); + }; + + var err = function(callbacks){ + callbacks.error('error', controller.model); + }; + + it('with a valid course id, fetches the model', function(){ + spyOn(controller.model, 'fetch').and.callFake(success); + var cb = function(err, view){ + expect(err).toBeNull(); + expect(controller.model.get('title')).toEqual('valid title'); + expect(controller.model.get('courseType')).toEqual('valid courseType'); + expect(controller.model.get('description')) + .toEqual('valid description'); + }; + + controller.showCourse(1, cb); + + }); + + it('with a valid course id, renders the view', function(){ + spyOn(controller.model, 'fetch').and.callFake(success); + spyOn(controller.view, 'render').and.callFake(function(){ + controller.view.$el = 'fake render'; + return controller.view; + }); + var cb = function(err, view){ + expect($('body')).toHaveText(''); + expect(view.cid).toEqual(controller.view.cid); + }; + controller.showCourse(1, cb); + }); + + it('with an invalid course id, renders an error message', function(){ + spyOn(controller.model, 'fetch').and.callFake(err); + var cb = function(err, view){ + expect(err).toBeTruthy(); + expect($('body')).toHaveText( + 'There was a problem rendering this course'); + }; + controller.showCourse('whatid', cb); + }); + }); +}); diff --git a/client/spa/js/course/spec/courses.view.spec.js b/client/spa/js/course/spec/courses.view.spec.js new file mode 100644 index 0000000..1b3baad --- /dev/null +++ b/client/spa/js/course/spec/courses.view.spec.js @@ -0,0 +1,159 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var View = require('../courses.view.js'); +var matchers = require('jasmine-jquery-matchers'); +var _ = require('../../vendor/index')._; +var Backbone = require('../../vendor/index').Backbone; + +// Test suite +console.log('Test courses.view'); + +describe('Courses view ', function(){ + var model; + var collection; + var view; + beforeEach(function(){ + // Add some convenience tests for working with the DOM + jasmine.addMatchers(matchers); + var Model = Backbone.Model.extend({}); + var Collection = Backbone.Collection.extend({model: Model}); + + // Needs to have the fields required by the template + model = new Model({ + title: 'Full Stack Dev I', + courseType: 'video', + description: 'Learn how to do single page apps advanced' + }); + + collection = new Collection(model); + view = new View({ + collection: collection + }); + }); + + describe('when the view is instantiated ', function() { + it('creates the correct element', function () { + // Element has to be uppercase + expect(view.el.nodeName).toEqual('DIV'); + }); + + it('sets the correct class', function () { + view.render(); + expect(view.$el).toHaveClass('courses'); + }); + }); + + describe('when collection events happen', function(){ + beforeEach(function () { + spyOn(view, 'render').and.callThrough(); + }); + + it('renders when something is added to the collection', function(){ + collection.trigger('add'); + expect(view.render).toHaveBeenCalled(); + }); + + it('renders when the collection is reset', function(){ + collection.trigger('reset'); + expect(view.render).toHaveBeenCalled(); + }); + + it('renders when the collection is sorted', function(){ + collection.trigger('sort'); + expect(view.render).toHaveBeenCalled(); + }); + }); + + describe('when the view is rendered', function(){ + it('returns the view object', function(){ + expect(view.render()).toEqual(view); + }); + + it('produces the correct HTML', function(){ + view.render(); + expect(view.$('h1').html()).toEqual('Courses'); + expect(view.$('.course')[0]).toHaveText('Full Stack Dev I'); + }); + }); + + describe('when the user clicks on the Sort By Id button ', function(){ + beforeEach(function(){ + view.render(); + }); + + it('triggers the sortById event on the collection', function(){ + var spy = jasmine.createSpy('sortById'); + collection.on('sortById', spy); + view.$('.sortById').trigger('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('renders the view', function(){ + spyOn(view, 'render'); + view.$('.sortById').trigger('click'); + expect(view.render).toHaveBeenCalled(); + }); + }); + + describe('when the user clicks on the Sort By Title button ', function(){ + beforeEach(function(){ + view.render(); + }); + + it('triggers the sortByTitle event on the collection', function(){ + var spy = jasmine.createSpy('sortByTitle'); + collection.on('sortByTitle', spy); + view.$('.sortByTitle').trigger('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('renders the view', function(){ + spyOn(view, 'render'); + view.$('.sortByTitle').trigger('click'); + expect(view.render).toHaveBeenCalled(); + }); + }); + + describe('when the user clicks on the Sort By Course Type ', function(){ + beforeEach(function(){ + view.render(); + }); + + it('triggers the sortByCourseType event on the collection', function(){ + var spy = jasmine.createSpy('sortByCourseType'); + collection.on('sortByCourseType', spy); + view.$('.sortByCourseType').trigger('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('renders the view', function(){ + spyOn(view, 'render'); + view.$('.sortByCourseType').trigger('click'); + expect(view.render).toHaveBeenCalled(); + }); + }); + + describe('when the user clicks on the Filter By Course Type ', function(){ + beforeEach(function(){ + view.render(); + }); + + it('triggers the filterByCourseType event on the collection', function(){ + var spy = jasmine.createSpy('filterByCourseType'); + collection.on('filterByCourseType', spy); + view.$('.filterByCourseType').trigger('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('renders the view', function(){ + spyOn(view, 'render'); + view.$('.filterByCourseType').trigger('click'); + expect(view.render).toHaveBeenCalled(); + }); + }); +}); diff --git a/client/spa/js/instructor/editInstructor.html b/client/spa/js/instructor/editInstructor.html index ca8ff13..457bf72 100644 --- a/client/spa/js/instructor/editInstructor.html +++ b/client/spa/js/instructor/editInstructor.html @@ -1,20 +1,14 @@ - -
-
+
+
-
-
-
- - +
+ + + +
+ + +
-
+
diff --git a/client/spa/js/instructor/instructor.controller.js b/client/spa/js/instructor/instructor.controller.js index 8701efa..85f4149 100644 --- a/client/spa/js/instructor/instructor.controller.js +++ b/client/spa/js/instructor/instructor.controller.js @@ -7,7 +7,8 @@ var View = require('./instructor.view'); module.exports = Backbone.Controller.extend({ routes: { - 'instructors/:id': 'showInstructor' + 'instructors/:id': 'showInstructor', + 'instructors/new': 'addInstructor' }, initialize: function(){ this.options.container = this.options.container || 'body'; @@ -18,12 +19,20 @@ module.exports = Backbone.Controller.extend({ this.fetchModel(instructorId, function(err){ var view; - this.remove(); + this.view.remove(); this.view = new View({model: this.model}); + /* listen to display:courses to display this + * event in the view, trigger event on itself + */ + this.listenTo(this.view, 'display:courses', function(data) { + this.trigger('display:courses', data); + }); + if (err){ view = this.renderError(); } else { + this.view.template = this.view.showTemplate; view = this.renderView(); } if (cb){ @@ -32,6 +41,15 @@ module.exports = Backbone.Controller.extend({ }.bind(this)); }, + addInstructor: function() { + this.model = new Model(); + this.model.isNew(); + + this.view.remove(); + + this.view.template = this.view.editTemplate; + this.renderView(); + }, fetchModel: function(instructorId, cb){ this.model.set({id: instructorId}); this.model.fetch({ @@ -50,6 +68,7 @@ module.exports = Backbone.Controller.extend({ }, renderView: function(){ this.renderToContainer(this.view.render().$el); + this.view.delegateEvents(); // delegate for add in collections return this.view; }, renderError: function(){ diff --git a/client/spa/js/instructor/instructor.html b/client/spa/js/instructor/instructor.html index 7db63c3..6563fd9 100644 --- a/client/spa/js/instructor/instructor.html +++ b/client/spa/js/instructor/instructor.html @@ -1,21 +1,11 @@ - -
-
+
+

<%- firstName %> <%- lastName %>

<%- skills %>

- - + + +
-
+
+
diff --git a/client/spa/js/instructor/instructor.model.js b/client/spa/js/instructor/instructor.model.js index 0cae394..61c9a68 100644 --- a/client/spa/js/instructor/instructor.model.js +++ b/client/spa/js/instructor/instructor.model.js @@ -24,6 +24,6 @@ module.exports = Backbone.Model.extend({ if (!attrs.skills){ errors.push('skills cannot be empty'); } - return errors; + return errors.length > 0 ? errors: false; } }); diff --git a/client/spa/js/instructor/instructor.view.js b/client/spa/js/instructor/instructor.view.js index eb9bf32..dc669e4 100644 --- a/client/spa/js/instructor/instructor.view.js +++ b/client/spa/js/instructor/instructor.view.js @@ -6,56 +6,100 @@ var fs = require('fs'); //will be replaced by brfs in the browser // readFileSync will be evaluated statically so errors can't be caught var template = fs.readFileSync(__dirname + '/instructor.html', 'utf8'); var editTemplate = fs.readFileSync(__dirname + '/editInstructor.html', 'utf8'); +// class, not an instance of courses +//var CoursesView = require('../course/courses.view'); module.exports = Backbone.View.extend({ className: 'instructor', template: _.template(template), + showTemplate: _.template(template), editTemplate: _.template(editTemplate), events: { - 'click .i-delete': 'destroy', - 'click .i-edit': 'edit', - 'click .i-save': 'save', - 'click .i-cancel': 'cancel' + 'click .delete': 'destroy', + 'click .modify': 'modify', + 'click .save': 'save', + 'click .cancel': 'cancel', + 'click .displayCourses': 'displayCourses' }, initialize: function(){ this.listenTo(this.model, 'destroy', this.remove); this.listenTo(this.model, 'change', this.render); +// this.coursesView = new CoursesView({instructor: this.model}); }, render: function(){ var context = this.model.toJSON(); this.$el.html(this.template(context)); + // if it's adding new model, change button to Add + if (this.model.get('id') === undefined) { + this.$('.save').html('Add'); + } + +// this.coursesView.render(); + return this; }, + displayCourses: function() { + console.log('inside instructor display'); + // trigger event to populate the courses for this instructorId + this.trigger('display:courses', { + container: '.instructor-courses', + instructorId: this.model.get('id') + }); + }, destroy: function(){ this.model.destroy(); + + $('body').append($('
').addClass('instructor') + .append($('
') + .addClass('container main') + .append($('
') + .attr('id', 'result') + .addClass('success content') + .html('Successfully deleted instructor')))); + + this.remove(); }, - edit: function(e){ + modify: function(e){ var context = this.model.toJSON(); this.$el.html(this.editTemplate(context)); return this; }, save: function(e) { - e.preventDefault(); // if there's no changes, do not do anything + // if there's no changes, do not do anything + e.preventDefault(); var formData = { firstName: this.$('#firstName').val().trim(), lastName: this.$('#lastName').val().trim(), skills: this.$('#skills').val().trim() }; - var validate = { + + var check = { success: function() { $('#result').addClass('success') - .html('Successfully updated instructor') - .fadeIn().delay(4000).fadeOut(); + .html('Successfully updated instructor') + .fadeIn().delay(4000).fadeOut(); + + var addNew = $('.save').html(); + + if (addNew === 'Add') { + $('#added').addClass('success') + .html('Successfully added new instructor') + .fadeIn().delay(4000).fadeOut(); + } }, - error: function(model, error) { + error: function(model, errors) { + _.each(errors, function (err) { + $('#result').addClass('error'); + $('input').find('.help-inline').text(err.message); + }, this); } }; - this.model.save(formData, validate); + this.model.save(formData, check); }, cancel: function(e) { e.preventDefault(); // prevent event bubbling diff --git a/client/spa/js/instructor/instructors.collection.js b/client/spa/js/instructor/instructors.collection.js new file mode 100644 index 0000000..dc1c9cb --- /dev/null +++ b/client/spa/js/instructor/instructors.collection.js @@ -0,0 +1,44 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; + +module.exports = Backbone.Collection.extend({ + url: '/api/instructors/', + + initialize: function(){ + this.on('sortById', this.sortById); + this.on('sortByFirstName', this.sortByFirstName); + this.on('sortByLastName', this.sortByLastName); + this.on('addNew', this.addNew); + this.trigger('sortById'); + }, + + sortById: function(){ + this.comparator = function(model){ + return model.get('id'); + }; + this.sort(); + }, + + sortByFirstName: function(){ + this.comparator = function(model){ + return model.get('firstName'); + }; + this.sort(); + }, + + sortByLastName: function(){ + this.comparator = function(model){ + return model.get('lastName'); + }; + this.sort(); + }, + + addNew: function() { + this.create = function(model) { + model.get('firstName'); + model.get('lastName'); + model.get('skills'); + }; + } +}); diff --git a/client/spa/js/instructor/instructors.controller.js b/client/spa/js/instructor/instructors.controller.js new file mode 100644 index 0000000..cffaef4 --- /dev/null +++ b/client/spa/js/instructor/instructors.controller.js @@ -0,0 +1,59 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +var $ = require('../vendor/index').$; +var Model = require('./instructor.model'); +var Collection = require('./instructors.collection'); +var View = require('./instructors.view'); + +module.exports = Backbone.Controller.extend({ + routes: { + 'instructors': 'showInstructors' + }, + initialize: function(){ + this.options.container = this.options.container || 'body'; + }, + getCollection: function(){ + if (!this.collection){ + Collection = Collection.extend({model: Model}); + this.collection = new Collection(); + } + return this.collection; + }, + getView: function(){ + if (!this.view){ + var V = View.extend({collection: this.collection}); + this.view = new V(); + this.view.on('addNew', function() { + // trigger the router for addNew + this.navigate('instructors/new', { trigger: true }); + }.bind(this)); + } + return this.view; + }, + showInstructors: function(){ + var self = this; + + this.getCollection().fetch({ + success: function(collection, response, options){ + self.getView(); + self.renderView(); + }, + error: function(collection, response, options){ + self.renderError(); + } + }); + }, + renderToContainer: function(html){ + return $(this.options.container).html(html); + }, + renderView: function(){ + this.renderToContainer(this.view.render().$el); + this.view.delegateEvents(); + return this.view; + }, + renderError: function(){ + return this.renderToContainer( + '

There was a problem rendering instructors

'); + } +}); diff --git a/client/spa/js/instructor/instructors.html b/client/spa/js/instructor/instructors.html new file mode 100644 index 0000000..4734408 --- /dev/null +++ b/client/spa/js/instructor/instructors.html @@ -0,0 +1,16 @@ +
+
+

Instructors

+ + + + +
    + <% _.each( models, function( model ){ %> +
  • + <%- model.attributes.firstName %> <%- model.attributes.lastName %> +
  • + <% }); %> +
+
+
diff --git a/client/spa/js/instructor/instructors.view.js b/client/spa/js/instructor/instructors.view.js new file mode 100644 index 0000000..65291d9 --- /dev/null +++ b/client/spa/js/instructor/instructors.view.js @@ -0,0 +1,58 @@ +'use strict'; + +var Backbone = require('../vendor/index').Backbone; +var _ = require('../vendor/index')._; +var fs = require('fs'); //will be replaced by brfs in the browser +// readFileSync will be evaluated statically so errors can't be caught +var template = fs.readFileSync(__dirname + '/instructors.html', 'utf8'); + + +module.exports = Backbone.View.extend({ + className: 'instructors', + template: _.template(template), + events:{ + 'click .sortById': 'sortById', + 'click .sortByFirstName': 'sortByFirstName', + 'click .sortByLastName': 'sortByLastName', + 'click .addNew': 'addNew' + }, + + initialize: function() { + this.listenTo(this.collection, 'add', function(){ + this.render(); + }); + this.listenTo(this.collection, 'reset', function(){ + this.render(); + }); + this.listenTo(this.collection, 'sort', function(){ + this.render(); + }); + }, + + render: function() { + var context = this.collection; + this.$el.html(this.template(context)); + return this; + }, + + addNew: function() { + this.trigger('addNew'); + this.remove(); + this.render(); + }, + + sortById: function(){ + this.collection.trigger('sortById'); + this.render(); + }, + + sortByFirstName: function(){ + this.collection.trigger('sortByFirstName'); + this.render(); + }, + + sortByLastName: function(){ + this.collection.trigger('sortByLastName'); + this.render(); + } +}); diff --git a/client/spa/js/instructor/schedule.html b/client/spa/js/instructor/schedule.html new file mode 100644 index 0000000..722325b --- /dev/null +++ b/client/spa/js/instructor/schedule.html @@ -0,0 +1,8 @@ +
+

Courses for <%- firstName %> <%- lastName %>

+ <% _.each( models, function( model ){ %> +
+ <%- model.attributes.firstName %> <%- model.attributes.lastName %> +
+ <% }); %> +
diff --git a/client/spa/js/instructor/spec/instructor.view.spec.js b/client/spa/js/instructor/spec/instructor.view.spec.js index 7b51e47..c383339 100644 --- a/client/spa/js/instructor/spec/instructor.view.spec.js +++ b/client/spa/js/instructor/spec/instructor.view.spec.js @@ -69,7 +69,7 @@ describe('Instructor view ', function(){ // call delegate after spyOn view.delegateEvents(); view.render(); - view.$('.i-edit').trigger('click'); + view.$('.modify').trigger('click'); }); describe('when the user enters new instructor information ', function(){ @@ -77,7 +77,7 @@ describe('Instructor view ', function(){ describe('when user clicks on the cancel button', function(){ beforeEach(function(){ - view.$('.i-cancel').trigger('click'); + view.$('.cancel').trigger('click'); }); it('cancels the user input', function(){ @@ -91,7 +91,7 @@ describe('Instructor view ', function(){ view.$('#lastName').val('changed lastName'); view.$('#skills').val('changed skills'); - view.$('.i-save').trigger('click'); + view.$('.save').trigger('click'); }); it('updates the model', function(){ @@ -120,7 +120,7 @@ describe('Instructor view ', function(){ it('deletes the model', function(){ // Must render for the event to be fired view.render(); - view.$('.i-delete').trigger('click'); + view.$('.delete').trigger('click'); expect(view.destroy).toHaveBeenCalled(); expect(model.destroy).toHaveBeenCalled(); }); // end delete model test diff --git a/client/spa/js/instructor/spec/instructors.collection.spec.js b/client/spa/js/instructor/spec/instructors.collection.spec.js new file mode 100644 index 0000000..4fe9f2c --- /dev/null +++ b/client/spa/js/instructor/spec/instructors.collection.spec.js @@ -0,0 +1,85 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Collection = require('../instructors.collection'); +// Test suite +console.log('test instructors.collection'); +describe('Instructors collection ', function(){ + var collection; + var modelA; + var modelB; + var modelC; + + beforeEach(function(){ + + // Set up test data + modelA = {id: 3, firstName: 'Jeff', lastName: 'Thomas'}; + modelB = {id: 1, firstName: 'Tom', lastName: 'Shell'}; + modelC = {id: 2, firstName: 'Emily', lastName: 'Row'}; + }); + + describe('when models are added to the collection ', function(){ + beforeEach(function(){ + collection = new Collection(); + collection.add([ + modelA, + modelC, + modelB + ], + {silent: false} // Set to true to suppress add event + ); + }); + + it('orders the models by the instructor id', function(){ + collection.trigger('sortById'); + expect(collection.at(2).get('id')).toEqual(modelA.id); + expect(collection.at(0).get('id')).toEqual(modelB.id); + expect(collection.at(1).get('id')).toEqual(modelC.id); + }); + }); + + describe('when the collection interacts with the server', function(){ + it('fetches from the correct url', function(){ + collection = new Collection(); + expect(collection.url).toEqual('/api/instructors/'); + }); + }); + + describe('when a sort event is triggered', function(){ + beforeEach(function(){ + collection = new Collection(); + collection.add([ + modelC, + modelB, + modelA + ], + {silent: false} // Set to true to suppress add event + ); + }); + + it('sorts by id', function(){ + collection.trigger('sortById'); + expect(collection.at(2).get('id')).toEqual(modelA.id); + expect(collection.at(0).get('id')).toEqual(modelB.id); + expect(collection.at(1).get('id')).toEqual(modelC.id); + }); + + it('sorts by firstName', function(){ + collection.trigger('sortByFirstName'); + expect(collection.at(1).get('firstName')).toEqual(modelA.firstName); + expect(collection.at(2).get('firstName')).toEqual(modelB.firstName); + expect(collection.at(0).get('firstName')).toEqual(modelC.firstName); + }); + + it('sorts by lastName', function(){ + collection.trigger('sortByLastName'); + expect(collection.at(2).get('lastName')).toEqual(modelA.lastName); + expect(collection.at(1).get('lastName')).toEqual(modelB.lastName); + expect(collection.at(0).get('lastName')).toEqual(modelC.lastName); + }); + }); +}); diff --git a/client/spa/js/instructor/spec/instructors.controller.spec.js b/client/spa/js/instructor/spec/instructors.controller.spec.js new file mode 100644 index 0000000..b0ebe71 --- /dev/null +++ b/client/spa/js/instructor/spec/instructors.controller.spec.js @@ -0,0 +1,114 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var Backbone = require('../../vendor/index').Backbone; +var Controller = require('../instructors.controller'); +var $ = require('jquery'); +var matchers = require('jasmine-jquery-matchers'); + +// Test suite +console.log('test instructors.controller'); + +describe('Instructors controller', function(){ + var controller; + + beforeEach(function(){ + controller = new Controller(); + }); + + it('can be created', function(){ + expect(controller).toBeDefined(); + }); + + describe('when it is created', function(){ + it('has the expected routes', function(){ + expect(controller.routes).toEqual(jasmine.objectContaining({ + 'instructors': 'showInstructors' + })); + }); + + it('without a container option, uses body as the container', function(){ + expect(controller.options.container).toEqual('body'); + }); + + it('with a container option, uses specified container', function(){ + var ctrl = new Controller({container: '.newcontainer'}); + expect(ctrl.options.container).toEqual('.newcontainer'); + }); + }); + + describe('when asked to showInstructors', function(){ + beforeEach(function(){ + jasmine.addMatchers(matchers); + }); + + describe('and fetch is successful', function(){ + beforeEach(function(){ + spyOn(Backbone.Collection.prototype, 'fetch').and.callFake( + function(options){ + options.success(); + } + ); + }); + + it('sets up the collection if it is not already', function(){ + expect(controller.collection).not.toBeDefined(); + controller.showInstructors(); + expect(controller.collection).toBeDefined(); + }); + + it('uses the existing collection if it is already setup', function(){ + controller.showInstructors(); + controller.collection.add({id: 'xyz'}); + controller.showInstructors(); + expect(controller.collection.at(0).get('id')).toEqual('xyz'); + }); + + it('fetches data for the collection', function(){ + controller.showInstructors(); + expect(controller.collection.fetch).toHaveBeenCalled(); + }); + + it('sets up the view if it is not already', function(){ + expect(controller.view).not.toBeDefined(); + controller.showInstructors(); + expect(controller.view).toBeDefined(); + }); + + it('uses the existing view if it is already setup', function(){ + controller.showInstructors(); + controller.view.test = true; + controller.showInstructors(); + expect(controller.view.test).toBeTruthy(); + }); + + it('renders the view to the correct container', function() { + spyOn(controller, 'renderView').and.callThrough(); + controller.showInstructors(); + var returnedView = controller.renderView.calls.mostRecent().object.view; + expect(returnedView).toEqual(controller.view); + expect($('body h1')).toHaveText('Instructors'); + }); + }); + + describe('and fetch errors', function(){ + beforeEach(function(){ + + spyOn(Backbone.Collection.prototype, 'fetch').and.callFake( + function(options){ + options.error(); + } + ); + }); + + it('renders error', function(){ + controller.showInstructors(); + expect($('body')).toHaveText('There was a problem rendering instructors'); + }); + }); +}); +}); diff --git a/client/spa/js/instructor/spec/instructors.view.spec.js b/client/spa/js/instructor/spec/instructors.view.spec.js new file mode 100644 index 0000000..fc7256e --- /dev/null +++ b/client/spa/js/instructor/spec/instructors.view.spec.js @@ -0,0 +1,141 @@ +'use strict'; + +/* +global jasmine, describe, it, expect, beforeEach, afterEach, xdescribe, xit, +spyOn +*/ +// Get the code you want to test +var View = require('../instructors.view.js'); +var matchers = require('jasmine-jquery-matchers'); +var _ = require('../../vendor/index')._; +var Backbone = require('../../vendor/index').Backbone; + +// Test suite +console.log('test instructors.view'); + +describe('Instructors view ', function(){ + var model; + var collection; + var view; + + beforeEach(function(){ + // Add some convenience tests for working with the DOM + jasmine.addMatchers(matchers); + var Model = Backbone.Model.extend({}); + var Collection = Backbone.Collection.extend({model: Model}); + + // Needs to have the fields required by the template + model = new Model({ + firstName: 'Instructor <3', + lastName: 'new', + skills: 'Teaching, Cooking' + }); + + collection = new Collection(model); + view = new View({ + collection: collection + }); + }); + + describe('when the view is instantiated ', function() { + it('creates the correct element', function () { + // Element has to be uppercase + expect(view.el.nodeName).toEqual('DIV'); + }); + + it('sets the correct class', function () { + view.render(); + expect(view.$el).toHaveClass('instructors'); + }); + }); + + describe('when collection events happen', function(){ + beforeEach(function () { + spyOn(view, 'render').and.callThrough(); + }); + + it('renders when something is added to the collection', function(){ + collection.trigger('add'); + expect(view.render).toHaveBeenCalled(); + }); + + it('renders when the collection is reset', function(){ + collection.trigger('reset'); + expect(view.render).toHaveBeenCalled(); + }); + + it('renders when the collection is sorted', function(){ + collection.trigger('sort'); + expect(view.render).toHaveBeenCalled(); + }); + }); + + describe('when the view is rendered', function(){ + it('returns the view object', function(){ + expect(view.render()).toEqual(view); + }); + + it('produces the correct HTML', function(){ + view.render(); + expect(view.$('h1').html()).toEqual('Instructors'); + expect(view.$('.instructor')[0]).toHaveText('Instructor <3'); + }); + }); + + describe('when the user clicks on the Sort By Id button ', function(){ + beforeEach(function(){ + view.render(); + }); + + it('triggers the sortById event on the collection', function(){ + var spy = jasmine.createSpy('sortById'); + collection.on('sortById', spy); + view.$('.sortById').trigger('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('renders the view', function(){ + spyOn(view, 'render'); + view.$('.sortById').trigger('click'); + expect(view.render).toHaveBeenCalled(); + }); + }); + + describe('when the user clicks on the Sort By First Name button ', function(){ + beforeEach(function(){ + view.render(); + }); + + it('triggers the sortByFirstName event on the collection', function(){ + var spy = jasmine.createSpy('sortByFirstName'); + collection.on('sortByFirstName', spy); + view.$('.sortByFirstName').trigger('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('renders the view', function(){ + spyOn(view, 'render'); + view.$('.sortByFirstName').trigger('click'); + expect(view.render).toHaveBeenCalled(); + }); + }); + + describe('when the user clicks on the Sort By Last Name button ', function(){ + beforeEach(function(){ + view.render(); + }); + + it('triggers the sortByLastName event on the collection', function(){ + var spy = jasmine.createSpy('sortByLastName'); + collection.on('sortByLastName', spy); + view.$('.sortByLastName').trigger('click'); + expect(spy).toHaveBeenCalled(); + }); + + it('renders the view', function(){ + spyOn(view, 'render'); + view.$('.sortByLastName').trigger('click'); + expect(view.render).toHaveBeenCalled(); + }); + }); +}); diff --git a/client/spa/js/main.js b/client/spa/js/main.js index b1b493f..fb7e8bf 100644 --- a/client/spa/js/main.js +++ b/client/spa/js/main.js @@ -3,14 +3,25 @@ window.Backbone = require('./vendor').Backbone; // Include your code +var Course = require('./course/course.controller'); +var Courses = require('./course/courses.controller'); var Instructor = require('./instructor/instructor.controller'); +var Instructors = require('./instructor/instructors.controller'); var Resource = require('./learning-resource/learning-resource.controller'); // Initialize it +window.course = new Course({router:true, container: 'body'}); +window.courses = new Courses({router:true, container: 'body'}); window.instructor = new Instructor({router:true, container: 'body'}); +window.instructors = new Instructors({router:true, container: 'body'}); window.resource = new Resource({router:true, container: 'body'}); - // Additional modules go here +/* live instance to coordinate controller behavior for courses and instructor + * glues both controllers together + */ +window.instructor.on('display:courses', function(data) { + window.courses.trigger('display:courses', data); +}); // This should be the last line window.Backbone.history.start();