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