parent
ccd0298ec9
commit
7a20691f13
Binary file not shown.
|
@ -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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
|
@ -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"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"projectType": "testing"
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
|
@ -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>
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
/** Mock client-side authentication/authorization service */
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
getAuthorizationToken() {
|
||||||
|
return 'some-auth-token';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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 };
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"data": [
|
|
||||||
{ "id": 1, "name": "Windstorm" },
|
|
||||||
{ "id": 2, "name": "Bombasto" },
|
|
||||||
{ "id": 3, "name": "Magneta" },
|
|
||||||
{ "id": 4, "name": "Tornado" }
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface Hero {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 -->
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
});
|
|
@ -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
|
||||||
|
}
|
|
@ -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 );
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -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};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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 -->
|
|
@ -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; }
|
||||||
|
|
||||||
|
}
|
|
@ -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', []))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}.`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
*/
|
|
|
@ -1,6 +0,0 @@
|
||||||
// #docregion
|
|
||||||
export class Hero {
|
|
||||||
constructor(
|
|
||||||
public id: number,
|
|
||||||
public name: string) { }
|
|
||||||
}
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"heroesUrl": "api/heroes",
|
||||||
|
"textfile": "assets/textfile.txt"
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
This is the downloaded text 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
|
@ -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`
|
||||||
|
-->
|
|
@ -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>
|
||||||
|
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
@import "~jasmine-core/lib/jasmine-core/jasmine.css"
|
|
@ -0,0 +1,3 @@
|
||||||
|
import jasmineRequire from 'jasmine-core/lib/jasmine-core/jasmine.js';
|
||||||
|
|
||||||
|
window['jasmineRequire'] = jasmineRequire;
|
|
@ -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
|
|
@ -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
|
@ -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'
|
||||||
|
|
Loading…
Reference in New Issue