Differenzansicht 08-http
im Vergleich zu 07-inputparams

Zurück zur Übersicht | ← Vorherige | Nächste → | Demo | Quelltext auf GitHub
src/app/books-portal/book-details-page/book-details-page.html CHANGED
@@ -28,6 +28,10 @@
28
  <img [src]="b.imageUrl" alt="Cover" />
29
  <footer>
30
  <a routerLink="/books">Back to list</a>
 
 
 
 
31
  </footer>
32
  </article>
33
  }
 
28
  <img [src]="b.imageUrl" alt="Cover" />
29
  <footer>
30
  <a routerLink="/books">Back to list</a>
31
+ <a routerLink="/books/details/9783988890641">Angular Book</a>
32
+ <button type="button" (click)="removeBook(b.isbn)">
33
+ Delete book
34
+ </button>
35
  </footer>
36
  </article>
37
  }
src/app/books-portal/book-details-page/book-details-page.spec.ts CHANGED
@@ -1,28 +1,36 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { provideRouter, Router } from '@angular/router';
3
- import { provideLocationMocks } from '@angular/common/testing';
4
  import { Location } from '@angular/common';
 
5
  import { inputBinding, signal, WritableSignal } from '@angular/core';
 
 
 
 
6
 
7
- import { BookDetailsPage } from './book-details-page';
8
- import { booksPortalRoutes } from '../books-portal.routes';
9
  import { BookStore } from '../../shared/book-store';
 
 
10
 
11
  describe('BookDetailsPage', () => {
12
  let component: BookDetailsPage;
13
  let fixture: ComponentFixture<BookDetailsPage>;
14
- let bookStore: BookStore;
15
 
16
  let isbn: WritableSignal<string>;
 
17
 
18
  beforeEach(async () => {
19
  isbn = signal('12345');
 
20
 
21
  await TestBed.configureTestingModule({
22
  imports: [BookDetailsPage],
23
  providers: [
24
  provideRouter(booksPortalRoutes),
25
  provideLocationMocks(),
 
 
 
 
26
  ]
27
  })
28
  .compileComponents();
@@ -31,7 +39,6 @@ describe('BookDetailsPage', () => {
31
  bindings: [inputBinding('isbn', isbn)]
32
  });
33
  component = fixture.componentInstance;
34
- bookStore = TestBed.inject(BookStore);
35
  await fixture.whenStable();
36
  });
37
 
@@ -40,8 +47,8 @@ describe('BookDetailsPage', () => {
40
  });
41
 
42
  it('should load the correct book by ISBN', async () => {
43
- const expectedBook = bookStore.getSingle('12345');
44
- expect(component['book']()).toEqual(expectedBook);
45
  });
46
 
47
  it('should navigate to the details page', async () => {
@@ -54,10 +61,13 @@ describe('BookDetailsPage', () => {
54
  });
55
 
56
  it('should update the book when ISBN changes', async () => {
 
 
 
57
  isbn.set('67890');
58
  await fixture.whenStable();
59
 
60
- const expectedBook = bookStore.getSingle('67890');
61
- expect(component['book']()).toEqual(expectedBook);
62
  });
63
  });
 
 
 
 
1
  import { Location } from '@angular/common';
2
+ import { provideLocationMocks } from '@angular/common/testing';
3
  import { inputBinding, signal, WritableSignal } from '@angular/core';
4
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
5
+ import { provideRouter, Router } from '@angular/router';
6
+ import { of } from 'rxjs';
7
+ import { Mock } from 'vitest';
8
 
 
 
9
  import { BookStore } from '../../shared/book-store';
10
+ import { booksPortalRoutes } from '../books-portal.routes';
11
+ import { BookDetailsPage } from './book-details-page';
12
 
13
  describe('BookDetailsPage', () => {
14
  let component: BookDetailsPage;
15
  let fixture: ComponentFixture<BookDetailsPage>;
16
+ let getSingleMock: Mock;
17
 
18
  let isbn: WritableSignal<string>;
19
+ const testBook = { isbn: '12345', title: 'Test Book 1', authors: [] };
20
 
21
  beforeEach(async () => {
22
  isbn = signal('12345');
23
+ getSingleMock = vi.fn().mockReturnValue(of(testBook));
24
 
25
  await TestBed.configureTestingModule({
26
  imports: [BookDetailsPage],
27
  providers: [
28
  provideRouter(booksPortalRoutes),
29
  provideLocationMocks(),
30
+ {
31
+ provide: BookStore,
32
+ useValue: { getSingle: getSingleMock }
33
+ }
34
  ]
35
  })
36
  .compileComponents();
 
39
  bindings: [inputBinding('isbn', isbn)]
40
  });
41
  component = fixture.componentInstance;
 
42
  await fixture.whenStable();
43
  });
44
 
 
47
  });
