From c52502c5b68d9eb5ed515d7343c554e8c729893d Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Fri, 9 Aug 2024 08:37:25 +0200
Subject: [PATCH] OZG-6129 Add unit tests

---
 .../apps/demo/src/app/app.component.ts        |  10 +-
 .../instant-search.component.spec.ts          | 254 ++++++++++++++++--
 .../instant-search.component.ts               |  84 ++++--
 .../instant-search/instant-search.model.ts    |   2 +-
 .../search-result-item.component.spec.ts      |   8 +-
 .../search-result-item.component.ts           |   8 +-
 6 files changed, 301 insertions(+), 65 deletions(-)

diff --git a/alfa-client/apps/demo/src/app/app.component.ts b/alfa-client/apps/demo/src/app/app.component.ts
index 2e7493754a..31d5d75101 100644
--- a/alfa-client/apps/demo/src/app/app.component.ts
+++ b/alfa-client/apps/demo/src/app/app.component.ts
@@ -77,19 +77,19 @@ export class AppComponent {
 
   instantSearchItems: InstantSearchResult<unknown>[] = [
     {
-      caption: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht',
+      title: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht',
       description: 'Fabrikstraße  8-10, 24103 Kiel',
     },
     {
-      caption: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck',
+      title: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck',
       description: 'Rathausmarkt 7, Hersbruck',
     },
     {
-      caption: 'Amt für Digitalisierung, Breitband und Vermessung Stuttgart',
+      title: 'Amt für Digitalisierung, Breitband und Vermessung Stuttgart',
       description: 'Rathausmarkt 7, Stuttgart',
     },
     {
-      caption: 'Amt für Digitalisierung, Breitband und Vermessung Ulm',
+      title: 'Amt für Digitalisierung, Breitband und Vermessung Ulm',
       description: 'Rathausmarkt 7, Ulm',
     },
   ];
@@ -98,7 +98,7 @@ export class AppComponent {
   getInstantSearchResults() {
     if (this.instantSearchFormControl.value.length < 2) return [];
     return this.instantSearchItems.filter((item) =>
-      item.caption.includes(this.instantSearchFormControl.value),
+      item.title.toLowerCase().includes(this.instantSearchFormControl.value.toLowerCase()),
     );
   }
 
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 d61bc19f65..d9ae0f0ab8 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,5 +1,6 @@
 import { getElementFromFixtureByType } from '@alfa-client/test-utils';
 import { ComponentFixture, TestBed } 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';
@@ -9,8 +10,8 @@ describe('InstantSearchComponent', () => {
   let fixture: ComponentFixture<InstantSearchComponent>;
 
   const searchResults: InstantSearchResult<unknown>[] = [
-    { caption: 'test', description: 'test' },
-    { caption: 'caption', description: 'desc' },
+    { title: 'test', description: 'test' },
+    { title: 'caption', description: 'desc' },
   ];
 
   beforeEach(async () => {
@@ -28,6 +29,19 @@ describe('InstantSearchComponent', () => {
   });
 
   describe('ngOnInit', () => {
+    // it('should call showResults after inputting 2 chars', fakeAsync(() => {
+    //   component.showResults = jest.fn();
+    //   const input: HTMLInputElement = getElementFromFixture(fixture, 'input');
+
+    //   component.ngOnInit();
+    //   input.value = 'Am';
+    //   dispatchEventFromFixture(fixture, 'input', 'input');
+    //   fixture.detectChanges();
+    //   tick(400);
+
+    //   expect(component.showResults).toHaveBeenCalledTimes(1);
+    // }));
+
     it('should subscribe to value changes', () => {
       component.control.valueChanges.subscribe = jest.fn();
 
@@ -37,42 +51,106 @@ describe('InstantSearchComponent', () => {
     });
   });
 
+  describe('ngOnDestroy', () => {
+    it('should subscribe to value changes', () => {
+      component.formControlSubscription = new Subscription();
+      component.formControlSubscription.unsubscribe = jest.fn();
+
+      component.ngOnDestroy();
+
+      expect(component.formControlSubscription.unsubscribe).toHaveBeenCalled();
+    });
+  });
+
   describe('searchResultSelected', () => {
     it('should emit event', () => {
-      const result = { caption: 'test', description: 'test' };
+      const result: InstantSearchResult<unknown> = { title: 'test', description: 'test' };
       component.searchResults = [result];
-      component.isShowResults = true;
+      component.showResults();
       fixture.detectChanges();
       const element = getElementFromFixtureByType(fixture, SearchResultItemComponent);
       const emitSpy = jest.spyOn(component.searchResultSelected, 'emit');
 
-      element.clickItem.emit();
+      element.itemClicked.emit();
 
       expect(emitSpy).toHaveBeenCalledWith(result);
     });
   });
 
   describe('set searchResults', () => {
-    it('should set results if they are different', () => {
-      component.searchResults = searchResults;
+    describe('on different results', () => {
+      it('should call setSearchResults', () => {
+        component.setSearchResults = jest.fn();
+
+        component.searchResults = searchResults;
+
+        expect(component.setSearchResults).toHaveBeenCalled();
+      });
+    });
+
+    describe('on same results', () => {
+      it('should not call setSearchResults', () => {
+        component.setSearchResults = jest.fn();
+
+        component.searchResults = [];
+
+        expect(component.setSearchResults).not.toHaveBeenCalled();
+      });
+    });
+
+    describe('on null or undefined', () => {
+      it.each([null, undefined])(
+        'should not call setSearchResults for %s',
+        (searchResults: InstantSearchResult<unknown>[]) => {
+          component.setSearchResults = jest.fn();
+
+          component.searchResults = searchResults;
+
+          expect(component.setSearchResults).not.toHaveBeenCalled();
+        },
+      );
+    });
+  });
+
+  describe('setSearchResults', () => {
+    it('should set results', () => {
+      component.setSearchResults(searchResults);
 
       expect(component.results).toEqual(searchResults);
     });
 
-    it('should call buildAriaLiveText with search results length if results are different', () => {
+    it('should call buildAriaLiveText with search results length', () => {
       component.buildAriaLiveText = jest.fn();
 
-      component.searchResults = searchResults;
+      component.setSearchResults(searchResults);
 
       expect(component.buildAriaLiveText).toHaveBeenCalledWith(searchResults.length);
     });
   });
 
+  describe('setFocusOnResultItem', () => {
+    beforeEach(() => {
+      component.resultsRef.get = jest.fn().mockReturnValue({ setFocus: jest.fn() });
+    });
+    it('should call get for resultsRef with index', () => {
+      component.setFocusOnResultItem(1);
+
+      expect(component.resultsRef.get).toHaveBeenCalledWith(1);
+    });
+
+    it('should call setFocus', () => {
+      component.setFocusOnResultItem(1);
+
+      expect(component.resultsRef.get(1).setFocus).toHaveBeenCalled();
+    });
+  });
+
   describe('handleArrowNavigation', () => {
     beforeEach(() => {
       component.setFocusOnResultItem = jest.fn();
+      component.getResultIndexForKey = jest.fn();
     });
-    const event = new KeyboardEvent('arrow');
+    const event: KeyboardEvent = new KeyboardEvent('arrow');
 
     it('should call prevent default', () => {
       event.preventDefault = jest.fn();
@@ -83,15 +161,13 @@ describe('InstantSearchComponent', () => {
     });
 
     it('should call getResultIndexForKey with key of event', () => {
-      component.getResultIndexForKey = jest.fn();
-
       component.handleArrowNavigation(event);
 
       expect(component.getResultIndexForKey).toHaveBeenCalledWith(event.key);
     });
 
     it('should call setFocusOnResultItem new index', () => {
-      component.getResultIndexForKey = jest.fn(() => 0);
+      component.getResultIndexForKey = jest.fn().mockReturnValue(0);
 
       component.handleArrowNavigation(event);
 
@@ -100,7 +176,7 @@ describe('InstantSearchComponent', () => {
   });
 
   describe('handleEscape', () => {
-    const event = new KeyboardEvent('esc');
+    const event: KeyboardEvent = new KeyboardEvent('esc');
 
     it('should call prevent default', () => {
       event.preventDefault = jest.fn();
@@ -121,39 +197,39 @@ describe('InstantSearchComponent', () => {
 
   describe('getNextResultIndex', () => {
     it('should return 0 if index is undefined', () => {
-      const result = component.getNextResultIndex(undefined, 2);
+      const result: number = component.getNextResultIndex(undefined, 2);
 
       expect(result).toBe(0);
     });
 
-    it('should return 0 if index is more or equal than results length', () => {
-      const result = component.getNextResultIndex(2, 2);
+    it('should return 0 if current index is last', () => {
+      const result: number = component.getNextResultIndex(1, 2);
 
       expect(result).toBe(0);
     });
 
-    it('should iterate', () => {
-      const result = component.getNextResultIndex(0, 2);
+    it('should return next search result index', () => {
+      const result: number = component.getNextResultIndex(0, 2);
 
       expect(result).toBe(1);
     });
   });
 
   describe('getPreviousResultIndex', () => {
-    it('should return results length -1 if index is undefined', () => {
-      const result = component.getPreviousResultIndex(undefined, 2);
+    it('should return last index if current index is undefined', () => {
+      const result: number = component.getPreviousResultIndex(undefined, 2);
 
       expect(result).toBe(1);
     });
 
-    it('should return results length -1 if index is less or equal than 0', () => {
-      const result = component.getPreviousResultIndex(0, 2);
+    it('should return last index if current index is first', () => {
+      const result: number = component.getPreviousResultIndex(0, 2);
 
       expect(result).toBe(1);
     });
 
-    it('should decrement', () => {
-      const result = component.getPreviousResultIndex(1, 2);
+    it('should return previous search result index', () => {
+      const result: number = component.getPreviousResultIndex(1, 2);
 
       expect(result).toBe(0);
     });
@@ -177,13 +253,27 @@ describe('InstantSearchComponent', () => {
     });
   });
 
+  describe('getLastItemIndex', () => {
+    it('should return 0', () => {
+      const result: number = component.getLastItemIndex(0);
+
+      expect(result).toBe(0);
+    });
+
+    it('should return decrement of array length', () => {
+      const result: number = component.getLastItemIndex(5);
+
+      expect(result).toBe(4);
+    });
+  });
+
   describe('buildAriaLiveText', () => {
     beforeEach(() => {
       component.control.setValue('test');
     });
 
     it('should return text for one result', () => {
-      const result = component.buildAriaLiveText(1);
+      const result: string = component.buildAriaLiveText(1);
 
       expect(result).toBe(
         'Ein Suchergebnis für Eingabe test. Nutze Pfeiltaste nach unten, um das zu erreichen.',
@@ -191,7 +281,7 @@ describe('InstantSearchComponent', () => {
     });
 
     it('should return text for many results', () => {
-      const result = component.buildAriaLiveText(4);
+      const result: string = component.buildAriaLiveText(4);
 
       expect(result).toBe(
         '4 Suchergebnisse für Eingabe test. Nutze Pfeiltaste nach unten, um diese zu erreichen.',
@@ -199,7 +289,7 @@ describe('InstantSearchComponent', () => {
     });
 
     it('should return text for no results', () => {
-      const result = component.buildAriaLiveText(0);
+      const result: string = component.buildAriaLiveText(0);
 
       expect(result).toBe('Keine Ergebnisse');
     });
@@ -221,6 +311,92 @@ describe('InstantSearchComponent', () => {
     });
   });
 
+  describe('isSearchResultsEmpty', () => {
+    it('should return true', () => {
+      component.results = [];
+
+      const result: boolean = component.isSearchResultsEmpty();
+
+      expect(result).toBe(true);
+    });
+
+    it('should return false', () => {
+      component.results = searchResults;
+
+      const result: boolean = component.isSearchResultsEmpty();
+
+      expect(result).toBe(false);
+    });
+  });
+
+  describe('isArrowNavigationKey', () => {
+    it('should return true if key is ArrowDown', () => {
+      const arrowDownEvent = { ...new KeyboardEvent('key'), key: 'ArrowDown' };
+
+      const result: boolean = component.isArrowNavigationKey(arrowDownEvent);
+
+      expect(result).toBe(true);
+    });
+
+    it('should return true if key is ArrowUp', () => {
+      const arrowUpEvent = { ...new KeyboardEvent('navigation'), key: 'ArrowUp' };
+
+      const result: boolean = component.isArrowNavigationKey(arrowUpEvent);
+
+      expect(result).toBe(true);
+    });
+
+    it('should return false', () => {
+      const result: boolean = component.isArrowNavigationKey(new KeyboardEvent('not arrow'));
+
+      expect(result).toBe(false);
+    });
+  });
+
+  describe('isEscapeKey', () => {
+    it('should return true', () => {
+      const escapeKeyEvent = { ...new KeyboardEvent('esc'), key: 'Escape' };
+
+      const result: boolean = component.isEscapeKey(escapeKeyEvent);
+
+      expect(result).toBe(true);
+    });
+
+    it('should return false', () => {
+      const result: boolean = component.isEscapeKey(new KeyboardEvent('not escape'));
+
+      expect(result).toBe(false);
+    });
+  });
+
+  describe('isLastItemOrOutOfArray', () => {
+    it.each([3, 5])('should return true for %s', (index: number) => {
+      const result: boolean = component.isLastItemOrOutOfArray(index, 4);
+
+      expect(result).toBe(true);
+    });
+
+    it('should return false', () => {
+      const result: boolean = component.isLastItemOrOutOfArray(1, 3);
+
+      expect(result).toBe(false);
+    });
+  });
+
+  describe('isFirstItemOrOutOfArray', () => {
+    it.each([0, -1])('should return true for %s', (index: number) => {
+      const result: boolean = component.isFirstItemOrOutOfArray(index);
+
+      expect(result).toBe(true);
+    });
+
+    it('should return false', () => {
+      const result: boolean = component.isFirstItemOrOutOfArray(1);
+
+      expect(result).toBe(false);
+    });
+  });
+
   describe('onClickItem', () => {
     it('should emit searchResultSelected', () => {
       component.searchResultSelected.emit = jest.fn();
@@ -230,4 +406,26 @@ describe('InstantSearchComponent', () => {
       expect(component.searchResultSelected.emit).toHaveBeenCalledWith(searchResults[0]);
     });
   });
+
+  describe('onClickHandler', () => {
+    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);
+
+      expect(component.hideResults).toHaveBeenCalled();
+    });
+
+    it('should not call hideResults if instant search contains event target', () => {
+      component.ref.nativeElement.contains = jest.fn().mockReturnValue(true);
+
+      component.onClickHandler(e);
+
+      expect(component.hideResults).not.toHaveBeenCalled();
+    });
+  });
 });
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 a34aef1436..e1e2e7224f 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
@@ -13,7 +13,7 @@ import {
   ViewChildren,
 } from '@angular/core';
 import { FormControl } from '@angular/forms';
-import { isEqual } from 'lodash-es';
+import { isEqual, isUndefined } from 'lodash-es';
 import { Subscription, debounceTime, distinctUntilChanged, filter } from 'rxjs';
 import { AriaLiveRegionComponent } from '../../aria-live-region/aria-live-region.component';
 import { SearchFieldComponent } from '../search-field/search-field.component';
@@ -52,9 +52,9 @@ import { InstantSearchResult } from './instant-search.model';
       <ods-search-result-header [text]="headerText" [count]="results.length" header />
       <ods-search-result-item
         *ngFor="let result of results; let i = index"
-        [caption]="result.caption"
+        [title]="result.title"
         [description]="result.description"
-        (clickItem)="onClickItem(result, i)"
+        (itemClicked)="onClickItem(result, i)"
         #results
       ></ods-search-result-item>
     </ods-search-result-layer>
@@ -67,9 +67,8 @@ export class InstantSearchComponent implements OnInit, OnDestroy {
   @Input() control: FormControl<string> = new FormControl(EMPTY_STRING);
 
   @Input() set searchResults(searchResults: InstantSearchResult<unknown>[]) {
-    if (!isEqual(searchResults, this.results)) {
-      this.results = searchResults;
-      this.ariaLiveText = this.buildAriaLiveText(searchResults.length);
+    if (!isEqual(searchResults, this.results) && isNotNil(searchResults)) {
+      this.setSearchResults(searchResults);
     }
   }
 
@@ -77,14 +76,15 @@ export class InstantSearchComponent implements OnInit, OnDestroy {
     InstantSearchResult<unknown>
   >();
 
-  readonly PREVIEW_SEARCH_STRING_MIN_LENGTH = 2;
+  readonly FIRST_ITEM_INDEX: number = 0;
+  readonly PREVIEW_SEARCH_STRING_MIN_LENGTH: number = 2;
   results: InstantSearchResult<unknown>[] = [];
   ariaLiveText: string = '';
   isShowResults: boolean = true;
   private focusedResult: number | undefined = undefined;
-  private formControlSubscription: Subscription;
+  formControlSubscription: Subscription;
 
-  constructor(private ref: ElementRef) {}
+  constructor(public ref: ElementRef) {}
 
   @ViewChildren('results') resultsRef: QueryList<SearchResultItemComponent>;
 
@@ -96,7 +96,9 @@ export class InstantSearchComponent implements OnInit, OnDestroy {
         filter((value: string) => value.length >= this.PREVIEW_SEARCH_STRING_MIN_LENGTH),
         distinctUntilChanged(),
       )
-      .subscribe(() => this.showResults());
+      .subscribe(() => {
+        this.showResults();
+      });
   }
 
   ngOnDestroy(): void {
@@ -104,18 +106,18 @@ export class InstantSearchComponent implements OnInit, OnDestroy {
   }
 
   @HostListener('document:keydown', ['$event'])
-  onKeydownHandler(e: KeyboardEvent) {
-    if (this.results.length === 0) return;
-    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
+  onKeydownHandler(e: KeyboardEvent): void {
+    if (this.isSearchResultsEmpty()) return;
+    if (this.isArrowNavigationKey(e)) {
       this.handleArrowNavigation(e);
     }
-    if (e.key === 'Escape') {
+    if (this.isEscapeKey(e)) {
       this.handleEscape(e);
     }
   }
 
   @HostListener('document:click', ['$event'])
-  onClickHandler(e: MouseEvent) {
+  onClickHandler(e: MouseEvent): void {
     if (!this.ref.nativeElement.contains(e.target)) {
       this.hideResults();
     }
@@ -128,29 +130,45 @@ export class InstantSearchComponent implements OnInit, OnDestroy {
     this.setFocusOnResultItem(newIndex);
   }
 
-  setFocusOnResultItem(index: number) {
-    this.resultsRef.get(index).setFocus();
-  }
-
   handleEscape(e: KeyboardEvent): void {
     e.preventDefault();
     this.hideResults();
   }
 
+  setFocusOnResultItem(index: number): void {
+    this.resultsRef.get(index).setFocus();
+  }
+
+  setSearchResults(searchResults: InstantSearchResult<unknown>[]): void {
+    this.results = searchResults;
+    this.ariaLiveText = this.buildAriaLiveText(searchResults.length);
+  }
+
   getNextResultIndex(index: number | undefined, resultLength: number): number {
-    if (index === undefined || index >= resultLength - 1) {
-      return 0;
+    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 (index === undefined || index <= 0) {
-      return resultLength - 1;
+    if (isUndefined(index)) {
+      return this.getLastItemIndex(resultLength);
+    }
+    if (this.isFirstItemOrOutOfArray(index)) {
+      return this.getLastItemIndex(resultLength);
     }
     return index - 1;
   }
 
+  getLastItemIndex(arrayLength: number): number {
+    if (arrayLength < 1) return this.FIRST_ITEM_INDEX;
+    return arrayLength - 1;
+  }
+
   getResultIndexForKey(key: string): number {
     switch (key) {
       case 'ArrowDown':
@@ -180,6 +198,26 @@ export class InstantSearchComponent implements OnInit, OnDestroy {
     this.focusedResult = undefined;
   }
 
+  isLastItemOrOutOfArray(index: number, arrayLength: number): boolean {
+    return index >= arrayLength - 1;
+  }
+
+  isFirstItemOrOutOfArray(index: number): boolean {
+    return index <= this.FIRST_ITEM_INDEX;
+  }
+
+  isSearchResultsEmpty(): boolean {
+    return this.results.length === 0;
+  }
+
+  isArrowNavigationKey(e: KeyboardEvent): boolean {
+    return e.key === 'ArrowDown' || e.key === 'ArrowUp';
+  }
+
+  isEscapeKey(e: KeyboardEvent): boolean {
+    return e.key === 'Escape';
+  }
+
   onClickItem(result: InstantSearchResult<unknown>, index: number) {
     this.searchResultSelected.emit(result);
     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 61f66832f5..8b09565f30 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
@@ -1,5 +1,5 @@
 export interface InstantSearchResult<T> {
-  caption: string;
+  title: string;
   description: string;
   data?: T;
 }
diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.spec.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.spec.ts
index 8cf0cc8f6d..52538ebbe1 100644
--- a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.spec.ts
+++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.spec.ts
@@ -14,7 +14,7 @@ describe('SearchResultItemComponent', () => {
 
     fixture = TestBed.createComponent(SearchResultItemComponent);
     component = fixture.componentInstance;
-    component.caption = 'Test';
+    component.title = 'Test';
     fixture.detectChanges();
   });
 
@@ -25,11 +25,11 @@ describe('SearchResultItemComponent', () => {
   describe('clickItem', () => {
     it('should emit event', () => {
       const button = getElementFromFixture(fixture, getDataTestIdOf('item-button'));
-      const emitSpy = jest.spyOn(component.clickItem, 'emit');
+      const emitSpy = jest.spyOn(component.itemClicked, 'emit');
 
       button.click();
 
-      expect(emitSpy).toHaveBeenCalledTimes(1);
+      expect(emitSpy).toHaveBeenCalled();
     });
   });
 
@@ -39,7 +39,7 @@ describe('SearchResultItemComponent', () => {
 
       component.setFocus();
 
-      expect(component.buttonRef.nativeElement.focus).toHaveBeenCalledTimes(1);
+      expect(component.buttonRef.nativeElement.focus).toHaveBeenCalled();
     });
   });
 });
diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts
index d14c174358..7a19b9d4b9 100644
--- a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts
+++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts
@@ -6,7 +6,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@
   standalone: true,
   imports: [CommonModule],
   template: `<button
-    *ngIf="caption"
+    *ngIf="title"
     [ngClass]="[
       'flex w-full justify-between border-2 border-transparent px-6 py-3',
       'hover:border-focus focus:border-focus focus:outline-none',
@@ -18,17 +18,17 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@
     #button
   >
     <div class="flex flex-col items-start justify-between text-text">
-      <p class="text-base font-medium">{{ caption }}</p>
+      <p class="text-base font-medium">{{ title }}</p>
       <p class="text-sm">{{ description }}</p>
     </div>
     <ng-content select="[action-button]" />
   </button>`,
 })
 export class SearchResultItemComponent {
-  @Input({ required: true }) caption!: string;
+  @Input({ required: true }) title!: string;
   @Input() description: string = '';
 
-  @Output() public clickItem: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();
+  @Output() public itemClicked: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>();
 
   @ViewChild('button') buttonRef: ElementRef;
 
-- 
GitLab