From 34ad33ff654ff7c567d4a70779098d393eb7f819 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 01/14] step-0 bootstrap angular app - add ngApp directive to bootstrap the app - add simple template with an expression --- app/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/index.html b/app/index.html index f4c7d5c4d..910a5ef81 100644 --- a/app/index.html +++ b/app/index.html @@ -1,5 +1,5 @@ - + My HTML File @@ -8,5 +8,8 @@ + +

Nothing here {{'yet' + '!'}}

+ - \ No newline at end of file + From 69228399c1777587e6cd8abceae581397f02ddbd Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 02/14] step-1 static phone list - Added static html list with two phones into index.html --- app/css/app.css | 5 +++++ app/index.html | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/css/app.css b/app/css/app.css index 8d3eae692..e8d83bcb5 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -1 +1,6 @@ /* app css stylesheet */ + +body { + padding-top: 20px; +} + diff --git a/app/index.html b/app/index.html index 910a5ef81..7ed5b5c82 100644 --- a/app/index.html +++ b/app/index.html @@ -2,14 +2,27 @@ - My HTML File + Google Phone Gallery -

Nothing here {{'yet' + '!'}}

+ From 88615c87818e61d30dbd073a759bc73dd28d9b8e Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 18 Oct 2014 14:45:31 +0200 Subject: [PATCH 03/14] step-2 angular template with repeater - Converted the static html list into dynamic one by: - creating PhoneListCtrl controller for the application - extracting the data from HTML into a the controller as an in-memory dataset - converting the static document into a template with the use of `[ngRepeat]` [directive] which iterates over the dataset with phones, clones the ngRepeat template for each instance and renders it into the view - Added a simple unit test to show off how to write tests and run them with Karma (see README.md for instructions) --- app/index.html | 19 ++++++------------- app/js/controllers.js | 13 +++++++++++++ test/unit/controllersSpec.js | 13 ++++++++++--- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/app/index.html b/app/index.html index 7ed5b5c82..e8898b348 100644 --- a/app/index.html +++ b/app/index.html @@ -1,26 +1,19 @@ - + Google Phone Gallery + - +
    -
  • - Nexus S -

    - Fast just got faster with Nexus S. -

    -
  • -
  • - Motorola XOOM™ with Wi-Fi -

    - The Next, Next Generation tablet. -

    +
  • + {{phone.name}} +

    {{phone.snippet}}

