@@ -1,4 +1,5 @@
|
|
1 |
import { NgModule } from '@angular/core';
|
|
|
2 |
import { BrowserModule } from '@angular/platform-browser';
|
3 |
|
4 |
import { AppRoutingModule } from './app-routing.module';
|
@@ -14,6 +15,7 @@ import { HomeComponent } from './home/home.component';
|
|
14 |
imports: [
|
15 |
BrowserModule,
|
16 |
AppRoutingModule,
|
|
|
17 |
BooksModule
|
18 |
],
|
19 |
providers: [],
|
1 |
import { NgModule } from '@angular/core';
|
2 |
+
import { HttpClientModule } from '@angular/common/http';
|
3 |
import { BrowserModule } from '@angular/platform-browser';
|
4 |
|
5 |
import { AppRoutingModule } from './app-routing.module';
|
15 |
imports: [
|
16 |
BrowserModule,
|
17 |
AppRoutingModule,
|
18 |
+
HttpClientModule,
|
19 |
BooksModule
|
20 |
],
|
21 |
providers: [],
|
@@ -23,4 +23,7 @@
|
|
23 |
[src]="book.thumbnailUrl"
|
24 |
alt="Cover">
|
25 |
<a class="button arrow-left" routerLink="..">Back to list</a>
|
|
|
|
|
|
|
26 |
</div>
|
23 |
[src]="book.thumbnailUrl"
|
24 |
alt="Cover">
|
25 |
<a class="button arrow-left" routerLink="..">Back to list</a>
|
26 |
+
<button class="red" (click)="removeBook(book.isbn)">
|
27 |
+
Remove book
|
28 |
+
</button>
|
29 |
</div>
|
@@ -1,5 +1,5 @@
|
|
1 |
import { Component } from '@angular/core';
|
2 |
-
import { ActivatedRoute } from '@angular/router';
|
3 |
|
4 |
import { BookStoreService } from '../../shared/book-store.service';
|
5 |
import { Book } from '../../shared/book';
|
@@ -14,8 +14,19 @@ export class BookDetailsComponent {
|
|
14 |
constructor(
|
15 |
private service: BookStoreService,
|
16 |
private route: ActivatedRoute,
|
|
|
17 |
) {
|
18 |
const isbn = this.route.snapshot.paramMap.get('isbn')!;
|
19 |
-
this.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
}
|
1 |
import { Component } from '@angular/core';
|
2 |
+
import { ActivatedRoute, Router } from '@angular/router';
|
3 |
|
4 |
import { BookStoreService } from '../../shared/book-store.service';
|
5 |
import { Book } from '../../shared/book';
|
14 |
constructor(
|
15 |
private service: BookStoreService,
|
16 |
private route: ActivatedRoute,
|
17 |
+
private router: Router
|
18 |
) {
|
19 |
const isbn = this.route.snapshot.paramMap.get('isbn')!;
|
20 |
+
this.service.getSingle(isbn).subscribe(book => {
|
21 |
+
this.book = book;
|
22 |
+
});
|
23 |
+
}
|
24 |
+
|
25 |
+
removeBook(isbn: string) {
|
26 |
+
if (window.confirm('Remove book?')) {
|
27 |
+
this.service.remove(isbn).subscribe(() => {
|
28 |
+
this.router.navigateByUrl('/books');
|
29 |
+
});
|
30 |
+
}
|
31 |
}
|
32 |
}
|
@@ -1,23 +0,0 @@
|
|
1 |
-
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
-
|
3 |
-
import { BookListComponent } from './book-list.component';
|
4 |
-
|
5 |
-
describe('BookListComponent', () => {
|
6 |
-
let component: BookListComponent;
|
7 |
-
let fixture: ComponentFixture<BookListComponent>;
|
8 |
-
|
9 |
-
beforeEach(async () => {
|
10 |
-
await TestBed.configureTestingModule({
|
11 |
-
declarations: [ BookListComponent ]
|
12 |
-
})
|
13 |
-
.compileComponents();
|
14 |
-
|
15 |
-
fixture = TestBed.createComponent(BookListComponent);
|
16 |
-
component = fixture.componentInstance;
|
17 |
-
fixture.detectChanges();
|
18 |
-
});
|
19 |
-
|
20 |
-
it('should create', () => {
|
21 |
-
expect(component).toBeTruthy();
|
22 |
-
});
|
23 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -12,6 +12,8 @@ export class BookListComponent {
|
|
12 |
books: Book[] = [];
|
13 |
|
14 |
constructor(private service: BookStoreService) {
|
15 |
-
this.
|
|
|
|
|
16 |
}
|
17 |
}
|
12 |
books: Book[] = [];
|
13 |
|
14 |
constructor(private service: BookStoreService) {
|
15 |
+
this.service.getAll().subscribe(books => {
|
16 |
+
this.books = books;
|
17 |
+
});
|
18 |
}
|
19 |
}
|
@@ -1,42 +1,23 @@
|
|
1 |
-
import { Location } from '@angular/common';
|
2 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
3 |
-
import { RouterTestingModule } from '@angular/router/testing';
|
4 |
|
5 |
import { BookListItemComponent } from './book-list-item.component';
|
6 |
|
7 |
describe('BookListItemComponent', () => {
|
8 |
let component: BookListItemComponent;
|
9 |
let fixture: ComponentFixture<BookListItemComponent>;
|
10 |
-
let location: Location;
|
11 |
|
12 |
beforeEach(async () => {
|
13 |
await TestBed.configureTestingModule({
|
14 |
-
declarations: [BookListItemComponent]
|
15 |
-
imports: [
|
16 |
-
RouterTestingModule.withRoutes([
|
17 |
-
{ path: ':isbn', children: [] }
|
18 |
-
])
|
19 |
-
]
|
20 |
})
|
21 |
.compileComponents();
|
22 |
|
23 |
-
location = TestBed.inject(Location);
|
24 |
fixture = TestBed.createComponent(BookListItemComponent);
|
25 |
component = fixture.componentInstance;
|
26 |
-
// Buch zum Anzeigen
|
27 |
-
component.book = {
|
28 |
-
isbn: '111',
|
29 |
-
title: 'Book 1',
|
30 |
-
authors: [],
|
31 |
-
thumbnailUrl: 'https://cdn.ng-buch.de/kochen.png'
|
32 |
-
};
|
33 |
fixture.detectChanges();
|
34 |
});
|
35 |
|
36 |
-
it('should
|
37 |
-
|
38 |
-
fixture.nativeElement.querySelector('img').click();
|
39 |
-
await fixture.whenStable();
|
40 |
-
expect(location.path()).toEqual('/111');
|
41 |
});
|
42 |
});
|
|
|
1 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
|
2 |
|
3 |
import { BookListItemComponent } from './book-list-item.component';
|
4 |
|
5 |
describe('BookListItemComponent', () => {
|
6 |
let component: BookListItemComponent;
|
7 |
let fixture: ComponentFixture<BookListItemComponent>;
|
|
|
8 |
|
9 |
beforeEach(async () => {
|
10 |
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookListItemComponent ]
|
|
|
|
|
|
|
|
|
|
|
12 |
})
|
13 |
.compileComponents();
|
14 |
|
|
|
15 |
fixture = TestBed.createComponent(BookListItemComponent);
|
16 |
component = fixture.componentInstance;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
fixture.detectChanges();
|
18 |
});
|
19 |
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
|
|
|
|
|
|
22 |
});
|
23 |
});
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
3 |
+
|
4 |
+
import { Book } from './book';
|
5 |
+
import { BookStoreService } from './book-store.service';
|
6 |
+
import { HttpErrorResponse } from '@angular/common/http';
|
7 |
+
|
8 |
+
describe('BookStoreService', () => {
|
9 |
+
let service: BookStoreService;
|
10 |
+
let httpMock: HttpTestingController;
|
11 |
+
const expectedBooks = [
|
12 |
+
{ isbn: '111', title: 'Book 1', authors: [] },
|
13 |
+
{ isbn: '222', title: 'Book 2', authors: [] }
|
14 |
+
];
|
15 |
+
|
16 |
+
beforeEach(() => {
|
17 |
+
TestBed.configureTestingModule({
|
18 |
+
imports: [HttpClientTestingModule]
|
19 |
+
});
|
20 |
+
|
21 |
+
service = TestBed.inject(BookStoreService);
|
22 |
+
httpMock = TestBed.inject(HttpTestingController);
|
23 |
+
});
|
24 |
+
|
25 |
+
it('should GET a list of all books', () => {
|
26 |
+
|
27 |
+
let receivedBooks!: Book[];
|
28 |
+
service.getAll().subscribe(books => receivedBooks = books);
|
29 |
+
|
30 |
+
// Request aus der Warteschlange holen
|
31 |
+
const req = httpMock.expectOne(
|
32 |
+
'https://api5.angular-buch.com/books');
|
33 |
+
expect(req.request.method).toEqual('GET');
|
34 |
+
|
35 |
+
// flush -- jetzt werden die Bücher emittiert
|
36 |
+
req.flush(expectedBooks);
|
37 |
+
|
38 |
+
expect(receivedBooks).toHaveSize(2);
|
39 |
+
expect(receivedBooks[0].isbn).toBe('111');
|
40 |
+
expect(receivedBooks[1].isbn).toBe('222');
|
41 |
+
});
|
42 |
+
|
43 |
+
it('should throw a HttpErrorResponse on server errors', () => {
|
44 |
+
|
45 |
+
let errorResponse!: HttpErrorResponse;
|
46 |
+
|
47 |
+
service.getAll().subscribe({
|
48 |
+
error: (error: any) => errorResponse = error
|
49 |
+
});
|
50 |
+
|
51 |
+
const req = httpMock.expectOne('https://api5.angular-buch.com/books');
|
52 |
+
|
53 |
+
req.flush('Fehler!', {
|
54 |
+
status: 500,
|
55 |
+
statusText: 'Internal Server Error'
|
56 |
+
});
|
57 |
+
|
58 |
+
expect(errorResponse.status).toBe(500);
|
59 |
+
expect(errorResponse.statusText).toBe('Internal Server Error');
|
60 |
+
});
|
61 |
+
|
62 |
+
afterEach(() => {
|
63 |
+
// prüfen, ob kein Request übrig geblieben ist
|
64 |
+
httpMock.verify();
|
65 |
+
});
|
66 |
+
});
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { HttpClient } from '@angular/common/http';
|
2 |
+
import { TestBed } from '@angular/core/testing';
|
3 |
+
import { Observable, of } from 'rxjs';
|
4 |
+
|
5 |
+
import { Book } from '../shared/book';
|
6 |
+
import { BookStoreService } from './book-store.service';
|
7 |
+
|
8 |
+
describe('BookStoreService', () => {
|
9 |
+
let service: BookStoreService;
|
10 |
+
let httpMock: {
|
11 |
+
get: (url: string) => Observable<Book[]>;
|
12 |
+
};
|
13 |
+
|
14 |
+
beforeEach(() => {
|
15 |
+
|
16 |
+
const expectedBooks: Book[] = [
|
17 |
+
{ isbn: '111', title: 'Book 1', authors: [] },
|
18 |
+
{ isbn: '222', title: 'Book 2', authors: [] }
|
19 |
+
];
|
20 |
+
|
21 |
+
httpMock = {
|
22 |
+
get: () => of(expectedBooks)
|
23 |
+
};
|
24 |
+
|
25 |
+
spyOn(httpMock, 'get').and.callThrough();
|
26 |
+
|
27 |
+
TestBed.configureTestingModule({
|
28 |
+
providers: [
|
29 |
+
{ provide: HttpClient, useValue: httpMock },
|
30 |
+
BookStoreService
|
31 |
+
]
|
32 |
+
});
|
33 |
+
service = TestBed.inject(BookStoreService);
|
34 |
+
});
|
35 |
+
|
36 |
+
it('should GET a list of all books', () => {
|
37 |
+
|
38 |
+
let receivedBooks!: Book[];
|
39 |
+
service.getAll().subscribe(b => receivedBooks = b);
|
40 |
+
|
41 |
+
expect(receivedBooks).toHaveSize(2);
|
42 |
+
expect(receivedBooks[0].isbn).toBe('111');
|
43 |
+
expect(receivedBooks[1].isbn).toBe('222');
|
44 |
+
|
45 |
+
// NEU
|
46 |
+
expect(httpMock.get).toHaveBeenCalledOnceWith('https://api5.angular-buch.com/books');
|
47 |
+
});
|
48 |
+
});
|
@@ -1,16 +0,0 @@
|
|
1 |
-
import { TestBed } from '@angular/core/testing';
|
2 |
-
|
3 |
-
import { BookStoreService } from './book-store.service';
|
4 |
-
|
5 |
-
describe('BookStoreService', () => {
|
6 |
-
let service: BookStoreService;
|
7 |
-
|
8 |
-
beforeEach(() => {
|
9 |
-
TestBed.configureTestingModule({});
|
10 |
-
service = TestBed.inject(BookStoreService);
|
11 |
-
});
|
12 |
-
|
13 |
-
it('should be created', () => {
|
14 |
-
expect(service).toBeTruthy();
|
15 |
-
});
|
16 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { HttpClient } from '@angular/common/http';
|
2 |
+
import { TestBed } from '@angular/core/testing';
|
3 |
+
import { of } from 'rxjs';
|
4 |
+
|
5 |
+
import { Book } from './book';
|
6 |
+
import { BookStoreService } from './book-store.service';
|
7 |
+
|
8 |
+
describe('BookStoreService', () => {
|
9 |
+
let service: BookStoreService;
|
10 |
+
|
11 |
+
beforeEach(() => {
|
12 |
+
|
13 |
+
const expectedBooks: Book[] = [
|
14 |
+
{ isbn: '111', title: 'Book 1', authors: [] },
|
15 |
+
{ isbn: '222', title: 'Book 2', authors: [] }
|
16 |
+
];
|
17 |
+
|
18 |
+
const httpStub = {
|
19 |
+
get: () => of(expectedBooks)
|
20 |
+
};
|
21 |
+
|
22 |
+
TestBed.configureTestingModule({
|
23 |
+
providers: [
|
24 |
+
{ provide: HttpClient, useValue: httpStub }
|
25 |
+
]
|
26 |
+
});
|
27 |
+
service = TestBed.inject(BookStoreService);
|
28 |
+
});
|
29 |
+
|
30 |
+
it('should GET a list of all books', () => {
|
31 |
+
|
32 |
+
let receivedBooks!: Book[];
|
33 |
+
service.getAll().subscribe(b => receivedBooks = b);
|
34 |
+
|
35 |
+
expect(receivedBooks).toHaveSize(2);
|
36 |
+
expect(receivedBooks[0].isbn).toBe('111');
|
37 |
+
expect(receivedBooks[1].isbn).toBe('222');
|
38 |
+
});
|
39 |
+
});
|
@@ -1,4 +1,6 @@
|
|
|
|
1 |
import { Injectable } from '@angular/core';
|
|
|
2 |
|
3 |
import { Book } from './book';
|
4 |
|
@@ -6,36 +8,19 @@ import { Book } from './book';
|
|
6 |
providedIn: 'root'
|
7 |
})
|
8 |
export class BookStoreService {
|
9 |
-
private
|
10 |
|
11 |
-
constructor() {
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
title: 'Tierisch gut kochen',
|
16 |
-
authors: ['Mrs Chimp', 'Mr Gorilla'],
|
17 |
-
published: '2022-06-20',
|
18 |
-
subtitle: 'Rezepte von Affe bis Zebra',
|
19 |
-
thumbnailUrl: 'https://cdn.ng-buch.de/kochen.png',
|
20 |
-
description: 'Immer lecker und gut'
|
21 |
-
},
|
22 |
-
{
|
23 |
-
isbn: '67890',
|
24 |
-
title: 'Backen mit Affen',
|
25 |
-
authors: ['Orang Utan'],
|
26 |
-
published: '2022-07-15',
|
27 |
-
subtitle: 'Bananenbrot und mehr',
|
28 |
-
thumbnailUrl: 'https://cdn.ng-buch.de/backen.png',
|
29 |
-
description: 'Tolle Backtipps für Mensch und Tier'
|
30 |
-
}
|
31 |
-
];
|
32 |
}
|
33 |
|
34 |
-
|
35 |
-
return this.books;
|
36 |
}
|
37 |
|
38 |
-
|
39 |
-
return this.
|
40 |
}
|
41 |
}
|
1 |
+
import { HttpClient } from '@angular/common/http';
|
2 |
import { Injectable } from '@angular/core';
|
3 |
+
import { Observable } from 'rxjs';
|
4 |
|
5 |
import { Book } from './book';
|
6 |
|
8 |
providedIn: 'root'
|
9 |
})
|
10 |
export class BookStoreService {
|
11 |
+
private apiUrl = 'https://api5.angular-buch.com';
|
12 |
|
13 |
+
constructor(private http: HttpClient) {}
|
14 |
+
|
15 |
+
getAll(): Observable<Book[]> {
|
16 |
+
return this.http.get<Book[]>(`${this.apiUrl}/books`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
}
|
18 |
|
19 |
+
getSingle(isbn: string): Observable<Book> {
|
20 |
+
return this.http.get<Book>(`${this.apiUrl}/books/${isbn}`);
|
21 |
}
|
22 |
|
23 |
+
remove(isbn: string): Observable<unknown> {
|
24 |
+
return this.http.delete(`${this.apiUrl}/books/${isbn}`);
|
25 |
}
|
26 |
}
|