docs: add http guide sample and adjust text (#21326)

PR Close #21326
This commit is contained in:
Ward Bell 2018-01-05 01:46:55 -08:00 committed by Jason Aden
parent ccd0298ec9
commit 7a20691f13
67 changed files with 2935 additions and 1090 deletions

BIN
aio/content/examples/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -1,138 +0,0 @@
'use strict'; // necessary for es6 output in node
import { browser, element, by } from 'protractor';
describe('Server Communication', function () {
beforeAll(function () {
browser.get('');
});
describe('Tour of Heroes (Observable)', function () {
let initialHeroCount = 4;
let newHeroName = 'Mr. IQ';
let heroCountAfterAdd = 5;
let heroListComp = element(by.tagName('hero-list'));
let addButton = heroListComp.element(by.tagName('button'));
let heroTags = heroListComp.all(by.tagName('li'));
let heroNameInput = heroListComp.element(by.tagName('input'));
it('should exist', function() {
expect(heroListComp).toBeDefined('<hero-list> must exist');
});
it('should display ' + initialHeroCount + ' heroes after init', function () {
expect(heroTags.count()).toBe(initialHeroCount);
});
it('should not add hero with empty name', function () {
expect(addButton).toBeDefined('"Add Hero" button must be defined');
addButton.click().then(function() {
expect(heroTags.count()).toBe(initialHeroCount, 'No new hero should be added');
});
});
it('should add a new hero to the list', function () {
expect(heroNameInput).toBeDefined('<input> for hero name must exist');
expect(addButton).toBeDefined('"Add Hero" button must be defined');
heroNameInput.sendKeys(newHeroName);
addButton.click().then(function() {
expect(heroTags.count()).toBe(heroCountAfterAdd, 'A new hero should be added');
let newHeroInList = heroTags.get(heroCountAfterAdd - 1).getText();
expect(newHeroInList).toBe(newHeroName, 'The hero should be added to the end of the list');
});
});
});
describe('Wikipedia Demo', function () {
it('should initialize the demo with empty result list', function () {
let myWikiComp = element(by.tagName('my-wiki'));
expect(myWikiComp).toBeDefined('<my-wiki> must exist');
let resultList = myWikiComp.all(by.tagName('li'));
expect(resultList.count()).toBe(0, 'result list must be empty');
});
describe('Fetches after each keystroke', function () {
it('should fetch results after "B"', function(done: any) {
testForRefreshedResult('B', done);
});
it('should fetch results after "Ba"', function(done: any) {
testForRefreshedResult('a', done);
});
it('should fetch results after "Bas"', function(done: any) {
testForRefreshedResult('s', done);
});
it('should fetch results after "Basic"', function(done: any) {
testForRefreshedResult('ic', done);
});
});
function testForRefreshedResult(keyPressed: string, done: () => void) {
testForResult('my-wiki', keyPressed, false, done);
}
});
describe('Smarter Wikipedia Demo', function () {
it('should initialize the demo with empty result list', function () {
let myWikiSmartComp = element(by.tagName('my-wiki-smart'));
expect(myWikiSmartComp).toBeDefined('<my-wiki-smart> must exist');
let resultList = myWikiSmartComp.all(by.tagName('li'));
expect(resultList.count()).toBe(0, 'result list must be empty');
});
it('should fetch results after "Java"', function(done: any) {
testForNewResult('Java', done);
});
it('should fetch results after "JavaS"', function(done: any) {
testForStaleResult('S', done);
});
it('should fetch results after "JavaSc"', function(done: any) {
testForStaleResult('c', done);
});
it('should fetch results after "JavaScript"', function(done: any) {
testForStaleResult('ript', done);
});
function testForNewResult(keyPressed: string, done: () => void) {
testForResult('my-wiki-smart', keyPressed, false, done);
}
function testForStaleResult(keyPressed: string, done: () => void) {
testForResult('my-wiki-smart', keyPressed, true, done);
}
});
function testForResult(componentTagName: string, keyPressed: string, hasListBeforeSearch: boolean, done: () => void) {
let searchWait = 1000; // Wait for wikipedia but not so long that tests timeout
let wikiComponent = element(by.tagName(componentTagName));
expect(wikiComponent).toBeDefined('<' + componentTagName + '> must exist');
let searchBox = wikiComponent.element(by.tagName('input'));
expect(searchBox).toBeDefined('<input> for search must exist');
searchBox.sendKeys(keyPressed).then(function () {
let resultList = wikiComponent.all(by.tagName('li'));
if (hasListBeforeSearch) {
expect(resultList.count()).toBeGreaterThan(0, 'result list should not be empty before search');
}
setTimeout(function() {
expect(resultList.count()).toBeGreaterThan(0, 'result list should not be empty after search');
done();
}, searchWait);
});
}
});

View File

@ -0,0 +1,139 @@
import { browser, element, by, ElementFinder } from 'protractor';
import { resolve } from 'path';
const page = {
configClearButton: element.all(by.css('app-config > div button')).get(2),
configErrorButton: element.all(by.css('app-config > div button')).get(3),
configErrorMessage: element(by.css('app-config p')),
configGetButton: element.all(by.css('app-config > div button')).get(0),
configGetResponseButton: element.all(by.css('app-config > div button')).get(1),
configSpan: element(by.css('app-config span')),
downloadButton: element.all(by.css('app-downloader button')).get(0),
downloadClearButton: element.all(by.css('app-downloader button')).get(1),
downloadMessage: element(by.css('app-downloader p')),
heroesListAddButton: element.all(by.css('app-heroes > div button')).get(0),
heroesListInput: element(by.css('app-heroes > div input')),
heroesListSearchButton: element.all(by.css('app-heroes > div button')).get(1),
heroesListItems: element.all(by.css('app-heroes ul li')),
logClearButton: element(by.css('app-messages button')),
logList: element(by.css('app-messages ol')),
logListItems: element.all(by.css('app-messages ol li')),
searchInput: element(by.css('app-package-search input#name')),
searchListItems: element.all(by.css('app-package-search li')),
uploadInput: element(by.css('app-uploader input')),
uploadMessage: element(by.css('app-uploader p'))
};
let checkLogForMessage = (message: string) => {
expect(page.logList.getText()).toContain(message);
};
describe('Http Tests', function() {
beforeEach(() => {
browser.get('');
});
describe('Heroes', () => {
it('retrieves the list of heroes at startup', () => {
expect(page.heroesListItems.count()).toBe(4);
expect(page.heroesListItems.get(0).getText()).toContain('Mr. Nice');
checkLogForMessage('GET "api/heroes"');
});
it('makes a POST to add a new hero', () => {
page.heroesListInput.sendKeys('Magneta');
page.heroesListAddButton.click();
expect(page.heroesListItems.count()).toBe(5);
checkLogForMessage('POST "api/heroes"');
});
it('makes a GET to search for a hero', () => {
page.heroesListInput.sendKeys('Celeritas');
page.heroesListSearchButton.click();
checkLogForMessage('GET "api/heroes?name=Celeritas"');
});
});
describe('Messages', () => {
it('can clear the logs', () => {
expect(page.logListItems.count()).toBe(1);
page.logClearButton.click();
expect(page.logListItems.count()).toBe(0);
});
});
describe('Configuration', () => {
it('can fetch the configuration JSON file', () => {
page.configGetButton.click();
checkLogForMessage('GET "assets/config.json"');
expect(page.configSpan.getText()).toContain('Heroes API URL is "api/heroes"');
expect(page.configSpan.getText()).toContain('Textfile URL is "assets/textfile.txt"');
});
it('can fetch the configuration JSON file with headers', () => {
page.configGetResponseButton.click();
checkLogForMessage('GET "assets/config.json"');
expect(page.configSpan.getText()).toContain('Response headers:');
expect(page.configSpan.getText()).toContain('content-type: application/json; charset=UTF-8');
});
it('can clear the configuration log', () => {
page.configGetResponseButton.click();
expect(page.configSpan.getText()).toContain('Response headers:');
page.configClearButton.click();
expect(page.configSpan.isPresent()).toBeFalsy();
});
it('throws an error for a non valid url', () => {
page.configErrorButton.click();
checkLogForMessage('GET "not/a/real/url"');
expect(page.configErrorMessage.getText()).toContain('"Something bad happened; please try again later."');
});
});
describe('Download', () => {
it('can download a txt file and show it', () => {
page.downloadButton.click();
checkLogForMessage('DownloaderService downloaded "assets/textfile.txt"');
checkLogForMessage('GET "assets/textfile.txt"');
expect(page.downloadMessage.getText()).toContain('Contents: "This is the downloaded text file "');
});
it('can clear the log of the download', () => {
page.downloadButton.click();
expect(page.downloadMessage.getText()).toContain('Contents: "This is the downloaded text file "');
page.downloadClearButton.click();
expect(page.downloadMessage.isPresent()).toBeFalsy();
});
});
describe('Upload', () => {
it('can upload a file', () => {
const filename = 'app.po.ts';
const url = resolve(__dirname, filename);
page.uploadInput.sendKeys(url);
checkLogForMessage('POST "/upload/file" succeeded in');
expect(page.uploadMessage.getText()).toContain(
`File "${filename}" was completely uploaded!`);
});
});
describe('PackageSearch', () => {
it('can search for npm package and find in cache', () => {
const packageName = 'angular';
page.searchInput.sendKeys(packageName);
checkLogForMessage(
'Caching response from "https://npmsearch.com/query?q=angular"');
expect(page.searchListItems.count()).toBeGreaterThan(1, 'angular items');
page.searchInput.clear();
page.searchInput.sendKeys(' ');
expect(page.searchListItems.count()).toBe(0, 'search empty');
page.searchInput.clear();
page.searchInput.sendKeys(packageName);
checkLogForMessage(
'Found cached response for "https://npmsearch.com/query?q=angular"');
});
});
});

View File

@ -0,0 +1,3 @@
{
"projectType": "testing"
}

View File

@ -0,0 +1,18 @@
{
"description": "Http Guide Testing",
"files":[
"src/app/heroes/heroes.service.ts",
"src/app/heroes/heroes.service.spec.ts",
"src/app/http-error-handler.service.ts",
"src/app/message.service.ts",
"src/testing/*.ts",
"src/styles.css",
"src/test.css",
"src/main-specs.ts",
"src/index-specs.html"
],
"main": "src/index-specs.html",
"tags": ["http", "testing"]
}

View File

@ -0,0 +1,24 @@
<h1>HTTP Sample</h1>
<div>
<input type="checkbox" id="heroes" [checked]="toggleHeroes" (click)="toggleHeroes()">
<label for="heroes">Heroes</label>
<input type="checkbox" id="config" [checked]="showConfig" (click)="toggleConfig()">
<label for="config">Config</label>
<input type="checkbox" id="downloader" [checked]="showDownloader" (click)="toggleDownloader()">
<label for="downloader">Downloader</label>
<input type="checkbox" id="uploader" [checked]="showUploader" (click)="toggleUploader()">
<label for="uploader">Uploader</label>
<input type="checkbox" id="search" [checked]="showSearch" (click)="toggleSearch()">
<label for="search">Search</label>
</div>
<app-heroes *ngIf="showHeroes"></app-heroes>
<app-messages></app-messages>
<app-config *ngIf="showConfig"></app-config>
<app-downloader *ngIf="showDownloader"></app-downloader>
<app-uploader *ngIf="showUploader"></app-uploader>
<app-package-search *ngIf="showSearch"></app-package-search>

View File

@ -1,13 +1,19 @@
// #docregion
import { Component } from '@angular/core'; import { Component } from '@angular/core';
@Component({ @Component({
selector: 'my-app', selector: 'app-root',
template: ` templateUrl: './app.component.html'
<hero-list></hero-list>
<hero-list-promise></hero-list-promise>
<my-wiki></my-wiki>
<my-wiki-smart></my-wiki-smart>
`
}) })
export class AppComponent { } export class AppComponent {
showHeroes = true;
showConfig = true;
showDownloader = true;
showUploader = true;
showSearch = true;
toggleHeroes() { this.showHeroes = !this.showHeroes; }
toggleConfig() { this.showConfig = !this.showConfig; }
toggleDownloader() { this.showDownloader = !this.showDownloader; }
toggleUploader() { this.showUploader = !this.showUploader; }
toggleSearch() { this.showSearch = !this.showSearch; }
}

View File

@ -1,23 +0,0 @@
// #docregion
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
JsonpModule
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule {
}

View File

@ -1,46 +1,89 @@
// #docplaster // #docplaster
// #docregion // #docregion sketch
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
// #enddocregion sketch
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { HttpModule, JsonpModule } from '@angular/http'; // #docregion sketch
import { HttpClientModule } from '@angular/common/http';
// #enddocregion sketch
import { HttpClientXsrfModule } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { RequestCache, RequestCacheWithMap } from './request-cache.service';
import { HeroData } from './hero-data';
import { requestOptionsProvider } from './default-request-options.service';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AuthService } from './auth.service';
import { ConfigComponent } from './config/config.component';
import { DownloaderComponent } from './downloader/downloader.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HttpErrorHandler } from './http-error-handler.service';
import { MessageService } from './message.service';
import { MessagesComponent } from './messages/messages.component';
import { PackageSearchComponent } from './package-search/package-search.component';
import { UploaderComponent } from './uploader/uploader.component';
import { HeroListComponent } from './toh/hero-list.component'; import { httpInterceptorProviders } from './http-interceptors/index';
import { HeroListPromiseComponent } from './toh/hero-list.component.promise'; // #docregion sketch
import { WikiComponent } from './wiki/wiki.component';
import { WikiSmartComponent } from './wiki/wiki-smart.component';
@NgModule({ @NgModule({
// #docregion xsrf
imports: [ imports: [
// #enddocregion xsrf
BrowserModule, BrowserModule,
// #enddocregion sketch
FormsModule, FormsModule,
HttpModule, // #docregion sketch
JsonpModule, // import HttpClientModule after BrowserModule.
// #docregion in-mem-web-api // #docregion xsrf
InMemoryWebApiModule.forRoot(HeroData) HttpClientModule,
// #enddocregion in-mem-web-api // #enddocregion sketch
HttpClientXsrfModule.withOptions({
cookieName: 'My-Xsrf-Cookie',
headerName: 'My-Xsrf-Header',
}),
// #enddocregion xsrf
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, {
dataEncapsulation: false,
passThruUnknownUrl: true,
put204: false // return entity after PUT/update
}
)
// #docregion sketch, xsrf
], ],
// #enddocregion xsrf
declarations: [ declarations: [
AppComponent, AppComponent,
HeroListComponent, // #enddocregion sketch
HeroListPromiseComponent, ConfigComponent,
WikiComponent, DownloaderComponent,
WikiSmartComponent HeroesComponent,
MessagesComponent,
UploaderComponent,
PackageSearchComponent,
// #docregion sketch
], ],
// #docregion provide-default-request-options // #enddocregion sketch
providers: [ requestOptionsProvider ], // #docregion interceptor-providers
// #enddocregion provide-default-request-options providers: [
// #enddocregion interceptor-providers
AuthService,
HttpErrorHandler,
MessageService,
{ provide: RequestCache, useClass: RequestCacheWithMap },
// #docregion interceptor-providers
httpInterceptorProviders
],
// #enddocregion interceptor-providers
// #docregion sketch
bootstrap: [ AppComponent ] bootstrap: [ AppComponent ]
}) })
export class AppModule {} export class AppModule {}
// #enddocregion sketch