48
 
49
  it('should load the correct book by ISBN', async () => {
50
+ expect(getSingleMock).toHaveBeenCalledExactlyOnceWith('12345');
51
+ expect(component['book']()).toEqual(testBook);
52
  });
53
 
54
  it('should navigate to the details page', async () => {
 
61
  });
62
 
63
  it('should update the book when ISBN changes', async () => {
64
+ const anotherBook = { isbn: '67890', title: 'Test Book 2', authors: [] };
65
+ getSingleMock.mockReturnValue(of(anotherBook));
66
+
67
  isbn.set('67890');
68
  await fixture.whenStable();
69
 
70
+ expect(getSingleMock).toHaveBeenCalledWith('67890');
71
+ expect(component['book']()).toEqual(anotherBook);
72
  });
73
  });
src/app/books-portal/book-details-page/book-details-page.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { Component, computed, inject, input } from '@angular/core';
2
- import { RouterLink } from '@angular/router';
3
 
 
4
  import { BookStore } from '../../shared/book-store';
5
 
6
  @Component({
@@ -11,7 +12,24 @@ import { BookStore } from '../../shared/book-store';
11
  })
12
  export class BookDetailsPage {
13
  #bookStore = inject(BookStore);
 
14
 
15
  readonly isbn = input.required<string>();
16
- protected book = computed(() => this.#bookStore.getSingle(this.isbn()));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
 
1
+ import { Component, effect, inject, input, signal } from '@angular/core';
2
+ import { Router, RouterLink } from '@angular/router';
3
 
4
+ import { Book } from '../../shared/book';
5
  import { BookStore } from '../../shared/book-store';
6
 
7
  @Component({
 
12
  })
13
  export class BookDetailsPage {
14
  #bookStore = inject(BookStore);
15
+ #router = inject(Router);
16
 
17
  readonly isbn = input.required<string>();
18
+ protected book = signal<Book | undefined>(undefined);
19
+
20
+ constructor() {
21
+ effect(() => {
22
+ this.#bookStore.getSingle(this.isbn()).subscribe(book => {
23
+ this.book.set(book);
24
+ });
25
+ });
26
+ }
27
+
28
+ removeBook(isbn: string) {
29
+ if (window.confirm('Delete book?')) {
30
+ this.#bookStore.remove(isbn).subscribe(() => {
31
+ this.#router.navigateByUrl('/books');
32
+ });
33
+ }
34
+ }
35
  }
src/app/books-portal/books-overview-page/books-overview-page.spec.ts CHANGED
@@ -1,18 +1,34 @@
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
  import { provideRouter } from '@angular/router';
3
  import { RouterTestingHarness } from '@angular/router/testing';
 
4
 
5
  import { BooksOverviewPage } from './books-overview-page';
6
  import { booksPortalRoutes } from '../books-portal.routes';
 
 
7
 
