From 6959c22740883cbec1018e2b26c8fd5f71cbaa25 Mon Sep 17 00:00:00 2001 From: Diana Date: Thu, 11 Sep 2025 11:15:00 +0300 Subject: [PATCH 1/2] test(institutions): tested institutions, institutions-list, institutions-search components --- .../institutions.component.spec.ts | 6 + .../institutions-list.component.spec.ts | 128 +++++++++++++----- .../institutions-search.component.spec.ts | 74 +++++++--- src/app/shared/mocks/index.ts | 1 + 4 files changed, 158 insertions(+), 51 deletions(-) diff --git a/src/app/features/institutions/institutions.component.spec.ts b/src/app/features/institutions/institutions.component.spec.ts index e8e7def49..80bbe42de 100644 --- a/src/app/features/institutions/institutions.component.spec.ts +++ b/src/app/features/institutions/institutions.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { InstitutionsComponent } from './institutions.component'; @@ -19,4 +20,9 @@ describe('InstitutionsComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should render router outlet', () => { + const routerOutlet = fixture.debugElement.query(By.css('router-outlet')); + expect(routerOutlet).toBeTruthy(); + }); }); diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts index fbb8b3051..40a7d0801 100644 --- a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts @@ -1,47 +1,47 @@ -import { provideStore } from '@ngxs/store'; +import { MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe } from 'ng-mocks'; +import { PaginatorState } from 'primeng/paginator'; -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; -import { - CustomPaginatorComponent, - LoadingSpinnerComponent, - SearchInputComponent, - SubHeaderComponent, -} from '@shared/components'; -import { InstitutionsState } from '@shared/stores/institutions'; +import { InstitutionsSelectors } from '@osf/shared/stores/institutions'; +import { MOCK_INSTITUTION } from '@shared/mocks/institution.mock'; import { InstitutionsListComponent } from './institutions-list.component'; -describe.skip('InstitutionsListComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('InstitutionsListComponent', () => { let component: InstitutionsListComponent; let fixture: ComponentFixture; + let routerMock: ReturnType; + let activatedRouteMock: ReturnType; + + const mockInstitutions = [MOCK_INSTITUTION]; + const mockTotalCount = 2; beforeEach(async () => { + routerMock = RouterMockBuilder.create().build(); + activatedRouteMock = ActivatedRouteMockBuilder.create() + .withQueryParams({ page: '1', size: '10', search: '' }) + .build(); + await TestBed.configureTestingModule({ - imports: [ - InstitutionsListComponent, - ...MockComponents(SubHeaderComponent, SearchInputComponent, CustomPaginatorComponent, LoadingSpinnerComponent), - MockPipe(TranslatePipe), - ], + imports: [InstitutionsListComponent, OSFTestingModule], providers: [ - { - provide: ActivatedRoute, - useValue: { - snapshot: { paramMap: { get: () => '1' } }, - queryParams: of({}), - }, - }, - provideStore([InstitutionsState]), - provideHttpClient(), - provideHttpClientTesting(), + provideMockStore({ + signals: [ + { selector: InstitutionsSelectors.getInstitutions, value: mockInstitutions }, + { selector: InstitutionsSelectors.getInstitutionsTotalCount, value: mockTotalCount }, + { selector: InstitutionsSelectors.isInstitutionsLoading, value: false }, + ], + }), + MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, activatedRouteMock), ], }).compileComponents(); @@ -53,4 +53,70 @@ describe.skip('InstitutionsListComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should update currentPage, first, and call updateQueryParams when page is provided', () => { + const paginatorEvent: PaginatorState = { + page: 1, + first: 20, + rows: 10, + pageCount: 5, + }; + + component.onPageChange(paginatorEvent); + + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(20); + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.any(Object), + queryParams: { + page: '2', + size: '10', + }, + queryParamsHandling: 'merge', + }); + }); + + it('should set currentPage to 1 when page is not provided', () => { + const paginatorEvent: PaginatorState = { + page: undefined, + first: 0, + rows: 20, + pageCount: 3, + }; + + component.onPageChange(paginatorEvent); + + expect(component.currentPage()).toBe(1); + expect(component.first()).toBe(0); + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.any(Object), + queryParams: { + page: '1', + size: '20', + }, + queryParamsHandling: 'merge', + }); + }); + + it('should handle first being undefined', () => { + const paginatorEvent: PaginatorState = { + page: 2, + first: undefined, + rows: 15, + pageCount: 4, + }; + + component.onPageChange(paginatorEvent); + + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(0); + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.any(Object), + queryParams: { + page: '2', + size: '15', + }, + queryParamsHandling: 'merge', + }); + }); }); diff --git a/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts b/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts index a78b8182a..3c4cec461 100644 --- a/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts +++ b/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts @@ -1,46 +1,80 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipes } from 'ng-mocks'; +import { Store } from '@ngxs/store'; -import { SafeHtmlPipe } from 'primeng/menu'; +import { MockComponents, MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; -import { - FilterChipsComponent, - ReusableFilterComponent, - SearchHelpTutorialComponent, - SearchInputComponent, - SearchResultsContainerComponent, -} from '@shared/components'; +import { SetDefaultFilterValue } from '@osf/shared/stores/global-search'; +import { FetchInstitutionById, InstitutionsSearchSelectors } from '@osf/shared/stores/institutions-search'; +import { GlobalSearchComponent, LoadingSpinnerComponent } from '@shared/components'; +import { MOCK_INSTITUTION } from '@shared/mocks'; import { InstitutionsSearchComponent } from './institutions-search.component'; -describe.skip('InstitutionsSearchComponent', () => { +import { OSFTestingModule } from '@testing/osf.testing.module'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('InstitutionsSearchComponent', () => { let component: InstitutionsSearchComponent; let fixture: ComponentFixture; + let activatedRouteMock: ReturnType; + let store: jest.Mocked; beforeEach(async () => { + activatedRouteMock = ActivatedRouteMockBuilder.create().build(); + await TestBed.configureTestingModule({ imports: [ InstitutionsSearchComponent, - ...MockComponents( - ReusableFilterComponent, - SearchResultsContainerComponent, - FilterChipsComponent, - SearchHelpTutorialComponent, - SearchInputComponent - ), - MockPipes(TranslatePipe, SafeHtmlPipe), + ...MockComponents(LoadingSpinnerComponent, GlobalSearchComponent), + OSFTestingModule, + ], + providers: [ + MockProvider(ActivatedRoute, activatedRouteMock), + provideMockStore({ + signals: [ + { selector: InstitutionsSearchSelectors.getInstitution, value: MOCK_INSTITUTION }, + { selector: InstitutionsSearchSelectors.getInstitutionLoading, value: false }, + ], + }), ], - providers: [], }).compileComponents(); fixture = TestBed.createComponent(InstitutionsSearchComponent); component = fixture.componentInstance; + + store = TestBed.inject(Store) as jest.Mocked; + store.dispatch = jest.fn().mockReturnValue(of(undefined)); + fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should fetch institution and set default filter value on ngOnInit when institution-id is provided', () => { + activatedRouteMock.snapshot!.params = { 'institution-id': MOCK_INSTITUTION.id }; + + store.dispatch.mockReturnValue(of(undefined)); + + component.ngOnInit(); + + expect(store.dispatch).toHaveBeenCalledWith(new FetchInstitutionById(MOCK_INSTITUTION.id)); + expect(store.dispatch).toHaveBeenCalledWith( + new SetDefaultFilterValue('affiliation', MOCK_INSTITUTION.iris.join(',')) + ); + }); + + it('should not fetch institution on ngOnInit when institution-id is not provided', () => { + activatedRouteMock.snapshot!.params = {}; + + component.ngOnInit(); + + expect(store.dispatch).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/shared/mocks/index.ts b/src/app/shared/mocks/index.ts index c079f9343..b18854749 100644 --- a/src/app/shared/mocks/index.ts +++ b/src/app/shared/mocks/index.ts @@ -9,6 +9,7 @@ export { MOCK_EMPLOYMENT } from './employment.mock'; export * from './employment.mock'; export * from './filters.mock'; export { MOCK_FUNDERS } from './funder.mock'; +export { MOCK_INSTITUTION } from './institution.mock'; export * from './license.mock'; export { MOCK_LICENSE } from './license.mock'; export { LoaderServiceMock } from './loader-service.mock'; From 4ca02892e09a510d7c5e694bef196ab7aa092b19 Mon Sep 17 00:00:00 2001 From: Diana Date: Thu, 11 Sep 2025 11:54:39 +0300 Subject: [PATCH 2/2] fix(tests): fixed tests --- .../dashboard/dashboard.component.spec.ts | 2 ++ .../create-project-dialog.component.spec.ts | 3 +- .../my-projects/my-projects.component.spec.ts | 35 ++++++++----------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/app/features/home/pages/dashboard/dashboard.component.spec.ts b/src/app/features/home/pages/dashboard/dashboard.component.spec.ts index e73a4ff1a..9f442b1d3 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.spec.ts +++ b/src/app/features/home/pages/dashboard/dashboard.component.spec.ts @@ -1,5 +1,6 @@ import { MockComponents, MockProvider } from 'ng-mocks'; +import { ConfirmationService } from 'primeng/api'; import { TablePageEvent } from 'primeng/table'; import { of } from 'rxjs'; @@ -41,6 +42,7 @@ describe('DashboardComponent', () => { { selector: MyResourcesSelectors.getProjectsLoading, value: false }, ], }), + MockProvider(ConfirmationService, { confirm: jest.fn() }), MockProvider(Router, routerMock), MockProvider(IS_MEDIUM, of(false)), MockProvider(ActivatedRoute, ActivatedRouteMock.withQueryParams({}).build()), diff --git a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts index 8352284b6..ecd77e681 100644 --- a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts +++ b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts @@ -79,11 +79,12 @@ describe('CreateProjectDialogComponent', () => { fillValidForm('Title', 'Desc', 'Tpl', 'Storage', ['a1']); (MOCK_STORE.dispatch as jest.Mock).mockReturnValue(of(undefined)); + (MOCK_STORE.selectSnapshot as jest.Mock).mockReturnValue([{ id: 'new-project-id' }]); component.submitForm(); expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new CreateProject('Title', 'Desc', 'Tpl', 'Storage', ['a1'])); expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new GetMyProjects(1, MY_PROJECTS_TABLE_PARAMS.rows, {})); - expect((dialogRef as any).close).toHaveBeenCalled(); + expect((dialogRef as any).close).toHaveBeenCalledWith({ project: { id: 'new-project-id' } }); }); }); diff --git a/src/app/features/my-projects/my-projects.component.spec.ts b/src/app/features/my-projects/my-projects.component.spec.ts index 037918721..90b5fa1cb 100644 --- a/src/app/features/my-projects/my-projects.component.spec.ts +++ b/src/app/features/my-projects/my-projects.component.spec.ts @@ -2,6 +2,7 @@ import { Store } from '@ngxs/store'; import { MockComponents, MockProvider } from 'ng-mocks'; +import { ConfirmationService } from 'primeng/api'; import { DialogService } from 'primeng/dynamicdialog'; import { BehaviorSubject, of } from 'rxjs'; @@ -15,6 +16,7 @@ import { IS_MEDIUM } from '@osf/shared/helpers'; import { MOCK_STORE } from '@osf/shared/mocks'; import { BookmarksSelectors, GetMyProjects, MyResourcesSelectors } from '@osf/shared/stores'; import { MyProjectsTableComponent, SelectComponent, SubHeaderComponent } from '@shared/components'; +import { ProjectRedirectDialogService } from '@shared/services'; import { MyProjectsComponent } from './my-projects.component'; @@ -32,6 +34,8 @@ describe('MyProjectsComponent', () => { isMediumSubject = new BehaviorSubject(false); queryParamsSubject = new BehaviorSubject>({}); + queryParamsSubject.next({}); + (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { if ( selector === MyResourcesSelectors.getTotalProjects || @@ -60,9 +64,11 @@ describe('MyProjectsComponent', () => { providers: [ MockProvider(Store, MOCK_STORE), MockProvider(DialogService, { open: jest.fn() }), + MockProvider(ConfirmationService, { confirm: jest.fn() }), MockProvider(ActivatedRoute, { queryParams: queryParamsSubject.asObservable() }), MockProvider(Router, { navigate: jest.fn() }), MockProvider(IS_MEDIUM, isMediumSubject), + MockProvider(ProjectRedirectDialogService, { showProjectRedirectDialog: jest.fn() }), ], }).compileComponents(); @@ -73,6 +79,8 @@ describe('MyProjectsComponent', () => { store.dispatch.mockReturnValue(of(undefined)); + (component as any).queryParams = () => ({}); + fixture.detectChanges(); }); @@ -131,8 +139,8 @@ describe('MyProjectsComponent', () => { component.handleSearch('query'); expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: expect.anything(), - queryParams: { page: '1', size: '25', search: 'query', sortColumn: 'name', sortOrder: 'desc' }, + relativeTo: TestBed.inject(ActivatedRoute), + queryParams: { page: '1', search: 'query' }, }); }); @@ -143,8 +151,8 @@ describe('MyProjectsComponent', () => { component.onPageChange({ first: 30, rows: 15 } as any); expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: expect.anything(), - queryParams: { page: '3', size: '15', sortColumn: 'title', sortOrder: 'asc' }, + relativeTo: TestBed.inject(ActivatedRoute), + queryParams: { page: '3', size: '15' }, }); }); @@ -154,7 +162,7 @@ describe('MyProjectsComponent', () => { component.onSort({ field: 'updated', order: SortOrder.Desc } as any); expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: expect.anything(), + relativeTo: TestBed.inject(ActivatedRoute), queryParams: { sortColumn: 'updated', sortOrder: 'desc' }, }); }); @@ -166,26 +174,13 @@ describe('MyProjectsComponent', () => { component.onTabChange(1); expect(router.navigate).toHaveBeenCalledWith([], { - relativeTo: expect.anything(), - queryParams: { page: '1', size: '50' }, + relativeTo: TestBed.inject(ActivatedRoute), + queryParams: { page: '1', size: undefined }, }); expect(store.dispatch).toHaveBeenCalled(); }); - it('should open create project dialog with responsive width', () => { - const openSpy = jest.spyOn(component.dialogService, 'open'); - - isMediumSubject.next(false); - component.createProject(); - expect(openSpy).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ width: '95vw' })); - - openSpy.mockClear(); - isMediumSubject.next(true); - component.createProject(); - expect(openSpy).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ width: '850px' })); - }); - it('should navigate to project and set active project', () => { const project = { id: 'p1' } as any; component.navigateToProject(project);