View File

@ -0,0 +1,9 @@
import { Injectable } from '@angular/core';
/** Mock client-side authentication/authorization service */
@Injectable()
export class AuthService {
getAuthorizationToken() {
return 'some-auth-token';
}
}

View File

@ -0,0 +1,18 @@
<h3>Get configuration from JSON file</h3>
<div>
<button (click)="clear(); showConfig()">get</button>
<button (click)="clear(); showConfigResponse()">getResponse</button>
<button (click)="clear()">clear</button>
<button (click)="clear(); makeError()">error</button>
<span *ngIf="config">
<p>Heroes API URL is "{{config.heroesUrl}}"</p>
<p>Textfile URL is "{{config.textfile}}"</p>
<div *ngIf="headers">
Response headers:
<ul>
<li *ngFor="let header of headers">{{header}}</li>
</ul>
</div>
</span>
</div>
<p *ngIf="error" class="error">{{error | json}}</p>

View File

@ -0,0 +1,78 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
import { Config, ConfigService } from './config.service';
import { MessageService } from '../message.service';
@Component({
selector: 'app-config',
templateUrl: './config.component.html',
providers: [ ConfigService ],
styles: ['.error {color: red;}']
})
export class ConfigComponent {
error: any;
headers: string[];
// #docregion v2
config: Config;
// #enddocregion v2
constructor(private configService: ConfigService) {}
clear() {
this.config = undefined;
this.error = undefined;
this.headers = undefined;
}
// #docregion v1, v2, v3
showConfig() {
this.configService.getConfig()
// #enddocregion v1, v2
.subscribe(
data => this.config = { ...data }, // success path
error => this.error = error // error path
);
}
// #enddocregion v3
showConfig_v1() {
this.configService.getConfig_1()
// #docregion v1, v1_callback
.subscribe(data => this.config = {
heroesUrl: data['heroesUrl'],
textfile: data['textfile']
});
// #enddocregion v1_callback
}
// #enddocregion v1
showConfig_v2() {
this.configService.getConfig()
// #docregion v2, v2_callback
// clone the data object, using its known Config shape
.subscribe(data => this.config = { ...data });
// #enddocregion v2_callback
}
// #enddocregion v2
// #docregion showConfigResponse
showConfigResponse() {
this.configService.getConfigResponse()
// resp is of type `HttpResponse<Config>`
.subscribe(resp => {
// display its headers
const keys = resp.headers.keys();
this.headers = keys.map(key =>
`${key}: ${resp.headers.get(key)}`);
// access the body directly, which is typed as `Config`.
this.config = { ... resp.body };
});
}
// #enddocregion showConfigResponse
makeError() {
this.configService.makeIntentionalError().subscribe(null, error => this.error = error );
}
}
// #enddocregion

