docs(upgrade) add PhoneCat upgrade tutorial
This commit is contained in:
parent
ce0d1c0319
commit
472c9d98c7
|
@ -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
Loading…
Reference in New Issue