|
@@ -1,3 +1,15 @@
|
|
| 1 |
<main>
|
| 2 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
</main>
|
|
|
|
| 1 |
<main>
|
| 2 |
+
<nav>
|
| 3 |
+
<ul>
|
| 4 |
+
<li>
|
| 5 |
+
<a routerLink="/home" routerLinkActive="active"
|
| 6 |
+
ariaCurrentWhenActive="page">Home</a>
|
| 7 |
+
</li>
|
| 8 |
+
<li>
|
| 9 |
+
<a routerLink="/books" routerLinkActive="active"
|
| 10 |
+
ariaCurrentWhenActive="page">Books</a>
|
| 11 |
+
</li>
|
| 12 |
+
</ul>
|
| 13 |
+
</nav>
|
| 14 |
+
<router-outlet />
|
| 15 |
</main>
|
|
@@ -1,3 +1,10 @@
|
|
| 1 |
import { Routes } from '@angular/router';
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { Routes } from '@angular/router';
|
| 2 |
|
| 3 |
+
import { HomePage } from './home-page/home-page';
|
| 4 |
+
import { booksPortalRoutes } from './books-portal/books-portal.routes';
|
| 5 |
+
|
| 6 |
+
export const routes: Routes = [
|
| 7 |
+
{ path: '', redirectTo: 'home', pathMatch: 'full' },
|
| 8 |
+
{ path: 'home', component: HomePage, title: 'BookManager' },
|
| 9 |
+
...booksPortalRoutes
|
| 10 |
+
];
|
|
@@ -1,10 +1,14 @@
|
|
| 1 |
import { TestBed } from '@angular/core/testing';
|
| 2 |
import { App } from './app';
|
|
|
|
| 3 |
|
| 4 |
describe('App', () => {
|
| 5 |
beforeEach(async () => {
|
| 6 |
await TestBed.configureTestingModule({
|
| 7 |
imports: [App],
|
|
|
|
|
|
|
|
|
|
| 8 |
}).compileComponents();
|
| 9 |
});
|
| 10 |
|
|
|
|
| 1 |
import { TestBed } from '@angular/core/testing';
|
| 2 |
import { App } from './app';
|
| 3 |
+
import { provideRouter } from '@angular/router';
|
| 4 |
|
| 5 |
describe('App', () => {
|
| 6 |
beforeEach(async () => {
|
| 7 |
await TestBed.configureTestingModule({
|
| 8 |
imports: [App],
|
| 9 |
+
providers: [
|
| 10 |
+
provideRouter([]) // Verhindert: NG0201: No provider found for `ActivatedRoute`.
|
| 11 |
+
]
|
| 12 |
}).compileComponents();
|
| 13 |
});
|
| 14 |
|
|
@@ -1,10 +1,9 @@
|
|
| 1 |
import { Component } from '@angular/core';
|
| 2 |
-
|
| 3 |
-
import { BooksOverviewPage } from './books-portal/books-overview-page/books-overview-page';
|
| 4 |
|
| 5 |
@Component({
|
| 6 |
selector: 'app-root',
|
| 7 |
-
imports: [
|
| 8 |
templateUrl: './app.html',
|
| 9 |
styleUrl: './app.scss'
|
| 10 |
})
|
|
|
|
| 1 |
import { Component } from '@angular/core';
|
| 2 |
+
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
|
|
| 3 |
|
| 4 |
@Component({
|
| 5 |
selector: 'app-root',
|
| 6 |
+
imports: [RouterOutlet, RouterLink, RouterLinkActive],
|
| 7 |
templateUrl: './app.html',
|
| 8 |
styleUrl: './app.scss'
|
| 9 |
})
|
|
@@ -12,6 +12,7 @@
|
|
| 12 |
ISBN: {{ b.isbn }}
|
| 13 |
</div>
|
| 14 |
<footer>
|
|
|
|
| 15 |
<button type="button" class="secondary" (click)="likeBook()">Like</button>
|
| 16 |
</footer>
|
| 17 |
</article>
|
|
|
|
| 12 |
ISBN: {{ b.isbn }}
|
| 13 |
</div>
|
| 14 |
<footer>
|
| 15 |
+
<a [routerLink]="['details', b.isbn]">Details</a>
|
| 16 |
<button type="button" class="secondary" (click)="likeBook()">Like</button>
|
| 17 |
</footer>
|
| 18 |
</article>
|
|
@@ -1,8 +1,9 @@
|
|
|
|
|
| 1 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
|
|
| 2 |
|
| 3 |
-
import { BookCard } from './book-card';
|
| 4 |
import { Book } from '../../shared/book';
|
| 5 |
-
import {
|
| 6 |
|
| 7 |
describe('BookCard', () => {
|
| 8 |
let fixture: ComponentFixture<BookCard>;
|
|
@@ -22,7 +23,10 @@ describe('BookCard', () => {
|
|
| 22 |
emittedBook = undefined;
|
| 23 |
|
| 24 |
await TestBed.configureTestingModule({
|
| 25 |
-
imports: [BookCard]
|
|
|
|
|
|
|
|
|
|
| 26 |
}).compileComponents();
|
| 27 |
|
| 28 |
fixture = TestBed.createComponent(BookCard, {
|
|
|
|
| 1 |
+
import { inputBinding, outputBinding } from '@angular/core';
|
| 2 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
| 3 |
+
import { provideRouter } from '@angular/router';
|
| 4 |
|
|
|
|
| 5 |
import { Book } from '../../shared/book';
|
| 6 |
+
import { BookCard } from './book-card';
|
| 7 |
|
| 8 |
describe('BookCard', () => {
|
| 9 |
let fixture: ComponentFixture<BookCard>;
|
|
|
|
| 23 |
emittedBook = undefined;
|
| 24 |
|
| 25 |
await TestBed.configureTestingModule({
|
| 26 |
+
imports: [BookCard],
|
| 27 |
+
providers: [
|
| 28 |
+
provideRouter([]) // Verhindert: NG0201: No provider found for `ActivatedRoute`.
|
| 29 |
+
]
|
| 30 |
}).compileComponents();
|
| 31 |
|
| 32 |
fixture = TestBed.createComponent(BookCard, {
|
|
@@ -1,10 +1,11 @@
|
|
| 1 |
import { Component, input, output } from '@angular/core';
|
|
|
|
| 2 |
|
| 3 |
import { Book } from '../../shared/book';
|
| 4 |
|
| 5 |
@Component({
|
| 6 |
selector: 'app-book-card',
|
| 7 |
-
imports: [],
|
| 8 |
templateUrl: './book-card.html',
|
| 9 |
styleUrl: './book-card.scss'
|
| 10 |
})
|
|
|
|
| 1 |
import { Component, input, output } from '@angular/core';
|
| 2 |
+
import { RouterLink } from '@angular/router';
|
| 3 |
|
| 4 |
import { Book } from '../../shared/book';
|
| 5 |
|
| 6 |
@Component({
|
| 7 |
selector: 'app-book-card',
|
| 8 |
+
imports: [RouterLink],
|
| 9 |
templateUrl: './book-card.html',
|
| 10 |
styleUrl: './book-card.scss'
|
| 11 |
})
|
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Location } from '@angular/common';
|
| 2 |
+
import { provideLocationMocks } from '@angular/common/testing';
|
| 3 |
+
import { TestBed } from '@angular/core/testing';
|
| 4 |
+
import { provideRouter, Router } from '@angular/router';
|
| 5 |
+
|
| 6 |
+
import { booksPortalRoutes } from '../books-portal.routes';
|
| 7 |
+
|
| 8 |
+
describe('BookDetailsPage Routing', () => {
|
| 9 |
+
it('should naviate to the details page', async () => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
providers: [
|
| 12 |
+
provideRouter(booksPortalRoutes),
|
| 13 |
+
provideLocationMocks()
|
| 14 |
+
]
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
const location = TestBed.inject(Location);
|
| 18 |
+
const router = TestBed.inject(Router);
|
| 19 |
+
|
| 20 |
+
// Hier wird später im produktiven Code eine Aktion stattfinden,
|
| 21 |
+
// z.B. das Absenden eines Formulars und eine anschließende Navigation
|
| 22 |
+
await router.navigate(['/books/details/12345']);
|
| 23 |
+
|
| 24 |
+
// Prüfung, ob Navigation zur erwarteten Ziel-URL stattgefunden hat
|
| 25 |
+
expect(location.path()).toBe('/books/details/12345');
|
| 26 |
+
});
|
| 27 |
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@if (book(); as b) {
|
| 2 |
+
<article>
|
| 3 |
+
<header>
|
| 4 |
+
<h1>{{ b.title }}</h1>
|
| 5 |
+
@if (b.subtitle) {
|
| 6 |
+
<p role="doc-subtitle">{{ b.subtitle }}</p>
|
| 7 |
+
}
|
| 8 |
+
<div class="grid">
|
| 9 |
+
<div>
|
| 10 |
+
<h2>Authors</h2>
|
| 11 |
+
<ul>
|
| 12 |
+
@for (author of b.authors; track $index) {
|
| 13 |
+
<li>{{ author }}</li>
|
| 14 |
+
}
|
| 15 |
+
</ul>
|
| 16 |
+
</div>
|
| 17 |
+
<div>
|
| 18 |
+
<h2>ISBN</h2>
|
| 19 |
+
{{ b.isbn }}
|
| 20 |
+
</div>
|
| 21 |
+
<div>
|
| 22 |
+
<h2>Created at</h2>
|
| 23 |
+
{{ b.createdAt }}
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
</header>
|
| 27 |
+
<p>{{ b.description }}</p>
|
| 28 |
+
<img [src]="b.imageUrl" alt="Cover" />
|
| 29 |
+
<footer>
|
| 30 |
+
<a routerLink="/books">Back to list</a>
|
| 31 |
+
</footer>
|
| 32 |
+
</article>
|
| 33 |
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { TestBed } from '@angular/core/testing';
|
| 2 |
+
import { RouterTestingHarness } from '@angular/router/testing';
|
| 3 |
+
import { provideRouter } from '@angular/router';
|
| 4 |
+
|
| 5 |
+
import { BookDetailsPage } from './book-details-page';
|
| 6 |
+
import { booksPortalRoutes } from '../books-portal.routes';
|
| 7 |
+
import { BookStore } from '../../shared/book-store';
|
| 8 |
+
|
| 9 |
+
describe('BookDetailsPage Routing', () => {
|
| 10 |
+
it('should load the correct book by ISBN', async () => {
|
| 11 |
+
TestBed.configureTestingModule({
|
| 12 |
+
imports: [BookDetailsPage],
|
| 13 |
+
providers: [provideRouter(booksPortalRoutes)]
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
const harness = await RouterTestingHarness.create();
|
| 17 |
+
const component = await harness.navigateByUrl('/books/details/12345', BookDetailsPage);
|
| 18 |
+
const bookStore = TestBed.inject(BookStore);
|
| 19 |
+
|
| 20 |
+
const expectedBook = bookStore.getSingle('12345');
|
| 21 |
+
|
| 22 |
+
expect(component).toBeTruthy();
|
| 23 |
+
expect(component['book']()).toEqual(expectedBook);
|
| 24 |
+
expect(document.title).toBe('Book Details');
|
| 25 |
+
});
|
| 26 |
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component, inject, signal } from '@angular/core';
|
| 2 |
+
import { ActivatedRoute, RouterLink } from '@angular/router';
|
| 3 |
+
|
| 4 |
+
import { Book } from '../../shared/book';
|
| 5 |
+
import { BookStore } from '../../shared/book-store';
|
| 6 |
+
|
| 7 |
+
@Component({
|
| 8 |
+
selector: 'app-book-details-page',
|
| 9 |
+
imports: [RouterLink],
|
| 10 |
+
templateUrl: './book-details-page.html',
|
| 11 |
+
styleUrl: './book-details-page.scss'
|
| 12 |
+
})
|
| 13 |
+
export class BookDetailsPage {
|
| 14 |
+
#bookStore = inject(BookStore);
|
| 15 |
+
#route = inject(ActivatedRoute);
|
| 16 |
+
|
| 17 |
+
protected book = signal<Book | undefined>(undefined);
|
| 18 |
+
|
| 19 |
+
constructor() {
|
| 20 |
+
const isbn = this.#route.snapshot.paramMap.get('isbn');
|
| 21 |
+
if (isbn) {
|
| 22 |
+
this.book.set(this.#bookStore.getSingle(isbn));
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
|
@@ -1,47 +1,21 @@
|
|
| 1 |
-
import {
|
| 2 |
-
import {
|
| 3 |
-
|
| 4 |
-
describe('BooksOverviewPage', () => {
|
| 5 |
-
let component: BooksOverviewPage;
|
| 6 |
-
let fixture: ComponentFixture<BooksOverviewPage>;
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
imports: [BooksOverviewPage]
|
| 11 |
-
}).compileComponents();
|
| 12 |
-
|
| 13 |
-
fixture = TestBed.createComponent(BooksOverviewPage);
|
| 14 |
-
component = fixture.componentInstance;
|
| 15 |
-
fixture.detectChanges();
|
| 16 |
-
});
|
| 17 |
-
|
| 18 |
-
it('should display all books if the search term is empty', () => {
|
| 19 |
-
component['searchTerm'].set('');
|
| 20 |
-
|
| 21 |
-
const books = component['filteredBooks']();
|
| 22 |
-
expect(books.length).toBe(2);
|
| 23 |
-
});
|
| 24 |
-
|
| 25 |
-
it('should filter books based on the search term', () => {
|
| 26 |
-
component['searchTerm'].set('Affe');
|
| 27 |
-
|
| 28 |
-
const books = component['filteredBooks']();
|
| 29 |
-
expect(books.length).toBe(1);
|
| 30 |
-
expect(books[0].title).toBe('Backen mit Affen');
|
| 31 |
-
});
|
| 32 |
-
|
| 33 |
-
it('should filter books ignoring case sensitivity', () => {
|
| 34 |
-
component['searchTerm'].set('AFFEN');
|
| 35 |
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
-
component
|
| 43 |
|
| 44 |
-
|
| 45 |
-
expect(
|
| 46 |
});
|
| 47 |
});
|
|
|
|
| 1 |
+
import { TestBed } from '@angular/core/testing';
|
| 2 |
+
import { provideRouter } from '@angular/router';
|
| 3 |
+
import { RouterTestingHarness } from '@angular/router/testing';
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
+
import { booksPortalRoutes } from '../books-portal.routes';
|
| 6 |
+
import { BooksOverviewPage } from './books-overview-page';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
describe('BooksOverviewPage Routing', () => {
|
| 9 |
+
it('should load the BooksOverviewPage for /books', async () => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
imports: [BooksOverviewPage],
|
| 12 |
+
providers: [provideRouter(booksPortalRoutes)]
|
| 13 |
+
});
|
| 14 |
|
| 15 |
+
const harness = await RouterTestingHarness.create();
|
| 16 |
+
const component = await harness.navigateByUrl('/books', BooksOverviewPage);
|
| 17 |
|
| 18 |
+
expect(component).toBeTruthy();
|
| 19 |
+
expect(document.title).toBe('Books');
|
| 20 |
});
|
| 21 |
});
|
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Routes } from '@angular/router';
|
| 2 |
+
|
| 3 |
+
import { BooksOverviewPage } from './books-overview-page/books-overview-page';
|
| 4 |
+
import { BookDetailsPage } from './book-details-page/book-details-page';
|
| 5 |
+
|
| 6 |
+
export const booksPortalRoutes: Routes = [
|
| 7 |
+
{ path: 'books', component: BooksOverviewPage, title: 'Books' },
|
| 8 |
+
{ path: 'books/details/:isbn', component: BookDetailsPage, title: 'Book Details' },
|
| 9 |
+
];
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
<h1>Welcome to the BookManager!</h1>
|
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { TestBed } from '@angular/core/testing';
|
| 2 |
+
import { provideRouter } from '@angular/router';
|
| 3 |
+
import { RouterTestingHarness } from '@angular/router/testing';
|
| 4 |
+
|
| 5 |
+
import { routes } from '../app.routes';
|
| 6 |
+
import { HomePage } from './home-page';
|
| 7 |
+
|
| 8 |
+
describe('HomePage Routing', () => {
|
| 9 |
+
it('should load the HomePage component for /home', async () => {
|
| 10 |
+
TestBed.configureTestingModule({
|
| 11 |
+
imports: [HomePage],
|
| 12 |
+
providers: [provideRouter(routes)]
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
const harness = await RouterTestingHarness.create();
|
| 16 |
+
const component = await harness.navigateByUrl('/home', HomePage);
|
| 17 |
+
|
| 18 |
+
expect(component).toBeTruthy();
|
| 19 |
+
expect(document.title).toBe('BookManager');
|
| 20 |
+
});
|
| 21 |
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Component } from '@angular/core';
|
| 2 |
+
|
| 3 |
+
@Component({
|
| 4 |
+
selector: 'app-home-page',
|
| 5 |
+
imports: [],
|
| 6 |
+
templateUrl: './home-page.html',
|
| 7 |
+
styleUrl: './home-page.scss'
|
| 8 |
+
})
|
| 9 |
+
export class HomePage {
|
| 10 |
+
|
| 11 |
+
}
|
|
@@ -31,4 +31,8 @@ export class BookStore {
|
|
| 31 |
getAll(): Book[] {
|
| 32 |
return this.#books;
|
| 33 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
|
|
|
| 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 |
}
|