From b01b82e3b5a033f9e0aad0dc81eed7659eb0c917 Mon Sep 17 00:00:00 2001 From: OZGCloud <ozgcloud@mgm-tp.com> Date: Fri, 9 Aug 2024 10:52:02 +0200 Subject: [PATCH] OZG-6129 improve test --- .../instant-search.component.spec.ts | 161 +++++++++++++----- .../instant-search.component.ts | 50 +++--- .../instant-search/instant-search.model.ts | 4 + 3 files changed, 152 insertions(+), 63 deletions(-) diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.spec.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.spec.ts index f6f686486f..95c3f7d780 100644 --- a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.spec.ts +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.spec.ts @@ -1,9 +1,15 @@ -import { getElementFromFixtureByType } from '@alfa-client/test-utils'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { EventEmitter } from '@angular/core'; +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + tick, +} from '@angular/core/testing'; import { Subscription } from 'rxjs'; -import { SearchResultItemComponent } from '../search-result-item/search-result-item.component'; import { InstantSearchComponent } from './instant-search.component'; -import { InstantSearchResult } from './instant-search.model'; +import { InstantSearchQuery, InstantSearchResult } from './instant-search.model'; describe('InstantSearchComponent', () => { let component: InstantSearchComponent; @@ -13,14 +19,23 @@ describe('InstantSearchComponent', () => { { title: 'test', description: 'test' }, { title: 'caption', description: 'desc' }, ]; + const searchBy: string = 'query'; + + let searchQueryChanged: Mock<EventEmitter<any>>; + let searchResultSelected: Mock<EventEmitter<any>>; beforeEach(async () => { + searchQueryChanged = <any>mock(EventEmitter); + searchResultSelected = <any>mock(EventEmitter); + await TestBed.configureTestingModule({ imports: [InstantSearchComponent], }).compileComponents(); fixture = TestBed.createComponent(InstantSearchComponent); component = fixture.componentInstance; + component.searchQueryChanged = useFromMock(searchQueryChanged); + component.searchResultSelected = useFromMock(searchResultSelected); fixture.detectChanges(); }); @@ -29,26 +44,110 @@ describe('InstantSearchComponent', () => { }); describe('ngOnInit', () => { - // it('should call showResults after inputting 2 chars', fakeAsync(() => { - // component.showResults = jest.fn(); - // const input: HTMLInputElement = getElementFromFixture(fixture, 'input'); + it('should handle value changes', () => { + component.handleValueChanges = jest.fn(); - // component.ngOnInit(); - // input.value = 'Am'; - // dispatchEventFromFixture(fixture, 'input', 'input'); - // fixture.detectChanges(); - // tick(400); + component.ngOnInit(); - // expect(component.showResults).toHaveBeenCalledTimes(1); - // })); + expect(component.handleValueChanges).toHaveBeenCalled(); + }); + }); + + describe('handleValueChanges', () => { + beforeEach(() => { + component.showResults = jest.fn(); + }); it('should subscribe to value changes', () => { component.control.valueChanges.subscribe = jest.fn(); - component.ngOnInit(); + component.handleValueChanges(); expect(component.control.valueChanges.subscribe).toHaveBeenCalled(); }); + + it('should emit query', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + component.control.valueChanges.subscribe(); + + expect(searchQueryChanged.emit).toHaveBeenCalledWith({ searchBy } as InstantSearchQuery); + })); + + it('should not emit query', fakeAsync(() => { + component.handleValueChanges(); + + const searchBy: string = 'q'; + component.control.setValue(searchBy); + + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + component.control.valueChanges.subscribe(); + + expect(searchQueryChanged.emit).not.toHaveBeenCalled(); + })); + + describe('result are already visible', () => { + beforeEach(() => { + component.areResultsVisible = true; + }); + + it('should not show results', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + + component.control.valueChanges.subscribe(); + expect(component.showResults).not.toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + }); + + describe('results are not visible', () => { + beforeEach(() => { + component.areResultsVisible = false; + }); + + it('should show results', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + + component.control.valueChanges.subscribe(); + expect(component.showResults).toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + + it('should not show results if debounce time not reached', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS - 1); + + component.control.valueChanges.subscribe(); + expect(component.showResults).not.toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + + it('should not show results if not enough characters entered', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue('q'); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + + component.control.valueChanges.subscribe(); + expect(component.showResults).not.toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + }); }); describe('ngOnDestroy', () => { @@ -62,21 +161,6 @@ describe('InstantSearchComponent', () => { }); }); - describe('searchResultSelected', () => { - it('should emit event', () => { - const result: InstantSearchResult<unknown> = { title: 'test', description: 'test' }; - component.searchResults = [result]; - component.showResults(); - fixture.detectChanges(); - const element = getElementFromFixtureByType(fixture, SearchResultItemComponent); - const emitSpy = jest.spyOn(component.searchResultSelected, 'emit'); - - element.itemClicked.emit(); - - expect(emitSpy).toHaveBeenCalledWith(result); - }); - }); - describe('set searchResults', () => { describe('on different results', () => { it('should call setSearchResults', () => { @@ -132,6 +216,7 @@ describe('InstantSearchComponent', () => { beforeEach(() => { component.resultsRef.get = jest.fn().mockReturnValue({ setFocus: jest.fn() }); }); + it('should call get for resultsRef with index', () => { component.setFocusOnResultItem(1); @@ -300,7 +385,7 @@ describe('InstantSearchComponent', () => { it('should set isShowResults to true', () => { component.showResults(); - expect(component.isShowResults).toBe(true); + expect(component.areResultsVisible).toBe(true); }); }); @@ -308,7 +393,7 @@ describe('InstantSearchComponent', () => { it('should set isShowResults to false', () => { component.hideResults(); - expect(component.isShowResults).toBe(false); + expect(component.areResultsVisible).toBe(false); }); }); @@ -482,23 +567,21 @@ describe('InstantSearchComponent', () => { }); }); - describe('onClickItem', () => { + describe('onItemClicked', () => { it('should emit searchResultSelected', () => { - component.searchResultSelected.emit = jest.fn(); - - component.onClickItem(searchResults[0], 0); + component.onItemClicked(searchResults[0], 0); - expect(component.searchResultSelected.emit).toHaveBeenCalledWith(searchResults[0]); + expect(searchResultSelected.emit).toHaveBeenCalledWith(searchResults[0]); }); }); describe('onClickHandler', () => { + const e: MouseEvent = { ...new MouseEvent('test') }; + beforeEach(() => { component.hideResults = jest.fn(); }); - const e: MouseEvent = { ...new MouseEvent('test') }; - it('should call hideResults if instant search does not contain event target', () => { component.onClickHandler(e); diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts index 39c716b45f..c0fbaabedc 100644 --- a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts @@ -20,7 +20,7 @@ import { SearchFieldComponent } from '../search-field/search-field.component'; import { SearchResultHeaderComponent } from '../search-result-header/search-result-header.component'; import { SearchResultItemComponent } from '../search-result-item/search-result-item.component'; import { SearchResultLayerComponent } from '../search-result-layer/search-result-layer.component'; -import { InstantSearchResult } from './instant-search.model'; +import { InstantSearchQuery, InstantSearchResult } from './instant-search.model'; @Component({ selector: 'ods-instant-search', @@ -45,7 +45,7 @@ import { InstantSearchResult } from './instant-search.model'; /> <ods-aria-live-region [text]="ariaLiveText" /> <ods-search-result-layer - *ngIf="results.length && isShowResults" + *ngIf="results.length && areResultsVisible" class="absolute z-50 mt-3 w-full" id="results" > @@ -54,13 +54,15 @@ import { InstantSearchResult } from './instant-search.model'; *ngFor="let result of results; let i = index" [title]="result.title" [description]="result.description" - (itemClicked)="onClickItem(result, i)" + (itemClicked)="onItemClicked(result, i)" #results ></ods-search-result-item> </ods-search-result-layer> </div>`, }) export class InstantSearchComponent implements OnInit, OnDestroy { + static readonly DEBOUNCE_TIME_IN_MILLIS: number = 300; + @Input() label: string = EMPTY_STRING; @Input() placeholder: string = EMPTY_STRING; @Input() headerText: string = EMPTY_STRING; @@ -75,12 +77,14 @@ export class InstantSearchComponent implements OnInit, OnDestroy { @Output() searchResultSelected: EventEmitter<InstantSearchResult<unknown>> = new EventEmitter< InstantSearchResult<unknown> >(); + @Output() searchQueryChanged: EventEmitter<InstantSearchQuery> = + new EventEmitter<InstantSearchQuery>(); readonly FIRST_ITEM_INDEX: number = 0; readonly PREVIEW_SEARCH_STRING_MIN_LENGTH: number = 2; results: InstantSearchResult<unknown>[] = []; ariaLiveText: string = ''; - isShowResults: boolean = true; + areResultsVisible: boolean = true; private focusedResult: number | undefined = undefined; formControlSubscription: Subscription; @@ -89,15 +93,21 @@ export class InstantSearchComponent implements OnInit, OnDestroy { @ViewChildren('results') resultsRef: QueryList<SearchResultItemComponent>; ngOnInit(): void { + this.handleValueChanges(); + } + + handleValueChanges() { this.formControlSubscription = this.control.valueChanges .pipe( - debounceTime(300), - filter(() => !this.isShowResults), + debounceTime(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS), filter((value: string) => value.length >= this.PREVIEW_SEARCH_STRING_MIN_LENGTH), distinctUntilChanged(), ) - .subscribe(() => { - this.showResults(); + .subscribe((searchBy: string) => { + this.searchQueryChanged.emit({ searchBy }); + if (!this.areResultsVisible) { + this.showResults(); + } }); } @@ -141,22 +151,14 @@ export class InstantSearchComponent implements OnInit, OnDestroy { } getNextResultIndex(index: number | undefined, resultLength: number): number { - if (isUndefined(index)) { - return this.FIRST_ITEM_INDEX; - } - if (this.isLastItemOrOutOfArray(index, resultLength)) { - return this.FIRST_ITEM_INDEX; - } + if (isUndefined(index)) return this.FIRST_ITEM_INDEX; + if (this.isLastItemOrOutOfArray(index, resultLength)) return this.FIRST_ITEM_INDEX; return index + 1; } getPreviousResultIndex(index: number | undefined, resultLength: number): number { - if (isUndefined(index)) { - return this.getLastItemIndex(resultLength); - } - if (this.isFirstItemOrOutOfArray(index)) { - return this.getLastItemIndex(resultLength); - } + if (isUndefined(index)) return this.getLastItemIndex(resultLength); + if (this.isFirstItemOrOutOfArray(index)) return this.getLastItemIndex(resultLength); return index - 1; } @@ -185,12 +187,12 @@ export class InstantSearchComponent implements OnInit, OnDestroy { } showResults(): void { - this.isShowResults = true; + this.areResultsVisible = true; this.focusedResult = undefined; } hideResults(): void { - this.isShowResults = false; + this.areResultsVisible = false; this.focusedResult = undefined; } @@ -214,8 +216,8 @@ export class InstantSearchComponent implements OnInit, OnDestroy { return e.key === 'Escape'; } - onClickItem(result: InstantSearchResult<unknown>, index: number) { - this.searchResultSelected.emit(result); + onItemClicked(searchResult: InstantSearchResult<unknown>, index: number) { + this.searchResultSelected.emit(searchResult); this.focusedResult = index; } } diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.model.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.model.ts index 8b09565f30..5debae8ceb 100644 --- a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.model.ts +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.model.ts @@ -3,3 +3,7 @@ export interface InstantSearchResult<T> { description: string; data?: T; } + +export interface InstantSearchQuery { + searchBy: string; +} -- GitLab