View File

@ -0,0 +1,100 @@
// #docplaster
// #docregion , proto
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
// #enddocregion proto
import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
// #docregion rxjs-imports
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { catchError, retry } from 'rxjs/operators';
// #enddocregion rxjs-imports
// #docregion config-interface
export interface Config {
heroesUrl: string;
textfile: string;
}
// #enddocregion config-interface
// #docregion proto
@Injectable()
export class ConfigService {
// #enddocregion proto
// #docregion getConfig_1
configUrl = 'assets/config.json';
// #enddocregion getConfig_1
// #docregion proto
constructor(private http: HttpClient) { }
// #enddocregion proto
// #docregion getConfig, getConfig_1, getConfig_2, getConfig_3
getConfig() {
// #enddocregion getConfig_1, getConfig_2, getConfig_3
return this.http.get<Config>(this.configUrl)
.pipe(
retry(3), // retry a failed request up to 3 times
catchError(this.handleError) // then handle the error
);
}
// #enddocregion getConfig
getConfig_1() {
// #docregion getConfig_1
return this.http.get(this.configUrl);
}
// #enddocregion getConfig_1
getConfig_2() {
// #docregion getConfig_2
// now returns an Observable of Config
return this.http.get<Config>(this.configUrl);
}
// #enddocregion getConfig_2
getConfig_3() {
// #docregion getConfig_3
return this.http.get<Config>(this.configUrl)
.pipe(
catchError(this.handleError)
);
}
// #enddocregion getConfig_3
// #docregion getConfigResponse
getConfigResponse(): Observable<HttpResponse<Config>> {
return this.http.get<Config>(
this.configUrl, { observe: 'response' });
}
// #enddocregion getConfigResponse
// #docregion handleError
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// return an ErrorObservable with a user-facing error message
return new ErrorObservable(
'Something bad happened; please try again later.');
};
// #enddocregion handleError
makeIntentionalError() {
return this.http.get('not/a/real/url')
.pipe(
catchError(this.handleError)
);
}
// #docregion proto
}
// #enddocregion proto

View File

@ -1,16 +0,0 @@
// #docregion
import { Injectable } from '@angular/core';
import { BaseRequestOptions, RequestOptions } from '@angular/http';
@Injectable()
export class DefaultRequestOptions extends BaseRequestOptions {
constructor() {
super();
// Set the default 'Content-Type' header
this.headers.set('Content-Type', 'application/json');
}
}
export const requestOptionsProvider = { provide: RequestOptions, useClass: DefaultRequestOptions };

View File

@ -0,0 +1,4 @@
<h3>Download the textfile</h3>
<button (click)="download()">Download</button>
<button (click)="clear()">clear</button>
<p *ngIf="contents">Contents: "{{contents}}"</p>

View File

@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { DownloaderService } from './downloader.service';
@Component({
selector: 'app-downloader',
templateUrl: './downloader.component.html',
providers: [ DownloaderService ]
})
export class DownloaderComponent {
contents: string;
constructor(private downloaderService: DownloaderService) {}
clear() {
this.contents = undefined;
}
// #docregion download
download() {
this.downloaderService.getTextFile('assets/textfile.txt')
.subscribe(results => this.contents = results);
}
// #enddocregion download
}

View File

@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class DownloaderService {
constructor(
private http: HttpClient,
private messageService: MessageService) { }
// #docregion getTextFile
getTextFile(filename: string) {
// The Observable returned by get() is of type Observable<string>
// because a text response was specified.
// There's no need to pass a <string> type parameter to get().
return this.http.get(filename, {responseType: 'text'})
.pipe(
tap( // Log the result or error
data => this.log(filename, data),
error => this.logError(filename, error)
)
);
}
// #enddocregion getTextFile
private log(filename: string, data: string) {
const message = `DownloaderService downloaded "${filename}" and got "${data}".`;
this.messageService.add(message);
}
private logError(filename: string, error: any) {
const message = `DownloaderService failed to download "${filename}"; got error "${error.message}".`;
console.error(message);
this.messageService.add(message);
}
}

View File

@ -1,13 +0,0 @@
// #docregion
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class HeroData implements InMemoryDbService {
createDb() {
let heroes = [
{ id: 1, name: 'Windstorm' },
{ id: 2, name: 'Bombasto' },
{ id: 3, name: 'Magneta' },
{ id: 4, name: 'Tornado' }
];
return {heroes};
}
}

View File