diff --git a/app/js/controllers.js b/app/js/controllers.js index d314a3331..aa7ffe086 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -1,3 +1,16 @@ 'use strict'; /* Controllers */ + +var phonecatApp = angular.module('phonecatApp', []); + +phonecatApp.controller('PhoneListCtrl', function($scope) { + $scope.phones = [ + {'name': 'Nexus S', + 'snippet': 'Fast just got faster with Nexus S.'}, + {'name': 'Motorola XOOM™ with Wi-Fi', + 'snippet': 'The Next, Next Generation tablet.'}, + {'name': 'MOTOROLA XOOM™', + 'snippet': 'The Next, Next Generation tablet.'} + ]; +}); diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js index 63d80c3c3..9e1257c1b 100644 --- a/test/unit/controllersSpec.js +++ b/test/unit/controllersSpec.js @@ -1,11 +1,18 @@ 'use strict'; /* jasmine specs for controllers go here */ +describe('PhoneCat controllers', function() { -describe('controllers', function() { + describe('PhoneListCtrl', function(){ - it("should do something", function() { + beforeEach(module('phonecatApp')); - }); + it('should create "phones" model with 3 phones', inject(function($controller) { + var scope = {}, + ctrl = $controller('PhoneListCtrl', {$scope:scope}); + + expect(scope.phones.length).toBe(3); + })); + }); }); From 1071c2c5871bb4f02dfec6276cc02526f6dca2ca Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sun, 19 Oct 2014 09:19:13 +0100 Subject: [PATCH 04/14] step-3 interactive search - Added a search box to demonstrate how: - the data-binding works on input fields - to use [filter] filter - [ngRepeat] automatically shrinks and grows the number of phones in the view - Added an end-to-end test to: - show how end-to-end tests are written and used - to prove that the search box and the repeater are correctly wired together --- app/index.html | 27 +++++++++++++++++++++------ test/e2e/scenarios.js | 26 ++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/app/index.html b/app/index.html index e8898b348..1e2d0b4f0 100644 --- a/app/index.html +++ b/app/index.html @@ -10,12 +10,27 @@ -
    -
  • - {{phone.name}} -

    {{phone.snippet}}

    -
  • -
+
+
+
+ + + Search: + +
+
+ + +
    +
  • + {{phone.name}} +

    {{phone.snippet}}

    +
  • +
+ +
+
+
diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index ed4b2c3e7..5d9013b60 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -2,10 +2,28 @@ /* http://docs.angularjs.org/guide/dev_guide.e2e-testing */ -describe('my app', function() { +describe('PhoneCat App', function() { - beforeEach(function() { - browser.get('app/index.html'); - }); + describe('Phone list view', function() { + + beforeEach(function() { + browser.get('app/index.html'); + }); + + + it('should filter the phone list as a user types into the search box', function() { + var phoneList = element.all(by.repeater('phone in phones')); + var query = element(by.model('query')); + + expect(phoneList.count()).toBe(3); + + query.sendKeys('nexus'); + expect(phoneList.count()).toBe(1); + + query.clear(); + query.sendKeys('motorola'); + expect(phoneList.count()).toBe(2); + }); + }); }); From b168e8f3a754bd4db5dee71a80a826f23b91c0a2 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sun, 19 Oct 2014 09:19:49 +0100 Subject: [PATCH 05/14] step-4 phone ordering - Add "age" property to the phone model - Add select box to control phone list order - Override the default order value in controller - Add unit and e2e test for this feature Closes #213 --- app/index.html | 7 ++++++- app/js/controllers.js | 11 ++++++++--- test/e2e/scenarios.js | 27 +++++++++++++++++++++++++++ test/unit/controllersSpec.js | 16 ++++++++++++---- 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/app/index.html b/app/index.html index 1e2d0b4f0..414443d2c 100644 --- a/app/index.html +++ b/app/index.html @@ -16,13 +16,18 @@ Search: + Sort by: +
    -
  • +
  • {{phone.name}}

    {{phone.snippet}}

  • diff --git a/app/js/controllers.js b/app/js/controllers.js index aa7ffe086..8af52cd09 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -7,10 +7,15 @@ var phonecatApp = angular.module('phonecatApp', []); phonecatApp.controller('PhoneListCtrl', function($scope) { $scope.phones = [ {'name': 'Nexus S', - 'snippet': 'Fast just got faster with Nexus S.'}, + 'snippet': 'Fast just got faster with Nexus S.', + 'age': 1}, {'name': 'Motorola XOOM™ with Wi-Fi', - 'snippet': 'The Next, Next Generation tablet.'}, + 'snippet': 'The Next, Next Generation tablet.', + 'age': 2}, {'name': 'MOTOROLA XOOM™', - 'snippet': 'The Next, Next Generation tablet.'} + 'snippet': 'The Next, Next Generation tablet.', + 'age': 3} ]; + + $scope.orderProp = 'age'; }); diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index 5d9013b60..b0a0bf1c6 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -25,5 +25,32 @@ describe('PhoneCat App', function() { query.sendKeys('motorola'); expect(phoneList.count()).toBe(2); }); + + + it('should be possible to control phone order via the drop down select box', function() { + + var phoneNameColumn = element.all(by.repeater('phone in phones').column('phone.name')); + var query = element(by.model('query')); + + function getNames() { + return phoneNameColumn.map(function(elm) { + return elm.getText(); + }); + } + + query.sendKeys('tablet'); //let's narrow the dataset to make the test assertions shorter + + expect(getNames()).toEqual([ + "Motorola XOOM\u2122 with Wi-Fi", + "MOTOROLA XOOM\u2122" + ]); + + element(by.model('orderProp')).element(by.css('option[value="name"]')).click(); + + expect(getNames()).toEqual([ + "MOTOROLA XOOM\u2122", + "Motorola XOOM\u2122 with Wi-Fi" + ]); + }); }); }); diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js index 9e1257c1b..353d20dd1 100644 --- a/test/unit/controllersSpec.js +++ b/test/unit/controllersSpec.js @@ -4,15 +4,23 @@ describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ + var scope, ctrl; beforeEach(module('phonecatApp')); - it('should create "phones" model with 3 phones', inject(function($controller) { - var scope = {}, - ctrl = $controller('PhoneListCtrl', {$scope:scope}); + beforeEach(inject(function($controller) { + scope = {}; + ctrl = $controller('PhoneListCtrl', {$scope:scope}); + })); + + it('should create "phones" model with 3 phones', function() { expect(scope.phones.length).toBe(3); - })); + }); + + it('should set the default value of orderProp model', function() { + expect(scope.orderProp).toBe('age'); + }); }); }); From c74c9f546caad279a5a6c8c54ca41a82bc03f4fb Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 06/14] step-5 XHR and dependency injection - Replaced the in-memory dataset with data loaded from the server (in the form of static phone.json file to make this tutorial backend agnostic) - The json file is loaded using the [$http] service - Demonstrate the use of [services][service] and [dependency injection][DI] - The [$http] is injected into the controller through [dependency injection][DI] (Added inline annotation - thanks to @guatebus - closes #207) --- app/js/controllers.js | 18 +++++------------- test/e2e/scenarios.js | 4 ++-- test/unit/controllersSpec.js | 19 +++++++++++++------ 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/app/js/controllers.js b/app/js/controllers.js index 8af52cd09..9584280e6 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -4,18 +4,10 @@ var phonecatApp = angular.module('phonecatApp', []); -phonecatApp.controller('PhoneListCtrl', function($scope) { - $scope.phones = [ - {'name': 'Nexus S', - 'snippet': 'Fast just got faster with Nexus S.', - 'age': 1}, - {'name': 'Motorola XOOM™ with Wi-Fi', - 'snippet': 'The Next, Next Generation tablet.', - 'age': 2}, - {'name': 'MOTOROLA XOOM™', - 'snippet': 'The Next, Next Generation tablet.', - 'age': 3} - ]; +phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) { + $http.get('phones/phones.json').success(function(data) { + $scope.phones = data; + }); $scope.orderProp = 'age'; -}); +}]); diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index b0a0bf1c6..526e12381 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -16,14 +16,14 @@ describe('PhoneCat App', function() { var phoneList = element.all(by.repeater('phone in phones')); var query = element(by.model('query')); - expect(phoneList.count()).toBe(3); + expect(phoneList.count()).toBe(20); query.sendKeys('nexus'); expect(phoneList.count()).toBe(1); query.clear(); query.sendKeys('motorola'); - expect(phoneList.count()).toBe(2); + expect(phoneList.count()).toBe(8); }); diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js index 353d20dd1..2e4e78028 100644 --- a/test/unit/controllersSpec.js +++ b/test/unit/controllersSpec.js @@ -4,18 +4,25 @@ describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ - var scope, ctrl; + var scope, ctrl, $httpBackend; beforeEach(module('phonecatApp')); + beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { + $httpBackend = _$httpBackend_; + $httpBackend.expectGET('phones/phones.json'). + respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); - beforeEach(inject(function($controller) { - scope = {}; - ctrl = $controller('PhoneListCtrl', {$scope:scope}); + scope = $rootScope.$new(); + ctrl = $controller('PhoneListCtrl', {$scope: scope}); })); - it('should create "phones" model with 3 phones', function() { - expect(scope.phones.length).toBe(3); + it('should create "phones" model with 2 phones fetched from xhr', function() { + expect(scope.phones).toBeUndefined(); + $httpBackend.flush(); + + expect(scope.phones).toEqual([{name: 'Nexus S'}, + {name: 'Motorola DROID'}]); }); From 5b569b2d8f377b2aad529e4a34b70c73bd4cca66 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 07/14] step-6 phone images and links - adding phone image and links to phone pages - add end2end test that verifies our phone links - css to style the page just a notch --- app/css/app.css | 17 +++++++++++++++++ app/index.html | 5 +++-- test/e2e/scenarios.js | 10 ++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/app/css/app.css b/app/css/app.css index e8d83bcb5..9e156c0d1 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -4,3 +4,20 @@ body { padding-top: 20px; } +.phones { + list-style: none; +} + +.thumb { + float: left; + margin: -0.5em 1em 1.5em 0; + padding-bottom: 1em; + height: 100px; + width: 100px; +} + +.phones li { + clear: both; + height: 115px; + padding-top: 15px; +} diff --git a/app/index.html b/app/index.html index 414443d2c..4f7bc0ce0 100644 --- a/app/index.html +++ b/app/index.html @@ -27,8 +27,9 @@ diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index 526e12381..27676f4f8 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -52,5 +52,15 @@ describe('PhoneCat App', function() { "Motorola XOOM\u2122 with Wi-Fi" ]); }); + + + it('should render phone specific links', function() { + var query = element(by.model('query')); + query.sendKeys('nexus'); + element(by.css('.phones li a')).click(); + browser.getLocationAbsUrl().then(function(url) { + expect(url).toEqual('/phones/nexus-s'); + }); + }); }); }); From 042c66a513282b83310d0a1fc686a20b26cdd156 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 08/14] step-7 $route and app partitioning - Introduce the [$route] service which allows binding URLs for deep-linking with views - Create PhoneCatCtrl which governs the entire app and contains $route configuration - Load the ngRoute module - Map `/phones' to PhoneListCtrl and partails/phones-list.html - Map `/phones/' to PhoneDetailCtrl and partails/phones-detail.html - Copy deep linking parameters to root controller `params` property for access in sub controllers - Replace content of index.html with [ngView] directive - Create phone list route - Preserve existing PhoneListCtrl controller - Move existing html from index.html to partials/phone-list.html - Create phone details route - Empty placeholder PhoneDetailsCtrl controller - Empty placeholder partials/phane-details.html template --- app/index.html | 32 ++++---------------------------- app/js/app.js | 21 +++++++++++++++++++++ app/js/controllers.js | 20 +++++++++++++------- app/partials/phone-detail.html | 1 + app/partials/phone-list.html | 27 +++++++++++++++++++++++++++ bower.json | 3 ++- test/e2e/scenarios.js | 23 ++++++++++++++++++++++- test/unit/controllersSpec.js | 4 ++++ 8 files changed, 94 insertions(+), 37 deletions(-) create mode 100644 app/partials/phone-detail.html create mode 100644 app/partials/phone-list.html diff --git a/app/index.html b/app/index.html index 4f7bc0ce0..53f590674 100644 --- a/app/index.html +++ b/app/index.html @@ -6,37 +6,13 @@ + + - + -
    -
    -
    - - - Search: - Sort by: - - -
    -
    - - - - -
    -
    -
    +
    diff --git a/app/js/app.js b/app/js/app.js index 7a8f274a0..7b24e7d4c 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -1,3 +1,24 @@ 'use strict'; /* App Module */ + +var phonecatApp = angular.module('phonecatApp', [ + 'ngRoute', + 'phonecatControllers' +]); + +phonecatApp.config(['$routeProvider', + function($routeProvider) { + $routeProvider. + when('/phones', { + templateUrl: 'partials/phone-list.html', + controller: 'PhoneListCtrl' + }). + when('/phones/:phoneId', { + templateUrl: 'partials/phone-detail.html', + controller: 'PhoneDetailCtrl' + }). + otherwise({ + redirectTo: '/phones' + }); + }]); diff --git a/app/js/controllers.js b/app/js/controllers.js index 9584280e6..25b6ee972 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -2,12 +2,18 @@ /* Controllers */ -var phonecatApp = angular.module('phonecatApp', []); +var phonecatControllers = angular.module('phonecatControllers', []); -phonecatApp.controller('PhoneListCtrl', ['$scope', '$http', function($scope, $http) { - $http.get('phones/phones.json').success(function(data) { - $scope.phones = data; - }); +phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http', + function($scope, $http) { + $http.get('phones/phones.json').success(function(data) { + $scope.phones = data; + }); - $scope.orderProp = 'age'; -}]); + $scope.orderProp = 'age'; + }]); + +phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', + function($scope, $routeParams) { + $scope.phoneId = $routeParams.phoneId; + }]); diff --git a/app/partials/phone-detail.html b/app/partials/phone-detail.html new file mode 100644 index 000000000..6656d6f43 --- /dev/null +++ b/app/partials/phone-detail.html @@ -0,0 +1 @@ +TBD: detail view for {{phoneId}} diff --git a/app/partials/phone-list.html b/app/partials/phone-list.html new file mode 100644 index 000000000..a66a6d694 --- /dev/null +++ b/app/partials/phone-list.html @@ -0,0 +1,27 @@ +
    +
    +
    + + + Search: + Sort by: + + +
    +
    + + + + +
    +
    +
    diff --git a/bower.json b/bower.json index ced73f6da..4869d80e1 100644 --- a/bower.json +++ b/bower.json @@ -9,6 +9,7 @@ "angular": "1.4.x", "angular-mocks": "1.4.x", "jquery": "~2.1.1", - "bootstrap": "~3.1.1" + "bootstrap": "~3.1.1", + "angular-route": "1.4.x" } } diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index 27676f4f8..34019f1c6 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -4,10 +4,18 @@ describe('PhoneCat App', function() { + it('should redirect index.html to index.html#/phones', function() { + browser.get('app/index.html'); + browser.getLocationAbsUrl().then(function(url) { + expect(url).toEqual('/phones'); + }); + }); + + describe('Phone list view', function() { beforeEach(function() { - browser.get('app/index.html'); + browser.get('app/index.html#/phones'); }); @@ -63,4 +71,17 @@ describe('PhoneCat App', function() { }); }); }); + + + describe('Phone detail view', function() { + + beforeEach(function() { + browser.get('app/index.html#/phones/nexus-s'); + }); + + + it('should display placeholder page with phoneId', function() { + expect(element(by.binding('phoneId')).getText()).toBe('nexus-s'); + }); + }); }); diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js index 2e4e78028..a755e4a6e 100644 --- a/test/unit/controllersSpec.js +++ b/test/unit/controllersSpec.js @@ -30,4 +30,8 @@ describe('PhoneCat controllers', function() { expect(scope.orderProp).toBe('age'); }); }); + + + describe('PhoneDetailCtrl', function(){ + }); }); From 561d13b5c4f0c19b921ddbe2c8fd694e080c1341 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 09/14] step-8 phone details view - Fetch data for and render phone detail view - PhoneDetailCtrl controller to fetch details json with [$http] for a specific phone - template for the phone detailed view - CSS to make the phone details page look "pretty" --- app/css/app.css | 56 ++++++++++++++++ app/js/controllers.js | 8 ++- app/partials/phone-detail.html | 114 ++++++++++++++++++++++++++++++++- test/e2e/scenarios.js | 6 +- test/unit/controllersSpec.js | 21 +++++- 5 files changed, 197 insertions(+), 8 deletions(-) diff --git a/app/css/app.css b/app/css/app.css index 9e156c0d1..aff07e85a 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -21,3 +21,59 @@ body { height: 115px; padding-top: 15px; } + +/** Detail View **/ +img.phone { + float: left; + border: 1px solid black; + margin-right: 3em; + margin-bottom: 2em; + background-color: white; + padding: 2em; + height: 400px; + width: 400px; +} + +ul.phone-thumbs { + margin: 0; + list-style: none; +} + +ul.phone-thumbs li { + border: 1px solid black; + display: inline-block; + margin: 1em; + background-color: white; +} + +ul.phone-thumbs img { + height: 100px; + width: 100px; + padding: 1em; +} + +ul.specs { + clear: both; + margin: 0; + padding: 0; + list-style: none; +} + +ul.specs > li{ + display: inline-block; + width: 200px; + vertical-align: top; +} + +ul.specs > li > span{ + font-weight: bold; + font-size: 1.2em; +} + +ul.specs dt { + font-weight: bold; +} + +h1 { + border-bottom: 1px solid gray; +} diff --git a/app/js/controllers.js b/app/js/controllers.js index 25b6ee972..2a51d20eb 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -13,7 +13,9 @@ phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http', $scope.orderProp = 'age'; }]); -phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', - function($scope, $routeParams) { - $scope.phoneId = $routeParams.phoneId; +phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http', + function($scope, $routeParams, $http) { + $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) { + $scope.phone = data; + }); }]); diff --git a/app/partials/phone-detail.html b/app/partials/phone-detail.html index 6656d6f43..900ffd84d 100644 --- a/app/partials/phone-detail.html +++ b/app/partials/phone-detail.html @@ -1 +1,113 @@ -TBD: detail view for {{phoneId}} + + +

    {{phone.name}}

    + +

    {{phone.description}}

    + +
      +
    • + +
    • +
    + +
      +
    • + Availability and Networks +
      +
      Availability
      +
      {{availability}}
      +
      +
    • +
    • + Battery +
      +
      Type
      +
      {{phone.battery.type}}
      +
      Talk Time
      +
      {{phone.battery.talkTime}}
      +
      Standby time (max)
      +
      {{phone.battery.standbyTime}}
      +
      +
    • +
    • + Storage and Memory +
      +
      RAM
      +
      {{phone.storage.ram}}
      +
      Internal Storage
      +
      {{phone.storage.flash}}
      +
      +
    • +
    • + Connectivity +
      +
      Network Support
      +
      {{phone.connectivity.cell}}
      +
      WiFi
      +
      {{phone.connectivity.wifi}}
      +
      Bluetooth
      +
      {{phone.connectivity.bluetooth}}
      +
      Infrared
      +
      {{phone.connectivity.infrared}}
      +
      GPS
      +
      {{phone.connectivity.gps}}
      +
      +
    • +
    • + Android +
      +
      OS Version
      +
      {{phone.android.os}}
      +
      UI
      +
      {{phone.android.ui}}
      +
      +
    • +
    • + Size and Weight +
      +
      Dimensions
      +
      {{dim}}
      +
      Weight
      +
      {{phone.sizeAndWeight.weight}}
      +
      +
    • +
    • + Display +
      +
      Screen size
      +
      {{phone.display.screenSize}}
      +
      Screen resolution
      +
      {{phone.display.screenResolution}}
      +
      Touch screen
      +
      {{phone.display.touchScreen}}
      +
      +
    • +
    • + Hardware +
      +
      CPU
      +
      {{phone.hardware.cpu}}
      +
      USB
      +
      {{phone.hardware.usb}}
      +
      Audio / headphone jack
      +
      {{phone.hardware.audioJack}}
      +
      FM Radio
      +
      {{phone.hardware.fmRadio}}
      +
      Accelerometer
      +
      {{phone.hardware.accelerometer}}
      +
      +
    • +
    • + Camera +
      +
      Primary
      +
      {{phone.camera.primary}}
      +
      Features
      +
      {{phone.camera.features.join(', ')}}
      +
      +
    • +
    • + Additional Features +
      {{phone.additionalFeatures}}
      +
    • +
    diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index 34019f1c6..d4d2903f4 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -65,7 +65,7 @@ describe('PhoneCat App', function() { it('should render phone specific links', function() { var query = element(by.model('query')); query.sendKeys('nexus'); - element(by.css('.phones li a')).click(); + element.all(by.css('.phones li a')).first().click(); browser.getLocationAbsUrl().then(function(url) { expect(url).toEqual('/phones/nexus-s'); }); @@ -80,8 +80,8 @@ describe('PhoneCat App', function() { }); - it('should display placeholder page with phoneId', function() { - expect(element(by.binding('phoneId')).getText()).toBe('nexus-s'); + it('should display nexus-s page', function() { + expect(element(by.binding('phone.name')).getText()).toBe('Nexus S'); }); }); }); diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js index a755e4a6e..f91d821e5 100644 --- a/test/unit/controllersSpec.js +++ b/test/unit/controllersSpec.js @@ -3,10 +3,11 @@ /* jasmine specs for controllers go here */ describe('PhoneCat controllers', function() { + beforeEach(module('phonecatApp')); + describe('PhoneListCtrl', function(){ var scope, ctrl, $httpBackend; - beforeEach(module('phonecatApp')); beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/phones.json'). @@ -33,5 +34,23 @@ describe('PhoneCat controllers', function() { describe('PhoneDetailCtrl', function(){ + var scope, $httpBackend, ctrl; + + beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { + $httpBackend = _$httpBackend_; + $httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'}); + + $routeParams.phoneId = 'xyz'; + scope = $rootScope.$new(); + ctrl = $controller('PhoneDetailCtrl', {$scope: scope}); + })); + + + it('should fetch phone detail', function() { + expect(scope.phone).toBeUndefined(); + $httpBackend.flush(); + + expect(scope.phone).toEqual({name:'phone xyz'}); + }); }); }); From 471d0f6d4a25c4f99a3f5ce6a4cfd4f8823c8fd9 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 10/14] step-9 checkmark filter - Added custom checkmark filter - Update phone detail template to use checkmark filter - Added spec for the filter --- app/index.html | 1 + app/js/app.js | 3 ++- app/js/filters.js | 6 ++++++ app/partials/phone-detail.html | 10 +++++----- test/unit/filtersSpec.js | 11 +++++++++++ 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/index.html b/app/index.html index 53f590674..599502ee8 100644 --- a/app/index.html +++ b/app/index.html @@ -9,6 +9,7 @@ + diff --git a/app/js/app.js b/app/js/app.js index 7b24e7d4c..7238d2166 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -4,7 +4,8 @@ var phonecatApp = angular.module('phonecatApp', [ 'ngRoute', - 'phonecatControllers' + 'phonecatControllers', + 'phonecatFilters' ]); phonecatApp.config(['$routeProvider', diff --git a/app/js/filters.js b/app/js/filters.js index 85e8440f8..4f62309ba 100644 --- a/app/js/filters.js +++ b/app/js/filters.js @@ -1,3 +1,9 @@ 'use strict'; /* Filters */ + +angular.module('phonecatFilters', []).filter('checkmark', function() { + return function(input) { + return input ? '\u2713' : '\u2718'; + }; +}); diff --git a/app/partials/phone-detail.html b/app/partials/phone-detail.html index 900ffd84d..310441cf2 100644 --- a/app/partials/phone-detail.html +++ b/app/partials/phone-detail.html @@ -48,9 +48,9 @@

    {{phone.name}}

    Bluetooth
    {{phone.connectivity.bluetooth}}
    Infrared
    -
    {{phone.connectivity.infrared}}
    +
    {{phone.connectivity.infrared | checkmark}}
    GPS
    -
    {{phone.connectivity.gps}}
    +
    {{phone.connectivity.gps | checkmark}}
  • @@ -79,7 +79,7 @@

    {{phone.name}}

    Screen resolution
    {{phone.display.screenResolution}}
    Touch screen
    -
    {{phone.display.touchScreen}}
    +
    {{phone.display.touchScreen | checkmark}}
  • @@ -92,9 +92,9 @@

    {{phone.name}}

    Audio / headphone jack
    {{phone.hardware.audioJack}}
    FM Radio
    -
    {{phone.hardware.fmRadio}}
    +
    {{phone.hardware.fmRadio | checkmark}}
    Accelerometer
    -
    {{phone.hardware.accelerometer}}
    +
    {{phone.hardware.accelerometer | checkmark}}
  • diff --git a/test/unit/filtersSpec.js b/test/unit/filtersSpec.js index 5fdc76a26..e5cbb7262 100644 --- a/test/unit/filtersSpec.js +++ b/test/unit/filtersSpec.js @@ -4,4 +4,15 @@ describe('filter', function() { + beforeEach(module('phonecatFilters')); + + + describe('checkmark', function() { + + it('should convert boolean values to unicode checkmark or cross', + inject(function(checkmarkFilter) { + expect(checkmarkFilter(true)).toBe('\u2713'); + expect(checkmarkFilter(false)).toBe('\u2718'); + })); + }); }); From 49b278cc050cb447f0efd3475bc90456d97208e9 Mon Sep 17 00:00:00 2001 From: Igor Minar Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 11/14] step-10 image swapping with ng:click In the phone detail view, clicking on a thumbnail image, changes the main phone image to be the large version of the thumbnail image. - Define mainImageUrl model variable in the PhoneDetailCtrl and set its default value - Create setImage controller method to change mainImageUrl - Register ng:click handler for thumb images to use setImage controller method - Add e2e tests for this feature - Add css to change the mouse cursor when user points at thumnail images --- app/css/app.css | 5 +++++ app/js/controllers.js | 5 +++++ app/partials/phone-detail.html | 4 ++-- test/e2e/scenarios.js | 14 ++++++++++++++ test/unit/controllersSpec.js | 13 ++++++++++--- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app/css/app.css b/app/css/app.css index aff07e85a..78bab744d 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -52,6 +52,11 @@ ul.phone-thumbs img { padding: 1em; } +ul.phone-thumbs img:hover { + cursor: pointer; +} + + ul.specs { clear: both; margin: 0; diff --git a/app/js/controllers.js b/app/js/controllers.js index 2a51d20eb..b884837c9 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -17,5 +17,10 @@ phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$h function($scope, $routeParams, $http) { $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) { $scope.phone = data; + $scope.mainImageUrl = data.images[0]; }); + + $scope.setImage = function(imageUrl) { + $scope.mainImageUrl = imageUrl; + }; }]); diff --git a/app/partials/phone-detail.html b/app/partials/phone-detail.html index 310441cf2..5785e94ca 100644 --- a/app/partials/phone-detail.html +++ b/app/partials/phone-detail.html @@ -1,4 +1,4 @@ - +

    {{phone.name}}

    @@ -6,7 +6,7 @@

    {{phone.name}}

    • - +
    diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index d4d2903f4..e81832aa6 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -83,5 +83,19 @@ describe('PhoneCat App', function() { it('should display nexus-s page', function() { expect(element(by.binding('phone.name')).getText()).toBe('Nexus S'); }); + + + it('should display the first phone image as the main phone image', function() { + expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); + }); + + + it('should swap main image if a thumbnail image is clicked on', function() { + element(by.css('.phone-thumbs li:nth-child(3) img')).click(); + expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/); + + element(by.css('.phone-thumbs li:nth-child(1) img')).click(); + expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); + }); }); }); diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js index f91d821e5..904dea344 100644 --- a/test/unit/controllersSpec.js +++ b/test/unit/controllersSpec.js @@ -34,11 +34,18 @@ describe('PhoneCat controllers', function() { describe('PhoneDetailCtrl', function(){ - var scope, $httpBackend, ctrl; + var scope, $httpBackend, ctrl, + xyzPhoneData = function() { + return { + name: 'phone xyz', + images: ['image/url1.png', 'image/url2.png'] + } + }; + beforeEach(inject(function(_$httpBackend_, $rootScope, $routeParams, $controller) { $httpBackend = _$httpBackend_; - $httpBackend.expectGET('phones/xyz.json').respond({name:'phone xyz'}); + $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData()); $routeParams.phoneId = 'xyz'; scope = $rootScope.$new(); @@ -50,7 +57,7 @@ describe('PhoneCat controllers', function() { expect(scope.phone).toBeUndefined(); $httpBackend.flush(); - expect(scope.phone).toEqual({name:'phone xyz'}); + expect(scope.phone).toEqual(xyzPhoneData()); }); }); }); From a4a78ee25b40b1b051f7120314a27cd836262da9 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 12/14] step-11 custom service and $resource - Replaced [$http] with [$resource] - Created a custom Phone service that represents the $resource client --- app/index.html | 2 ++ app/js/app.js | 3 ++- app/js/controllers.js | 18 +++++++----------- app/js/services.js | 8 ++++++++ bower.json | 3 ++- test/karma.conf.js | 1 + test/unit/controllersSpec.js | 19 ++++++++++++++----- test/unit/servicesSpec.js | 11 ++++++++--- 8 files changed, 44 insertions(+), 21 deletions(-) diff --git a/app/index.html b/app/index.html index 599502ee8..02e7a01a5 100644 --- a/app/index.html +++ b/app/index.html @@ -7,9 +7,11 @@ + + diff --git a/app/js/app.js b/app/js/app.js index 7238d2166..7103787cc 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -5,7 +5,8 @@ var phonecatApp = angular.module('phonecatApp', [ 'ngRoute', 'phonecatControllers', - 'phonecatFilters' + 'phonecatFilters', + 'phonecatServices' ]); phonecatApp.config(['$routeProvider', diff --git a/app/js/controllers.js b/app/js/controllers.js index b884837c9..13e3c487d 100644 --- a/app/js/controllers.js +++ b/app/js/controllers.js @@ -4,20 +4,16 @@ var phonecatControllers = angular.module('phonecatControllers', []); -phonecatControllers.controller('PhoneListCtrl', ['$scope', '$http', - function($scope, $http) { - $http.get('phones/phones.json').success(function(data) { - $scope.phones = data; - }); - +phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone', + function($scope, Phone) { + $scope.phones = Phone.query(); $scope.orderProp = 'age'; }]); -phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$http', - function($scope, $routeParams, $http) { - $http.get('phones/' + $routeParams.phoneId + '.json').success(function(data) { - $scope.phone = data; - $scope.mainImageUrl = data.images[0]; +phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', 'Phone', + function($scope, $routeParams, Phone) { + $scope.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) { + $scope.mainImageUrl = phone.images[0]; }); $scope.setImage = function(imageUrl) { diff --git a/app/js/services.js b/app/js/services.js index 8207480df..e0b81a8ac 100644 --- a/app/js/services.js +++ b/app/js/services.js @@ -2,3 +2,11 @@ /* Services */ +var phonecatServices = angular.module('phonecatServices', ['ngResource']); + +phonecatServices.factory('Phone', ['$resource', + function($resource){ + return $resource('phones/:phoneId.json', {}, { + query: {method:'GET', params:{phoneId:'phones'}, isArray:true} + }); + }]); diff --git a/bower.json b/bower.json index 4869d80e1..783bf7636 100644 --- a/bower.json +++ b/bower.json @@ -10,6 +10,7 @@ "angular-mocks": "1.4.x", "jquery": "~2.1.1", "bootstrap": "~3.1.1", - "angular-route": "1.4.x" + "angular-route": "1.4.x", + "angular-resource": "1.4.x" } } diff --git a/test/karma.conf.js b/test/karma.conf.js index ab92fa9fb..b6939eeea 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -6,6 +6,7 @@ module.exports = function(config){ files : [ 'app/bower_components/angular/angular.js', 'app/bower_components/angular-route/angular-route.js', + 'app/bower_components/angular-resource/angular-resource.js', 'app/bower_components/angular-mocks/angular-mocks.js', 'app/js/**/*.js', 'test/unit/**/*.js' diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js index 904dea344..e85cc111a 100644 --- a/test/unit/controllersSpec.js +++ b/test/unit/controllersSpec.js @@ -3,7 +3,16 @@ /* jasmine specs for controllers go here */ describe('PhoneCat controllers', function() { + beforeEach(function(){ + this.addMatchers({ + toEqualData: function(expected) { + return angular.equals(this.actual, expected); + } + }); + }); + beforeEach(module('phonecatApp')); + beforeEach(module('phonecatServices')); describe('PhoneListCtrl', function(){ var scope, ctrl, $httpBackend; @@ -19,11 +28,11 @@ describe('PhoneCat controllers', function() { it('should create "phones" model with 2 phones fetched from xhr', function() { - expect(scope.phones).toBeUndefined(); + expect(scope.phones).toEqualData([]); $httpBackend.flush(); - expect(scope.phones).toEqual([{name: 'Nexus S'}, - {name: 'Motorola DROID'}]); + expect(scope.phones).toEqualData( + [{name: 'Nexus S'}, {name: 'Motorola DROID'}]); }); @@ -54,10 +63,10 @@ describe('PhoneCat controllers', function() { it('should fetch phone detail', function() { - expect(scope.phone).toBeUndefined(); + expect(scope.phone).toEqualData({}); $httpBackend.flush(); - expect(scope.phone).toEqual(xyzPhoneData()); + expect(scope.phone).toEqualData(xyzPhoneData()); }); }); }); diff --git a/test/unit/servicesSpec.js b/test/unit/servicesSpec.js index db8a232de..b495870cb 100644 --- a/test/unit/servicesSpec.js +++ b/test/unit/servicesSpec.js @@ -1,7 +1,12 @@ 'use strict'; -/* jasmine specs for services go here */ - describe('service', function() { -}); + // load modules + beforeEach(module('phonecatApp')); + + // Test service availability + it('check the existence of Phone factory', inject(function(Phone) { + expect(Phone).toBeDefined(); + })); +}); \ No newline at end of file From 5e54104e8d98cdb0cdbf7210af5293e9371f76e5 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Mon, 10 Nov 2014 09:27:44 +0000 Subject: [PATCH 13/14] step-12 animations --- app/css/animations.css | 97 ++++++++++++++++++++++++++++++++++ app/css/app.css | 17 +++++- app/index.html | 9 +++- app/js/animations.js | 52 ++++++++++++++++++ app/js/app.js | 2 + app/partials/phone-detail.html | 7 ++- app/partials/phone-list.html | 3 +- bower.json | 3 +- test/e2e/scenarios.js | 6 +-- test/karma.conf.js | 1 + 10 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 app/css/animations.css create mode 100644 app/js/animations.js diff --git a/app/css/animations.css b/app/css/animations.css new file mode 100644 index 000000000..46f3da6ec --- /dev/null +++ b/app/css/animations.css @@ -0,0 +1,97 @@ +/* + * animations css stylesheet + */ + +/* animate ngRepeat in phone listing */ + +.phone-listing.ng-enter, +.phone-listing.ng-leave, +.phone-listing.ng-move { + -webkit-transition: 0.5s linear all; + -moz-transition: 0.5s linear all; + -o-transition: 0.5s linear all; + transition: 0.5s linear all; +} + +.phone-listing.ng-enter, +.phone-listing.ng-move { + opacity: 0; + height: 0; + overflow: hidden; +} + +.phone-listing.ng-move.ng-move-active, +.phone-listing.ng-enter.ng-enter-active { + opacity: 1; + height: 120px; +} + +.phone-listing.ng-leave { + opacity: 1; + overflow: hidden; +} + +.phone-listing.ng-leave.ng-leave-active { + opacity: 0; + height: 0; + padding-top: 0; + padding-bottom: 0; +} + +/* cross fading between routes with ngView */ + +.view-container { + position: relative; +} + +.view-frame.ng-enter, +.view-frame.ng-leave { + background: white; + position: absolute; + top: 0; + left: 0; + right: 0; +} + +.view-frame.ng-enter { + -webkit-animation: 0.5s fade-in; + -moz-animation: 0.5s fade-in; + -o-animation: 0.5s fade-in; + animation: 0.5s fade-in; + z-index: 100; +} + +.view-frame.ng-leave { + -webkit-animation: 0.5s fade-out; + -moz-animation: 0.5s fade-out; + -o-animation: 0.5s fade-out; + animation: 0.5s fade-out; + z-index: 99; +} + +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +@-moz-keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +@-webkit-keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fade-out { + from { opacity: 1; } + to { opacity: 0; } +} +@-moz-keyframes fade-out { + from { opacity: 1; } + to { opacity: 0; } +} +@-webkit-keyframes fade-out { + from { opacity: 1; } + to { opacity: 0; } +} + diff --git a/app/css/app.css b/app/css/app.css index 78bab744d..951ea087c 100644 --- a/app/css/app.css +++ b/app/css/app.css @@ -4,6 +4,16 @@ body { padding-top: 20px; } + +.phone-images { + background-color: white; + width: 450px; + height: 450px; + overflow: hidden; + position: relative; + float: left; +} + .phones { list-style: none; } @@ -25,15 +35,20 @@ body { /** Detail View **/ img.phone { float: left; - border: 1px solid black; margin-right: 3em; margin-bottom: 2em; background-color: white; padding: 2em; height: 400px; width: 400px; + display: none; +} + +img.phone:first-child { + display: block; } + ul.phone-thumbs { margin: 0; list-style: none; diff --git a/app/index.html b/app/index.html index 02e7a01a5..e2e20d4be 100644 --- a/app/index.html +++ b/app/index.html @@ -5,17 +5,24 @@ Google Phone Gallery + + + + + -
    +
    +
    +
    diff --git a/app/js/animations.js b/app/js/animations.js new file mode 100644 index 000000000..8f3404265 --- /dev/null +++ b/app/js/animations.js @@ -0,0 +1,52 @@ +var phonecatAnimations = angular.module('phonecatAnimations', ['ngAnimate']); + +phonecatAnimations.animation('.phone', function() { + + var animateUp = function(element, className, done) { + if(className != 'active') { + return; + } + element.css({ + position: 'absolute', + top: 500, + left: 0, + display: 'block' + }); + + jQuery(element).animate({ + top: 0 + }, done); + + return function(cancel) { + if(cancel) { + element.stop(); + } + }; + } + + var animateDown = function(element, className, done) { + if(className != 'active') { + return; + } + element.css({ + position: 'absolute', + left: 0, + top: 0 + }); + + jQuery(element).animate({ + top: -500 + }, done); + + return function(cancel) { + if(cancel) { + element.stop(); + } + }; + } + + return { + addClass: animateUp, + removeClass: animateDown + }; +}); diff --git a/app/js/app.js b/app/js/app.js index 7103787cc..a58955cd1 100644 --- a/app/js/app.js +++ b/app/js/app.js @@ -4,6 +4,8 @@ var phonecatApp = angular.module('phonecatApp', [ 'ngRoute', + 'phonecatAnimations', + 'phonecatControllers', 'phonecatFilters', 'phonecatServices' diff --git a/app/partials/phone-detail.html b/app/partials/phone-detail.html index 5785e94ca..5fc4da2ae 100644 --- a/app/partials/phone-detail.html +++ b/app/partials/phone-detail.html @@ -1,4 +1,9 @@ - +
    + +

    {{phone.name}}

    diff --git a/app/partials/phone-list.html b/app/partials/phone-list.html index a66a6d694..e9ec19e39 100644 --- a/app/partials/phone-list.html +++ b/app/partials/phone-list.html @@ -15,7 +15,8 @@
      -
    • +
    • {{phone.name}}

      {{phone.snippet}}

      diff --git a/bower.json b/bower.json index 783bf7636..f605c1600 100644 --- a/bower.json +++ b/bower.json @@ -11,6 +11,7 @@ "jquery": "~2.1.1", "bootstrap": "~3.1.1", "angular-route": "1.4.x", - "angular-resource": "1.4.x" + "angular-resource": "1.4.x", + "angular-animate": "1.4.x" } } diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js index e81832aa6..79b5b6edc 100644 --- a/test/e2e/scenarios.js +++ b/test/e2e/scenarios.js @@ -86,16 +86,16 @@ describe('PhoneCat App', function() { it('should display the first phone image as the main phone image', function() { - expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); + expect(element(by.css('img.phone.active')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); }); it('should swap main image if a thumbnail image is clicked on', function() { element(by.css('.phone-thumbs li:nth-child(3) img')).click(); - expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/); + expect(element(by.css('img.phone.active')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/); element(by.css('.phone-thumbs li:nth-child(1) img')).click(); - expect(element(by.css('img.phone')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); + expect(element(by.css('img.phone.active')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/); }); }); }); diff --git a/test/karma.conf.js b/test/karma.conf.js index b6939eeea..16c43543a 100644 --- a/test/karma.conf.js +++ b/test/karma.conf.js @@ -7,6 +7,7 @@ module.exports = function(config){ 'app/bower_components/angular/angular.js', 'app/bower_components/angular-route/angular-route.js', 'app/bower_components/angular-resource/angular-resource.js', + 'app/bower_components/angular-animate/angular-animate.js', 'app/bower_components/angular-mocks/angular-mocks.js', 'app/js/**/*.js', 'test/unit/**/*.js' From e77aff0ce00ccf7ebfbfd1bdf8c36cebcf24a802 Mon Sep 17 00:00:00 2001 From: Arie van Deursen Date: Sat, 5 Sep 2015 14:01:25 +0200 Subject: [PATCH 14/14] End-to- end test suite making use of state objects. The test suite is compatible with Protractor 2.x --- test/e2e/state-objects.js | 185 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 test/e2e/state-objects.js diff --git a/test/e2e/state-objects.js b/test/e2e/state-objects.js new file mode 100644 index 000000000..628e58c41 --- /dev/null +++ b/test/e2e/state-objects.js @@ -0,0 +1,185 @@ +/* + * Re-implementation of the Angular Phonecat Protractor scenarios.js + * to make use of State Objects, an extension of Page Objects + * in which a state machine drives test generation. + * + * Copyright (c) Arie van Deursen, Delft University of Technology, 2015. + * + * Like the original scenarios.js file, these tests are distributed under the MIT License. + */ + + +'use strict'; + +var PhoneList = (function () { + var query = element(by.model('query')), + phoneList = element.all(by.repeater('phone in phones')), + phoneNameColumn = element.all(by.repeater('phone in phones').column('phone.name')), + phoneLinks = element.all(by.css('.phones li a')); + + function sortBy(property) { + return element(by.model('orderProp')).element(by.css('option[value="' + property + '"]')).click(); + } + + return { + get: function () { + return browser.get('app/index.html'); + }, + selfCheck: function () { + expect(browser.getLocationAbsUrl()).toBe('/phones'); + }, + search: function (text) { + return query.clear().then(function () { + query.sendKeys(text); + }); + }, + count: function () { + return phoneList.count(); + }, + getNames: function () { + return phoneNameColumn.map(function(elm) { + return elm.getText(); + }); + }, + byAge: function () { + return sortBy('age'); + }, + byName: function () { + return sortBy('name'); + }, + firstPhone: function () { + return phoneLinks.first().click().then(function() { + return Nexus; + }); + } + }; +}()); + + +var Nexus = (function () { + return { + selfCheck: function () { + expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s'); + }, + get: function () { + browser.get('app/index.html#/phones/nexus-s'); + }, + name: function () { + return element(by.binding('phone.name')).getText(); + }, + currentImage: function () { + return element(by.css('img.phone.active')).getAttribute('src'); + }, + thumb: function (index) { + return element(by.css('.phone-thumbs li:nth-child(' + index + ') img')).click(); + }, + backToList: function () { + return browser.driver.navigate().back(); + } + }; +}()); + + +describe('PhoneCat App', function() { + + it('should redirect index.html to index.html#/phones', function() { + PhoneList.get(); + PhoneList.selfCheck(); + }); + + + describe('Phone list view', function() { + + it('should start with 20 phones', function() { + expect(PhoneList.count()).toBe(20); + }); + + it('should find a single nexus', function (done) { + PhoneList.search('nexus').then(function () { + expect(PhoneList.count()).toBe(1); + done(); + }); + }); + + it('should find multiple motorolas', function (done) { + PhoneList.search('motorola').then(function () { + expect(PhoneList.count()).toBe(8); + done(); + }); + }); + + it('should filter the phone list as a user types into the search box', function(done) { + PhoneList.search('nexus').then(function () { + PhoneList.search('motorola').then(function () { + expect(PhoneList.count()).toBe(8); + done(); + }); + }); + }); + + it('should distinguish two tablets', function() { + PhoneList.search('tablet').then(function () { + expect(PhoneList.getNames()).toEqual([ + "Motorola XOOM\u2122 with Wi-Fi", + "MOTOROLA XOOM\u2122" + ]); + }); + }); + + it('should sort by name', function () { + PhoneList.search('tablet') + .then(PhoneList.byName) + .then(function () { + expect(PhoneList.getNames()).toEqual([ + "MOTOROLA XOOM\u2122", + "Motorola XOOM\u2122 with Wi-Fi" + ]); + }); + }); + + it('should sort by age', function () { + PhoneList.search('tablet') + .then(PhoneList.byAge) + .then(function () { + expect(PhoneList.getNames()).toEqual([ + "Motorola XOOM\u2122 with Wi-Fi", + "MOTOROLA XOOM\u2122" + ]); + }); + }); + + it('should render phone specific links and return', function() { + PhoneList.search('nexus') + .then(PhoneList.firstPhone) + .then(Nexus.selfCheck) + .then(Nexus.backToList) + .then(PhoneList.selfCheck); + }); + }); + + + describe('Phone detail view', function() { + + it('should start at a nexus', function () { + Nexus.get(); + Nexus.selfCheck(); + }); + + it('should display nexus-s page', function() { + expect(Nexus.name()).toBe('Nexus S'); + }); + + it('should display the first phone image as the main phone image', function() { + expect(Nexus.currentImage()).toMatch(/img\/phones\/nexus-s.0.jpg/); + }); + + it('should swap main image if a thumbnail image is clicked on', function() { + Nexus.thumb(3).then(function () { + expect(Nexus.currentImage()).toMatch(/nexus-s.2.jpg/); + Nexus.thumb(2).then(function () { + expect(Nexus.currentImage()).toMatch(/nexus-s.1.jpg/); + }); + }); + }); + }); +});