diff --git a/.gitignore b/.gitignore index 3483078233..e49727ac4c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .nodemonignore .sass-cache/ node_modules/ -public/lib \ No newline at end of file +public/lib +test/coverage/ diff --git a/bower.json b/bower.json index 1ee578ef41..311d5caf82 100644 --- a/bower.json +++ b/bower.json @@ -1,12 +1,16 @@ { - "name": "mean", - "version": "0.1.0", - "dependencies": { - "bootstrap": "2.3.2", - "angular": "1.0.6", - "angular-resource": "1.0.6", - "angular-cookies": "1.0.6", - "angular-bootstrap": "0.6.0", - "angular-ui-utils": "0.0.4" - } + "name": "mean", + "version": "0.1.0", + "dependencies": { + "bootstrap": "2.3.2", + "angular": "1.0.6", + "angular-resource": "1.0.6", + "angular-cookies": "1.0.6", + "angular-bootstrap": "0.6.0", + "angular-ui-utils": "0.0.4", + "angular-mocks": "~1.0.8" + }, + "resolutions": { + "angular": "1.0.6" + } } diff --git a/gruntfile.js b/gruntfile.js index 2c41e8d626..56947225c0 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -30,7 +30,7 @@ module.exports = function(grunt) { } }, jshint: { - all: ['gruntfile.js', 'public/js/**/*.js', 'test/**/*.js', 'app/**/*.js'] + all: ['gruntfile.js', 'public/js/**/*.js', 'test/mocha/**/*.js', 'test/karma/**/*.js', 'app/**/*.js'] }, nodemon: { dev: { @@ -50,7 +50,7 @@ module.exports = function(grunt) { } }, concurrent: { - tasks: ['nodemon', 'watch'], + tasks: ['nodemon', 'watch', 'karma:unit'], options: { logConcurrentOutput: true } @@ -59,12 +59,17 @@ module.exports = function(grunt) { options: { reporter: 'spec' }, - src: ['test/**/*.js'] + src: ['test/mocha/**/*.js'] }, env: { test: { NODE_ENV: 'test' } + }, + karma: { + unit: { + configFile: 'test/karma/karma.conf.js' + } } }); @@ -72,6 +77,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-mocha-test'); + grunt.loadNpmTasks('grunt-karma'); grunt.loadNpmTasks('grunt-nodemon'); grunt.loadNpmTasks('grunt-concurrent'); grunt.loadNpmTasks('grunt-env'); diff --git a/package.json b/package.json index 7ccf118ab5..76b64463f5 100755 --- a/package.json +++ b/package.json @@ -1,52 +1,63 @@ { - "name": "mean", - "description": "MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).", - "version": "0.1.0", - "private": false, - "author": "Amos Haviv", - "repository": { - "type": "git", - "url": "https://github.com/linnovate/mean.git" - }, - "engines": { - "node": "0.10.x", - "npm": "1.2.x" - }, - "scripts": { - "start": "node node_modules/grunt-cli/bin/grunt", - "test": "node node_modules/grunt-cli/bin/grunt test", - "postinstall": "bower install" - }, - "dependencies": { - "express": "latest", - "jade": "latest", - "mongoose": "latest", - "connect-mongo": "latest", - "connect-flash": "latest", - "crypto": "latest", - "passport": "latest", - "passport-local": "latest", - "passport-facebook": "latest", - "passport-twitter": "latest", - "passport-github": "latest", - "passport-google-oauth": "latest", - "underscore": "latest", - "async": "latest", - "view-helpers": "latest", - "mean-logger": "latest", - "forever": "latest", - "bower": "latest", - "grunt": "latest", - "grunt-cli": "latest", - "grunt-env": "latest" - }, - "devDependencies": { - "supertest": "latest", - "should": "latest", - "grunt-contrib-watch": "latest", - "grunt-contrib-jshint": "latest", - "grunt-nodemon": "latest", - "grunt-concurrent": "latest", - "grunt-mocha-test": "latest" - } + "name": "mean", + "description": "MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).", + "version": "0.1.0", + "private": false, + "author": "Amos Haviv", + "repository": { + "type": "git", + "url": "https://github.com/linnovate/mean.git" + }, + "engines": { + "node": "0.10.x", + "npm": "1.2.x" + }, + "scripts": { + "start": "node node_modules/grunt-cli/bin/grunt", + "test": "node node_modules/grunt-cli/bin/grunt test", + "postinstall": "bower install" + }, + "dependencies": { + "express": "latest", + "jade": "latest", + "mongoose": "latest", + "connect-mongo": "latest", + "connect-flash": "latest", + "crypto": "latest", + "passport": "latest", + "passport-local": "latest", + "passport-facebook": "latest", + "passport-twitter": "latest", + "passport-github": "latest", + "passport-google-oauth": "latest", + "underscore": "latest", + "async": "latest", + "view-helpers": "latest", + "mean-logger": "latest", + "forever": "latest", + "bower": "latest", + "grunt": "latest", + "grunt-cli": "latest", + "grunt-env": "latest" + }, + "devDependencies": { + "supertest": "latest", + "should": "latest", + "grunt-contrib-watch": "latest", + "grunt-contrib-jshint": "latest", + "grunt-nodemon": "latest", + "grunt-concurrent": "latest", + "grunt-mocha-test": "latest", + "karma-script-launcher": "~0.1.0", + "karma-chrome-launcher": "~0.1.0", + "karma-firefox-launcher": "~0.1.0", + "karma-html2js-preprocessor": "~0.1.0", + "karma-jasmine": "~0.1.3", + "karma-requirejs": "~0.1.0", + "karma-coffee-preprocessor": "~0.1.0", + "karma-phantomjs-launcher": "~0.1.0", + "karma": "~0.10.4", + "grunt-karma": "~0.6.2", + "karma-coverage": "~0.1.0" + } } diff --git a/test/karma/karma.conf.js b/test/karma/karma.conf.js new file mode 100644 index 0000000000..46d3137620 --- /dev/null +++ b/test/karma/karma.conf.js @@ -0,0 +1,99 @@ +// Karma configuration +// Generated on Sat Oct 05 2013 22:00:14 GMT+0700 (ICT) + +module.exports = function (config) { + config.set({ + + // base path, that will be used to resolve files and exclude + basePath: '../../', + + + // frameworks to use + frameworks: ['jasmine'], + + + // list of files / patterns to load in the browser + files: [ + 'public/lib/angular/angular.js', + 'public/lib/angular-mocks/angular-mocks.js', + 'public/lib/angular-cookies/angular-cookies.js', + 'public/lib/angular-resource/angular-resource.js', + 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js', + 'public/lib/angular-bootstrap/ui-bootstrap.js', + 'public/lib/angular-ui-utils/modules/route/route.js', + 'public/js/app.js', + 'public/js/config.js', + 'public/js/directives.js', + 'public/js/filters.js', + 'public/js/services/global.js', + 'public/js/services/articles.js', + 'public/js/controllers/articles.js', + 'public/js/controllers/index.js', + 'public/js/controllers/header.js', + 'public/js/init.js', + 'test/karma/unit/**/*.js' + ], + + + // list of files to exclude + exclude: [ + + ], + + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + //reporters: ['progress'], + reporters: ['progress', 'coverage'], + + // coverage + preprocessors: { + // source files, that you wanna generate coverage for + // do not include tests or libraries + // (these files will be instrumented by Istanbul) + 'public/js/controllers/*.js': ['coverage'], + 'public/js/services/*.js': ['coverage'] + }, + + coverageReporter: { + type : 'html', + dir : 'test/coverage/' + }, + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera + // - Safari (only Mac) + // - PhantomJS + // - IE (only Windows) + browsers: ['PhantomJS'], + + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +}; diff --git a/test/karma/unit/controllers/articles.spec.js b/test/karma/unit/controllers/articles.spec.js new file mode 100644 index 0000000000..2cbaffe8f0 --- /dev/null +++ b/test/karma/unit/controllers/articles.spec.js @@ -0,0 +1,201 @@ +(function () { + 'use strict'; + +// Articles Controller Spec + + describe('MEAN controllers', function () { + + describe('ArticlesController', function () { + + // The $resource service augments the response object with methods for updating and deleting the resource. + // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match + // the responses exactly. To solve the problem, we use a newly-defined toEqualData Jasmine matcher. + // When the toEqualData matcher compares two objects, it takes only object properties into + // account and ignores methods. + beforeEach(function () { + this.addMatchers({ + toEqualData: function (expected) { + return angular.equals(this.actual, expected); + } + }); + }); + + // Load the controllers module + beforeEach(module('mean')); + + // Initialize the controller and a mock scope + var ArticlesController, + scope, + $httpBackend, + $routeParams, + $location; + + // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). + // This allows us to inject a service but then attach it to a variable + // with the same name as the service. + beforeEach(inject(function ($controller, $rootScope, _$location_, _$routeParams_, _$httpBackend_) { + + scope = $rootScope.$new(); + + ArticlesController = $controller('ArticlesController', { + $scope: scope + }); + + $routeParams = _$routeParams_; + + $httpBackend = _$httpBackend_; + + $location = _$location_; + + })); + + it('$scope.find() should create an array with at least one article object ' + + 'fetched from XHR', function () { + + // test expected GET request + $httpBackend.expectGET('articles').respond([ + {title: 'An Article about MEAN', content: 'MEAN rocks!'} + ]); + + // run controller + scope.find(); + $httpBackend.flush(); + + // test scope value + expect(scope.articles).toEqualData([ + {title: 'An Article about MEAN', content: 'MEAN rocks!'} + ]); + + }); + + it('$scope.findOne() should create an array with one article object fetched ' + + 'from XHR using a articleId URL parameter', function () { + // fixture URL parament + $routeParams.articleId = '525a8422f6d0f87f0e407a33'; + + // fixture response object + var testArticleData = function () { + return { + title: 'An Article about MEAN', + content: 'MEAN rocks!' + }; + }; + + // test expected GET request with response object + $httpBackend.expectGET(/articles\/([0-9a-fA-F]{24})$/).respond(testArticleData()); + + // run controller + scope.findOne(); + $httpBackend.flush(); + + // test scope value + expect(scope.article).toEqualData(testArticleData()); + + }); + + it('$scope.create() with valid form data should send a POST request ' + + 'with the form input values and then ' + + 'locate to new object URL', function () { + + // fixture expected POST data + var postArticleData = function () { + return { + title: 'An Article about MEAN', + content: 'MEAN rocks!' + }; + }; + + // fixture expected response data + var responseArticleData = function () { + return { + _id: '525cf20451979dea2c000001', + title: 'An Article about MEAN', + content: 'MEAN rocks!' + }; + }; + + // fixture mock form input values + scope.title = 'An Article about MEAN'; + scope.content = 'MEAN rocks!'; + + // test post request is sent + $httpBackend.expectPOST('articles', postArticleData()).respond(responseArticleData()); + + // Run controller + scope.create(); + $httpBackend.flush(); + + // test form input(s) are reset + expect(scope.title).toEqual(''); + expect(scope.content).toEqual(''); + + // test URL location to new object + expect($location.path()).toBe('/articles/' + responseArticleData()._id); + }); + + it('$scope.update() should update a valid article', inject(function (Articles) { + + // fixture rideshare + var putArticleData = function () { + return { + _id: '525a8422f6d0f87f0e407a33', + title: 'An Article about MEAN', + to: 'MEAN is great!' + }; + }; + + // mock article object from form + var article = new Articles(putArticleData()); + + // mock article in scope + scope.article = article; + + // test PUT happens correctly + $httpBackend.expectPUT(/articles\/([0-9a-fA-F]{24})$/).respond(); + + // testing the body data is out for now until an idea for testing the dynamic updated array value is figured out + //$httpBackend.expectPUT(/articles\/([0-9a-fA-F]{24})$/, putArticleData()).respond(); + /* + Error: Expected PUT /articles\/([0-9a-fA-F]{24})$/ with different data + EXPECTED: {"_id":"525a8422f6d0f87f0e407a33","title":"An Article about MEAN","to":"MEAN is great!"} + GOT: {"_id":"525a8422f6d0f87f0e407a33","title":"An Article about MEAN","to":"MEAN is great!","updated":[1383534772975]} + */ + + // run controller + scope.update(); + $httpBackend.flush(); + + // test URL location to new object + expect($location.path()).toBe('/articles/' + putArticleData()._id); + + })); + + it('$scope.remove() should send a DELETE request with a valid articleId' + + 'and remove the article from the scope', inject(function (Articles) { + + // fixture rideshare + var article = new Articles({ + _id: '525a8422f6d0f87f0e407a33' + }); + + // mock rideshares in scope + scope.articles = []; + scope.articles.push(article); + + // test expected rideshare DELETE request + $httpBackend.expectDELETE(/articles\/([0-9a-fA-F]{24})$/).respond(204); + + // run controller + scope.remove(article); + $httpBackend.flush(); + + // test after successful delete URL location articles lis + //expect($location.path()).toBe('/articles'); + expect(scope.articles.length).toBe(0); + + })); + + }); + + }); +}()); \ No newline at end of file diff --git a/test/karma/unit/controllers/headers.spec.js b/test/karma/unit/controllers/headers.spec.js new file mode 100644 index 0000000000..98ea968f6b --- /dev/null +++ b/test/karma/unit/controllers/headers.spec.js @@ -0,0 +1,32 @@ +(function() { + 'use strict'; + + describe('MEAN controllers', function() { + + describe('HeaderController', function() { + + // Load the controllers module + beforeEach(module('mean')); + + var scope, + HeaderController; + + beforeEach(inject(function($controller, $rootScope) { + scope = $rootScope.$new(); + + HeaderController = $controller('HeaderController', { + $scope: scope + }); + })); + + it('should expose some global scope', function() { + + expect(scope.global).toBeTruthy(); + + }); + + }); + + }); + +})(); \ No newline at end of file diff --git a/test/karma/unit/controllers/index.spec.js b/test/karma/unit/controllers/index.spec.js new file mode 100644 index 0000000000..3a32aa5aa4 --- /dev/null +++ b/test/karma/unit/controllers/index.spec.js @@ -0,0 +1,32 @@ +(function() { + 'use strict'; + + describe('MEAN controllers', function() { + + describe('IndexController', function() { + + // Load the controllers module + beforeEach(module('mean')); + + var scope, + IndexController; + + beforeEach(inject(function($controller, $rootScope) { + scope = $rootScope.$new(); + + IndexController = $controller('IndexController', { + $scope: scope + }); + })); + + it('should expose some global scope', function() { + + expect(scope.global).toBeTruthy(); + + }); + + }); + + }); + +})(); \ No newline at end of file diff --git a/test/article/model.js b/test/mocha/article/model.js similarity index 97% rename from test/article/model.js rename to test/mocha/article/model.js index 844f917d7a..54800fba40 100644 --- a/test/article/model.js +++ b/test/mocha/article/model.js @@ -2,7 +2,7 @@ * Module dependencies. */ var should = require('should'), - app = require('../../server'), + app = require('../../../server'), mongoose = require('mongoose'), User = mongoose.model('User'), Article = mongoose.model('Article'); diff --git a/test/user/model.js b/test/mocha/user/model.js similarity index 97% rename from test/user/model.js rename to test/mocha/user/model.js index eb8a2e5480..c88316b99d 100644 --- a/test/user/model.js +++ b/test/mocha/user/model.js @@ -2,7 +2,7 @@ * Module dependencies. */ var should = require('should'), - app = require('../../server'), + app = require('../../../server'), mongoose = require('mongoose'), User = mongoose.model('User');