@ -1,8 +0,0 @@
{
"data": [
{ "id": 1, "name": "Windstorm" },
{ "id": 2, "name": "Bombasto" },
{ "id": 3, "name": "Magneta" },
{ "id": 4, "name": "Tornado" }
]
}

View File

@ -0,0 +1,4 @@
export interface Hero {
id: number;
name: string;
}

View File

@ -0,0 +1,89 @@
/* HeroesComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
width: 19em;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes a {
color: #888;
text-decoration: none;
position: relative;
display: block;
width: 250px;
}
.heroes a:hover {
color:#607D8B;
}
.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;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
.button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
font-family: Arial;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: relative;
left: 24em;
top: -32px;
background-color: gray !important;
color: white;
display: inherit;
padding: 5px 8px;
width: 2em;
}
input {
font-size: 100%;
margin-bottom: 2px;
width: 11em;
}
.heroes input {
position: relative;
top: -3px;
width: 12em;
}

View File

@ -0,0 +1,32 @@
<h3>Heroes</h3>
<!-- #docregion add -->
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
<button (click)="search(heroName.value)">
search
</button>
</div>
<!-- #enddocregion add -->
<!-- #docregion list -->
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a (click)="edit(hero)">
<span class="badge">{{ hero.id || -1 }}</span>
<span *ngIf="hero!==editHero">{{hero.name}}</span>
<input *ngIf="hero===editHero" [(ngModel)]="hero.name"
(blur)="update()" (keyup.enter)="update()">
</a>
<!-- #docregion delete -->
<button class="delete" title="delete hero"
(click)="delete(hero)">x</button>
<!-- #enddocregion delete -->
</li>
</ul>
<!-- #enddocregion list -->

View File

@ -0,0 +1,76 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroesService } from './heroes.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
providers: [ HeroesService ],
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
editHero: Hero; // the hero currently being edited
constructor(private heroesService: HeroesService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroesService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
this.editHero = undefined;
name = name.trim();
if (!name) { return; }
// The server will generate the id for this new hero
const newHero: Hero = { name } as Hero;
// #docregion add-hero-subscribe
this.heroesService.addHero(newHero)
.subscribe(hero => this.heroes.push(hero));
// #enddocregion add-hero-subscribe
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
// #docregion delete-hero-subscribe
this.heroesService.deleteHero(hero.id).subscribe();
// #enddocregion delete-hero-subscribe
/*
// #docregion delete-hero-no-subscribe
// oops ... subscribe() is missing so nothing happens
this.heroesService.deleteHero(hero.id);
// #enddocregion delete-hero-no-subscribe
*/
}
edit(hero) {
this.editHero = hero;
}
search(searchTerm: string) {
this.editHero = undefined;
if (searchTerm) {
this.heroesService.searchHeroes(searchTerm)
.subscribe(heroes => this.heroes = heroes);
}
}
update() {
if (this.editHero) {
this.heroesService.updateHero(this.editHero)
.subscribe(hero => {
// replace the hero in the heroes list with update from server
const ix = hero ? this.heroes.findIndex(h => h.id === hero.id) : -1;
if (ix > -1) { this.heroes[ix] = hero; }
});
this.editHero = undefined;
}
}
}

View File

@ -0,0 +1,156 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpResponse } from '@angular/common/http';
import { Hero } from './hero';
import { HeroesService } from './heroes.service';
import { HttpErrorHandler } from '../http-error-handler.service';
import { MessageService } from '../message.service';
describe('HeroesService', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let heroService: HeroesService;
beforeEach(() => {
TestBed.configureTestingModule({
// Import the HttpClient mocking services
imports: [ HttpClientTestingModule ],
// Provide the service-under-test and its dependencies
providers: [
HeroesService,
HttpErrorHandler,
MessageService
]
});
// Inject the http, test controller, and service-under-test
// as they will be referenced by each test.
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
heroService = TestBed.get(HeroesService);
});
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
/// HeroService method tests begin ///
describe('#getHeroes', () => {
let expectedHeroes: Hero[];
beforeEach(() => {
heroService = TestBed.get(HeroesService);
expectedHeroes = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
] as Hero[];
});
it('should return expected heroes (called once)', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
// HeroService should have made one request to GET heroes from expected URL
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('GET');
// Respond with the mock heroes
req.flush(expectedHeroes);
});
it('should be OK returning no heroes', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
req.flush([]); // Respond with no heroes
});
// This service reports the error but finds a way to let the app keep going.
it('should turn 404 into an empty heroes result', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes.length).toEqual(0, 'should return empty heroes array'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
const msg = 'deliberate 404 error';
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
it('should return expected heroes (called multiple times)', () => {
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
const requests = httpTestingController.match(heroService.heroesUrl);
expect(requests.length).toEqual(3, 'calls to getHeroes()');
// Respond to each request with different mock hero results
requests[0].flush([]);
requests[1].flush([{id: 1, name: 'bob'}]);
requests[2].flush(expectedHeroes);
});
});
describe('#updateHero', () => {
// Expecting the query form of URL so should not 404 when id not found
const makeUrl = (id: number) => `${heroService.heroesUrl}/?id=${id}`;
it('should update a hero and return it', () => {
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
data => expect(data).toEqual(updateHero, 'should return the hero'),
fail
);
// HeroService should have made one request to PUT hero
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('PUT');
expect(req.request.body).toEqual(updateHero);
// Expect server to return the hero after PUT
const expectedResponse = new HttpResponse(
{ status: 200, statusText: 'OK', body: updateHero });
req.event(expectedResponse);
});
// This service reports the error but finds a way to let the app keep going.
it('should turn 404 error into return of the update hero', () => {
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
data => expect(data).toEqual(updateHero, 'should return the update hero'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
const msg = 'deliberate 404 error';
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
});
// TODO: test other HeroService methods
});

View File

@ -0,0 +1,99 @@
// #docplaster
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
// #docregion http-options
import { HttpHeaders } from '@angular/common/http';
// #enddocregion http-options
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError } from 'rxjs/operators';
import { Hero } from './hero';
import { HttpErrorHandler, HandleError } from '../http-error-handler.service';
// #docregion http-options
const httpOptions = {
headers: new HttpHeaders({
'Content-Type': 'application/json',
'Authorization': 'my-auth-token'
})
};
// #enddocregion http-options
@Injectable()
export class HeroesService {
heroesUrl = 'api/heroes'; // URL to web api
private handleError: HandleError;
constructor(
private http: HttpClient,
httpErrorHandler: HttpErrorHandler) {
this.handleError = httpErrorHandler.createHandleError('HeroesService');
}
/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
catchError(this.handleError('getHeroes', []))
);
}
// #docregion searchHeroes
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
term = term.trim();
// Add safe, URL encoded search parameter if there is a search term
const options = term ?
{ params: new HttpParams().set('name', term) } : {};
return this.http.get<Hero[]>(this.heroesUrl, options)
.pipe(
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
// #enddocregion searchHeroes
//////// Save methods //////////
// #docregion addHero
/** POST: add a new hero to the database */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions)
.pipe(
catchError(this.handleError('addHero', hero))
);
}
// #enddocregion addHero
// #docregion deleteHero
/** DELETE: delete the hero from the server */
deleteHero (id: number): Observable<{}> {
const url = `${this.heroesUrl}/${id}`; // DELETE api/heroes/42
return this.http.delete(url, httpOptions)
.pipe(
catchError(this.handleError('deleteHero'))
);
}
// #enddocregion deleteHero
// #docregion updateHero
/** PUT: update the hero on the server. Returns the updated hero upon success. */
updateHero (hero: Hero): Observable<Hero> {
// #enddocregion updateHero
// #docregion update-headers
httpOptions.headers =
httpOptions.headers.set('Authorization', 'my-new-auth-token');
// #enddocregion update-headers
// #docregion updateHero
return this.http.put<Hero>(this.heroesUrl, hero, httpOptions)
.pipe(
catchError(this.handleError('updateHero', hero))
);
}
// #enddocregion updateHero
}

