diff --git a/apps/okreads-e2e/src/integration/search-books.spec.ts b/apps/okreads-e2e/src/integration/search-books.spec.ts index 56a4c33..87dae40 100644 --- a/apps/okreads-e2e/src/integration/search-books.spec.ts +++ b/apps/okreads-e2e/src/integration/search-books.spec.ts @@ -11,7 +11,16 @@ describe('When: Use the search feature', () => { cy.get('[data-testing="book-item"]').should('have.length.greaterThan', 1); }); - xit('Then: I should see search results as I am typing', () => { - // TODO: Implement this test! + it('Then: I should see search results as I am typing', () => { + cy.get('tmo-root').should('contain.text', 'okreads'); + cy.get('#searchInput').type('nodejs'); + cy.get('[data-testing="book-item"]').its('length').should('be.gt', 1); + }); + + it('Then: I should see error message with no data', () => { + const errorMsg = 'Books are not available with the given search input'; + cy.get('tmo-root').should('contain.text', 'okreads'); + cy.get('#searchInput').type('#'); + cy.get('#errorMsg').should('contain.text', errorMsg); }); }); diff --git a/libs/api/books/src/lib/books.service.ts b/libs/api/books/src/lib/books.service.ts index 0b9197b..5844bfc 100644 --- a/libs/api/books/src/lib/books.service.ts +++ b/libs/api/books/src/lib/books.service.ts @@ -1,11 +1,13 @@ import { HttpService, Injectable } from '@nestjs/common'; -import { Book } from '@tmo/shared/models'; + import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { catchError, map } from 'rxjs/operators'; + +import { Book } from '@tmo/shared/models'; @Injectable() export class BooksService { - constructor(private readonly http: HttpService) {} + constructor(private readonly http: HttpService) { } search(term: string): Observable { if (!term) { @@ -29,7 +31,7 @@ export class BooksService { coverUrl: item.volumeInfo?.imageLinks?.thumbnail }; }); - }) + }), catchError(error => { throw error; }) ); } } diff --git a/libs/books/feature/src/lib/book-search/book-search.component.spec.ts b/libs/books/feature/src/lib/book-search/book-search.component.spec.ts index 292a608..8fe7328 100644 --- a/libs/books/feature/src/lib/book-search/book-search.component.spec.ts +++ b/libs/books/feature/src/lib/book-search/book-search.component.spec.ts @@ -1,6 +1,6 @@ import { provideMockStore, MockStore } from '@ngrx/store/testing'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; @@ -166,4 +166,24 @@ describe('BookSearch Component', () => { expect(store.dispatch).not.toHaveBeenCalledWith(clearSearch()); }); + + it('should show books with respect to user input while typing', fakeAsync(() => { + const searchCtrl = fixture.debugElement.query(By.css('#searchInput')); + searchCtrl.nativeElement.value = 'nodejs'; + searchCtrl.nativeElement.dispatchEvent(new Event('input')); + tick(500); + + expect(component.searchForm.value.term).toEqual('nodejs'); + expect(store.dispatch).toHaveBeenCalledWith(searchBooks({ term: 'nodejs' })); + })); + + it('should show error message when user types an input input', fakeAsync(() => { + const searchCtrl = fixture.debugElement.query(By.css('#searchInput')); + searchCtrl.nativeElement.value = '#'; + searchCtrl.nativeElement.dispatchEvent(new Event('input')); + tick(500); + + expect(component.searchForm.value.term).toEqual('#'); + expect(store.dispatch).toHaveBeenCalledWith(searchBooks({ term: encodeURIComponent('#') })); + })); }); diff --git a/libs/books/feature/src/lib/book-search/book-search.component.ts b/libs/books/feature/src/lib/book-search/book-search.component.ts index 0605ecf..cc14455 100644 --- a/libs/books/feature/src/lib/book-search/book-search.component.ts +++ b/libs/books/feature/src/lib/book-search/book-search.component.ts @@ -1,7 +1,13 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { Observable } from 'rxjs'; +import { Observable, Subject } from 'rxjs'; +import { + debounceTime, + distinctUntilChanged, + filter, + takeUntil +} from 'rxjs/operators'; import { Store } from '@ngrx/store'; @@ -21,7 +27,7 @@ import { Book } from '@tmo/shared/models'; templateUrl: './book-search.component.html', styleUrls: ['./book-search.component.scss'] }) -export class BookSearchComponent { +export class BookSearchComponent implements OnInit, OnDestroy { loaderConfig = { color: 'primary', mode: 'indeterminate', @@ -33,11 +39,24 @@ export class BookSearchComponent { books$: Observable = this.store.select(getAllBooks); error$: Observable = this.store.select(getBooksError); loader$: Observable = this.store.select(getBooksLoaded); + searchKey$: Observable<{ term: string }> = this.searchForm.valueChanges.pipe( + filter(key => key.term.trim()), + debounceTime(500), + distinctUntilChanged() + ); + + unSubscribe$ = new Subject(); constructor( private readonly store: Store, private readonly fb: FormBuilder - ) {} + ) { } + + ngOnInit() { + this.searchKey$ + .pipe(takeUntil(this.unSubscribe$)) + .subscribe(searchKey => { this.searchBooks(searchKey.term) }); + } get searchTerm(): string { return this.searchForm.value.term; @@ -55,13 +74,14 @@ export class BookSearchComponent { searchExample() { this.searchForm.controls.term.setValue('javascript'); - this.searchBooks(); + this.searchBooks(null); } - searchBooks() { - if (this.searchForm.value.term.trim()) { + searchBooks(term: string | void) { + const searchTerm = term ? term : this.searchTerm.trim(); - this.store.dispatch(searchBooks({ term: encodeURIComponent(this.searchTerm) })); + if (searchTerm) { + this.store.dispatch(searchBooks({ term: encodeURIComponent(searchTerm) })); return; } @@ -73,4 +93,9 @@ export class BookSearchComponent { this.store.dispatch(clearSearch()); } } + + ngOnDestroy() { + this.unSubscribe$.next(); + this.unSubscribe$.complete(); + } } diff --git a/libs/books/feature/src/lib/books-feature.module.ts b/libs/books/feature/src/lib/books-feature.module.ts index 513e02e..8649cbb 100644 --- a/libs/books/feature/src/lib/books-feature.module.ts +++ b/libs/books/feature/src/lib/books-feature.module.ts @@ -11,6 +11,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatIconModule } from '@angular/material/icon'; import { MatBadgeModule } from '@angular/material/badge'; import { MatSnackBarModule } from '@angular/material/snack-bar'; + import { BooksDataAccessModule } from '@tmo/books/data-access'; import { BookSearchComponent } from './book-search/book-search.component';