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