View File

@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { MessageService } from './message.service';
/** Type of the handleError function returned by HttpErrorHandler.createHandleError */
export type HandleError =
<T> (operation?: string, result?: T) => (error: HttpErrorResponse) => Observable<T>;
/** Handles HttpClient errors */
@Injectable()
export class HttpErrorHandler {
constructor(private messageService: MessageService) { }
/** Create curried handleError function that already knows the service name */
createHandleError = (serviceName = '') => <T>
(operation = 'operation', result = {} as T) => this.handleError(serviceName, operation, result);
/**
* Returns a function that handles Http operation failures.
* This error handler lets the app continue to run as if no error occurred.
* @param serviceName = name of the data service that attempted the operation
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
handleError<T> (serviceName = '', operation = 'operation', result = {} as T) {
return (error: HttpErrorResponse): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
const message = (error.error instanceof ErrorEvent) ?
error.error.message :
`server returned code ${error.status} with body "${error.error}"`;
// TODO: better job of transforming error for user consumption
this.messageService.add(`${serviceName}: ${operation} failed: ${message}`);
// Let the app keep running by returning a safe result.
return of( result );
};
}
}

View File

@ -0,0 +1,42 @@
// #docplaster
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
// #docregion
import { AuthService } from '../auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// Get the auth token from the service.
const authToken = this.auth.getAuthorizationToken();
// #enddocregion
/*
* The verbose way:
// #docregion
// Clone the request and replace the original headers with
// cloned headers, updated with the authorization.
const authReq = req.clone({
headers: req.headers.set('Authorization', authToken)
});
// #enddocregion
*/
// #docregion set-header-shortcut
// Clone the request and set the new header in one step.
const authReq = req.clone({ setHeaders: { Authorization: authToken } });
// #enddocregion set-header-shortcut
// #docregion
// send cloned request with header to the next handler.
return next.handle(authReq);
}
}
// #enddocregion

View File

@ -0,0 +1,86 @@
// #docplaster
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpHeaders, HttpRequest, HttpResponse,
HttpInterceptor, HttpHandler
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { startWith, tap } from 'rxjs/operators';
import { RequestCache } from '../request-cache.service';
import { searchUrl } from '../package-search/package-search.service';
/**
* If request is cachable (e.g., package search) and
* response is in cache return the cached response as observable.
* If has 'x-refresh' header that is true,
* then also re-run the package search, using response from next(),
* returning an observable that emits the cached response first.
*
* If not in cache or not cachable,
* pass request through to next()
*/
// #docregion v1
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cache: RequestCache) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// continue if not cachable.
if (!isCachable(req)) { return next.handle(req); }
const cachedResponse = this.cache.get(req);
// #enddocregion v1
// #docregion intercept-refresh
// cache-then-refresh
if (req.headers.get('x-refresh')) {
const results$ = sendRequest(req, next, this.cache);
return cachedResponse ?
results$.pipe( startWith(cachedResponse) ) :
results$;
}
// cache-or-fetch
// #docregion v1
return cachedResponse ?
of(cachedResponse) : sendRequest(req, next, this.cache);
// #enddocregion intercept-refresh
}
}
// #enddocregion v1
/** Is this request cachable? */
function isCachable(req: HttpRequest<any>) {
// Only GET requests are cachable
return req.method === 'GET' &&
// Only npm package search is cachable in this app
-1 < req.url.indexOf(searchUrl);
}
// #docregion send-request
/**
* Get server response observable by sending request to `next()`.
* Will add the response to the cache on the way out.
*/
function sendRequest(
req: HttpRequest<any>,
next: HttpHandler,
cache: RequestCache): Observable<HttpEvent<any>> {
// No headers allowed in npm search request
const noHeaderReq = req.clone({ headers: new HttpHeaders() });
return next.handle(noHeaderReq).pipe(
tap(event => {
// There may be other events besides the response.
if (event instanceof HttpResponse) {
cache.put(req, event); // Update the cache.
}
})
);
}
// #enddocregion send-request

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class EnsureHttpsInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// #docregion excerpt
// clone request and replace 'http://' with 'https://' at the same time
const secureReq = req.clone({
url: req.url.replace('http://', 'https://')
});
// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);
// #enddocregion excerpt
}
}

View File

@ -0,0 +1,34 @@
// #docplaster
// #docregion interceptor-providers
/* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';
// #enddocregion interceptor-providers
import { AuthInterceptor } from './auth-interceptor';
import { CachingInterceptor } from './caching-interceptor';
import { EnsureHttpsInterceptor } from './ensure-https-interceptor';
import { LoggingInterceptor } from './logging-interceptor';
// #docregion interceptor-providers
import { NoopInterceptor } from './noop-interceptor';
// #enddocregion interceptor-providers
import { TrimNameInterceptor } from './trim-name-interceptor';
import { UploadInterceptor } from './upload-interceptor';
// #docregion interceptor-providers
/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
// #docregion noop-provider
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
// #enddocregion noop-provider, interceptor-providers
{ provide: HTTP_INTERCEPTORS, useClass: EnsureHttpsInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: UploadInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true },
// #docregion interceptor-providers
];
// #enddocregion interceptor-providers

View File

@ -0,0 +1,39 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler,
HttpRequest, HttpResponse
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
// #docregion excerpt
import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
constructor(private messenger: MessageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const started = Date.now();
let ok: string;
// extend server response observable with logging
return next.handle(req)
.pipe(
tap(
// Succeeds when there is a response; ignore other events
event => ok = event instanceof HttpResponse ? 'succeeded' : '',
// Operation failed; error is an HttpErrorResponse
error => ok = 'failed'
),
// Log when response observable either completes or errors
finalize(() => {
const elapsed = Date.now() - started;
const msg = `${req.method} "${req.urlWithParams}"
${ok} in ${elapsed} ms.`;
this.messenger.add(msg);
})
);
}
}
// #enddocregion excerpt

View File

@ -0,0 +1,16 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(req);
}
}

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class TrimNameInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const body = req.body;
if (!body || !body.name ) {
return next.handle(req);
}
// #docregion excerpt
// copy the body and trim whitespace from the name property
const newBody = { ...body, name: body.name.trim() };
// clone request and set its body
const newReq = req.clone({ body: newBody });
// send the cloned request to the next handler.
return next.handle(newReq);
// #enddocregion excerpt
}
}

View File

@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler,
HttpRequest, HttpResponse,
HttpEventType, HttpProgressEvent
} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
/** Simulate server replying to file upload request */
@Injectable()
export class UploadInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.indexOf('/upload/file') === -1) {
return next.handle(req);
}
const delay = 300; // Todo: inject delay?
return createUploadEvents(delay);
}
}
/** Create simulation of upload event stream */
function createUploadEvents(delay: number) {
// Simulate XHR behavior which would provide this information in a ProgressEvent
const chunks = 5;
const total = 12345678;
const chunkSize = Math.ceil(total / chunks);
return new Observable<HttpEvent<any>>(observer => {
// notify the event stream that the request was sent.
observer.next({type: HttpEventType.Sent});
uploadLoop(0);
function uploadLoop(loaded: number) {
// N.B.: Cannot use setInterval or rxjs delay (which uses setInterval)
// because e2e test won't complete. A zone thing?
// Use setTimeout and tail recursion instead.
setTimeout(() => {
loaded += chunkSize;
if (loaded >= total) {
const doneResponse = new HttpResponse({
status: 201, // OK but no body;
});
observer.next(doneResponse);
observer.complete();
return;
}
const progressEvent: HttpProgressEvent = {
type: HttpEventType.UploadProgress,
loaded,
total
};
observer.next(progressEvent);
uploadLoop(loaded);
}, delay);
}
});
}

