Merged PR 396: #1193 Trefferliste incl. Unit Tests

Related work items: #1193
This commit is contained in:
Sebastian Neumair
2020-10-27 16:48:06 +00:00
committed by Lorenz Hilpert
8 changed files with 229 additions and 119 deletions

View File

@@ -83,4 +83,6 @@ describe('CustomerSearch', () => {
describe('addLoadingProducts', () => {});
describe('removeLoadingProducts', () => {});
describe('shouldFetchNewProducts', () => {});
});

View File

@@ -135,11 +135,13 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
return this.search();
}
search(options: { isNewSearch: boolean } = { isNewSearch: true }): void {
if (
!options.isNewSearch &&
this.numberOfResultsFetched >= this.totalResults
) {
search(
options: { isNewSearch: boolean; take?: number } = {
isNewSearch: true,
take: 10,
}
): void {
if (!this.shouldFetchNewProducts(options)) {
return; // early exit because no new products need to be fetched
}
if (this.searchState !== 'fetching') {
@@ -150,10 +152,10 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
take(1),
tap((result) => {
const effectiveTake = options.isNewSearch
? 10
: this.numberOfResultsFetched + 10 > result.hits
? options.take
: this.numberOfResultsFetched + options.take > result.hits
? result.hits - this.numberOfResultsFetched
: 10;
: options.take;
this.searchResult$.next(
this.addLoadingProducts(this.searchResult, effectiveTake)
@@ -163,7 +165,7 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
return this.customerSearch
.getCustomers(this.queryFilter.query, {
skip: options.isNewSearch ? 0 : this.numberOfResultsFetched,
take: 10,
take: options.take,
})
.pipe(
takeUntil(this.destroy$),
@@ -196,7 +198,6 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
...r,
result: r.result.map((a) => ({ ...a, loaded: true })),
});
if (this.searchState === 'result') {
if (r.hits === 1) {
this.navigateToDetails(r.result[0].id);
@@ -285,4 +286,19 @@ export abstract class CustomerSearch implements OnInit, OnDestroy {
result: currentResult.result.filter((r) => !!r.loaded),
};
}
private shouldFetchNewProducts(options: {
isNewSearch: boolean;
take?: number;
}): boolean {
if (options.isNewSearch) {
return true;
}
if (this.totalResults > 0) {
return !(this.numberOfResultsFetched + options.take >= this.totalResults);
}
return true;
}
}

View File

@@ -1,5 +1,9 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { EnvironmentService } from '@core/environment';
import {
Component,
OnInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { Filter } from '@ui/filter';
import { CustomerSearch } from '../customer-search.service';

View File

@@ -1,18 +1,12 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { forwardRef } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { ReactiveFormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { PagedResult } from '@domain/defs';
import { CustomerInfoDTO } from '@swagger/crm';
import { EnvironmentService } from '@core/environment';
import { UiIconModule } from '@ui/icon';
import {
UiSearchboxAutocompleteComponent,
UiSearchboxModule,
} from '@ui/searchbox';
import { UiSearchboxModule } from '@ui/searchbox';
import { BehaviorSubject, of } from 'rxjs';
import { take } from 'rxjs/operators';
import { CustomerSearchComponent } from '../customer-search.component';
import { CustomerSearch } from '../customer-search.service';
import { CustomerSearchMainComponent } from './search-main.component';
@@ -20,18 +14,11 @@ import { CustomerSearchMainComponent } from './search-main.component';
describe('CustomerSearchMainComponent', () => {
let fixture: ComponentFixture<CustomerSearchMainComponent>;
let component: CustomerSearchMainComponent;
let autocompleteComponent: UiSearchboxAutocompleteComponent;
let searchService: jasmine.SpyObj<CustomerSearch>;
let envService: jasmine.SpyObj<EnvironmentService>;
const result = [{ id: 'test 1' }, { id: 'test 2' }];
const pagedResult: PagedResult<CustomerInfoDTO> = {
hits: 2,
result: [{ userName: '1' }, { userName: '2' }],
};
const mockActivatedRoute = ({
snapshot: { queryParams: { query: 'test' } },
} as unknown) as ActivatedRoute;
beforeEach(() => {
TestBed.configureTestingModule({
@@ -45,30 +32,31 @@ describe('CustomerSearchMainComponent', () => {
],
providers: [
CustomerSearchComponent,
{
provide: EnvironmentService,
useValue: jasmine.createSpyObj('environmentService', ['isMobile']),
},
{
provide: CustomerSearch,
useExisting: forwardRef(() => CustomerSearchComponent),
},
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
],
}).compileComponents();
fixture = TestBed.createComponent(CustomerSearchMainComponent);
component = fixture.componentInstance;
autocompleteComponent = TestBed.createComponent(
UiSearchboxAutocompleteComponent
).componentInstance;
component.autocomplete = autocompleteComponent;
component.search.queryControl = new FormControl('');
searchService = TestBed.inject(CustomerSearch) as jasmine.SpyObj<
CustomerSearch
>;
searchService.autocompleteResult$ = of(result);
searchService.searchState$ = new BehaviorSubject('init');
searchService.searchResult$ = new BehaviorSubject(pagedResult);
envService = TestBed.inject(EnvironmentService) as jasmine.SpyObj<
EnvironmentService
>;
envService.isMobile.and.returnValue(Promise.resolve(true));
});
it('should be created', () => {
@@ -76,90 +64,21 @@ describe('CustomerSearchMainComponent', () => {
expect(component instanceof CustomerSearchMainComponent).toBeTruthy();
});
describe('initAutocomplete', () => {
describe('detectDevice', () => {
beforeEach(() => {
spyOn(component, 'detectDevice').and.callThrough();
fixture.detectChanges();
spyOn(component, 'initAutocomplete').and.callThrough();
spyOn(component.autocomplete, 'open').and.callFake(() => {});
spyOn(component.autocomplete, 'close').and.callFake(() => {});
});
it('should be called OnInit', async () => {
component.ngOnInit();
await searchService.autocompleteResult$.pipe(take(1)).toPromise();
expect(component.initAutocomplete).toHaveBeenCalledTimes(1);
expect(component.detectDevice).toHaveBeenCalledTimes(1);
});
it('should open the autocomplete results', async () => {
component.ngOnInit();
it('should set isMobile', async () => {
expect(envService.isMobile).toHaveBeenCalled();
await searchService.autocompleteResult$.pipe(take(1)).toPromise();
expect(component.autocomplete.open).toHaveBeenCalledTimes(1);
});
it('should close the autocomplete results', async () => {
searchService.autocompleteResult$ = of([]);
component.ngOnInit();
await searchService.autocompleteResult$.pipe(take(1)).toPromise();
expect(component.autocomplete.close).toHaveBeenCalledTimes(1);
});
});
describe('parseQueryParams', () => {
it('should be called onInit', () => {
spyOn(component, 'initAutocomplete').and.callFake(() => {});
spyOn(component, 'subscribeToSearchResult').and.callFake(() => {});
spyOn(component.search, 'parseQueryParams').and.callFake(() => {});
component.ngOnInit();
expect(component.search.parseQueryParams).toHaveBeenCalledWith(
mockActivatedRoute
);
});
});
describe('subscribeToSearchResult', () => {
beforeEach(() => {
searchService.searchState$ = new BehaviorSubject('result');
});
it('should be called OnInit', () => {
spyOn(component, 'initAutocomplete').and.callFake(() => {});
spyOn(component.search, 'parseQueryParams').and.callFake(() => {});
spyOn(component, 'subscribeToSearchResult').and.callFake(() => {});
component.ngOnInit();
expect(component.subscribeToSearchResult).toHaveBeenCalledTimes(1);
});
it('should navigate to results if more than 1 result exists', async () => {
spyOn(component.search, 'navigateToResults').and.callFake(() => {});
component.subscribeToSearchResult();
await component.search.searchResult$.pipe(take(1)).toPromise();
expect(component.search.navigateToResults).toHaveBeenCalled();
});
it('should navigate to details if 1 result exists', async () => {
const singleResult: PagedResult<CustomerInfoDTO> = {
hits: 1,
result: [{ userName: '1', id: 1 }],
};
searchService.searchResult$ = new BehaviorSubject(singleResult);
spyOn(component.search, 'navigateToDetails').and.callFake(() => {});
component.subscribeToSearchResult();
await component.search.searchResult$.pipe(take(1)).toPromise();
expect(component.search.navigateToDetails).toHaveBeenCalledWith(
singleResult.result[0].id
);
await envService.isMobile();
expect(component['isMobile']).toBeTruthy();
});
});
});

View File

@@ -1,9 +1,12 @@
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnDestroy,
OnInit,
} from '@angular/core';
import { EnvironmentService } from '@core/environment';
import { UiSearchboxAutocompleteComponent } from '@ui/searchbox';
import { Subject } from 'rxjs';
import { delay, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { CustomerSearch } from '../customer-search.service';
@Component({
@@ -20,7 +23,6 @@ export class CustomerSearchMainComponent implements OnInit, OnDestroy {
constructor(
public search: CustomerSearch,
public cdr: ChangeDetectorRef,
private activatedRoute: ActivatedRoute,
public environmentService: EnvironmentService
) {}

View File

@@ -0,0 +1,79 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { CustomerSearchType } from '../../defs';
import { CustomerResultCardComponent } from './customer-result-card.component';
describe('CustomerResultCardComponent', () => {
let fixture: ComponentFixture<CustomerResultCardComponent>;
let component: CustomerResultCardComponent;
const mockCustomer: CustomerSearchType = {
loaded: true,
created: new Date().toISOString(),
customerType: 8,
firstName: 'Unit',
lastName: 'Test',
communicationDetails: {
email: 'unit@test.de',
},
features: [{ enabled: true, description: 'Great Feature' }],
};
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [CustomerResultCardComponent],
}).compileComponents();
fixture = TestBed.createComponent(CustomerResultCardComponent);
component = fixture.componentInstance;
component.customer = mockCustomer;
});
it('should be created', () => {
expect(component instanceof CustomerResultCardComponent).toBeTruthy();
});
it('should show the customers name', () => {
fixture.detectChanges();
const name: HTMLSpanElement = fixture.debugElement.query(
By.css('.heading > span')
).nativeElement;
expect(name.textContent).toContain(mockCustomer.firstName);
expect(name.textContent).toContain(mockCustomer.lastName);
});
it('should show the customers created date', () => {
fixture.detectChanges();
const createdDate: HTMLSpanElement = fixture.debugElement.query(
By.css('.date')
).nativeElement;
expect(createdDate.textContent).toContain(
String(new Date(mockCustomer.created).getMonth() + 1)
);
expect(createdDate.textContent).toContain(
String(new Date(mockCustomer.created).getDay())
);
});
it('should show thecustomers features', () => {
fixture.detectChanges();
const features: HTMLDivElement[] = fixture.debugElement
.queryAll(By.css('.feature'))
.map((db) => db.nativeElement);
const firstFeatures: HTMLSpanElement = fixture.debugElement.query(
By.css('.feature > span ')
).nativeElement;
expect(features.length).toEqual(mockCustomer.features.length);
expect(firstFeatures.textContent).toContain(
mockCustomer.features[0].description
);
});
});

View File

@@ -0,0 +1,88 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { PagedResult } from 'apps/domain/defs/src/lib/models';
import { BehaviorSubject } from 'rxjs';
import { CustomerSearchModule } from '../customer-search.module';
import { CustomerSearch } from '../customer-search.service';
import { CustomerSearchType, ResultState } from '../defs';
import { CustomerSearchResultComponent } from './search-results.component';
class MockCustomerSearch {
searchState$ = new BehaviorSubject<ResultState>('init');
searchResult$ = new BehaviorSubject<PagedResult<CustomerSearchType>>({
result: [{} as CustomerSearchType, {} as CustomerSearchType],
});
search(options?: { isNewSearch: boolean; take?: number }) {
return options;
}
}
fdescribe('CustomerSearchResultComponent', () => {
let fixture: ComponentFixture<CustomerSearchResultComponent>;
let component: CustomerSearchResultComponent;
let searchService: CustomerSearch;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CustomerSearchModule],
providers: [
{
provide: CustomerSearch,
useClass: MockCustomerSearch,
},
],
}).compileComponents();
fixture = TestBed.createComponent(CustomerSearchResultComponent);
component = fixture.componentInstance;
searchService = TestBed.inject(CustomerSearch);
});
it('should be created', () => {
expect(component instanceof CustomerSearchResultComponent).toBeTruthy();
});
describe('checkIfReload', () => {
beforeEach(() => {
spyOn(component, 'triggerSearch').and.callThrough();
spyOn(component, 'checkIfReload').and.callThrough();
});
it('should be called on viewportEntered Event', () => {
fixture.detectChanges();
const resultCard = fixture.debugElement.query(By.css('a'));
const target: HTMLAnchorElement = resultCard.nativeElement;
target.classList.add('last');
resultCard.triggerEventHandler('viewportEntered', target);
expect(component.checkIfReload).toHaveBeenCalledWith(target);
expect(component.triggerSearch).toHaveBeenCalled();
});
it('should not call triggerSearch if the target element does not include the last class', () => {
fixture.detectChanges();
const resultCard = fixture.debugElement.query(By.css('a'));
const target: HTMLAnchorElement = resultCard.nativeElement;
resultCard.triggerEventHandler('viewportEntered', target);
expect(component.triggerSearch).not.toHaveBeenCalled();
});
});
describe('triggerSearch', () => {
it('should call search on the SearchService', () => {
spyOn(searchService, 'search').and.callThrough();
component.triggerSearch();
expect(searchService.search).toHaveBeenCalledWith({
isNewSearch: false,
take: 10,
});
});
});
});

View File

@@ -31,6 +31,6 @@ export class CustomerSearchResultComponent implements OnInit, OnDestroy {
}
triggerSearch() {
this.search.search({ isNewSearch: false });
this.search.search({ isNewSearch: false, take: 10 });
}
}