diff --git a/README.md b/README.md
index e1bc9108b..5d0d274dc 100644
--- a/README.md
+++ b/README.md
@@ -14,20 +14,20 @@ The full tutorial can be found at http://docs.angularjs.org/tutorial.
### Git
-- A good place to learn about setting up git is [here][git-github]
-- Git [home][git-home] (download, documentation)
+- A good place to learn about setting up git is [here][git-github].
+- Git [home][git-home] (download, documentation).
### Node.js and Tools
- Get [Node.js][node-download].
-- Install the tool dependencies (`npm install`)
+- Install the tool dependencies (`npm install`).
## Workings of the application
- The application filesystem layout structure is based on the [angular-seed] project.
- There is no dynamic backend (no application server) for this application. Instead we fake the
- an application server by fetching static json files.
+ application server by fetching static json files.
- Read the Development section at the end to familiarize yourself with running and developing
an angular application.
@@ -41,8 +41,8 @@ To see the changes which between any two lessons use the git diff command.
### step-0
-- Add ngApp directive to bootstrap the app
-- Add simple template with an expression
+- Add ngApp directive to bootstrap the app.
+- Add simple template with an expression.
### step-1
@@ -54,12 +54,12 @@ To see the changes which between any two lessons use the git diff command.
### step-2
- Convert the static html list into dynamic one by:
- - creating `PhoneListCtrl` controller for the application
+ - creating `PhoneListCtrl` controller for the application.
- extracting the data from HTML, moving it into the controller as an in-memory dataset.
- converting the static HTML document into an Angular template with the use of the `ngRepeat`
directive which iterates over the dataset of phones.
`ngRepeat` clones its contents for each instance in the dataset and renders it into the view.
-- Add a simple unit test to show off how to write tests and run them with Karma
+- Add a simple unit test to show off how to write tests and run them with Karma.
### step-3
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 8d3eae692..951ea087c 100644
--- a/app/css/app.css
+++ b/app/css/app.css
@@ -1 +1,99 @@
/* app css stylesheet */
+
+body {
+ padding-top: 20px;
+}
+
+
+.phone-images {
+ background-color: white;
+ width: 450px;
+ height: 450px;
+ overflow: hidden;
+ position: relative;
+ float: left;
+}
+
+.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;
+}
+
+/** Detail View **/
+img.phone {
+ float: left;
+ 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;
+}
+
+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.phone-thumbs img:hover {
+ cursor: pointer;
+}
+
+
+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/index.html b/app/index.html
index f4c7d5c4d..ad6c2d979 100644
--- a/app/index.html
+++ b/app/index.html
@@ -1,12 +1,28 @@
-
+
- My HTML File
+ Google Phone Gallery
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
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 7a8f274a0..a58955cd1 100644
--- a/app/js/app.js
+++ b/app/js/app.js
@@ -1,3 +1,28 @@
'use strict';
/* App Module */
+
+var phonecatApp = angular.module('phonecatApp', [
+ 'ngRoute',
+ 'phonecatAnimations',
+
+ 'phonecatControllers',
+ 'phonecatFilters',
+ 'phonecatServices'
+]);
+
+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 d314a3331..c8ecfbba1 100644
--- a/app/js/controllers.js
+++ b/app/js/controllers.js
@@ -1,3 +1,22 @@
'use strict';
/* Controllers */
+
+var phonecatControllers = angular.module('phonecatControllers', []);
+
+phonecatControllers.controller('PhoneListCtrl', ['$scope', 'Phone',
+ function($scope, Phone) {
+ $scope.phones = Phone.query();
+ $scope.orderProp = 'age';
+ }]);
+
+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) {
+ $scope.mainImageUrl = imageUrl;
+ }
+ }]);
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/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/app/partials/phone-detail.html b/app/partials/phone-detail.html
new file mode 100644
index 000000000..5fc4da2ae
--- /dev/null
+++ b/app/partials/phone-detail.html
@@ -0,0 +1,118 @@
+
+
![]()
+
+
+{{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 | checkmark}}
+ - GPS
+ - {{phone.connectivity.gps | checkmark}}
+
+
+ -
+ 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 | checkmark}}
+
+
+ -
+ Hardware
+
+ - CPU
+ - {{phone.hardware.cpu}}
+ - USB
+ - {{phone.hardware.usb}}
+ - Audio / headphone jack
+ - {{phone.hardware.audioJack}}
+ - FM Radio
+ - {{phone.hardware.fmRadio | checkmark}}
+ - Accelerometer
+ - {{phone.hardware.accelerometer | checkmark}}
+
+
+ -
+ Camera
+
+ - Primary
+ - {{phone.camera.primary}}
+ - Features
+ - {{phone.camera.features.join(', ')}}
+
+
+ -
+ Additional Features
+
- {{phone.additionalFeatures}}
+
+
diff --git a/app/partials/phone-list.html b/app/partials/phone-list.html
new file mode 100644
index 000000000..e9ec19e39
--- /dev/null
+++ b/app/partials/phone-list.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Search:
+ Sort by:
+
+
+
+
+
+
diff --git a/bower.json b/bower.json
index 3a7b6949a..c14b47247 100644
--- a/bower.json
+++ b/bower.json
@@ -9,6 +9,9 @@
"angular": "1.3.x",
"angular-mocks": "1.3.x",
"jquery": "1.10.2",
- "bootstrap": "~3.1.1"
+ "bootstrap": "~3.1.1",
+ "angular-route": "1.3.x",
+ "angular-resource": "1.3.x",
+ "angular-animate": "1.3.x"
}
}
diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js
index ed4b2c3e7..3847c9eba 100644
--- a/test/e2e/scenarios.js
+++ b/test/e2e/scenarios.js
@@ -2,10 +2,100 @@
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
-describe('my app', function() {
+describe('PhoneCat App', function() {
- beforeEach(function() {
+ it('should redirect index.html to index.html#/phones', function() {
browser.get('app/index.html');
+ browser.getLocationAbsUrl().then(function(url) {
+ expect(url.split('#')[1]).toBe('/phones');
+ });
});
+
+ describe('Phone list view', function() {
+
+ beforeEach(function() {
+ browser.get('app/index.html#/phones');
+ });
+
+
+ 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(20);
+
+ query.sendKeys('nexus');
+ expect(phoneList.count()).toBe(1);
+
+ query.clear();
+ query.sendKeys('motorola');
+ expect(phoneList.count()).toBe(8);
+ });
+
+
+ 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"
+ ]);
+ });
+
+
+ it('should render phone specific links', function() {
+ var query = element(by.model('query'));
+ query.sendKeys('nexus');
+ element.all(by.css('.phones li a')).first().click();
+ browser.getLocationAbsUrl().then(function(url) {
+ expect(url.split('#')[1]).toBe('/phones/nexus-s');
+ });
+ });
+ });
+
+
+ describe('Phone detail view', function() {
+
+ beforeEach(function() {
+ browser.get('app/index.html#/phones/nexus-s');
+ });
+
+
+ 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.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.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.active')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
+ });
+ });
});
diff --git a/test/karma.conf.js b/test/karma.conf.js
index 60e360b29..9622a52b9 100644
--- a/test/karma.conf.js
+++ b/test/karma.conf.js
@@ -6,6 +6,8 @@ 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-animate/angular-animate.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 63d80c3c3..e85cc111a 100644
--- a/test/unit/controllersSpec.js
+++ b/test/unit/controllersSpec.js
@@ -1,11 +1,72 @@
'use strict';
/* jasmine specs for controllers go here */
+describe('PhoneCat controllers', function() {
-describe('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;
+
+ beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
+ $httpBackend = _$httpBackend_;
+ $httpBackend.expectGET('phones/phones.json').
+ respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
+
+ scope = $rootScope.$new();
+ ctrl = $controller('PhoneListCtrl', {$scope: scope});
+ }));
+
+
+ it('should create "phones" model with 2 phones fetched from xhr', function() {
+ expect(scope.phones).toEqualData([]);
+ $httpBackend.flush();
+
+ expect(scope.phones).toEqualData(
+ [{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
+ });
- it("should do something", function() {
+ it('should set the default value of orderProp model', function() {
+ expect(scope.orderProp).toBe('age');
+ });
});
+
+ describe('PhoneDetailCtrl', function(){
+ 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(xyzPhoneData());
+
+ $routeParams.phoneId = 'xyz';
+ scope = $rootScope.$new();
+ ctrl = $controller('PhoneDetailCtrl', {$scope: scope});
+ }));
+
+
+ it('should fetch phone detail', function() {
+ expect(scope.phone).toEqualData({});
+ $httpBackend.flush();
+
+ expect(scope.phone).toEqualData(xyzPhoneData());
+ });
+ });
});
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');
+ }));
+ });
});
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