View File

@ -0,0 +1,13 @@
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
];
return {heroes};
}
}

View File

@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}

View File

@ -0,0 +1,8 @@
<div *ngIf="messageService.messages.length">
<h3>Messages</h3>
<button class="clear" (click)="messageService.clear()">clear</button>
<br>
<ol>
<li *ngFor='let message of messageService.messages'> {{message}} </li>
</ol>
</div>

View File

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { MessageService } from '../message.service';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html'
})
export class MessagesComponent {
constructor(public messageService: MessageService) {}
}

View File

@ -0,0 +1,17 @@
<!-- #docplaster -->
<h3>Search Npm Packages</h3>
<p><i>Searches when typing stops. Caches for 30 seconds.</i></p>
<!-- #docregion search -->
<input (keyup)="search($event.target.value)" id="name" placeholder="Search"/>
<!-- #enddocregion search -->
<input type="checkbox" id="refresh" [checked]="withRefresh" (click)="toggleRefresh()">
<label for="refresh">with refresh</label>
<!-- #docregion search -->
<ul>
<li *ngFor="let package of packages$ | async">
<b>{{package.name}} v.{{package.version}}</b> -
<i>{{package.description}}</i>
</li>
</ul>
<!-- #enddocregion search -->

View File

@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { NpmPackageInfo, PackageSearchService } from './package-search.service';
@Component({
selector: 'app-package-search',
templateUrl: './package-search.component.html',
providers: [ PackageSearchService ]
})
export class PackageSearchComponent implements OnInit {
// #docregion debounce
withRefresh = false;
packages$: Observable<NpmPackageInfo[]>;
private searchText$ = new Subject<string>();
search(packageName: string) {
this.searchText$.next(packageName);
}
ngOnInit() {
this.packages$ = this.searchText$.pipe(
debounceTime(500),
distinctUntilChanged(),
switchMap(packageName =>
this.searchService.search(packageName, this.withRefresh))
);
}
constructor(private searchService: PackageSearchService) { }
// #enddocregion debounce
toggleRefresh() { this.withRefresh = ! this.withRefresh; }
}

View File

@ -0,0 +1,62 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, map } from 'rxjs/operators';
import { HttpErrorHandler, HandleError } from '../http-error-handler.service';
export interface NpmPackageInfo {
name: string;
version: string;
description: string;
}
export const searchUrl = 'https://npmsearch.com/query';
const httpOptions = {
headers: new HttpHeaders({
'x-refresh': 'true'
})
};
function createHttpOptions(packageName: string, refresh = false) {
// npm package name search api
// e.g., http://npmsearch.com/query?q=dom'
const params = new HttpParams({ fromObject: { q: packageName } });
const headerMap = refresh ? {'x-refresh': 'true'} : {};
const headers = new HttpHeaders(headerMap) ;
return { headers, params };
}
@Injectable()
export class PackageSearchService {
private handleError: HandleError;
constructor(
private http: HttpClient,
httpErrorHandler: HttpErrorHandler) {
this.handleError = httpErrorHandler.createHandleError('HeroesService');
}
search (packageName: string, refresh = false): Observable<NpmPackageInfo[]> {
// clear if no pkg name
if (!packageName.trim()) { return of([]); }
const options = createHttpOptions(packageName, refresh);
// TODO: Add error handling
return this.http.get(searchUrl, options).pipe(
map((data: any) => {
return data.results.map(entry => ({
name: entry.name[0],
version: entry.version[0],
description: entry.description[0]
} as NpmPackageInfo )
);
}),
catchError(this.handleError('search', []))
);
}
}

View File

@ -0,0 +1,60 @@
import { Injectable } from '@angular/core';
import { HttpRequest, HttpResponse } from '@angular/common/http';
import { MessageService } from './message.service';
export interface RequestCacheEntry {
url: string;
response: HttpResponse<any>;
lastRead: number;
}
// #docregion request-cache
export abstract class RequestCache {
abstract get(req: HttpRequest<any>): HttpResponse<any> | undefined;
abstract put(req: HttpRequest<any>, response: HttpResponse<any>): void
}
// #enddocregion request-cache
const maxAge = 30000; // maximum cache age (ms)
@Injectable()
export class RequestCacheWithMap implements RequestCache {
cache = new Map<string, RequestCacheEntry>();
constructor(private messenger: MessageService) { }
get(req: HttpRequest<any>): HttpResponse<any> | undefined {
const url = req.urlWithParams;
const cached = this.cache.get(url);
if (!cached) {
return undefined;
}
const isExpired = cached.lastRead < (Date.now() - maxAge);
const expired = isExpired ? 'expired ' : '';
this.messenger.add(
`Found ${expired}cached response for "${url}".`);
return isExpired ? undefined : cached.response;
}
put(req: HttpRequest<any>, response: HttpResponse<any>): void {
const url = req.urlWithParams;
this.messenger.add(`Caching response from "${url}".`);
const entry = { url, response, lastRead: Date.now() };
this.cache.set(url, entry);
// remove expired cache entries
const expired = Date.now() - maxAge;
this.cache.forEach(entry => {
if (entry.lastRead < expired) {
this.cache.delete(entry.url);
}
});
this.messenger.add(`Request cache size: ${this.cache.size}.`);
}
}

View File

@ -1,11 +0,0 @@
<!-- #docregion -->
<h1>Tour of Heroes ({{mode}})</h1>
<h3>Heroes:</h3>
<ul>
<li *ngFor="let hero of heroes">{{hero.name}}</li>
</ul>
<label>New hero name: <input #newHeroName /></label>
<button (click)="addHero(newHeroName.value); newHeroName.value=''">Add Hero</button>
<p class="error" *ngIf="errorMessage">{{errorMessage}}</p>

View File

