docs(upgrade) add PhoneCat upgrade tutorial

This commit is contained in:
Tero Parviainen
2015-12-15 08:02:20 +02:00
committed by Naomi Black
parent ce0d1c0319
commit 472c9d98c7
156 changed files with 5947 additions and 5 deletions
+6
View File
@@ -0,0 +1,6 @@
Each subdirectory here is a version of the PhoneCat app.
Each version is fully functional, but omits the JSON data and image
files in the interest of keeping things clean. To run each app
in its full form, copy the `img` and `phones` directories from
https://github.com/angular/angular-phonecat/tree/master/app under `app`.
@@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}
@@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map
@@ -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; }
}
@@ -0,0 +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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Google Phone Gallery</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/animations.css">
<!-- #docregion scripts -->
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-animate/angular-animate.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script>
System.config({
packages: {'js': {defaultExtension: 'js'}}
});
System.import('js/app.module');
</script>
<!-- #enddocregion scripts -->
</head>
<body>
<div class="view-container">
<div ng-view class="view-frame"></div>
</div>
</body>
</html>
@@ -0,0 +1,40 @@
// #docregion pre-bootstrap
// #docregion typings
/// <reference path="../../typings/angularjs/angular.d.ts" />
/// <reference path="../../typings/angularjs/angular-resource.d.ts" />
/// <reference path="../../typings/angularjs/angular-route.d.ts" />
// #enddocregion
import core from './core/core.module';
import phoneList from './phone_list/phone_list.module';
import phoneDetail from './phone_detail/phone_detail.module';
angular.module('phonecatApp', [
'ngRoute',
core.name,
phoneList.name,
phoneDetail.name
]).config(configure);
configure.$inject = ['$routeProvider'];
function configure($routeProvider) {
$routeProvider.
when('/phones', {
templateUrl: 'js/phone_list/phone_list.html',
controller: 'PhoneListCtrl',
controllerAs: 'vm'
}).
when('/phones/:phoneId', {
templateUrl: 'js/phone_detail/phone_detail.html',
controller: 'PhoneDetailCtrl',
controllerAs: 'vm'
}).
otherwise({
redirectTo: '/phones'
});
}
// #enddocregion pre-bootstrap
// #docregion bootstrap
angular.bootstrap(document.documentElement, ['phonecatApp']);
// #enddocregion bootstrap
@@ -0,0 +1,6 @@
// #docregion
export default function checkmarkFilter() {
return function(input:boolean):string {
return input ? '\u2713' : '\u2718';
};
}
@@ -0,0 +1,9 @@
// #docregion
import Phone from './phone.factory';
import checkmarkFilter from './checkmark.filter';
export default angular.module('phonecat.core', [
'ngResource'
])
.factory('Phone', Phone)
.filter('checkmark', checkmarkFilter);
@@ -0,0 +1,10 @@
// #docregion
Phone.$inject = ['$resource'];
function Phone($resource) {
return $resource('phones/:phoneId.json', {}, {
query: {method:'GET', params:{phoneId:'phones'}, isArray:true}
});
}
export default Phone;
@@ -0,0 +1,22 @@
// #docregion
interface PhoneRouteParams {
phoneId: string
}
class PhoneDetailCtrl {
phone:any;
mainImageUrl:string;
constructor($routeParams:PhoneRouteParams, Phone) {
this.phone = Phone.get({phoneId: $routeParams.phoneId}, (phone) =>
this.mainImageUrl = phone.images[0]
);
}
setImage(url:string) {
this.mainImageUrl = url;
}
}
PhoneDetailCtrl.$inject = ['$routeParams', 'Phone'];
export default PhoneDetailCtrl;
@@ -0,0 +1,118 @@
<div class="phone-images">
<img ng-src="{{img}}"
class="phone"
ng-repeat="img in vm.phone.images"
ng-class="{active: vm.mainImageUrl==img}">
</div>
<h1>{{vm.phone.name}}</h1>
<p>{{vm.phone.description}}</p>
<ul class="phone-thumbs">
<li ng-repeat="img in vm.phone.images">
<img ng-src="{{img}}" ng-click="vm.setImage(img)">
</li>
</ul>
<ul class="specs">
<li>
<span>Availability and Networks</span>
<dl>
<dt>Availability</dt>
<dd ng-repeat="availability in vm.phone.availability">{{availability}}</dd>
</dl>
</li>
<li>
<span>Battery</span>
<dl>
<dt>Type</dt>
<dd>{{vm.phone.battery.type}}</dd>
<dt>Talk Time</dt>
<dd>{{vm.phone.battery.talkTime}}</dd>
<dt>Standby time (max)</dt>
<dd>{{vm.phone.battery.standbyTime}}</dd>
</dl>
</li>
<li>
<span>Storage and Memory</span>
<dl>
<dt>RAM</dt>
<dd>{{vm.phone.storage.ram}}</dd>
<dt>Internal Storage</dt>
<dd>{{vm.phone.storage.flash}}</dd>
</dl>
</li>
<li>
<span>Connectivity</span>
<dl>
<dt>Network Support</dt>
<dd>{{vm.phone.connectivity.cell}}</dd>
<dt>WiFi</dt>
<dd>{{vm.phone.connectivity.wifi}}</dd>
<dt>Bluetooth</dt>
<dd>{{vm.phone.connectivity.bluetooth}}</dd>
<dt>Infrared</dt>
<dd>{{vm.phone.connectivity.infrared | checkmark}}</dd>
<dt>GPS</dt>
<dd>{{vm.phone.connectivity.gps | checkmark}}</dd>
</dl>
</li>
<li>
<span>Android</span>
<dl>
<dt>OS Version</dt>
<dd>{{vm.phone.android.os}}</dd>
<dt>UI</dt>
<dd>{{vm.phone.android.ui}}</dd>
</dl>
</li>
<li>
<span>Size and Weight</span>
<dl>
<dt>Dimensions</dt>
<dd ng-repeat="dim in vm.phone.sizeAndWeight.dimensions">{{dim}}</dd>
<dt>Weight</dt>
<dd>{{vm.phone.sizeAndWeight.weight}}</dd>
</dl>
</li>
<li>
<span>Display</span>
<dl>
<dt>Screen size</dt>
<dd>{{vm.phone.display.screenSize}}</dd>
<dt>Screen resolution</dt>
<dd>{{vm.phone.display.screenResolution}}</dd>
<dt>Touch screen</dt>
<dd>{{vm.phone.display.touchScreen | checkmark}}</dd>
</dl>
</li>
<li>
<span>Hardware</span>
<dl>
<dt>CPU</dt>
<dd>{{vm.phone.hardware.cpu}}</dd>
<dt>USB</dt>
<dd>{{vm.phone.hardware.usb}}</dd>
<dt>Audio / headphone jack</dt>
<dd>{{vm.phone.hardware.audioJack}}</dd>
<dt>FM Radio</dt>
<dd>{{vm.phone.hardware.fmRadio | checkmark}}</dd>
<dt>Accelerometer</dt>
<dd>{{vm.phone.hardware.accelerometer | checkmark}}</dd>
</dl>
</li>
<li>
<span>Camera</span>
<dl>
<dt>Primary</dt>
<dd>{{vm.phone.camera.primary}}</dd>
<dt>Features</dt>
<dd>{{vm.phone.camera.features.join(', ')}}</dd>
</dl>
</li>
<li>
<span>Additional Features</span>
<dd>{{vm.phone.additionalFeatures}}</dd>
</li>
</ul>
@@ -0,0 +1,8 @@
// #docregion
import PhoneDetailCtrl from './phone_detail.controller';
export default angular.module('phonecat.detail', [
'ngRoute',
'phonecat.core'
])
.controller('PhoneDetailCtrl', PhoneDetailCtrl);
@@ -0,0 +1,14 @@
// #docregion
class PhoneListCtrl {
phones:any[];
orderProp:string;
query:string;
constructor(Phone) {
this.phones = Phone.query();
this.orderProp = 'age';
}
}
PhoneListCtrl.$inject = ['Phone'];
export default PhoneListCtrl;
@@ -0,0 +1,28 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
Search: <input ng-model="vm.query">
Sort by:
<select ng-model="vm.orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
</div>
<div class="col-md-10">
<!--Body content-->
<ul class="phones">
<li ng-repeat="phone in vm.phones | filter:vm.query | orderBy:vm.orderProp"
class="thumbnail phone-listing">
<a href="#/phones/{{phone.id}}" class="thumb"><img ng-src="{{phone.imageUrl}}"></a>
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
</div>
</div>
</div>
@@ -0,0 +1,5 @@
// #docregion
import PhoneListCtrl from './phone_list.controller';
export default angular.module('phonecat.list', ['phonecat.core'])
.controller('PhoneListCtrl', PhoneListCtrl);
@@ -0,0 +1,20 @@
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.5.0-beta.2",
"angular-mocks": "1.5.0-beta.2",
"jquery": "~2.1.1",
"bootstrap": "~3.1.1",
"angular-route": "1.5.0-beta.2",
"angular-resource": "1.5.0-beta.2",
"angular-animate": "1.5.0-beta.2"
},
"resolutions": {
"angular": "1.5.0-beta.2"
}
}
@@ -0,0 +1,103 @@
'use strict';
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
// #docregion declares
declare var browser:any, element:any, by:any;
// #enddocregion declares
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#/phones');
});
it('should filter the phone list as a user types into the search box', function() {
var phoneList = element.all(by.repeater('phone in vm.phones'));
var query = element(by.model('vm.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 vm.phones').column('phone.name'));
var query = element(by.model('vm.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('vm.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('vm.query'));
query.sendKeys('nexus');
element.all(by.css('.phones li a')).first().click();
browser.getLocationAbsUrl().then(function(url) {
expect(url).toEqual('/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('vm.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/);
});
});
});
@@ -0,0 +1,6 @@
// #docregion
declare module jasmine {
interface Matchers {
toEqualData(expected: any):boolean;
}
}
@@ -0,0 +1,44 @@
// #docregion
// Cancel Karma's synchronous start,
// we will call `__karma__.start()` later, once all the specs are loaded.
__karma__.loaded = function() {};
System.config({
packages: {
'base/app/js': {
defaultExtension: false,
format: 'register',
map: Object.keys(window.__karma__.files).
filter(onlyAppFiles).
reduce(function createPathRecords(pathsMapping, appPath) {
// creates local module name mapping to global path with karma's fingerprint in path, e.g.:
// './hero.service': '/base/src/app/hero.service.js?f4523daf879cfb7310ef6242682ccf10b2041b3e'
var moduleName = appPath.replace(/^\/base\/app\/js\//, './').replace(/\.js$/, '');
pathsMapping[moduleName] = appPath + '?' + window.__karma__.files[appPath]
return pathsMapping;
}, {})
}
}
});
Promise.all(
Object.keys(window.__karma__.files) // All files served by Karma.
.filter(onlySpecFiles)
.map(function(moduleName) {
// loads all spec files via their global module names
return System.import(moduleName);
}))
.then(function() {
__karma__.start();
}, function(error) {
__karma__.error(error.stack || error);
});
function onlyAppFiles(filePath) {
return /^\/base\/app\/js\/.*\.js$/.test(filePath)
}
function onlySpecFiles(path) {
return /\.spec\.js$/.test(path);
}
@@ -0,0 +1,21 @@
exports.config = {
allScriptsTimeout: 11000,
specs: [
'e2e/*.js'
],
capabilities: {
'browserName': 'chrome'
},
chromeOnly: true,
baseUrl: 'http://localhost:8000/',
framework: 'jasmine',
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
}
};
@@ -0,0 +1,3 @@
// #docregion
/// <reference path="../typings/jasmine/jasmine.d.ts" />
/// <reference path="../typings/angularjs/angular-mocks.d.ts" />
@@ -0,0 +1,15 @@
// #docregion top
import '../../app/js/core/core.module';
// #enddocregion top
describe('checkmarkFilter', function() {
beforeEach(angular.mock.module('phonecat.core'));
it('should convert boolean values to unicode checkmark or cross',
inject(function(checkmarkFilter) {
expect(checkmarkFilter(true)).toBe('\u2713');
expect(checkmarkFilter(false)).toBe('\u2718');
}));
});
@@ -0,0 +1,15 @@
// #docregion top
import '../../app/js/core/core.module';
// #enddocregion top
describe('phoneFactory', function() {
// load modules
beforeEach(angular.mock.module('phonecat.core'));
// Test service availability
it('check the existence of Phone factory', inject(function(Phone) {
expect(Phone).toBeDefined();
}));
});
@@ -0,0 +1,44 @@
// #docregion top
import '../../app/js/phone_detail/phone_detail.module';
// #enddocregion top
describe('PhoneDetailCtrl', function(){
var scope, $httpBackend, ctrl,
xyzPhoneData = function() {
return {
name: 'phone xyz',
images: ['image/url1.png', 'image/url2.png']
}
};
beforeEach(angular.mock.module('phonecat.detail'));
beforeEach(function(){
jasmine.addMatchers({
toEqualData: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {pass: angular.equals(actual, expected)};
}
};
}
});
});
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(ctrl.phone).toEqualData({});
$httpBackend.flush();
expect(ctrl.phone).toEqualData(xyzPhoneData());
});
});
@@ -0,0 +1,44 @@
// #docregion top
import '../../app/js/phone_list/phone_list.module';
// #enddocregion top
describe('PhoneListCtrl', function(){
var scope, ctrl, $httpBackend;
beforeEach(angular.mock.module('phonecat.list'));
beforeEach(function(){
jasmine.addMatchers({
toEqualData: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {pass: angular.equals(actual, expected)};
}
};
}
});
});
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(ctrl.phones).toEqualData([]);
$httpBackend.flush();
expect(ctrl.phones).toEqualData(
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]);
});
it('should set the default value of orderProp model', function() {
expect(ctrl.orderProp).toBe('age');
});
});
@@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}
@@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map
@@ -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; }
}
@@ -0,0 +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-of-type {
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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Google Phone Gallery</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/animations.css">
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-animate/angular-animate.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="../node_modules/angular2/bundles/angular2.dev.js"></script>
<script src="../node_modules/angular2/bundles/http.dev.js"></script>
<script>
System.config({
packages: {
'js': {
defaultExtension: 'js'
},
'rxjs': {
defaultExtension: 'js'
}
},
map: {
'rxjs' : '/node_modules/rxjs'
}
});
System.import('js/app.module');
</script>
</head>
<body>
<div class="view-container">
<div ng-view class="view-frame"></div>
</div>
</body>
</html>
@@ -0,0 +1,50 @@
/// <reference path="../../typings/angularjs/angular.d.ts" />
/// <reference path="../../typings/angularjs/angular-resource.d.ts" />
/// <reference path="../../typings/angularjs/angular-route.d.ts" />
// #docregion adapter-import
import {UpgradeAdapter} from 'angular2/upgrade';
// #enddocregion adapter-import
// #docregion adapter-state-import
import upgradeAdapter from './core/upgrade_adapter';
// #enddocregion adapter-state-import
// #docregion http-import
import {HTTP_PROVIDERS} from 'angular2/http';
// #enddocregion http-import
import core from './core/core.module';
import phoneList from './phone_list/phone_list.module';
import phoneDetail from './phone_detail/phone_detail.module';
upgradeAdapter.addProvider(HTTP_PROVIDERS);
// #docregion upgrade-route-params
upgradeAdapter.upgradeNg1Provider('$routeParams');
// #enddocregion
angular.module('phonecatApp', [
'ngRoute',
core.name,
phoneList.name,
phoneDetail.name
]).config(configure);
configure.$inject = ['$routeProvider'];
function configure($routeProvider) {
// #docregion list-route
$routeProvider.
when('/phones', {
template: '<pc-phone-list></pc-phone-list>'
}).
// #enddocregion list-route
// #docregion detail-route
when('/phones/:phoneId', {
template: '<pc-phone-detail></pc-phone-detail>'
}).
// #enddocregion detail-route
otherwise({
redirectTo: '/phones'
});
}
// #docregion app-bootstrap
upgradeAdapter.bootstrap(document.documentElement, ['phonecatApp']);
// #enddocregion app-bootstrap
@@ -0,0 +1,9 @@
// #docregion
import {Pipe} from 'angular2/core';
@Pipe({name: 'checkmark'})
export class CheckmarkPipe {
transform(input:string): string {
return input ? '\u2713' : '\u2718';
}
}
@@ -0,0 +1,37 @@
// #docregion full
import {Injectable} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {Observable} from 'rxjs';
import 'rxjs/add/operator/map';
// #docregion phone-interface
export interface Phone {
name: string;
snippet: string;
images: string[];
}
// #enddocregion phone-interface
// #docregion fullclass
// #docregion class
@Injectable()
export class Phones {
// #enddocregion class
constructor(private http: Http) { }
query():Observable<Phone[]> {
return this.http.get(`phones/phones.json`)
.map((res:Response) => res.json());
}
get(id: string):Observable<Phone> {
return this.http.get(`phones/${id}.json`)
.map((res:Response) => res.json());
}
// #docregion class
}
// #enddocregion class
// #enddocregion fullclass
// #docregion full
@@ -0,0 +1,8 @@
// #docregion
import {Phones} from './Phones';
import upgradeAdapter from './upgrade_adapter';
upgradeAdapter.addProvider(Phones);
export default angular.module('phonecat.core', [])
.factory('phones', upgradeAdapter.downgradeNg2Provider(Phones));
@@ -0,0 +1,9 @@
// #docregion full
import {UpgradeAdapter} from 'angular2/upgrade';
// #docregion adapter-init
const upgradeAdapter = new UpgradeAdapter();
// #enddocregion adapter-init
export default upgradeAdapter;
// #enddocregion full
@@ -0,0 +1,33 @@
// #docregion
// #docregion top
import {Component, Inject} from 'angular2/core';
import {Phones, Phone} from '../core/Phones';
import {CheckmarkPipe} from '../core/CheckmarkPipe';
interface PhoneRouteParams {
phoneId: string
}
@Component({
selector: 'pc-phone-detail',
templateUrl: 'js/phone_detail/phone_detail.html',
pipes: [CheckmarkPipe]
})
class PhoneDetail {
// #enddocregion top
phone:Phone = undefined;
mainImageUrl:string;
constructor(@Inject('$routeParams') $routeParams:PhoneRouteParams,
phones:Phones) {
phones.get($routeParams.phoneId)
.subscribe(phone => {
this.phone = phone;
this.mainImageUrl = phone.images[0];
});
}
setImage(url:string) {
this.mainImageUrl = url;
}
}
export default PhoneDetail;
@@ -0,0 +1,29 @@
// #docregion
import {Component, Inject} from 'angular2/core';
import {Phones, Phone} from '../core/Phones';
interface PhoneRouteParams {
phoneId: string
}
@Component({
selector: 'pc-phone-detail',
templateUrl: 'js/phone_detail/phone_detail.html'
})
class PhoneDetail {
phone:Phone = undefined;
mainImageUrl:string;
constructor(@Inject('$routeParams') $routeParams:PhoneRouteParams,
phones:Phones) {
phones.get($routeParams.phoneId)
.subscribe(phone => {
this.phone = phone;
this.mainImageUrl = phone.images[0];
});
}
setImage(url:string) {
this.mainImageUrl = url;
}
}
export default PhoneDetail;
@@ -0,0 +1,115 @@
<!-- #docregion -->
<div class="phone-images">
<img [src]="img"
class="phone"
*ngFor="#img of phone?.images"
[ngClass]="{active: mainImageUrl==img}">
</div>
<h1>{{phone?.name}}</h1>
<p>{{phone?.description}}</p>
<ul class="phone-thumbs">
<li *ngFor="#img of phone?.images">
<img [src]="img" (click)="setImage(img)">
</li>
</ul>
<ul class="specs">
<li>
<span>Availability and Networks</span>
<dl>
<dt>Availability</dt>
<dd *ngFor="#availability of phone?.availability">{{availability}}</dd>
</dl>
</li>
<li>
<span>Battery</span>
<dl>
<dt>Type</dt>
<dd>{{phone?.battery?.type}}</dd>
<dt>Talk Time</dt>
<dd>{{phone?.battery?.talkTime}}</dd>
<dt>Standby time (max)</dt>
<dd>{{phone?.battery?.standbyTime}}</dd>
</dl>
</li>
<li>
<span>Storage and Memory</span>
<dl>
<dt>RAM</dt>
<dd>{{phone?.storage?.ram}}</dd>
<dt>Internal Storage</dt>
<dd>{{phone?.storage?.flash}}</dd>
</dl>
</li>
<li>
<span>Connectivity</span>
<dl>
<dt>Network Support</dt>
<dd>{{phone?.connectivity?.cell}}</dd>
<dt>WiFi</dt>
<dd>{{phone?.connectivity?.wifi}}</dd>
<dt>Bluetooth</dt>
<dd>{{phone?.connectivity?.bluetooth}}</dd>
<dt>Infrared</dt>
<dd>{{phone?.connectivity?.infrared | checkmark}}</dd>
<dt>GPS</dt>
<dd>{{phone?.connectivity?.gps | checkmark}}</dd>
</dl>
</li>
<li>
<span>Android</span>
<dl>
<dt>OS Version</dt>
<dd>{{phone?.android?.os}}</dd>
<dt>UI</dt>
<dd>{{phone?.android?.ui}}</dd>
</dl>
</li>
<li>
<span>Size and Weight</span>
<dl>
<dt>Dimensions</dt>
<dd *ngFor="#dim of phone?.sizeAndWeight?.dimensions">{{dim}}</dd>
<dt>Weight</dt>
<dd>{{phone?.sizeAndWeight?.weight}}</dd>
</dl>
</li>
<li>
<span>Display</span>
<dl>
<dt>Screen size</dt>
<dd>{{phone?.display?.screenSize}}</dd>
<dt>Screen resolution</dt>
<dd>{{phone?.display?.screenResolution}}</dd>
<dt>Touch screen</dt>
<dd>{{phone?.display?.touchScreen | checkmark}}</dd>
</dl>
</li>
<li>
<span>Hardware</span>
<dl>
<dt>CPU</dt>
<dd>{{phone?.hardware?.cpu}}</dd>
<dt>USB</dt>
<dd>{{phone?.hardware?.usb}}</dd>
<dt>Audio / headphone jack</dt>
<dd>{{phone?.hardware?.audioJack}}</dd>
<dt>FM Radio</dt>
<dd>{{phone?.hardware?.fmRadio | checkmark}}</dd>
<dt>Accelerometer</dt>
<dd>{{phone?.hardware?.accelerometer | checkmark}}</dd>
</dl>
</li>
<li>
<span>Camera</span>
<dl>
<dt>Primary</dt>
<dd>{{phone?.camera?.primary}}</dd>
<dt>Features</dt>
<dd>{{phone?.camera?.features?.join(', ')}}</dd>
</dl>
</li>
<li>
<span>Additional Features</span>
<dd>{{phone?.additionalFeatures}}</dd>
</li>
</ul>
@@ -0,0 +1,10 @@
// #docregion
import PhoneDetail from './PhoneDetail';
import upgradeAdapter from '../core/upgrade_adapter';
export default angular.module('phonecat.detail', [
'ngRoute',
'phonecat.core'
])
.directive('pcPhoneDetail',
<angular.IDirectiveFactory>upgradeAdapter.downgradeNg2Component(PhoneDetail))
@@ -0,0 +1,24 @@
// #docregion
import {Pipe} from 'angular2/core';
@Pipe({name: 'orderBy'})
export default class OrderByPipe {
transform<T>(input:T[], args:string[]): T[] {
if (input) {
let property = args[0];
return input.slice().sort((a, b) => {
if (a[property] < b[property]) {
return -1;
} else if (b[property] < a[property]) {
return 1;
} else {
return 0;
}
});
} else {
return input;
}
}
}
@@ -0,0 +1,22 @@
// #docregion
import {Pipe} from 'angular2/core';
import {Phone} from '../core/Phones';
@Pipe({name: 'phoneFilter'})
export default class PhoneFilterPipe {
transform(input:Phone[], args:string[]): Phone[] {
let query = args[0];
if (query) {
query = query.toLowerCase();
return input.filter((phone) => {
const name = phone.name.toLowerCase();
const snippet = phone.snippet.toLowerCase();
return name.indexOf(query) >= 0 || snippet.indexOf(query) >= 0;
});
} else {
return input;
}
}
}
@@ -0,0 +1,26 @@
// #docregion full
// #docregion top
import {Component} from 'angular2/core';
import {Observable} from 'rxjs';
import {Phones, Phone} from '../core/Phones';
import PhoneFilterPipe from './PhoneFilterPipe';
import OrderByPipe from './OrderByPipe';
@Component({
selector: 'pc-phone-list',
templateUrl: 'js/phone_list/phone_list.html',
pipes: [PhoneFilterPipe, OrderByPipe],
})
class PhoneList {
// #enddocregion top
phones:Observable<Phone[]>;
orderProp:string;
query:string;
constructor(phones:Phones) {
this.phones = phones.query();
this.orderProp = 'age';
}
}
export default PhoneList;
@@ -0,0 +1,22 @@
// #docregion top
import {Component} from 'angular2/core';
import {Observable} from 'rxjs';
import {Phones, Phone} from '../core/Phones';
@Component({
selector: 'pc-phone-list',
templateUrl: 'js/phone_list/phone_list.html'
})
class PhoneList {
// #enddocregion top
phones:Observable<Phone[]>;
orderProp:string;
query:string;
constructor(phones:Phones) {
this.phones = phones.query();
this.orderProp = 'age';
}
}
export default PhoneList;
@@ -0,0 +1,32 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
<!-- #docregion controls -->
Search: <input [(ngModel)]="query">
Sort by:
<select [(ngModel)]="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<!-- #enddocregion controls -->
</div>
<div class="col-md-10">
<!--Body content-->
<!-- #docregion list -->
<ul class="phones">
<li *ngFor="#phone of phones | async | phoneFilter:query | orderBy:orderProp"
class="thumbnail phone-listing">
<a href="#/phones/{{phone.id}}" class="thumb"><img [src]="phone.imageUrl"></a>
<a href="#/phones/{{phone.id}}" class="name">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
<!-- #enddocregion list -->
</div>
</div>
</div>
@@ -0,0 +1,9 @@
// #docregion
import PhoneList from './PhoneList';
import upgradeAdapter from '../core/upgrade_adapter';
export default angular.module('phonecat.list', [
'phonecat.core'
])
.directive('pcPhoneList',
<angular.IDirectiveFactory>upgradeAdapter.downgradeNg2Component(PhoneList));
@@ -0,0 +1,32 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
<!-- #docregion controls -->
Search: <input [(ngModel)]="query">
Sort by:
<select [(ngModel)]="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<!-- #enddocregion controls -->
</div>
<div class="col-md-10">
<!--Body content-->
<!-- #docregion list -->
<ul class="phones">
<li *ngFor="#phone of phones | phoneFilter:query | orderBy:orderProp"
class="thumbnail phone-listing">
<a href="#/phones/{{phone.id}}" class="thumb"><img [src]="phone.imageUrl"></a>
<a href="#/phones/{{phone.id}}" class="name">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
<!-- #enddocregion list -->
</div>
</div>
</div>
@@ -0,0 +1,32 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
<!-- #docregion controls -->
Search: <input [(ngModel)]="query">
Sort by:
<select [(ngModel)]="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<!-- #enddocregion controls -->
</div>
<div class="col-md-10">
<!--Body content-->
<!-- #docregion list -->
<ul class="phones">
<li *ngFor="#phone of phones | filter:query | orderBy:orderProp"
class="thumbnail phone-listing">
<a href="#/phones/{{phone.id}}" class="thumb"><img [src]="phone.imageUrl"></a>
<a href="#/phones/{{phone.id}}" class="name">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
<!-- #enddocregion list -->
</div>
</div>
</div>
@@ -0,0 +1,20 @@
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.5.0-beta.2",
"angular-mocks": "1.5.0-beta.2",
"jquery": "~2.1.1",
"bootstrap": "~3.1.1",
"angular-route": "1.5.0-beta.2",
"angular-resource": "1.5.0-beta.2",
"angular-animate": "1.5.0-beta.2"
},
"resolutions": {
"angular": "1.5.0-beta.2"
}
}
@@ -0,0 +1,112 @@
'use strict';
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
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#/phones');
});
it('should filter the phone list as a user types into the search box', function() {
var phoneList = element.all(by.css('.phones li'));
var query = element(by.css('input'));
expect(phoneList.count()).toBe(20);
query.sendKeys('nexus');
expect(phoneList.count()).toBe(1);
query.clear();
// https://github.com/angular/protractor/issues/2019
let str = 'motorola';
for (let i:number = 0; i < str.length; i++) {
query.sendKeys(str.charAt(i));
}
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.css('.phones .name'));
var query = element(by.css('input'));
function getNames() {
return phoneNameColumn.map(function(elm) {
return elm.getText();
});
}
//let's narrow the dataset to make the test assertions shorter
// https://github.com/angular/protractor/issues/2019
let str = 'tablet';
for (let i:number = 0; i < str.length; i++) {
query.sendKeys(str.charAt(i));
}
expect(getNames()).toEqual([
"Motorola XOOM\u2122 with Wi-Fi",
"MOTOROLA XOOM\u2122"
]);
element(by.css('select')).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.css('input'));
// https://github.com/angular/protractor/issues/2019
let str = 'nexus';
for (let i:number = 0; i < str.length; i++) {
query.sendKeys(str.charAt(i));
}
element.all(by.css('.phones li a')).first().click();
browser.getLocationAbsUrl().then(function(url) {
expect(url).toEqual('/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.css('h1')).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-of-type(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-of-type(1) img')).click();
expect(element(by.css('img.phone.active')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
});
});
});
@@ -0,0 +1,54 @@
// #docregion
// Cancel Karma's synchronous start,
// we will call `__karma__.start()` later, once all the specs are loaded.
__karma__.loaded = function() {};
System.config({
packages: {
'base/app/js': {
defaultExtension: false,
format: 'register',
map: Object.keys(window.__karma__.files).
filter(onlyAppFiles).
reduce(function createPathRecords(pathsMapping, appPath) {
// creates local module name mapping to global path with karma's fingerprint in path, e.g.:
// './hero.service': '/base/src/app/hero.service.js?f4523daf879cfb7310ef6242682ccf10b2041b3e'
var moduleName = appPath.replace(/^\/base\/app\/js\//, './').replace(/\.js$/, '');
pathsMapping[moduleName] = appPath + '?' + window.__karma__.files[appPath]
return pathsMapping;
}, {})
},
'rxjs': {
defaultExtension: 'js'
}
},
map: {
'rxjs' : '/base/node_modules/rxjs'
}
});
// #docregion ng2
System.import('angular2/src/platform/browser/browser_adapter').then(function(browser_adapter) {
browser_adapter.BrowserDomAdapter.makeCurrent();
}).then(function() {
return Promise.all(
Object.keys(window.__karma__.files) // All files served by Karma.
.filter(onlySpecFiles)
.map(function(moduleName) {
// loads all spec files via their global module names
return System.import(moduleName);
}));
}).then(function() {
__karma__.start();
}, function(error) {
__karma__.error(error.stack || error);
});
// #enddocregion ng2
function onlyAppFiles(filePath) {
return /^\/base\/app\/js\/.*\.js$/.test(filePath)
}
function onlySpecFiles(path) {
return /\.spec\.js$/.test(path);
}
@@ -0,0 +1,21 @@
exports.config = {
allScriptsTimeout: 11000,
specs: [
'e2e/*.js'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:8000/',
framework: 'jasmine',
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
}
};
@@ -0,0 +1,2 @@
// #docregion
/// <reference path="../typings/angularjs/angular-mocks.d.ts" />
@@ -0,0 +1,15 @@
// #docregion
import {describe, beforeEachProviders, it, inject, expect} from 'angular2/testing';
import {CheckmarkPipe} from '../../app/js/core/CheckmarkPipe';
describe('CheckmarkPipe', function() {
beforeEachProviders(() => [CheckmarkPipe]);
it('should convert boolean values to unicode checkmark or cross',
inject([CheckmarkPipe], (checkmarkPipe) => {
expect(checkmarkPipe.transform(true)).toBe('\u2713');
expect(checkmarkPipe.transform(false)).toBe('\u2718');
}));
});
@@ -0,0 +1,19 @@
// #docregion
import {describe, beforeEachProviders, it, inject} from 'angular2/testing';
import OrderByPipe from '../../app/js/phone_list/OrderByPipe';
describe('OrderByPipe', function() {
let input:any[] = [
{name: 'Nexus S', snippet: 'The Nexus S Phone', images: []},
{name: 'Motorola DROID', snippet: 'An Android-for-business smartphone', images: []}
];
beforeEachProviders(() => [OrderByPipe]);
it('should order by the given property', inject([OrderByPipe], (orderByPipe) => {
expect(orderByPipe.transform(input, ['name'])).toEqual([input[1], input[0]]);
}));
});
@@ -0,0 +1,49 @@
// #docregion
import {provide} from 'angular2/core';
import {HTTP_PROVIDERS} from 'angular2/http';
import {Observable} from 'rxjs';
import {FromObservable} from 'rxjs/observable/from';
import {
describe,
beforeEachProviders,
injectAsync,
it,
expect,
TestComponentBuilder
} from 'angular2/testing';
import PhoneDetail from '../../app/js/phone_detail/PhoneDetail';
import {Phones, Phone} from '../../app/js/core/Phones';
function xyzPhoneData():Phone {
return {
name: 'phone xyz',
snippet: '',
images: ['image/url1.png', 'image/url2.png']
}
}
class MockPhones extends Phones {
get(id):Observable<Phone> {
return FromObservable.create([xyzPhoneData()]);
}
}
describe('PhoneDetail', function(){
beforeEachProviders(() => [
provide(Phones, {useClass: MockPhones}),
provide('$routeParams', {useValue: {phoneId: 'xyz'}}),
HTTP_PROVIDERS
]);
it('should fetch phone detail', injectAsync([TestComponentBuilder], (tcb) => {
return tcb.createAsync(PhoneDetail).then((fixture) => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.h1')).toHaveText(xyzPhoneData().name);
});
}));
});
@@ -0,0 +1,28 @@
// #docregion
import {describe, beforeEachProviders, it, inject} from 'angular2/testing';
import PhoneFilterPipe from '../../app/js/phone_list/PhoneFilterPipe';
import {Phone} from '../../app/js/core/Phones';
describe('PhoneFilterPipe', function() {
let phones:Phone[] = [
{name: 'Nexus S', snippet: 'The Nexus S Phone', images: []},
{name: 'Motorola DROID', snippet: 'an Android-for-business smartphone', images: []}
];
beforeEachProviders(() => [PhoneFilterPipe]);
it('should return input when no query', inject([PhoneFilterPipe], (phoneFilterPipe) => {
expect(phoneFilterPipe.transform(phones, [])).toEqual(phones);
}));
it('should match based on name', inject([PhoneFilterPipe], (phoneFilterPipe) => {
expect(phoneFilterPipe.transform(phones, ['nexus'])).toEqual([phones[0]]);
}));
it('should match based on snippet', inject([PhoneFilterPipe], (phoneFilterPipe) => {
expect(phoneFilterPipe.transform(phones, ['android'])).toEqual([phones[1]]);
}));
});
@@ -0,0 +1,58 @@
// #docregion
import {provide} from 'angular2/core';
import {HTTP_PROVIDERS} from 'angular2/http';
import {Observable} from 'rxjs';
import {FromObservable} from 'rxjs/observable/from';
import {
describe,
beforeEachProviders,
injectAsync,
it,
expect,
TestComponentBuilder
} from 'angular2/testing';
import PhoneList from '../../app/js/phone_list/PhoneList';
import {Phones, Phone} from '../../app/js/core/Phones';
class MockPhones extends Phones {
query():Observable<Phone[]> {
return FromObservable.create([
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]
])
}
}
describe('PhoneList', function(){
beforeEachProviders(() => [
provide(Phones, {useClass: MockPhones}),
HTTP_PROVIDERS
]);
it('should create "phones" model with 2 phones fetched from xhr',
injectAsync([TestComponentBuilder], (tcb) => {
return tcb.createAsync(PhoneList).then((fixture) => {
fixture.detectChanges();
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelectorAll('.phone-listing').length).toBe(2);
expect(compiled.querySelector('.phone-listing:nth-child(1)')).toHaveText('Nexus S');
expect(compiled.querySelector('.phone-listing:nth-child(2)')).toHaveText('Motorola DROID');
});
}));
it('should set the default value of orderProp model',
injectAsync([TestComponentBuilder], (tcb) => {
return tcb.createAsync(PhoneList).then((fixture) => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('select option:last-child').selected).toBe(true);
});
}));
});
@@ -0,0 +1,16 @@
// #docregion
import {describe, beforeEachProviders, it, inject} from 'angular2/testing';
import {HTTP_PROVIDERS} from 'angular2/http';
import {Phones} from '../../app/js/core/Phones';
describe('Phones', function() {
// load providers
beforeEachProviders(() => [Phones, HTTP_PROVIDERS]);
// Test service availability
it('check the existence of Phones', inject([Phones], (phones) => {
expect(phones).toBeDefined();
}));
});
@@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}
@@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map
@@ -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; }
}
@@ -0,0 +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-of-type {
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;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,40 @@
<!-- #docregion -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Google Phone Gallery</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/animations.css">
<!-- #docregion scripts -->
<script src="../node_modules/systemjs/dist/system.src.js"></script>
<script src="../node_modules/angular2/bundles/angular2.dev.js"></script>
<script src="../node_modules/angular2/bundles/http.dev.js"></script>
<!-- #docregion ng2-router -->
<script src="../node_modules/angular2/bundles/router.dev.js"></script>
<!-- #enddocregion ng2-router -->
<script>
System.config({
packages: {
'js': {
defaultExtension: 'js'
},
'rxjs': {
defaultExtension: 'js'
}
},
map: {
'rxjs' : '/node_modules/rxjs'
}
});
System.import('js/app');
</script>
<!-- #enddocregion scripts -->
</head>
<!-- #docregion body -->
<body>
<pc-app></pc-app>
</body>
<!-- #enddocregion body -->
</html>
@@ -0,0 +1,48 @@
// #docregion
// #docregion importbootstrap
import {Component, provide} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {Phones} from './core/Phones';
import PhoneList from './phone_list/PhoneList';
import PhoneDetail from './phone_detail/PhoneDetail';
// #enddocregion importbootstrap
// #docregion http-import
import {HTTP_PROVIDERS} from 'angular2/http';
// #enddocregion http-import
// #docregion router-import
import {
RouteConfig,
LocationStrategy,
HashLocationStrategy,
ROUTER_DIRECTIVES,
ROUTER_PROVIDERS
} from 'angular2/router';
// #enddocregion router-import
// #docregion appcomponent
@RouteConfig([
{path:'/phones', as: 'Phones', component: PhoneList},
{path:'/phones/:phoneId', as: 'Phone', component: PhoneDetail},
{path:'/', redirectTo: ['/phones']}
])
@Component({
selector: 'pc-app',
template: '<router-outlet></router-outlet>',
directives: [ROUTER_DIRECTIVES]
})
class AppComponent {
}
// #enddocregion appcomponent
// #docregion bootstrap
bootstrap(AppComponent, [
HTTP_PROVIDERS,
ROUTER_PROVIDERS,
ROUTER_DIRECTIVES,
provide(LocationStrategy, {useClass: HashLocationStrategy}),
Phones
]);
// #enddocregion bootstrap
@@ -0,0 +1,9 @@
// #docregion
import {Pipe} from 'angular2/core';
@Pipe({name: 'checkmark'})
export class CheckmarkPipe {
transform(input:string): string {
return input ? '\u2713' : '\u2718';
}
}
@@ -0,0 +1,37 @@
// #docregion full
import {Injectable} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import {Observable} from 'rxjs';
import 'rxjs/add/operator/map';
// #docregion phone-interface
export interface Phone {
name: string;
snippet: string;
images: string[];
}
// #enddocregion phone-interface
// #docregion fullclass
// #docregion class
@Injectable()
export class Phones {
// #enddocregion class
constructor(private http: Http) { }
query():Observable<Phone[]> {
return this.http.get(`phones/phones.json`)
.map((res:Response) => res.json());
}
get(id: string):Observable<Phone> {
return this.http.get(`phones/${id}.json`)
.map((res:Response) => res.json());
}
// #docregion class
}
// #enddocregion class
// #enddocregion fullclass
// #docregion full
@@ -0,0 +1,9 @@
// #docregion full
import {UpgradeAdapter} from 'angular2/upgrade';
// #docregion adapter-init
const upgradeAdapter = new UpgradeAdapter();
// #enddocregion adapter-init
export default upgradeAdapter;
// #enddocregion full
@@ -0,0 +1,30 @@
// #docregion
// #docregion top
import {Component, Inject} from 'angular2/core';
import {RouteParams} from 'angular2/router';
import {Phones, Phone} from '../core/Phones';
import {CheckmarkPipe} from '../core/CheckmarkPipe';
@Component({
selector: 'pc-phone-detail',
templateUrl: 'js/phone_detail/phone_detail.html',
pipes: [CheckmarkPipe]
})
class PhoneDetail {
// #enddocregion top
phone:Phone = undefined;
mainImageUrl:string;
constructor(params:RouteParams,
phones:Phones) {
phones.get(params.get('phoneId'))
.subscribe(phone => {
this.phone = phone;
this.mainImageUrl = phone.images[0];
});
}
setImage(url:string) {
this.mainImageUrl = url;
}
}
export default PhoneDetail;
@@ -0,0 +1,115 @@
<!-- #docregion -->
<div class="phone-images">
<img [src]="img"
class="phone"
*ngFor="#img of phone?.images"
[ngClass]="{active: mainImageUrl==img}">
</div>
<h1>{{phone?.name}}</h1>
<p>{{phone?.description}}</p>
<ul class="phone-thumbs">
<li *ngFor="#img of phone?.images">
<img [src]="img" (click)="setImage(img)">
</li>
</ul>
<ul class="specs">
<li>
<span>Availability and Networks</span>
<dl>
<dt>Availability</dt>
<dd *ngFor="#availability of phone?.availability">{{availability}}</dd>
</dl>
</li>
<li>
<span>Battery</span>
<dl>
<dt>Type</dt>
<dd>{{phone?.battery?.type}}</dd>
<dt>Talk Time</dt>
<dd>{{phone?.battery?.talkTime}}</dd>
<dt>Standby time (max)</dt>
<dd>{{phone?.battery?.standbyTime}}</dd>
</dl>
</li>
<li>
<span>Storage and Memory</span>
<dl>
<dt>RAM</dt>
<dd>{{phone?.storage?.ram}}</dd>
<dt>Internal Storage</dt>
<dd>{{phone?.storage?.flash}}</dd>
</dl>
</li>
<li>
<span>Connectivity</span>
<dl>
<dt>Network Support</dt>
<dd>{{phone?.connectivity?.cell}}</dd>
<dt>WiFi</dt>
<dd>{{phone?.connectivity?.wifi}}</dd>
<dt>Bluetooth</dt>
<dd>{{phone?.connectivity?.bluetooth}}</dd>
<dt>Infrared</dt>
<dd>{{phone?.connectivity?.infrared | checkmark}}</dd>
<dt>GPS</dt>
<dd>{{phone?.connectivity?.gps | checkmark}}</dd>
</dl>
</li>
<li>
<span>Android</span>
<dl>
<dt>OS Version</dt>
<dd>{{phone?.android?.os}}</dd>
<dt>UI</dt>
<dd>{{phone?.android?.ui}}</dd>
</dl>
</li>
<li>
<span>Size and Weight</span>
<dl>
<dt>Dimensions</dt>
<dd *ngFor="#dim of phone?.sizeAndWeight?.dimensions">{{dim}}</dd>
<dt>Weight</dt>
<dd>{{phone?.sizeAndWeight?.weight}}</dd>
</dl>
</li>
<li>
<span>Display</span>
<dl>
<dt>Screen size</dt>
<dd>{{phone?.display?.screenSize}}</dd>
<dt>Screen resolution</dt>
<dd>{{phone?.display?.screenResolution}}</dd>
<dt>Touch screen</dt>
<dd>{{phone?.display?.touchScreen | checkmark}}</dd>
</dl>
</li>
<li>
<span>Hardware</span>
<dl>
<dt>CPU</dt>
<dd>{{phone?.hardware?.cpu}}</dd>
<dt>USB</dt>
<dd>{{phone?.hardware?.usb}}</dd>
<dt>Audio / headphone jack</dt>
<dd>{{phone?.hardware?.audioJack}}</dd>
<dt>FM Radio</dt>
<dd>{{phone?.hardware?.fmRadio | checkmark}}</dd>
<dt>Accelerometer</dt>
<dd>{{phone?.hardware?.accelerometer | checkmark}}</dd>
</dl>
</li>
<li>
<span>Camera</span>
<dl>
<dt>Primary</dt>
<dd>{{phone?.camera?.primary}}</dd>
<dt>Features</dt>
<dd>{{phone?.camera?.features?.join(', ')}}</dd>
</dl>
</li>
<li>
<span>Additional Features</span>
<dd>{{phone?.additionalFeatures}}</dd>
</li>
</ul>
@@ -0,0 +1,24 @@
// #docregion
import {Pipe} from 'angular2/core';
@Pipe({name: 'orderBy'})
export default class OrderByPipe {
transform<T>(input:T[], args:string[]): T[] {
if (input) {
let property = args[0];
return input.slice().sort((a, b) => {
if (a[property] < b[property]) {
return -1;
} else if (b[property] < a[property]) {
return 1;
} else {
return 0;
}
});
} else {
return input;
}
}
}
@@ -0,0 +1,22 @@
// #docregion
import {Pipe} from 'angular2/core';
import {Phone} from '../core/Phones';
@Pipe({name: 'phoneFilter'})
export default class PhoneFilterPipe {
transform(input:Phone[], args:string[]): Phone[] {
let query = args[0];
if (query) {
query = query.toLowerCase();
return input.filter((phone) => {
const name = phone.name.toLowerCase();
const snippet = phone.snippet.toLowerCase();
return name.indexOf(query) >= 0 || snippet.indexOf(query) >= 0;
});
} else {
return input;
}
}
}
@@ -0,0 +1,28 @@
// #docregion full
// #docregion top
import {Component} from 'angular2/core';
import {RouterLink} from 'angular2/router';
import {Observable} from 'rxjs';
import {Phones, Phone} from '../core/Phones';
import PhoneFilterPipe from './PhoneFilterPipe';
import OrderByPipe from './OrderByPipe';
@Component({
selector: 'pc-phone-list',
templateUrl: 'js/phone_list/phone_list.html',
pipes: [PhoneFilterPipe, OrderByPipe],
directives: [RouterLink]
})
class PhoneList {
// #enddocregion top
phones:Observable<Phone[]>;
orderProp:string;
query:string;
constructor(phones:Phones) {
this.phones = phones.query();
this.orderProp = 'age';
}
}
export default PhoneList;
@@ -0,0 +1,32 @@
<div class="container-fluid">
<div class="row">
<div class="col-md-2">
<!--Sidebar content-->
<!-- #docregion controls -->
Search: <input [(ngModel)]="query">
Sort by:
<select [(ngModel)]="orderProp">
<option value="name">Alphabetical</option>
<option value="age">Newest</option>
</select>
<!-- #enddocregion controls -->
</div>
<div class="col-md-10">
<!--Body content-->
<!-- #docregion list -->
<ul class="phones">
<li *ngFor="#phone of phones | async | phoneFilter:query | orderBy:orderProp"
class="thumbnail phone-listing">
<a [routerLink]="['/Phone', {phoneId: phone.id}]" class="thumb"><img [src]="phone.imageUrl"></a>
<a [routerLink]="['/Phone', {phoneId: phone.id}]" class="name">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
<!-- #enddocregion list -->
</div>
</div>
</div>
@@ -0,0 +1,20 @@
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
"dependencies": {
"angular": "1.5.0-beta.2",
"angular-mocks": "1.5.0-beta.2",
"jquery": "~2.1.1",
"bootstrap": "~3.1.1",
"angular-route": "1.5.0-beta.2",
"angular-resource": "1.5.0-beta.2",
"angular-animate": "1.5.0-beta.2"
},
"resolutions": {
"angular": "1.5.0-beta.2"
}
}
@@ -0,0 +1,114 @@
'use strict';
/* http://docs.angularjs.org/guide/dev_guide.e2e-testing */
describe('PhoneCat App', function() {
// #docregion redirect
it('should redirect index.html to index.html#/phones', function() {
browser.get('app/index.html');
browser.waitForAngular();
browser.getCurrentUrl().then(function(url) {
expect(url.endsWith('/phones')).toBe(true);
});
});
// #enddocregion redirect
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.css('.phones li'));
var query = element(by.css('input'));
expect(phoneList.count()).toBe(20);
query.sendKeys('nexus');
expect(phoneList.count()).toBe(1);
query.clear();
// https://github.com/angular/protractor/issues/2019
let str = 'motorola';
for (let i:number = 0; i < str.length; i++) {
query.sendKeys(str.charAt(i));
}
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.css('.phones .name'));
var query = element(by.css('input'));
function getNames() {
return phoneNameColumn.map(function(elm) {
return elm.getText();
});
}
//let's narrow the dataset to make the test assertions shorter
// https://github.com/angular/protractor/issues/2019
let str = 'tablet';
for (let i:number = 0; i < str.length; i++) {
query.sendKeys(str.charAt(i));
}
expect(getNames()).toEqual([
"Motorola XOOM\u2122 with Wi-Fi",
"MOTOROLA XOOM\u2122"
]);
element(by.css('select')).element(by.css('option[value="name"]')).click();
expect(getNames()).toEqual([
"MOTOROLA XOOM\u2122",
"Motorola XOOM\u2122 with Wi-Fi"
]);
});
// #docregion links
it('should render phone specific links', function() {
var query = element(by.css('input'));
// https://github.com/angular/protractor/issues/2019
let str = 'nexus';
for (let i:number = 0; i < str.length; i++) {
query.sendKeys(str.charAt(i));
}
element.all(by.css('.phones li a')).first().click();
browser.getCurrentUrl().then(function(url) {
expect(url.endsWith('/phones/nexus-s')).toBe(true);
});
});
});
// #enddocregion links
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.css('h1')).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-of-type(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-of-type(1) img')).click();
expect(element(by.css('img.phone.active')).getAttribute('src')).toMatch(/img\/phones\/nexus-s.0.jpg/);
});
});
});
@@ -0,0 +1,54 @@
// #docregion
// Cancel Karma's synchronous start,
// we will call `__karma__.start()` later, once all the specs are loaded.
__karma__.loaded = function() {};
System.config({
packages: {
'base/app/js': {
defaultExtension: false,
format: 'register',
map: Object.keys(window.__karma__.files).
filter(onlyAppFiles).
reduce(function createPathRecords(pathsMapping, appPath) {
// creates local module name mapping to global path with karma's fingerprint in path, e.g.:
// './hero.service': '/base/src/app/hero.service.js?f4523daf879cfb7310ef6242682ccf10b2041b3e'
var moduleName = appPath.replace(/^\/base\/app\/js\//, './').replace(/\.js$/, '');
pathsMapping[moduleName] = appPath + '?' + window.__karma__.files[appPath]
return pathsMapping;
}, {})
},
'rxjs': {
defaultExtension: 'js'
}
},
map: {
'rxjs' : '/base/node_modules/rxjs'
}
});
// #docregion ng2
System.import('angular2/src/platform/browser/browser_adapter').then(function(browser_adapter) {
browser_adapter.BrowserDomAdapter.makeCurrent();
}).then(function() {
return Promise.all(
Object.keys(window.__karma__.files) // All files served by Karma.
.filter(onlySpecFiles)
.map(function(moduleName) {
// loads all spec files via their global module names
return System.import(moduleName);
}));
}).then(function() {
__karma__.start();
}, function(error) {
__karma__.error(error.stack || error);
});
// #enddocregion ng2
function onlyAppFiles(filePath) {
return /^\/base\/app\/js\/.*\.js$/.test(filePath)
}
function onlySpecFiles(path) {
return /\.spec\.js$/.test(path);
}
@@ -0,0 +1,25 @@
exports.config = {
allScriptsTimeout: 11000,
specs: [
'e2e/*.js'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:8000/',
framework: 'jasmine',
jasmineNodeOpts: {
defaultTimeoutInterval: 30000
},
// #docregion ng2
useAllAngular2AppRoots: true
// #enddocregion ng2
};
@@ -0,0 +1,15 @@
// #docregion
import {describe, beforeEachProviders, it, inject, expect} from 'angular2/testing';
import {CheckmarkPipe} from '../../app/js/core/CheckmarkPipe';
describe('CheckmarkPipe', function() {
beforeEachProviders(() => [CheckmarkPipe]);
it('should convert boolean values to unicode checkmark or cross',
inject([CheckmarkPipe], (checkmarkPipe) => {
expect(checkmarkPipe.transform(true)).toBe('\u2713');
expect(checkmarkPipe.transform(false)).toBe('\u2718');
}));
});
@@ -0,0 +1,19 @@
// #docregion
import {describe, beforeEachProviders, it, inject} from 'angular2/testing';
import OrderByPipe from '../../app/js/phone_list/OrderByPipe';
describe('OrderByPipe', function() {
let input:any[] = [
{name: 'Nexus S', snippet: 'The Nexus S Phone', images: []},
{name: 'Motorola DROID', snippet: 'An Android-for-business smartphone', images: []}
];
beforeEachProviders(() => [OrderByPipe]);
it('should order by the given property', inject([OrderByPipe], (orderByPipe) => {
expect(orderByPipe.transform(input, ['name'])).toEqual([input[1], input[0]]);
}));
});
@@ -0,0 +1,53 @@
import {provide} from 'angular2/core';
// #docregion routeparams
import {RouteParams} from 'angular2/router';
// #enddocregion routeparams
import {HTTP_PROVIDERS} from 'angular2/http';
import {Observable} from 'rxjs';
import {FromObservable} from 'rxjs/observable/from';
import {
describe,
beforeEachProviders,
injectAsync,
it,
expect,
TestComponentBuilder
} from 'angular2/testing';
import PhoneDetail from '../../app/js/phone_detail/PhoneDetail';
import {Phones, Phone} from '../../app/js/core/Phones';
function xyzPhoneData():Phone {
return {
name: 'phone xyz',
snippet: '',
images: ['image/url1.png', 'image/url2.png']
}
}
class MockPhones extends Phones {
get(id):Observable<Phone> {
return FromObservable.create([xyzPhoneData()]);
}
}
// #docregion routeparams
describe('PhoneDetail', function(){
beforeEachProviders(() => [
provide(Phones, {useClass: MockPhones}),
provide(RouteParams, {useValue: new RouteParams({phoneId: 'xyz'})}),
HTTP_PROVIDERS
]);
// #enddocregion routeparams
it('should fetch phone detail', injectAsync([TestComponentBuilder], (tcb) => {
return tcb.createAsync(PhoneDetail).then((fixture) => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.h1')).toHaveText(xyzPhoneData().name);
});
}));
});
@@ -0,0 +1,28 @@
// #docregion
import {describe, beforeEachProviders, it, inject} from 'angular2/testing';
import PhoneFilterPipe from '../../app/js/phone_list/PhoneFilterPipe';
import {Phone} from '../../app/js/core/Phones';
describe('PhoneFilterPipe', function() {
let phones:Phone[] = [
{name: 'Nexus S', snippet: 'The Nexus S Phone', images: []},
{name: 'Motorola DROID', snippet: 'an Android-for-business smartphone', images: []}
];
beforeEachProviders(() => [PhoneFilterPipe]);
it('should return input when no query', inject([PhoneFilterPipe], (phoneFilterPipe) => {
expect(phoneFilterPipe.transform(phones, [])).toEqual(phones);
}));
it('should match based on name', inject([PhoneFilterPipe], (phoneFilterPipe) => {
expect(phoneFilterPipe.transform(phones, ['nexus'])).toEqual([phones[0]]);
}));
it('should match based on snippet', inject([PhoneFilterPipe], (phoneFilterPipe) => {
expect(phoneFilterPipe.transform(phones, ['android'])).toEqual([phones[1]]);
}));
});
@@ -0,0 +1,57 @@
// #docregion
import {provide} from 'angular2/core';
import {HTTP_PROVIDERS} from 'angular2/http';
import {Observable} from 'rxjs';
import {FromObservable} from 'rxjs/observable/from';
import {
describe,
beforeEachProviders,
injectAsync,
it,
expect,
TestComponentBuilder
} from 'angular2/testing';
import PhoneList from '../../app/js/phone_list/PhoneList';
import {Phones, Phone} from '../../app/js/core/Phones';
class MockPhones extends Phones {
query():Observable<Phone[]> {
return FromObservable.create([
[{name: 'Nexus S'}, {name: 'Motorola DROID'}]
])
}
}
describe('PhoneList', function(){
beforeEachProviders(() => [
provide(Phones, {useClass: MockPhones}),
HTTP_PROVIDERS
]);
it('should create "phones" model with 2 phones fetched from xhr',
injectAsync([TestComponentBuilder], (tcb) => {
return tcb.createAsync(PhoneList).then((fixture) => {
fixture.detectChanges();
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelectorAll('.phone-listing').length).toBe(2);
expect(compiled.querySelector('.phone-listing:nth-child(1)')).toHaveText('Nexus S');
expect(compiled.querySelector('.phone-listing:nth-child(2)')).toHaveText('Motorola DROID');
});
}));
it('should set the default value of orderProp model',
injectAsync([TestComponentBuilder], (tcb) => {
return tcb.createAsync(PhoneList).then((fixture) => {
fixture.detectChanges();
let compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('select option:last-child').selected).toBe(true);
});
}));
});
@@ -0,0 +1,16 @@
// #docregion
import {describe, beforeEachProviders, it, inject} from 'angular2/testing';
import {HTTP_PROVIDERS} from 'angular2/http';
import {Phones} from '../../app/js/core/Phones';
describe('Phones', function() {
// load providers
beforeEachProviders(() => [Phones, HTTP_PROVIDERS]);
// Test service availability
it('check the existence of Phones', inject([Phones], (phones) => {
expect(phones).toBeDefined();
}));
});
@@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}
@@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map
@@ -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; }
}

Some files were not shown because too many files have changed in this diff Show More