3378 lines
105 KiB
HTML
3378 lines
105 KiB
HTML
<html lang="en"><head></head><body><form id="mainForm" method="post" action="http://plnkr.co/edit/?p=preview" target="_self"><input type="hidden" name="files[browser-test-shim.js]" value="// BROWSER TESTING SHIM
|
|
// Keep it in-sync with what karma-test-shim does
|
|
/*global jasmine, __karma__, window*/
|
|
(function () {
|
|
|
|
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
|
|
|
|
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
|
|
// Error.stackTraceLimit = Infinity; //
|
|
|
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000;
|
|
|
|
var baseURL = document.baseURI;
|
|
baseURL = baseURL + baseURL[baseURL.length-1] ? '' : '/';
|
|
|
|
System.config({
|
|
baseURL: baseURL,
|
|
// Extend usual application package list with test folder
|
|
packages: { 'testing': { main: 'index.js', defaultExtension: 'js' } },
|
|
|
|
// Assume npm: is set in `paths` in systemjs.config
|
|
// Map the angular testing umd bundles
|
|
map: {
|
|
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
|
|
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
|
|
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
|
|
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
|
|
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
|
|
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
|
|
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
|
|
'@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
|
|
},
|
|
});
|
|
|
|
System.import('systemjs.config.js')
|
|
.then(importSystemJsExtras)
|
|
.then(initTestBed)
|
|
.then(initTesting);
|
|
|
|
/** Optional SystemJS configuration extras. Keep going w/o it */
|
|
function importSystemJsExtras(){
|
|
return System.import('systemjs.config.extras.js')
|
|
.catch(function(reason) {
|
|
console.log(
|
|
'Note: System.import could not load "systemjs.config.extras.js" where you might have added more configuration. It is an optional file so we will continue without it.'
|
|
);
|
|
console.log(reason);
|
|
});
|
|
}
|
|
|
|
function initTestBed(){
|
|
return Promise.all([
|
|
System.import('@angular/core/testing'),
|
|
System.import('@angular/platform-browser-dynamic/testing')
|
|
])
|
|
|
|
.then(function (providers) {
|
|
var coreTesting = providers[0];
|
|
var browserTesting = providers[1];
|
|
|
|
coreTesting.TestBed.initTestEnvironment(
|
|
browserTesting.BrowserDynamicTestingModule,
|
|
browserTesting.platformBrowserDynamicTesting());
|
|
})
|
|
}
|
|
|
|
// Import all spec files defined in the html (__spec_files__)
|
|
// and start Jasmine testrunner
|
|
function initTesting () {
|
|
console.log('loading spec files: '+__spec_files__.join(', '));
|
|
return Promise.all(
|
|
__spec_files__.map(function(spec) {
|
|
return System.import(spec);
|
|
})
|
|
)
|
|
// After all imports load, re-execute `window.onload` which
|
|
// triggers the Jasmine test-runner start or explain what went wrong
|
|
.then(success, console.error.bind(console));
|
|
|
|
function success () {
|
|
console.log('Spec files loaded; starting Jasmine testrunner');
|
|
window.onload();
|
|
}
|
|
}
|
|
|
|
})();
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[systemjs.config.extras.js]" value="/** App specific SystemJS configuration */
|
|
System.config({
|
|
packages: {
|
|
// barrels
|
|
'app/model': {main:'index.js', defaultExtension:'js'},
|
|
'app/model/testing': {main:'index.js', defaultExtension:'js'}
|
|
}
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[styles.css]" value="/* Master Styles */
|
|
h1 {
|
|
color: #369;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
font-size: 250%;
|
|
}
|
|
h2, h3 {
|
|
color: #444;
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
font-weight: lighter;
|
|
}
|
|
body {
|
|
margin: 2em;
|
|
}
|
|
body, input[text], button {
|
|
color: #888;
|
|
font-family: Cambria, Georgia;
|
|
}
|
|
a {
|
|
cursor: pointer;
|
|
cursor: hand;
|
|
}
|
|
button {
|
|
font-family: Arial;
|
|
background-color: #eee;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
cursor: hand;
|
|
}
|
|
button:hover {
|
|
background-color: #cfd8dc;
|
|
}
|
|
button:disabled {
|
|
background-color: #eee;
|
|
color: #aaa;
|
|
cursor: auto;
|
|
}
|
|
|
|
/* Navigation link styles */
|
|
nav a {
|
|
padding: 5px 10px;
|
|
text-decoration: none;
|
|
margin-right: 10px;
|
|
margin-top: 10px;
|
|
display: inline-block;
|
|
background-color: #eee;
|
|
border-radius: 4px;
|
|
}
|
|
nav a:visited, a:link {
|
|
color: #607D8B;
|
|
}
|
|
nav a:hover {
|
|
color: #039be5;
|
|
background-color: #CFD8DC;
|
|
}
|
|
nav a.active {
|
|
color: #039be5;
|
|
}
|
|
|
|
/* items class */
|
|
.items {
|
|
margin: 0 0 2em 0;
|
|
list-style-type: none;
|
|
padding: 0;
|
|
width: 24em;
|
|
}
|
|
.items li {
|
|
cursor: pointer;
|
|
position: relative;
|
|
left: 0;
|
|
background-color: #EEE;
|
|
margin: .5em;
|
|
padding: .3em 0;
|
|
height: 1.6em;
|
|
border-radius: 4px;
|
|
}
|
|
.items li:hover {
|
|
color: #607D8B;
|
|
background-color: #DDD;
|
|
left: .1em;
|
|
}
|
|
.items li.selected {
|
|
background-color: #CFD8DC;
|
|
color: white;
|
|
}
|
|
.items li.selected:hover {
|
|
background-color: #BBD8DC;
|
|
}
|
|
.items .text {
|
|
position: relative;
|
|
top: -3px;
|
|
}
|
|
.items .badge {
|
|
display: inline-block;
|
|
font-size: small;
|
|
color: white;
|
|
padding: 0.8em 0.7em 0 0.7em;
|
|
background-color: #607D8B;
|
|
line-height: 1em;
|
|
position: relative;
|
|
left: -1px;
|
|
top: -4px;
|
|
height: 1.8em;
|
|
margin-right: .8em;
|
|
border-radius: 4px 0 0 4px;
|
|
}
|
|
/* everywhere else */
|
|
* {
|
|
font-family: Arial, Helvetica, sans-serif;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/banner.component.css]" value="h1 { color: green; font-size: 350%}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard-hero.component.css]" value=".hero {
|
|
padding: 20px;
|
|
position: relative;
|
|
text-align: center;
|
|
color: #eee;
|
|
max-height: 120px;
|
|
min-width: 120px;
|
|
background-color: #607D8B;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.hero:hover {
|
|
background-color: #EEE;
|
|
cursor: pointer;
|
|
color: #607d8b;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.hero {
|
|
font-size: 10px;
|
|
max-height: 75px; }
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.hero {
|
|
min-width: 60px;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard.component.css]" value="[class*='col-'] {
|
|
float: left;
|
|
}
|
|
*, *:after, *:before {
|
|
-webkit-box-sizing: border-box;
|
|
-moz-box-sizing: border-box;
|
|
box-sizing: border-box;
|
|
}
|
|
h3 {
|
|
text-align: center; margin-bottom: 0;
|
|
}
|
|
[class*='col-'] {
|
|
padding-right: 20px;
|
|
padding-bottom: 20px;
|
|
}
|
|
[class*='col-']:last-of-type {
|
|
padding-right: 0;
|
|
}
|
|
.grid {
|
|
margin: 0;
|
|
}
|
|
.col-1-4 {
|
|
width: 25%;
|
|
}
|
|
.grid-pad {
|
|
padding: 10px 0;
|
|
}
|
|
.grid-pad > [class*='col-']:last-of-type {
|
|
padding-right: 20px;
|
|
}
|
|
@media (max-width: 1024px) {
|
|
.grid {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-detail.component.css]" value="label {
|
|
display: inline-block;
|
|
width: 3em;
|
|
margin: .5em 0;
|
|
color: #607D8B;
|
|
font-weight: bold;
|
|
}
|
|
input {
|
|
height: 2em;
|
|
font-size: 1em;
|
|
padding-left: .4em;
|
|
}
|
|
button {
|
|
margin-top: 20px;
|
|
font-family: Arial;
|
|
background-color: #eee;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
cursor: pointer; cursor: hand;
|
|
}
|
|
button:hover {
|
|
background-color: #cfd8dc;
|
|
}
|
|
button:disabled {
|
|
background-color: #eee;
|
|
color: #ccc;
|
|
cursor: auto;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-list.component.css]" value=".selected {
|
|
background-color: #CFD8DC !important;
|
|
color: white;
|
|
}
|
|
.heroes {
|
|
margin: 0 0 2em 0;
|
|
list-style-type: none;
|
|
padding: 0;
|
|
width: 10em;
|
|
}
|
|
.heroes li {
|
|
cursor: pointer;
|
|
position: relative;
|
|
left: 0;
|
|
background-color: #EEE;
|
|
margin: .5em;
|
|
padding: .3em 0;
|
|
height: 1.6em;
|
|
border-radius: 4px;
|
|
}
|
|
.heroes li:hover {
|
|
color: #607D8B;
|
|
background-color: #DDD;
|
|
left: .1em;
|
|
}
|
|
.heroes li.selected:hover {
|
|
background-color: #BBD8DC !important;
|
|
color: white;
|
|
}
|
|
.heroes .text {
|
|
position: relative;
|
|
top: -3px;
|
|
}
|
|
.heroes .badge {
|
|
display: inline-block;
|
|
font-size: small;
|
|
color: white;
|
|
padding: 0.8em 0.7em 0 0.7em;
|
|
background-color: #607D8B;
|
|
line-height: 1em;
|
|
position: relative;
|
|
left: -1px;
|
|
top: -4px;
|
|
height: 1.8em;
|
|
margin-right: .8em;
|
|
border-radius: 4px 0 0 4px;
|
|
}
|
|
button {
|
|
font-family: Arial;
|
|
background-color: #eee;
|
|
border: none;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
cursor: hand;
|
|
}
|
|
button:hover {
|
|
background-color: #cfd8dc;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/app.component.html]" value="<app-banner></app-banner>
|
|
<app-welcome></app-welcome>
|
|
|
|
<nav>
|
|
<a routerLink="/dashboard">Dashboard</a>
|
|
<a routerLink="/heroes">Heroes</a>
|
|
<a routerLink="/about">About</a>
|
|
</nav>
|
|
|
|
<router-outlet></router-outlet>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="files[app/banner.component.html]" value="<h1>{{title}}</h1>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="files[app/dashboard/dashboard-hero.component.html]" value="<div (click)="click()" class="hero">
|
|
{{hero.name | uppercase}}
|
|
</div>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="files[app/dashboard/dashboard.component.html]" value="<h2 highlight>{{title}}</h2>
|
|
|
|
<div class="grid grid-pad">
|
|
<dashboard-hero *ngFor="let hero of heroes" class="col-1-4"
|
|
[hero]=hero (selected)="gotoDetail($event)" >
|
|
</dashboard-hero>
|
|
</div>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="files[app/hero/hero-detail.component.html]" value="<div *ngIf="hero">
|
|
<h2><span>{{hero.name | titlecase}}</span> Details</h2>
|
|
<div>
|
|
<label>id: </label>{{hero.id}}</div>
|
|
<div>
|
|
<label for="name">name: </label>
|
|
<input id="name" [(ngModel)]="hero.name" placeholder="name" />
|
|
</div>
|
|
<button (click)="save()">Save</button>
|
|
<button (click)="cancel()">Cancel</button>
|
|
</div>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="files[app/hero/hero-list.component.html]" value="<h2 highlight="gold">My Heroes</h2>
|
|
<ul class="heroes">
|
|
<li *ngFor="let hero of heroes | async "
|
|
[class.selected]="hero === selectedHero"
|
|
(click)="onSelect(hero)">
|
|
<span class="badge">{{hero.id}}</span> {{hero.name}}
|
|
</li>
|
|
</ul>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="files[app/about.component.spec.ts]" value="import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
|
|
import { AboutComponent } from './about.component';
|
|
import { HighlightDirective } from './shared/highlight.directive';
|
|
|
|
let fixture: ComponentFixture<AboutComponent>;
|
|
|
|
describe('AboutComponent (highlightDirective)', () => {
|
|
beforeEach(() => {
|
|
fixture = TestBed.configureTestingModule({
|
|
declarations: [ AboutComponent, HighlightDirective],
|
|
schemas: [ NO_ERRORS_SCHEMA ]
|
|
})
|
|
.createComponent(AboutComponent);
|
|
fixture.detectChanges(); // initial binding
|
|
});
|
|
|
|
it('should have skyblue <h2>', () => {
|
|
const de = fixture.debugElement.query(By.css('h2'));
|
|
const bgColor = de.nativeElement.style.backgroundColor;
|
|
expect(bgColor).toBe('skyblue');
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/about.component.ts]" value="import { Component } from '@angular/core';
|
|
@Component({
|
|
template: `
|
|
<h2 highlight="skyblue">About</h2>
|
|
<twain-quote></twain-quote>
|
|
<p>All about this sample</p>`
|
|
})
|
|
export class AboutComponent { }
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/app-routing.module.ts]" value="import { NgModule } from '@angular/core';
|
|
import { RouterModule } from '@angular/router';
|
|
|
|
import { AboutComponent } from './about.component';
|
|
|
|
@NgModule({
|
|
imports: [
|
|
RouterModule.forRoot([
|
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full'},
|
|
{ path: 'about', component: AboutComponent },
|
|
{ path: 'heroes', loadChildren: 'app/hero/hero.module#HeroModule'}
|
|
])
|
|
],
|
|
exports: [ RouterModule ] // re-export the module declarations
|
|
})
|
|
export class AppRoutingModule { };
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/app.component.router.spec.ts]" value="// For more examples:
|
|
// https://github.com/angular/angular/blob/master/modules/@angular/router/test/integration.spec.ts
|
|
|
|
import { async, ComponentFixture, fakeAsync, TestBed, tick,
|
|
} from '@angular/core/testing';
|
|
|
|
import { RouterTestingModule } from '@angular/router/testing';
|
|
import { SpyLocation } from '@angular/common/testing';
|
|
|
|
import { click } from '../testing';
|
|
|
|
// r - for relatively obscure router symbols
|
|
import * as r from '@angular/router';
|
|
import { Router, RouterLinkWithHref } from '@angular/router';
|
|
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement, Type } from '@angular/core';
|
|
import { Location } from '@angular/common';
|
|
|
|
import { AppModule } from './app.module';
|
|
import { AppComponent } from './app.component';
|
|
import { AboutComponent } from './about.component';
|
|
import { DashboardHeroComponent } from './dashboard/dashboard-hero.component';
|
|
import { TwainService } from './shared/twain.service';
|
|
|
|
let comp: AppComponent;
|
|
let fixture: ComponentFixture<AppComponent>;
|
|
let page: Page;
|
|
let router: Router;
|
|
let location: SpyLocation;
|
|
|
|
describe('AppComponent & RouterTestingModule', () => {
|
|
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ AppModule, RouterTestingModule ]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
it('should navigate to "Dashboard" immediately', fakeAsync(() => {
|
|
createComponent();
|
|
expect(location.path()).toEqual('/dashboard', 'after initialNavigation()');
|
|
expectElementOf(DashboardHeroComponent);
|
|
}));
|
|
|
|
it('should navigate to "About" on click', fakeAsync(() => {
|
|
createComponent();
|
|
click(page.aboutLinkDe);
|
|
// page.aboutLinkDe.nativeElement.click(); // ok but fails in phantom
|
|
|
|
advance();
|
|
expectPathToBe('/about');
|
|
expectElementOf(AboutComponent);
|
|
|
|
page.expectEvents([
|
|
[r.NavigationStart, '/about'], [r.RoutesRecognized, '/about'],
|
|
[r.NavigationEnd, '/about']
|
|
]);
|
|
}));
|
|
|
|
it('should navigate to "About" w/ browser location URL change', fakeAsync(() => {
|
|
createComponent();
|
|
location.simulateHashChange('/about');
|
|
// location.go('/about'); // also works ... except in plunker
|
|
advance();
|
|
expectPathToBe('/about');
|
|
expectElementOf(AboutComponent);
|
|
}));
|
|
|
|
// Can't navigate to lazy loaded modules with this technique
|
|
xit('should navigate to "Heroes" on click', fakeAsync(() => {
|
|
createComponent();
|
|
page.heroesLinkDe.nativeElement.click();
|
|
advance();
|
|
expectPathToBe('/heroes');
|
|
}));
|
|
|
|
});
|
|
|
|
|
|
///////////////
|
|
import { NgModuleFactoryLoader } from '@angular/core';
|
|
import { SpyNgModuleFactoryLoader } from '@angular/router/testing';
|
|
|
|
import { HeroModule } from './hero/hero.module'; // should be lazy loaded
|
|
import { HeroListComponent } from './hero/hero-list.component';
|
|
|
|
let loader: SpyNgModuleFactoryLoader;
|
|
|
|
///////// Can't get lazy loaded Heroes to work yet
|
|
xdescribe('AppComponent & Lazy Loading', () => {
|
|
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ AppModule, RouterTestingModule ]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
beforeEach(fakeAsync(() => {
|
|
createComponent();
|
|
loader = TestBed.get(NgModuleFactoryLoader);
|
|
loader.stubbedModules = {expected: HeroModule};
|
|
router.resetConfig([{path: 'heroes', loadChildren: 'expected'}]);
|
|
}));
|
|
|
|
it('dummy', () => expect(true).toBe(true) );
|
|
|
|
|
|
it('should navigate to "Heroes" on click', async(() => {
|
|
page.heroesLinkDe.nativeElement.click();
|
|
advance();
|
|
expectPathToBe('/heroes');
|
|
expectElementOf(HeroListComponent);
|
|
}));
|
|
|
|
xit('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
|
|
location.go('/heroes');
|
|
advance();
|
|
expectPathToBe('/heroes');
|
|
expectElementOf(HeroListComponent);
|
|
|
|
page.expectEvents([
|
|
[r.NavigationStart, '/heroes'], [r.RoutesRecognized, '/heroes'],
|
|
[r.NavigationEnd, '/heroes']
|
|
]);
|
|
}));
|
|
});
|
|
|
|
////// Helpers /////////
|
|
|
|
/** Wait a tick, then detect changes */
|
|
function advance(): void {
|
|
tick();
|
|
fixture.detectChanges();
|
|
}
|
|
|
|
function createComponent() {
|
|
fixture = TestBed.createComponent(AppComponent);
|
|
comp = fixture.componentInstance;
|
|
|
|
const injector = fixture.debugElement.injector;
|
|
location = injector.get(Location);
|
|
router = injector.get(Router);
|
|
router.initialNavigation();
|
|
spyOn(injector.get(TwainService), 'getQuote')
|
|
.and.returnValue(Promise.resolve('Test Quote')); // fakes it
|
|
|
|
advance();
|
|
|
|
page = new Page();
|
|
}
|
|
|
|
class Page {
|
|
aboutLinkDe: DebugElement;
|
|
dashboardLinkDe: DebugElement;
|
|
heroesLinkDe: DebugElement;
|
|
recordedEvents: any[] = [];
|
|
|
|
// for debugging
|
|
comp: AppComponent;
|
|
location: SpyLocation;
|
|
router: Router;
|
|
fixture: ComponentFixture<AppComponent>;
|
|
|
|
expectEvents(pairs: any[]) {
|
|
const events = this.recordedEvents;
|
|
expect(events.length).toEqual(pairs.length, 'actual/expected events length mismatch');
|
|
for (let i = 0; i < events.length; ++i) {
|
|
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name, 'unexpected event name');
|
|
expect((<any>events[i]).url).toBe(pairs[i][1], 'unexpected event url');
|
|
}
|
|
}
|
|
|
|
constructor() {
|
|
router.events.forEach(e => this.recordedEvents.push(e));
|
|
const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
|
|
this.aboutLinkDe = links[2];
|
|
this.dashboardLinkDe = links[0];
|
|
this.heroesLinkDe = links[1];
|
|
|
|
// for debugging
|
|
this.comp = comp;
|
|
this.fixture = fixture;
|
|
this.router = router;
|
|
}
|
|
}
|
|
|
|
function expectPathToBe(path: string, expectationFailOutput?: any) {
|
|
expect(location.path()).toEqual(path, expectationFailOutput || 'location.path()');
|
|
}
|
|
|
|
function expectElementOf(type: Type<any>): any {
|
|
const el = fixture.debugElement.query(By.directive(type));
|
|
expect(el).toBeTruthy('expected an element for ' + type.name);
|
|
return el;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/app.component.spec.ts]" value="import { async, ComponentFixture, TestBed
|
|
} from '@angular/core/testing';
|
|
|
|
import { DebugElement } from '@angular/core';
|
|
import { By } from '@angular/platform-browser';
|
|
|
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
import { Component } from '@angular/core';
|
|
import { AppComponent } from './app.component';
|
|
import { BannerComponent } from './banner.component';
|
|
import { RouterLinkStubDirective } from '../testing';
|
|
import { RouterOutletStubComponent } from '../testing';
|
|
|
|
@Component({selector: 'app-welcome', template: ''})
|
|
class WelcomeStubComponent {}
|
|
|
|
|
|
let comp: AppComponent;
|
|
let fixture: ComponentFixture<AppComponent>;
|
|
|
|
describe('AppComponent & TestModule', () => {
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [
|
|
AppComponent,
|
|
BannerComponent, WelcomeStubComponent,
|
|
RouterLinkStubDirective, RouterOutletStubComponent
|
|
]
|
|
})
|
|
|
|
.compileComponents()
|
|
.then(() => {
|
|
fixture = TestBed.createComponent(AppComponent);
|
|
comp = fixture.componentInstance;
|
|
});
|
|
}));
|
|
tests();
|
|
});
|
|
|
|
//////// Testing w/ NO_ERRORS_SCHEMA //////
|
|
describe('AppComponent & NO_ERRORS_SCHEMA', () => {
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ AppComponent, RouterLinkStubDirective ],
|
|
schemas: [ NO_ERRORS_SCHEMA ]
|
|
})
|
|
|
|
.compileComponents()
|
|
.then(() => {
|
|
fixture = TestBed.createComponent(AppComponent);
|
|
comp = fixture.componentInstance;
|
|
});
|
|
}));
|
|
tests();
|
|
});
|
|
|
|
//////// Testing w/ real root module //////
|
|
// Tricky because we are disabling the router and its configuration
|
|
// Better to use RouterTestingModule
|
|
import { AppModule } from './app.module';
|
|
import { AppRoutingModule } from './app-routing.module';
|
|
|
|
describe('AppComponent & AppModule', () => {
|
|
|
|
beforeEach( async(() => {
|
|
|
|
TestBed.configureTestingModule({
|
|
imports: [ AppModule ]
|
|
})
|
|
|
|
// Get rid of app's Router configuration otherwise many failures.
|
|
// Doing so removes Router declarations; add the Router stubs
|
|
.overrideModule(AppModule, {
|
|
remove: {
|
|
imports: [ AppRoutingModule ]
|
|
},
|
|
add: {
|
|
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ]
|
|
}
|
|
})
|
|
|
|
.compileComponents()
|
|
|
|
.then(() => {
|
|
fixture = TestBed.createComponent(AppComponent);
|
|
comp = fixture.componentInstance;
|
|
});
|
|
}));
|
|
|
|
tests();
|
|
});
|
|
|
|
function tests() {
|
|
let links: RouterLinkStubDirective[];
|
|
let linkDes: DebugElement[];
|
|
|
|
beforeEach(() => {
|
|
// trigger initial data binding
|
|
fixture.detectChanges();
|
|
|
|
// find DebugElements with an attached RouterLinkStubDirective
|
|
linkDes = fixture.debugElement
|
|
.queryAll(By.directive(RouterLinkStubDirective));
|
|
|
|
// get the attached link directive instances using the DebugElement injectors
|
|
links = linkDes
|
|
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
|
|
});
|
|
|
|
it('can instantiate it', () => {
|
|
expect(comp).not.toBeNull();
|
|
});
|
|
|
|
it('can get RouterLinks from template', () => {
|
|
expect(links.length).toBe(3, 'should have 3 links');
|
|
expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard');
|
|
expect(links[1].linkParams).toBe('/heroes', '1st link should go to Heroes');
|
|
});
|
|
|
|
it('can click Heroes link in template', () => {
|
|
const heroesLinkDe = linkDes[1];
|
|
const heroesLink = links[1];
|
|
|
|
expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet');
|
|
|
|
heroesLinkDe.triggerEventHandler('click', null);
|
|
fixture.detectChanges();
|
|
|
|
expect(heroesLink.navigatedTo).toBe('/heroes');
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/app.component.ts]" value="import { Component } from '@angular/core';
|
|
@Component({
|
|
moduleId: module.id,
|
|
selector: 'my-app',
|
|
templateUrl: './app.component.html'
|
|
})
|
|
export class AppComponent { }
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/app.module.ts]" value="import { NgModule } from '@angular/core';
|
|
import { BrowserModule } from '@angular/platform-browser';
|
|
|
|
import { AppComponent } from './app.component';
|
|
import { AppRoutingModule } from './app-routing.module';
|
|
|
|
import { AboutComponent } from './about.component';
|
|
import { BannerComponent } from './banner.component';
|
|
import { HeroService,
|
|
UserService } from './model';
|
|
import { TwainService } from './shared/twain.service';
|
|
import { WelcomeComponent } from './welcome.component';
|
|
|
|
|
|
import { DashboardModule } from './dashboard/dashboard.module';
|
|
import { SharedModule } from './shared/shared.module';
|
|
|
|
@NgModule({
|
|
imports: [
|
|
BrowserModule,
|
|
DashboardModule,
|
|
AppRoutingModule,
|
|
SharedModule
|
|
],
|
|
providers: [ HeroService, TwainService, UserService ],
|
|
declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ],
|
|
bootstrap: [ AppComponent ]
|
|
})
|
|
export class AppModule { }
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/banner-inline.component.spec.ts]" value="import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import { BannerComponent } from './banner-inline.component';
|
|
|
|
describe('BannerComponent (inline template)', () => {
|
|
|
|
let comp: BannerComponent;
|
|
let fixture: ComponentFixture<BannerComponent>;
|
|
let de: DebugElement;
|
|
let el: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ BannerComponent ], // declare the test component
|
|
});
|
|
|
|
fixture = TestBed.createComponent(BannerComponent);
|
|
|
|
comp = fixture.componentInstance; // BannerComponent test instance
|
|
|
|
// query for the title <h1> by CSS element selector
|
|
de = fixture.debugElement.query(By.css('h1'));
|
|
el = de.nativeElement;
|
|
});
|
|
|
|
it('no title in the DOM until manually call `detectChanges`', () => {
|
|
expect(el.textContent).toEqual('');
|
|
});
|
|
|
|
it('should display original title', () => {
|
|
fixture.detectChanges();
|
|
expect(el.textContent).toContain(comp.title);
|
|
});
|
|
|
|
it('should display a different test title', () => {
|
|
comp.title = 'Test Title';
|
|
fixture.detectChanges();
|
|
expect(el.textContent).toContain('Test Title');
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/banner-inline.component.ts]" value="import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
selector: 'app-banner',
|
|
template: '<h1>{{title}}</h1>'
|
|
})
|
|
export class BannerComponent {
|
|
title = 'Test Tour of Heroes';
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/banner.component.detect-changes.spec.ts]" value="import { async } from '@angular/core/testing';
|
|
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
|
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import { BannerComponent } from './banner.component';
|
|
|
|
describe('BannerComponent (AutoChangeDetect)', () => {
|
|
let comp: BannerComponent;
|
|
let fixture: ComponentFixture<BannerComponent>;
|
|
let de: DebugElement;
|
|
let el: HTMLElement;
|
|
|
|
beforeEach(async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ BannerComponent ],
|
|
providers: [
|
|
{ provide: ComponentFixtureAutoDetect, useValue: true }
|
|
]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(BannerComponent);
|
|
comp = fixture.componentInstance;
|
|
de = fixture.debugElement.query(By.css('h1'));
|
|
el = de.nativeElement;
|
|
});
|
|
|
|
it('should display original title', () => {
|
|
// Hooray! No `fixture.detectChanges()` needed
|
|
expect(el.textContent).toContain(comp.title);
|
|
});
|
|
|
|
it('should still see original title after comp.title change', () => {
|
|
const oldTitle = comp.title;
|
|
comp.title = 'Test Title';
|
|
// Displayed title is old because Angular didn't hear the change :(
|
|
expect(el.textContent).toContain(oldTitle);
|
|
});
|
|
|
|
it('should display updated title after detectChanges', () => {
|
|
comp.title = 'Test Title';
|
|
fixture.detectChanges(); // detect changes explicitly
|
|
expect(el.textContent).toContain(comp.title);
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/banner.component.spec.ts]" value="import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import { BannerComponent } from './banner.component';
|
|
|
|
describe('BannerComponent (templateUrl)', () => {
|
|
|
|
let comp: BannerComponent;
|
|
let fixture: ComponentFixture<BannerComponent>;
|
|
let de: DebugElement;
|
|
let el: HTMLElement;
|
|
|
|
// async beforeEach
|
|
beforeEach(async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ BannerComponent ], // declare the test component
|
|
})
|
|
.compileComponents(); // compile template and css
|
|
}));
|
|
|
|
// synchronous beforeEach
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(BannerComponent);
|
|
|
|
comp = fixture.componentInstance; // BannerComponent test instance
|
|
|
|
// query for the title <h1> by CSS element selector
|
|
de = fixture.debugElement.query(By.css('h1'));
|
|
el = de.nativeElement;
|
|
});
|
|
|
|
it('no title in the DOM until manually call `detectChanges`', () => {
|
|
expect(el.textContent).toEqual('');
|
|
});
|
|
|
|
it('should display original title', () => {
|
|
fixture.detectChanges();
|
|
expect(el.textContent).toContain(comp.title);
|
|
});
|
|
|
|
it('should display a different test title', () => {
|
|
comp.title = 'Test Title';
|
|
fixture.detectChanges();
|
|
expect(el.textContent).toContain('Test Title');
|
|
});
|
|
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/banner.component.ts]" value="import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
moduleId: module.id,
|
|
selector: 'app-banner',
|
|
templateUrl: './banner.component.html',
|
|
styleUrls: ['./banner.component.css']
|
|
})
|
|
export class BannerComponent {
|
|
title = 'Test Tour of Heroes';
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard-hero.component.spec.ts]" value="import { async, ComponentFixture, TestBed
|
|
} from '@angular/core/testing';
|
|
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import { addMatchers, click } from '../../testing';
|
|
|
|
import { Hero } from '../model/hero';
|
|
import { DashboardHeroComponent } from './dashboard-hero.component';
|
|
|
|
beforeEach( addMatchers );
|
|
|
|
describe('DashboardHeroComponent when tested directly', () => {
|
|
|
|
let comp: DashboardHeroComponent;
|
|
let expectedHero: Hero;
|
|
let fixture: ComponentFixture<DashboardHeroComponent>;
|
|
let heroEl: DebugElement;
|
|
|
|
// async beforeEach
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ DashboardHeroComponent ],
|
|
})
|
|
.compileComponents(); // compile template and css
|
|
}));
|
|
|
|
// synchronous beforeEach
|
|
beforeEach(() => {
|
|
fixture = TestBed.createComponent(DashboardHeroComponent);
|
|
comp = fixture.componentInstance;
|
|
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element
|
|
|
|
// pretend that it was wired to something that supplied a hero
|
|
expectedHero = new Hero(42, 'Test Name');
|
|
comp.hero = expectedHero;
|
|
fixture.detectChanges(); // trigger initial data binding
|
|
});
|
|
|
|
it('should display hero name', () => {
|
|
const expectedPipedName = expectedHero.name.toUpperCase();
|
|
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
|
|
});
|
|
|
|
it('should raise selected event when clicked', () => {
|
|
let selectedHero: Hero;
|
|
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
|
|
|
|
heroEl.triggerEventHandler('click', null);
|
|
expect(selectedHero).toBe(expectedHero);
|
|
});
|
|
|
|
it('should raise selected event when clicked', () => {
|
|
let selectedHero: Hero;
|
|
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
|
|
|
|
click(heroEl); // triggerEventHandler helper
|
|
expect(selectedHero).toBe(expectedHero);
|
|
});
|
|
});
|
|
|
|
//////////////////
|
|
|
|
describe('DashboardHeroComponent when inside a test host', () => {
|
|
let testHost: TestHostComponent;
|
|
let fixture: ComponentFixture<TestHostComponent>;
|
|
let heroEl: DebugElement;
|
|
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
|
|
}).compileComponents();
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
// create TestHostComponent instead of DashboardHeroComponent
|
|
fixture = TestBed.createComponent(TestHostComponent);
|
|
testHost = fixture.componentInstance;
|
|
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
|
|
fixture.detectChanges(); // trigger initial data binding
|
|
});
|
|
|
|
it('should display hero name', () => {
|
|
const expectedPipedName = testHost.hero.name.toUpperCase();
|
|
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
|
|
});
|
|
|
|
it('should raise selected event when clicked', () => {
|
|
click(heroEl);
|
|
// selected hero should be the same data bound hero
|
|
expect(testHost.selectedHero).toBe(testHost.hero);
|
|
});
|
|
});
|
|
|
|
////// Test Host Component //////
|
|
import { Component } from '@angular/core';
|
|
|
|
@Component({
|
|
template: `
|
|
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
|
|
})
|
|
class TestHostComponent {
|
|
hero = new Hero(42, 'Test Name');
|
|
selectedHero: Hero;
|
|
onSelected(hero: Hero) { this.selectedHero = hero; }
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard-hero.component.ts]" value="import { Component, EventEmitter, Input, Output } from '@angular/core';
|
|
|
|
import { Hero } from '../model';
|
|
|
|
@Component({
|
|
moduleId: module.id,
|
|
selector: 'dashboard-hero',
|
|
templateUrl: './dashboard-hero.component.html',
|
|
styleUrls: [ './dashboard-hero.component.css' ]
|
|
})
|
|
export class DashboardHeroComponent {
|
|
@Input() hero: Hero;
|
|
@Output() selected = new EventEmitter<Hero>();
|
|
click() { this.selected.emit(this.hero); }
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard.component.no-testbed.spec.ts]" value="import { Router } from '@angular/router';
|
|
|
|
import { DashboardComponent } from './dashboard.component';
|
|
import { Hero } from '../model';
|
|
|
|
import { addMatchers } from '../../testing';
|
|
import { FakeHeroService } from '../model/testing';
|
|
|
|
class FakeRouter {
|
|
navigateByUrl(url: string) { return url; }
|
|
}
|
|
|
|
describe('DashboardComponent: w/o Angular TestBed', () => {
|
|
let comp: DashboardComponent;
|
|
let heroService: FakeHeroService;
|
|
let router: Router;
|
|
|
|
beforeEach(() => {
|
|
addMatchers();
|
|
router = new FakeRouter() as any as Router;
|
|
heroService = new FakeHeroService();
|
|
comp = new DashboardComponent(router, heroService);
|
|
});
|
|
|
|
it('should NOT have heroes before calling OnInit', () => {
|
|
expect(comp.heroes.length).toBe(0,
|
|
'should not have heroes before OnInit');
|
|
});
|
|
|
|
it('should NOT have heroes immediately after OnInit', () => {
|
|
comp.ngOnInit(); // ngOnInit -> getHeroes
|
|
expect(comp.heroes.length).toBe(0,
|
|
'should not have heroes until service promise resolves');
|
|
});
|
|
|
|
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
|
|
comp.ngOnInit(); // ngOnInit -> getHeroes
|
|
heroService.lastPromise // the one from getHeroes
|
|
.then(() => {
|
|
// throw new Error('deliberate error'); // see it fail gracefully
|
|
expect(comp.heroes.length).toBeGreaterThan(0,
|
|
'should have heroes after service promise resolves');
|
|
})
|
|
.then(done, done.fail);
|
|
});
|
|
|
|
it('should tell ROUTER to navigate by hero id', () => {
|
|
const hero = new Hero(42, 'Abbracadabra');
|
|
const spy = spyOn(router, 'navigateByUrl');
|
|
|
|
comp.gotoDetail(hero);
|
|
|
|
const navArgs = spy.calls.mostRecent().args[0];
|
|
expect(navArgs).toBe('/heroes/42', 'should nav to HeroDetail for Hero 42');
|
|
});
|
|
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard.component.spec.ts]" value="import { async, inject, ComponentFixture, TestBed
|
|
} from '@angular/core/testing';
|
|
|
|
import { addMatchers, click } from '../../testing';
|
|
import { HeroService } from '../model';
|
|
import { FakeHeroService } from '../model/testing';
|
|
|
|
import { By } from '@angular/platform-browser';
|
|
import { Router } from '@angular/router';
|
|
|
|
import { DashboardComponent } from './dashboard.component';
|
|
import { DashboardModule } from './dashboard.module';
|
|
|
|
class RouterStub {
|
|
navigateByUrl(url: string) { return url; }
|
|
}
|
|
|
|
beforeEach ( addMatchers );
|
|
|
|
let comp: DashboardComponent;
|
|
let fixture: ComponentFixture<DashboardComponent>;
|
|
|
|
//////// Deep ////////////////
|
|
|
|
describe('DashboardComponent (deep)', () => {
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ DashboardModule ]
|
|
});
|
|
});
|
|
|
|
compileAndCreate();
|
|
|
|
tests(clickForDeep);
|
|
|
|
function clickForDeep() {
|
|
// get first <div class="hero"> DebugElement
|
|
const heroEl = fixture.debugElement.query(By.css('.hero'));
|
|
click(heroEl);
|
|
}
|
|
});
|
|
|
|
//////// Shallow ////////////////
|
|
|
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
|
|
describe('DashboardComponent (shallow)', () => {
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ DashboardComponent ],
|
|
schemas: [NO_ERRORS_SCHEMA]
|
|
});
|
|
});
|
|
|
|
compileAndCreate();
|
|
|
|
tests(clickForShallow);
|
|
|
|
function clickForShallow() {
|
|
// get first <dashboard-hero> DebugElement
|
|
const heroEl = fixture.debugElement.query(By.css('dashboard-hero'));
|
|
heroEl.triggerEventHandler('selected', comp.heroes[0]);
|
|
}
|
|
});
|
|
|
|
/** Add TestBed providers, compile, and create DashboardComponent */
|
|
function compileAndCreate() {
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
providers: [
|
|
{ provide: HeroService, useClass: FakeHeroService },
|
|
{ provide: Router, useClass: RouterStub }
|
|
]
|
|
})
|
|
.compileComponents().then(() => {
|
|
fixture = TestBed.createComponent(DashboardComponent);
|
|
comp = fixture.componentInstance;
|
|
});
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* The (almost) same tests for both.
|
|
* Only change: the way that the first hero is clicked
|
|
*/
|
|
function tests(heroClick: Function) {
|
|
|
|
it('should NOT have heroes before ngOnInit', () => {
|
|
expect(comp.heroes.length).toBe(0,
|
|
'should not have heroes before ngOnInit');
|
|
});
|
|
|
|
it('should NOT have heroes immediately after ngOnInit', () => {
|
|
fixture.detectChanges(); // runs initial lifecycle hooks
|
|
|
|
expect(comp.heroes.length).toBe(0,
|
|
'should not have heroes until service promise resolves');
|
|
});
|
|
|
|
describe('after get dashboard heroes', () => {
|
|
|
|
// Trigger component so it gets heroes and binds to them
|
|
beforeEach( async(() => {
|
|
fixture.detectChanges(); // runs ngOnInit -> getHeroes
|
|
fixture.whenStable() // No need for the `lastPromise` hack!
|
|
.then(() => fixture.detectChanges()); // bind to heroes
|
|
}));
|
|
|
|
it('should HAVE heroes', () => {
|
|
expect(comp.heroes.length).toBeGreaterThan(0,
|
|
'should have heroes after service promise resolves');
|
|
});
|
|
|
|
it('should DISPLAY heroes', () => {
|
|
// Find and examine the displayed heroes
|
|
// Look for them in the DOM by css class
|
|
const heroes = fixture.debugElement.queryAll(By.css('dashboard-hero'));
|
|
expect(heroes.length).toBe(4, 'should display 4 heroes');
|
|
});
|
|
|
|
it('should tell ROUTER to navigate when hero clicked',
|
|
inject([Router], (router: Router) => { // ...
|
|
|
|
const spy = spyOn(router, 'navigateByUrl');
|
|
|
|
heroClick(); // trigger click on first inner <div class="hero">
|
|
|
|
// args passed to router.navigateByUrl()
|
|
const navArgs = spy.calls.first().args[0];
|
|
|
|
// expecting to navigate to id of the component's first hero
|
|
const id = comp.heroes[0].id;
|
|
expect(navArgs).toBe('/heroes/' + id,
|
|
'should nav to HeroDetail for first hero');
|
|
}));
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard.component.ts]" value="import { Component, OnInit } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
|
|
import { Hero, HeroService } from '../model';
|
|
|
|
@Component({
|
|
moduleId: module.id,
|
|
selector: 'app-dashboard',
|
|
templateUrl: './dashboard.component.html',
|
|
styleUrls: [ './dashboard.component.css' ]
|
|
})
|
|
export class DashboardComponent implements OnInit {
|
|
|
|
heroes: Hero[] = [];
|
|
|
|
constructor(
|
|
private router: Router,
|
|
private heroService: HeroService) {
|
|
}
|
|
|
|
ngOnInit() {
|
|
this.heroService.getHeroes()
|
|
.then(heroes => this.heroes = heroes.slice(1, 5));
|
|
}
|
|
|
|
gotoDetail(hero: Hero) {
|
|
let url = `/heroes/${hero.id}`;
|
|
this.router.navigateByUrl(url);
|
|
}
|
|
|
|
get title() {
|
|
let cnt = this.heroes.length;
|
|
return cnt === 0 ? 'No Heroes' :
|
|
cnt === 1 ? 'Top Hero' : `Top ${cnt} Heroes`;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/dashboard/dashboard.module.ts]" value="import { NgModule } from '@angular/core';
|
|
import { RouterModule, Routes } from '@angular/router';
|
|
|
|
import { SharedModule } from '../shared/shared.module';
|
|
|
|
import { DashboardComponent } from './dashboard.component';
|
|
import { DashboardHeroComponent } from './dashboard-hero.component';
|
|
|
|
const routes: Routes = [
|
|
{ path: 'dashboard', component: DashboardComponent },
|
|
];
|
|
|
|
@NgModule({
|
|
imports: [
|
|
SharedModule,
|
|
RouterModule.forChild(routes)
|
|
],
|
|
declarations: [ DashboardComponent, DashboardHeroComponent ]
|
|
})
|
|
export class DashboardModule { }
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-detail.component.no-testbed.spec.ts]" value="import { HeroDetailComponent } from './hero-detail.component';
|
|
import { Hero } from '../model';
|
|
|
|
import { ActivatedRouteStub } from '../../testing';
|
|
|
|
////////// Tests ////////////////////
|
|
|
|
describe('HeroDetailComponent - no TestBed', () => {
|
|
let activatedRoute: ActivatedRouteStub;
|
|
let comp: HeroDetailComponent;
|
|
let expectedHero: Hero;
|
|
let hds: any;
|
|
let router: any;
|
|
|
|
beforeEach( done => {
|
|
expectedHero = new Hero(42, 'Bubba');
|
|
activatedRoute = new ActivatedRouteStub();
|
|
activatedRoute.testParams = { id: expectedHero.id };
|
|
|
|
router = jasmine.createSpyObj('router', ['navigate']);
|
|
|
|
hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']);
|
|
hds.getHero.and.returnValue(Promise.resolve(expectedHero));
|
|
hds.saveHero.and.returnValue(Promise.resolve(expectedHero));
|
|
|
|
comp = new HeroDetailComponent(hds, <any> activatedRoute, router);
|
|
comp.ngOnInit();
|
|
|
|
// OnInit calls HDS.getHero; wait for it to get the fake hero
|
|
hds.getHero.calls.first().returnValue.then(done);
|
|
});
|
|
|
|
it('should expose the hero retrieved from the service', () => {
|
|
expect(comp.hero).toBe(expectedHero);
|
|
});
|
|
|
|
it('should navigate when click cancel', () => {
|
|
comp.cancel();
|
|
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
|
|
});
|
|
|
|
it('should save when click save', () => {
|
|
comp.save();
|
|
expect(hds.saveHero.calls.any()).toBe(true, 'HeroDetailService.save called');
|
|
expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet');
|
|
});
|
|
|
|
it('should navigate when click save resolves', done => {
|
|
comp.save();
|
|
// waits for async save to complete before navigating
|
|
hds.saveHero.calls.first().returnValue
|
|
.then(() => {
|
|
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
|
|
done();
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-detail.component.spec.ts]" value="import {
|
|
async, ComponentFixture, fakeAsync, inject, TestBed, tick
|
|
} from '@angular/core/testing';
|
|
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import {
|
|
ActivatedRoute, ActivatedRouteStub, click, newEvent, Router, RouterStub
|
|
} from '../../testing';
|
|
|
|
import { Hero } from '../model';
|
|
import { HeroDetailComponent } from './hero-detail.component';
|
|
import { HeroDetailService } from './hero-detail.service';
|
|
import { HeroModule } from './hero.module';
|
|
|
|
////// Testing Vars //////
|
|
let activatedRoute: ActivatedRouteStub;
|
|
let comp: HeroDetailComponent;
|
|
let fixture: ComponentFixture<HeroDetailComponent>;
|
|
let page: Page;
|
|
|
|
////// Tests //////
|
|
describe('HeroDetailComponent', () => {
|
|
beforeEach(() => {
|
|
activatedRoute = new ActivatedRouteStub();
|
|
});
|
|
describe('with HeroModule setup', heroModuleSetup);
|
|
describe('when override its provided HeroDetailService', overrideSetup);
|
|
describe('with FormsModule setup', formsModuleSetup);
|
|
describe('with SharedModule setup', sharedModuleSetup);
|
|
});
|
|
|
|
////////////////////
|
|
function overrideSetup() {
|
|
class HeroDetailServiceSpy {
|
|
testHero = new Hero(42, 'Test Hero');
|
|
|
|
getHero = jasmine.createSpy('getHero').and.callFake(
|
|
() => Promise
|
|
.resolve(true)
|
|
.then(() => Object.assign({}, this.testHero))
|
|
);
|
|
|
|
saveHero = jasmine.createSpy('saveHero').and.callFake(
|
|
(hero: Hero) => Promise
|
|
.resolve(true)
|
|
.then(() => Object.assign(this.testHero, hero))
|
|
);
|
|
}
|
|
|
|
// the `id` value is irrelevant because ignored by service stub
|
|
beforeEach(() => activatedRoute.testParams = { id: 99999 } );
|
|
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ HeroModule ],
|
|
providers: [
|
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
|
{ provide: Router, useClass: RouterStub},
|
|
// HeroDetailService at this level is IRRELEVANT!
|
|
{ provide: HeroDetailService, useValue: {} }
|
|
]
|
|
})
|
|
|
|
// Override component's own provider
|
|
.overrideComponent(HeroDetailComponent, {
|
|
set: {
|
|
providers: [
|
|
{ provide: HeroDetailService, useClass: HeroDetailServiceSpy }
|
|
]
|
|
}
|
|
})
|
|
|
|
.compileComponents();
|
|
}));
|
|
|
|
let hdsSpy: HeroDetailServiceSpy;
|
|
|
|
beforeEach( async(() => {
|
|
createComponent();
|
|
// get the component's injected HeroDetailServiceSpy
|
|
hdsSpy = fixture.debugElement.injector.get(HeroDetailService);
|
|
}));
|
|
|
|
it('should have called `getHero`', () => {
|
|
expect(hdsSpy.getHero.calls.count()).toBe(1, 'getHero called once');
|
|
});
|
|
|
|
it('should display stub hero\'s name', () => {
|
|
expect(page.nameDisplay.textContent).toBe(hdsSpy.testHero.name);
|
|
});
|
|
|
|
it('should save stub hero change', fakeAsync(() => {
|
|
const origName = hdsSpy.testHero.name;
|
|
const newName = 'New Name';
|
|
|
|
page.nameInput.value = newName;
|
|
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
|
|
|
|
expect(comp.hero.name).toBe(newName, 'component hero has new name');
|
|
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
|
|
|
|
click(page.saveBtn);
|
|
expect(hdsSpy.saveHero.calls.count()).toBe(1, 'saveHero called once');
|
|
|
|
tick(); // wait for async save to complete
|
|
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
|
|
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
|
}));
|
|
|
|
it('fixture injected service is not the component injected service',
|
|
inject([HeroDetailService], (service: HeroDetailService) => {
|
|
|
|
expect(service).toEqual({}, 'service injected from fixture');
|
|
expect(hdsSpy).toBeTruthy('service injected into component');
|
|
}));
|
|
}
|
|
|
|
////////////////////
|
|
import { HEROES, FakeHeroService } from '../model/testing';
|
|
import { HeroService } from '../model';
|
|
|
|
const firstHero = HEROES[0];
|
|
|
|
function heroModuleSetup() {
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ HeroModule ],
|
|
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
|
|
providers: [
|
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
|
{ provide: HeroService, useClass: FakeHeroService },
|
|
{ provide: Router, useClass: RouterStub},
|
|
]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
describe('when navigate to existing hero', () => {
|
|
let expectedHero: Hero;
|
|
|
|
beforeEach( async(() => {
|
|
expectedHero = firstHero;
|
|
activatedRoute.testParams = { id: expectedHero.id };
|
|
createComponent();
|
|
}));
|
|
|
|
it('should display that hero\'s name', () => {
|
|
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
|
});
|
|
|
|
it('should navigate when click cancel', () => {
|
|
click(page.cancelBtn);
|
|
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
|
});
|
|
|
|
it('should save when click save but not navigate immediately', () => {
|
|
// Get service injected into component and spy on its`saveHero` method.
|
|
// It delegates to fake `HeroService.updateHero` which delivers a safe test result.
|
|
const hds = fixture.debugElement.injector.get(HeroDetailService);
|
|
const saveSpy = spyOn(hds, 'saveHero').and.callThrough();
|
|
|
|
click(page.saveBtn);
|
|
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
|
|
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called');
|
|
});
|
|
|
|
it('should navigate when click save and save resolves', fakeAsync(() => {
|
|
click(page.saveBtn);
|
|
tick(); // wait for async save to complete
|
|
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
|
}));
|
|
|
|
it('should convert hero name to Title Case', () => {
|
|
const inputName = 'quick BROWN fox';
|
|
const titleCaseName = 'Quick Brown Fox';
|
|
|
|
// simulate user entering new name into the input box
|
|
page.nameInput.value = inputName;
|
|
|
|
// dispatch a DOM event so that Angular learns of input value change.
|
|
page.nameInput.dispatchEvent(newEvent('input'));
|
|
|
|
// Tell Angular to update the output span through the title pipe
|
|
fixture.detectChanges();
|
|
|
|
expect(page.nameDisplay.textContent).toBe(titleCaseName);
|
|
});
|
|
});
|
|
|
|
describe('when navigate with no hero id', () => {
|
|
beforeEach( async( createComponent ));
|
|
|
|
it('should have hero.id === 0', () => {
|
|
expect(comp.hero.id).toBe(0);
|
|
});
|
|
|
|
it('should display empty hero name', () => {
|
|
expect(page.nameDisplay.textContent).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('when navigate to non-existant hero id', () => {
|
|
beforeEach( async(() => {
|
|
activatedRoute.testParams = { id: 99999 };
|
|
createComponent();
|
|
}));
|
|
|
|
it('should try to navigate back to hero list', () => {
|
|
expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called');
|
|
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
|
|
});
|
|
});
|
|
|
|
// Why we must use `fixture.debugElement.injector` in `Page()`
|
|
it('cannot use `inject` to get component\'s provided HeroDetailService', () => {
|
|
let service: HeroDetailService;
|
|
fixture = TestBed.createComponent(HeroDetailComponent);
|
|
expect(
|
|
// Throws because `inject` only has access to TestBed's injector
|
|
// which is an ancestor of the component's injector
|
|
inject([HeroDetailService], (hds: HeroDetailService) => service = hds )
|
|
)
|
|
.toThrowError(/No provider for HeroDetailService/);
|
|
|
|
// get `HeroDetailService` with component's own injector
|
|
service = fixture.debugElement.injector.get(HeroDetailService);
|
|
expect(service).toBeDefined('debugElement.injector');
|
|
});
|
|
}
|
|
|
|
/////////////////////
|
|
import { FormsModule } from '@angular/forms';
|
|
import { TitleCasePipe } from '../shared/title-case.pipe';
|
|
|
|
function formsModuleSetup() {
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ FormsModule ],
|
|
declarations: [ HeroDetailComponent, TitleCasePipe ],
|
|
providers: [
|
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
|
{ provide: HeroService, useClass: FakeHeroService },
|
|
{ provide: Router, useClass: RouterStub},
|
|
]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
it('should display 1st hero\'s name', fakeAsync(() => {
|
|
const expectedHero = firstHero;
|
|
activatedRoute.testParams = { id: expectedHero.id };
|
|
createComponent().then(() => {
|
|
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
|
});
|
|
}));
|
|
}
|
|
|
|
///////////////////////
|
|
import { SharedModule } from '../shared/shared.module';
|
|
|
|
function sharedModuleSetup() {
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ SharedModule ],
|
|
declarations: [ HeroDetailComponent ],
|
|
providers: [
|
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
|
{ provide: HeroService, useClass: FakeHeroService },
|
|
{ provide: Router, useClass: RouterStub},
|
|
]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
it('should display 1st hero\'s name', fakeAsync(() => {
|
|
const expectedHero = firstHero;
|
|
activatedRoute.testParams = { id: expectedHero.id };
|
|
createComponent().then(() => {
|
|
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
|
|
});
|
|
}));
|
|
}
|
|
|
|
/////////// Helpers /////
|
|
|
|
/** Create the HeroDetailComponent, initialize it, set test variables */
|
|
function createComponent() {
|
|
fixture = TestBed.createComponent(HeroDetailComponent);
|
|
comp = fixture.componentInstance;
|
|
page = new Page();
|
|
|
|
// 1st change detection triggers ngOnInit which gets a hero
|
|
fixture.detectChanges();
|
|
return fixture.whenStable().then(() => {
|
|
// 2nd change detection displays the async-fetched hero
|
|
fixture.detectChanges();
|
|
page.addPageElements();
|
|
});
|
|
}
|
|
|
|
class Page {
|
|
gotoSpy: jasmine.Spy;
|
|
navSpy: jasmine.Spy;
|
|
|
|
saveBtn: DebugElement;
|
|
cancelBtn: DebugElement;
|
|
nameDisplay: HTMLElement;
|
|
nameInput: HTMLInputElement;
|
|
|
|
constructor() {
|
|
const router = TestBed.get(Router); // get router from root injector
|
|
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
|
|
this.navSpy = spyOn(router, 'navigate');
|
|
}
|
|
|
|
/** Add page elements after hero arrives */
|
|
addPageElements() {
|
|
if (comp.hero) {
|
|
// have a hero so these elements are now in the DOM
|
|
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
|
this.saveBtn = buttons[0];
|
|
this.cancelBtn = buttons[1];
|
|
this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement;
|
|
this.nameInput = fixture.debugElement.query(By.css('input')).nativeElement;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-detail.component.ts]" value="/* tslint:disable:member-ordering */
|
|
import { Component, Input, OnInit } from '@angular/core';
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
import 'rxjs/add/operator/map';
|
|
|
|
import { Hero } from '../model';
|
|
import { HeroDetailService } from './hero-detail.service';
|
|
|
|
@Component({
|
|
moduleId: module.id,
|
|
selector: 'app-hero-detail',
|
|
templateUrl: './hero-detail.component.html',
|
|
styleUrls: ['./hero-detail.component.css' ],
|
|
providers: [ HeroDetailService ]
|
|
})
|
|
export class HeroDetailComponent implements OnInit {
|
|
constructor(
|
|
private heroDetailService: HeroDetailService,
|
|
private route: ActivatedRoute,
|
|
private router: Router) {
|
|
}
|
|
|
|
@Input() hero: Hero;
|
|
|
|
ngOnInit(): void {
|
|
// get hero when `id` param changes
|
|
this.route.params.map(p => p['id'])
|
|
.forEach(id => this.getHero(id))
|
|
.catch(() => this.hero = new Hero()); // no id; should edit new hero
|
|
}
|
|
|
|
private getHero(id: string): void {
|
|
this.heroDetailService.getHero(id).then(hero => {
|
|
if (hero) {
|
|
this.hero = hero;
|
|
} else {
|
|
this.gotoList(); // id not found; navigate to list
|
|
}
|
|
});
|
|
}
|
|
|
|
save(): void {
|
|
this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
|
|
}
|
|
|
|
cancel() { this.gotoList(); }
|
|
|
|
gotoList() {
|
|
this.router.navigate(['../'], {relativeTo: this.route});
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-detail.service.ts]" value="import { Injectable } from '@angular/core';
|
|
|
|
import { Hero, HeroService } from '../model';
|
|
|
|
@Injectable()
|
|
export class HeroDetailService {
|
|
constructor(private heroService: HeroService) { }
|
|
|
|
// Returns a clone which caller may modify safely
|
|
getHero(id: number | string): Promise<Hero> {
|
|
if (typeof id === 'string') {
|
|
id = parseInt(id as string, 10);
|
|
}
|
|
return this.heroService.getHero(id).then(hero => {
|
|
return hero ? Object.assign({}, hero) : null; // clone or null
|
|
});
|
|
}
|
|
|
|
saveHero(hero: Hero) {
|
|
return this.heroService.updateHero(hero);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-list.component.spec.ts]" value="import { async, ComponentFixture, fakeAsync, TestBed, tick
|
|
} from '@angular/core/testing';
|
|
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import { addMatchers, newEvent, Router, RouterStub
|
|
} from '../../testing';
|
|
|
|
import { HEROES, FakeHeroService } from '../model/testing';
|
|
|
|
import { HeroModule } from './hero.module';
|
|
import { HeroListComponent } from './hero-list.component';
|
|
import { HighlightDirective } from '../shared/highlight.directive';
|
|
import { HeroService } from '../model';
|
|
|
|
let comp: HeroListComponent;
|
|
let fixture: ComponentFixture<HeroListComponent>;
|
|
let page: Page;
|
|
|
|
/////// Tests //////
|
|
|
|
describe('HeroListComponent', () => {
|
|
|
|
beforeEach( async(() => {
|
|
addMatchers();
|
|
TestBed.configureTestingModule({
|
|
imports: [HeroModule],
|
|
providers: [
|
|
{ provide: HeroService, useClass: FakeHeroService },
|
|
{ provide: Router, useClass: RouterStub}
|
|
]
|
|
})
|
|
.compileComponents()
|
|
.then(createComponent);
|
|
}));
|
|
|
|
it('should display heroes', () => {
|
|
expect(page.heroRows.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('1st hero should match 1st test hero', () => {
|
|
const expectedHero = HEROES[0];
|
|
const actualHero = page.heroRows[0].textContent;
|
|
expect(actualHero).toContain(expectedHero.id, 'hero.id');
|
|
expect(actualHero).toContain(expectedHero.name, 'hero.name');
|
|
});
|
|
|
|
it('should select hero on click', fakeAsync(() => {
|
|
const expectedHero = HEROES[1];
|
|
const li = page.heroRows[1];
|
|
li.dispatchEvent(newEvent('click'));
|
|
tick();
|
|
// `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
|
|
expect(comp.selectedHero).toEqual(expectedHero);
|
|
}));
|
|
|
|
it('should navigate to selected hero detail on click', fakeAsync(() => {
|
|
const expectedHero = HEROES[1];
|
|
const li = page.heroRows[1];
|
|
li.dispatchEvent(newEvent('click'));
|
|
tick();
|
|
|
|
// should have navigated
|
|
expect(page.navSpy.calls.any()).toBe(true, 'navigate called');
|
|
|
|
// composed hero detail will be URL like 'heroes/42'
|
|
// expect link array with the route path and hero id
|
|
// first argument to router.navigate is link array
|
|
const navArgs = page.navSpy.calls.first().args[0];
|
|
expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL');
|
|
expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id');
|
|
|
|
}));
|
|
|
|
it('should find `HighlightDirective` with `By.directive', () => {
|
|
// Can find DebugElement either by css selector or by directive
|
|
const h2 = fixture.debugElement.query(By.css('h2'));
|
|
const directive = fixture.debugElement.query(By.directive(HighlightDirective));
|
|
expect(h2).toBe(directive);
|
|
});
|
|
|
|
it('should color header with `HighlightDirective`', () => {
|
|
const h2 = page.highlightDe.nativeElement as HTMLElement;
|
|
const bgColor = h2.style.backgroundColor;
|
|
|
|
// different browsers report color values differently
|
|
const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)';
|
|
expect(isExpectedColor).toBe(true, 'backgroundColor');
|
|
});
|
|
|
|
it('the `HighlightDirective` is among the element\'s providers', () => {
|
|
expect(page.highlightDe.providerTokens).toContain(HighlightDirective, 'HighlightDirective');
|
|
});
|
|
});
|
|
|
|
/////////// Helpers /////
|
|
|
|
/** Create the component and set the `page` test variables */
|
|
function createComponent() {
|
|
fixture = TestBed.createComponent(HeroListComponent);
|
|
comp = fixture.componentInstance;
|
|
|
|
// change detection triggers ngOnInit which gets a hero
|
|
fixture.detectChanges();
|
|
|
|
return fixture.whenStable().then(() => {
|
|
// got the heroes and updated component
|
|
// change detection updates the view
|
|
fixture.detectChanges();
|
|
page = new Page();
|
|
});
|
|
}
|
|
|
|
class Page {
|
|
/** Hero line elements */
|
|
heroRows: HTMLLIElement[];
|
|
|
|
/** Highlighted element */
|
|
highlightDe: DebugElement;
|
|
|
|
/** Spy on router navigate method */
|
|
navSpy: jasmine.Spy;
|
|
|
|
constructor() {
|
|
this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement);
|
|
|
|
// Find the first element with an attached HighlightDirective
|
|
this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective));
|
|
|
|
// Get the component's injected router and spy on it
|
|
const router = fixture.debugElement.injector.get(Router);
|
|
this.navSpy = spyOn(router, 'navigate');
|
|
};
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-list.component.ts]" value="import { Component, OnInit } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
|
|
import { Hero, HeroService } from '../model';
|
|
|
|
@Component({
|
|
moduleId: module.id,
|
|
selector: 'app-heroes',
|
|
templateUrl: './hero-list.component.html',
|
|
styleUrls: [ './hero-list.component.css' ]
|
|
})
|
|
export class HeroListComponent implements OnInit {
|
|
heroes: Promise<Hero[]>;
|
|
selectedHero: Hero;
|
|
|
|
constructor(
|
|
private router: Router,
|
|
private heroService: HeroService) { }
|
|
|
|
ngOnInit() {
|
|
this.heroes = this.heroService.getHeroes();
|
|
}
|
|
|
|
onSelect(hero: Hero) {
|
|
this.selectedHero = hero;
|
|
this.router.navigate(['../heroes', this.selectedHero.id ]);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero-routing.module.ts]" value="import { NgModule } from '@angular/core';
|
|
import { RouterModule, Routes } from '@angular/router';
|
|
|
|
import { HeroListComponent } from './hero-list.component';
|
|
import { HeroDetailComponent } from './hero-detail.component';
|
|
|
|
const routes: Routes = [
|
|
{ path: '', component: HeroListComponent },
|
|
{ path: ':id', component: HeroDetailComponent }
|
|
];
|
|
|
|
export const routedComponents = [HeroDetailComponent, HeroListComponent];
|
|
|
|
@NgModule({
|
|
imports: [RouterModule.forChild(routes)],
|
|
exports: [RouterModule]
|
|
})
|
|
export class HeroRoutingModule {}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/hero/hero.module.ts]" value="import { NgModule } from '@angular/core';
|
|
import { SharedModule } from '../shared/shared.module';
|
|
import { routedComponents, HeroRoutingModule } from './hero-routing.module';
|
|
|
|
@NgModule({
|
|
imports: [ SharedModule, HeroRoutingModule ],
|
|
declarations: [ routedComponents ]
|
|
})
|
|
export class HeroModule { }
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/hero.service.ts]" value="import { Injectable } from '@angular/core';
|
|
|
|
import { Hero } from './hero';
|
|
import { HEROES } from './test-heroes';
|
|
|
|
@Injectable()
|
|
/** Dummy HeroService. Pretend it makes real http requests */
|
|
export class HeroService {
|
|
getHeroes() {
|
|
return Promise.resolve(HEROES);
|
|
}
|
|
|
|
getHero(id: number | string): Promise<Hero> {
|
|
if (typeof id === 'string') {
|
|
id = parseInt(id as string, 10);
|
|
}
|
|
return this.getHeroes().then(
|
|
heroes => heroes.find(hero => hero.id === id)
|
|
);
|
|
}
|
|
|
|
updateHero(hero: Hero): Promise<Hero> {
|
|
return this.getHero(hero.id).then(h => {
|
|
if (!h) {
|
|
throw new Error(`Hero ${hero.id} not found`);
|
|
}
|
|
return Object.assign(h, hero);
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/hero.spec.ts]" value="import { Hero } from './hero';
|
|
|
|
describe('Hero', () => {
|
|
it('has name', () => {
|
|
const hero = new Hero(1, 'Super Cat');
|
|
expect(hero.name).toBe('Super Cat');
|
|
});
|
|
|
|
it('has id', () => {
|
|
const hero = new Hero(1, 'Super Cat');
|
|
expect(hero.id).toBe(1);
|
|
});
|
|
|
|
it('can clone itself', () => {
|
|
const hero = new Hero(1, 'Super Cat');
|
|
const clone = hero.clone();
|
|
expect(hero).toEqual(clone);
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/hero.ts]" value="export class Hero {
|
|
constructor(public id = 0, public name = '') { }
|
|
clone() { return new Hero(this.id, this.name); }
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/http-hero.service.spec.ts]" value="import {
|
|
async, inject, TestBed
|
|
} from '@angular/core/testing';
|
|
|
|
import {
|
|
MockBackend,
|
|
MockConnection
|
|
} from '@angular/http/testing';
|
|
|
|
import {
|
|
HttpModule, Http, XHRBackend, Response, ResponseOptions
|
|
} from '@angular/http';
|
|
|
|
import { Observable } from 'rxjs/Observable';
|
|
import 'rxjs/add/observable/of';
|
|
|
|
import 'rxjs/add/operator/catch';
|
|
import 'rxjs/add/operator/do';
|
|
import 'rxjs/add/operator/toPromise';
|
|
|
|
import { Hero } from './hero';
|
|
import { HttpHeroService as HeroService } from './http-hero.service';
|
|
|
|
const makeHeroData = () => [
|
|
{ id: 1, name: 'Windstorm' },
|
|
{ id: 2, name: 'Bombasto' },
|
|
{ id: 3, name: 'Magneta' },
|
|
{ id: 4, name: 'Tornado' }
|
|
] as Hero[];
|
|
|
|
//////// Tests /////////////
|
|
describe('Http-HeroService (mockBackend)', () => {
|
|
|
|
beforeEach( async(() => {
|
|
TestBed.configureTestingModule({
|
|
imports: [ HttpModule ],
|
|
providers: [
|
|
HeroService,
|
|
{ provide: XHRBackend, useClass: MockBackend }
|
|
]
|
|
})
|
|
.compileComponents();
|
|
}));
|
|
|
|
it('can instantiate service when inject service',
|
|
inject([HeroService], (service: HeroService) => {
|
|
expect(service instanceof HeroService).toBe(true);
|
|
}));
|
|
|
|
|
|
|
|
it('can instantiate service with "new"', inject([Http], (http: Http) => {
|
|
expect(http).not.toBeNull('http should be provided');
|
|
let service = new HeroService(http);
|
|
expect(service instanceof HeroService).toBe(true, 'new service should be ok');
|
|
}));
|
|
|
|
|
|
it('can provide the mockBackend as XHRBackend',
|
|
inject([XHRBackend], (backend: MockBackend) => {
|
|
expect(backend).not.toBeNull('backend should be provided');
|
|
}));
|
|
|
|
describe('when getHeroes', () => {
|
|
let backend: MockBackend;
|
|
let service: HeroService;
|
|
let fakeHeroes: Hero[];
|
|
let response: Response;
|
|
|
|
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
|
|
backend = be;
|
|
service = new HeroService(http);
|
|
fakeHeroes = makeHeroData();
|
|
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
|
|
response = new Response(options);
|
|
}));
|
|
|
|
it('should have expected fake heroes (then)', async(inject([], () => {
|
|
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
|
|
|
|
service.getHeroes().toPromise()
|
|
// .then(() => Promise.reject('deliberate'))
|
|
.then(heroes => {
|
|
expect(heroes.length).toBe(fakeHeroes.length,
|
|
'should have expected no. of heroes');
|
|
});
|
|
})));
|
|
|
|
it('should have expected fake heroes (Observable.do)', async(inject([], () => {
|
|
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
|
|
|
|
service.getHeroes()
|
|
.do(heroes => {
|
|
expect(heroes.length).toBe(fakeHeroes.length,
|
|
'should have expected no. of heroes');
|
|
})
|
|
.toPromise();
|
|
})));
|
|
|
|
|
|
it('should be OK returning no heroes', async(inject([], () => {
|
|
let resp = new Response(new ResponseOptions({status: 200, body: {data: []}}));
|
|
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
|
|
|
|
service.getHeroes()
|
|
.do(heroes => {
|
|
expect(heroes.length).toBe(0, 'should have no heroes');
|
|
})
|
|
.toPromise();
|
|
})));
|
|
|
|
it('should treat 404 as an Observable error', async(inject([], () => {
|
|
let resp = new Response(new ResponseOptions({status: 404}));
|
|
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
|
|
|
|
service.getHeroes()
|
|
.do(heroes => {
|
|
fail('should not respond with heroes');
|
|
})
|
|
.catch(err => {
|
|
expect(err).toMatch(/Bad response status/, 'should catch bad response status code');
|
|
return Observable.of(null); // failure is the expected test result
|
|
})
|
|
.toPromise();
|
|
})));
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/http-hero.service.ts]" value="import { Injectable } from '@angular/core';
|
|
import { Http, Response } from '@angular/http';
|
|
import { Headers, RequestOptions } from '@angular/http';
|
|
import { Hero } from './hero';
|
|
|
|
import { Observable } from 'rxjs/Observable';
|
|
import 'rxjs/add/observable/throw';
|
|
|
|
import 'rxjs/add/operator/do';
|
|
import 'rxjs/add/operator/catch';
|
|
import 'rxjs/add/operator/map';
|
|
|
|
@Injectable()
|
|
export class HttpHeroService {
|
|
private _heroesUrl = 'app/heroes'; // URL to web api
|
|
|
|
constructor (private http: Http) {}
|
|
|
|
getHeroes (): Observable<Hero[]> {
|
|
return this.http.get(this._heroesUrl)
|
|
.map(this.extractData)
|
|
// .do(data => console.log(data)) // eyeball results in the console
|
|
.catch(this.handleError);
|
|
}
|
|
|
|
getHero(id: number | string) {
|
|
return this.http
|
|
.get('app/heroes/?id=${id}')
|
|
.map((r: Response) => r.json().data as Hero[]);
|
|
}
|
|
|
|
addHero (name: string): Observable<Hero> {
|
|
let body = JSON.stringify({ name });
|
|
let headers = new Headers({ 'Content-Type': 'application/json' });
|
|
let options = new RequestOptions({ headers: headers });
|
|
|
|
return this.http.post(this._heroesUrl, body, options)
|
|
.map(this.extractData)
|
|
.catch(this.handleError);
|
|
}
|
|
|
|
updateHero (hero: Hero): Observable<Hero> {
|
|
let body = JSON.stringify(hero);
|
|
let headers = new Headers({ 'Content-Type': 'application/json' });
|
|
let options = new RequestOptions({ headers: headers });
|
|
|
|
return this.http.put(this._heroesUrl, body, options)
|
|
.map(this.extractData)
|
|
.catch(this.handleError);
|
|
}
|
|
|
|
private extractData(res: Response) {
|
|
if (res.status < 200 || res.status >= 300) {
|
|
throw new Error('Bad response status: ' + res.status);
|
|
}
|
|
let body = res.json();
|
|
return body.data || { };
|
|
}
|
|
|
|
private handleError (error: any) {
|
|
// In a real world app, we might send the error to remote logging infrastructure
|
|
let errMsg = error.message || 'Server error';
|
|
console.error(errMsg); // log to console instead
|
|
return Observable.throw(errMsg);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/index.ts]" value="// Model barrel
|
|
export * from './hero';
|
|
export * from './hero.service';
|
|
export * from './http-hero.service';
|
|
export * from './test-heroes';
|
|
|
|
export * from './user.service';
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/test-heroes.ts]" value="import { Hero } from './hero';
|
|
|
|
export var HEROES: Hero[] = [
|
|
new Hero(11, 'Mr. Nice'),
|
|
new Hero(12, 'Narco'),
|
|
new Hero(13, 'Bombasto'),
|
|
new Hero(14, 'Celeritas'),
|
|
new Hero(15, 'Magneta'),
|
|
new Hero(16, 'RubberMan')
|
|
];
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/testing/fake-hero.service.ts]" value="// re-export for tester convenience
|
|
export { Hero } from '../hero';
|
|
export { HeroService } from '../hero.service';
|
|
|
|
import { Hero } from '../hero';
|
|
import { HeroService } from '../hero.service';
|
|
|
|
export var HEROES: Hero[] = [
|
|
new Hero(41, 'Bob'),
|
|
new Hero(42, 'Carol'),
|
|
new Hero(43, 'Ted'),
|
|
new Hero(44, 'Alice'),
|
|
new Hero(45, 'Speedy'),
|
|
new Hero(46, 'Stealthy')
|
|
];
|
|
|
|
export class FakeHeroService implements HeroService {
|
|
|
|
heroes = HEROES.map(h => h.clone());
|
|
lastPromise: Promise<any>; // remember so we can spy on promise calls
|
|
|
|
getHero(id: number | string) {
|
|
if (typeof id === 'string') {
|
|
id = parseInt(id as string, 10);
|
|
}
|
|
let hero = this.heroes.find(h => h.id === id);
|
|
return this.lastPromise = Promise.resolve(hero);
|
|
}
|
|
|
|
getHeroes() {
|
|
return this.lastPromise = Promise.resolve<Hero[]>(this.heroes);
|
|
}
|
|
|
|
updateHero(hero: Hero): Promise<Hero> {
|
|
return this.lastPromise = this.getHero(hero.id).then(h => {
|
|
return h ?
|
|
Object.assign(h, hero) :
|
|
Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>;
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/testing/index.ts]" value="export * from './fake-hero.service';
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/model/user.service.ts]" value="import { Injectable } from '@angular/core';
|
|
|
|
@Injectable()
|
|
export class UserService {
|
|
isLoggedIn = true;
|
|
user = {name: 'Sam Spade'};
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/highlight.directive.spec.ts]" value="import { Component, DebugElement } from '@angular/core';
|
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
|
|
import { HighlightDirective } from './highlight.directive';
|
|
import { newEvent } from '../../testing';
|
|
|
|
@Component({
|
|
template: `
|
|
<h2 highlight="yellow">Something Yellow</h2>
|
|
<h2 highlight>The Default (Gray)</h2>
|
|
<h2>No Highlight</h2>
|
|
<input #box [highlight]="box.value" value="cyan"/>`
|
|
})
|
|
class TestComponent { }
|
|
|
|
describe('HighlightDirective', () => {
|
|
|
|
let fixture: ComponentFixture<TestComponent>;
|
|
let des: DebugElement[]; // the three elements w/ the directive
|
|
let bareH2: DebugElement; // the <h2> w/o the directive
|
|
|
|
beforeEach(() => {
|
|
fixture = TestBed.configureTestingModule({
|
|
declarations: [ HighlightDirective, TestComponent ]
|
|
})
|
|
.createComponent(TestComponent);
|
|
|
|
fixture.detectChanges(); // initial binding
|
|
|
|
// all elements with an attached HighlightDirective
|
|
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
|
|
|
|
// the h2 without the HighlightDirective
|
|
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
|
|
});
|
|
|
|
// color tests
|
|
it('should have three highlighted elements', () => {
|
|
expect(des.length).toBe(3);
|
|
});
|
|
|
|
it('should color 1st <h2> background "yellow"', () => {
|
|
const bgColor = des[0].nativeElement.style.backgroundColor;
|
|
expect(bgColor).toBe('yellow');
|
|
});
|
|
|
|
it('should color 2nd <h2> background w/ default color', () => {
|
|
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
|
|
const bgColor = des[1].nativeElement.style.backgroundColor;
|
|
expect(bgColor).toBe(dir.defaultColor);
|
|
});
|
|
|
|
it('should bind <input> background to value color', () => {
|
|
// easier to work with nativeElement
|
|
const input = des[2].nativeElement as HTMLInputElement;
|
|
expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
|
|
|
|
// dispatch a DOM event so that Angular responds to the input value change.
|
|
input.value = 'green';
|
|
input.dispatchEvent(newEvent('input'));
|
|
fixture.detectChanges();
|
|
|
|
expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
|
|
});
|
|
|
|
|
|
it('bare <h2> should not have a customProperty', () => {
|
|
expect(bareH2.properties['customProperty']).toBeUndefined();
|
|
});
|
|
|
|
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
|
|
// // customProperty tests
|
|
// it('all highlighted elements should have a true customProperty', () => {
|
|
// const allTrue = des.map(de => !!de.properties['customProperty']).every(v => v === true);
|
|
// expect(allTrue).toBe(true);
|
|
// });
|
|
|
|
// injected directive
|
|
// attached HighlightDirective can be injected
|
|
it('can inject `HighlightDirective` in 1st <h2>', () => {
|
|
const dir = des[0].injector.get(HighlightDirective);
|
|
expect(dir).toBeTruthy();
|
|
});
|
|
|
|
it('cannot inject `HighlightDirective` in 3rd <h2>', () => {
|
|
const dir = bareH2.injector.get(HighlightDirective, null);
|
|
expect(dir).toBe(null);
|
|
});
|
|
|
|
// DebugElement.providerTokens
|
|
// attached HighlightDirective should be listed in the providerTokens
|
|
it('should have `HighlightDirective` in 1st <h2> providerTokens', () => {
|
|
expect(des[0].providerTokens).toContain(HighlightDirective);
|
|
});
|
|
|
|
it('should not have `HighlightDirective` in 3rd <h2> providerTokens', () => {
|
|
expect(bareH2.providerTokens).not.toContain(HighlightDirective);
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/highlight.directive.ts]" value="import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
|
|
|
|
@Directive({ selector: '[highlight]' })
|
|
/** Set backgroundColor for the attached element to highlight color
|
|
* and set the element's customProperty to true */
|
|
export class HighlightDirective implements OnChanges {
|
|
|
|
defaultColor = 'rgb(211, 211, 211)'; // lightgray
|
|
|
|
@Input('highlight') bgColor: string;
|
|
|
|
constructor(private el: ElementRef) {
|
|
el.nativeElement.style.customProperty = true;
|
|
}
|
|
|
|
ngOnChanges() {
|
|
this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/shared.module.ts]" value="import { NgModule } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
|
|
import { HighlightDirective } from './highlight.directive';
|
|
import { TitleCasePipe } from './title-case.pipe';
|
|
import { TwainComponent } from './twain.component';
|
|
|
|
@NgModule({
|
|
imports: [ CommonModule ],
|
|
exports: [ CommonModule, FormsModule,
|
|
HighlightDirective, TitleCasePipe, TwainComponent ],
|
|
declarations: [ HighlightDirective, TitleCasePipe, TwainComponent ]
|
|
})
|
|
export class SharedModule { }
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/title-case.pipe.spec.ts]" value="import { TitleCasePipe } from './title-case.pipe';
|
|
|
|
describe('TitleCasePipe', () => {
|
|
// This pipe is a pure, stateless function so no need for BeforeEach
|
|
let pipe = new TitleCasePipe();
|
|
|
|
it('transforms "abc" to "Abc"', () => {
|
|
expect(pipe.transform('abc')).toBe('Abc');
|
|
});
|
|
|
|
it('transforms "abc def" to "Abc Def"', () => {
|
|
expect(pipe.transform('abc def')).toBe('Abc Def');
|
|
});
|
|
|
|
// ... more tests ...
|
|
it('leaves "Abc Def" unchanged', () => {
|
|
expect(pipe.transform('Abc Def')).toBe('Abc Def');
|
|
});
|
|
|
|
it('transforms "abc-def" to "Abc-def"', () => {
|
|
expect(pipe.transform('abc-def')).toBe('Abc-def');
|
|
});
|
|
|
|
it('transforms " abc def" to " Abc Def" (preserves spaces) ', () => {
|
|
expect(pipe.transform(' abc def')).toBe(' Abc Def');
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/title-case.pipe.ts]" value="import { Pipe, PipeTransform } from '@angular/core';
|
|
|
|
@Pipe({name: 'titlecase', pure: false})
|
|
/** Transform to Title Case: uppercase the first letter of the words in a string.*/
|
|
export class TitleCasePipe implements PipeTransform {
|
|
transform(input: string): string {
|
|
return input.length === 0 ? '' :
|
|
input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() ));
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/twain.component.spec.ts]" value="import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import { TwainService } from './twain.service';
|
|
import { TwainComponent } from './twain.component';
|
|
|
|
describe('TwainComponent', () => {
|
|
|
|
let comp: TwainComponent;
|
|
let fixture: ComponentFixture<TwainComponent>;
|
|
|
|
let spy: jasmine.Spy;
|
|
let de: DebugElement;
|
|
let el: HTMLElement;
|
|
let twainService: TwainService; // the actually injected service
|
|
|
|
const testQuote = 'Test Quote';
|
|
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({
|
|
declarations: [ TwainComponent ],
|
|
providers: [ TwainService ],
|
|
});
|
|
|
|
fixture = TestBed.createComponent(TwainComponent);
|
|
comp = fixture.componentInstance;
|
|
|
|
// TwainService actually injected into the component
|
|
twainService = fixture.debugElement.injector.get(TwainService);
|
|
|
|
// Setup spy on the `getQuote` method
|
|
spy = spyOn(twainService, 'getQuote')
|
|
.and.returnValue(Promise.resolve(testQuote));
|
|
|
|
// Get the Twain quote element by CSS selector (e.g., by class name)
|
|
de = fixture.debugElement.query(By.css('.twain'));
|
|
el = de.nativeElement;
|
|
});
|
|
|
|
it('should not show quote before OnInit', () => {
|
|
expect(el.textContent).toBe('', 'nothing displayed');
|
|
expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
|
|
});
|
|
|
|
it('should still not show quote after component initialized', () => {
|
|
fixture.detectChanges();
|
|
// getQuote service is async => still has not returned with quote
|
|
expect(el.textContent).toBe('...', 'no quote yet');
|
|
expect(spy.calls.any()).toBe(true, 'getQuote called');
|
|
});
|
|
|
|
it('should show quote after getQuote promise (async)', async(() => {
|
|
fixture.detectChanges();
|
|
|
|
fixture.whenStable().then(() => { // wait for async getQuote
|
|
fixture.detectChanges(); // update view with quote
|
|
expect(el.textContent).toBe(testQuote);
|
|
});
|
|
}));
|
|
|
|
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
|
|
fixture.detectChanges();
|
|
tick(); // wait for async getQuote
|
|
fixture.detectChanges(); // update view with quote
|
|
expect(el.textContent).toBe(testQuote);
|
|
}));
|
|
|
|
it('should show quote after getQuote promise (done)', done => {
|
|
fixture.detectChanges();
|
|
|
|
// get the spy promise and wait for it to resolve
|
|
spy.calls.mostRecent().returnValue.then(() => {
|
|
fixture.detectChanges(); // update view with quote
|
|
expect(el.textContent).toBe(testQuote);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/twain.component.ts]" value="import { Component, OnInit } from '@angular/core';
|
|
|
|
import { TwainService } from './twain.service';
|
|
|
|
@Component({
|
|
selector: 'twain-quote',
|
|
template: '<p class="twain"><i>{{quote}}</i></p>'
|
|
})
|
|
export class TwainComponent implements OnInit {
|
|
intervalId: number;
|
|
quote = '...';
|
|
constructor(private twainService: TwainService) { }
|
|
|
|
ngOnInit(): void {
|
|
this.twainService.getQuote().then(quote => this.quote = quote);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/shared/twain.service.ts]" value="import { Injectable } from '@angular/core';
|
|
|
|
const quotes = [
|
|
'Always do right. This will gratify some people and astonish the rest.',
|
|
'I have never let my schooling interfere with my education.',
|
|
'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.',
|
|
'Whenever you find yourself on the side of the majority, it is time to pause and reflect.',
|
|
'If you tell the truth, you don\'t have to remember anything.',
|
|
'Clothes make the man. Naked people have little or no influence on society.',
|
|
'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.',
|
|
'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.',
|
|
'The man who does not read good books has no advantage over the man who cannot read them.',
|
|
'Get your facts first, and then you can distort them as much as you please.',
|
|
];
|
|
|
|
@Injectable()
|
|
export class TwainService {
|
|
private next = 0;
|
|
|
|
// Imaginary todo: get quotes from a remote quote service
|
|
// returns quote after delay simulating server latency
|
|
getQuote(): Promise<string> {
|
|
return new Promise(resolve => {
|
|
setTimeout( () => resolve(this.nextQuote()), 500 );
|
|
});
|
|
}
|
|
|
|
private nextQuote() {
|
|
if (this.next === quotes.length) { this.next = 0; }
|
|
return quotes[ this.next++ ];
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/welcome.component.spec.ts]" value="import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
|
import { By } from '@angular/platform-browser';
|
|
import { DebugElement } from '@angular/core';
|
|
|
|
import { UserService } from './model';
|
|
import { WelcomeComponent } from './welcome.component';
|
|
|
|
describe('WelcomeComponent', () => {
|
|
|
|
let comp: WelcomeComponent;
|
|
let fixture: ComponentFixture<WelcomeComponent>;
|
|
let componentUserService: UserService; // the actually injected service
|
|
let userService: UserService; // the TestBed injected service
|
|
let de: DebugElement; // the DebugElement with the welcome message
|
|
let el: HTMLElement; // the DOM element with the welcome message
|
|
|
|
let userServiceStub: {
|
|
isLoggedIn: boolean;
|
|
user: { name: string}
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// stub UserService for test purposes
|
|
userServiceStub = {
|
|
isLoggedIn: true,
|
|
user: { name: 'Test User'}
|
|
};
|
|
|
|
TestBed.configureTestingModule({
|
|
declarations: [ WelcomeComponent ],
|
|
// providers: [ UserService ] // NO! Don't provide the real service!
|
|
// Provide a test-double instead
|
|
providers: [ {provide: UserService, useValue: userServiceStub } ]
|
|
});
|
|
|
|
fixture = TestBed.createComponent(WelcomeComponent);
|
|
comp = fixture.componentInstance;
|
|
|
|
// UserService actually injected into the component
|
|
userService = fixture.debugElement.injector.get(UserService);
|
|
componentUserService = userService;
|
|
// UserService from the root injector
|
|
userService = TestBed.get(UserService);
|
|
|
|
// get the "welcome" element by CSS selector (e.g., by class name)
|
|
de = fixture.debugElement.query(By.css('.welcome'));
|
|
el = de.nativeElement;
|
|
});
|
|
|
|
it('should welcome the user', () => {
|
|
fixture.detectChanges();
|
|
const content = el.textContent;
|
|
expect(content).toContain('Welcome', '"Welcome ..."');
|
|
expect(content).toContain('Test User', 'expected name');
|
|
});
|
|
|
|
it('should welcome "Bubba"', () => {
|
|
userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
|
|
fixture.detectChanges();
|
|
expect(el.textContent).toContain('Bubba');
|
|
});
|
|
|
|
it('should request login if not logged in', () => {
|
|
userService.isLoggedIn = false; // welcome message hasn't been shown yet
|
|
fixture.detectChanges();
|
|
const content = el.textContent;
|
|
expect(content).not.toContain('Welcome', 'not welcomed');
|
|
expect(content).toMatch(/log in/i, '"log in"');
|
|
});
|
|
|
|
it('should inject the component\'s UserService instance',
|
|
inject([UserService], (service: UserService) => {
|
|
expect(service).toBe(componentUserService);
|
|
}));
|
|
|
|
it('TestBed and Component UserService should be the same', () => {
|
|
expect(userService === componentUserService).toBe(true);
|
|
});
|
|
|
|
it('stub object and injected UserService should not be the same', () => {
|
|
expect(userServiceStub === userService).toBe(false);
|
|
|
|
// Changing the stub object has no effect on the injected service
|
|
userServiceStub.isLoggedIn = false;
|
|
expect(userService.isLoggedIn).toBe(true);
|
|
});
|
|
});
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[app/welcome.component.ts]" value="import { Component, OnInit } from '@angular/core';
|
|
import { UserService } from './model';
|
|
|
|
@Component({
|
|
selector: 'app-welcome',
|
|
template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>'
|
|
})
|
|
export class WelcomeComponent implements OnInit {
|
|
welcome = '-- not initialized yet --';
|
|
constructor(private userService: UserService) { }
|
|
|
|
ngOnInit(): void {
|
|
this.welcome = this.userService.isLoggedIn ?
|
|
'Welcome, ' + this.userService.user.name :
|
|
'Please log in.';
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/index.ts]" value="import { DebugElement } from '@angular/core';
|
|
import { tick, ComponentFixture } from '@angular/core/testing';
|
|
|
|
export * from './jasmine-matchers';
|
|
export * from './router-stubs';
|
|
|
|
///// Short utilities /////
|
|
|
|
/** Wait a tick, then detect changes */
|
|
export function advance(f: ComponentFixture<any>): void {
|
|
tick();
|
|
f.detectChanges();
|
|
}
|
|
|
|
/**
|
|
* Create custom DOM event the old fashioned way
|
|
*
|
|
* https://developer.mozilla.org/en-US/docs/Web/API/Event/initEvent
|
|
* Although officially deprecated, some browsers (phantom) don't accept the preferred "new Event(eventName)"
|
|
*/
|
|
export function newEvent(eventName: string, bubbles = false, cancelable = false) {
|
|
let evt = document.createEvent('CustomEvent'); // MUST be 'CustomEvent'
|
|
evt.initCustomEvent(eventName, bubbles, cancelable, null);
|
|
return evt;
|
|
}
|
|
|
|
// See https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
|
|
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
|
|
export const ButtonClickEvents = {
|
|
left: { button: 0 },
|
|
right: { button: 2 }
|
|
};
|
|
|
|
/** Simulate element click. Defaults to mouse left-button click event. */
|
|
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
|
|
if (el instanceof HTMLElement) {
|
|
el.click();
|
|
} else {
|
|
el.triggerEventHandler('click', eventObj);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/jasmine-matchers.d.ts]" value="declare namespace jasmine {
|
|
interface Matchers {
|
|
toHaveText(actual: any, expectationFailOutput?: any): jasmine.CustomMatcher;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/jasmine-matchers.ts]" value="/// <reference path="./jasmine-matchers.d.ts" />
|
|
|
|
//// Jasmine Custom Matchers ////
|
|
// Be sure to extend jasmine-matchers.d.ts when adding matchers
|
|
|
|
export function addMatchers(): void {
|
|
jasmine.addMatchers({
|
|
toHaveText: toHaveText
|
|
});
|
|
}
|
|
|
|
function toHaveText(): jasmine.CustomMatcher {
|
|
return {
|
|
compare: function (actual: any, expectedText: string, expectationFailOutput?: any): jasmine.CustomMatcherResult {
|
|
const actualText = elementText(actual);
|
|
const pass = actualText.indexOf(expectedText) > -1;
|
|
const message = pass ? '' : composeMessage();
|
|
return { pass, message };
|
|
|
|
function composeMessage () {
|
|
const a = (actualText.length < 100 ? actualText : actualText.substr(0, 100) + '...');
|
|
const efo = expectationFailOutput ? ` '${expectationFailOutput}'` : '';
|
|
return `Expected element to have text content '${expectedText}' instead of '${a}'${efo}`;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
function elementText(n: any): string {
|
|
if (n instanceof Array) {
|
|
return n.map(elementText).join('');
|
|
}
|
|
|
|
if (n.nodeType === Node.COMMENT_NODE) {
|
|
return '';
|
|
}
|
|
|
|
if (n.nodeType === Node.ELEMENT_NODE && n.hasChildNodes()) {
|
|
return elementText(Array.prototype.slice.call(n.childNodes));
|
|
}
|
|
|
|
if (n.nativeElement) { n = n.nativeElement; }
|
|
|
|
return n.textContent;
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[testing/router-stubs.ts]" value=" // export for convenience.
|
|
export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
|
|
|
|
import { Component, Directive, Injectable, Input } from '@angular/core';
|
|
import { NavigationExtras } from '@angular/router';
|
|
|
|
@Directive({
|
|
selector: '[routerLink]',
|
|
host: {
|
|
'(click)': 'onClick()'
|
|
}
|
|
})
|
|
export class RouterLinkStubDirective {
|
|
@Input('routerLink') linkParams: any;
|
|
navigatedTo: any = null;
|
|
|
|
onClick() {
|
|
this.navigatedTo = this.linkParams;
|
|
}
|
|
}
|
|
|
|
@Component({selector: 'router-outlet', template: ''})
|
|
export class RouterOutletStubComponent { }
|
|
|
|
@Injectable()
|
|
export class RouterStub {
|
|
navigate(commands: any[], extras?: NavigationExtras) { }
|
|
}
|
|
|
|
|
|
// Only implements params and part of snapshot.params
|
|
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
|
|
|
@Injectable()
|
|
export class ActivatedRouteStub {
|
|
|
|
// ActivatedRoute.params is Observable
|
|
private subject = new BehaviorSubject(this.testParams);
|
|
params = this.subject.asObservable();
|
|
|
|
// Test parameters
|
|
private _testParams: {};
|
|
get testParams() { return this._testParams; }
|
|
set testParams(params: {}) {
|
|
this._testParams = params;
|
|
this.subject.next(params);
|
|
}
|
|
|
|
// ActivatedRoute.snapshot.params
|
|
get snapshot() {
|
|
return { params: this.testParams };
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/"><input type="hidden" name="files[index.html]" value="<!-- Run application specs in a browser -->
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<script>document.write('<base href="' + document.location + '" />');</script>
|
|
<title>Sample App Specs</title>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="stylesheet" href="styles.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.css">
|
|
|
|
</head>
|
|
<body>
|
|
<!-- Polyfills -->
|
|
<script src="https://unpkg.com/core-js/client/shim.min.js"></script>
|
|
|
|
<script src="https://unpkg.com/systemjs@0.19.39/dist/system.src.js"></script>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/jasmine-html.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.4.1/boot.js"></script>
|
|
|
|
<script src="https://unpkg.com/zone.js@0.7.4?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/long-stack-trace-zone.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/proxy.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/sync-test.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/jasmine-patch.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/async-test.js?main=browser"></script>
|
|
<script src="https://unpkg.com/zone.js/dist/fake-async-test.js?main=browser"></script>
|
|
|
|
<script>
|
|
var __spec_files__ = [
|
|
'app/about.component.spec',
|
|
'app/app.component.spec',
|
|
'app/app.component.router.spec',
|
|
'app/banner.component.spec',
|
|
'app/banner.component.detect-changes.spec',
|
|
'app/banner-inline.component.spec',
|
|
'app/dashboard/dashboard.component.spec',
|
|
'app/dashboard/dashboard.component.no-testbed.spec',
|
|
'app/dashboard/dashboard-hero.component.spec',
|
|
'app/hero/hero-list.component.spec',
|
|
'app/hero/hero-detail.component.spec',
|
|
'app/hero/hero-detail.component.no-testbed.spec',
|
|
'app/model/hero.spec',
|
|
'app/model/http-hero.service.spec',
|
|
'app/shared/title-case.pipe.spec',
|
|
'app/shared/twain.component.spec',
|
|
'app/welcome.component.spec'
|
|
];
|
|
</script>
|
|
<script src="browser-test-shim.js"></script>
|
|
</body>
|
|
|
|
</html>
|
|
|
|
|
|
<!--
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
-->"><input type="hidden" name="tags[0]" value="angular"><input type="hidden" name="tags[1]" value="example"><input type="hidden" name="tags[2]" value="testing"><input type="hidden" name="private" value="true"><input type="hidden" name="description" value="Angular Example - Testing - app.specs"><input type="hidden" name="files[systemjs.config.js]" value="/**
|
|
* WEB ANGULAR VERSION
|
|
* (based on systemjs.config.js in angular.io)
|
|
* System configuration for Angular samples
|
|
* Adjust as necessary for your application needs.
|
|
*/
|
|
(function (global) {
|
|
System.config({
|
|
// DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
|
|
transpiler: 'ts',
|
|
typescriptOptions: {
|
|
// Copy of compiler options in standard tsconfig.json
|
|
"target": "es5",
|
|
"module": "commonjs",
|
|
"moduleResolution": "node",
|
|
"sourceMap": true,
|
|
"emitDecoratorMetadata": true,
|
|
"experimentalDecorators": true,
|
|
"lib": ["es2015", "dom"],
|
|
"noImplicitAny": true,
|
|
"suppressImplicitAnyIndexErrors": true
|
|
},
|
|
meta: {
|
|
'typescript': {
|
|
"exports": "ts"
|
|
}
|
|
},
|
|
paths: {
|
|
// paths serve as alias
|
|
'npm:': 'https://unpkg.com/'
|
|
},
|
|
// map tells the System loader where to look for things
|
|
map: {
|
|
// our app is within the app folder
|
|
app: 'app',
|
|
|
|
// angular bundles
|
|
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
|
|
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
|
|
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
|
|
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
|
|
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
|
|
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
|
|
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
|
|
'@angular/router/upgrade': 'npm:@angular/router/bundles/router-upgrade.umd.js',
|
|
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
|
|
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
|
|
'@angular/upgrade/static': 'npm:@angular/upgrade/bundles/upgrade-static.umd.js',
|
|
|
|
// other libraries
|
|
'rxjs': 'npm:rxjs@5.0.1',
|
|
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
|
|
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
|
|
'typescript': 'npm:typescript@2.0.10/lib/typescript.js',
|
|
|
|
},
|
|
// packages tells the System loader how to load when no filename and/or no extension
|
|
packages: {
|
|
app: {
|
|
main: './main.ts',
|
|
defaultExtension: 'ts'
|
|
},
|
|
rxjs: {
|
|
defaultExtension: 'js'
|
|
}
|
|
}
|
|
});
|
|
|
|
})(this);
|
|
|
|
/*
|
|
Copyright 2016 Google Inc. All Rights Reserved.
|
|
Use of this source code is governed by an MIT-style license that
|
|
can be found in the LICENSE file at http://angular.io/license
|
|
*/
|
|
"></form><script>document.getElementById("mainForm").submit();</script></body></html> |