Skip to content

Commit 3eef635

Browse files
authored
Merge pull request #970 from Vlad0n20/feat/ENG-9818
[ENG-9818] - collection search with shtrove
2 parents 29d3897 + d45a69a commit 3eef635

8 files changed

Lines changed: 334 additions & 109 deletions

File tree

src/app/features/collections/components/collections-discover/collections-discover.component.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ <h1 class="collections-heading flex align-items-center">{{ collectionProvider()?
3737
</div>
3838

3939
<div class="content-container flex-1">
40-
<osf-collections-main-content />
40+
@if (useShareTroveSearch) {
41+
@if (defaultSearchFiltersInitialized()) {
42+
<osf-global-search [searchControlInput]="searchControl" />
43+
}
44+
} @else {
45+
<osf-collections-main-content />
46+
}
4147
</div>
4248
</section>
4349
} @else {
Lines changed: 216 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,129 +1,261 @@
1+
import { Store } from '@ngxs/store';
2+
13
import { MockComponents, MockProvider } from 'ng-mocks';
24

5+
import { Mock } from 'vitest';
6+
37
import { ComponentFixture, TestBed } from '@angular/core/testing';
48
import { ActivatedRoute } from '@angular/router';
59

10+
import { ENVIRONMENT } from '@core/provider/environment.provider';
11+
import { GlobalSearchComponent } from '@osf/shared/components/global-search/global-search.component';
612
import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component';
713
import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component';
814
import { CustomDialogService } from '@osf/shared/services/custom-dialog.service';
915
import { ToastService } from '@osf/shared/services/toast.service';
1016
import { CollectionsSelectors } from '@shared/stores/collections';
17+
import { SetDefaultFilterValue, SetExtraFilters } from '@shared/stores/global-search';
1118

1219
import { MOCK_PROVIDER } from '@testing/mocks/provider.mock';
1320
import { provideOSFCore } from '@testing/osf.testing.provider';
1421
import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock';
1522
import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock';
1623
import { provideMockStore } from '@testing/providers/store-provider.mock';
17-
import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock';
24+
import { ToastServiceMock } from '@testing/providers/toast-provider.mock';
1825

1926
import { CollectionsQuerySyncService } from '../../services';
2027
import { CollectionsMainContentComponent } from '../collections-main-content/collections-main-content.component';
2128

2229
import { CollectionsDiscoverComponent } from './collections-discover.component';
2330

24-
describe('CollectionsDiscoverComponent', () => {
25-
let component: CollectionsDiscoverComponent;
26-
let fixture: ComponentFixture<CollectionsDiscoverComponent>;
27-
let toastServiceMock: ToastServiceMockType;
28-
let mockCustomDialogService: ReturnType<CustomDialogServiceMockBuilder['build']>;
29-
let mockRoute: ReturnType<ActivatedRouteMockBuilder['build']>;
30-
31-
beforeEach(() => {
32-
toastServiceMock = ToastServiceMock.simple();
33-
mockCustomDialogService = CustomDialogServiceMockBuilder.create().build();
34-
mockRoute = ActivatedRouteMockBuilder.create().withParams({ providerId: 'provider-1' }).build();
35-
36-
TestBed.configureTestingModule({
37-
imports: [
38-
CollectionsDiscoverComponent,
39-
...MockComponents(SearchInputComponent, CollectionsMainContentComponent, LoadingSpinnerComponent),
40-
],
41-
providers: [
42-
provideOSFCore(),
43-
MockProvider(ToastService, toastServiceMock),
44-
MockProvider(CustomDialogService, mockCustomDialogService),
45-
MockProvider(ActivatedRoute, mockRoute),
46-
provideMockStore({
47-
signals: [
48-
{ selector: CollectionsSelectors.getCollectionProvider, value: MOCK_PROVIDER },
49-
{ selector: CollectionsSelectors.getCollectionDetails, value: null },
50-
{ selector: CollectionsSelectors.getAllSelectedFilters, value: {} },
51-
{ selector: CollectionsSelectors.getSortBy, value: 'date' },
52-
{ selector: CollectionsSelectors.getSearchText, value: '' },
53-
{ selector: CollectionsSelectors.getPageNumber, value: '1' },
54-
{ selector: CollectionsSelectors.getCollectionProviderLoading, value: false },
55-
],
56-
}),
57-
],
58-
}).overrideComponent(CollectionsDiscoverComponent, {
59-
set: {
60-
providers: [MockProvider(CollectionsQuerySyncService)],
31+
const MOCK_COLLECTION_PROVIDER = {
32+
...MOCK_PROVIDER,
33+
primaryCollection: { id: 'collection-1', type: 'collections' },
34+
requiredMetadataTemplate: null,
35+
};
36+
37+
const MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE = {
38+
...MOCK_COLLECTION_PROVIDER,
39+
requiredMetadataTemplate: {
40+
id: 'template-1',
41+
type: 'cedar-metadata-templates' as const,
42+
attributes: {
43+
schema_name: 'Test',
44+
cedar_id: 'cedar-1',
45+
template: {
46+
'@id': 'https://repo.metadatacenter.org/templates/test',
47+
'@type': 'https://schema.metadatacenter.org/core/Template',
48+
type: 'object',
49+
title: 'Test',
50+
description: '',
51+
$schema: 'http://json-schema.org/draft-04/schema',
52+
'@context': {} as never,
53+
required: [],
54+
properties: {},
55+
_ui: {
56+
order: ['field1'],
57+
propertyLabels: { field1: 'Field One' },
58+
propertyDescriptions: {},
59+
},
6160
},
62-
});
61+
},
62+
},
63+
};
6364

64-
fixture = TestBed.createComponent(CollectionsDiscoverComponent);
65-
component = fixture.componentInstance;
66-
fixture.detectChanges();
67-
});
65+
interface SetupOptions {
66+
collectionSubmissionWithCedar?: boolean;
67+
provider?: typeof MOCK_COLLECTION_PROVIDER | typeof MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE;
68+
}
6869

69-
it('should create', () => {
70-
expect(component).toBeTruthy();
71-
});
70+
function setup(options: SetupOptions = {}) {
71+
const { collectionSubmissionWithCedar = false, provider = MOCK_COLLECTION_PROVIDER } = options;
72+
73+
const toastServiceMock = ToastServiceMock.simple();
74+
const mockCustomDialogService = CustomDialogServiceMockBuilder.create().build();
75+
const mockRoute = ActivatedRouteMockBuilder.create().withParams({ providerId: 'provider-1' }).build();
7276

73-
it('should initialize with default values', () => {
74-
expect(component.providerId()).toBe('provider-1');
75-
expect(component.searchControl.value).toBe('');
77+
TestBed.configureTestingModule({
78+
imports: [
79+
CollectionsDiscoverComponent,
80+
...MockComponents(
81+
SearchInputComponent,
82+
CollectionsMainContentComponent,
83+
GlobalSearchComponent,
84+
LoadingSpinnerComponent
85+
),
86+
],
87+
providers: [
88+
provideOSFCore(),
89+
{ provide: ENVIRONMENT, useValue: { apiDomainUrl: 'http://localhost:8000', collectionSubmissionWithCedar } },
90+
MockProvider(ToastService, toastServiceMock),
91+
MockProvider(CustomDialogService, mockCustomDialogService),
92+
MockProvider(ActivatedRoute, mockRoute),
93+
provideMockStore({
94+
signals: [
95+
{ selector: CollectionsSelectors.getCollectionProvider, value: provider },
96+
{ selector: CollectionsSelectors.getCollectionDetails, value: null },
97+
{ selector: CollectionsSelectors.getAllSelectedFilters, value: {} },
98+
{ selector: CollectionsSelectors.getSortBy, value: 'date' },
99+
{ selector: CollectionsSelectors.getSearchText, value: '' },
100+
{ selector: CollectionsSelectors.getPageNumber, value: '1' },
101+
{ selector: CollectionsSelectors.getCollectionProviderLoading, value: false },
102+
],
103+
}),
104+
],
105+
}).overrideComponent(CollectionsDiscoverComponent, {
106+
set: {
107+
providers: [MockProvider(CollectionsQuerySyncService)],
108+
},
76109
});
77110

78-
it('should handle search triggered', () => {
79-
const searchValue = 'test search';
111+
const fixture = TestBed.createComponent(CollectionsDiscoverComponent);
112+
const component = fixture.componentInstance;
113+
const store = TestBed.inject(Store);
114+
fixture.detectChanges();
80115

81-
component.onSearchTriggered(searchValue);
116+
return { fixture, component, store };
117+
}
82118

83-
expect(component).toBeTruthy();
84-
});
119+
describe('CollectionsDiscoverComponent', () => {
120+
describe('legacy mode (collectionSubmissionWithCedar = false)', () => {
121+
let component: CollectionsDiscoverComponent;
122+
let fixture: ComponentFixture<CollectionsDiscoverComponent>;
85123

86-
it('should have provider id signal', () => {
87-
expect(component.providerId()).toBe('provider-1');
88-
});
124+
beforeEach(() => {
125+
({ fixture, component } = setup());
126+
});
89127

90-
it('should have collection provider data', () => {
91-
expect(component.collectionProvider()).toEqual(MOCK_PROVIDER);
92-
});
128+
it('should create', () => {
129+
expect(component).toBeTruthy();
130+
});
93131

94-
it('should have collection details', () => {
95-
expect(component.collectionDetails()).toBeNull();
96-
});
132+
it('should set useShareTroveSearch to false', () => {
133+
expect(component.useShareTroveSearch).toBe(false);
134+
});
97135

98-
it('should have selected filters', () => {
99-
expect(component.selectedFilters()).toEqual({});
100-
});
136+
it('should initialize with default values', () => {
137+
expect(component.providerId()).toBe('provider-1');
138+
expect(component.searchControl.value).toBe('');
139+
});
101140

102-
it('should have sort by value', () => {
103-
expect(component.sortBy()).toBe('date');
104-
});
141+
it('should have collection provider data', () => {
142+
expect(component.collectionProvider()).toEqual(MOCK_COLLECTION_PROVIDER);
143+
});
105144

106-
it('should have search text', () => {
107-
expect(component.searchText()).toBe('');
108-
});
145+
it('should have collection details as null', () => {
146+
expect(component.collectionDetails()).toBeNull();
147+
});
109148

110-
it('should have page number', () => {
111-
expect(component.pageNumber()).toBe('1');
112-
});
149+
it('should have selected filters', () => {
150+
expect(component.selectedFilters()).toEqual({});
151+
});
113152

114-
it('should have loading state', () => {
115-
expect(component.isProviderLoading()).toBe(false);
116-
});
153+
it('should have sort by value', () => {
154+
expect(component.sortBy()).toBe('date');
155+
});
156+
157+
it('should have search text', () => {
158+
expect(component.searchText()).toBe('');
159+
});
117160

118-
it('should compute primary collection id', () => {
119-
expect(component.primaryCollectionId()).toBe(MOCK_PROVIDER.primaryCollection?.id);
161+
it('should have page number', () => {
162+
expect(component.pageNumber()).toBe('1');
163+
});
164+
165+
it('should have loading state', () => {
166+
expect(component.isProviderLoading()).toBe(false);
167+
});
168+
169+
it('should compute primary collection id', () => {
170+
expect(component.primaryCollectionId()).toBe('collection-1');
171+
});
172+
173+
it('should handle search control value changes', () => {
174+
component.searchControl.setValue('new search value');
175+
expect(component.searchControl.value).toBe('new search value');
176+
});
177+
178+
it('should not initialize default search filters', () => {
179+
expect(component.defaultSearchFiltersInitialized()).toBe(false);
180+
});
181+
182+
it('should render CollectionsMainContentComponent', () => {
183+
const el = fixture.nativeElement as HTMLElement;
184+
expect(el.querySelector('osf-collections-main-content')).toBeTruthy();
185+
expect(el.querySelector('osf-global-search')).toBeNull();
186+
});
187+
188+
it('should dispatch setSearchValue and setPageNumber on search triggered', () => {
189+
const { component: localComponent, store: localStore } = setup();
190+
(localStore.dispatch as Mock).mockClear();
191+
192+
localComponent.onSearchTriggered('my query');
193+
194+
const calls = (localStore.dispatch as Mock).mock.calls.flat();
195+
expect(calls.some((c: unknown) => c instanceof SetDefaultFilterValue)).toBe(false);
196+
});
120197
});
121198

122-
it('should handle search control value changes', () => {
123-
const searchValue = 'new search value';
199+
describe('shtrove mode (collectionSubmissionWithCedar = true)', () => {
200+
it('should set useShareTroveSearch to true', () => {
201+
const { component } = setup({ collectionSubmissionWithCedar: true });
202+
expect(component.useShareTroveSearch).toBe(true);
203+
});
204+
205+
it('should initialize default search filters', () => {
206+
const { component } = setup({ collectionSubmissionWithCedar: true });
207+
expect(component.defaultSearchFiltersInitialized()).toBe(true);
208+
});
209+
210+
it('should dispatch SetDefaultFilterValue with collection IRI', () => {
211+
const { store } = setup({ collectionSubmissionWithCedar: true });
212+
const dispatched = (store.dispatch as Mock).mock.calls.flat();
213+
const setDefaultFilter = dispatched.find(
214+
(c: unknown) => c instanceof SetDefaultFilterValue
215+
) as SetDefaultFilterValue;
216+
217+
expect(setDefaultFilter).toBeDefined();
218+
expect(setDefaultFilter.filterKey).toBe('isContainedBy');
219+
expect(setDefaultFilter.value).toBe('http://localhost:8000/v2/collections/collection-1/');
220+
});
221+
222+
it('should not dispatch SetExtraFilters when provider has no requiredMetadataTemplate', () => {
223+
const { store } = setup({ collectionSubmissionWithCedar: true });
224+
const dispatched = (store.dispatch as Mock).mock.calls.flat();
124225

125-
component.searchControl.setValue(searchValue);
226+
expect(dispatched.some((c: unknown) => c instanceof SetExtraFilters)).toBe(false);
227+
});
228+
229+
it('should dispatch SetExtraFilters when provider has a requiredMetadataTemplate', () => {
230+
const { store } = setup({
231+
collectionSubmissionWithCedar: true,
232+
provider: MOCK_COLLECTION_PROVIDER_WITH_TEMPLATE,
233+
});
126234

127-
expect(component.searchControl.value).toBe(searchValue);
235+
const dispatched = (store.dispatch as Mock).mock.calls.flat();
236+
const setExtraFilters = dispatched.find((c: unknown) => c instanceof SetExtraFilters) as SetExtraFilters;
237+
238+
expect(setExtraFilters).toBeDefined();
239+
expect(setExtraFilters.filters).toHaveLength(1);
240+
expect(setExtraFilters.filters[0].key).toBe('field1');
241+
expect(setExtraFilters.filters[0].label).toBe('Field One');
242+
});
243+
244+
it('should render GlobalSearchComponent when filters are initialized', () => {
245+
const { fixture } = setup({ collectionSubmissionWithCedar: true });
246+
const el = fixture.nativeElement as HTMLElement;
247+
248+
expect(el.querySelector('osf-global-search')).toBeTruthy();
249+
expect(el.querySelector('osf-collections-main-content')).toBeNull();
250+
});
251+
252+
it('should not dispatch any action on onSearchTriggered in shtrove mode', () => {
253+
const { component, store } = setup({ collectionSubmissionWithCedar: true });
254+
(store.dispatch as Mock).mockClear();
255+
256+
component.onSearchTriggered('query');
257+
258+
expect(store.dispatch).not.toHaveBeenCalled();
259+
});
128260
});
129261
});

0 commit comments

Comments
 (0)