@ -1,40 +0,0 @@
// #docregion
// Promise Version
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service.promise';
@Component({
selector: 'hero-list-promise',
templateUrl: './hero-list.component.html',
providers: [ HeroService ],
styles: ['.error {color:red;}']
})
// #docregion component
export class HeroListPromiseComponent implements OnInit {
errorMessage: string;
heroes: Hero[];
mode = 'Promise';
constructor (private heroService: HeroService) {}
ngOnInit() { this.getHeroes(); }
// #docregion methods
getHeroes() {
this.heroService.getHeroes()
.then(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) { return; }
this.heroService.addHero(name)
.then(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
// #enddocregion methods
}
// #enddocregion component

View File

@ -1,44 +0,0 @@
// #docregion
// Observable Version
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ],
styles: ['.error {color:red;}']
})
// #docregion component
export class HeroListComponent implements OnInit {
errorMessage: string;
heroes: Hero[];
mode = 'Observable';
constructor (private heroService: HeroService) {}
ngOnInit() { this.getHeroes(); }
// #docregion methods
// #docregion getHeroes
getHeroes() {
this.heroService.getHeroes()
.subscribe(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
// #enddocregion getHeroes
// #docregion addHero
addHero(name: string) {
if (!name) { return; }
this.heroService.create(name)
.subscribe(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}
// #enddocregion addHero
// #enddocregion methods
}
// #enddocregion component

View File

@ -1,60 +0,0 @@
// #docplaster
// #docregion
// Promise Version
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Headers, RequestOptions } from '@angular/http';
// #docregion rxjs-imports
import 'rxjs/add/operator/toPromise';
// #enddocregion rxjs-imports
import { Hero } from './hero';
@Injectable()
export class HeroService {
// URL to web api
private heroesUrl = 'app/heroes';
constructor (private http: Http) {}
// #docregion methods
getHeroes (): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
addHero (name: string): Promise<Hero> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Promise.reject(errMsg);
}
// #enddocregion methods
}
// #enddocregion

View File

@ -1,80 +0,0 @@
// #docplaster
// #docregion
// Observable Version
// #docregion v1
import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
// #enddocregion v1
// #docregion import-request-options
import { Headers, RequestOptions } from '@angular/http';
// #enddocregion import-request-options
// #docregion v1
// #docregion rxjs-imports
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
// #enddocregion rxjs-imports
import { Hero } from './hero';
@Injectable()
export class HeroService {
// #docregion endpoint
private heroesUrl = 'api/heroes'; // URL to web API
// #enddocregion endpoint
// #docregion ctor
constructor (private http: Http) {}
// #enddocregion ctor
// #docregion methods, error-handling, http-get
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
// #enddocregion error-handling, http-get, v1
// #docregion create, create-sig
create(name: string): Observable<Hero> {
// #enddocregion create-sig
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.map(this.extractData)
.catch(this.handleError);
}
// #enddocregion create
// #docregion v1, extract-data
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
// #enddocregion extract-data
// #docregion error-handling
private handleError (error: Response | any) {
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
// #enddocregion error-handling, methods
}
// #enddocregion
/*
// #docregion endpoint-json
private heroesUrl = 'app/heroes.json'; // URL to JSON file
// #enddocregion endpoint-json
*/

View File

@ -1,6 +0,0 @@
// #docregion
export class Hero {
constructor(
public id: number,
public name: string) { }
}

View File

@ -0,0 +1,12 @@
<h3>Upload file</h3>
<form enctype="multipart/form-data" method="post">
<div>
<label for="picked">Choose file to upload</label>
<div>
<input type="file" id="picked" #picked
(click)="message=''"
(change)="onPicked(picked)">
</div>
</div>
<p *ngIf="message">{{message}}</p>
</form>

View File

@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import { UploaderService } from './uploader.service';
@Component({
selector: 'app-uploader',
templateUrl: './uploader.component.html',
providers: [ UploaderService ]
})
export class UploaderComponent {
message: string;
constructor(private uploaderService: UploaderService) {}
onPicked(input: HTMLInputElement) {
const file = input.files[0];
if (file) {
this.uploaderService.upload(file).subscribe(
msg => {
input.value = null;
this.message = msg;
}
);
}
}
}

View File

@ -0,0 +1,105 @@
import { Injectable } from '@angular/core';
import {
HttpClient, HttpEvent, HttpEventType, HttpProgressEvent,
HttpRequest, HttpResponse, HttpErrorResponse
} from '@angular/common/http';
import { of } from 'rxjs/observable/of';
import { catchError, last, map, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class UploaderService {
constructor(
private http: HttpClient,
private messenger: MessageService) {}
// If uploading multiple files, change to:
// upload(files: FileList) {
// const formData = new FormData();
// files.forEach(f => formData.append(f.name, f));
// new HttpRequest('POST', '/upload/file', formData, {reportProgress: true});
// ...
// }
upload(file: File) {
if (!file) { return; }
// COULD HAVE WRITTEN:
// return this.http.post('/upload/file', file, {
// reportProgress: true,
// observe: 'events'
// }).pipe(
// Create the request object that POSTs the file to an upload endpoint.
// The `reportProgress` option tells HttpClient to listen and return
// XHR progress events.
// #docregion upload-request
const req = new HttpRequest('POST', '/upload/file', file, {
reportProgress: true
});
// #enddocregion upload-request
// #docregion upload-body
// The `HttpClient.request` API produces a raw event stream
// which includes start (sent), progress, and response events.
return this.http.request(req).pipe(
map(event => this.getEventMessage(event, file)),
tap(message => this.showProgress(message)),
last(), // return last (completed) message to caller
catchError(this.handleError(file))
);
// #enddocregion upload-body
}
// #docregion getEventMessage
/** Return distinct message for sent, upload progress, & response events */
private getEventMessage(event: HttpEvent<any>, file: File) {
switch (event.type) {
case HttpEventType.Sent:
return `Uploading file "${file.name}" of size ${file.size}.`;
case HttpEventType.UploadProgress:
// Compute and show the % done:
const percentDone = Math.round(100 * event.loaded / event.total);
return `File "${file.name}" is ${percentDone}% uploaded.`;
case HttpEventType.Response:
return `File "${file.name}" was completely uploaded!`;
default:
return `File "${file.name}" surprising upload event: ${event.type}.`;
}
}
// #enddocregion getEventMessage
/**
* Returns a function that handles Http upload failures.
* @param file - File object for file being uploaded
*
* When no `UploadInterceptor` and no server,
* you'll end up here in the error handler.
*/
private handleError(file: File) {
const userMessage = `${file.name} upload failed.`;
return (error: HttpErrorResponse) => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
const message = (error.error instanceof Error) ?
error.error.message :
`server returned code ${error.status} with body "${error.error}"`;
this.messenger.add(`${userMessage} ${message}`);
// Let app keep running but indicate failure.
return of(userMessage);
};
}
private showProgress(message: string) {
this.messenger.add(message);
}
}

View File

@ -1,47 +0,0 @@
/* tslint:disable: member-ordering forin */
// #docplaster
// #docregion
import { Component, OnInit } from '@angular/core';
// #docregion rxjs-imports
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import 'rxjs/add/operator/switchMap';
// #docregion import-subject
import { Subject } from 'rxjs/Subject';
// #enddocregion import-subject
import { WikipediaService } from './wikipedia.service';
@Component({
selector: 'my-wiki-smart',
template: `
<h1>Smarter Wikipedia Demo</h1>
<p>Search when typing stops</p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`,
providers: [ WikipediaService ]
})
export class WikiSmartComponent implements OnInit {
items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) {}
// #docregion subject
private searchTermStream = new Subject<string>();
search(term: string) { this.searchTermStream.next(term); }
// #enddocregion subject
ngOnInit() {
// #docregion observable-operators
this.items = this.searchTermStream
.debounceTime(300)
.distinctUntilChanged()
.switchMap((term: string) => this.wikipediaService.search(term));
// #enddocregion observable-operators
}
}

View File

@ -1,26 +0,0 @@
// #docregion
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { WikipediaService } from './wikipedia.service';
@Component({
selector: 'my-wiki',
template: `
<h1>Wikipedia Demo</h1>
<p>Search after each keystroke</p>
<input #term (keyup)="search(term.value)"/>
<ul>
<li *ngFor="let item of items | async">{{item}}</li>
</ul>`,
providers: [ WikipediaService ]
})
export class WikiComponent {
items: Observable<string[]>;
constructor (private wikipediaService: WikipediaService) { }
search (term: string) {
this.items = this.wikipediaService.search(term);
}
}

View File

@ -1,26 +0,0 @@
// Create the query string by hand
// #docregion
import { Injectable } from '@angular/core';
import { Jsonp } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) { }
// TODO: Add error handling
search(term: string) {
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
// #docregion query-string
let queryString =
`?search=${term}&action=opensearch&format=json&callback=JSONP_CALLBACK`;
return this.jsonp
.get(wikiUrl + queryString)
.map(response => <string[]> response.json()[1]);
// #enddocregion query-string
}
}

