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

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`.

View File

@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}

View File

@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map

View File

@ -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; }
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,6 @@
// #docregion
export default function checkmarkFilter() {
return function(input:boolean):string {
return input ? '\u2713' : '\u2718';
};
}

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -0,0 +1,8 @@
// #docregion
import PhoneDetailCtrl from './phone_detail.controller';
export default angular.module('phonecat.detail', [
'ngRoute',
'phonecat.core'
])
.controller('PhoneDetailCtrl', PhoneDetailCtrl);

View File

@ -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;

View File

@ -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>

View File

@ -0,0 +1,5 @@
// #docregion
import PhoneListCtrl from './phone_list.controller';
export default angular.module('phonecat.list', ['phonecat.core'])
.controller('PhoneListCtrl', PhoneListCtrl);

View File

@ -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"
}
}

View File

@ -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/);
});
});
});

View File

@ -0,0 +1,6 @@
// #docregion
declare module jasmine {
interface Matchers {
toEqualData(expected: any):boolean;
}
}

View File

@ -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);
}

View File

@ -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
}
};

View File

@ -0,0 +1,3 @@
// #docregion
/// <reference path="../typings/jasmine/jasmine.d.ts" />
/// <reference path="../typings/angularjs/angular-mocks.d.ts" />

View File

@ -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');
}));
});

View File

@ -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();
}));
});

View File

@ -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());
});
});

View File

@ -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');
});
});

View File

@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}

View File

@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map

View File

@ -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; }
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,9 @@
// #docregion
import {Pipe} from 'angular2/core';
@Pipe({name: 'checkmark'})
export class CheckmarkPipe {
transform(input:string): string {
return input ? '\u2713' : '\u2718';
}
}

View File

@ -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

View File

@ -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));

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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))

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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));

View File

@ -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>

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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/);
});
});
});

View File

@ -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);
}

View File

@ -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
}
};

View File

@ -0,0 +1,2 @@
// #docregion
/// <reference path="../typings/angularjs/angular-mocks.d.ts" />

View File

@ -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');
}));
});

View File

@ -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]]);
}));
});

View File

@ -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);
});
}));
});

View File

@ -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]]);
}));
});

View File

@ -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);
});
}));
});

View File

@ -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();
}));
});

View File

@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}

View File

@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map

View File

@ -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; }
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,9 @@
// #docregion
import {Pipe} from 'angular2/core';
@Pipe({name: 'checkmark'})
export class CheckmarkPipe {
transform(input:string): string {
return input ? '\u2713' : '\u2718';
}
}

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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"
}
}

View File

@ -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/);
});
});
});

View File

@ -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);
}

View File

@ -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
};

View File

@ -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');
}));
});

View File

@ -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]]);
}));
});

View File

@ -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);
});
}));
});

View File

@ -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]]);
}));
});

View File

@ -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);
});
}));
});

View File

@ -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();
}));
});

View File

@ -0,0 +1,3 @@
{
"directory": "app/bower_components"
}

View File

@ -0,0 +1,6 @@
app/**/*.js
app/**/*.js.map
test/unit/**/*.js
test/unit/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map

View File

@ -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