8
  describe('BooksOverviewPage', () => {
9
  let component: BooksOverviewPage;
10
  let fixture: ComponentFixture<BooksOverviewPage>;
11
 
 
 
 
 
 
12
  beforeEach(async () => {
13
  await TestBed.configureTestingModule({
14
  imports: [BooksOverviewPage],
15
- providers: [provideRouter(booksPortalRoutes)]
 
 
 
 
 
 
 
 
16
  })
17
  .compileComponents();
18
 
 
1
  import { ComponentFixture, TestBed } from '@angular/core/testing';
2
  import { provideRouter } from '@angular/router';
3
  import { RouterTestingHarness } from '@angular/router/testing';
4
+ import { of } from 'rxjs';
5
 
6
  import { BooksOverviewPage } from './books-overview-page';
7
  import { booksPortalRoutes } from '../books-portal.routes';
8
+ import { Book } from '../../shared/book';
9
+ import { BookStore } from '../../shared/book-store';
10
 
11
  describe('BooksOverviewPage', () => {
12
  let component: BooksOverviewPage;
13
  let fixture: ComponentFixture<BooksOverviewPage>;
14
 
15
+ const mockBooks: Partial<Book>[] = [
16
+ { isbn: '1234', title: 'Tierisch gut kochen' },
17
+ { isbn: '5678', title: 'Backen mit Affen' }
18
+ ];
19
+
20
  beforeEach(async () => {
21
  await TestBed.configureTestingModule({
22
  imports: [BooksOverviewPage],
23
+ providers: [
24
+ provideRouter(booksPortalRoutes),
25
+ {
26
+ provide: BookStore,
27
+ useValue: {
28
+ getAll: () => of(mockBooks)
29
+ }
30
+ }
31
+ ]
32
  })
33
  .compileComponents();
34
 
src/app/books-portal/books-overview-page/books-overview-page.ts CHANGED
@@ -28,7 +28,9 @@ export class BooksOverviewPage {
28
  });
29
 
30
  constructor() {
31
- this.books.set(this.#bookStore.getAll());
 
 
32
  }
33
 
34
  addLikedBook(newLikedBook: Book) {
 
28
  });
29
 
30
  constructor() {
31
+ this.#bookStore.getAll().subscribe(books => {
32
+ this.books.set(books);
33
+ });
34
  }
35
 
36
  addLikedBook(newLikedBook: Book) {
src/app/shared/book-store.spec.ts CHANGED
@@ -1,21 +1,79 @@
 
 
1
  import { TestBed } from '@angular/core/testing';
2
 
 
3
  import { BookStore } from './book-store';
4
 
5
  describe('BookStore', () => {
6
  let service: BookStore;
 
7
 
8
  beforeEach(() => {
9
- TestBed.configureTestingModule({});
 
 
10
  service = TestBed.inject(BookStore);
 
 
 
 
 
11
  });
12
 
13
  it('should be created', () => {
14
  expect(service).toBeTruthy();
15
  });
16
 
17
- it('should return a list of books', () => {
18
- const books = service.getAll();
19
- expect(books.length).toBeGreaterThan(0);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  });
21
  });
 
1
+ import { HttpErrorResponse } from '@angular/common/http';
2
+ import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
3
  import { TestBed } from '@angular/core/testing';
4
 
5
+ import { Book } from './book';
6
  import { BookStore } from './book-store';
7
 
8
  describe('BookStore', () => {
9
  let service: BookStore;
10
+ let httpTesting: HttpTestingController;
11
 
12
  beforeEach(() => {
13
+ TestBed.configureTestingModule({
14
+ providers: [provideHttpClientTesting()]
15
+ });
16
  service = TestBed.inject(BookStore);
17
+ httpTesting = TestBed.inject(HttpTestingController);
18
+ });
19
+
20
+ afterEach(() => {
21
+ httpTesting.verify();
22
  });
23
 
24
  it('should be created', () => {
25
  expect(service).toBeTruthy();
26
  });
27
 
28
+ it('should fetch all books from API', () => {
29
+ const mockBooks: Partial<Book>[] = [
30
+ { isbn: '123', title: 'Book 1' },
31
+ { isbn: '456', title: 'Book 2' }
32
+ ];
33
+
34
+ let receivedBooks: Book[] | undefined;
35
+ service.getAll().subscribe(books => receivedBooks = books);
36
+
37
+ const req = httpTesting.expectOne('https://api1.angular-buch.com/books');
38
+ expect(req.request.method).toBe('GET');
39
+ req.flush(mockBooks);
40
+
41
+ expect(receivedBooks).toEqual(mockBooks);
42
+ });
43
+
44
+ it('should fetch a single book by ISBN', () => {
45
+ const mockBook: Partial<Book> = { isbn: '123', title: 'Test Book' };
46
+
47
+ let receivedBook: Book | undefined;
48
+ service.getSingle('123').subscribe(book => receivedBook = book);
49
+
50
+ const req = httpTesting.expectOne('https://api1.angular-buch.com/books/123');
51
+ expect(req.request.method).toBe('GET');
52
+ req.flush(mockBook);
53
+
54
+ expect(receivedBook).toEqual(mockBook);
55
+ });
56
+
57
+ it('should delete a book', () => {
58
+ let success = false;
59
+ service.remove('123').subscribe(() => success = true);
60
+
61
+ const req = httpTesting.expectOne('https://api1.angular-buch.com/books/123');
62
+ expect(req.request.method).toBe('DELETE');
63
+ req.flush(null);
64
+
65
+ expect(success).toBe(true);
66
+ });
67
+
68
+ it('should handle server errors', () => {
69
+ let errorResponse: HttpErrorResponse | undefined;
70
+ service.getAll().subscribe({
71
+ error: (err: HttpErrorResponse) => errorResponse = err
72
+ });
73
+
74
+ const req = httpTesting.expectOne('https://api1.angular-buch.com/books');
75
+ req.flush('Server Error', { status: 500, statusText: 'Internal Server Error' });
76
+
77
+ expect(errorResponse?.status).toBe(500);
78
  });
79
  });
src/app/shared/book-store.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { Injectable } from '@angular/core';
 
 
2
 
3
  import { Book } from './book';
4
 
@@ -6,33 +8,18 @@ import { Book } from './book';
6
  providedIn: 'root'
7
  })
8
  export class BookStore {
 
 
9
 
10
- #books: Book[] = [
11
- {
12
- isbn: '12345',
13
- title: 'Tierisch gut kochen',
14
- authors: ['Mrs Chimp', 'Mr Gorilla'],
15
- subtitle: 'Rezepte von Affe bis Zebra',
16
- imageUrl: 'https://cdn.ng-buch.de/kochen.jpg',
17
- description: 'Immer lecker und gut',
18
- createdAt: new Date().toISOString()
19
- },
20
- {
21
- isbn: '67890',
22
- title: 'Backen mit Affen',
23
- subtitle: 'Bananenbrot und mehr',
24
- authors: ['Orang Utan'],
25
- imageUrl: 'https://cdn.ng-buch.de/backen.jpg',
26
- description: 'Tolle Backtipps für Mensch und Tier',
27
- createdAt: new Date().toISOString()
28
- }
29
- ];
30
 
31
- getAll(): Book[] {
32
- return this.#books;
33
  }
34
 
35
- getSingle(isbn: string): Book | undefined {
36
- return this.#books.find(book => book.isbn === isbn);
37
  }
38
  }
 
1
+ import { inject, Injectable } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import { Observable } from 'rxjs';
4
 
5
  import { Book } from './book';
6
 
 
8
  providedIn: 'root'
9
  })
10
  export class BookStore {
11
+ #http = inject(HttpClient);
12
+ #apiUrl = 'https://api1.angular-buch.com';
13
 
14
+ getAll(): Observable<Book[]> {
15
+ return this.#http.get<Book[]>(`${this.#apiUrl}/books`);
16
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
+ getSingle(isbn: string): Observable<Book> {
19
+ return this.#http.get<Book>(`${this.#apiUrl}/books/${isbn}`);
20
  }
21
 
22
+ remove(isbn: string): Observable<void> {
23
+ return this.#http.delete<void>(`${this.#apiUrl}/books/${isbn}`);
24
  }
25
  }