diff --git a/README.md b/README.md
index 36fbee57b..e0ba9dd95 100644
--- a/README.md
+++ b/README.md
@@ -165,6 +165,18 @@ git diff step-?..step-?
- CSS transition animations.
- CSS keyframe animations.
- JavaScript-based animations.
+
+### step-15 _Accessibility (a11y)_
+
+- Add labels to the search and order fields.
+- Add accessibility plugin for Protractor.
+ - Add missing alt attributes in the phone detail.
+- Add aria live regions to inform the user about results after searching and filtering elements.
+- Improve access via keyboard:
+ - Navigate between the images in a phone detail.
+ - Add headings elements.
+- Add information about checkmarks in the phone detail specs.
+
## Development with `angular-phonecat`
@@ -276,4 +288,4 @@ For more information on AngularJS, please check out https://angularjs.org/.
[karma]: https://karma-runner.github.io/
[node]: https://nodejs.org/
[protractor]: http://www.protractortest.org/
-[selenium]: http://docs.seleniumhq.org/
+[protractor-accessibility-plugin]: https://github.com/angular/protractor-accessibility-plugin
diff --git a/app/app.animations.css b/app/app.animations.css
new file mode 100644
index 000000000..175320b50
--- /dev/null
+++ b/app/app.animations.css
@@ -0,0 +1,67 @@
+/* Animate `ngRepeat` in `phoneList` component */
+.phone-list-item.ng-enter,
+.phone-list-item.ng-leave,
+.phone-list-item.ng-move {
+ overflow: hidden;
+ transition: 0.5s linear all;
+}
+
+.phone-list-item.ng-enter,
+.phone-list-item.ng-leave.ng-leave-active,
+.phone-list-item.ng-move {
+ height: 0;
+ margin-bottom: 0;
+ opacity: 0;
+ padding-bottom: 0;
+ padding-top: 0;
+}
+
+.phone-list-item.ng-enter.ng-enter-active,
+.phone-list-item.ng-leave,
+.phone-list-item.ng-move.ng-move-active {
+ height: 120px;
+ margin-bottom: 20px;
+ opacity: 1;
+ padding-bottom: 4px;
+ padding-top: 15px;
+}
+
+/* Animate view transitions with `ngView` */
+.view-container {
+ position: relative;
+}
+
+.view-frame {
+ margin-top: 20px;
+}
+
+.view-frame.ng-enter,
+.view-frame.ng-leave {
+ background: white;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.view-frame.ng-enter {
+ animation: 1s fade-in;
+ z-index: 100;
+}
+
+.view-frame.ng-leave {
+ animation: 1s fade-out;
+ z-index: 99;
+}
+
+@keyframes fade-in {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes fade-out {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+/* Older browsers might need vendor-prefixes for keyframes and animation! */
diff --git a/app/app.animations.js b/app/app.animations.js
new file mode 100644
index 000000000..394fcc944
--- /dev/null
+++ b/app/app.animations.js
@@ -0,0 +1,43 @@
+'use strict';
+
+angular.
+ module('phonecatApp').
+ animation('.phone', function phoneAnimationFactory() {
+ return {
+ addClass: animateIn,
+ removeClass: animateOut
+ };
+
+ function animateIn(element, className, done) {
+ if (className !== 'selected') return;
+
+ element.css({
+ display: 'block',
+ position: 'absolute',
+ top: 500,
+ left: 0
+ }).animate({
+ top: 0
+ }, done);
+
+ return function animateInEnd(wasCanceled) {
+ if (wasCanceled) element.stop();
+ };
+ }
+
+ function animateOut(element, className, done) {
+ if (className !== 'selected') return;
+
+ element.css({
+ position: 'absolute',
+ top: 0,
+ left: 0
+ }).animate({
+ top: -500
+ }, done);
+
+ return function animateOutEnd(wasCanceled) {
+ if (wasCanceled) element.stop();
+ };
+ }
+ });
diff --git a/app/app.config.js b/app/app.config.js
new file mode 100644
index 000000000..a060f5906
--- /dev/null
+++ b/app/app.config.js
@@ -0,0 +1,18 @@
+'use strict';
+
+angular.
+ module('phonecatApp').
+ config(['$locationProvider' ,'$routeProvider',
+ function config($locationProvider, $routeProvider) {
+ $locationProvider.hashPrefix('!');
+
+ $routeProvider.
+ when('/phones', {
+ template: ''
+ }).
+ when('/phones/:phoneId', {
+ template: ''
+ }).
+ otherwise('/phones');
+ }
+ ]);
diff --git a/app/app.css b/app/app.css
new file mode 100644
index 000000000..3854d3bf9
--- /dev/null
+++ b/app/app.css
@@ -0,0 +1,122 @@
+body {
+ padding: 20px;
+}
+
+h1 {
+ border-bottom: 1px solid gray;
+ margin-top: 0;
+}
+
+.aria-status { margin-bottom: 10px; }
+.aria-status-order{ margin-bottom: 5px; }
+
+/* View: Phone list */
+.phones {
+ list-style: none;
+}
+
+.phones li {
+ clear: both;
+ height: 115px;
+ padding-top: 15px;
+}
+
+.thumb {
+ float: left;
+ height: 100px;
+ margin: -0.5em 1em 1.5em 0;
+ padding-bottom: 1em;
+ width: 100px;
+}
+
+/* View: Phone detail */
+.phone {
+ background-color: white;
+ display: none;
+ float: left;
+ height: 400px;
+ margin-bottom: 2em;
+ margin-right: 3em;
+ padding: 2em;
+ width: 400px;
+}
+
+.phone:first-child {
+ display: block;
+}
+
+.phone-images {
+ background-color: white;
+ float: left;
+ height: 450px;
+ overflow: hidden;
+ position: relative;
+ width: 450px;
+}
+
+.phone-thumbs {
+ list-style: none;
+ margin: 0;
+}
+
+.phone-thumbs img {
+ height: 100px;
+ padding: 1em;
+ width: 100px;
+}
+
+.phone-thumbs li {
+ background-color: white;
+ border: 1px solid black;
+ cursor: pointer;
+ display: inline-block;
+ margin: 1em;
+}
+
+.phone-thumbs img:focus {
+ border:2px solid black;
+}
+
+.phone-thumbs img:hover {
+ cursor: pointer;
+}
+
+.phone-thumbs button {
+ background: none;
+ border: 0;
+ }
+
+.specs {
+ clear: both;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.specs dt {
+ font-weight: bold;
+}
+
+.specs > li {
+ display: inline-block;
+ vertical-align: top;
+ width: 200px;
+}
+
+.specs > li > span {
+ font-size: 1.2em;
+ font-weight: bold;
+}
+
+ul.specs h2 { font-size:1.5em }
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ border: 0;
+}
\ No newline at end of file
diff --git a/app/app.module.js b/app/app.module.js
new file mode 100644
index 000000000..8c392c4d6
--- /dev/null
+++ b/app/app.module.js
@@ -0,0 +1,10 @@
+'use strict';
+
+// Define the `phonecatApp` module
+angular.module('phonecatApp', [
+ 'ngAnimate',
+ 'ngRoute',
+ 'core',
+ 'phoneDetail',
+ 'phoneList'
+]);
diff --git a/app/core/checkmark/checkmark.filter.js b/app/core/checkmark/checkmark.filter.js
new file mode 100644
index 000000000..0132dfc02
--- /dev/null
+++ b/app/core/checkmark/checkmark.filter.js
@@ -0,0 +1,9 @@
+'use strict';
+
+angular.
+ module('core').
+ filter('checkmark', function() {
+ return function(input) {
+ return input ? '\u2713' : '\u2718';
+ };
+ });
diff --git a/app/core/checkmark/checkmark.filter.spec.js b/app/core/checkmark/checkmark.filter.spec.js
new file mode 100644
index 000000000..4d53baab9
--- /dev/null
+++ b/app/core/checkmark/checkmark.filter.spec.js
@@ -0,0 +1,14 @@
+'use strict';
+
+describe('checkmark', function() {
+
+ beforeEach(module('core'));
+
+ 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/app/core/core.module.js b/app/core/core.module.js
new file mode 100644
index 000000000..84a91dc7a
--- /dev/null
+++ b/app/core/core.module.js
@@ -0,0 +1,4 @@
+'use strict';
+
+// Define the `core` module
+angular.module('core', ['core.phone']);
diff --git a/app/core/phone/phone.module.js b/app/core/phone/phone.module.js
new file mode 100644
index 000000000..0b6b34889
--- /dev/null
+++ b/app/core/phone/phone.module.js
@@ -0,0 +1,4 @@
+'use strict';
+
+// Define the `core.phone` module
+angular.module('core.phone', ['ngResource']);
diff --git a/app/core/phone/phone.service.js b/app/core/phone/phone.service.js
new file mode 100644
index 000000000..048e66ae8
--- /dev/null
+++ b/app/core/phone/phone.service.js
@@ -0,0 +1,15 @@
+'use strict';
+
+angular.
+ module('core.phone').
+ factory('Phone', ['$resource',
+ function($resource) {
+ return $resource('phones/:phoneId.json', {}, {
+ query: {
+ method: 'GET',
+ params: {phoneId: 'phones'},
+ isArray: true
+ }
+ });
+ }
+ ]);
diff --git a/app/core/phone/phone.service.spec.js b/app/core/phone/phone.service.spec.js
new file mode 100644
index 000000000..f045c561c
--- /dev/null
+++ b/app/core/phone/phone.service.spec.js
@@ -0,0 +1,43 @@
+'use strict';
+
+describe('Phone', function() {
+ var $httpBackend;
+ var Phone;
+ var phonesData = [
+ {name: 'Phone X'},
+ {name: 'Phone Y'},
+ {name: 'Phone Z'}
+ ];
+
+ // Add a custom equality tester before each test
+ beforeEach(function() {
+ jasmine.addCustomEqualityTester(angular.equals);
+ });
+
+ // Load the module that contains the `Phone` service before each test
+ beforeEach(module('core.phone'));
+
+ // Instantiate the service and "train" `$httpBackend` before each test
+ beforeEach(inject(function(_$httpBackend_, _Phone_) {
+ $httpBackend = _$httpBackend_;
+ $httpBackend.expectGET('phones/phones.json').respond(phonesData);
+
+ Phone = _Phone_;
+ }));
+
+ // Verify that there are no outstanding expectations or requests after each test
+ afterEach(function () {
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should fetch the phones data from `/phones/phones.json`', function() {
+ var phones = Phone.query();
+
+ expect(phones).toEqual([]);
+
+ $httpBackend.flush();
+ expect(phones).toEqual(phonesData);
+ });
+
+});
diff --git a/app/index.html b/app/index.html
index 748eef01f..b10570885 100644
--- a/app/index.html
+++ b/app/index.html
@@ -1,11 +1,35 @@
-
+
- My HTML File
+ Google Phone Gallery
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
diff --git a/app/phone-detail/phone-detail.component.js b/app/phone-detail/phone-detail.component.js
new file mode 100644
index 000000000..3b38bf3af
--- /dev/null
+++ b/app/phone-detail/phone-detail.component.js
@@ -0,0 +1,20 @@
+'use strict';
+
+// Register `phoneDetail` component, along with its associated controller and template
+angular.
+ module('phoneDetail').
+ component('phoneDetail', {
+ templateUrl: 'phone-detail/phone-detail.template.html',
+ controller: ['$routeParams', 'Phone',
+ function PhoneDetailController($routeParams, Phone) {
+ var self = this;
+ self.phone = Phone.get({phoneId: $routeParams.phoneId}, function(phone) {
+ self.setImage(phone.images[0]);
+ });
+
+ self.setImage = function setImage(imageUrl) {
+ self.mainImageUrl = imageUrl;
+ };
+ }
+ ]
+ });
diff --git a/app/phone-detail/phone-detail.component.spec.js b/app/phone-detail/phone-detail.component.spec.js
new file mode 100644
index 000000000..8f4982682
--- /dev/null
+++ b/app/phone-detail/phone-detail.component.spec.js
@@ -0,0 +1,36 @@
+'use strict';
+
+describe('phoneDetail', function() {
+
+ // Load the module that contains the `phoneDetail` component before each test
+ beforeEach(module('phoneDetail'));
+
+ // Test the controller
+ describe('PhoneDetailController', function() {
+ var $httpBackend, ctrl;
+ var xyzPhoneData = {
+ name: 'phone xyz',
+ images: ['image/url1.png', 'image/url2.png']
+ };
+
+ beforeEach(inject(function($componentController, _$httpBackend_, $routeParams) {
+ $httpBackend = _$httpBackend_;
+ $httpBackend.expectGET('phones/xyz.json').respond(xyzPhoneData);
+
+ $routeParams.phoneId = 'xyz';
+
+ ctrl = $componentController('phoneDetail');
+ }));
+
+ it('should fetch the phone details', function() {
+ jasmine.addCustomEqualityTester(angular.equals);
+
+ expect(ctrl.phone).toEqual({});
+
+ $httpBackend.flush();
+ expect(ctrl.phone).toEqual(xyzPhoneData);
+ });
+
+ });
+
+});
diff --git a/app/phone-detail/phone-detail.module.js b/app/phone-detail/phone-detail.module.js
new file mode 100644
index 000000000..fd7cb3b92
--- /dev/null
+++ b/app/phone-detail/phone-detail.module.js
@@ -0,0 +1,7 @@
+'use strict';
+
+// Define the `phoneDetail` module
+angular.module('phoneDetail', [
+ 'ngRoute',
+ 'core.phone'
+]);
diff --git a/app/phone-detail/phone-detail.template.html b/app/phone-detail/phone-detail.template.html
new file mode 100644
index 000000000..03d92fb08
--- /dev/null
+++ b/app/phone-detail/phone-detail.template.html
@@ -0,0 +1,120 @@
+
+
![]()
+
+
+{{$ctrl.phone.name}}
+
+{{$ctrl.phone.description}}
+
+Click on each image to see the enlarge version.
+
+
+ -
+
+
+
+
+
+ -
+
Availability and Networks
+
+ - Availability
+ - {{availability}}
+
+
+ -
+
Battery
+
+ - Type
+ - {{$ctrl.phone.battery.type}}
+ - Talk Time
+ - {{$ctrl.phone.battery.talkTime}}
+ - Standby time (max)
+ - {{$ctrl.phone.battery.standbyTime}}
+
+
+ -
+
Storage and Memory
+
+ - RAM
+ - {{$ctrl.phone.storage.ram}}
+ - Internal Storage
+ - {{$ctrl.phone.storage.flash}}
+
+
+ -
+
Connectivity
+
+ - Network Support
+ - {{$ctrl.phone.connectivity.cell}}
+ - WiFi
+ - {{$ctrl.phone.connectivity.wifi}}
+ - Bluetooth
+ - {{$ctrl.phone.connectivity.bluetooth}}
+ - Infrared
+ - {{$ctrl.phone.connectivity.infrared | checkmark}}
+ - GPS
+ - {{$ctrl.phone.connectivity.gps | checkmark}}
+
+
+ -
+
Android
+
+ - OS Version
+ - {{$ctrl.phone.android.os}}
+ - UI
+ - {{$ctrl.phone.android.ui}}
+
+
+ -
+
Size and Weight
+
+ - Dimensions
+ - {{dim}}
+ - Weight
+ - {{$ctrl.phone.sizeAndWeight.weight}}
+
+
+ -
+
Display
+
+ - Screen size
+ - {{$ctrl.phone.display.screenSize}}
+ - Screen resolution
+ - {{$ctrl.phone.display.screenResolution}}
+ - Touch screen
+ - {{$ctrl.phone.display.touchScreen | checkmark}}
+
+
+ -
+
Hardware
+
+ - CPU
+ - {{$ctrl.phone.hardware.cpu}}
+ - USB
+ - {{$ctrl.phone.hardware.usb}}
+ - Audio / headphone jack
+ - {{$ctrl.phone.hardware.audioJack}}
+ - FM Radio
+ - {{$ctrl.phone.hardware.fmRadio | checkmark}}
+ - Accelerometer
+ - {{$ctrl.phone.hardware.accelerometer | checkmark}}
+
+
+ -
+
Camera
+
+ - Primary
+ - {{$ctrl.phone.camera.primary}}
+ - Features
+ - {{$ctrl.phone.camera.features.join(', ')}}
+
+
+ -
+
Additional Features
+ - {{$ctrl.phone.additionalFeatures}}
+
+
diff --git a/app/phone-list/phone-list.component.js b/app/phone-list/phone-list.component.js
new file mode 100644
index 000000000..484be4f88
--- /dev/null
+++ b/app/phone-list/phone-list.component.js
@@ -0,0 +1,14 @@
+'use strict';
+
+// Register `phoneList` component, along with its associated controller and template
+angular.
+ module('phoneList').
+ component('phoneList', {
+ templateUrl: 'phone-list/phone-list.template.html',
+ controller: ['Phone',
+ function PhoneListController(Phone) {
+ this.phones = Phone.query();
+ this.orderProp = 'age';
+ }
+ ]
+ });
diff --git a/app/phone-list/phone-list.component.spec.js b/app/phone-list/phone-list.component.spec.js
new file mode 100644
index 000000000..572260e49
--- /dev/null
+++ b/app/phone-list/phone-list.component.spec.js
@@ -0,0 +1,35 @@
+'use strict';
+
+describe('phoneList', function() {
+
+ // Load the module that contains the `phoneList` component before each test
+ beforeEach(module('phoneList'));
+
+ // Test the controller
+ describe('PhoneListController', function() {
+ var $httpBackend, ctrl;
+
+ beforeEach(inject(function($componentController, _$httpBackend_) {
+ $httpBackend = _$httpBackend_;
+ $httpBackend.expectGET('phones/phones.json')
+ .respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
+
+ ctrl = $componentController('phoneList');
+ }));
+
+ it('should create a `phones` property with 2 phones fetched with `$http`', function() {
+ jasmine.addCustomEqualityTester(angular.equals);
+
+ expect(ctrl.phones).toEqual([]);
+
+ $httpBackend.flush();
+ expect(ctrl.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
+ });
+
+ it('should set a default value for the `orderProp` property', function() {
+ expect(ctrl.orderProp).toBe('age');
+ });
+
+ });
+
+});
diff --git a/app/phone-list/phone-list.directive.js b/app/phone-list/phone-list.directive.js
new file mode 100644
index 000000000..2ed354493
--- /dev/null
+++ b/app/phone-list/phone-list.directive.js
@@ -0,0 +1,42 @@
+
+'use strict';
+
+/* Directives */
+
+angular.
+ module('phoneList').
+ directive('resultList', [function () {
+ var ariaStatus = document.querySelector('.aria-status');
+ return {
+ restrict: 'A',
+ link: function ($scope) {
+ $scope.$watch('filtered.length', function (length) {
+ if(length === 1) {
+ ariaStatus.innerHTML = length + ' item found';
+ } else if(length > 1) {
+ ariaStatus.innerHTML = length + ' items found';
+ } else {
+ ariaStatus.innerHTML = 'No items found';
+ }
+ });
+ }
+ }
+}
+]).
+ directive('informOrder', [function () {
+ var ariaStatusSort = document.querySelector('.aria-status-sort');
+ return {
+ restrict: 'A',
+ link: function ($scope) {
+ $scope.$watch('$ctrl.orderProp', function (order) {
+ if(order === 'age') {
+ ariaStatusSort.innerHTML = 'Items filter by newest'
+ } else {
+ ariaStatusSort.innerHTML = 'Items filter by alphabetical order'
+ }
+
+ });
+ }
+ }
+ }
+]);
\ No newline at end of file
diff --git a/app/phone-list/phone-list.module.js b/app/phone-list/phone-list.module.js
new file mode 100644
index 000000000..8ade7c5b8
--- /dev/null
+++ b/app/phone-list/phone-list.module.js
@@ -0,0 +1,4 @@
+'use strict';
+
+// Define the `phoneList` module
+angular.module('phoneList', ['core.phone']);
diff --git a/app/phone-list/phone-list.template.html b/app/phone-list/phone-list.template.html
new file mode 100644
index 000000000..7016920a4
--- /dev/null
+++ b/app/phone-list/phone-list.template.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bower.json b/bower.json
index a7a00a589..08280c3de 100644
--- a/bower.json
+++ b/bower.json
@@ -7,7 +7,11 @@
"private": true,
"dependencies": {
"angular": "1.5.x",
+ "angular-animate": "1.5.x",
"angular-mocks": "1.5.x",
- "bootstrap": "3.3.x"
+ "angular-resource": "1.5.x",
+ "angular-route": "1.5.x",
+ "bootstrap": "3.3.x",
+ "jquery": "2.2.x"
}
}
diff --git a/e2e-tests/protractor.conf.js b/e2e-tests/protractor.conf.js
index 13c5cb626..b9a5a0e33 100644
--- a/e2e-tests/protractor.conf.js
+++ b/e2e-tests/protractor.conf.js
@@ -17,6 +17,16 @@ exports.config = {
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
- }
+ },
+
+ chromeOnly: true,
+ directConnect: true,
+
+ plugins: [{
+ chromeA11YDevTools: {
+ treatWarningsAsFailures: true
+ },
+ package: 'protractor-accessibility-plugin'
+ }]
};
diff --git a/e2e-tests/scenarios.js b/e2e-tests/scenarios.js
index 8ecf38b99..ee914bd17 100644
--- a/e2e-tests/scenarios.js
+++ b/e2e-tests/scenarios.js
@@ -3,10 +3,111 @@
// Angular E2E Testing Guide:
// https://docs.angularjs.org/guide/e2e-testing
-describe('My app', function() {
+describe('PhoneCat Application', function() {
- beforeEach(function() {
+ it('should redirect `index.html` to `index.html#!/phones', function() {
browser.get('index.html');
+ expect(browser.getLocationAbsUrl()).toBe('/phones');
+ });
+
+ describe('View: Phone list', function() {
+
+ beforeEach(function() {
+ browser.get('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 filtered'));
+ var query = element(by.model('$ctrl.query'));
+
+ expect(phoneList.count()).toBe(20);
+
+ query.sendKeys('nexus');
+
+ browser.sleep(1000).then(function(){
+ expect(phoneList.count()).toBe(1);
+ });
+
+ query.clear();
+ query.sendKeys('motorola');
+
+ browser.sleep(1000).then(function(){
+ expect(phoneList.count()).toBe(8);
+ });
+
+ });
+
+ it('should be possible to control phone order via the drop-down menu', function() {
+ var queryField = element(by.model('$ctrl.query'));
+ var orderSelect = element(by.model('$ctrl.orderProp'));
+ var nameOption = orderSelect.element(by.css('option[value="name"]'));
+ var phoneNameColumn = element.all(by.repeater('phone in filtered').column('phone.name'));
+
+ function getNames() {
+ return phoneNameColumn.map(function(elem) {
+ return elem.getText();
+ });
+ }
+
+ queryField.sendKeys('tablet'); // Let's narrow the dataset to make the assertions shorter
+
+ browser.sleep(1000).then(function(){
+
+ expect(getNames()).toEqual([
+ 'Motorola XOOM\u2122 with Wi-Fi',
+ 'MOTOROLA XOOM\u2122'
+ ]);
+
+ nameOption.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('$ctrl.query'));
+ query.sendKeys('nexus');
+
+ browser.sleep(1000).then(function(){
+ element.all(by.css('.phones li a')).first().click();
+ expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
+ });
+
+ });
+
+ });
+
+ describe('View: Phone detail', function() {
+
+ beforeEach(function() {
+ browser.get('index.html#!/phones/nexus-s');
+ });
+
+ it('should display the `nexus-s` page', function() {
+ expect(element(by.binding('$ctrl.phone.name')).getText()).toBe('Nexus S');
+ });
+
+ it('should display the first phone image as the main phone image', function() {
+ var mainImage = element(by.css('img.phone.selected'));
+
+ expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
+ });
+
+ it('should swap the main image when clicking on a thumbnail image', function() {
+ var mainImage = element(by.css('img.phone.selected'));
+ var thumbnails = element.all(by.css('.phone-thumbs img'));
+
+ thumbnails.get(2).click();
+ expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.2.jpg/);
+
+ thumbnails.get(0).click();
+ expect(mainImage.getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
+ });
+
});
});
diff --git a/karma.conf.js b/karma.conf.js
index 704f45ed4..b365c8ead 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -6,6 +6,9 @@ module.exports = function(config) {
files: [
'bower_components/angular/angular.js',
+ 'bower_components/angular-animate/angular-animate.js',
+ 'bower_components/angular-resource/angular-resource.js',
+ 'bower_components/angular-route/angular-route.js',
'bower_components/angular-mocks/angular-mocks.js',
'**/*.module.js',
'*!(.module|.spec).js',
diff --git a/package.json b/package.json
index 358938fde..54c7a668f 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,9 @@
"karma-chrome-launcher": "^0.2.3",
"karma-firefox-launcher": "^0.1.7",
"karma-jasmine": "^0.3.8",
- "protractor": "^4.0.9"
+ "protractor": "^3.2.2",
+ "protractor-accessibility-plugin": "^0.1.1",
+ "shelljs": "^0.6.0"
},
"scripts": {
"postinstall": "bower install",