From 8097aeb96544ee06849f3e702b54f2e12ec3b7f6 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 27 Nov 2012 21:29:55 +0000 Subject: [PATCH 1/9] feat(button directive): Automatically add 'btn' class to buttons. This is a demonstration of a simple directive that just modifies the DOM (and saves typing). --- client/src/common/directives/button.js | 8 ++++++++ client/test/unit/common/directives/buttonSpec.js | 10 ++++++++++ 2 files changed, 18 insertions(+) create mode 100644 client/src/common/directives/button.js create mode 100644 client/test/unit/common/directives/buttonSpec.js diff --git a/client/src/common/directives/button.js b/client/src/common/directives/button.js new file mode 100644 index 00000000..799e0959 --- /dev/null +++ b/client/src/common/directives/button.js @@ -0,0 +1,8 @@ +angular.module('directives.button', []).directive('button', function() { + return { + restrict: 'E', + link: function(scope, element) { + element.addClass('btn'); + } + }; +}); \ No newline at end of file diff --git a/client/test/unit/common/directives/buttonSpec.js b/client/test/unit/common/directives/buttonSpec.js new file mode 100644 index 00000000..d3359dd2 --- /dev/null +++ b/client/test/unit/common/directives/buttonSpec.js @@ -0,0 +1,10 @@ +describe('button directive', function () { + beforeEach(module('directives.button')); + + it('adds a "btn" class to button elements', function() { + inject(function($compile, $rootScope) { + var element = $compile('')($rootScope); + expect(element.hasClass('btn')).toBe(true); + }); + }); +}); \ No newline at end of file From 005599fa61bf6b75fd153eee6992f41a1d14c55c Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 27 Nov 2012 21:38:51 +0000 Subject: [PATCH 2/9] refact(buttons): Remove btn css class since the button directive does this for us now. --- .../app/admin/projects/projects-edit.tpl.html | 4 +- .../app/admin/projects/projects-list.tpl.html | 2 +- .../src/app/admin/users/users-list.tpl.html | 2 +- client/src/app/app.js | 1 + client/src/app/login/form.tpl.html | 6 +- client/src/app/login/toolbar.tpl.html | 4 +- .../productbacklog-list.tpl.html | 2 +- .../projects/sprints/sprints-edit.tpl.html | 112 +++++++++--------- .../projects/sprints/sprints-list.tpl.html | 4 +- .../sprints/tasks/tasks-list.tpl.html | 2 +- .../src/common/directives/crud/crudButtons.js | 6 +- 11 files changed, 71 insertions(+), 74 deletions(-) diff --git a/client/src/app/admin/projects/projects-edit.tpl.html b/client/src/app/admin/projects/projects-edit.tpl.html index 782b4b92..10833210 100644 --- a/client/src/app/admin/projects/projects-edit.tpl.html +++ b/client/src/app/admin/projects/projects-edit.tpl.html @@ -30,13 +30,13 @@ {{usersLookup[userId].getFullName()}} - + - + diff --git a/client/src/app/admin/projects/projects-list.tpl.html b/client/src/app/admin/projects/projects-list.tpl.html index be15fa34..25d43098 100644 --- a/client/src/app/admin/projects/projects-list.tpl.html +++ b/client/src/app/admin/projects/projects-list.tpl.html @@ -13,5 +13,5 @@
- +
diff --git a/client/src/app/admin/users/users-list.tpl.html b/client/src/app/admin/users/users-list.tpl.html index 7f188243..186c52d5 100644 --- a/client/src/app/admin/users/users-list.tpl.html +++ b/client/src/app/admin/users/users-list.tpl.html @@ -15,5 +15,5 @@
- +
diff --git a/client/src/app/app.js b/client/src/app/app.js index 74e4d361..b13df3bf 100644 --- a/client/src/app/app.js +++ b/client/src/app/app.js @@ -8,6 +8,7 @@ angular.module('app', [ 'services.i18nNotifications', 'services.httpRequestTracker', 'directives.crud', + 'directives.button', 'templates']); angular.module('app').constant('MONGOLAB_CONFIG', { diff --git a/client/src/app/login/form.tpl.html b/client/src/app/login/form.tpl.html index 20110dee..3cb4d7c1 100644 --- a/client/src/app/login/form.tpl.html +++ b/client/src/app/login/form.tpl.html @@ -14,8 +14,8 @@

Sign in

diff --git a/client/src/app/login/toolbar.tpl.html b/client/src/app/login/toolbar.tpl.html index 63483ed1..1cca8561 100644 --- a/client/src/app/login/toolbar.tpl.html +++ b/client/src/app/login/toolbar.tpl.html @@ -5,12 +5,12 @@
  • \ No newline at end of file diff --git a/client/src/app/projects/productbacklog/productbacklog-list.tpl.html b/client/src/app/projects/productbacklog/productbacklog-list.tpl.html index 53e53c8f..b6335e63 100644 --- a/client/src/app/projects/productbacklog/productbacklog-list.tpl.html +++ b/client/src/app/projects/productbacklog/productbacklog-list.tpl.html @@ -20,5 +20,5 @@
    - +
    \ No newline at end of file diff --git a/client/src/app/projects/sprints/sprints-edit.tpl.html b/client/src/app/projects/sprints/sprints-edit.tpl.html index c1cba063..55e0e9cf 100644 --- a/client/src/app/projects/sprints/sprints-edit.tpl.html +++ b/client/src/app/projects/sprints/sprints-edit.tpl.html @@ -16,63 +16,59 @@

    Sprint

    - -
    -

    Sprint backlog

    -
    -
    -
    - - - - - - - - - - - - - - - - - - - - - - - -
    NameEstimationTools
    {{productBacklogLookup[sprintBacklogItem].name}}{{productBacklogLookup[sprintBacklogItem].estimation}} - -
    Estimation in total{{estimationInTotal()}}-
    -
    -
    - - - - - - - - - - - - - - - - -
    NameEstimationTools
    {{productBacklogItem.name}}{{productBacklogItem.estimation}}
    +
    +

    Sprint backlog

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    NameEstimationTools
    {{productBacklogLookup[sprintBacklogItem].name}}{{productBacklogLookup[sprintBacklogItem].estimation}} + +
    Estimation in total{{estimationInTotal()}}-
    +
    +
    + + + + + + + + + + + + + + + + +
    NameEstimationTools
    {{productBacklogItem.name}}{{productBacklogItem.estimation}}
    +
    -
    -
    -
    - - - -
    +
    + +
    \ No newline at end of file diff --git a/client/src/app/projects/sprints/sprints-list.tpl.html b/client/src/app/projects/sprints/sprints-list.tpl.html index 6d25cc0e..321bc9d2 100644 --- a/client/src/app/projects/sprints/sprints-list.tpl.html +++ b/client/src/app/projects/sprints/sprints-list.tpl.html @@ -15,7 +15,7 @@ {{sprint.end}} ACTIVE - + @@ -24,5 +24,5 @@
    - +
    \ No newline at end of file diff --git a/client/src/app/projects/sprints/tasks/tasks-list.tpl.html b/client/src/app/projects/sprints/tasks/tasks-list.tpl.html index 6102cfa3..d16e9bc0 100644 --- a/client/src/app/projects/sprints/tasks/tasks-list.tpl.html +++ b/client/src/app/projects/sprints/tasks/tasks-list.tpl.html @@ -20,5 +20,5 @@
    - +
    \ No newline at end of file diff --git a/client/src/common/directives/crud/crudButtons.js b/client/src/common/directives/crud/crudButtons.js index 0aebf891..fb4ffdfa 100644 --- a/client/src/common/directives/crud/crudButtons.js +++ b/client/src/common/directives/crud/crudButtons.js @@ -6,9 +6,9 @@ angular.module('directives.crud.buttons', []) replace:true, template: '
    ' + - ' ' + - ' '+ - ' '+ + ' ' + + ' '+ + ' '+ '
    ' }; }); \ No newline at end of file From db0c2f2913d5621cc1058c881a6bcc20b4b5ff42 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 27 Nov 2012 21:59:26 +0000 Subject: [PATCH 3/9] feat(primary-button): Add new directive to create a BootStrap primary-button. --- client/src/common/directives/button.js | 16 +++++++++++- .../test/unit/common/directives/buttonSpec.js | 26 +++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/client/src/common/directives/button.js b/client/src/common/directives/button.js index 799e0959..dcd95e67 100644 --- a/client/src/common/directives/button.js +++ b/client/src/common/directives/button.js @@ -1,8 +1,22 @@ -angular.module('directives.button', []).directive('button', function() { +angular.module('directives.button', []) + +.directive('button', function() { return { restrict: 'E', link: function(scope, element) { element.addClass('btn'); } }; +}) + +.directive('primaryButton', function() { + return { + restrict: 'E', + template: '', + transclude: true, + replace: true, + link: function(scope, element) { + element.addClass('btn-primary'); + } + }; }); \ No newline at end of file diff --git a/client/test/unit/common/directives/buttonSpec.js b/client/test/unit/common/directives/buttonSpec.js index d3359dd2..b2359ebd 100644 --- a/client/test/unit/common/directives/buttonSpec.js +++ b/client/test/unit/common/directives/buttonSpec.js @@ -1,10 +1,32 @@ describe('button directive', function () { beforeEach(module('directives.button')); - it('adds a "btn" class to button elements', function() { + it('adds a "btn" class to the button element', function() { inject(function($compile, $rootScope) { var element = $compile('')($rootScope); expect(element.hasClass('btn')).toBe(true); }); }); -}); \ No newline at end of file +}); + +describe('primaryButton directive', function () { + var element; + beforeEach(module('directives.button')); + beforeEach(function() { + inject(function($compile, $rootScope) { + element = $compile('')($rootScope); + }); + }); + + it('replaces the directive element with a button element', function() { + expect(element[0].localName).toBe('button'); + }); + + it('adds a "btn-primary" class to the button element', function() { + expect(element.hasClass('btn')).toBe(true); + }); + + it('adds a "btn" class to button elements (because it is a button!)', function() { + expect(element.hasClass('btn')).toBe(true); + }); +}); From 67bfbe0d8f63ca7eb1e3598073eabbba690d69e6 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 27 Nov 2012 22:00:01 +0000 Subject: [PATCH 4/9] refact(primary-buttons): Remove btn-primary css classes and use the primary-button directive instead. --- client/src/app/login/form.tpl.html | 2 +- client/src/app/projects/sprints/sprints-edit.tpl.html | 2 +- client/src/common/directives/crud/crudButtons.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/app/login/form.tpl.html b/client/src/app/login/form.tpl.html index 3cb4d7c1..1e4d05fe 100644 --- a/client/src/app/login/form.tpl.html +++ b/client/src/app/login/form.tpl.html @@ -14,7 +14,7 @@

    Sign in

    diff --git a/client/src/app/projects/sprints/sprints-edit.tpl.html b/client/src/app/projects/sprints/sprints-edit.tpl.html index 55e0e9cf..2a1e9bb6 100644 --- a/client/src/app/projects/sprints/sprints-edit.tpl.html +++ b/client/src/app/projects/sprints/sprints-edit.tpl.html @@ -62,7 +62,7 @@

    Sprint backlog

    {{productBacklogItem.name}} {{productBacklogItem.estimation}} - + Add to sprint diff --git a/client/src/common/directives/crud/crudButtons.js b/client/src/common/directives/crud/crudButtons.js index fb4ffdfa..4544b260 100644 --- a/client/src/common/directives/crud/crudButtons.js +++ b/client/src/common/directives/crud/crudButtons.js @@ -6,7 +6,7 @@ angular.module('directives.crud.buttons', []) replace:true, template: '
    ' + - ' ' + + ' Save' + ' '+ ' '+ '
    ' From 51ed1ada7ef23952f56ce363514a9325b048395e Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 27 Nov 2012 22:02:53 +0000 Subject: [PATCH 5/9] test(buttons): Add test to ensure that contents of the buttons are left intact. --- client/test/unit/common/directives/buttonSpec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client/test/unit/common/directives/buttonSpec.js b/client/test/unit/common/directives/buttonSpec.js index b2359ebd..2223468b 100644 --- a/client/test/unit/common/directives/buttonSpec.js +++ b/client/test/unit/common/directives/buttonSpec.js @@ -7,6 +7,13 @@ describe('button directive', function () { expect(element.hasClass('btn')).toBe(true); }); }); + + it('leaves the contents of the button intact', function() { + inject(function($compile, $rootScope) { + var element = $compile('')($rootScope); + expect(element.text()).toBe('Click Me!'); + }); + }); }); describe('primaryButton directive', function () { @@ -29,4 +36,11 @@ describe('primaryButton directive', function () { it('adds a "btn" class to button elements (because it is a button!)', function() { expect(element.hasClass('btn')).toBe(true); }); + + it('transcludes the contents of the button correctly', function() { + inject(function($compile, $rootScope) { + var element = $compile('Click Me!')($rootScope); + expect(element.text()).toBe('Click Me!'); + }); + }); }); From de8749c5245b9b9f2112d77c5fefb21a0432a47c Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 28 Nov 2012 08:50:01 +0000 Subject: [PATCH 6/9] refact(button directives): Move functionality into compile function, since it does not need to know the scope. --- client/src/common/directives/button.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/common/directives/button.js b/client/src/common/directives/button.js index dcd95e67..a97e47a6 100644 --- a/client/src/common/directives/button.js +++ b/client/src/common/directives/button.js @@ -3,7 +3,7 @@ angular.module('directives.button', []) .directive('button', function() { return { restrict: 'E', - link: function(scope, element) { + compile: function(element) { element.addClass('btn'); } }; @@ -15,7 +15,7 @@ angular.module('directives.button', []) template: '', transclude: true, replace: true, - link: function(scope, element) { + compile: function(element) { element.addClass('btn-primary'); } }; From 538f51f40671ba5b17b70272406f6a1a01af1756 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 29 Nov 2012 21:10:00 +0000 Subject: [PATCH 7/9] test(validateEquals directive): Test view changes --- .../unit/app/admin/users/admin-usersSpec.js | 76 ++++++++++++------- 1 file changed, 47 insertions(+), 29 deletions(-) diff --git a/client/test/unit/app/admin/users/admin-usersSpec.js b/client/test/unit/app/admin/users/admin-usersSpec.js index e050aea5..ff07985b 100644 --- a/client/test/unit/app/admin/users/admin-usersSpec.js +++ b/client/test/unit/app/admin/users/admin-usersSpec.js @@ -54,49 +54,67 @@ describe('admin users', function () { }); describe('validateEquals directive', function() { - var $scope, form; - - function setTestValue(value) { - $scope.model.testValue = value; - $scope.$digest(); - } - function setCompareTo(value) { - $scope.model.compareTo = value; - $scope.$digest(); - } + var $scope, modelCtrl, modelValue; beforeEach(inject(function($compile, $rootScope) { $scope = $rootScope; var element = angular.element( - '
    ' + '
    ' + + '' + + '
    ' ); - $scope.model = { - testValue: '', - compareTo: '' - }; $compile(element)($scope); + modelValue = $scope.model = {}; + modelCtrl = $scope.testForm.testInput; $scope.$digest(); - form = $scope.form; })); - describe('model validity', function() { - it('should be valid initially', function() { - expect(form.testInput.$valid).toBe(true); - }); + it('should be valid initially', function() { + expect(modelCtrl.$valid).toBeTruthy(); + }); + + describe('model value changes', function() { it('should be invalid if the model changes', function() { - setTestValue('different'); - expect(form.testInput.$valid).toBe(false); + modelValue.testValue = 'different'; + $scope.$digest(); + expect(modelCtrl.$valid).toBeFalsy(); + expect(modelCtrl.$viewValue).toBe(undefined); }); it('should be invalid if the reference model changes', function() { - setCompareTo('different'); - expect(form.testInput.$valid).toBe(false); + modelValue.compareTo = 'different'; + $scope.$digest(); + expect(modelCtrl.$valid).toBeFalsy(); + expect(modelCtrl.$viewValue).toBe(undefined); }); - it('should be valid if the model changes to be the same as the reference', function() { - setCompareTo('different'); - expect(form.testInput.$valid).toBe(false); + it('should be valid if the modelValue changes to be the same as the reference', function() { + modelValue.compareTo = 'different'; + $scope.$digest(); + expect(modelCtrl.$valid).toBeFalsy(); + + modelValue.testValue = 'different'; + $scope.$digest(); + expect(modelCtrl.$valid).toBeTruthy(); + expect(modelCtrl.$viewValue).toBe('different'); + }); + }); - setTestValue('different'); - expect(form.testInput.$valid).toBe(true); + describe('input value changes', function() { + it('should be invalid if the input value changes', function() { + modelCtrl.$setViewValue('different'); + expect(modelCtrl.$valid).toBeFalsy(); + expect(modelValue.testValue).toBe(undefined); + }); + + it('should be invalid if the input value changes to be the same as the reference', function() { + modelValue.compareTo = 'different'; + $scope.$digest(); + expect(modelCtrl.$valid).toBeFalsy(); + + modelCtrl.$setViewValue('different'); + expect(modelCtrl.$viewValue).toBe('different'); + expect(modelCtrl.$valid).toBeTruthy(); }); }); }); From 472a04c1805cc731613ca13c2739f45b58c75ee6 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 29 Nov 2012 21:10:34 +0000 Subject: [PATCH 8/9] refact(validateEquals directive): Simplify the directive code. --- .../src/app/admin/users/admin-users-edit.js | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/client/src/app/admin/users/admin-users-edit.js b/client/src/app/admin/users/admin-users-edit.js index 7145e078..dc7d1530 100644 --- a/client/src/app/admin/users/admin-users-edit.js +++ b/client/src/app/admin/users/admin-users-edit.js @@ -58,27 +58,17 @@ angular.module('admin-users-edit',['services.crud', 'services.i18nNotifications' restrict: 'A', require: 'ngModel', link: function(scope, elm, attrs, ctrl) { - - function validateEqual(myValue, otherValue) { - if (myValue === otherValue) { - ctrl.$setValidity('equal', true); - return myValue; - } else { - ctrl.$setValidity('equal', false); - return undefined; - } + function validateEqual(myValue) { + var valid = (myValue === scope.$eval(attrs.validateEquals)); + ctrl.$setValidity('equal', valid); + return valid ? myValue : undefined; } - scope.$watch(attrs.validateEquals, function(otherModelValue) { - ctrl.$setValidity('equal', ctrl.$viewValue === otherModelValue); - }); - - ctrl.$parsers.push(function(viewValue) { - return validateEqual(viewValue, scope.$eval(attrs.validateEquals)); - }); + ctrl.$parsers.push(validateEqual); + ctrl.$formatters.push(validateEqual); - ctrl.$formatters.push(function(modelValue) { - return validateEqual(modelValue, scope.$eval(attrs.validateEquals)); + scope.$watch(attrs.validateEquals, function() { + ctrl.$setViewValue(ctrl.$viewValue); }); } }; From b6f28fb0db7624ae95abe75bb15d1c71987ef441 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Tue, 4 Dec 2012 13:28:11 +0000 Subject: [PATCH 9/9] test(admin-users): Refactor uniqueEmail tests --- .../unit/app/admin/users/admin-usersSpec.js | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/client/test/unit/app/admin/users/admin-usersSpec.js b/client/test/unit/app/admin/users/admin-usersSpec.js index ff07985b..53043541 100644 --- a/client/test/unit/app/admin/users/admin-usersSpec.js +++ b/client/test/unit/app/admin/users/admin-usersSpec.js @@ -120,54 +120,54 @@ describe('admin users', function () { }); describe('uniqueEmail directive', function() { - var Users, $scope, form; + var $scope, testInput, querySpy, respondWith; - function setTestValue(value) { - $scope.model.testValue = value; - $scope.$digest(); - } - - // Mockup Users resource + // We are best to mock up the whole Users object this way because Users + // relies on so many other services, including the MONGOLAB_CONFIG constant angular.module('test', []).factory('Users', function() { - Users = jasmine.createSpyObj('Users', ['query']); - return Users; + querySpy = jasmine.createSpy('query'); + querySpy.andCallFake(function(query, response) { + // We capture the response so that the tests can call it with their own data + respondWith = response; + }); + return { query: querySpy }; }); - beforeEach(module('test')); + beforeEach(inject(function($compile, $rootScope){ $scope = $rootScope; var element = angular.element( - '
    ' + '
    ' + + '' + + '
    ' ); - $scope.model = { testValue: null}; $compile(element)($scope); + $scope.model = {}; $scope.$digest(); - form = $scope.form; + // Keep a reference to the test input for the tests + testInput = $scope.form.testInput; })); it('should be valid initially', function() { - expect(form.testInput.$valid).toBe(true); + expect(testInput.$valid).toBe(true); }); it('should not call Users.query when the model changes', function() { - setTestValue('different'); - expect(Users.query).not.toHaveBeenCalled(); + $scope.model.testValue = 'different'; + $scope.$digest(); + expect(querySpy).not.toHaveBeenCalled(); }); it('should call Users.query when the view changes', function() { - form.testInput.$setViewValue('different'); - expect(Users.query).toHaveBeenCalled(); + testInput.$setViewValue('different'); + expect(querySpy).toHaveBeenCalled(); }); - it('should set model to invalid if the Users callback contains users', function() { - Users.query.andCallFake(function(query, callback) { - callback(['someUser']); - }); - form.testInput.$setViewValue('different'); - expect(form.testInput.$valid).toBe(false); + it('should set model to invalid if the Users.query response contains users', function() { + testInput.$setViewValue('different'); + respondWith(['someUser']); + expect(testInput.$valid).toBe(false); }); - it('should set model to valid if the Users callback contains no users', function() { - Users.query.andCallFake(function(query, callback) { - callback([]); - }); - form.testInput.$setViewValue('different'); - expect(form.testInput.$valid).toBe(true); + it('should set model to valid if the Users.query response contains no users', function() { + testInput.$setViewValue('different'); + respondWith([]); + expect(testInput.$valid).toBe(true); }); }); }); \ No newline at end of file