import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Store } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { HostWindowService } from '../host-window.service'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { SearchComponent } from './search.component'; import { SearchService } from '../../core/shared/search/search.service'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; import { By } from '@angular/platform-browser'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { SidebarService } from '../sidebar/sidebar.service'; import { SearchFilterService } from '../../core/shared/search/search-filter.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-page.component'; import { RouteService } from '../../core/services/route.service'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; import { PaginatedSearchOptions } from './models/paginated-search-options.model'; import { SidebarServiceStub } from '../testing/sidebar-service.stub'; import { SearchConfig, SortConfig } from '../../core/shared/search/search-filters/search-config.model'; import { Item } from '../../core/shared/item.model'; import { RemoteData } from '../../core/data/remote-data'; import { SearchObjects } from './models/search-objects.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { SearchFilterConfig } from './models/search-filter-config.model'; import { FilterType } from './models/filter-type.model'; import { getCommunityPageRoute } from '../../community-page/community-page-routing-paths'; import { getCollectionPageRoute } from '../../collection-page/collection-page-routing-paths'; import { environment } from '../../../environments/environment.test'; import { APP_CONFIG } from '../../../config/app-config.interface'; let comp: SearchComponent; let fixture: ComponentFixture; const store: Store = jasmine.createSpyObj('store', { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ dispatch: {}, /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ select: observableOf(true) }); const sortConfigList: SortConfig[] = [ { name: 'score', sortOrder: SortDirection.DESC }, { name: 'dc.title', sortOrder: SortDirection.ASC }, { name: 'dc.title', sortOrder: SortDirection.DESC } ]; const sortOptionsList: SortOptions[] = [ new SortOptions('score', SortDirection.DESC), new SortOptions('dc.title', SortDirection.ASC), new SortOptions('dc.title', SortDirection.DESC) ]; const searchConfig = Object.assign(new SearchConfig(), { sortOptions: sortConfigList }); const paginationId = 'search-test-page-id'; const pagination: PaginationComponentOptions = new PaginationComponentOptions(); pagination.id = paginationId; pagination.currentPage = 1; pagination.pageSize = 10; const mockDso = Object.assign(new Item(), { metadata: { 'dc.title': [ { language: 'en_US', value: 'Item nr 1' } ] }, _links: { self: { href: 'selfLink1' } } }); const mockDso2 = Object.assign(new Item(), { metadata: { 'dc.title': [ { language: 'en_US', value: 'Item nr 2' } ] }, _links: { self: { href: 'selfLink2' } } }); const mockSearchResults: SearchObjects = Object.assign(new SearchObjects(), { page: [mockDso, mockDso2] }); const mockResultsRD: RemoteData> = createSuccessfulRemoteDataObject(mockSearchResults); const mockResultsRD$: Observable>> = observableOf(mockResultsRD); const searchServiceStub = jasmine.createSpyObj('SearchService', { search: mockResultsRD$, getSearchLink: '/search', getScopes: observableOf(['test-scope']), getSearchConfigurationFor: createSuccessfulRemoteDataObject$(searchConfig), trackSearch: {}, }) as SearchService; const queryParam = 'test query'; const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; const defaultSearchOptions = new PaginatedSearchOptions({ pagination }); const paginatedSearchOptions$ = new BehaviorSubject(defaultSearchOptions); const activatedRouteStub = { snapshot: { queryParamMap: new Map([ ['query', queryParam], ['scope', scopeParam] ]) }, queryParams: observableOf({ query: queryParam, scope: scopeParam }) }; const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), { name: 'test1', filterType: FilterType.text, hasFacets: false, isOpenByDefault: false, pageSize: 2 }); const mockFilterConfig2: SearchFilterConfig = Object.assign(new SearchFilterConfig(), { name: 'test2', filterType: FilterType.text, hasFacets: false, isOpenByDefault: false, pageSize: 1 }); const filtersConfigRD = createSuccessfulRemoteDataObject([mockFilterConfig, mockFilterConfig2]); const filtersConfigRD$ = observableOf(filtersConfigRD); const routeServiceStub = { getQueryParameterValue: () => { return observableOf(null); }, getQueryParamsWithPrefix: () => { return observableOf(null); }, setParameter: () => { return; } }; let searchConfigurationServiceStub; export function configureSearchComponentTestingModule(compType, additionalDeclarations: any[] = []) { searchConfigurationServiceStub = jasmine.createSpyObj('SearchConfigurationService', { getConfigurationSortOptions: sortOptionsList, getConfig: filtersConfigRD$, getConfigurationSearchConfig: observableOf(searchConfig), getCurrentConfiguration: observableOf('default'), getCurrentScope: observableOf('test-id'), getCurrentSort: observableOf(sortOptionsList[0]), updateFixedFilter: jasmine.createSpy('updateFixedFilter'), setPaginationId: jasmine.createSpy('setPaginationId') }); searchConfigurationServiceStub.setPaginationId.and.callFake((pageId) => { paginatedSearchOptions$.next(Object.assign(paginatedSearchOptions$.value, { pagination: Object.assign(new PaginationComponentOptions(), { id: pageId }) })); }); searchConfigurationServiceStub.paginatedSearchOptions = new BehaviorSubject(new PaginatedSearchOptions({pagination: {id: 'default'} as any})); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule], declarations: [compType, ...additionalDeclarations], providers: [ { provide: SearchService, useValue: searchServiceStub }, { provide: CommunityDataService, useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll']) }, { provide: ActivatedRoute, useValue: activatedRouteStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Store, useValue: store }, { provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService', { isXs: observableOf(true), isSm: observableOf(false), isXsOrSm: observableOf(true) }) }, { provide: SidebarService, useValue: SidebarServiceStub }, { provide: SearchFilterService, useValue: {} }, { provide: SEARCH_CONFIG_SERVICE, useValue: searchConfigurationServiceStub }, { provide: APP_CONFIG, useValue: environment }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(compType, { set: { changeDetection: ChangeDetectionStrategy.Default, providers: [{ provide: SearchConfigurationService, useValue: searchConfigurationServiceStub }] }, }).compileComponents(); } describe('SearchComponent', () => { beforeEach(waitForAsync(() => { configureSearchComponentTestingModule(SearchComponent); })); beforeEach(() => { fixture = TestBed.createComponent(SearchComponent); comp = fixture.componentInstance; // SearchComponent test instance comp.inPlaceSearch = false; comp.paginationId = paginationId; spyOn((comp as any), 'getSearchOptions').and.returnValue(paginatedSearchOptions$.asObservable()); }); afterEach(() => { comp = null; }); it('should init search parameters properly and call retrieveSearchResults', fakeAsync(() => { spyOn((comp as any), 'retrieveSearchResults').and.callThrough(); fixture.detectChanges(); tick(100); const expectedSearchOptions = Object.assign(paginatedSearchOptions$.value, { configuration: 'default', sort: sortOptionsList[0] }); expect(comp.currentConfiguration$).toBeObservable(cold('b', { b: 'default' })); expect(comp.currentSortOptions$).toBeObservable(cold('b', { b: sortOptionsList[0] })); expect(comp.sortOptionsList$).toBeObservable(cold('b', { b: sortOptionsList })); expect(comp.searchOptions$).toBeObservable(cold('b', { b: expectedSearchOptions })); expect((comp as any).retrieveSearchResults).toHaveBeenCalledWith(expectedSearchOptions); })); it('should retrieve SearchResults', fakeAsync(() => { fixture.detectChanges(); tick(100); const expectedResults = mockResultsRD; expect(comp.resultsRD$).toBeObservable(cold('b', { b: expectedResults })); })); it('should retrieve Search Filters', fakeAsync(() => { fixture.detectChanges(); tick(100); const expectedResults = filtersConfigRD; expect(comp.filtersRD$).toBeObservable(cold('b', { b: expectedResults })); })); it('should emit resultFound event', fakeAsync(() => { spyOn(comp.resultFound, 'emit'); const expectedResults = mockSearchResults; fixture.detectChanges(); tick(100); expect(comp.resultFound.emit).toHaveBeenCalledWith(expectedResults); })); describe('when the open sidebar button is clicked in mobile view', () => { beforeEach(() => { spyOn(comp, 'openSidebar'); }); it('should trigger the openSidebar function', fakeAsync(() => { fixture.detectChanges(); tick(100); fixture.detectChanges(); const openSidebarButton = fixture.debugElement.query(By.css('.open-sidebar')); openSidebarButton.triggerEventHandler('click', null); expect(comp.openSidebar).toHaveBeenCalled(); })); }); describe('getDsoUUIDFromUrl', () => { let url: string; let result: string; describe('when the navigated URL is an entity route', () => { beforeEach(() => { url = '/entities/publication/9a364471-3f19-4e7b-916a-a24a44ff48e3'; result = (comp as any).getDsoUUIDFromUrl(url); }); it('should return the UUID', () => { expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); }); }); describe('when the navigated URL is a community route', () => { beforeEach(() => { url = `${getCommunityPageRoute('9a364471-3f19-4e7b-916a-a24a44ff48e3')}`; result = (comp as any).getDsoUUIDFromUrl(url); }); it('should return the UUID', () => { expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); }); }); describe('when the navigated URL is a collection route', () => { beforeEach(() => { url = `${getCollectionPageRoute('9a364471-3f19-4e7b-916a-a24a44ff48e3')}`; result = (comp as any).getDsoUUIDFromUrl(url); }); it('should return the UUID', () => { expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); }); }); describe('when the navigated URL is an item route', () => { beforeEach(() => { url = '/items/9a364471-3f19-4e7b-916a-a24a44ff48e3'; result = (comp as any).getDsoUUIDFromUrl(url); }); it('should return the UUID', () => { expect(result).toEqual('9a364471-3f19-4e7b-916a-a24a44ff48e3'); }); }); describe('when the navigated URL is an invalid route', () => { beforeEach(() => { url = '/invalid/object/route/9a364471-3f19-4e7b-916a-a24a44ff48e3'; result = (comp as any).getDsoUUIDFromUrl(url); }); it('should return null', () => { expect(result).toBeNull(); }); }); }); });