View File

@ -1,30 +0,0 @@
// #docregion
import { Injectable } from '@angular/core';
import { Jsonp, URLSearchParams } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class WikipediaService {
constructor(private jsonp: Jsonp) {}
search (term: string) {
let wikiUrl = 'http://en.wikipedia.org/w/api.php';
// #docregion search-parameters
let params = new URLSearchParams();
params.set('search', term); // the user's search value
params.set('action', 'opensearch');
params.set('format', 'json');
params.set('callback', 'JSONP_CALLBACK');
// #enddocregion search-parameters
// #docregion call-jsonp
// TODO: Add error handling
return this.jsonp
.get(wikiUrl, { search: params })
.map(response => <string[]> response.json()[1]);
// #enddocregion call-jsonp
}
}

View File

@ -0,0 +1,4 @@
{
"heroesUrl": "api/heroes",
"textfile": "assets/textfile.txt"
}

View File

@ -0,0 +1 @@
This is the downloaded text file

View File

@ -0,0 +1,88 @@
// BROWSER TESTING SHIM
// Keep it in-sync with what karma-test-shim does
// #docregion
/*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/common/http/testing': 'npm:@angular/common/bundles/common-http-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) // not in this project
.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();
}
}
})();

View File

@ -0,0 +1,4 @@
<!--
Intentionally empty placeholder for Stackblitz.
Do not need index.html in zip-download either as you should run tests with `npm test`
-->

View File

@ -1,27 +1,14 @@
<!DOCTYPE html> <!doctype html>
<!-- #docregion -->
<html lang="en"> <html lang="en">
<head> <head>
<title>Angular Http Demo</title> <meta charset="utf-8">
<title>HttpClient Demo</title>
<base href="/"> <base href="/">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css"> <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<!-- Polyfills --> <body>
<script src="node_modules/core-js/client/shim.min.js"></script> <app-root></app-root>
</body>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app></my-app>
</body>
</html> </html>

View File

@ -0,0 +1,44 @@
import './testing/global-jasmine';
import 'jasmine-core/lib/jasmine-core/jasmine-html.js';
import 'jasmine-core/lib/jasmine-core/boot.js';
declare var jasmine;
import './polyfills';
import 'zone.js/dist/async-test';
import 'zone.js/dist/fake-async-test';
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/proxy.js';
import 'zone.js/dist/sync-test';
import 'zone.js/dist/jasmine-patch';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
// Import spec files individually for Stackblitz
import './app/heroes/heroes.service.spec.ts';
import './testing/http-client.spec.ts';
//
bootstrap();
//
function bootstrap () {
if (window['jasmineRef']) {
location.reload();
return;
} else {
window.onload(undefined);
window['jasmineRef'] = jasmine.getEnv();
}
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
}

View File

@ -0,0 +1 @@
@import "~jasmine-core/lib/jasmine-core/jasmine.css"

View File

@ -0,0 +1,3 @@
import jasmineRequire from 'jasmine-core/lib/jasmine-core/jasmine.js';
window['jasmineRequire'] = jasmineRequire;

View File

@ -0,0 +1,192 @@
// #docplaster
// #docregion imports
// Http testing module and mocking controller
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
// #enddocregion imports
import { HttpHeaders } from '@angular/common/http';
interface Data {
name: string;
}
const testUrl = '/data';
// #docregion setup
describe('HttpClient testing', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
// Inject the http service and test controller for each test
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
});
// #enddocregion setup
// #docregion afterEach
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
// #enddocregion afterEach
// #docregion setup
/// Tests begin ///
// #enddocregion setup
// #docregion get-test
it('can test HttpClient.get', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request
httpClient.get<Data>(testUrl)
.subscribe(data =>
// When observable resolves, result should match test data
expect(data).toEqual(testData)
);
// The following `expectOne()` will match the request's URL.
// If no requests or multiple requests matched that URL
// `expectOne()` would throw.
const req = httpTestingController.expectOne('/data');
// Assert that the request is a GET.
expect(req.request.method).toEqual('GET');
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush(testData);
// Finally, assert that there are no outstanding requests.
httpTestingController.verify();
});
// #enddocregion get-test
it('can test HttpClient.get with matching header', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request with specific header
httpClient.get<Data>(testUrl, {
headers: new HttpHeaders({'Authorization': 'my-auth-token'})
})
.subscribe(data =>
expect(data).toEqual(testData)
);
// Find request with a predicate function.
// #docregion predicate
// Expect one request with an authorization header
const req = httpTestingController.expectOne(
req => req.headers.has('Authorization')
);
// #enddocregion predicate
req.flush(testData);
});
it('can test multiple requests', () => {
let testData: Data[] = [
{ name: 'bob' }, { name: 'carol' },
{ name: 'ted' }, { name: 'alice' }
];
// Make three requests in a row
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
// #docregion multi-request
// get all pending requests that match the given URL
const requests = httpTestingController.match(testUrl);
expect(requests.length).toEqual(3);
// Respond to each request with different results
requests[0].flush([]);
requests[1].flush([testData[0]]);
requests[2].flush(testData);
// #enddocregion multi-request
});
// #docregion 404
it('can test for 404 error', () => {
const emsg = 'deliberate 404 error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the 404 error'),
(error: HttpErrorResponse) => {
expect(error.status).toEqual(404, 'status');
expect(error.error).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Respond with mock error
req.flush(emsg, { status: 404, statusText: 'Not Found' });
});
// #enddocregion 404
// #docregion network-error
it('can test for network error', () => {
const emsg = 'simulated network error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the network error'),
(error: HttpErrorResponse) => {
expect(error.error.message).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Create mock ErrorEvent, raised when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc
const errorEvent = new ErrorEvent('so sad', {
message: emsg,
// #enddocregion network-error
// The rest of this is optional and not used.
// Just showing that you could provide this too.
filename: 'HeroService.ts',
lineno: 42,
colno: 21
// #docregion network-error
});
// Respond with mock error
req.error(errorEvent);
});
// #enddocregion network-error
it('httpTestingController.verify should fail if HTTP response not simulated', () => {
// Sends request
httpClient.get('some/api').subscribe();
// verify() should fail because haven't handled the pending request.
expect(() => httpTestingController.verify()).toThrow();
// Now get and flush the request so that afterEach() doesn't fail
const req = httpTestingController.expectOne('some/api');
req.flush(null);
});
// Proves that verify in afterEach() really would catch error
// if test doesn't simulate the HTTP response.
//
// Must disable this test because can't catch an error in an afterEach().
// Uncomment if you want to confirm that afterEach() does the job.
// it('afterEach() should fail when HTTP response not simulated',() => {
// // Sends request which is never handled by this test
// httpClient.get('some/api').subscribe();
// });
// #docregion setup
});
// #enddocregion setup

View File

@ -3,7 +3,9 @@
"files":[ "files":[
"!**/*.d.ts", "!**/*.d.ts",
"!**/*.js", "!**/*.js",
"!**/*.[1].*"
"!src/testing/*.*",
"!src/index-specs.html"
], ],
"tags": ["http", "jsonp"] "tags": ["http"]
} }

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,6 @@ const CLI_SPEC_FILENAME = 'e2e/app.e2e-spec.ts';
const EXAMPLE_CONFIG_FILENAME = 'example-config.json'; const EXAMPLE_CONFIG_FILENAME = 'example-config.json';
const IGNORED_EXAMPLES = [ // temporary ignores const IGNORED_EXAMPLES = [ // temporary ignores
'quickstart', 'quickstart',
'http',
'setup', 'setup',
'webpack', 'webpack',
'upgrade-p' 'upgrade-p'