@@ -114,7 +114,8 @@
|
|
114 |
},
|
115 |
"cli": {
|
116 |
"schematicCollections": [
|
117 |
-
"@angular-eslint/schematics"
|
|
|
118 |
]
|
119 |
}
|
120 |
-
}
|
114 |
},
|
115 |
"cli": {
|
116 |
"schematicCollections": [
|
117 |
+
"@angular-eslint/schematics",
|
118 |
+
"@ngrx/schematics"
|
119 |
]
|
120 |
}
|
121 |
+
}
|
@@ -20,6 +20,9 @@
|
|
20 |
"@angular/platform-browser": "^18.2.0",
|
21 |
"@angular/platform-browser-dynamic": "^18.2.0",
|
22 |
"@angular/router": "^18.2.0",
|
|
|
|
|
|
|
23 |
"angular-date-value-accessor": "^3.0.0",
|
24 |
"book-monkey5-styles": "^1.0.4",
|
25 |
"rxjs": "~7.8.0",
|
@@ -30,10 +33,12 @@
|
|
30 |
"@angular-devkit/build-angular": "^18.2.1",
|
31 |
"@angular/cli": "^18.2.1",
|
32 |
"@angular/compiler-cli": "^18.2.0",
|
|
|
33 |
"@types/jasmine": "~5.1.0",
|
34 |
"angular-eslint": "18.3.0",
|
35 |
"eslint": "^9.9.0",
|
36 |
"jasmine-core": "~5.2.0",
|
|
|
37 |
"karma": "~6.4.0",
|
38 |
"karma-chrome-launcher": "~3.2.0",
|
39 |
"karma-coverage": "~2.2.0",
|
20 |
"@angular/platform-browser": "^18.2.0",
|
21 |
"@angular/platform-browser-dynamic": "^18.2.0",
|
22 |
"@angular/router": "^18.2.0",
|
23 |
+
"@ngrx/effects": "^18.0.2",
|
24 |
+
"@ngrx/store": "^18.0.2",
|
25 |
+
"@ngrx/store-devtools": "^18.0.2",
|
26 |
"angular-date-value-accessor": "^3.0.0",
|
27 |
"book-monkey5-styles": "^1.0.4",
|
28 |
"rxjs": "~7.8.0",
|
33 |
"@angular-devkit/build-angular": "^18.2.1",
|
34 |
"@angular/cli": "^18.2.1",
|
35 |
"@angular/compiler-cli": "^18.2.0",
|
36 |
+
"@ngrx/schematics": "^18.0.2",
|
37 |
"@types/jasmine": "~5.1.0",
|
38 |
"angular-eslint": "18.3.0",
|
39 |
"eslint": "^9.9.0",
|
40 |
"jasmine-core": "~5.2.0",
|
41 |
+
"jasmine-marbles": "^0.9.2",
|
42 |
"karma": "~6.4.0",
|
43 |
"karma-chrome-launcher": "~3.2.0",
|
44 |
"karma-coverage": "~2.2.0",
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookCreateComponent } from './book-create.component';
|
4 |
+
|
5 |
+
describe('BookCreateComponent', () => {
|
6 |
+
let component: BookCreateComponent;
|
7 |
+
let fixture: ComponentFixture<BookCreateComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookCreateComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookCreateComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookEditComponent } from './book-edit.component';
|
4 |
+
|
5 |
+
describe('BookEditComponent', () => {
|
6 |
+
let component: BookEditComponent;
|
7 |
+
let fixture: ComponentFixture<BookEditComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookEditComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookEditComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookFormComponent } from './book-form.component';
|
4 |
+
|
5 |
+
describe('BookFormComponent', () => {
|
6 |
+
let component: BookFormComponent;
|
7 |
+
let fixture: ComponentFixture<BookFormComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookFormComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookFormComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { FormErrorsComponent } from './form-errors.component';
|
4 |
+
|
5 |
+
describe('FormErrorsComponent', () => {
|
6 |
+
let component: FormErrorsComponent;
|
7 |
+
let fixture: ComponentFixture<FormErrorsComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ FormErrorsComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(FormErrorsComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { AsyncValidatorsService } from './async-validators.service';
|
4 |
+
|
5 |
+
describe('AsyncValidatorsService', () => {
|
6 |
+
let service: AsyncValidatorsService;
|
7 |
+
|
8 |
+
beforeEach(() => {
|
9 |
+
TestBed.configureTestingModule({});
|
10 |
+
service = TestBed.inject(AsyncValidatorsService);
|
11 |
+
});
|
12 |
+
|
13 |
+
it('should be created', () => {
|
14 |
+
expect(service).toBeTruthy();
|
15 |
+
});
|
16 |
+
});
|
@@ -1,6 +1,9 @@
|
|
1 |
-
import { NgModule } from '@angular/core';
|
2 |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
3 |
import { BrowserModule } from '@angular/platform-browser';
|
|
|
|
|
|
|
4 |
|
5 |
import { AppRoutingModule } from './app-routing.module';
|
6 |
import { AppComponent } from './app.component';
|
@@ -18,6 +21,9 @@ import { AuthInterceptor } from './shared/auth.interceptor';
|
|
18 |
BrowserModule,
|
19 |
AppRoutingModule,
|
20 |
HttpClientModule,
|
|
|
|
|
|
|
21 |
],
|
22 |
providers: [
|
23 |
{
|
1 |
+
import { NgModule, isDevMode } from '@angular/core';
|
2 |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
3 |
import { BrowserModule } from '@angular/platform-browser';
|
4 |
+
import { StoreModule } from '@ngrx/store';
|
5 |
+
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
|
6 |
+
import { EffectsModule } from '@ngrx/effects';
|
7 |
|
8 |
import { AppRoutingModule } from './app-routing.module';
|
9 |
import { AppComponent } from './app.component';
|
21 |
BrowserModule,
|
22 |
AppRoutingModule,
|
23 |
HttpClientModule,
|
24 |
+
StoreModule.forRoot({}, {}),
|
25 |
+
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: !isDevMode() }),
|
26 |
+
EffectsModule.forRoot([]),
|
27 |
],
|
28 |
providers: [
|
29 |
{
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { BookDetailsComponent } from './book-details.component';
|
4 |
+
|
5 |
+
describe('BookDetailsComponent', () => {
|
6 |
+
let component: BookDetailsComponent;
|
7 |
+
let fixture: ComponentFixture<BookDetailsComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ BookDetailsComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(BookDetailsComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -7,3 +7,5 @@
|
|
7 |
No books available.
|
8 |
</li>
|
9 |
</ul>
|
|
|
|
7 |
No books available.
|
8 |
</li>
|
9 |
</ul>
|
10 |
+
|
11 |
+
<div class="loader" *ngIf="loading$ | async">Loading ...</div>
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
import { provideMockStore } from '@ngrx/store/testing';
|
3 |
+
import { selectAllBooks, selectBooksLoading } from '../store/book.selectors';
|
4 |
+
|
5 |
+
import { BookListComponent } from './book-list.component';
|
6 |
+
|
7 |
+
describe('BookListComponent', () => {
|
8 |
+
beforeEach(async () => {
|
9 |
+
await TestBed.configureTestingModule({
|
10 |
+
declarations: [ BookListComponent ],
|
11 |
+
providers: [
|
12 |
+
provideMockStore({
|
13 |
+
selectors: [
|
14 |
+
{ selector: selectBooksLoading, value: true },
|
15 |
+
{ selector: selectAllBooks, value: [] },
|
16 |
+
]
|
17 |
+
})
|
18 |
+
]
|
19 |
+
})
|
20 |
+
.compileComponents();
|
21 |
+
});
|
22 |
+
|
23 |
+
it('should show a loading text', () => {
|
24 |
+
const fixture = TestBed.createComponent(BookListComponent);
|
25 |
+
fixture.detectChanges();
|
26 |
+
const element = fixture.nativeElement;
|
27 |
+
expect(element.querySelector('.loader').textContent).toBe('Loading ...');
|
28 |
+
});
|
29 |
+
});
|
@@ -1,8 +1,10 @@
|
|
1 |
import { Component } from '@angular/core';
|
|
|
2 |
import { Observable } from 'rxjs';
|
3 |
|
4 |
import { Book } from '../../shared/book';
|
5 |
-
import {
|
|
|
6 |
|
7 |
@Component({
|
8 |
selector: 'bm-book-list',
|
@@ -11,8 +13,12 @@ import { BookStoreService } from '../../shared/book-store.service';
|
|
11 |
})
|
12 |
export class BookListComponent {
|
13 |
books$: Observable<Book[]>;
|
|
|
14 |
|
15 |
-
constructor(private
|
16 |
-
this.books$ = this.
|
|
|
|
|
|
|
17 |
}
|
18 |
}
|
1 |
import { Component } from '@angular/core';
|
2 |
+
import { Store } from '@ngrx/store';
|
3 |
import { Observable } from 'rxjs';
|
4 |
|
5 |
import { Book } from '../../shared/book';
|
6 |
+
import { loadBooks } from '../store/book.actions';
|
7 |
+
import { selectAllBooks, selectBooksLoading } from '../store/book.selectors';
|
8 |
|
9 |
@Component({
|
10 |
selector: 'bm-book-list',
|
13 |
})
|
14 |
export class BookListComponent {
|
15 |
books$: Observable<Book[]>;
|
16 |
+
loading$: Observable<boolean>;
|
17 |
|
18 |
+
constructor(private store: Store) {
|
19 |
+
this.books$ = this.store.select(selectAllBooks);
|
20 |
+
this.loading$ = this.store.select(selectBooksLoading);
|
21 |
+
|
22 |
+
this.store.dispatch(loadBooks());
|
23 |
}
|
24 |
}
|
@@ -1,5 +1,7 @@
|
|
1 |
import { NgModule } from '@angular/core';
|
2 |
import { CommonModule } from '@angular/common';
|
|
|
|
|
3 |
|
4 |
import { BooksRoutingModule } from './books-routing.module';
|
5 |
import { BookListComponent } from './book-list/book-list.component';
|
@@ -8,6 +10,8 @@ import { BookDetailsComponent } from './book-details/book-details.component';
|
|
8 |
import { IsbnPipe } from './shared/isbn.pipe';
|
9 |
import { ConfirmDirective } from './shared/confirm.directive';
|
10 |
import { LoggedinOnlyDirective } from './shared/loggedin-only.directive';
|
|
|
|
|
11 |
|
12 |
@NgModule({
|
13 |
declarations: [
|
@@ -20,7 +24,9 @@ import { LoggedinOnlyDirective } from './shared/loggedin-only.directive';
|
|
20 |
],
|
21 |
imports: [
|
22 |
CommonModule,
|
23 |
-
BooksRoutingModule
|
|
|
|
|
24 |
]
|
25 |
})
|
26 |
export class BooksModule { }
|
1 |
import { NgModule } from '@angular/core';
|
2 |
import { CommonModule } from '@angular/common';
|
3 |
+
import { StoreModule } from '@ngrx/store';
|
4 |
+
import { EffectsModule } from '@ngrx/effects';
|
5 |
|
6 |
import { BooksRoutingModule } from './books-routing.module';
|
7 |
import { BookListComponent } from './book-list/book-list.component';
|
10 |
import { IsbnPipe } from './shared/isbn.pipe';
|
11 |
import { ConfirmDirective } from './shared/confirm.directive';
|
12 |
import { LoggedinOnlyDirective } from './shared/loggedin-only.directive';
|
13 |
+
import { BookEffects } from './store/book.effects';
|
14 |
+
import * as fromBook from './store/book.reducer';
|
15 |
|
16 |
@NgModule({
|
17 |
declarations: [
|
24 |
],
|
25 |
imports: [
|
26 |
CommonModule,
|
27 |
+
BooksRoutingModule,
|
28 |
+
StoreModule.forFeature(fromBook.bookFeatureKey, fromBook.reducer),
|
29 |
+
EffectsModule.forFeature([BookEffects])
|
30 |
]
|
31 |
})
|
32 |
export class BooksModule { }
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { LoggedinOnlyDirective } from './loggedin-only.directive';
|
2 |
+
|
3 |
+
describe('LoggedinOnlyDirective', () => {
|
4 |
+
it('should create an instance', () => {
|
5 |
+
const directive = new LoggedinOnlyDirective();
|
6 |
+
expect(directive).toBeTruthy();
|
7 |
+
});
|
8 |
+
});
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createAction, props } from '@ngrx/store';
|
2 |
+
|
3 |
+
import { Book } from '../../shared/book';
|
4 |
+
|
5 |
+
export const loadBooks = createAction(
|
6 |
+
'[Book] Load Books'
|
7 |
+
);
|
8 |
+
|
9 |
+
export const loadBooksSuccess = createAction(
|
10 |
+
'[Book] Load Books Success',
|
11 |
+
props<{ data: Book[] }>()
|
12 |
+
);
|
13 |
+
|
14 |
+
export const loadBooksFailure = createAction(
|
15 |
+
'[Book] Load Books Failure',
|
16 |
+
props<{ error: string }>()
|
17 |
+
);
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
import { Actions } from '@ngrx/effects';
|
3 |
+
import { provideMockActions } from '@ngrx/effects/testing';
|
4 |
+
import { provideMockStore, MockStore } from '@ngrx/store/testing';
|
5 |
+
import { cold, hot } from 'jasmine-marbles';
|
6 |
+
import { of } from 'rxjs';
|
7 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
8 |
+
|
9 |
+
import { loadBooks, loadBooksSuccess } from './book.actions';
|
10 |
+
import { BookEffects } from './book.effects.alternative';
|
11 |
+
import { initialState } from './book.reducer';
|
12 |
+
import { book } from './test-helper';
|
13 |
+
|
14 |
+
describe('BookEffects', () => {
|
15 |
+
let actions$: Actions;
|
16 |
+
let effects: BookEffects;
|
17 |
+
|
18 |
+
beforeEach(() => {
|
19 |
+
TestBed.configureTestingModule({
|
20 |
+
providers: [
|
21 |
+
BookEffects,
|
22 |
+
provideMockActions(() => actions$),
|
23 |
+
provideMockStore({
|
24 |
+
initialState: { book: initialState }
|
25 |
+
}),
|
26 |
+
{
|
27 |
+
provide: BookStoreService,
|
28 |
+
useValue: {
|
29 |
+
getAll: () => of([])
|
30 |
+
}
|
31 |
+
}
|
32 |
+
]
|
33 |
+
});
|
34 |
+
|
35 |
+
effects = TestBed.inject(BookEffects);
|
36 |
+
});
|
37 |
+
|
38 |
+
it('should fire loadBooksSuccess for loadBooks if store is empty', () => {
|
39 |
+
|
40 |
+
actions$ = hot('--a', { a: loadBooks() });
|
41 |
+
const expected = cold('--b', { b: loadBooksSuccess({ data: [] }) });
|
42 |
+
|
43 |
+
expect(effects.loadBooks$).toBeObservable(expected);
|
44 |
+
});
|
45 |
+
|
46 |
+
it('should do nothing if store is already filled', () => {
|
47 |
+
|
48 |
+
const store = TestBed.inject(MockStore);
|
49 |
+
store.setState({
|
50 |
+
book: {
|
51 |
+
books: [book(1)],
|
52 |
+
loading: false
|
53 |
+
}
|
54 |
+
});
|
55 |
+
|
56 |
+
actions$ = hot('--a', { a: loadBooks() });
|
57 |
+
const expected = cold('---');
|
58 |
+
|
59 |
+
expect(effects.loadBooks$).toBeObservable(expected);
|
60 |
+
});
|
61 |
+
});
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Injectable } from '@angular/core';
|
2 |
+
import { Store } from '@ngrx/store';
|
3 |
+
import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects';
|
4 |
+
import { catchError, filter, map, switchMap } from 'rxjs/operators';
|
5 |
+
import { of } from 'rxjs';
|
6 |
+
|
7 |
+
import * as BookActions from './book.actions';
|
8 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
9 |
+
import { selectAllBooks } from './book.selectors';
|
10 |
+
|
11 |
+
@Injectable()
|
12 |
+
export class BookEffects {
|
13 |
+
|
14 |
+
loadBooks$ = createEffect(() => {
|
15 |
+
return this.actions$.pipe(
|
16 |
+
ofType(BookActions.loadBooks),
|
17 |
+
concatLatestFrom(() => this.store.select(selectAllBooks)),
|
18 |
+
filter(([action, books]) => !books.length),
|
19 |
+
switchMap(() =>
|
20 |
+
this.service.getAll().pipe(
|
21 |
+
map(data => BookActions.loadBooksSuccess({ data })),
|
22 |
+
catchError(error => of(BookActions.loadBooksFailure({ error: error.message })))
|
23 |
+
)
|
24 |
+
)
|
25 |
+
);
|
26 |
+
});
|
27 |
+
|
28 |
+
constructor(
|
29 |
+
private actions$: Actions,
|
30 |
+
private service: BookStoreService,
|
31 |
+
private store: Store
|
32 |
+
) {}
|
33 |
+
}
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
import { provideMockActions } from '@ngrx/effects/testing';
|
3 |
+
import { Action } from '@ngrx/store';
|
4 |
+
import { Observable, of } from 'rxjs';
|
5 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
6 |
+
import { loadBooks, loadBooksSuccess } from './book.actions';
|
7 |
+
import { hot, cold } from 'jasmine-marbles';
|
8 |
+
|
9 |
+
import { BookEffects } from './book.effects';
|
10 |
+
import { book } from './test-helper';
|
11 |
+
|
12 |
+
describe('BookEffects', () => {
|
13 |
+
let actions$: Observable<any>;
|
14 |
+
let effects: BookEffects;
|
15 |
+
|
16 |
+
beforeEach(() => {
|
17 |
+
TestBed.configureTestingModule({
|
18 |
+
providers: [
|
19 |
+
BookEffects,
|
20 |
+
provideMockActions(() => actions$),
|
21 |
+
{
|
22 |
+
provide: BookStoreService,
|
23 |
+
useValue: { getAll: () => of([]) }
|
24 |
+
}
|
25 |
+
]
|
26 |
+
});
|
27 |
+
|
28 |
+
effects = TestBed.inject(BookEffects);
|
29 |
+
});
|
30 |
+
|
31 |
+
it('should fire loadBooksSuccess for loadBooks', () => {
|
32 |
+
const books = [book(1), book(2), book(3)];
|
33 |
+
|
34 |
+
// Implementierung von getAll() ersetzen
|
35 |
+
const bs = TestBed.inject(BookStoreService);
|
36 |
+
spyOn(bs, 'getAll').and.callFake(() => of(books));
|
37 |
+
|
38 |
+
// Action auslösen
|
39 |
+
actions$ = hot('--a', { a: loadBooks() });
|
40 |
+
const expected = cold('--b', { b: loadBooksSuccess({ data: books }) });
|
41 |
+
|
42 |
+
// Datenströme vergleichen
|
43 |
+
expect(effects.loadBooks$).toBeObservable(expected);
|
44 |
+
|
45 |
+
// Serviceaufruf prüfen
|
46 |
+
expect(bs.getAll).toHaveBeenCalled();
|
47 |
+
});
|
48 |
+
});
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { TestBed } from '@angular/core/testing';
|
2 |
+
import { provideMockActions } from '@ngrx/effects/testing';
|
3 |
+
import { Action } from '@ngrx/store';
|
4 |
+
import { Observable, of } from 'rxjs';
|
5 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
6 |
+
import { loadBooks, loadBooksSuccess } from './book.actions';
|
7 |
+
|
8 |
+
import { BookEffects } from './book.effects';
|
9 |
+
import { book } from './test-helper';
|
10 |
+
|
11 |
+
describe('BookEffects', () => {
|
12 |
+
let actions$: Observable<any>;
|
13 |
+
let effects: BookEffects;
|
14 |
+
|
15 |
+
beforeEach(() => {
|
16 |
+
TestBed.configureTestingModule({
|
17 |
+
providers: [
|
18 |
+
BookEffects,
|
19 |
+
provideMockActions(() => actions$),
|
20 |
+
{
|
21 |
+
provide: BookStoreService,
|
22 |
+
useValue: { getAll: () => of([]) }
|
23 |
+
}
|
24 |
+
]
|
25 |
+
});
|
26 |
+
|
27 |
+
effects = TestBed.inject(BookEffects);
|
28 |
+
});
|
29 |
+
|
30 |
+
it('should fire loadBooksSuccess for loadBooks', () => {
|
31 |
+
const books = [book(1), book(2), book(3)];
|
32 |
+
|
33 |
+
// Implementierung von getAll() ersetzen
|
34 |
+
const bs = TestBed.inject(BookStoreService);
|
35 |
+
spyOn(bs, 'getAll').and.callFake(() => of(books));
|
36 |
+
|
37 |
+
// Action auslösen
|
38 |
+
actions$ = of(loadBooks());
|
39 |
+
|
40 |
+
// Actions aus Effect empfangen
|
41 |
+
let dispatchedAction: Action | undefined;
|
42 |
+
effects.loadBooks$.subscribe(action => {
|
43 |
+
dispatchedAction = action;
|
44 |
+
});
|
45 |
+
|
46 |
+
// Actions vergleichen
|
47 |
+
const expectedAction = loadBooksSuccess({ data: books });
|
48 |
+
expect(dispatchedAction).toEqual(expectedAction);
|
49 |
+
|
50 |
+
// Serviceaufruf prüfen
|
51 |
+
expect(bs.getAll).toHaveBeenCalled();
|
52 |
+
});
|
53 |
+
});
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Injectable } from '@angular/core';
|
2 |
+
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
3 |
+
import { catchError, map, switchMap } from 'rxjs/operators';
|
4 |
+
import { of } from 'rxjs';
|
5 |
+
|
6 |
+
import * as BookActions from './book.actions';
|
7 |
+
import { BookStoreService } from '../../shared/book-store.service';
|
8 |
+
|
9 |
+
@Injectable()
|
10 |
+
export class BookEffects {
|
11 |
+
|
12 |
+
loadBooks$ = createEffect(() => {
|
13 |
+
return this.actions$.pipe(
|
14 |
+
ofType(BookActions.loadBooks),
|
15 |
+
switchMap(() =>
|
16 |
+
this.service.getAll().pipe(
|
17 |
+
map(data => BookActions.loadBooksSuccess({ data })),
|
18 |
+
catchError(error => of(BookActions.loadBooksFailure({ error: error.message })))
|
19 |
+
)
|
20 |
+
)
|
21 |
+
);
|
22 |
+
});
|
23 |
+
|
24 |
+
constructor(
|
25 |
+
private actions$: Actions,
|
26 |
+
private service: BookStoreService
|
27 |
+
) {}
|
28 |
+
}
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { loadBooks } from './book.actions';
|
2 |
+
import { reducer } from './book.reducer';
|
3 |
+
|
4 |
+
describe('Book Reducer', () => {
|
5 |
+
it('should enable the loading flag for loadBooks', () => {
|
6 |
+
const state = {
|
7 |
+
books: [],
|
8 |
+
loading: false
|
9 |
+
};
|
10 |
+
const action = loadBooks();
|
11 |
+
|
12 |
+
const newState = reducer(state, action);
|
13 |
+
expect(newState.loading).toBe(true);
|
14 |
+
});
|
15 |
+
});
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createReducer, on } from '@ngrx/store';
|
2 |
+
|
3 |
+
import { Book } from '../../shared/book';
|
4 |
+
import * as BookActions from './book.actions';
|
5 |
+
|
6 |
+
export const bookFeatureKey = 'book';
|
7 |
+
|
8 |
+
export interface State {
|
9 |
+
books: Book[];
|
10 |
+
loading: boolean;
|
11 |
+
}
|
12 |
+
|
13 |
+
export const initialState: State = {
|
14 |
+
books: [],
|
15 |
+
loading: false
|
16 |
+
};
|
17 |
+
|
18 |
+
export const reducer = createReducer(
|
19 |
+
initialState,
|
20 |
+
|
21 |
+
on(BookActions.loadBooks, (state): State => {
|
22 |
+
return { ...state, loading: true };
|
23 |
+
}),
|
24 |
+
|
25 |
+
on(BookActions.loadBooksSuccess, (state, action): State => {
|
26 |
+
return {
|
27 |
+
...state,
|
28 |
+
books: action.data,
|
29 |
+
loading: false
|
30 |
+
};
|
31 |
+
}),
|
32 |
+
|
33 |
+
on(BookActions.loadBooksFailure, (state, action): State => {
|
34 |
+
return { ...state, loading: false };
|
35 |
+
}),
|
36 |
+
);
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { selectAllBooks } from './book.selectors';
|
2 |
+
import { book } from './test-helper';
|
3 |
+
|
4 |
+
describe('Book Selectors', () => {
|
5 |
+
it('should select all books', () => {
|
6 |
+
const books = [book(1), book(2), book(3)];
|
7 |
+
const bookState = { books, loading: false };
|
8 |
+
|
9 |
+
const result = selectAllBooks.projector(bookState);
|
10 |
+
expect(result).toEqual(books);
|
11 |
+
});
|
12 |
+
});
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { selectAllBooks } from './book.selectors';
|
2 |
+
import { book } from './test-helper';
|
3 |
+
|
4 |
+
describe('Book Selectors', () => {
|
5 |
+
it('should select all books', () => {
|
6 |
+
const books = [book(1), book(2), book(3)];
|
7 |
+
const state = {
|
8 |
+
book: { books, loading: false }
|
9 |
+
};
|
10 |
+
|
11 |
+
const result = selectAllBooks(state);
|
12 |
+
expect(result).toEqual(books);
|
13 |
+
});
|
14 |
+
});
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
2 |
+
import * as fromBook from './book.reducer';
|
3 |
+
|
4 |
+
export const selectBookState = createFeatureSelector<fromBook.State>(
|
5 |
+
fromBook.bookFeatureKey
|
6 |
+
);
|
7 |
+
|
8 |
+
export const selectBooksLoading = createSelector(
|
9 |
+
selectBookState,
|
10 |
+
state => state.loading
|
11 |
+
);
|
12 |
+
|
13 |
+
export const selectAllBooks = createSelector(
|
14 |
+
selectBookState,
|
15 |
+
state => state.books
|
16 |
+
);
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Book } from '../../shared/book';
|
2 |
+
|
3 |
+
export function book(i: number): Book {
|
4 |
+
return {
|
5 |
+
isbn: 'isbn' + i,
|
6 |
+
title: 'title' + i,
|
7 |
+
authors: []
|
8 |
+
};
|
9 |
+
}
|
@@ -1,4 +1,3 @@
|
|
1 |
-
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
2 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
3 |
|
4 |
import { HomeComponent } from './home.component';
|
@@ -9,8 +8,7 @@ describe('HomeComponent', () => {
|
|
9 |
|
10 |
beforeEach(async () => {
|
11 |
await TestBed.configureTestingModule({
|
12 |
-
declarations: [ HomeComponent ]
|
13 |
-
schemas: [NO_ERRORS_SCHEMA]
|
14 |
})
|
15 |
.compileComponents();
|
16 |
|
|
|
1 |
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
|
3 |
import { HomeComponent } from './home.component';
|
8 |
|
9 |
beforeEach(async () => {
|
10 |
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ HomeComponent ]
|
|
|
12 |
})
|
13 |
.compileComponents();
|
14 |
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
2 |
+
|
3 |
+
import { SearchComponent } from './search.component';
|
4 |
+
|
5 |
+
describe('SearchComponent', () => {
|
6 |
+
let component: SearchComponent;
|
7 |
+
let fixture: ComponentFixture<SearchComponent>;
|
8 |
+
|
9 |
+
beforeEach(async () => {
|
10 |
+
await TestBed.configureTestingModule({
|
11 |
+
declarations: [ SearchComponent ]
|
12 |
+
})
|
13 |
+
.compileComponents();
|
14 |
+
|
15 |
+
fixture = TestBed.createComponent(SearchComponent);
|
16 |
+
component = fixture.componentInstance;
|
17 |
+
fixture.detectChanges();
|
18 |
+
});
|
19 |
+
|
20 |
+
it('should create', () => {
|
21 |
+
expect(component).toBeTruthy();
|
22 |
+
});
|
23 |
+
});
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
});
|