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..8e2ff4db1 100644
--- a/app/css/app.css
+++ b/app/css/app.css
@@ -1 +1,92 @@
/* app css stylesheet */
+
+body {
+ padding-top: 20px;
+}
+
+
+.phone-images {
+ width: 450px;
+ height: 450px;
+ overflow: hidden;
+ position: relative;
+ float: left;
+}
+
+.phones {
+ list-style: none;
+}
+
+.thumb {
+ float: left;
+ margin: -1em 1em 1.5em 0em;
+ padding-bottom: 1em;
+ height: 100px;
+ width: 100px;
+}
+
+.phones li {
+ clear: both;
+ height: 100px;
+ padding-top: 15px;
+}
+
+/** Detail View **/
+img.phone {
+ float: left;
+ 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.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 66698631f..26e294f0e 100644
--- a/app/index.html
+++ b/app/index.html
@@ -1,12 +1,29 @@
-
+
- My HTML File
+ 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 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..b7280249a
--- /dev/null
+++ b/app/partials/phone-list.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+ Search:
+ Sort by:
+
+
+
+
+
+
diff --git a/test/e2e/scenarios.js b/test/e2e/scenarios.js
index a59d2f688..f67a7a3db 100644
--- a/test/e2e/scenarios.js
+++ b/test/e2e/scenarios.js
@@ -2,10 +2,78 @@
/* 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().navigateTo('app/index.html');
+ expect(browser().location().url()).toBe('/phones');
});
+
+ describe('Phone list view', function() {
+
+ beforeEach(function() {
+ browser().navigateTo('app/index.html#/phones');
+ });
+
+
+ it('should filter the phone list as user types into the search box', function() {
+ expect(repeater('.phones li').count()).toBe(20);
+
+ input('query').enter('nexus');
+ expect(repeater('.phones li').count()).toBe(1);
+
+ input('query').enter('motorola');
+ expect(repeater('.phones li').count()).toBe(8);
+ });
+
+
+ it('should be possible to control phone order via the drop down select box', function() {
+ input('query').enter('tablet'); //let's narrow the dataset to make the test assertions shorter
+
+ expect(repeater('.phones li', 'Phone List').column('phone.name')).
+ toEqual(["Motorola XOOM\u2122 with Wi-Fi",
+ "MOTOROLA XOOM\u2122"]);
+
+ select('orderProp').option('Alphabetical');
+
+ expect(repeater('.phones li', 'Phone List').column('phone.name')).
+ toEqual(["MOTOROLA XOOM\u2122",
+ "Motorola XOOM\u2122 with Wi-Fi"]);
+ });
+
+
+ it('should render phone specific links', function() {
+ input('query').enter('nexus');
+ element('.phones li a').click();
+ expect(browser().location().url()).toBe('/phones/nexus-s');
+ });
+ });
+
+
+ describe('Phone detail view', function() {
+
+ beforeEach(function() {
+ browser().navigateTo('app/index.html#/phones/nexus-s');
+ });
+
+
+ it('should display nexus-s page', function() {
+ expect(binding('phone.name')).toBe('Nexus S');
+ });
+
+
+ it('should display the first phone image as the main phone image', function() {
+ expect(element('img.phone.active').attr('src')).toBe('img/phones/nexus-s.0.jpg');
+ });
+
+
+ it('should swap main image if a thumbnail image is clicked on', function() {
+ element('.phone-thumbs li:nth-child(3) img').click();
+ expect(element('img.phone.active').attr('src')).toBe('img/phones/nexus-s.2.jpg');
+
+ element('.phone-thumbs li:nth-child(1) img').click();
+ expect(element('img.phone.active').attr('src')).toBe('img/phones/nexus-s.0.jpg');
+ });
+ });
});
diff --git a/test/unit/controllersSpec.js b/test/unit/controllersSpec.js
index 37fd9fe45..e85cc111a 100644
--- a/test/unit/controllersSpec.js
+++ b/test/unit/controllersSpec.js
@@ -1,7 +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 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');
+ }));
+ });
});